[object Object]

← back to Norma

IG: add serialized, per-account-aware Spoonflower delete script (openclaw)

c90044bf6ecf45b77b93175318039dd52dd6bd71 · 2026-08-17 07:58:15 -0700 · Steve Abrams

Reads the confirmed hitlist and deletes via logged-in Chrome (··· -> Delete ->
confirm -> verify gone). Serialized + fails loudly if not logged in as the
owning account. --dry does a reachability probe that deletes nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit c90044bf6ecf45b77b93175318039dd52dd6bd71
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 17 07:58:15 2026 -0700

    IG: add serialized, per-account-aware Spoonflower delete script (openclaw)
    
    Reads the confirmed hitlist and deletes via logged-in Chrome (··· -> Delete ->
    confirm -> verify gone). Serialized + fails loudly if not logged in as the
    owning account. --dry does a reachability probe that deletes nothing.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 agents/instagram-agent/delete-spoonflower.js | 118 +++++++++++++++++++++++++++
 1 file changed, 118 insertions(+)

diff --git a/agents/instagram-agent/delete-spoonflower.js b/agents/instagram-agent/delete-spoonflower.js
new file mode 100644
index 0000000..35eb023
--- /dev/null
+++ b/agents/instagram-agent/delete-spoonflower.js
@@ -0,0 +1,118 @@
+#!/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}`);
+  }
+})();

← ffc9303 Bulk-select delete on IG auto-post viewer  ·  back to Norma  ·  auto-data-snapshot: 2026-08-17T08:03:25 (1 data files) — age a843c5b →