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