Add tool to clean up facebook

This commit is contained in:
James Guan
2026-09-15 21:41:27 -04:00
commit 48839bca78
7 changed files with 1030 additions and 0 deletions
+37
View File
@@ -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
+39
View File
@@ -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
View File
@@ -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, "&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();
}
})();
+25
View File
@@ -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"
}
}
+148
View File
@@ -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
View File
@@ -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>
+77
View File
@@ -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));
}
});