// ==UserScript==
// @name Twitter自用增强
// @namespace http://viayoo.com/bhivy
// @version 1.9
// @description 自用脚本,过滤推特(Twitter)评论区,去广告,尝试解锁敏感限制。
// @author Via && Deepseek
// @match https://x.com/*
// @exclude https://x.com/*/status/*/video/*
// @exclude https://x.com/*/status/*/photo/*
// @exclude https://x.com/*/status/*/mediaViewer*
// @exclude https://x.com/settings
// @exclude https://x.com/i/*/creators/
// @exclude https://x.com/i/bookmarks
// @exclude https://x.com/i/premium-business
// @exclude https://x.com/*/lists
// @exclude https://x.com/i/follow_people
// @exclude https://x.com/i/chat
// @exclude https://x.com/notifications
// @exclude https://x.com/i/grok
// @icon data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAA0pJREFUWAntVk1oE1EQnnlJbFK3KUq9VJPYWgQVD/5QD0qpfweL1YJQoZAULBRPggp6kB78PQn14kHx0jRB0UO9REVFb1YqVBEsbZW2SbVS0B6apEnbbMbZ6qbZdTempqCHPAjvzcw3P5mdmfcAiquYgX+cAVwu/+5AdDMQnSPCHUhQA0hf+Rxy2OjicIvzm+qnKhito0qpb2wvJhWeJgCPP7oPELeHvdJ1VSGf3eOPnSWga0S0Qo9HxEkEusDBuNjbEca8G291nlBxmgDc/ukuIvAJxI6wr+yKCsq1ewLxQ2lZfpQLo8oQ4ZXdCkfnACrGWpyDCl+oQmVn5xuVPU102e2P3qoJkFOhzVb9S7KSnL5jJs/mI+As01PJFPSlZeFSZZoAGBRXBZyq9lk5NrC+e7pJ5en30c+JWk59pZ5vRDOuhAD381c/H/FKz1SMNgCE16rg505r5TT0uLqme93d0fbq+1SeLSeU83Ke0RHYFPGVPcjQfNDUwIa7M665+dQAEEjZoMwZMcEF9RxIDAgBQ2mCcqJ0Z0b+h4MNbZ4RnyOSDbNmE2iRk5jCNgIIckFoZAs4IgfLGrlKGjkzS16iwj6pV9I4mUvCPf73JVytH9nRJj24QHrqU8NCIWrMaGqAC+Ut/3ZzAS63cx4v2K/x/IvQBOCwWzu5KmJGwEJ5PIgeG9nQBDDcXPpFoDjJ7ThvBC6EZxXWkJG+JgAFwGM4KBAOcibeGCn8FQ/hyajXPmSk+1sACogn4hYk7OdiHDFSWipPkPWSmY6mCzIghEEuxJvcEYUvxIdhX2mvmSHDDPBF9AJRnDZTyp+P40671JYLbxiAohDxSTfQIg4oNxgPzCWPHaWQBViOf2jGqVwBaEaxGbAqOFMrp+SefC8eNhoFIY5lXzpmtnMGUB2IbU3JdIqVW9m5zcxINn/hAYKiIexdaTh4srHKORMAP0b28PNgJyGt5gvHzQVYx91QpVcwpRFl/p63HSR1DLbid1OcTpAJQOG7u+KH+aI5Qwj13IsamU5vkUSIc8uGLDa8OtoivV8U5HcydFLtT7hlSDVy2nfxI2Ibg9awuVU8IeJAOMF5m2B6jFs1tM5R9rS3GRP5uSuiihn4DzPwA7z7GDH+43gqAAAAAElFTkSuQmCC
// @license MIT
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_setClipboard
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
const api = {
getValue: (key, def) => {
if (typeof GM_getValue !== 'undefined') return GM_getValue(key, def);
const val = localStorage.getItem(key);
return val === null ? def : JSON.parse(val);
},
setValue: (key, val) => {
if (typeof GM_setValue !== 'undefined') { GM_setValue(key, val); }
else { localStorage.setItem(key, JSON.stringify(val)); }
},
setClipboard: (text) => {
if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(text); }
else { navigator.clipboard.writeText(text); }
}
};
let config = api.getValue('filterConfig', { badUsers: [], badWords: ["/有(哥哥|弟弟).*线(上|下)(约|吗)/i","/^[\\p{Emoji}\\n]+$/u","/^(线上|线下)?(蹲|找)一?个(男|女)?(长期|临时|温柔)?(固炮|搭子|主人|哥哥|弟弟|主|[Ss])/","/^(?=.*我是真人)(?=.*线(上|下)(的|吗))/","/dd个?线下的(姐姐|弟弟|哥哥|妹妹).?/i","/有没有离(得|的)近(得|的)$/","/急需?(一个|一位|找)(主人|爸爸|固炮|搭子).?$/","/(主人|爸爸)快来(领|找)我.?$/","/^线下dd$/","/^..找..(主人|主|爸爸|固炮|搭子)$/","/小(狗|🐶|[Mm])在线(等|找)个?(主人|主|爸爸|固炮|搭子|调|你).?/","/小(狗|🐶|[Mm])想(和|跟)(大家|主人|主|爸爸|固炮|搭子|调|你)玩.?/","/^有?万达广场附近的吗$/","/^(男大|女大)大?(哥哥|弟弟|姐姐|妹妹)快?来$/","/^谁来当我(主人|爸爸|狗狗|小狗|骚母狗).?$/","/(同城|附近).*(满足|草|操|艹)我/","/(同城|附近).*(男大|女大|体育生|母狗)$/","/^(小|母)狗求(主人|爸爸|哥哥)(抱抱|操|艹|草|抱走)/","/^有没有(单男|母狗|骚逼).?$/"] });
const compileRules = (words) => words.map(r => {
try {
if (r.startsWith('/') && r.lastIndexOf('/') > 0) {
const lastSlashIndex = r.lastIndexOf('/');
return new RegExp(r.substring(1, lastSlashIndex), r.substring(lastSlashIndex + 1));
}
return new RegExp(r);
} catch (e) { return null; }
}).filter(Boolean);
let compiledRules = compileRules(config.badWords);
const PROCESSED_TWEETS = new WeakSet();
const FILTER_CACHE = new Map();
const cleanText = (text) => {
if (!text) return "";
return text.replace(/[\u0000-\u001F\u007F-\u009F\u00AD\u061C\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF\u200D\u200C\u180E\u202F\u205F\u2060\uFE00-\uFE0F]/g, '').trim();
};
const isSpamBot = (tweetTextEl) => {
if (!tweetTextEl) return false;
const emojis = tweetTextEl.querySelectorAll('img[src*="emoji"]');
if (emojis.length === 0) return false;
const pureText = cleanText(tweetTextEl.innerText || "");
const hasPunctuation = /[\u3002\uff0c\uff1f\uff01\uff1b\uff1a\u3001.?!,;:]/.test(pureText);
if (!hasPunctuation && (pureText.length < 3)) return true;
return false;
};
const twitterContentUnblocker = (() => {
const TARGET_ENDPOINTS = ['UserTweets', 'TweetDetail', 'SearchTimeline', 'HomeTimeline'];
const rewriteResponse = (rawText) => {
try {
return rawText
.replace(/"possibly_sensitive":\s*true/g, '"possibly_sensitive":false')
.replace(/"profile_interstitial_type":\s*"sensitive_media"/g, '"profile_interstitial_type":""')
.replace(/"mediaVisibilityResults":/g, '"mediaVisibilityResults_bypass":');
} catch (e) { return rawText; }
};
return () => {
const XHR = XMLHttpRequest.prototype;
const send = XHR.send;
const open = XHR.open;
XHR.open = function(method, url) { this._url = url; return open.apply(this, arguments);};
XHR.send = function() {
if (this._url && TARGET_ENDPOINTS.some(ep => this._url.includes(ep))) {
const responseTextGetter = Object.getOwnPropertyDescriptor(XHR, 'responseText').get;
Object.defineProperty(this, 'responseText', {
get: function() {
const originalText = responseTextGetter.call(this);
return typeof originalText === 'string' ? rewriteResponse(originalText) : originalText;
},
configurable: true
});
}
return send.apply(this, arguments);
};
};
})();
const twitterAdCleaner = (() => {
const PROCESSED_NODES = new WeakSet();
const AD_SELECTORS = ["div[data-testid='placementTracking']", "button[aria-label='Grok actions']", "aside[role='complementary']"];
const isAdText = (t) => ['Promoted', '广告', '推廣', '広告', 'Sponsorisé'].includes(t);
const processNode = (node) => {
if (!node || node.nodeType !== 1 || PROCESSED_NODES.has(node)) return;
try {
if (node.getAttribute('data-testid') === 'cellInnerDiv') {
const hasAdSignal = node.querySelector(AD_SELECTORS[0]);
const adLabel = Array.from(node.querySelectorAll('article span')).find(s => isAdText(s.textContent));
if (hasAdSignal || adLabel) {
node.style.setProperty('display', 'none', 'important');
PROCESSED_NODES.add(node);
return;
}
}
if (node.matches?.(AD_SELECTORS[1]) || node.matches?.(AD_SELECTORS[2])) {
node.style.display = 'none';
}
} catch (e) {}
};
return () => {
const observer = new MutationObserver((m) => {
requestAnimationFrame(() => m.forEach(mu => mu.addedNodes.forEach(processNode)));
});
observer.observe(document.body, { childList: true, subtree: true });
document.querySelectorAll('div[data-testid="cellInnerDiv"]').forEach(processNode);
};
})();
const quickFilterUI = (tweetData) => {
const existing = document.getElementById('tf-quick-panel');
if (existing) existing.remove();
const box = document.createElement('div');
box.id = 'tf-quick-panel';
const shadow = box.attachShadow({mode: 'closed'});
const style = document.createElement('style');
style.textContent = `
:host { position: fixed; inset: 0; z-index: 1000000; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.5); font-family: -apple-system, system-ui; }
.card { background: white; width: 320px; padding: 20px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }
@media (prefers-color-scheme: dark) { .card { background: #15202b; color: white; } }
h4 { margin: 0 0 15px 0; font-size: 16px; }
.opt { background: rgba(128,128,128,0.1); padding: 10px; border-radius: 8px; margin-bottom: 8px; cursor: pointer; font-size: 13px; word-break: break-all; border: 1px solid transparent; }
.opt:hover { border-color: #1d9bf0; background: rgba(29,155,240,0.1); }
textarea { width: 100%; height: 60px; margin-top: 5px; border-radius: 8px; border: 1px solid #ccc; padding: 5px; font-size: 12px; resize: none; box-sizing: border-box; }
.btns { display: flex; gap: 8px; margin-top: 15px; }
button { flex: 1; padding: 8px; border-radius: 20px; border: none; font-weight: bold; cursor: pointer; }
.add { background: #1d9bf0; color: white; }
.cancel { background: #eee; color: #333; }
`;
const cleanVal = cleanText(tweetData.text);
const regexVal = `/${cleanVal.substring(0, 10).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*/`;
shadow.appendChild(style);
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
快速过滤
屏蔽用户: ${tweetData.userId}
提取正则: ${regexVal}
自定义规则:
`;
shadow.appendChild(card);
document.body.appendChild(box);
const close = () => box.remove();
shadow.getElementById('cbtn').onclick = close;
shadow.getElementById('addU').onclick = () => { shadow.getElementById('customR').value = tweetData.userId; shadow.getElementById('abtn').click(); };
shadow.getElementById('addR').onclick = () => { shadow.getElementById('customR').value = regexVal; shadow.getElementById('abtn').click(); };
shadow.getElementById('abtn').onclick = () => {
const val = shadow.getElementById('customR').value.trim();
if (val.startsWith('@')) {
config.badUsers.push(val);
} else {
config.badWords.push(val);
}
api.setValue('filterConfig', config);
location.reload();
};
};
const addFilterBtn = (tweet, data) => {
const group = tweet.querySelector('div[role="group"]:last-child');
if (!group || group.querySelector('.tf-qbtn')) return;
const btn = document.createElement('div');
btn.className = 'tf-qbtn';
btn.style = 'display:flex;align-items:center;justify-content:center;padding:0 12px;cursor:pointer;opacity:0.6;';
btn.innerHTML = '';
btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); quickFilterUI(data); };
const likeBtn = group.querySelector('div[data-testid="like"]');
if (likeBtn) {
likeBtn.after(btn);
} else {
const replyBtn = group.querySelector('div[data-testid="reply"]') || group.firstChild;
if (replyBtn) replyBtn.after(btn);
}
};
const processFilterNode = (tweet) => {
if (!tweet || PROCESSED_TWEETS.has(tweet)) return;
const textEl = tweet.querySelector('[data-testid="tweetText"]');
const tweetLink = tweet.querySelector('a[href*="/status/"]');
const tweetId = tweetLink ? tweetLink.getAttribute('href').split('/status/')[1]?.split('?')[0] : null;
const avatar = tweet.querySelector('[data-testid^="UserAvatar-Container-"]');
const userId = avatar ? '@' + avatar.getAttribute('data-testid').replace('UserAvatar-Container-', '') : null;
const nameContainer = tweet.querySelector('[data-testid="User-Name"]');
const userName = nameContainer ? nameContainer.innerText.split('\n')[0] : null;
const rawText = textEl ? textEl.innerText : "";
if (tweetId && FILTER_CACHE.get(tweetId)) {
tweet.style.setProperty('display', 'none', 'important');
PROCESSED_TWEETS.add(tweet); return;
}
const isSpam = isSpamBot(textEl);
const shouldHide = isSpam || config.badUsers.some(u => {
const t = u.toLowerCase();
return (userId && userId.toLowerCase() === t) || (userName && userName.toLowerCase().includes(t));
}) || (textEl && compiledRules.some(r => r.test(cleanText(rawText))));
if (shouldHide) {
tweet.style.setProperty('display', 'none', 'important');
if (tweetId) FILTER_CACHE.set(tweetId, true);
} else {
addFilterBtn(tweet, { userId, text: rawText });
}
PROCESSED_TWEETS.add(tweet);
};
const filterObserver = new MutationObserver((mutations) => {
if (shouldExclude()) return;
requestAnimationFrame(() => {
for (let i = 0; i < mutations.length; i++) {
mutations[i].addedNodes.forEach(node => {
if (node.nodeType === 1) {
const tweet = node.matches('article[data-testid="tweet"]') ? node : node.querySelector('article[data-testid="tweet"]');
if (tweet) processFilterNode(tweet);
else if (node.getAttribute?.('data-testid') === 'cellInnerDiv') {
setTimeout(() => { const r = node.querySelector('article[data-testid="tweet"]'); if (r) processFilterNode(r); }, 100);
}
}
});
}
});
});
const createUI = () => {
const container = document.createElement('div');
container.id = 'tf-plugin';
document.body.appendChild(container);
const shadow = container.attachShadow({ mode: 'closed' });
const style = document.createElement('style');
style.textContent = `
#mask { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: none; z-index: 999998; backdrop-filter: blur(4px); }
#fab { position: fixed; right: 0; top: 50%; transform: translateY(-50%); width: 42px; height: 42px; background: rgba(255, 255, 255, 0.6); color: #000; border-radius: 21px 0 0 21px; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: -2px 0 12px rgba(0,0,0,0.1); z-index: 999997; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); padding-left: 4px; backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.3); border-right: none; }
#fab.idle { opacity: 0.3; transform: translateY(-50%) translateX(25px); }
#panel { display: none; position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 340px; max-width: 85vw; max-height: 80vh; background: rgba(255,255,255,0.75); backdrop-filter: blur(12px); border-radius: 20px; z-index: 999999; padding: 24px; border: 1px solid rgba(255,255,255,0.3); overflow-y: auto; font-family: -apple-system, system-ui; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
.header h3 { margin: 0; font-size: 18px; }
.close-btn { cursor: pointer; padding: 4px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.close-btn:hover { background: rgba(0,0,0,0.1); }
label { display: block; font-size: 13px; font-weight: bold; margin-bottom: 5px; color: #536471; }
textarea { width: 100%; height: 80px; border: 1px solid rgba(0,0,0,0.1); border-radius: 12px; padding: 10px; margin-bottom: 10px; box-sizing: border-box; resize: none; outline: none; transition: all 0.2s; }
textarea:focus { border-color: #1d9bf0; background: #fff; }
.btn-group { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
button { padding: 10px; border-radius: 10px; border: none; font-weight: bold; cursor: pointer; transition: all 0.2s; }
.save { background: #1d9bf0; color: #fff; grid-column: span 2; }
.save:hover { background: #1a8cd8; transform: translateY(-1px); }
@media (prefers-color-scheme: dark) {
#fab { background: rgba(0, 0, 0, 0.6); color: #fff; }
#panel { background: rgba(21, 32, 43, 0.85); color: #fff; }
.close-btn:hover { background: rgba(255,255,255,0.1); }
label { color: #8b98a5; }
textarea { background: rgba(0,0,0,0.3); color: #fff; }
textarea:focus { background: rgba(0,0,0,0.5); }
}
`;
shadow.appendChild(style);
const div = document.createElement('div');
div.innerHTML = `
`;
shadow.appendChild(div);
const panel = shadow.getElementById('panel'), fab = shadow.getElementById('fab'), mask = shadow.getElementById('mask');
let hideTimer;
const resetTimer = () => { fab.classList.remove('idle'); clearTimeout(hideTimer); hideTimer = setTimeout(() => { if (panel.style.display !== 'block') fab.classList.add('idle'); }, 3000); };
const toggleUI = (show) => { panel.style.display = show ? 'block' : 'none'; mask.style.display = show ? 'block' : 'none'; if (!show) resetTimer(); };
fab.onclick = () => toggleUI(true);
mask.onclick = () => toggleUI(false);
shadow.getElementById('closeX').onclick = () => toggleUI(false);
fab.onmouseenter = () => fab.classList.remove('idle');
fab.onmouseleave = resetTimer;
shadow.getElementById('saveBtn').onclick = () => {
config.badUsers = shadow.getElementById('users').value.split('\n').map(s => s.trim()).filter(s => s);
config.badWords = shadow.getElementById('words').value.split('\n').map(s => s.trim()).filter(s => s);
api.setValue('filterConfig', config); location.reload();
};
shadow.getElementById('copyBtn').onclick = () => { api.setClipboard(JSON.stringify(config)); alert('已复制'); };
shadow.getElementById('importBtn').onclick = () => { const j = prompt("JSON:"); if(j){ try { const p = JSON.parse(j); if(p.badWords){ api.setValue('filterConfig', p); location.reload(); } } catch(e){ alert("格式错误"); } }};
window.addEventListener('popstate', () => { container.style.display = shouldExclude() ? 'none' : 'block'; });
document.addEventListener('click', () => setTimeout(() => { container.style.display = shouldExclude() ? 'none' : 'block'; }, 500));
resetTimer();
};
const shouldExclude = (() => {
const ex = /https:\/\/x\.com\/(?:.*\/status\/.*\/(?:video|photo|mediaViewer.*)|settings|i\/.*\/creators\/|i\/bookmarks|i\/premium-business|.*\/lists|i\/follow_people|i\/chat|notifications|i\/grok)/;
let lastUrl = ''; let lastResult = false;
return () => {
const curr = location.href; if (curr === lastUrl) return lastResult;
lastUrl = curr; lastResult = ex.test(curr);
if (lastResult) filterObserver.disconnect();
else filterObserver.observe(document.body, { childList: true, subtree: true });
return lastResult;
};
})();
twitterContentUnblocker();
const checkReady = setInterval(() => {
const main = document.querySelector('main');
if (main) { clearInterval(checkReady); createUI(); twitterAdCleaner(); if (!shouldExclude()) document.querySelectorAll('article[data-testid="tweet"]').forEach(processFilterNode); }
}, 1000);
})();