// ==UserScript==
// @name X/Twitter SOL CA Spam Killer (v2.6 – search page only, robust)
// @namespace https://x.com/
// @version 2.6.0
// @description 仅在搜索页(/search?q=)过滤 Solana 合约广告;离开搜索页自动停用并恢复。兼容不同语言/实验UI。
// @match https://x.com/*
// @match https://twitter.com/*
// @run-at document-start
// @grant GM_registerMenuCommand
// ==/UserScript==
(function () {
'use strict';
/*** 仅搜索页判定(更宽松:startsWith + 必须有 q 参数) ***/
const isSearchURL = (u = location.href) => {
try {
const url = new URL(u);
return url.pathname.startsWith('/search') && url.searchParams.has('q');
} catch { return false; }
};
/*** 监听 SPA 路由变化 ***/
const listeners = new Set();
const notify = () => listeners.forEach(fn => fn());
for (const k of ['pushState', 'replaceState']) {
const orig = history[k];
history[k] = function () { const r = orig.apply(this, arguments); setTimeout(notify, 0); return r; };
}
addEventListener('popstate', notify);
/*** 配置与规则 ***/
const STORAGE_KEY = 'sol_ca_filter_enabled_v26';
let ENABLED_MENU = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'true');
let ACTIVE = false;
const BLOCKED_DOMAINS = [
'okai.hk','okai.hk/alpha','alpha.mevx.io',
'gmgn.ai','gmgn.ai/sol/token','gmgn.cc','gmgn.org',
'photon-sol.com','dexscreener.com','birdeye.so','rugcheck.xyz',
'pump.fun','pumpswap','t.me'
];
const KEYWORDS = [
'token alert','token stats','links','security',
'mc $','market cap','vol','lp','ath',
'watch your entry','called at','quick buy','signal',
'up up up','gmgn','bonkbot','trojan',
'chain: solana','dev: holds token','mint authority: no','freeze authority: no',
'📍ca',' ca:',' ca:',' ca,',' ca,',' ca;',' ca;',' ca>',' ca>>','ca>'
];
const WHITELIST = ['$smiley','#smiley'];
const SOL_ADDR_RE = /\b(?=.{32,44}\b)(?!.*[OIl0])[1-9A-HJ-NP-Za-km-z]{32,44}\b/g;
const CA_NEAR_ADDR_RE = /\b(?:ca|contract|合约)\b[\s::,,;;>>»》›]+[\s\r\n]{0,40}[1-9A-HJ-NP-Za-km-z]{32,44}\b/i;
const TICKER_RE = /\$[A-Z]{2,8}\b/;
const normalize = (s) =>
(s||'').replace(/>/gi,'>').toLowerCase()
.replace(/[\u200B-\u200D\uFEFF]/g,'')
.replace(/[:﹕꞉⦂︰]/g,':')
.replace(/\s+/g,' ').trim();
const normKeywords = KEYWORDS.map(normalize);
const normWhitelist = WHITELIST.map(normalize);
const hasBlockedDomain = (el) => {
const links = el.querySelectorAll('a[href], a[role="link"]');
for (const a of links) {
const href = (a.getAttribute('href') || a.textContent || '').toLowerCase();
for (const d of BLOCKED_DOMAINS) if (href.includes(d)) return true;
}
const t = (el.innerText || '').toLowerCase();
return BLOCKED_DOMAINS.some(d => t.includes(d));
};
const keywordScore = (t) => normKeywords.reduce((n,k)=> n + (k && t.includes(k) ? 1 : 0), 0);
const hitWhitelist = (t) => normWhitelist.some(w => w && t.includes(w));
function isSpamArticle(article) {
const raw = article.innerText || article.textContent || '';
if (!raw) return false;
if (hasBlockedDomain(article)) return true;
const text = normalize(raw);
if (hitWhitelist(text)) return false;
const addrs = raw.match(SOL_ADDR_RE) || [];
const counts = {};
addrs.forEach(a => counts[a] = (counts[a] || 0) + 1);
const repeated = Object.values(counts).some(c => c >= 2);
if (CA_NEAR_ADDR_RE.test(raw)) return true;
if (addrs.length) {
if (repeated) return true;
const score = keywordScore(text);
const hasTicker = TICKER_RE.test(raw);
if (hasTicker && score >= 1) return true; // 地址 + $TICKER + ≥1 版式词
if (!hasTicker && score >= 2) return true; // 地址 + ≥2 版式词
}
if (!addrs.length && keywordScore(text) >= 4) return true;
return false;
}
/*** DOM 处理(在搜索页时对整页 article 扫描;离开即清空) ***/
const TWEET_SELECTOR = 'article[data-testid="tweet"], article[role="article"]';
const HIDE_CLASS = 'sol-ca-hide';
const style = document.createElement('style');
style.textContent = `.${HIDE_CLASS}{display:none !important;}`;
document.documentElement.appendChild(style);
const handleTweet = (a) => {
if (!ACTIVE || !a || a.dataset.__solCaChecked==='1') return;
a.dataset.__solCaChecked = '1';
if (isSpamArticle(a)) a.classList.add(HIDE_CLASS);
};
const scanAll = () => {
if (!ACTIVE) return;
document.querySelectorAll(TWEET_SELECTOR).forEach(handleTweet);
};
const clearAll = () => {
document.querySelectorAll(`.${HIDE_CLASS}`).forEach(n => n.classList.remove(HIDE_CLASS));
document.querySelectorAll(TWEET_SELECTOR).forEach(n => { n.dataset.__solCaChecked = ''; });
};
let obs = null;
const observe = () => {
if (obs) return;
obs = new MutationObserver(muts => {
if (!ACTIVE) return;
for (const m of muts) for (const node of m.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
if (node.matches?.(TWEET_SELECTOR)) handleTweet(node);
else node.querySelectorAll?.(TWEET_SELECTOR).forEach(handleTweet);
}
});
obs.observe(document.body, { childList: true, subtree: true });
};
const unobserve = () => { if (obs) { obs.disconnect(); obs = null; } };
/*** 激活/停用(仅搜索页) ***/
function reevaluate() {
const shouldRun = ENABLED_MENU && isSearchURL();
if (shouldRun && !ACTIVE) {
ACTIVE = true;
observe();
scanAll();
setTimeout(scanAll, 600);
setTimeout(scanAll, 2000);
} else if (!shouldRun && ACTIVE) {
ACTIVE = false;
unobserve();
clearAll(); // 离开搜索页恢复
}
}
listeners.add(reevaluate);
/*** 菜单 ***/
function menu() {
if (typeof GM_registerMenuCommand !== 'function') return;
GM_registerMenuCommand(`过滤器(仅搜索页):${ENABLED_MENU ? '✅ 开启' : '⛔ 关闭'}`, ()=>{});
GM_registerMenuCommand(ENABLED_MENU ? '🔕 关闭过滤器' : '🔔 开启过滤器', () => {
ENABLED_MENU = !ENABLED_MENU;
localStorage.setItem(STORAGE_KEY, JSON.stringify(ENABLED_MENU));
alert(ENABLED_MENU ? '过滤器开启(仅搜索页)' : '过滤器已关闭');
reevaluate();
});
}
/*** 启动 ***/
const ready = (fn) =>
(document.readyState === 'loading'
? document.addEventListener('DOMContentLoaded', fn, { once:true })
: fn());
ready(() => {
menu();
reevaluate();
setInterval(reevaluate, 1500); // 兜底
});
})();