Skip to content

Instantly share code, notes, and snippets.

@swayson
Last active March 27, 2025 19:12
Show Gist options
  • Save swayson/385e843349576d70979f0541146494a2 to your computer and use it in GitHub Desktop.
Save swayson/385e843349576d70979f0541146494a2 to your computer and use it in GitHub Desktop.
// == 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