// ==UserScript==
// @name Twitter/X 廣告與詐騙過濾器
// @namespace http://tampermonkey.net/
// @version 11
// @description 過濾推特廣告與詐騙。修正「圖案+文字」的順序讀取問題,確保 Emoji 在前或在後都能精準過濾。
// @author 程式夥伴
// @match https://twitter.com/*
// @match https://x.com/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_setClipboard
// @license MIT
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
// --- 1. 救援清單 ---
const recoveredKeywords = "alert scam solana, join group, buynow, don't miss, alpha feed, done x, best early entry only on tg, free calls, daily alpha, check bio for more, profits on my call, signal, dev's news channel, reddit, solana, check 👉";
// 讀取設定
let blockKeywords = GM_getValue('block_keywords_v3', recoveredKeywords).split(',').map(k => k.trim().toLowerCase()).filter(k => k);
let hideAds = GM_getValue('hide_ads', true);
let debugMode = GM_getValue('debug_mode', false);
// --- 2. CSS 樣式 ---
GM_addStyle(`
.filtered-placeholder {
background-color: #16181c; color: #71767b; font-size: 13px;
text-align: center; padding: 10px 0; margin: 5px 0;
border-radius: 8px; border: 1px dashed #2f3336;
display: flex; justify-content: center; align-items: center; gap: 10px;
}
.view-btn {
background: rgba(29, 155, 240, 0.1); color: #1d9bf0; border: 1px solid #1d9bf0;
padding: 2px 8px; border-radius: 12px; cursor: pointer; font-size: 11px; font-weight: bold;
}
.view-btn:hover { background: #1d9bf0; color: white; }
#filter-panel-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.7); z-index: 99999;
display: flex; justify-content: center; align-items: center;
}
#filter-panel {
background: #000; color: #e7e9ea; padding: 20px;
border-radius: 16px; width: 450px; max-width: 90%; border: 1px solid #333;
box-shadow: 0 0 20px rgba(255,255,255,0.1); font-family: sans-serif;
}
#keyword-list {
display: flex; flex-wrap: wrap; gap: 8px; max-height: 250px; overflow-y: auto;
margin-bottom: 15px; padding: 8px; background: #111; border-radius: 8px; border: 1px solid #333;
}
.kw-tag {
background: #1d9bf0; color: white; padding: 4px 10px;
border-radius: 20px; font-size: 13px; display: flex; align-items: center; gap: 6px;
}
.kw-tag span { cursor: pointer; font-weight: bold; opacity: 0.7; }
.input-group { display: flex; gap: 10px; margin-bottom: 20px; }
#new-kw-input { flex: 1; padding: 10px; background: #222; color: #fff; border: 1px solid #444; border-radius: 4px; }
#add-kw-btn { background: #00ba7c; color: white; border: none; padding: 8px 15px; cursor: pointer; border-radius: 4px; font-weight:bold;}
.panel-actions { display: flex; justify-content: flex-end; gap: 10px; border-top: 1px solid #333; padding-top: 15px; }
`);
// --- 3. UI 介面 ---
function createManagerUI() {
if (document.getElementById('filter-panel-overlay')) return;
const overlay = document.createElement('div');
overlay.id = 'filter-panel-overlay';
overlay.innerHTML = `
🚫 關鍵字過濾管理 (v10.0)
`;
document.body.appendChild(overlay);
const listDiv = document.getElementById('keyword-list');
const input = document.getElementById('new-kw-input');
let tempKeywords = [...blockKeywords];
function renderTags() {
listDiv.innerHTML = '';
if (tempKeywords.length === 0) listDiv.innerHTML = '無關鍵字
';
tempKeywords.forEach((kw, index) => {
const tag = document.createElement('div');
tag.className = 'kw-tag';
tag.innerHTML = `${kw} ✕`;
tag.querySelector('span').onclick = () => { tempKeywords.splice(index, 1); renderTags(); };
listDiv.appendChild(tag);
});
}
function addKeyword(val) {
val = val.trim().toLowerCase();
if (val && !tempKeywords.includes(val)) { tempKeywords.push(val); return true; }
return false;
}
document.getElementById('export-btn').onclick = () => { GM_setClipboard(tempKeywords.join(',')); alert('✅ 已複製清單'); };
document.getElementById('import-btn').onclick = () => {
const d = prompt("貼上清單(逗號分隔)"); if(d) { d.split(',').forEach(w=>addKeyword(w)); renderTags(); }
};
document.getElementById('add-kw-btn').onclick = () => { if(addKeyword(input.value)) input.value=''; renderTags(); };
input.onkeypress = (e) => { if(e.key==='Enter'){ if(addKeyword(input.value)) input.value=''; renderTags(); }};
document.getElementById('close-btn').onclick = () => overlay.remove();
document.getElementById('save-btn').onclick = () => {
blockKeywords = tempKeywords; GM_setValue('block_keywords_v3', blockKeywords.join(','));
overlay.remove(); location.reload();
};
renderTags();
}
function registerMenus() {
GM_registerMenuCommand(`⚙️ 管理關鍵字`, createManagerUI);
const adStatus = hideAds ? "✅" : "❌";
GM_registerMenuCommand(`📢 隱藏廣告 (${adStatus})`, () => {
GM_setValue('hide_ads', !hideAds); location.reload();
});
const debugStatus = debugMode ? "✅" : "❌";
GM_registerMenuCommand(`🐞 除錯模式 (${debugStatus})`, () => {
GM_setValue('debug_mode', !debugMode);
alert(`除錯模式已${!debugMode ? "開啟" : "關閉"}`);
location.reload();
});
}
registerMenus();
// --- 4. 核心邏輯 (v10.0: 依序讀取 DOM) ---
function normalize(str) {
if (!str) return "";
return str.toLowerCase().replace(/\s+/g, '');
}
// 遞迴掃描 DOM,依照視覺順序獲取文字與圖片含義
function getVisualTextContent(node) {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent;
}
if (node.nodeType === Node.ELEMENT_NODE) {
// 如果是圖片 (Emoji)
if (node.tagName === 'IMG' && node.alt) {
return node.alt;
}
// 如果是 SVG (圖標)
if (node.tagName === 'SVG') {
return node.getAttribute('aria-label') || "";
}
// 忽略隱藏元素 (避免讀到沒顯示的 metadata)
// 但 Twitter 的結構比較複雜,這裡暫時不做太嚴格的 style 檢查以免漏掉
let result = "";
for (let child of node.childNodes) {
result += getVisualTextContent(child);
}
return result;
}
return "";
}
function processTweets() {
const tweets = document.querySelectorAll('[data-testid="cellInnerDiv"]:not(.processed)');
tweets.forEach(tweetCell => {
let shouldBlock = false;
let blockReason = "";
// 使用新的讀取器,確保順序正確
const visualText = getVisualTextContent(tweetCell);
const cleanContent = normalize(visualText);
if (debugMode) {
console.log(`v10掃描: ${cleanContent.substring(0, 100)}...`);
}
// A. 廣告過濾
if (hideAds) {
const adIndicators = tweetCell.querySelectorAll('[dir="ltr"] > span');
for (let span of adIndicators) {
if (['Ad', 'Promoted', '推廣', '廣告'].includes(span.innerText)) {
shouldBlock = true;
blockReason = "廣告";
break;
}
}
}
// B. 關鍵字過濾
if (!shouldBlock && blockKeywords.length > 0) {
const hitKeyword = blockKeywords.find(keyword => {
const cleanKeyword = normalize(keyword);
// 檢查包含關係
return cleanContent.includes(cleanKeyword);
});
if (hitKeyword) {
shouldBlock = true;
blockReason = `關鍵字: ${hitKeyword}`;
}
}
// C. 執行過濾 (隱藏 + 偷看按鈕)
if (shouldBlock) {
tweetCell.classList.add('processed');
const children = Array.from(tweetCell.children);
children.forEach(child => {
if (!child.classList.contains('filtered-placeholder')) {
child.style.display = 'none';
child.classList.add('original-content-hidden');
}
});
if (!tweetCell.querySelector('.filtered-placeholder')) {
const placeholder = document.createElement('div');
placeholder.className = 'filtered-placeholder';
placeholder.innerHTML = `
🚫 已過濾 (${blockReason})
`;
const btn = placeholder.querySelector('.view-btn');
btn.onclick = function(e) {
e.stopPropagation();
const hiddenContents = tweetCell.querySelectorAll('.original-content-hidden');
const isCurrentlyHidden = hiddenContents[0].style.display === 'none';
if (isCurrentlyHidden) {
hiddenContents.forEach(el => el.style.display = '');
btn.innerText = '🙈 隱藏';
btn.style.background = '#333';
btn.style.color = '#ccc';
} else {
hiddenContents.forEach(el => el.style.display = 'none');
btn.innerText = '👀 查看';
btn.style.background = '';
btn.style.color = '';
}
};
tweetCell.appendChild(placeholder);
}
}
});
}
const observer = new MutationObserver((mutations) => {
let shouldProcess = false;
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
shouldProcess = true;
break;
}
}
if (shouldProcess) processTweets();
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(processTweets, 1000);
})();