(() => { if (window.__fbActivityCleanerLoaded) return; window.__fbActivityCleanerLoaded = true; const DEFAULT_SKIP = "tesla, ford, pokemon, pokémon, pika, pokejohn, pokemongo, pokemon go"; const DEFAULT_OFFENSIVE = "kill yourself, kys, rape, nigger, nigga, faggot, retard, whore, slut, cunt, " + "go die, hope you die, piece of shit, fucking idiot, dumb bitch, dumbass, " + "get railed, shitty people, get fucked"; const KEEP_POST_RE = /profile picture|cover photo|added \d+ new photo|added a new photo|uploaded a photo|uploaded \d+ photos|posted a photo|posted \d+ photos/i; const state = { running: false, mode: "comments", // comments | posts deleted: 0, skipped: 0, offensiveForced: 0, errors: 0, lastAction: "Idle", delayMs: 2500, failStreak: 0, }; let stopFlag = false; const countedSkipLabels = new Set(); function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function parseList(text) { return String(text || "") .split(/[\n,]+/) .map((s) => s.trim().toLowerCase()) .filter(Boolean); } function escapeRegExp(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function matchesAny(haystack, terms) { const h = haystack || ""; return terms.some((t) => { if (!t) return false; try { const re = new RegExp(`(^|[^a-z0-9])${escapeRegExp(t)}([^a-z0-9]|$)`, "i"); return re.test(h); } catch { return h.toLowerCase().includes(t.toLowerCase()); } }); } function pageText() { return document.body?.innerText || ""; } function isActivityLog() { return /\/allactivity/.test(location.pathname + location.search) || pageText().includes("Activity log"); } function detectMode() { const q = location.search; if (/MANAGEPOSTSPHOTOSANDVIDEOS/i.test(q)) return "posts"; if (/COMMENTSCLUSTER/i.test(q)) return "comments"; const h = pageText(); if (/Your posts, photos and videos/i.test(h)) return "posts"; if (/\bComments\b/.test(h) && /Activity log/i.test(h)) return "comments"; return state.mode; } function moreButtons() { // Never stamp dataset flags on these nodes — Facebook/React reuses them // after a delete, which made us skip every other item. return Array.from( document.querySelectorAll('[aria-label^="More options"]') ); } /** Tight row text only — walking too far up pulls in the whole list. */ function rowRoot(btn) { return ( btn.closest('[role="article"]') || btn.closest('[role="listitem"]') || btn.closest("li") || btn.parentElement?.parentElement?.parentElement || btn.parentElement ); } function rowText(btn) { const label = btn.getAttribute("aria-label") || ""; const root = rowRoot(btn); const block = (root && root.innerText) || ""; return `${label}\n${block.slice(0, 500)}`; } function robustClick(el) { if (!el) return false; try { // Single click only — double-firing confuses Facebook and triggers errors if (typeof el.click === "function") el.click(); else { el.dispatchEvent( new MouseEvent("click", { bubbles: true, cancelable: true, view: window }) ); } return true; } catch { return false; } } function clickText(text, root = document) { const els = Array.from( root.querySelectorAll('[role="button"], button, [role="menuitem"], span') ); const el = els.find((e) => (e.innerText || "").trim() === text); if (el) { robustClick(el); return true; } return false; } function hasFbError() { const t = pageText(); return ( /Something went wrong/i.test(t) || /Try again later/i.test(t) ); } function isProcessing() { const t = pageText(); return /still processing your changes/i.test(t) || /We're still processing/i.test(t); } function dismissErrorDialogs() { let dismissed = false; const dialogs = Array.from(document.querySelectorAll('[role="dialog"]')); for (const d of dialogs) { const txt = d.innerText || ""; if ( /Something went wrong|Please try again|Try again later/i.test(txt) ) { const ok = Array.from(d.querySelectorAll('[role="button"], button')).find((b) => /^(OK|Close|Got it|Dismiss)$/i.test((b.innerText || "").trim()) ) || d.querySelector('[aria-label="Close"]'); if (ok) { robustClick(ok); dismissed = true; } } } // Toast / non-dialog banners if (/Try again later|Something went wrong/i.test(pageText())) { if (clickText("OK")) dismissed = true; const closes = Array.from( document.querySelectorAll('[aria-label="Close"]') ); closes.slice(0, 3).forEach((c) => robustClick(c)); if (closes.length) dismissed = true; } return dismissed; } async function waitWhileProcessing(maxMs = 120000) { const start = Date.now(); while (isProcessing() && !stopFlag && Date.now() - start < maxMs) { log("Waiting for Facebook to finish processing…"); await sleep(3000); } } async function backoffOnError(reason) { state.failStreak++; state.errors++; dismissErrorDialogs(); const pauseSec = Math.min(300, 60 * state.failStreak); // 1m, 2m, 3m… max 5m log(`${reason} — cooling down ${pauseSec}s (streak ${state.failStreak})`); // Slow future deletes too state.delayMs = Math.min(15000, state.delayMs + 1000); const delayInput = document.getElementById("fb-cleaner-delay"); if (delayInput) delayInput.value = String(state.delayMs); const end = Date.now() + pauseSec * 1000; while (!stopFlag && Date.now() < end) { await sleep(1000); } dismissErrorDialogs(); } async function confirmDialogIfAny() { await sleep(500); const dialog = document.querySelector('[role="dialog"]'); if (!dialog) return true; const txt = dialog.innerText || ""; if (/Something went wrong|Please try again|Try again later/i.test(txt)) { return false; } const btns = Array.from(dialog.querySelectorAll('[role="button"], button')); const conf = btns.find((b) => /^(Move to Trash|Trash|Delete|Remove|Confirm)$/i.test( (b.innerText || "").trim() ) ); if (conf) { robustClick(conf); await sleep(900); return !hasFbError(); } return true; } async function openMenuAndAct(btn, actionLabels) { btn.scrollIntoView({ block: "center", behavior: "instant" }); await sleep(300); robustClick(btn); await sleep(550); let item = null; for (let attempt = 0; attempt < 8 && !item; attempt++) { const items = Array.from(document.querySelectorAll('[role="menuitem"]')); item = items.find((m) => { const t = (m.innerText || m.getAttribute("aria-label") || "") .trim() .toLowerCase(); return actionLabels.some((a) => t.includes(a.toLowerCase())); }); if (!item) await sleep(180); } if (!item) { document.body.click(); await sleep(200); return false; } robustClick(item); const confirmed = await confirmDialogIfAny(); await sleep(1000); if (!confirmed || hasFbError()) return false; return true; } function log(line) { state.lastAction = line; const el = document.getElementById("fb-cleaner-log"); if (!el) return; const ts = new Date().toLocaleTimeString(); el.textContent = `[${ts}] ${line}\n` + el.textContent.slice(0, 1800); updateStats(); } function updateStats() { const el = document.getElementById("fb-cleaner-stats"); if (!el) return; el.innerHTML = `
Status: ${state.running ? "Running" : "Stopped"} · Mode: ${state.mode}
` + `
Deleted: ${state.deleted} · Skipped: ${state.skipped}
` + `
Offensive forced: ${state.offensiveForced} · Errors: ${state.errors}
` + `
Last: ${escapeHtml(state.lastAction)}
`; } function escapeHtml(s) { return String(s) .replace(/&/g, "&") .replace(//g, ">"); } function classify(btn, skipTerms, offensiveTerms) { const label = btn.getAttribute("aria-label") || ""; const text = rowText(btn); // Community skip must use the button label only — row blocks are huge and // include neighboring Tesla/Ford/Pokemon items, which false-skipped everything. const skipCommunity = matchesAny(label, skipTerms); const offensive = matchesAny(text, offensiveTerms); const keepPost = KEEP_POST_RE.test(label); if (state.mode === "posts" && keepPost) { return { action: "skip", reason: "keep photo/profile", offensive: false }; } // Always delete offensive comments, even on Tesla/Ford/Pokemon pages if (offensive && state.mode === "comments") { return { action: "delete", reason: "offensive", offensive: true }; } if (skipCommunity) { return { action: "skip", reason: "skip community", offensive: false }; } return { action: "delete", reason: "cleanup", offensive: false }; } async function waitForRowGone(btn, labelBefore, maxMs = 5000) { const start = Date.now(); while (!stopFlag && Date.now() - start < maxMs) { if (!btn.isConnected) return true; const current = btn.getAttribute("aria-label") || ""; // React reused the node for a different row if (current && current !== labelBefore) return true; // Same label still present somewhere is OK if this node is gone; // if this node still has the label, keep waiting briefly await sleep(200); } return !btn.isConnected || (btn.getAttribute("aria-label") || "") !== labelBefore; } async function processOne(skipTerms, offensiveTerms) { if (hasFbError()) { await backoffOnError("Facebook error"); return "rate-limit"; } await waitWhileProcessing(); if (stopFlag) return "stopped"; const btns = moreButtons(); if (!btns.length) return "empty"; let chose = null; let classification = null; let skippedNow = 0; for (const btn of btns) { const c = classify(btn, skipTerms, offensiveTerms); if (c.action === "skip") { const lbl = btn.getAttribute("aria-label") || ""; if (lbl && !countedSkipLabels.has(lbl)) { countedSkipLabels.add(lbl); state.skipped++; skippedNow++; } else if (!lbl) { skippedNow++; } continue; } chose = btn; classification = c; break; } if (!chose) { if (skippedNow) { log(`Skipped ${skippedNow} visible (Tesla/Ford/Pokémon/keep) — scrolling`); } window.scrollBy(0, 2200); await sleep(1100); const after = moreButtons(); if (after.length === 0) return "exhausted-visible"; return "scrolled"; } if (skippedNow) { log(`Passing ${skippedNow} skip-community item(s)`); } const labelBefore = chose.getAttribute("aria-label") || ""; const label = labelBefore.slice(0, 90); log(`Deleting: ${label}`); const ok = await openMenuAndAct( chose, state.mode === "posts" ? ["move to trash", "delete", "trash"] : ["delete", "remove"] ); if (!ok || hasFbError()) { await backoffOnError(hasFbError() ? "Something went wrong" : "Delete failed"); return "rate-limit"; } await waitForRowGone(chose, labelBefore); if (hasFbError()) { await backoffOnError("Something went wrong"); return "rate-limit"; } state.deleted++; state.failStreak = 0; if (classification.offensive) state.offensiveForced++; log( `${classification.offensive ? "Offensive " : ""}Deleted: ${label}` ); await sleep(state.delayMs); return "deleted"; } async function runLoop() { const skipTerms = parseList( document.getElementById("fb-cleaner-skip")?.value || DEFAULT_SKIP ); const offensiveTerms = parseList( document.getElementById("fb-cleaner-offensive")?.value || DEFAULT_OFFENSIVE ); const delayRaw = Number( document.getElementById("fb-cleaner-delay")?.value || 2500 ); // Migrate old second-based values (< 100) to ms state.delayMs = delayRaw > 0 && delayRaw < 100 ? Math.round(delayRaw * 1000) : Math.max(0, Math.round(delayRaw || 2500)); const delayInput = document.getElementById("fb-cleaner-delay"); if (delayInput) delayInput.value = String(state.delayMs); state.failStreak = 0; countedSkipLabels.clear(); state.mode = detectMode(); state.running = true; stopFlag = false; updateStats(); setButtons(true); log(`Started in ${state.mode} mode · ${state.delayMs}ms between deletes`); let emptyStreak = 0; while (!stopFlag) { if (!isActivityLog()) { log("Not on Activity Log — open Comments or Posts manage mode"); break; } state.mode = detectMode(); const result = await processOne(skipTerms, offensiveTerms); if (stopFlag) break; if (result === "empty" || result === "exhausted-visible") { emptyStreak++; window.scrollBy(0, 2800); await sleep(1400); if (moreButtons().length === 0) { if (emptyStreak === 2) { window.scrollTo(0, 0); await sleep(1000); } } if (emptyStreak >= 6) { log("No more deletable items on this page"); break; } } else if (result !== "rate-limit") { emptyStreak = 0; } } state.running = false; setButtons(false); log("Stopped"); updateStats(); } function setButtons(running) { const start = document.getElementById("fb-cleaner-start"); const stop = document.getElementById("fb-cleaner-stop"); if (start) start.disabled = running; if (stop) stop.disabled = !running; } function ensurePanel() { if (document.getElementById("fb-cleaner-root")) return; const root = document.createElement("div"); root.id = "fb-cleaner-root"; root.innerHTML = `
FB Activity Cleaner
Skip communities (unless offensive)
Offensive phrases — always delete
Keeps profile/cover/photo uploads on Posts. Skips Tesla/Ford/Pokémon unless offensive. Slows down automatically if Facebook rate-limits.
`; document.documentElement.appendChild(root); document.getElementById("fb-cleaner-start").onclick = () => { if (state.running) return; runLoop(); }; document.getElementById("fb-cleaner-stop").onclick = () => { stopFlag = true; log("Stop requested…"); }; document.getElementById("fb-cleaner-minimize").onclick = () => { root.classList.toggle("collapsed"); }; // drag const header = document.getElementById("fb-cleaner-header"); let dragging = false; let ox = 0; let oy = 0; header.addEventListener("mousedown", (e) => { dragging = true; const rect = root.getBoundingClientRect(); ox = e.clientX - rect.left; oy = e.clientY - rect.top; e.preventDefault(); }); window.addEventListener("mousemove", (e) => { if (!dragging) return; root.style.left = Math.max(0, e.clientX - ox) + "px"; root.style.top = Math.max(0, e.clientY - oy) + "px"; root.style.right = "auto"; root.style.bottom = "auto"; }); window.addEventListener("mouseup", () => { dragging = false; }); chrome.storage?.local?.get(["skip", "offensive", "delay"], (data) => { if (data?.skip) { document.getElementById("fb-cleaner-skip").value = data.skip; } if (data?.offensive) { document.getElementById("fb-cleaner-offensive").value = data.offensive; } if (data?.delay != null && data.delay !== "") { const n = Number(data.delay); const ms = n > 0 && n < 100 ? Math.round(n * 1000) : Math.round(n); document.getElementById("fb-cleaner-delay").value = String( Number.isFinite(ms) ? ms : 2500 ); } }); const persist = () => { chrome.storage?.local?.set({ skip: document.getElementById("fb-cleaner-skip").value, offensive: document.getElementById("fb-cleaner-offensive").value, delay: document.getElementById("fb-cleaner-delay").value, }); }; document.getElementById("fb-cleaner-skip").addEventListener("change", persist); document .getElementById("fb-cleaner-offensive") .addEventListener("change", persist); document .getElementById("fb-cleaner-delay") .addEventListener("change", persist); updateStats(); log("Panel ready — click Start deletion"); } function startFromExternal() { ensurePanel(); if (state.running) return { ok: true, alreadyRunning: true }; runLoop(); return { ok: true }; } function stopFromExternal() { stopFlag = true; log("Stop requested…"); return { ok: true }; } chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { if (!msg || !msg.type) return false; if (msg.type === "FB_CLEANER_PING") { sendResponse({ ok: true }); return false; } if (msg.type === "FB_CLEANER_START") { sendResponse(startFromExternal()); return false; } if (msg.type === "FB_CLEANER_STOP") { sendResponse(stopFromExternal()); return false; } if (msg.type === "FB_CLEANER_STATUS") { sendResponse({ ok: true, state: { ...state } }); return false; } return false; }); function boot() { ensurePanel(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } })();