您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Aggressively blocks Torn Chat with a visual toggle switch and per-page always-block via long press.
// ==UserScript== // @name Torn Chat Blocker // @namespace https://greasyfork.org/en/users/1431907-theeeunknown // @version 1.2 // @description Aggressively blocks Torn Chat with a visual toggle switch and per-page always-block via long press. // @author TR0LL // @license MIT // @match https://www.torn.com/* // @grant GM_addStyle // @grant unsafeWindow // @run-at document-start // ==/UserScript== (function() { 'use strict'; // --- Configuration --- const DEBUG_MODE = false; // Set to true for detailed console logging, false for production const CHAT_URL_PATTERNS_TO_BLOCK = [ // General chat patterns /https:\/\/www\.torn\.com\/chat/, /https:\/\/www\.torn\.com\/builds\/chat\//, /chat-worker\.js/, /fetch-worker\.js/, /https:\/\/www\.torn\.com\/js\/chat/, // Specific script files /https:\/\/www\.torn\.com\/builds\/chat\/app\.[a-f0-9]+\.js/, /https:\/\/www\.torn\.com\/builds\/chat\/vendors\.[a-f0-9]+\.js/, /https:\/\/www\.torn\.com\/builds\/chat\/runtime\.[a-f0-9]+\.js/, // Specific CSS files /https:\/\/www\.torn\.com\/builds\/chat\/app\.[a-f0-9]+\.css/, /https:\/\/www\.torn\.com\/builds\/chat\/vendors\.[a-f0-9]+\.css/, // Ultra-aggressive chat asset blocking /torn\.com\/builds\/chat\/.*\.js/, /torn\.com\/builds\/chat\/.*\.css/, /torn\.com\/builds\/chat\/.*\.json/, /torn\.com\/builds\/chat\/.*\.png/, /torn\.com\/builds\/chat\/.*\.jpg/, /torn\.com\/builds\/chat\/.*\.svg/, /torn\.com\/builds\/chat\/.*\.woff/, /torn\.com\/builds\/chat\/.*\.mp3/, // Sendbird related patterns /sendbird\.(com|io)/, /api\.sendbird\.com/, /ws\.sendbird\.com/, /sb\.scorpion\.io/, /^\wss?:\/\/.*sendbird/, /.*\.sendbird\..*/i, // Broader chat patterns /torn\.com\/.*chat/i, /torn\.com\/.*sendbird/i, // Added rule for profile-mini (Example, adjust as needed) /https:\/\/www\.torn\.com\/builds\/profile-mini\// ]; const TOGGLE_BUTTON_ID = 'torn-chat-blocker-toggle'; const LOCAL_STORAGE_KEY_GLOBAL_BLOCK = 'tornChatBlockingEnabled'; const LOCAL_STORAGE_KEY_ALWAYS_BLOCK_PAGES = 'tornChatAlwaysBlockedPages'; const DEBOUNCE_DELAY = 250; const LONG_PRESS_DURATION = 750; // ms // --- Global State --- let isBlockingEnabled = localStorage.getItem(LOCAL_STORAGE_KEY_GLOBAL_BLOCK) !== 'false'; // Defaults to true let alwaysBlockedPages = new Set(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY_ALWAYS_BLOCK_PAGES) || '[]')); let domObserver = null; let longPressTimer = null; let isLongPressFlag = false; // Distinguish long press from click // --- Logging Utility --- function log(...args) { if (DEBUG_MODE) { console.log('Torn Chat Blocker:', ...args); } } function warn(...args) { if (DEBUG_MODE) { console.warn('Torn Chat Blocker:', ...args); } } // --- CSS for the toggle button (Switch Style) --- GM_addStyle(` #${TOGGLE_BUTTON_ID} { position: fixed; top: 10px; right: 10px; z-index: 9999; width: 50px; height: 26px; border-radius: 13px; cursor: pointer; transition: background-color 0.3s ease, border-color 0.3s ease; box-shadow: 0 1px 3px rgba(0,0,0,0.1), inset 0 1px 1px rgba(0,0,0,0.1); box-sizing: border-box; -webkit-appearance: none; -moz-appearance: none; appearance: none; padding: 0; font-size: 0; line-height: 0; color: transparent; /* Default background/border will be set by specific classes */ } #${TOGGLE_BUTTON_ID}::after { /* The slider knob */ content: ""; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background-color: white; border-radius: 50%; box-shadow: 0 1px 3px rgba(0,0,0,0.4); transition: transform 0.3s ease; box-sizing: border-box; } /* State when global blocking is ON (GREEN) */ #${TOGGLE_BUTTON_ID}.blocking-on { background-color: #388e3c; /* Green */ border: 1px solid #2e7d32; /* Darker green */ } #${TOGGLE_BUTTON_ID}.blocking-on::after { transform: translateX(24px); /* Knob right */ } /* State when global blocking is OFF (RED) */ #${TOGGLE_BUTTON_ID}.blocking-off { background-color: #d32f2f; /* Red */ border: 1px solid #c62828; /* Darker red */ } #${TOGGLE_BUTTON_ID}.blocking-off::after { transform: translateX(0); /* Knob left */ } /* State when current page is ALWAYS BLOCKED (ORANGE) */ #${TOGGLE_BUTTON_ID}.blocking-always { background-color: #FFA500; /* Orange */ border: 1px solid #E69500; /* Darker orange */ } #${TOGGLE_BUTTON_ID}.blocking-always::after { transform: translateX(24px); /* Knob right (appears "ON") */ } #${TOGGLE_BUTTON_ID}:hover { filter: brightness(1.1); } `); // --- Core Blocking Logic --- function isCurrentPageAlwaysBlocked() { return alwaysBlockedPages.has(window.location.href); } // Determines if chat/sendbird assets should be blocked for the given requestUrl function shouldBlockUrl(requestUrl) { const pageIsAlwaysBlocked = isCurrentPageAlwaysBlocked(); const effectiveBlockingActive = isBlockingEnabled || pageIsAlwaysBlocked; if (!effectiveBlockingActive) { return false; // Neither global nor page-specific override says block } // If blocking is active (globally or for this page), check patterns if (requestUrl && typeof requestUrl === 'string') { for (const pattern of CHAT_URL_PATTERNS_TO_BLOCK) { if (pattern.test(requestUrl)) { log(`Blocking request to ${requestUrl} by pattern ${pattern} (Global: ${isBlockingEnabled}, PageAlwaysBlocked: ${pageIsAlwaysBlocked})`); return true; } } // Fallback keyword check if (requestUrl.includes('chat') || requestUrl.includes('sendbird')) { if (!CHAT_URL_PATTERNS_TO_BLOCK.some(p => p.test(requestUrl))) { // Log only if not caught by a specific pattern log(`Blocking request to ${requestUrl} by keyword fallback (Global: ${isBlockingEnabled}, PageAlwaysBlocked: ${pageIsAlwaysBlocked})`); } return true; } } return false; } // Determines if DOM elements related to chat should be hidden on the current page function pageShouldHaveElementsHidden() { return isBlockingEnabled || isCurrentPageAlwaysBlocked(); } // --- Request Interception --- const originalFetch = unsafeWindow.fetch; const originalXHRopen = unsafeWindow.XMLHttpRequest.prototype.open; const originalWebSocket = unsafeWindow.WebSocket; unsafeWindow.fetch = function(...args) { const requestInfo = args[0]; const url = (typeof requestInfo === 'string') ? requestInfo : requestInfo.url; if (shouldBlockUrl(url)) { log(`Blocking fetch request to ${url}`); return Promise.reject(new Error(`Torn Chat Blocker: Request to ${url} blocked`)); } return originalFetch.apply(unsafeWindow, args); }; unsafeWindow.XMLHttpRequest.prototype.open = function(...args) { const method = args[0]; const url = args[1]; if (shouldBlockUrl(url)) { log(`Preparing to block XHR ${method} request to ${url}`); this._blockedUrl = url; this._isBlockedByScript = true; } else { this._isBlockedByScript = false; } return originalXHRopen.apply(this, args); }; const originalXHRSend = unsafeWindow.XMLHttpRequest.prototype.send; unsafeWindow.XMLHttpRequest.prototype.send = function(...args) { if (this._isBlockedByScript && this._blockedUrl) { log(`Preventing XHR send for ${this._blockedUrl}`); const xhrInstance = this; setTimeout(() => { const errorEvent = new ProgressEvent('error'); if (typeof xhrInstance.onerror === 'function') xhrInstance.onerror(errorEvent); if (typeof xhrInstance.onloadend === 'function') xhrInstance.onloadend(errorEvent); try { xhrInstance.dispatchEvent(new Event('error')); xhrInstance.dispatchEvent(new Event('loadend')); } catch (e) { warn('Error dispatching events on blocked XHR:', e); } }, 10); return; } return originalXHRSend.apply(this, args); }; unsafeWindow.WebSocket = function(url, protocols) { if (shouldBlockUrl(url)) { log('Blocking WebSocket connection to', url); const fakeWS = { url: url, protocol: protocols && protocols.length > 0 ? protocols[0] : '', readyState: 3, bufferedAmount: 0, extensions: '', binaryType: 'blob', CONNECTING: 0, OPEN: 1, CLOSING: 2, CLOSED: 3, send: function() { log('Fake WebSocket: send called on blocked WS'); return false; }, close: function(code, reason) { log('Fake WebSocket: close called on blocked WS', code, reason); this.readyState = this.CLOSING; setTimeout(() => { this.readyState = this.CLOSED; const closeEvent = new CloseEvent('close', { code: code || 1006, reason: reason || "Connection blocked", wasClean: false }); if (typeof this.onclose === 'function') this.onclose(closeEvent); try { this.dispatchEvent(closeEvent); } catch(e) {warn('Error dispatching close on fake WS', e);} }, 0); }, onopen: null, onclose: null, onerror: null, onmessage: null, _listeners: {}, addEventListener: function(type, listener) { if (!this._listeners[type]) this._listeners[type] = []; this._listeners[type].push(listener); }, removeEventListener: function(type, listener) { if (!this._listeners[type]) return; this._listeners[type] = this._listeners[type].filter(l => l !== listener); }, dispatchEvent: function(event) { if (!this._listeners[event.type]) return true; this._listeners[event.type].forEach(cb => (typeof cb === 'function' ? cb.call(this, event) : cb.handleEvent(event))); return !event.defaultPrevented; } }; setTimeout(() => { const errorEvent = new Event('error'); if (typeof fakeWS.onerror === 'function') fakeWS.onerror(errorEvent); try { fakeWS.dispatchEvent(errorEvent); } catch(e) {warn('Error dispatching error on fake WS', e);} const closeEvent = new CloseEvent('close', { code: 1006, reason: "WebSocket blocked by script", wasClean: false }); if (typeof fakeWS.onclose === 'function') fakeWS.onclose(closeEvent); try { fakeWS.dispatchEvent(closeEvent); } catch(e) {warn('Error dispatching close on fake WS', e);} fakeWS.readyState = fakeWS.CLOSED; }, 5); return fakeWS; } const wsInstance = new originalWebSocket(url, protocols); return wsInstance; }; // Ensure prototype and static constants are correctly set up if needed by Torn's code if (originalWebSocket) { unsafeWindow.WebSocket.prototype = originalWebSocket.prototype; unsafeWindow.WebSocket.CONNECTING = originalWebSocket.CONNECTING; unsafeWindow.WebSocket.OPEN = originalWebSocket.OPEN; unsafeWindow.WebSocket.CLOSING = originalWebSocket.CLOSING; unsafeWindow.WebSocket.CLOSED = originalWebSocket.CLOSED; } // --- UI Toggle Button --- function updateToggleButton() { const button = document.getElementById(TOGGLE_BUTTON_ID); if (!button) return; button.classList.remove('blocking-on', 'blocking-off', 'blocking-always'); let ariaLabel = ''; if (isCurrentPageAlwaysBlocked()) { button.classList.add('blocking-always'); // Orange ariaLabel = 'Page Always Blocked. Click to remove from always-block list.'; button.setAttribute('aria-checked', 'true'); // Visually "on" } else if (isBlockingEnabled) { button.classList.add('blocking-on'); // Green ariaLabel = 'Global Chat Blocking: ON. Click to turn OFF. Long press to always-block this page.'; button.setAttribute('aria-checked', 'true'); } else { button.classList.add('blocking-off'); // Red ariaLabel = 'Global Chat Blocking: OFF. Click to turn ON. Long press to always-block this page.'; button.setAttribute('aria-checked', 'false'); } button.setAttribute('aria-label', ariaLabel); button.setAttribute('role', 'switch'); } function showNotification(message, backgroundColor) { const notification = document.createElement('div'); notification.textContent = message; Object.assign(notification.style, { position: 'fixed', bottom: '20px', left: '50%', transform: 'translateX(-50%)', backgroundColor: backgroundColor, color: 'white', padding: '10px 20px', borderRadius: '5px', zIndex: '10000', boxShadow: '0 2px 10px rgba(0,0,0,0.2)' }); document.body.appendChild(notification); setTimeout(() => notification.remove(), 3500); } function addToggleButton() { if (document.getElementById(TOGGLE_BUTTON_ID)) return; const button = document.createElement('button'); button.id = TOGGLE_BUTTON_ID; const handleLongPress = () => { isLongPressFlag = true; // Set flag const currentPageUrl = window.location.href; if (!alwaysBlockedPages.has(currentPageUrl)) { alwaysBlockedPages.add(currentPageUrl); localStorage.setItem(LOCAL_STORAGE_KEY_ALWAYS_BLOCK_PAGES, JSON.stringify(Array.from(alwaysBlockedPages))); log(`Long press: Added ${currentPageUrl} to always-block list.`); showNotification(`Page added to always-block list. Refresh for full effect.`, '#FFA500'); // Orange notification updateToggleButton(); if (pageShouldHaveElementsHidden()) { // Check if blocking should now be active blockChatElementsInDOM(); // Ensure DOM observer is active } } else { log(`Long press: ${currentPageUrl} is already always-blocked.`); // Optional: showNotification(`${currentPageUrl} is already always-blocked.`, '#FFA500'); } }; const clearLongPressTimer = () => { if (longPressTimer) clearTimeout(longPressTimer); longPressTimer = null; }; // Mouse events button.addEventListener('mousedown', (e) => { if (e.button !== 0) return; // Only left click isLongPressFlag = false; // Reset flag clearLongPressTimer(); longPressTimer = setTimeout(handleLongPress, LONG_PRESS_DURATION); }); button.addEventListener('mouseup', (e) => { if (e.button !== 0) return; clearLongPressTimer(); // Click event will handle logic if not a long press }); button.addEventListener('mouseleave', clearLongPressTimer); // Touch events button.addEventListener('touchstart', (e) => { isLongPressFlag = false; // Reset flag clearLongPressTimer(); longPressTimer = setTimeout(handleLongPress, LONG_PRESS_DURATION); // e.preventDefault(); // Could prevent scroll, be cautious }, { passive: true }); // Passive if not preventing default button.addEventListener('touchend', (e) => { clearLongPressTimer(); if (isLongPressFlag) { e.preventDefault(); // Prevent click event firing after a long touch } // Click event will handle logic if not a long press }); button.addEventListener('touchcancel', clearLongPressTimer); button.addEventListener('click', (e) => { if (isLongPressFlag) { // If flag is set, it was a long press; reset and ignore click isLongPressFlag = false; e.stopImmediatePropagation(); // Prevent other click listeners if any return; } const currentPageUrl = window.location.href; if (isCurrentPageAlwaysBlocked()) { // Click on ORANGE switch: Remove from always-block alwaysBlockedPages.delete(currentPageUrl); localStorage.setItem(LOCAL_STORAGE_KEY_ALWAYS_BLOCK_PAGES, JSON.stringify(Array.from(alwaysBlockedPages))); log(`Clicked to unblock always-blocked page: ${currentPageUrl}`); showNotification(`Page removed from always-block list. Refresh for full effect.`, isBlockingEnabled ? '#388e3c' : '#d32f2f'); updateToggleButton(); if (!pageShouldHaveElementsHidden() && domObserver) { domObserver.disconnect(); log('DOM Observer disconnected as page is no longer effectively blocked.'); } } else { // Click on GREEN/RED switch: Toggle global blocking isBlockingEnabled = !isBlockingEnabled; localStorage.setItem(LOCAL_STORAGE_KEY_GLOBAL_BLOCK, isBlockingEnabled.toString()); log(`Toggled Global Resource Blocking: ${isBlockingEnabled ? 'ON' : 'OFF'}`); showNotification(`Global Resource Blocking ${isBlockingEnabled ? 'enabled' : 'disabled'}. Refresh for full effect.`, isBlockingEnabled ? '#388e3c' : '#d32f2f'); updateToggleButton(); if (pageShouldHaveElementsHidden()) { blockChatElementsInDOM(); } else { if (domObserver) domObserver.disconnect(); log("Global Resource Blocking OFF and page not always-blocked. DOM Observer potentially stopped. Refresh to restore elements."); } } }); if (document.body) { document.body.appendChild(button); } else { window.addEventListener('DOMContentLoaded', () => { if (document.body) document.body.appendChild(button); }); } updateToggleButton(); // Initialize button state } // --- Debounce Utility --- function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // --- Block elements in DOM (Primarily for chat UI) --- const chatSelectors = [ 'div[id*="chat"]', 'div[class*="chat"]', 'div[id*="sendbird"]', 'div[class*="sendbird"]', 'iframe[src*="chat"]', 'iframe[src*="sendbird"]', '#chatRoot', '.chat-box', '.chat-container', '.chat-wrapper', '*[id*="chat-"]', '*[class*="chat-"]', '*[id*="-chat"]', '*[class*="-chat"]', '*[id*="sendbird-"]', '*[class*="sendbird-"]', 'div[aria-label*="chat" i]', 'section[aria-label*="chat" i]' ]; function hideMatchedElements() { if (!pageShouldHaveElementsHidden()) { log('DOM element hiding is OFF for this page. Previously hidden elements will remain hidden until refresh.'); // We don't attempt to unhide, refresh is cleaner. return; } log('Scanning and hiding chat-related DOM elements...'); chatSelectors.forEach(selector => { try { document.querySelectorAll(selector).forEach(el => { if (el.id === TOGGLE_BUTTON_ID || el.closest(`#${TOGGLE_BUTTON_ID}`)) return; if (el.style.display !== 'none') { log('Hiding DOM element matching selector:', selector, el); el.style.setProperty('display', 'none', 'important'); el.style.setProperty('visibility', 'hidden', 'important'); el.style.setProperty('opacity', '0', 'important'); el.style.setProperty('pointer-events', 'none', 'important'); el.dataset.tornChatBlocked = 'true'; } }); } catch (e) { warn('Error applying selector:', selector, e.message); } }); } const debouncedHideMatchedElements = debounce(hideMatchedElements, DEBOUNCE_DELAY); function blockChatElementsInDOM() { if (!pageShouldHaveElementsHidden()) { if (domObserver) { domObserver.disconnect(); log('DOM Observer disconnected as blocking is not active for this page.'); } return; } log('Actively scanning/hiding chat DOM elements. Ensuring DOM observer is running.'); hideMatchedElements(); // Initial scan if (!domObserver || !domObserver.takeRecords().length) { // Check if observer exists and is active domObserver = new MutationObserver((mutations) => { if (!pageShouldHaveElementsHidden()) { // Re-check condition within observer callback if (domObserver) domObserver.disconnect(); log('DOM Observer disconnected from within callback.'); return; } let needsRescan = false; for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.id === TOGGLE_BUTTON_ID) continue; if (chatSelectors.some(sel => node.matches && node.matches(sel)) || (node.querySelector && node.querySelector(chatSelectors.join(',')))) { needsRescan = true; break; } } } } else if (mutation.type === 'attributes') { if (mutation.target.nodeType === Node.ELEMENT_NODE && chatSelectors.some(sel => mutation.target.matches && mutation.target.matches(sel))) { needsRescan = true; } } if (needsRescan) break; } if (needsRescan) { log('DOM mutation detected, queueing re-hide for chat elements.'); debouncedHideMatchedElements(); } }); const observeTarget = document.body || document.documentElement; if (observeTarget) { domObserver.observe(observeTarget, { childList: true, subtree: true, attributes: true }); log('DOM Observer started for chat elements.'); } else { // Fallback if body not ready, though @run-at document-start + DOMContentLoaded should handle most window.addEventListener('DOMContentLoaded', () => { const target = document.body || document.documentElement; if (target && pageShouldHaveElementsHidden()) { // Check again before observing domObserver.observe(target, { childList: true, subtree: true, attributes: true }); log('DOM Observer started after DOMContentLoaded for chat elements.'); } else if (!target) { warn("Failed to start DOM Observer: No body or documentElement found post-DOMContentLoaded."); } }); } } else { log('DOM Observer already running.'); } } // --- Initialization --- function initialize() { log('Initializing Torn Chat Blocker...'); addToggleButton(); // This will also call updateToggleButton if (pageShouldHaveElementsHidden()) { log('Initial state requires blocking. Activating DOM blocking.'); blockChatElementsInDOM(); } else { log('Initial state does not require blocking.'); } // Attempt to nullify global chat-related variables // This runs slightly after script start to catch variables defined by Torn's early scripts setTimeout(() => { if (pageShouldHaveElementsHidden()) { // Only nuke if blocking is active try { const targetVars = ['chat', 'Chat', 'SendBird', 'sendbird', 'sb', '_sendbird', 'SENDBIRD']; targetVars.forEach(v => { if (typeof unsafeWindow[v] !== 'undefined' && unsafeWindow[v] !== null) { // Check if not already null log(`Attempting to nullify unsafeWindow.${v}`); try { Object.defineProperty(unsafeWindow, v, { value: null, writable: false, configurable: false }); } catch (e) { warn(`Failed to Object.defineProperty ${v}, falling back to simple null. Error: ${e.message}`); try { unsafeWindow[v] = null; } catch (e2) { warn(`Failed to even assign null to unsafeWindow.${v}. Error: ${e2.message}`); } } } }); } catch (e) { warn('Error while trying to nuke JS variables.', e.message); } } }, 200); // Increased delay slightly } if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } })();