您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在推文右上角添加屏蔽按钮,一键隐藏特定用户的推文内容,支持导入导出屏蔽用户列表,无需Twitter API拉黑
// ==UserScript== // @name Twitter用户屏蔽器 // @namespace http://tampermonkey.net/ // @version 2.5 // @description 在推文右上角添加屏蔽按钮,一键隐藏特定用户的推文内容,支持导入导出屏蔽用户列表,无需Twitter API拉黑 // @author https://x.com/0xfocu5 // @license MIT // @match https://twitter.com/* // @match https://x.com/* // @match https://mobile.twitter.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_registerMenuCommand // ==/UserScript== (function() { 'use strict'; // 配置参数 const CONFIG = { buttonColor: 'rgb(83, 100, 113)', hoverColor: 'rgb(244, 33, 46)', timeouts: { init: 1500, checkInterval: 500 }, selectors: { tweet: 'article[data-testid="tweet"]', actionBar: '[role="group"]', userLink: 'a[role="link"][href*="/"]', moreButton: '[data-testid="caret"]', menuItem: '[role="menuitem"]', confirmButton: 'div[role="button"]', cellInnerDiv: '[data-testid="cellInnerDiv"]', socialContext: '[data-testid="socialContext"]' } }; // 添加样式 GM_addStyle(` .block-user-btn { background: transparent; color: rgb(244, 33, 46); border: none; border-radius: 50%; padding: 8px; cursor: pointer; transition: all 0.2s; z-index: 1000; position: relative; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; margin: 0; } .block-user-btn:hover { background: rgba(244, 33, 46, 0.1); color: rgb(185, 28, 37); } .block-user-btn.blocked { background: transparent; color: rgb(120, 86, 255); } .block-user-btn.blocked:hover { background: rgba(120, 86, 255, 0.1); color: rgb(88, 66, 234); } .blocked-tweet { opacity: 0.3; filter: blur(2px); pointer-events: none; } .blocked-tweet .block-user-btn { opacity: 1; filter: none; pointer-events: auto; } .hidden-tweet { display: none !important; } .block-user-btn svg { width: 20px; height: 20px; fill: currentColor; } `); // 定义屏蔽按钮的SVG图标,使用用户提供的图标 const blockIconSVG = ` <svg viewBox="0 0 33 32" aria-hidden="true" class="r-4qtqp9 r-yyyyoo r-dnmrzs r-bnwqim r-lrvibr r-m6rgpd r-1xvli5t r-1hdv0qi"> <g> <path d="M12.745 20.54l10.97-8.19c.539-.4 1.307-.244 1.564.38 1.349 3.288.746 7.241-1.938 9.955-2.683 2.714-6.417 3.31-9.83 1.954l-3.728 1.745c5.347 3.697 11.84 2.782 15.898-1.324 3.219-3.255 4.216-7.692 3.284-11.693l.008.009c-1.351-5.878.332-8.227 3.782-13.031L33 0l-4.54 4.59v-.014L12.743 20.544m-2.263 1.987c-3.837-3.707-3.175-9.446.1-12.755 2.42-2.449 6.388-3.448 9.852-1.979l3.72-1.737c-.67-.49-1.53-1.017-2.515-1.387-4.455-1.854-9.789-.931-13.41 2.728-3.483 3.523-4.579 8.94-2.697 13.561 1.405 3.454-.899 5.898-3.22 8.364C1.49 30.2.666 31.074 0 32l10.478-9.466"></path> </g> </svg> `; // 获取屏蔽用户列表 function getBlockedUsers() { try { const users = GM_getValue('blockedUsers', []); console.log(`获取屏蔽用户列表,共 ${users.length} 个用户:`, users); return users; } catch (error) { console.error('获取屏蔽用户列表失败:', error); return []; } } // 保存屏蔽用户列表 function saveBlockedUsers(users) { try { if (!Array.isArray(users)) { console.error('保存失败:用户列表不是数组:', users); return false; } GM_setValue('blockedUsers', users); console.log(`成功保存屏蔽用户列表,共 ${users.length} 个用户:`, users); return true; } catch (error) { console.error('保存屏蔽用户列表失败:', error); return false; } } // 隐藏被屏蔽用户的推文 function hideBlockedUserTweets() { const blockedUsers = getBlockedUsers(); if (blockedUsers.length === 0) return; const tweets = document.querySelectorAll(CONFIG.selectors.tweet); tweets.forEach(tweet => { const userLink = tweet.querySelector(CONFIG.selectors.userLink); if (!userLink) return; const username = userLink.href.split('/').filter(Boolean).pop(); if (!username || username === 'home' || username === 'explore') return; if (blockedUsers.includes(username)) { // 找到推文的容器元素并隐藏 const tweetContainer = tweet.closest(CONFIG.selectors.cellInnerDiv); if (tweetContainer) { tweetContainer.classList.add('hidden-tweet'); } } }); } // 显示被屏蔽用户的推文(取消屏蔽时) function showBlockedUserTweets(username) { const tweets = document.querySelectorAll(CONFIG.selectors.tweet); tweets.forEach(tweet => { const userLink = tweet.querySelector(CONFIG.selectors.userLink); if (!userLink) return; const tweetUsername = userLink.href.split('/').filter(Boolean).pop(); if (tweetUsername === username) { const tweetContainer = tweet.closest(CONFIG.selectors.cellInnerDiv); if (tweetContainer) { tweetContainer.classList.remove('hidden-tweet'); } } }); } // 添加屏蔽按钮 function addBlockButton(tweetElement) { // 检查是否已经添加过按钮 if (tweetElement.querySelector('.block-user-btn')) { return; } console.log('开始为推文添加屏蔽按钮:', tweetElement); // 查找推文右上角的操作区域 let actionArea = tweetElement.querySelector('[data-testid="tweet"]') || tweetElement.querySelector('[role="article"]') || tweetElement; if (!actionArea) { console.log('未找到actionArea'); return; } // 查找用户名 let usernameElement = tweetElement.querySelector('a[href^="/"][role="link"]'); if (!usernameElement) { console.log('未找到用户名元素'); return; } let username = usernameElement.getAttribute('href').substring(1); if (!username || username === 'home' || username === 'explore') { console.log('无效的用户名:', username); return; } console.log('找到用户名:', username); // 检查用户是否已被屏蔽 let blockedUsers = getBlockedUsers(); let isBlocked = blockedUsers.includes(username); // 创建屏蔽按钮 let blockBtn = document.createElement('div'); blockBtn.className = 'block-user-btn'; if (isBlocked) { blockBtn.classList.add('blocked'); } blockBtn.innerHTML = blockIconSVG; blockBtn.title = isBlocked ? '取消屏蔽此用户' : '屏蔽此用户'; // 悬停效果由CSS类控制,无需JavaScript设置 // 添加点击事件 blockBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); // 获取最新的屏蔽用户列表 let currentBlockedUsers = getBlockedUsers(); let currentIsBlocked = currentBlockedUsers.includes(username); if (currentIsBlocked) { // 取消屏蔽 currentBlockedUsers = currentBlockedUsers.filter(user => user !== username); blockBtn.title = '屏蔽此用户'; blockBtn.classList.remove('blocked'); console.log(`取消屏蔽用户: @${username}`); // 显示该用户的推文 showBlockedUserTweets(username); } else { // 屏蔽用户 currentBlockedUsers.push(username); blockBtn.title = '取消屏蔽此用户'; blockBtn.classList.add('blocked'); console.log(`屏蔽用户: @${username}`); // 立即隐藏该用户的推文 hideBlockedUserTweets(); } // 保存更新后的列表 saveBlockedUsers(currentBlockedUsers); console.log(`屏蔽用户列表已更新,当前共 ${currentBlockedUsers.length} 个用户`); // 更新本地状态 isBlocked = !currentIsBlocked; }); // 查找合适的位置插入按钮 - 定位到右侧红框位置 console.log('开始查找按钮插入位置...'); // 查找推文右上角的操作按钮容器 let actionContainer = tweetElement.querySelector('div[class*="r-1awozwy r-18u37iz r-1cmwbt1 r-1wtj0ep"]'); if (!actionContainer) { // 备用方法:查找包含操作按钮的容器 actionContainer = tweetElement.querySelector('div[class*="r-1awozwy r-18u37iz"]'); } if (!actionContainer) { // 再备用:查找包含按钮的容器 actionContainer = tweetElement.querySelector('div[role="group"]'); } if (actionContainer) { console.log('找到actionContainer,准备插入按钮'); // 查找More按钮(三个点的按钮) let moreButton = actionContainer.querySelector('[data-testid="caret"]'); if (moreButton) { // 在More按钮之前插入屏蔽按钮 moreButton.parentElement.insertBefore(blockBtn, moreButton); console.log('按钮已插入到More按钮之前'); } else { // 如果找不到More按钮,插入到容器末尾 actionContainer.appendChild(blockBtn); console.log('按钮已添加到操作容器末尾'); } } else { console.log('未找到actionContainer,尝试查找其他位置'); // 备用方法:查找推文头部容器 let headerContainer = tweetElement.querySelector('div[dir="ltr"]') || tweetElement.querySelector('div[style*="display: flex"]') || tweetElement.querySelector('div[style*="flex-direction: row"]'); if (headerContainer) { headerContainer.appendChild(blockBtn); console.log('按钮已添加到头部容器'); } else { console.log('未找到合适位置,直接插入到推文元素'); tweetElement.insertBefore(blockBtn, tweetElement.firstChild); } } // 如果用户已被屏蔽,应用屏蔽样式 if (isBlocked) { tweetElement.classList.add('blocked-tweet'); } } // 处理推文元素 function processTweets() { // 查找所有推文元素 - 使用更精确的选择器 let tweets = document.querySelectorAll('article[data-testid="tweet"]'); console.log('找到推文数量:', tweets.length); if (tweets.length === 0) { // 如果没找到,尝试其他选择器 tweets = document.querySelectorAll('[role="article"]'); console.log('使用备用选择器找到推文数量:', tweets.length); } tweets.forEach((tweet, index) => { // 检查是否是真正的推文(不是其他内容) if (tweet.querySelector('a[href^="/"][role="link"]')) { console.log(`处理第${index + 1}条推文:`, tweet); addBlockButton(tweet); } }); // 隐藏被屏蔽用户的推文 hideBlockedUserTweets(); } // 监听页面变化 function observePageChanges() { const observer = new MutationObserver(function(mutations) { let shouldProcess = false; mutations.forEach(function(mutation) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { shouldProcess = true; } }); if (shouldProcess) { setTimeout(processTweets, 100); } }); observer.observe(document.body, { childList: true, subtree: true }); } // 初始化 function init() { // 等待页面加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function() { setTimeout(processTweets, 1000); observePageChanges(); }); } else { setTimeout(processTweets, 1000); observePageChanges(); } // 定期检查新推文 setInterval(processTweets, 3000); } // 导出屏蔽用户列表到文件 function exportBlockedUsers() { const blockedUsers = getBlockedUsers(); if (blockedUsers.length === 0) { alert('当前没有屏蔽任何用户'); return; } const data = { version: '1.0', exportDate: new Date().toISOString(), blockedUsers: blockedUsers }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `twitter_blocked_users_${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); console.log('已导出屏蔽用户列表'); } // 导入屏蔽用户列表从文件 function importBlockedUsers() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = function(e) { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(e) { try { const data = JSON.parse(e.target.result); if (data.blockedUsers && Array.isArray(data.blockedUsers)) { const currentBlocked = getBlockedUsers(); const newBlocked = [...new Set([...currentBlocked, ...data.blockedUsers])]; if (saveBlockedUsers(newBlocked)) { alert(`成功导入 ${data.blockedUsers.length} 个用户\n当前总共屏蔽 ${newBlocked.length} 个用户`); console.log('已导入屏蔽用户列表:', data.blockedUsers); // 刷新页面状态 processTweets(); } else { alert('导入失败,保存用户列表时出错'); } } else { alert('文件格式错误,请选择正确的屏蔽用户导出文件'); } } catch (error) { alert('文件解析失败,请检查文件格式'); console.error('导入文件解析失败:', error); } }; reader.readAsText(file); }; input.click(); } // 删除指定的屏蔽用户 function removeBlockedUser(username) { try { const blockedUsers = getBlockedUsers(); const newBlockedUsers = blockedUsers.filter(user => user !== username); if (saveBlockedUsers(newBlockedUsers)) { console.log(`已删除屏蔽用户: @${username}`); // 刷新页面状态 processTweets(); // 重新显示用户列表(会自动关闭旧的模态框) showBlockedUsersList(); } else { console.error(`删除屏蔽用户失败: @${username}`); alert('删除用户失败,请重试'); } } catch (error) { console.error('删除屏蔽用户时发生错误:', error); alert('删除用户时发生错误,请重试'); } } // 关闭所有已存在的模态框 function closeAllModals() { const existingModals = document.querySelectorAll('.blocked-users-modal'); existingModals.forEach(modal => { if (modal && modal.parentNode) { modal.parentNode.removeChild(modal); } }); } // 显示屏蔽用户列表(带删除按钮) function showBlockedUsersList() { // 先关闭已存在的模态框 closeAllModals(); const blockedUsers = getBlockedUsers(); if (blockedUsers.length === 0) { alert('当前没有屏蔽任何用户'); return; } // 创建模态对话框 const modal = document.createElement('div'); modal.className = 'blocked-users-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 10000; display: flex; align-items: center; justify-content: center; `; const content = document.createElement('div'); content.style.cssText = ` background: white; border-radius: 12px; padding: 20px; max-width: 500px; max-height: 80vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); `; const title = document.createElement('h3'); title.textContent = `已屏蔽的用户 (${blockedUsers.length})`; title.style.cssText = ` margin: 0 0 20px 0; color: #333; font-size: 18px; font-weight: bold; `; const userList = document.createElement('div'); userList.style.cssText = ` margin-bottom: 20px; `; blockedUsers.forEach((username, index) => { const userItem = document.createElement('div'); userItem.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 10px; margin: 5px 0; background: #f8f9fa; border-radius: 6px; border: 1px solid #e9ecef; `; const usernameSpan = document.createElement('span'); usernameSpan.textContent = `@${username}`; usernameSpan.style.cssText = ` font-weight: 500; color: #333; `; const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除'; deleteBtn.style.cssText = ` background: #dc3545; color: white; border: none; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 12px; transition: background 0.2s; `; deleteBtn.onmouseover = () => deleteBtn.style.background = '#c82333'; deleteBtn.onmouseout = () => deleteBtn.style.background = '#dc3545'; deleteBtn.onclick = () => { if (confirm(`确定要取消屏蔽用户 @${username} 吗?`)) { removeBlockedUser(username); } }; userItem.appendChild(usernameSpan); userItem.appendChild(deleteBtn); userList.appendChild(userItem); }); const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; gap: 10px; justify-content: center; `; const exportBtn = document.createElement('button'); exportBtn.textContent = '导出列表'; exportBtn.style.cssText = ` background: #007bff; color: white; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer; font-size: 14px; transition: background 0.2s; `; exportBtn.onmouseover = () => exportBtn.style.background = '#0056b3'; exportBtn.onmouseout = () => exportBtn.style.background = '#007bff'; exportBtn.onclick = exportBlockedUsers; const importBtn = document.createElement('button'); importBtn.textContent = '导入列表'; importBtn.style.cssText = ` background: #28a745; color: white; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer; font-size: 14px; transition: background 0.2s; `; importBtn.onmouseover = () => importBtn.style.background = '#1e7e34'; importBtn.onmouseout = () => importBtn.style.background = '#28a745'; importBtn.onclick = importBlockedUsers; const closeBtn = document.createElement('button'); closeBtn.textContent = '关闭'; closeBtn.style.cssText = ` background: #6c757d; color: white; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer; font-size: 14px; transition: background 0.2s; `; closeBtn.onmouseover = () => closeBtn.style.background = '#5a6268'; closeBtn.onmouseout = () => closeBtn.style.background = '#6c757d'; closeBtn.onclick = closeAllModals; buttonContainer.appendChild(exportBtn); buttonContainer.appendChild(importBtn); buttonContainer.appendChild(closeBtn); content.appendChild(title); content.appendChild(userList); content.appendChild(buttonContainer); modal.appendChild(content); // 点击背景关闭模态框 modal.onclick = (e) => { if (e.target === modal) { closeAllModals(); } }; document.body.appendChild(modal); } // 注册油猴菜单命令 if (typeof GM_registerMenuCommand !== 'undefined') { GM_registerMenuCommand('显示所有被屏蔽用户', () => { showBlockedUsersList(); }, 's'); GM_registerMenuCommand('导出屏蔽用户列表', () => { exportBlockedUsers(); }, 'e'); GM_registerMenuCommand('导入屏蔽用户列表', () => { importBlockedUsers(); }, 'i'); GM_registerMenuCommand('刷新页面状态', () => { processTweets(); console.log('已刷新页面状态'); }, 'r'); GM_registerMenuCommand('调试缓存状态', () => { const blockedUsers = getBlockedUsers(); console.log('=== 缓存状态调试 ==='); console.log('当前屏蔽用户数量:', blockedUsers.length); console.log('用户列表:', blockedUsers); console.log('缓存键名: blockedUsers'); console.log('=================='); }, 'd'); } // 启动脚本 init(); })();