Greasy Fork

Greasy Fork is available in English.

F**k 三观

try to take over the world!

当前为 2018-07-23 提交的版本,查看 最新版本

// ==UserScript==
// @name         F**k 三观
// @namespace    http://tampermonkey.net/
// @version      0.13
// @description  try to take over the world!
// @author       yetone
// @match        https://*.douban.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_log
// ==/UserScript==

(function() {
    'use strict';

    let $style = document.createElement('style');
    $style.innerText = `
.my-hl {
-webkit-animation: highlight 1.6s ease-out;
animation: highlight 1.6s ease-out
}
@-webkit-keyframes highlight {
0% {
background: #ebebeb
}

html[data-theme=dark] 0% {
background: #444
}

to {
background: transparent none repeat 0 0/auto auto padding-box border-box scroll;
background: initial
}
}

@keyframes highlight {
0% {
background: #ebebeb
}

html[data-theme=dark] 0% {
background: #444
}

to {
background: transparent none repeat 0 0/auto auto padding-box border-box scroll;
background: initial
}
}`;
    document.head.appendChild($style);
    const iptTypes = {
        Array: {
            type: 'largetext',
            processor: arr => arr.join(','),
            extractor: $node => $node.value.split(',').filter(x => !!x),
        },
        String: {
            type: 'text',
        },
        Boolean: {
            type: 'checkbox',
            extractor: $node => $node.checked,
        },
        Number: {
            type: 'number',
        },
    };
    const settingDef = {
        kws: {
            type: 'Array',
            label: '关键词(懒得做好看了,英文逗号隔开,别怪我没提醒你)',
            iptStyle: 'width: 300px; height: 60px; padding: 8px;',
            default: ['三观'],
            getProcessor: kws => kws.filter(x => !!x),
            setProcessor: kws => kws.filter(x => !!x),
        },
        forward: {
            type: 'Boolean',
            label: '包括转发的原内容?',
            default: true,
        },
        blockThreshold: {
            type: 'Number',
            label: '提示拉黑的阈值',
            default: 3,
            getProcessor: x => x | 0,
            setProcessor: x => x | 0,
        },
        textSegment: {
            type: 'Boolean',
            label: '是否开启分词(会慢,且第一次开启后会跳到是否允许跨域的页面,点击 always allow 就好了,嘻嘻嘻嘻嘻嘻)',
            default: false,
        },
    };
    const setting = makeSetting();
    if (location.pathname.match(/^\/$|^\/people\/[^\/]+\/status\/\d+[\/]$/) !== null) {
        renderSettingArea();
    }
    process();

    function makeSetting() {
        let props = Object.keys(settingDef).reduce((p, c) => {
            let key = `fk-setting-${c}`;
            let { type, default: dft, getProcessor = x => x, setProcessor = x => x } = settingDef[c];
            type = type === void 0 ? getType(dft) : type;
            return {
                ...p,
                [c]: {
                    get() {
                        let v = GM_getValue(key);
                        v = getType(v) === type ? v : dft;
                        return getProcessor(v);
                    },
                    set(v) {
                        v = setProcessor(v);
                        GM_setValue(key, v);
                    }
                }
            };
        }, {});
        return Object.defineProperties({}, props);
    }

    function renderSettingFields() {
        return Object.keys(settingDef).map(k => {
            let { type = 'String', label = k, iptStyle = '' } = settingDef[k];
            let { type: iptType = 'text', processor = x => x } = iptTypes[type];
            let name = `fk-${k}`;
            let value = setting[k];
            return `<p><label for="${name}" style="cursor: pointer;">${label}:</label>${(() => {
                if (iptType === 'largetext') {
                    return `<textarea id=${name} name="${name}" style="${iptStyle}">${processor(value)}</textarea>`;
                } else {
                    return `<input type="${iptType}" style="${iptStyle}" id="${name}" name="${name}" value="${processor(value)}" ${iptType === 'checkbox' ? (value ? 'checked' : '') : ''}/>`;

                }
            })()}</p>`;
        }).join('');
    }

    function setSetting($form) {
        Object.keys(settingDef).forEach(k => {
            let { type = 'String', label = k } = settingDef[k];
            let { extractor = $node => $node.value } = iptTypes[type];
            let $ipt = $form.querySelector(`[name=fk-${k}]`);
            let value = extractor($ipt);
            setting[k] = value;
        });
    }

    function renderSettingArea() {
        let $w = document.querySelector('.aside');
        if ($w === null) {
            return;
        }
        let $div = document.createElement('div');
        $div.style.padding = '8px 0';
        let $a = document.createElement('a');
        $div.append($a);
        $a.innerText = 'F**k 设置';
        $a.href = 'javascript:;';
        $a.style.color = '#ccc';
        $a.addEventListener('click', function() {
            let $area = $div.querySelector('.fk-setting');
            if ($area !== null) {
                $area.remove();
                return;
            }
            $area = document.createElement('div');
            $area.classList.add('fk-setting');
            $area.style.padding = '8px 0';
            let $form = document.createElement('form');
            $form.innerHTML = `${renderSettingFields()}<p><input type="submit" value="保存"/></p>`;
            $area.appendChild($form);
            $form.addEventListener('submit', function(e) {
                e.preventDefault();
                setSetting($form);
                alert('保存成功!');
            }, true);
            $div.append($area);
        }, true);
        $w.prepend($div);
    }

    function getType(obj) {
        return Object.prototype.toString.call(obj).slice(8, -1);
    }

    function fetchStatus(sid) {
        let url = `https://www.douban.com/doubanapp/dispatch?uri=/status/${sid}/`;
        return new Promise((resolve, reject) => {
            $.ajax_withck({url, success: r => {
                let $n = $(r).find('.status-wrapper');
                $n.find('.actions').remove();
                let $pubtime = $n.find('.pubtime')
                let pubtime = $pubtime.html();
                $pubtime.html(`<a target="_blank" href="${url}">${pubtime}</a>`);
                resolve($n[0]);
            }, error: () => resolve(sid)});
        });
    }

    function fetchWords(text) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                data: `data=${encodeURIComponent(text)}`,
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                },
                url: document.baseURI.split('://')[0] + '://bosonnlp.com/analysis/tag',
                onload: function(res) {
                    resolve(JSON.parse(res.responseText)[0].word);
                }
            });
        });
    }

    function renderStatuses(sids) {
        if (sids.length === 0) {
            return '没有广播';
        }
        return Promise.all(sids.map(fetchStatus)).then(res => {
            let missing = [];
            let lines = [];
            for (let r of res) {
                if (r instanceof String) {
                    missing.push(r);
                    lines.push(`<p>${r} 已经不存在了</p>`);
                    continue;
                }
                lines.push(r.outerHTML);
            }
            return [lines.join('<div style="border-top: 1px solid #e5e5e5;margin-bottom: 20px;"></div>'), missing];
        });
    }

    function process() {
        let $statuses = document.querySelectorAll('.status-wrapper, .item-status');
        $statuses.forEach(async $x => {
                          if ($x.dataset.fk) {
            return;
        }
        let kws = await search($x);
        $x.dataset.fk = true;
        if (kws.size === 0) {
            return;
        }
        let info = getStatusInfo($x);
        let $div = document.createElement('div');
        $div.style.color = '#bfbfbf';
        $div.style.fontSize = '12px';
        $div.style.textAlign = 'center';
        $div.style.borderBottom = '1px solid #e5e5e5';
        $div.style.padding = '8px 0';
        let $tip = document.createElement('div');
        $tip.innerText = `${info.user.name}的广播包含你设置的${Array.from(kws).map(x => `「${x}」`).join('、')},已折叠`;
        $tip.style.cursor = 'pointer';
        $tip.style.display = 'inline-block';
        $div.addEventListener('click', function() {
            $div.style.display = 'none';
            $x.style.display = 'block';
            $x.classList.add('my-hl');
        }, false);
        $div.appendChild($tip);
        let key = `imangry-sids-${info.user.id}`;
        let sidsMap = GM_getValue(key) || {};
        let sids = []
        kws.forEach(kw => {
            let _sids = sidsMap[kw] || [];
            if (_sids.indexOf(info.id) === -1) {
                _sids.push(info.id);
                sidsMap[kw] = _sids;
            }
            _sids.forEach(sid => {
                if (sids.indexOf(sid) === -1) {
                    sids.push(sid);
                }
            });
        });
        GM_setValue(key, sidsMap);
        if (sids.length >= setting.blockThreshold) {
            let $dd = document.createElement('div');
            $dd.style.display = 'inline-block';
            $dd.style.marginLeft = '8px';
            let $span = document.createElement('span');
            $span.innerText = `已发布${sids.length}次`;
            $span.style.color = '#888';
            $span.style.cursor = 'pointer';
            $span.addEventListener('click', function(e) {
                e.stopPropagation();
                let old = $span.innerText;
                $span.innerText = '加载中...';
                renderStatuses(Array.from(sids)).then(([ html, missing ]) => {
                    show_dialog(`<div id="fk-dialog" style="position: relative">
<a href="javascript: close_dialog()" style="position: absolute; top: 10px; right: 12px; padding: 0 3px;">X</a>
<div class="fk-dialog-hd" style="padding: 10px 10px 6px; color: #666; background: #ebf5eb; border-radius: 4px 4px 0 0;">命中${Array.from(kws).map(x => `「${x}」`).join('、')}的广播们</div>
<div class="fk-dialog-bd" style="padding: 10px 10px 6px; max-height: 600px; overflow-y: scroll;">${html}</div>
</div>`, 760);
                    search(document.querySelector('#fk-dialog .fk-dialog-bd'));
                    $span.innerText = old;
                });
            });
            $dd.appendChild($span);
            let $btn = document.createElement('a');
            $btn.href = 'javasript:;';
            $btn.innerText = '拉黑';
            $btn.style.marginLeft = '8px';
            $btn.addEventListener('click', function(e) {
                e.stopPropagation();
                if (!confirm(`确定拉黑${info.user.name}?`)) {
                    return;
                }
                $.postJSON_withck('https://www.douban.com/j/contact/addtoblacklist', {
                    people: info.user.id
                }, function() {
                    alert(`已拉黑${info.user.name}`);
                });
            }, false);
            $dd.appendChild($btn);
            $div.appendChild($dd);
        }
        insertAfter($div, $x);
        $x.style.display = 'none';
    });
}

 function getUserInfo($node) {
    let $pic = $node.querySelector('.usr-pic img') || $node.querySelector('img.avatar');
    let match = $pic === null ? null : $pic.src.match(/(\d+)-\d+\.(jpg|jpeg|png|gif)/);
    let id = match !== null ? match[1] : $node.querySelector('.status-item').dataset.uid;
    let $lnk = $node.querySelector('.lnk-people') || $node.querySelector('a.author');
    let name = $lnk === null ? 'unknow' : $lnk.innerText;
    return {id, name};
}
function getStatusInfo($node) {
    let id = $node.dataset.sid;
    let $text = $node.querySelector('.status-saying') || $node.querySelector('.status-preview');
    let text = $text === null ? '' : $text.innerText;
    let user = getUserInfo($node);
    return { id, text, user };
}
function insertAfter($new, $target) {
    let $p = $target.parentNode;

    if($p.lastChild === $target) {
        $p.appendChild($new);
    } else {
        $p.insertBefore($new, $target.nextSibling);
    }
}
async function search($parent) {
    let nodes = [];
    let need = false;
    let kws = new Set();

    if (setting.kws.length === 0) {
        console.log('您没有设置关键词!!!');
        return kws;
    }
    let pattern = new RegExp(setting.kws.join('|'), 'g');
    for (let $node of $parent.childNodes) {
        const nodeName = $node.nodeName;
        if (nodeName !== '#text') {
            nodes.push($node);
            if (nodeName === 'DIV'){
                if (!setting.forward && $node.classList.contains('status-real-wrapper')) {
                    continue;
                }
                if ($node.classList.contains('actions') && $node.parentNode.classList.contains('bd')) {
                    continue;
                }
            } else if (nodeName === 'SPAN') {
                if ($node.classList.contains('reshared_by')) {
                    continue;
                }
            } else if (nodeName === 'FORM') {
                continue;
            }
            for (let kw of await search($node)) {
                kws.add(kw);
            }
            continue;
        }
        let text = $node.textContent;
        if (setting.textSegment && text.match(pattern) !== null) {
            let words = await fetchWords(text);
            GM_log(words);
            let t = '';
            words.forEach(w => {
                if (setting.kws.indexOf(w) >= 0) {
                    need = true;
                    if (t !== '') {
                        nodes.push(new Text(t));
                        t = '';
                    }
                    let $b = document.createElement('b');
                    $b.style.background = '#ffb56e';
                    $b.style.fontWeight = 'normal';
                    $b.innerText = w;
                    nodes.push($b);
                    kws.add(w);
                    return;
                }
                t += w;
            });

            if (t !== '') {
                nodes.push(new Text(t));
            }

            continue;
        }
        let lastIdx = 0;
        text.replace(pattern, (c, idx, t) => {
            need = true;
            kws.add(c);
            nodes.push(new Text(t.substring(lastIdx, idx)));
            lastIdx = idx + c.length;
            let $b = document.createElement('b');
            $b.style.background = '#ffb56e';
            $b.style.fontWeight = 'normal';
            $b.innerText = c;
            nodes.push($b);
        });
        nodes.push(new Text(text.substring(lastIdx)));
    }
    if (need) {
        while ($parent.childNodes.length > 0) {
            $parent.childNodes.forEach($x => {
                $parent.removeChild($x);
            });
        }
        nodes.forEach($x => {
            $parent.appendChild($x);
        });
    }
    return kws;
}
})();