Files
SocialMedia-Cleanup-Tool/content.js
T

621 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
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 =
`<div>Status: <b>${state.running ? "Running" : "Stopped"}</b> · Mode: <b>${state.mode}</b></div>` +
`<div>Deleted: <b>${state.deleted}</b> · Skipped: <b>${state.skipped}</b></div>` +
`<div>Offensive forced: <b>${state.offensiveForced}</b> · Errors: <b>${state.errors}</b></div>` +
`<div>Last: ${escapeHtml(state.lastAction)}</div>`;
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
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 = `
<div id="fb-cleaner-panel">
<div id="fb-cleaner-header">
<strong>FB Activity Cleaner</strong>
<button id="fb-cleaner-minimize" title="Collapse"></button>
</div>
<div id="fb-cleaner-body">
<label>
<input type="checkbox" id="fb-cleaner-auto-mode" checked />
Auto-detect Comments vs Posts page
</label>
<div>
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px">Skip communities (unless offensive)</div>
<textarea id="fb-cleaner-skip">${DEFAULT_SKIP}</textarea>
</div>
<div>
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px">Offensive phrases — always delete</div>
<textarea id="fb-cleaner-offensive">${DEFAULT_OFFENSIVE}</textarea>
</div>
<label style="align-items:center">
Delay until next delete (ms)
<input type="number" id="fb-cleaner-delay" min="0" max="60000" step="50" value="2500" style="width:80px;margin-left:auto;border-radius:6px;border:1px solid #4b5563;background:#1f2937;color:#fff;padding:4px 6px" />
</label>
<div id="fb-cleaner-stats"></div>
<div id="fb-cleaner-log"></div>
<div id="fb-cleaner-actions">
<button id="fb-cleaner-start" type="button">Start deletion</button>
<button id="fb-cleaner-stop" type="button" disabled>Stop</button>
</div>
<div style="font-size:11px;color:#9ca3af;line-height:1.35">
Keeps profile/cover/photo uploads on Posts. Skips Tesla/Ford/Pokémon unless offensive. Slows down automatically if Facebook rate-limits.
</div>
</div>
</div>
`;
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();
}
})();