Greasy Fork is available in English.

Twitter 推特推文(X帖子) 屏蔽器&过滤器[月潆]

以皎洁高雅的月光洗礼信息溪流: 自定义屏蔽不想看到推文(黄推等),支持自定义;自定义推特图标等

// ==UserScript==
// @name		Twitter Tweets(X Posts) Blocker & Filter
// @name:en		Twitter Tweets(X Posts) Blocker & Filter[Moonstream]
// @name:zh		Twitter 推特推文(X帖子) 屏蔽器&过滤器[月潆]
// @name:ja		Twitter ツイッターのツイート(Xの投稿)ブロック&フィルター[月潆(つきろう)]
// @namespace		http://github.com/yuhanawa/UserScript
// @name:zh-CN		Twitter 推特推文(X帖子) 屏蔽器&过滤器[月潆]
// @name:zh-TW		Twitter 推特推文(X帖子) 屏蔽器&过滤器[月潆]
// @description		The elegant moonlight bathes the stream of information, cleansing it: Customize filters  unwanted tweets (adult content etc). Support custom and importing rules.
// @description:en		The elegant moonlight bathes the stream of information, cleansing it: Customize filters  unwanted tweets (adult content etc). Support custom and importing rules.
// @description:zh		以皎洁高雅的月光洗礼信息溪流: 自定义屏蔽不想看到推文(黄推等),支持自定义;自定义推特图标等
// @description:ja		月光が情報の流れに洗礼を与え、浄化する: 迷惑なツイート(アダルト)をブロックまたは非表示にするために、フィルターをカスタマイズ可能
// @description:zh-CN		以皎洁高雅的月光洗礼信息溪流: 自定义屏蔽不想看到推文(黄推等),支持自定义;自定义推特图标等
// @description:zh-TW		以皎洁高雅的月光洗礼信息溪流: 自定义屏蔽不想看到推文(黄推等),支持自定义;自定义推特图标等
// @grant		GM_setValue
// @grant		GM_getValue
// @grant		GM_addStyle
// @grant		GM_registerMenuCommand
// @grant		GM_xmlhttpRequest
// @grant		GM_openInTab
// @grant		unsafeWindow
// @match		*://twitter.com/*
// @match		*://x.com/*
// @match		*://yuhan-script-config.netlify.app/*
// @match		*://user-script-config-form.vercel.app/*
// @match		*://yuhanawa.github.io/tools/userscriptconfig/*
// @version		0.1.15
// @author		Yuhanawa
// @license		GPL-3.0
// @icon		none
// @run-at		document-start
// @connect		yuhan-script-config.netlify.app
// @connect		*
// ==/UserScript==

/* 
    twitter v.0.1.15 by Yuhanawa
    Source: https://github.com/Yuhanawa/UserScript
*/

isLoaded=!1,onload(()=>isLoaded=!0);const __props__=new Map;
function get(k,d){return GM_getValue(k,void 0===d?__props__.get(k):d)}
function set(k,v){return GM_setValue(k,v)}
function cfg(k,v){return void 0===v?get(k):set(k,v)}
function getOptionKeyAndName(optionStr){var key=optionStr.match(/\$([^ ]+)/)?.[0];return key?{key:key.replace("$",""),name:optionStr.replace(key,"")}:{key:optionStr,name:optionStr}}
function style(css){var node;"undefined"!=typeof GM_addStyle?GM_addStyle(css):((node=document.createElement("style")).appendChild(document.createTextNode(css)),document.body.appendChild(node))}
function addMenu(name,key,options,current,index,onclick){const getOptionKey=o=>getOptionKeyAndName(o).key;void 0!==current&&null!=index&&-1!==index||(current=set(key,getOptionKey(options[0])),index=0);var o=`${name}:${o=options[index],getOptionKeyAndName(o).name}[${index+1}/${options.length}]<点击切换`;return GM_registerMenuCommand(o,()=>{if(set(key,getOptionKey(options[index+1>=options.length?0:index+1])),onclick)try{onclick()}catch(e){console.log(`发生错误(${name}-${current}-onclick): `+e)}location.reload()}),index}
function onload(f){isLoaded?f():document.addEventListener("DOMContentLoaded",()=>f())}
function timeoutAfterLoad(f,t){onload(()=>setTimeout(()=>f(),t))}
function intervalAfterLoad(f,t,runOnFirst){onload(()=>{runOnFirst&&f(),setInterval(f,t)})}
function run(fts){void 0===fts&&(fts=features);for(const key of Object.keys(fts))try{const feature=fts[key];("boolean"==typeof feature.match&&1==feature.match||0!==feature.match.filter(m=>"string"==typeof m?null!==window.location.href.match(m):m.test(window.location.href)).length)&&addFeature(key,feature)}catch(error){console.error("发生了一个意料之外的错误, 这可能是因为非法的feature所造成的, 不过请放心, 脚本将继续运行而不会崩溃. ",feature,error)}}
function addFeature(key,feature){var{name,values}=feature;if(!feature.switchable||get(key+"_switch",feature.default_switch_state??!0))if("$"===name||feature.directlyRun)try{"function"==typeof values?"string"==typeof(result=values(feature))&&style(result):"string"==typeof values&&style(values)}catch(e){console.error(e)}else{var result=Object.keys(values),key0=getOptionKeyAndName(result[0]).key;let current=get(key,key0),index=result.findIndex(x=>getOptionKeyAndName(x).key===current);-1!==index&&void 0!==index||(set(key,key0),index=0,current=key0),feature.hideInMenu||addMenu(name,key,result,current,index);try{var value=values[result[index]];if(null!=value)if("function"==typeof value){const result=value(feature);"string"==typeof result&&style(result)}else"string"==typeof value&&style(value)}catch(e){console.error(e)}}}
function findFastestSite(sites){return new Promise((resolve,reject)=>{let fastestSite=null,fastestTime=1/0,completedRequests=0;sites.forEach(function(site){const xhr=new XMLHttpRequest,startTime=(new Date).getTime();xhr.onreadystatechange=()=>{var timeElapsed;fastestTime<100&&(xhr.abort(),resolve(fastestSite)),xhr.readyState===XMLHttpRequest.DONE&&(timeElapsed=(new Date).getTime()-startTime,console.log(`Ping ${site} took ${timeElapsed}ms`),console.log("Status: "+xhr.status),xhr.status<400&&timeElapsed<fastestTime&&(fastestTime=timeElapsed,fastestSite=site),++completedRequests===sites.length)&&resolve(fastestSite)},xhr.onprogress=()=>{fastestTime<100&&(xhr.abort(),resolve(fastestSite))},xhr.onload=()=>{console.log("Pinging "+site)},xhr.open("GET",site,!0),xhr.timeout=2e3,xhr.send()})})}
function getConfigPage(){return findFastestSite(["https://user-script-config-form.vercel.app","https://yuhan-script-config.netlify.app","https://yuhanawa.github.io/tools/userscriptconfig/"]).then(fastestSite=>fastestSite).catch(error=>(console.error("Error:",error),null))}
function showConfigPage(){document.querySelector("#config-page-awa")?document.querySelector("#config-page-awa").style.display="block":getConfigPage().then(fastestSite=>{void 0!==GM_openInTab?GM_openInTab(fastestSite,{active:!0}):location.href=fastestSite})}
function LoadConfigPage(name){if(!document.querySelector("#config-page-awa"))return style(`
    .config-page-awa {
        position: fixed;
        background-color: rgba(245, 200, 200, 0.2);
        z-index: 9999;
        top: 0;
        left: 0;
        width: 100vw;
        height: 100vh;
        display: block;
        justify-content: center;
        align-items: center;
        flex-direction: column;
        backdrop-filter: blur(20px);
      }
      .config-page-container {
        width: 60%;
        height: 60%;
        position: absolute;
        top: 15%;
        left: 15%;
        cursor: auto;
        border: 1px thin #cccccc10;
        border-radius: 20px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.65);
        background-color: rgba(255, 255, 255, 0.6);
        overflow: hidden;
        padding: 15px;
        box-sizing: border-box;
        overflow-y: hidden;
        min-width: 360px;
        min-height: 420px;
        resize: both;
      }
      .config-page-drag-area {
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        cursor: move;
        background-color: transparent;
      }
      .config-page-iframe {
        border: 0;
        border-radius: 18px;
        overflow: hidden;
        box-sizing: border-box;
        overflow-y: auto;
        opacity: 0.95;
        width: 100%;
        height: 100%;
        box-shadow: 1px 1px 4px rgba(185, 185, 185, 0.2);
        background-color: rgba(255, 255, 255, 0.25);
        margin: -1px;
      }
      .config-page-close-btn {
        position: absolute;
        top: 4px;
        right: 5px;
        font-size: 20px;
        background-color: transparent;
        border: 0;
        color: #C00;
        cursor: pointer;
        outline: none;
        padding: 0;
        margin: 0;
      }
      .config-page-close-btn:hover {
        color: #A00;
      }
      .config-page-close-btn:active {
        color: #f00;
        transform: scale(0.8);
        transition: 0.15s;
      }
      
    `),getConfigPage().then(fastestSite=>{document.body.insertAdjacentHTML("afterend",`
        <div class="config-page-awa" id="config-page-awa" style="display: none;">
        <div class="config-page-container">
            <div class="config-page-drag-area"></div>
            <iframe class="config-page-iframe"
                src="${fastestSite}?menuKey=${name}&iniframe"></iframe>
            <button class="config-page-close-btn">⭕</button>
        </div>
    </div>`);const configPage=document.querySelector("#config-page-awa"),container=configPage.querySelector(".config-page-container"),iframe=configPage.querySelector(".config-page-iframe");var fastestSite=configPage.querySelector(".config-page-drag-area"),pos1=0,pos2=0,pos3=0,pos4=0;
function elementDrag(e){(e=e||window.event).preventDefault(),pos1=pos3-e.clientX,pos2=pos4-e.clientY,pos3=e.clientX,pos4=e.clientY,container.style.top=container.offsetTop-pos2+"px",container.style.left=container.offsetLeft-pos1+"px"}
function closeDragElement(){iframe.style.pointerEvents="auto",configPage.onmouseup=null,configPage.onmousemove=null}return fastestSite.onmousedown=function(e){(e=e||window.event).preventDefault(),pos3=e.clientX,pos4=e.clientY,iframe.style.pointerEvents="none",configPage.onmouseup=closeDragElement,configPage.onmousemove=elementDrag;e=window.getComputedStyle(event.target).cursor;console.log("当前鼠标样式:"+e)},configPage.querySelector(".config-page-close-btn").onclick=function(){configPage.style.display="none"},Promise.resolve()});showConfigPage()}
function loadConfig(name,properties){GM_registerMenuCommand("在新窗口打开设置中心",()=>{showConfigPage()}),GM_registerMenuCommand("在页面内镶嵌设置中心(BETA)",()=>{LoadConfigPage(name).then(()=>showConfigPage())}),anchors=[];for(const key of Object.keys(properties))__props__.set(name+"_"+key,properties[key].default),key.startsWith("#")&&anchors.push({key:key,href:properties[key].href||key,title:properties[key].title||properties[key].description||key});(location.href.match("yuhan-script-config.netlify.app")||location.href.match("user-script-config-form.vercel.app")||location.href.match("yuhanawa.github.io/tools/userscriptconfig")||location.href.match("localhost"))&&(void 0===unsafeWindow.awa&&(unsafeWindow.awa={}),void 0===unsafeWindow.awa.userscript&&(unsafeWindow.awa.userscript={}),unsafeWindow.awa.userscript[name]={props:properties,anchors:anchors,get:get,set:set})}

String.prototype.hashCode = function () {
    var hash = 0, i, chr;
    if (this.length === 0) return hash;
    for (i = 0; i < this.length; i++) {
        chr = this.charCodeAt(i);
        hash = ((hash << 5) - hash) + chr;
        hash |= 0; // Convert to 32bit integer
    }
    return hash;
};


function showToast(message) {
    const toast = document.createElement('div');
    toast.classList.add('toast');
    toast.textContent = message;

    setTimeout(() => {
        toast.classList.add('toast-active');
        setTimeout(() => {
            toast.classList.add('hide');
            setTimeout(() => {
                toast.remove();
            }, 3500)
        }, 3500)
    }, 500)

    document.body.appendChild(toast);
}


function getCookie(cname) {
    const name = cname + '='
    const ca = document.cookie.split(';')
    for (let i = 0; i < ca.length; ++i) {
        const c = ca[i].trim()
        if (c.indexOf(name) === 0) {
            return c.substring(name.length, c.length)
        }
    }
    return ''
}

function blockUserByScreenName(screen_name) {
    const xhr = new XMLHttpRequest();

    // Open request
    xhr.open('POST', 'https://api.twitter.com/1.1/blocks/create.json');
    xhr.withCredentials = true

    // Set request headers
    xhr.setRequestHeader('Authorization', 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA');
    xhr.setRequestHeader('X-Twitter-Auth-Type', 'OAuth2Session');
    xhr.setRequestHeader('X-Twitter-Active-User', 'yes');
    xhr.setRequestHeader('X-Csrf-Token', getCookie('ct0'));
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

    // Send request
    xhr.send(`screen_name=${screen_name}`);
    console.log(`已为您自动屏蔽用户 ${screen_name}`);
    showToast(`已为您自动屏蔽用户 ${screen_name}`);
}
async function blockUserById(id, display) {
    const xhr = new XMLHttpRequest();

    // Open request
    xhr.open('POST', 'https://api.twitter.com/1.1/blocks/create.json');
    xhr.withCredentials = true

    // Set request headers
    xhr.setRequestHeader('Authorization', 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA');
    xhr.setRequestHeader('X-Twitter-Auth-Type', 'OAuth2Session');
    xhr.setRequestHeader('X-Twitter-Active-User', 'yes');
    xhr.setRequestHeader('X-Csrf-Token', getCookie('ct0'));
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

    // Send request
    xhr.send(`user_id=${id}`);
    console.log(`已为您自动屏蔽用户id ${id}, 用户名:${display ?? "未知"}`);
    showToast(`已为您自动屏蔽用户id ${id} ${display ? `用户名:${display}` : '(通过id精准匹配)'}`);
}

function check(rule, screen_name, key, target, notShowNote) {
    if (!target) return false;

    if (rule[key]?.some(i => target?.includes(i))) {
        blackList.set(screen_name, {
            // id: id,
            screen_name: screen_name,
            rule: rule['rule-name'],
            type: key,
            notShowNote: notShowNote
        })
    } else if (rule[key + "-reg"]?.some(i => i.test(target ?? ''))) {
        blackList.set(screen_name, {
            // id: id,
            screen_name: screen_name,
            rule: rule['rule-name'],
            type: key + "-reg",
            notShowNote: notShowNote
        })
    } else return false

    return true

}

unsafeWindow.addEventListener('load', function () {
    if (!location.host.includes('x.com') && !location.host.includes('twitter.com')) return

    var originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (method, url) {

        if (url.startsWith('https://twitter.com/i/api/2/notifications/all.json') && false) {
            this.addEventListener('load', function () {
                // console.log('拦截到请求:', method, url);
                // console.log('响应内容:', this.responseText);

                if (this.responseText?.globalObjects?.users) {
                    const users = this.responseText.globalObjects.users
                    for (var user of users) {
                        var id = user.id_str
                        var name = user.name
                        var screen_name = user.screen_name
                        var location = user.location
                        var description = user.description
                        var created_at = user.created_at
                        var followers_count = user.followers_count
                        var friends_count = user.friends_count
                        var following = user.following
                        var url = user.url

                        if (whiteList.has(screen_name) || blackList.has(screen_name)) return;

                        if (following) {
                            whiteList.add(screen_name)
                            continue
                        }

                        var auto_block = get('twitter_auto_block', 'on') === 'on';
                        var auto_block_by_more = auto_block && get('twitter_auto_block_by_more', 'off') === 'on';
                        if (check(internalRule, screen_name, 'name', name, auto_block) || check(internalRule, screen_name, 'bio', description, auto_block)
                            || check(internalRule, screen_name, 'location', location, auto_block) || check(internalRule, screen_name, 'url', url, auto_block)) {
                            if (auto_block) {
                                blockUserById(id, screen_name)
                                showToast(`[Beta] 用户${name}@${screen_name}由内置规则自动屏蔽`)
                            }
                            continue;
                        }

                        for (const rule of rules) {
                            if (rule["id_num"]?.some(i => id === i)) {
                                blackList.set(screen_name, {
                                    id: id,
                                    screen_name: screen_name,
                                    rule: rule['rule-name'],
                                    type: 'id-num',
                                })
                            } else if (rule["id"]?.some(i => screen_name === i)) {
                                blackList.set(screen_name, {
                                    id: id,
                                    screen_name: screen_name,
                                    rule: rule['rule-name'],
                                    type: 'id',
                                })
                            } else if (rule["id-reg"]?.some(i => i.test(screen_name ?? ''))) {
                                blackList.set(screen_name, {
                                    id: id,
                                    screen_name: screen_name,
                                    rule: rule['rule-name'],
                                    type: 'id-reg',
                                })
                            } else if (check(rule, screen_name, 'name', name, auto_block_by_more) || check(rule, screen_name, 'bio', description, auto_block_by_more) || check(rule, screen_name, 'location', location, auto_block_by_more) || check(rule, screen_name, 'url', url, auto_block_by_more)) {
                                if (auto_block_by_more) blockUserById(id, screen_name)
                            } else continue
                            break
                        }
                    }
                }
            })
        } else if (url.startsWith('https://twitter.com/i/api/graphql')) {
            // console.log("拦截到请求:", method, url);

            this.addEventListener('load', function () {
                // console.log("响应内容:", this.responseText);

                // 处理 result
                const handledResult = (result) => {
                    // console.log(result);


                    let legacy = result.legacy

                    let id = result.rest_id
                    let name = legacy.name
                    let created_at = legacy.created_at
                    let description = legacy.description
                    let followers_count = legacy.followers_count
                    let location = legacy.location
                    let screen_name = legacy.screen_name
                    let following = legacy.following ?? false
                    let blocking = legacy.blocking ?? false
                    var url = legacy.entities?.url?.urls[0]?.display_url ?? ''

                    if (following) {
                        whiteList.add(screen_name)
                        return
                    }
                    if (blocking) {
                        blackList.set(screen_name, {
                            id: id,
                            screen_name: screen_name,
                            rule: 'blocking',
                            type: 'blocking',
                            notShowNote: true
                        })
                        return
                    }

                    var auto_block = get('twitter_auto_block', 'on') === 'on';
                    var auto_block_by_more = auto_block && get('twitter_auto_block_by_more', 'off') === 'on';
                    if (check(internalRule, screen_name, 'name', name, auto_block) || check(internalRule, screen_name, 'bio', description, auto_block) || check(internalRule, screen_name, 'location', location, auto_block) || check(internalRule, screen_name, 'url', url, auto_block)) {
                        if (auto_block) {
                            blockUserById(id, screen_name)
                            showToast(`[Beta] 用户${name}@${screen_name}由内置规则自动屏蔽`)
                        }
                        return;
                    }

                    for (const rule of rules) {
                        if (rule["id_num"]?.some(i => id === i)) {
                            blackList.set(screen_name, {
                                id: id,
                                screen_name: screen_name,
                                rule: rule['rule-name'],
                                type: 'id-num',
                            })
                            if (auto_block) blockUserById(id, screen_name)
                        } else if (rule["id"]?.some(i => screen_name === i)) {
                            blackList.set(screen_name, {
                                id: id,
                                screen_name: screen_name,
                                rule: rule['rule-name'],
                                type: 'id',
                            })
                            if (auto_block) blockUserById(id, screen_name)
                        } else if (rule["id-reg"]?.some(i => i.test(screen_name ?? ''))) {
                            blackList.set(screen_name, {
                                id: id,
                                screen_name: screen_name,
                                rule: rule['rule-name'],
                                type: 'id-reg',
                            })
                        } else if (check(rule, screen_name, 'name', name, auto_block_by_more) || check(rule, screen_name, 'bio', description, auto_block_by_more) || check(rule, screen_name, 'location', location, auto_block_by_more) || check(rule, screen_name, 'url', url, auto_block_by_more)) {
                            if (auto_block_by_more) blockUserById(id, screen_name)
                        } else continue
                    }
                }


                if (this.responseText.startsWith('{"data":{"user":{"result"')) {
                    // 用户页 
                    handledResult(JSON.parse(this.responseText).data.user.result)
                } else if (this.responseText.includes('threaded_conversation_with_injections_v2')) {
                    // 推文页
                    let instructions = JSON.parse(this.responseText).data.threaded_conversation_with_injections_v2.instructions;

                    for (entry of instructions.filter(i => i.entries).map(i => i.entries)) {
                        for (content of entry.filter(i => i.content).map(i => i.content)) {
                            let items = [];
                            if (content.itemContent != undefined) {
                                items = [content?.itemContent?.tweet_results?.result?.tweet?.core ?? content?.itemContent?.tweet_results?.result?.core]
                            } else if (content.items != undefined) {
                                items = content.items.filter(i => i.item?.itemContent?.tweet_results?.result?.core).map(i => i.item.itemContent.tweet_results.result.core)
                            }

                            for (const core of items) {
                                if (core == null || core == undefined) {
                                    continue
                                }

                                handledResult(core.user_results.result)
                            }
                        }
                    }
                } else {
                    // console.log(`content:${this.responseText}`);
                }
            })
        }

        originalOpen.apply(this, arguments);
        // console.log(blackList);
    };
});

const urlListenCallbacks = []
function UrlListener(callback) {
    urlListenCallbacks.push(callback)
}
let old_url = "";
setInterval(() => {
    if (old_url != window.location.href) {
        urlListenCallbacks.forEach(callback => callback(
            {
                old_url: old_url,
                new_url: window.location.href
            }
        ))
        old_url = window.location.href
    }
}, 500)

let internalRuleStr = ""
if (get("twitter_internal_blocker", true)) {
    internalRuleStr = `
#rule-name
内置屏蔽规则
#rule-description
最高优先级
#rule-lastupdate
2077-02-31
#rule-more
null

#name
🔞
反差
私信领福利
同城
可约

#bio
/(?=.*(私))(?=.*(福利))/
/(?=.*(同城))(?=.*(约))/
/(?=.*(寂寞|孤独|刺激|激情|情趣))(?=.*(性|骚扰))/
/(?=.*(年轻|未成年|青少年|\d{2}以下)|未满\d{2})(?=.*(勿扰))/
/(.*(男[Mm]|女[Ss]|反差|调教|人妻|勿扰|(探索|玩法)|(线下|同城|私信|电报|联系)|(sm|SM))){4}/
/(.*(另一面|渴望|冲动|脱|放肆|人妻|宣泄|寂寞|孤独|刺激|激情|情趣|性|骚扰|(电报|私信|联系)|(线下|同城))){5}/
t.me/dwaydgfuya
t.me/OgdenDelia14
t.me/RefMonster3
t.me/cdyu168

#url
t.me/dwaydgfuya
t.me/OgdenDelia14
t.me/RefMonster3
t.me/Anzzmingyue
t.me/Kau587
t.me/MegNaLiSha520
t.me/nDxyS520
t.me/SegWKeC520

#text
/^想上课的私信主人/
/^太阳射不进来的地方/
/^挂空就是舒服,接点地气/
/^总说我下面水太多/
/^在这个炮火连天的夜晚/
/^只进入身体不进入生活/
/^生活太多伪装,只能在推上面卸下伪装/
/^生活枯燥无味,一个人的夜晚总想找个/
/^我每天都有好好的穿衣服.*俘获/
/^人不可能每一步都正确,我不想回头看,也不想批判当时的自己/
/^如果你连试着的胆量也没有,你也就配不上拥有性福/
/^我希望以后可以不用再送我回家,而是我们一起回我们的家/
/^勇敢一点我们在.*就有故事/
/^只要你主动一点点我们就会有机会.*线下/
`
}
const internalRule = parseRule(internalRuleStr)

const rules = new Set();
const whiteList = new Set();
const blackList = new Map();

function parseRule(str) {
    if (!str || str.trim() === '') return;
    let key;
    const rule = {};
    for (line of str.split('\n')) {
        line = line.trim();
        if (!line || line.startsWith('//')) continue;

        if (line.startsWith('#')) {
            key = line.slice(1);
            if (line.startsWith('#rule-')) {
                rule[key] = '';
            } else {
                rule[key] = [];
                rule[key + "-reg"] = [];
            }
        } else {
            if (key.startsWith('rule-'))
                rule[key] += line
            else if (line.startsWith('/') && line.endsWith('/'))
                rule[key + "-reg"].push(new RegExp(line.slice(1, line.length - 1)))
            else
                rule[key].push(line);
        }
    };
    return rule;
}


function loadRule(str) {
    rules.add(parseRule(str));
}


loadConfig('twitter', {".line_bc":{"widget":"line","title":"❗❗❗修改完记得点保存(在最下面)❗❗❗"},"icon":{"title":"自定义图标开关","default":"off","widget":"select","props":{"options":[{"label":"开启","value":"on"},{"label":"关闭","value":"off"}]}},"icon_value":{"title":"自定义图标","default":"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 335 276' fill='%233ba9ee'%3E%3Cpath d='m302 70a195 195 0 0 1 -299 175 142 142 0 0 0 97 -30 70 70 0 0 1 -58 -47 70 70 0 0 0 31 -2 70 70 0 0 1 -57 -66 70 70 0 0 0 28 5 70 70 0 0 1 -18 -90 195 195 0 0 0 141 72 67 67 0 0 1 116 -62 117 117 0 0 0 43 -17 65 65 0 0 1 -31 38 117 117 0 0 0 39 -11 65 65 0 0 1 -32 35'/%3E%3C/svg%3E","extra":"默认是小蓝鸟","hidden":"{{ formData.icon === 'off' }}"},"internal_blocker":{"title":"是否启用内置屏蔽规则(推荐)","default":true,"widget":"switch","type":"boolean"},"show_note":{"title":"是否显示屏蔽提示","default":true,"widget":"switch","type":"boolean"},"block_on_home":{"title":"是否在home页启用脚本","default":"on","widget":"select","props":{"options":[{"label":"开启","value":"on"},{"label":"关闭","value":"off"}]}},"auto_block":{"title":"自动屏蔽被精准匹配的用户","default":"on","widget":"select","props":{"options":[{"label":"开启","value":"on"},{"label":"关闭","value":"off"}]}},"auto_block_by_more":{"title":"自动屏蔽被用户名及BIO匹配的用户(少量误判但效果好)","default":"off","widget":"select","props":{"options":[{"label":"开启","value":"on"},{"label":"关闭","value":"off"}]},"hidden":"{{ formData.auto_block === 'off' }}"},"feed_rule":{"title":"规则订阅","default":"https://yuhan-script-config.netlify.app/xrules/sex.tr\nhttps://yuhan-script-config.netlify.app/xrules/htv2.txt\n","props":{"autoSize":true},"extra":"一行一条","widget":"textArea"},"feed_rule_cache":{"title":"规则查看","default":"","props":{"autoSize":true},"extra":"目前支持预览功能,未来将支持快速添加与删除","widget":"Twitter_rules_viewer"},".feed_rule_recommend":{"title":"订阅推荐","default":"黄推屏蔽v2:https://yuhan-script-config.netlify.app/xrules/htv2.txt | 黄推屏蔽https://yuhan-script-config.netlify.app/xrules/sex.tr \n视频下载机器人屏蔽https://yuhan-script-config.netlify.app/xrules/shipinbot.tr\n反贼屏蔽https://yuhan-script-config.netlify.app/xrules/fz.tr\n粉红屏蔽https://yuhan-script-config.netlify.app/xrules/fh.tr","disabled":true,"props":{"autoSize":true},"extra":"把链接复制到规则订阅中即可","widget":"textArea"},"user_rule":{"title":"自定义规则","default":"// /支持正则/ \n\n#name\n// 用户名关键词\n\n#bio\n// 用户介绍关键词\n\n#text\n// 推文关键词 作用于正文","props":{"autoSize":true},"widget":"twitter_user_rule_editor"}})

let features_twitter_1315146485 = {
	
  twitter_setting: {
    name: "设置按钮",
    match: [/./],
    values: {
      已开启$on: (f) => {
        timeoutAfterLoad(() => {
          UrlListener((i) => {
            f.add(f);
          });
        }, 200);
      },
      已关闭$off: null,
    },
    add: (f) => {
      const footer = document.querySelector(
        "nav.css-1dbjc4n.r-18u37iz.r-1w6e6rj.r-ymttw5",
      );
      if (footer && !footer.hasFilterSetting) {
        const a = document.createElement("a");
        a.href = "https://yuhan-script-config.netlify.app/?menuKey=twitter";
        a.innerText = "⚙️ Filter Setting";
        a.target = "_blank";
        a.style = `margin: 4px;font-size: 16px;color: #4ea1db;opacity: 0.75;`;
        footer.append(a);
        footer.hasFilterSetting = true;
      } else {
        setTimeout(() => {
          f.add(f);
        }, 100);
      }
    },
  },
  twitter_refresh_hash: {
    name: "手动更新规则",
    match: [/./],
    values: {
      点我更新$ready: () => {},
      以开始更新$running: () => {
        set("twitter_feed_rule_hash", 0);
        set("twitter_refresh_hash", "ready");
      },
    },
  },
  twitter_icon: {
    name: "自定义推特图标",
    match: ["twitter.com", "x.com"],
    values: {
      已关闭$off: null,
      已开启$on: () => {
        style(`body{--twitter-icon-value: url("${get("twitter_icon_value")}")`);
        return '@charset "UTF-8";header h1 a[href="/home"]{margin:6px 4px 2px}header h1 a[href="/home"] div{background-image:var(--twitter-icon-value);background-size:contain;background-position:center;background-repeat:no-repeat;margin:4px}header h1 a[href="/home"] div svg{display:none}header h1 a[href="/home"] :hover :after{content:"图标已被修改为自定义图标";font:message-box;position:absolute;left:48px}';
      },
    },
  },
  twitter_filter: {
    name: "屏蔽器总开关",
    match: [/./],
    values: {
      已开启$on: (f) => {
        const filter = () => {
          if (
            !get("twitter_block_on_home", "on") === "on" &&
            (location.href.includes("twitter.com/home") ||
              location.href.includes("x.com/home"))
          )
            return;

          const articles = document.querySelectorAll(
            "article:not([data-filter-checked])",
          );
          for (const article of articles) {
            try {
              article.setAttribute("data-filter-checked", "true");

              const id = article
                .querySelector("div[data-testid='User-Name'] a > div > span")
                ?.innerText.substring(1);

              // console.log(whiteList);
              if (whiteList.has(id)) continue;

              if (!blackList.has(id)) {
                const articleText = article.innerText;
                const retweet = article.querySelector(
                  "span[data-testid='socialContext'] > span >span",
                )?.innerText;
                const text =
                  article.querySelector("div[lang]")?.innerText ?? "";

                if (
                  articleText == "这个帖子来自一个你已屏蔽的账号。\n查看" &&
                  text === ""
                ) {
                  article.style.display = "none";
                  showToast("隐藏了一条来自已屏蔽的账号的推文");
                  continue;
                }

                for (const rule of rules) {
                  try {
                    if (
                      check(rule, id, "name", retweet) ||
                      check(rule, id, "text", text) ||
                      check(rule, id, "all", articleText)
                    ) {
                      break;
                    }
                  } catch {}
                }
              }

              // console.log(whiteList);
              // console.log(blackList);
              if (blackList.has(id)) {
                article.style.display = "none";
                if (
                  get("twitter_show_note", true) &&
                  blackList.get(id).notShowNote !== true &&
                  blackList.get(id).type !== "id" &&
                  blackList.get(id).type !== "id_sum"
                ) {
                  const note = document.createElement("div");
                  note.innerHTML = `<div class="note-tweet">推文已被<a href="" target="_blank">屏蔽器</a>通过⌊${blackList.get(id).rule}⌉(${blackList.get(id).type})规则屏蔽,点击显示推文(你可以通过设置不再显示该提示)</div>`;
                  note.onclick = () => {
                    article.style.display = "block";
                    note.style.display = "none";

                    const blockbtn = document.createElement("a");
                    blockbtn.className = "block_btn";
                    blockbtn.innerText = "屏蔽用户";
                    blockbtn.onclick = () => {
                      blockUserByScreenName(id);
                      article.style.display = "none";
                    
};
                    article
                      .querySelector("div[data-testid=caret]")
                      .parentElement.parentElement.parentElement.insertAdjacentElement(
                        "beforeBegin",
                        blockbtn,
                      );
                  
};
                  article.parentElement.appendChild(note);
                }
                continue;
              }
            } catch (error) {
              // 忽略错误
              // console.error(error);
            }
          }
        
};
        const wait = () => {
          if (!document.querySelector("main")) {
            setTimeout(wait, 200);
            return;
          }

          new MutationObserver(filter).observe(document.querySelector("main"), {
            attributes: false,
            childList: true,
            subtree: true,
          });
        
};
        wait();

        return ".note-tweet{transition:opacity .5s ease-out 0s color .3s ease-out 0s;opacity:.9;padding:2px 16px;border:#eff3f4 1px;cursor:pointer;font-size:13px;color:#636366}.note-tweet a{color:#666680}.note-tweet:hover{opacity:1;background-color:rgba(247,247,247,.9333333333);color:#303023}.note-tweet:hover a{color:#55f}.block_btn,.note-update{color:#00f;text-decoration:underline;cursor:pointer}.note-update{position:fixed;right:20px;bottom:20px;background:#b0c4de;box-shadow:gray 2px 2px 10px 2.2px;font-size:16px;padding:12px;border-radius:8px;border:1px;z-index:114514;opacity:.4}.block_btn{margin:4px}.toast{position:fixed;right:28px;top:24px;background:rgba(49,255,83,.5333333333);color:#1d9bf0;box-shadow:#fff 2px 2px 10px 2.2px;font-size:16px;padding:8px;border-radius:4px;border:1px;z-index:114514;opacity:0;backdrop-filter:blur(4px);transform:translateX(100%);transition:opacity .3s ease-out 0s,transform .3s ease-out 0s}.toast-active,.toast:hover{opacity:1;transform:translateX(0);transition:opacity .4s ease-out 0s,transform .25s ease-out 0s}.toast-active{opacity:.8}.hide{opacity:0}";
      },
      已关闭$off: null,
    },
  },
  twitter_config: {
    name: "$",
    match: ["yuhan-script-config.netlify.app", "twitter.com", "x.com"],
    values: () => {
      timeoutAfterLoad(async () => {
        let callback_num = 0;

        const getText = (url, callback) => {
          callback_num += 1;
          GM_xmlhttpRequest({
            method: "GET",
            url: url,
            headers: {
              "Content-Type": "application/json",
            },
            onload: function (response) {
              callback(response.responseText);
              callback_num -= 1;
            },
          });
        
};
        const onupdate = (reload) => {
          if (callback_num === 0) {
            set("twitter_feed_rule_cache_last_check", Date.now());
            set("twitter_feed_rule_cache", ruleObj);

            if (reload) location.reload();

            const btn = document.createElement("button");
            btn.onclick = () => location.reload();
            btn.className = "note-update";
            btn.innerText = "屏蔽器规则更新完成|刷新即可生效|点击刷新";
            document.body.insertAdjacentElement("beforeend", btn);
          } else {
            setTimeout(() => onupdate(reload), 1000);
          }
        
};

        let user_rule = parseRule(get("twitter_user_rule"));
        if (user_rule) {
          user_rule["rule-name"] = "自定义用户规则";
          rules.add(user_rule);
        }

        const feed = get("twitter_feed_rule").trim();
        const feed_hash = feed.hashCode();

        let ruleObj = get("twitter_feed_rule_cache", {});
        if (feed_hash !== get("twitter_feed_rule_hash")) {
          set("twitter_feed_rule_hash", feed_hash);
          set("twitter_feed_rule_cache", {});
          ruleObj = {
};

          const btn = document.createElement("button");
          btn.onclick = () => location.reload();
          btn.className = "note-update";
          btn.innerText = "屏蔽器规则已清空|正在重新获取规则";
          btn.style.zIndex = "10086";
          document.body.insertAdjacentElement("beforeend", btn);

          for (let url of feed.split("\n")) {
            url = url.trim();
            if (url.length === 0) continue;

            getText(url, (str) => {
              ruleObj[url] = str;
              set("twitter_feed_rule_cache", ruleObj);
            });
          }

          setTimeout(() => onupdate(true), 1200);
          set("twitter_feed_rule_cache_last_check", Date.now());
        }
        let lastCheckTime = get("twitter_feed_rule_cache_last_check", 0);

        for (const key of Object.keys(ruleObj)) {
          loadRule(ruleObj[key]);
        }
        // 一天检查一次
        if (Date.now() - lastCheckTime > 24 * 60 * 60 * 1000) {
          for (const url of Object.keys(ruleObj)) {
            getText(url, (str) => {
              ruleObj[url] = str;
              set("twitter_feed_rule_cache", ruleObj);
            });
          }
          setTimeout(() => onupdate(), 800);
        }
      }, 250);
    },
  },
  twitter_block_on_home: {
    name: "在关注和推荐页面",
    match: ["/twitter.com/home", "/x.com/home"],
    values: {
      启用屏蔽功能$on: null,
      关闭屏蔽功能$off: null,
    },
  },
  twitter_auto_block: {
    name: "自动屏蔽被精准匹配的用户",
    match: ["twitter.com", "x.com"],
    values: {
      已开启$on: null,
      已关闭$off: null,
    },
  },

};

run(features_twitter_1315146485);