← back to Norma
agents/instagram-agent/delete-spoonflower.js
119 lines
#!/usr/bin/env node
/**
* delete-spoonflower.js — delete the discovered Spoonflower posts on Instagram via openclaw
* (logged-in real Chrome). The Graph API cannot delete a published post, so this drives the
* ··· → Delete → confirm flow and VERIFIES the post is gone before recording it.
*
* Reads data/spoonflower-hitlist.jsonl (the 4 confirmed targets).
* Writes data/deleted-posts.jsonl (tombstones) + data/spoonflower-delete-results.jsonl.
*
* SAFETY:
* - Serialized (one delete at a time) — rapid browser deletes are Meta's #1 ban trigger.
* - Per-post it detects the ··· menu; if the logged-in account does NOT own the post,
* it SKIPS loudly (never guesses) so you can switch accounts and re-run.
* - Only tombstones a post it VERIFIED is gone (permalink 404s afterward).
*
* Usage:
* node delete-spoonflower.js --dry # navigate + report reachability, delete NOTHING
* node delete-spoonflower.js # delete every reachable target
* node delete-spoonflower.js grassclothwallpaper # only this account's posts
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const DATA = path.join(__dirname, 'data');
const HITLIST = path.join(DATA, 'spoonflower-hitlist.jsonl');
const TOMBS = path.join(DATA, 'deleted-posts.jsonl');
const RESULTS = path.join(DATA, 'spoonflower-delete-results.jsonl');
const args = process.argv.slice(2);
const DRY = args.includes('--dry');
const onlyHandle = args.find((a) => !a.startsWith('--')) || null;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function oc(cmd) { return execSync(`openclaw browser ${cmd}`, { encoding: 'utf8', timeout: 60000, stdio: ['ignore', 'pipe', 'pipe'] }); }
let TAB = null;
function ocOpen(url) { const o = oc(`open ${JSON.stringify(url)} --timeout 30000`); TAB = (o.match(/id:\s*([A-F0-9]+)/i) || [])[1] || TAB; return TAB; }
function ocNav(url) { if (!TAB) return ocOpen(url); oc(`navigate ${JSON.stringify(url)} --target-id ${TAB}`); }
function ocSnap() { try { return oc(`snapshot --format ai --limit 800 ${TAB ? `--target-id ${TAB}` : ''}`); } catch { return ''; } }
function ocFindRef(snap, re) {
for (const line of String(snap).split('\n')) {
if (re.test(line)) { const m = line.match(/\[ref=([A-Za-z0-9_]+)\]/); if (m) return m[1]; }
}
return null;
}
function ocClick(ref) { oc(`click ${ref} --target-id ${TAB}`); }
async function loggedIn() {
ocOpen('https://www.instagram.com/'); await sleep(2500);
const s = ocSnap();
if (/Enter the code|Log in|Log In|Phone number, username|Create new account/i.test(s) && !/\bHome\b|\bSearch\b|\bProfile\b/i.test(s)) return false;
return true;
}
// Delete one post; returns {status, detail}. status ∈ deleted|not-owner|no-menu|still-present|error
async function del(permalink) {
ocNav(permalink); await sleep(2800);
let snap = ocSnap();
if (/isn't available|Sorry, this page|Page Not Found/i.test(snap)) return { status: 'already-gone' };
const more = ocFindRef(snap, /More options|^\s*-?\s*button "More"|More$/i);
if (!more) return { status: 'no-menu', detail: 'no ··· menu — Chrome not logged in as the owning account' };
ocClick(more); await sleep(1400);
const del1 = ocFindRef(ocSnap(), /\bDelete\b/);
if (!del1) return { status: 'not-owner', detail: 'no Delete item — logged-in account does not own this post' };
ocClick(del1); await sleep(1400);
const confirm = ocFindRef(ocSnap(), /\bDelete\b/);
if (!confirm) return { status: 'error', detail: 'no confirm Delete button appeared' };
ocClick(confirm); await sleep(3200);
ocNav(permalink); await sleep(2800);
const gone = /isn't available|Sorry, this page|Page Not Found/i.test(ocSnap());
return gone ? { status: 'deleted' } : { status: 'still-present', detail: 'clicked Delete but post still resolves' };
}
(async () => {
let targets = fs.readFileSync(HITLIST, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
if (onlyHandle) targets = targets.filter((t) => t.handle === onlyHandle.replace(/^@/, ''));
console.log(`${DRY ? 'DRY-RUN — ' : ''}${targets.length} target(s)${onlyHandle ? ` on @${onlyHandle}` : ''}:\n`);
if (!(await loggedIn())) {
console.error('✋ Instagram is NOT logged in (login/2FA screen detected). Complete the login in the openclaw Chrome first, then re-run.');
process.exit(2);
}
console.log('✓ Instagram session is logged in.\n');
const results = [];
for (const t of targets) {
process.stdout.write(`@${t.handle} ${t.permalink} … `);
if (DRY) {
// reachability probe only: open, look for ··· menu, DO NOT delete
ocNav(t.permalink); await sleep(2600);
const s = ocSnap();
const gone = /isn't available|Sorry, this page|Page Not Found/i.test(s);
const reach = !!ocFindRef(s, /More options|More$/i);
const status = gone ? 'already-gone' : reach ? 'reachable(owner)' : 'not-reachable(switch account)';
console.log(status);
results.push({ ...t, dry: true, status });
continue;
}
let r;
try { r = await del(t.permalink); } catch (e) { r = { status: 'error', detail: e.message.slice(0, 120) }; }
console.log(r.status + (r.detail ? ` — ${r.detail}` : ''));
if (r.status === 'deleted' || r.status === 'already-gone') {
fs.appendFileSync(TOMBS, JSON.stringify({ id: t.permalink, permalink: t.permalink, media_id: t.media_id || '',
handle: t.handle, ts: new Date().toISOString(), mode: 'live', reason: 'spoonflower-purge', verified: r.status === 'deleted' }) + '\n');
}
results.push({ ...t, status: r.status, detail: r.detail || '' });
await sleep(4000); // pace between deletes — ban avoidance
}
fs.appendFileSync(RESULTS, results.map((r) => JSON.stringify({ ...r, at: new Date().toISOString() })).join('\n') + '\n');
const done = results.filter((r) => r.status === 'deleted' || r.status === 'already-gone').length;
const stuck = results.filter((r) => ['not-owner', 'no-menu', 'not-reachable(switch account)'].includes(r.status));
console.log(`\n=== ${DRY ? 'DRY-RUN complete' : `${done}/${targets.length} removed`} ===`);
if (stuck.length) {
console.log(`\n⚠ ${stuck.length} post(s) need a different account logged in:`);
for (const s of stuck) console.log(` @${s.handle} ${s.permalink} → log into/switch to @${s.handle}, then re-run: node delete-spoonflower.js ${s.handle}`);
}
})();