Instantly share code, notes, and snippets.
Last active
March 27, 2025 19:12
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save swayson/385e843349576d70979f0541146494a2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// == Enhanced Blocker Script v2 == | |
// Purpose: Automates blocking users on a target page (designed for X/Twitter search results) | |
// Features: Start, Pause, Resume, Stop controls; Enhanced reliability; Status feedback; Rate limit mitigation (delay+jitter). | |
// Usage: Paste into Firefox Web Developer Tools console on the relevant page. | |
(function() { | |
// --- Configuration (First Principles: Define fundamental parameters clearly) --- | |
const config = { | |
selectors: { | |
itemContainer: '[role="article"]', | |
moreButton: '[aria-label="More"]', // Check label accuracy | |
blockOption: '[data-testid="block"]', | |
confirmButton: '[data-testid="confirmationSheetConfirm"]', | |
}, | |
timeouts: { | |
uiElementWait: 5000, | |
// Base delay (ms) AFTER successfully blocking one user, BEFORE starting the next. | |
baseDelayBetweenBlocks: 1500, // Principle: Slow down proactively. | |
// Max additional random delay (ms) to add to the base delay. | |
jitterAmount: 1000, // Principle: Introduce variability. | |
pauseCheckInterval: 500, | |
}, | |
processedAttribute: 'data-enhanced-blocker-processed', | |
processingAttribute: 'data-enhanced-blocker-processing', | |
maxBlocks: 0, | |
controllerId: 'enhanced-blocker-controller', | |
}; | |
// --- State Variables (First Principle: Explicit State Management) --- | |
let isRunning = false; | |
let isPaused = false; | |
let blockCount = 0; | |
let controllerDiv = null; | |
let abortController = null; | |
// --- Helper Functions --- | |
/** | |
* Waits for a specific element, interruptible by signal. | |
* Principle: Dynamic Waiting, Verification before Action, Graceful Error Handling. | |
* (No changes from previous version) | |
*/ | |
function waitForElement(selector, signal, timeout = config.timeouts.uiElementWait) { | |
return new Promise((resolve, reject) => { | |
const existingElement = document.querySelector(selector); | |
if (existingElement) { resolve(existingElement); return; } | |
let observer; | |
let timer; | |
const cleanup = () => { | |
if (observer) observer.disconnect(); | |
if (timer) clearTimeout(timer); | |
signal?.removeEventListener('abort', handleAbort); // Use optional chaining | |
}; | |
const handleAbort = () => { | |
cleanup(); | |
reject(new DOMException('Aborted by user', 'AbortError')); | |
}; | |
if (signal) { | |
if (signal.aborted) return reject(new DOMException('Aborted by user', 'AbortError')); | |
signal.addEventListener('abort', handleAbort, { once: true }); | |
} else { | |
// If no signal provided, create a dummy one that never aborts, simplifies logic below | |
// This shouldn't happen with how it's called, but defensive coding. | |
console.warn("[Blocker] waitForElement called without an AbortSignal."); | |
} | |
timer = setTimeout(() => { | |
cleanup(); | |
console.warn(`[Blocker] Timeout waiting for element: ${selector}`); | |
reject(new Error(`Timeout waiting for element: ${selector}`)); | |
}, timeout); | |
observer = new MutationObserver(() => { | |
const targetElement = document.querySelector(selector); | |
if (targetElement) { cleanup(); resolve(targetElement); } | |
}); | |
observer.observe(document.body, { childList: true, subtree: true }); | |
}); | |
} | |
/** | |
* Creates an interruptible delay using setTimeout. | |
* Principle: Graceful Handling (interruptibility), Encapsulation. | |
* @param {number} durationMs - The delay duration. | |
* @param {AbortSignal} signal - The signal to listen for abort requests. | |
* @returns {Promise<void>} Resolves after the delay, rejects if aborted. | |
*/ | |
function interruptibleDelay(durationMs, signal) { | |
return new Promise((resolve, reject) => { | |
if (signal?.aborted) { | |
return reject(new DOMException('Aborted before delay start', 'AbortError')); | |
} | |
const handleAbort = () => { | |
clearTimeout(timer); | |
reject(new DOMException('Aborted during delay', 'AbortError')); | |
}; | |
const timer = setTimeout(() => { | |
signal?.removeEventListener('abort', handleAbort); | |
resolve(); | |
}, durationMs); | |
signal?.addEventListener('abort', handleAbort, { once: true }); | |
}); | |
} | |
/** | |
* Logs status messages. | |
* Principle: Clear Feedback. | |
* (No changes from previous version) | |
*/ | |
function logStatus(message) { | |
console.log(`[Blocker] ${message}`); | |
if (controllerDiv) { | |
const statusEl = controllerDiv.querySelector('.blocker-status'); | |
if (statusEl) { statusEl.textContent = `Status: ${message}`; } | |
} | |
updateButtonStates(); | |
} | |
/** | |
* Updates UI button states based on script state. | |
* Principle: Clear Feedback, User Control. | |
* (No changes from previous version) | |
*/ | |
function updateButtonStates() { | |
if (!controllerDiv) return; | |
const startBtn = controllerDiv.querySelector('.blocker-start'); | |
const pauseBtn = controllerDiv.querySelector('.blocker-pause'); | |
const resumeBtn = controllerDiv.querySelector('.blocker-resume'); | |
const stopBtn = controllerDiv.querySelector('.blocker-stop'); | |
startBtn.disabled = isRunning; | |
pauseBtn.disabled = !isRunning || isPaused; | |
resumeBtn.disabled = !isRunning || !isPaused; | |
stopBtn.disabled = !isRunning; | |
startBtn.textContent = isRunning && isPaused ? "Resume" : "Start"; | |
startBtn.onclick = (isRunning && isPaused) ? resumeBlocker : runBlocker; | |
} | |
// --- Core Logic --- | |
/** | |
* Performs the block action sequence for a single element. | |
* Principle: Encapsulation, Error Handling, State Tracking. | |
* (Removed postBlockDelay from here, moved delay logic to main loop) | |
*/ | |
async function processBlock(itemElement, signal) { | |
itemElement.setAttribute(config.processingAttribute, 'true'); | |
const itemIdentifier = itemElement.querySelector('a[href*="/status/"]')?.href || // Try tweet link | |
itemElement.querySelector('a[href^="/"] [dir="ltr"]')?.textContent || // Try @handle | |
'item'; // Fallback | |
logStatus(`Processing ${itemIdentifier}...`); | |
try { | |
const moreButton = itemElement.querySelector(config.selectors.moreButton); | |
if (!moreButton) throw new Error(`Could not find 'More' button.`); | |
moreButton.click(); | |
const blockOption = await waitForElement(config.selectors.blockOption, signal); | |
blockOption.click(); | |
const confirmButton = await waitForElement(config.selectors.confirmButton, signal); | |
confirmButton.click(); | |
// Wait briefly for the confirmation dialog to disappear reliably. Use interruptibleDelay. | |
// This is NOT the main rate limit delay, just ensuring the dialog closes. | |
await interruptibleDelay(300, signal); | |
itemElement.setAttribute(config.processedAttribute, 'true'); | |
blockCount++; | |
logStatus(`Successfully blocked user #${blockCount} (${itemIdentifier}).`); | |
return true; | |
} catch (error) { | |
if (error.name === 'AbortError') { | |
logStatus(`Block action aborted for ${itemIdentifier}.`); | |
} else { | |
logStatus(`Error processing ${itemIdentifier}: ${error.message}. Skipping.`); | |
console.error('[Blocker] Detailed Error:', error); | |
itemElement.setAttribute(config.processedAttribute, 'failed'); | |
} | |
try { document.body.click(); } catch (e) {} // Attempt to close menus | |
return false; | |
} finally { | |
itemElement.removeAttribute(config.processingAttribute); | |
} | |
} | |
/** | |
* Main execution loop. Includes pause/resume, stop, item finding, processing, and DELAY logic. | |
* Principle: Main control flow, State checks, Action integration, Rate limit mitigation. | |
*/ | |
async function runBlocker() { | |
if (isRunning && !isPaused) { logStatus("Already running."); return; } | |
if(isPaused) { resumeBlocker(); return; } | |
isRunning = true; | |
isPaused = false; | |
// blockCount = 0; // Reset count only on a fresh start? Or keep accumulating? User decision - let's keep accumulating for now. | |
if (blockCount === 0) logStatus("Starting fresh run..."); // Log start only if count is 0 | |
// Ensure a fresh AbortController for each run | |
if (abortController && !abortController.signal.aborted) { | |
abortController.abort(); // Abort any previous controller just in case | |
} | |
abortController = new AbortController(); | |
const signal = abortController.signal; | |
logStatus("Running..."); | |
while (isRunning) { | |
try { | |
if (signal.aborted) { logStatus("Stop signal received, exiting."); break; } | |
if (isPaused) { | |
logStatus("Paused. Waiting..."); | |
await interruptibleDelay(config.timeouts.pauseCheckInterval, signal); // Wait interruptibly | |
continue; // Go back to loop start to re-evaluate state | |
} | |
if (config.maxBlocks > 0 && blockCount >= config.maxBlocks) { | |
logStatus(`Reached max blocks limit (${config.maxBlocks}). Stopping.`); | |
stopBlocker(); | |
break; | |
} | |
const nextItem = document.querySelector( | |
`${config.selectors.itemContainer}:not([${config.processedAttribute}]):not([${config.processingAttribute}])` | |
); | |
if (!nextItem) { | |
logStatus("No more unprocessed items found. Scroll down or Stop."); | |
pauseBlocker(); // Pause to allow user action | |
continue; | |
} | |
// Process the item | |
const success = await processBlock(nextItem, signal); | |
// --- Rate Limiting Delay --- | |
if (success && isRunning && !isPaused) { // Only delay if successful and still running/not paused | |
// Calculate delay (Base + Random Jitter) | |
const delay = Math.floor(config.timeouts.baseDelayBetweenBlocks + Math.random() * config.timeouts.jitterAmount); | |
logStatus(`Applying delay of ${delay}ms...`); | |
try { | |
await interruptibleDelay(delay, signal); | |
} catch (delayError) { | |
if (delayError.name === 'AbortError') { | |
logStatus("Stop signal received during delay, exiting."); | |
break; // Exit the main while loop | |
} else { | |
throw delayError; // Rethrow unexpected errors | |
} | |
} | |
} | |
// --- End Rate Limiting Delay --- | |
} catch (error) { | |
if (error.name === 'AbortError') { | |
logStatus("Loop aborted."); | |
break; // Exit loop cleanly on abort | |
} else { | |
logStatus(`Unexpected error in main loop: ${error.message}. Attempting to continue...`); | |
console.error("[Blocker] Main loop error:", error); | |
// Add a small safety delay to prevent rapid error loops on unexpected issues | |
try { await interruptibleDelay(1000, signal); } catch (e) { if (e.name === 'AbortError') break; } | |
} | |
} | |
} // end while(isRunning) | |
// Cleanup after loop exit | |
isRunning = false; | |
isPaused = false; | |
logStatus("Blocker stopped."); | |
// Abort controller might already be aborted if stop was called, but ensure it happens. | |
if (abortController && !abortController.signal.aborted) { | |
abortController.abort(); | |
} | |
abortController = null; | |
updateButtonStates(); // Final UI state update | |
} | |
function pauseBlocker() { | |
if (!isRunning || isPaused) return; | |
isPaused = true; | |
logStatus("Pausing..."); | |
} | |
function resumeBlocker() { | |
if (!isRunning || !isPaused) return; | |
isPaused = false; | |
logStatus("Resuming..."); | |
} | |
function stopBlocker() { | |
if (!isRunning) return; | |
logStatus("Stopping..."); // Log immediately | |
isRunning = false; // Signal loop termination | |
isPaused = false; | |
if (abortController) { | |
abortController.abort(); // Trigger abortion for waits/delays | |
} | |
// Loop exit and final status update happen within runBlocker | |
} | |
// --- UI Setup --- | |
function createControllerUI() { | |
if (document.getElementById(config.controllerId)) { | |
// If UI exists, ensure state variables match UI reality (e.g., after page refresh) | |
isRunning = false; | |
isPaused = false; | |
controllerDiv = document.getElementById(config.controllerId); | |
updateButtonStates(); | |
logStatus("Script re-initialized. UI found."); | |
return; // Don't recreate | |
} | |
controllerDiv = document.createElement('div'); | |
controllerDiv.id = config.controllerId; | |
Object.assign(controllerDiv.style, { | |
position: 'fixed', top: '10px', right: '10px', backgroundColor: 'rgba(0, 0, 0, 0.8)', | |
color: 'white', padding: '15px', borderRadius: '8px', zIndex: '9999', | |
fontFamily: 'Arial, sans-serif', fontSize: '12px', border: '1px solid white', minWidth: '150px' | |
}); | |
controllerDiv.innerHTML = ` | |
<h4 style="margin: 0 0 10px 0; padding: 0; text-align: center;">Blocker v2</h4> | |
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 5px;"> | |
<button class="blocker-start" style="grid-column: 1 / 3">Start</button> | |
<button class="blocker-pause">Pause</button> | |
<button class="blocker-resume">Resume</button> | |
<button class="blocker-stop" style="grid-column: 1 / 3">Stop</button> | |
</div> | |
<div class="blocker-status" style="margin-top: 10px; padding-top: 5px; border-top: 1px solid #555;">Status: Idle</div> | |
`; | |
controllerDiv.querySelector('.blocker-start').onclick = runBlocker; | |
controllerDiv.querySelector('.blocker-pause').onclick = pauseBlocker; | |
controllerDiv.querySelector('.blocker-resume').onclick = resumeBlocker; | |
controllerDiv.querySelector('.blocker-stop').onclick = stopBlocker; | |
document.body.appendChild(controllerDiv); | |
updateButtonStates(); | |
logStatus("Script loaded. Press Start."); | |
} | |
// --- Initialization --- | |
createControllerUI(); | |
// Toolkits Used: Socratic Questioning, Decomposition and Reconstruction, First Principles enumeration, State Management. | |
})(); // End IIFE |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment