Add tool to clean up facebook
This commit is contained in:
+37
@@ -0,0 +1,37 @@
|
|||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
Icon?
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Editor / IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Logs & debug
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Dependencies (if added later)
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Env / secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Chrome extension packaging
|
||||||
|
*.crx
|
||||||
|
*.pem
|
||||||
|
*.zip
|
||||||
|
web-ext-artifacts/
|
||||||
|
|
||||||
|
# OS / misc
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# FB Activity Cleaner
|
||||||
|
|
||||||
|
Chrome extension that cleans your Facebook **Activity Log** while you stay logged in.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
**Comments page**
|
||||||
|
- Deletes comments one-by-one (More options → Delete)
|
||||||
|
- Skips Tesla / Ford / Pokémon community items by default
|
||||||
|
- **Always deletes offensive comments**, even if they are on a skipped community page
|
||||||
|
|
||||||
|
**Posts / photos / videos page**
|
||||||
|
- Moves shares and status updates to trash
|
||||||
|
- Keeps profile pictures, cover photos, and photo uploads
|
||||||
|
|
||||||
|
## Install (Chrome / Edge / Brave)
|
||||||
|
|
||||||
|
1. Open `chrome://extensions`
|
||||||
|
2. Enable **Developer mode**
|
||||||
|
3. Click **Load unpacked**
|
||||||
|
4. Select this folder: `facebook-activity-cleaner`
|
||||||
|
5. Open Facebook → Activity log → Comments (or Posts) with **manage mode** on
|
||||||
|
6. Use the floating **FB Activity Cleaner** panel → **Start**
|
||||||
|
|
||||||
|
## Useful URLs
|
||||||
|
|
||||||
|
Replace with your profile if needed:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://www.facebook.com/me/allactivity?category_key=COMMENTSCLUSTER&manage_mode=true
|
||||||
|
https://www.facebook.com/me/allactivity?category_key=MANAGEPOSTSPHOTOSANDVIDEOS&manage_mode=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Meta rate-limits bulk actions; the extension pauses ~90s on “Try again later”
|
||||||
|
- Skip / offensive phrase lists are editable in the panel and saved locally
|
||||||
|
- Run it on the Activity Log manage-mode pages only
|
||||||
|
- Review skips before you walk away — community names must appear in the row text/label to be skipped
|
||||||
+620
@@ -0,0 +1,620 @@
|
|||||||
|
(() => {
|
||||||
|
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, "&")
|
||||||
|
.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 = `
|
||||||
|
<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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "FB Activity Cleaner",
|
||||||
|
"version": "1.0.6",
|
||||||
|
"description": "Bulk-clean Facebook Activity Log: delete comments and shared posts with skip rules.",
|
||||||
|
"permissions": ["storage", "activeTab", "scripting"],
|
||||||
|
"host_permissions": ["https://www.facebook.com/*", "https://facebook.com/*"],
|
||||||
|
"action": {
|
||||||
|
"default_title": "FB Activity Cleaner",
|
||||||
|
"default_popup": "popup.html"
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["https://www.facebook.com/*", "https://facebook.com/*"],
|
||||||
|
"js": ["content.js"],
|
||||||
|
"css": ["panel.css"],
|
||||||
|
"run_at": "document_idle"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
#fb-cleaner-root {
|
||||||
|
all: initial;
|
||||||
|
position: fixed !important;
|
||||||
|
bottom: 20px !important;
|
||||||
|
right: 20px !important;
|
||||||
|
z-index: 2147483646 !important;
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-root * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-panel {
|
||||||
|
width: 320px;
|
||||||
|
background: #111827;
|
||||||
|
color: #f9fafb;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #0b1220;
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-header strong {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-body {
|
||||||
|
padding: 12px;
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-body label {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-body input[type="checkbox"] {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-body textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 52px;
|
||||||
|
resize: vertical;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #4b5563;
|
||||||
|
background: #1f2937;
|
||||||
|
color: #f9fafb;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-stats {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: #d1d5db;
|
||||||
|
background: #1f2937;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-stats b {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-log {
|
||||||
|
max-height: 110px;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #9ca3af;
|
||||||
|
background: #0b1220;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #374151;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-actions button {
|
||||||
|
flex: 1;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 9px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 650;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-start {
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 11px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-start:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-stop {
|
||||||
|
background: #374151;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-stop:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-minimize {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fb-cleaner-root.collapsed #fb-cleaner-body {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>FB Activity Cleaner</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
width: 300px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 14px;
|
||||||
|
color: #1c1e21;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 15px;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #65676b;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 11px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 650;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#start {
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
#start:hover { background: #1d4ed8; }
|
||||||
|
#stop {
|
||||||
|
background: #e4e6eb;
|
||||||
|
color: #050505;
|
||||||
|
}
|
||||||
|
#status {
|
||||||
|
font-size: 12px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
min-height: 18px;
|
||||||
|
}
|
||||||
|
ol {
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
}
|
||||||
|
li { margin-bottom: 5px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>FB Activity Cleaner</h1>
|
||||||
|
<p>Open Activity Log manage mode, then start deletion here or on the page panel.</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button id="start" type="button">Start deletion</button>
|
||||||
|
<button id="stop" type="button">Stop</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status">Ready</div>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Open Comments or Posts Activity Log</li>
|
||||||
|
<li>Make sure checkboxes (manage mode) are visible</li>
|
||||||
|
<li>Click <strong>Start deletion</strong></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
const statusEl = document.getElementById("status");
|
||||||
|
|
||||||
|
function setStatus(text) {
|
||||||
|
statusEl.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFacebookTab() {
|
||||||
|
const tabs = await chrome.tabs.query({
|
||||||
|
active: true,
|
||||||
|
currentWindow: true,
|
||||||
|
});
|
||||||
|
const tab = tabs[0];
|
||||||
|
if (!tab?.id) throw new Error("No active tab");
|
||||||
|
if (!/^https:\/\/(www\.)?facebook\.com\//i.test(tab.url || "")) {
|
||||||
|
throw new Error("Open a facebook.com Activity Log tab first");
|
||||||
|
}
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ping(tabId) {
|
||||||
|
return chrome.tabs.sendMessage(tabId, { type: "FB_CLEANER_PING" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function injectContent(tabId) {
|
||||||
|
if (!chrome.scripting?.executeScript) {
|
||||||
|
throw new Error(
|
||||||
|
"Reload this extension on chrome://extensions, then refresh Facebook"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await chrome.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
files: ["content.js"],
|
||||||
|
});
|
||||||
|
if (chrome.scripting.insertCSS) {
|
||||||
|
await chrome.scripting.insertCSS({
|
||||||
|
target: { tabId },
|
||||||
|
files: ["panel.css"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Give the content script a moment to register listeners
|
||||||
|
await new Promise((r) => setTimeout(r, 150));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureContent(tabId) {
|
||||||
|
try {
|
||||||
|
await ping(tabId);
|
||||||
|
} catch {
|
||||||
|
await injectContent(tabId);
|
||||||
|
await ping(tabId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function send(type) {
|
||||||
|
const tab = await getFacebookTab();
|
||||||
|
await ensureContent(tab.id);
|
||||||
|
return chrome.tabs.sendMessage(tab.id, { type });
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("start").addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
setStatus("Starting…");
|
||||||
|
const res = await send("FB_CLEANER_START");
|
||||||
|
setStatus(res?.ok ? "Deletion started on the page" : res?.error || "Failed");
|
||||||
|
} catch (e) {
|
||||||
|
setStatus(e.message || String(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("stop").addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
setStatus("Stopping…");
|
||||||
|
const res = await send("FB_CLEANER_STOP");
|
||||||
|
setStatus(res?.ok ? "Stop requested" : res?.error || "Failed");
|
||||||
|
} catch (e) {
|
||||||
|
setStatus(e.message || String(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user