← back to Norma
agents/instagram-agent/reshare-erase.js
110 lines
#!/usr/bin/env node
/**
* reshare-erase.js — TK-10758. Steve: "stop reposting other people's content + erase those
* we reshared with the sassy comments across all IG accounts."
*
* The resharer (hashtag-reshare.js / reshare-fleet.js — now DISABLED) republished OTHER people's
* images on DW accounts with a rotating SASS[...] caption. Every such post is logged in
* data/reshares-<account>.jsonl as { ts, permalink, our_media_id }. This tool reads all those
* ledgers and, per our_media_id:
* - GET /{id}?fields=id,caption,permalink → is it still live? capture the (sassy) caption.
* Then classifies live vs already-gone and writes the erase list.
*
* node reshare-erase.js # PROBE (read-only): list live reshares + captions, delete nothing
* node reshare-erase.js --go # DELETE each live reshare, verified + tombstoned (destructive)
*
* Deletion is browser-free Graph-API (no ban vector), paced, verified. Default probe-only.
*/
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const https = require('https');
const DATA = path.join(__dirname, 'data');
const TOMBS = path.join(DATA, 'deleted-posts.jsonl');
const TOKEN = process.env.IG_ACCESS_TOKEN;
const VER = process.env.IG_GRAPH_VERSION || 'v21.0';
const HOST = 'graph.facebook.com';
const PACE_MS = 5000; // browser-free Graph deletes on our own accounts — faster is fine
const TICKET = 'TK-10758';
const args = process.argv.slice(2);
const GO = args.includes('--go');
const only = (() => { const i = args.indexOf('--only'); return i >= 0 ? args[i + 1] : null; })();
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function req(method, p) {
return new Promise((resolve) => {
const r = https.request(`https://${HOST}/${VER}/${p}`, { method }, (res) => {
let s = ''; res.on('data', (d) => (s += d));
res.on('end', () => { let j = null; try { j = JSON.parse(s); } catch {} resolve({ status: res.statusCode, body: j }); });
});
r.on('error', () => resolve({ status: 0, body: null }));
r.end();
});
}
const get = (p) => req('GET', p);
const del = (p) => req('DELETE', p);
function loadLedgers() {
const rows = [];
for (const f of fs.readdirSync(DATA)) {
const m = f.match(/^reshares-(.+)\.jsonl$/);
if (!m) continue;
const handle = m[1];
if (only && handle !== only) continue;
fs.readFileSync(path.join(DATA, f), 'utf8').trim().split('\n').filter(Boolean).forEach((l) => {
try { const j = JSON.parse(l); if (j.our_media_id) rows.push({ handle, ...j }); } catch {}
});
}
// dedup by media id
const seen = new Set();
return rows.filter((r) => (seen.has(r.our_media_id) ? false : seen.add(r.our_media_id)));
}
function tombstoned() {
if (!fs.existsSync(TOMBS)) return new Set();
return new Set(fs.readFileSync(TOMBS, 'utf8').trim().split('\n').filter(Boolean)
.map((l) => { try { return JSON.parse(l).media_id; } catch { return null; } }).filter(Boolean));
}
(async () => {
const rows = loadLedgers();
const done = tombstoned();
const report = [];
for (const r of rows) {
if (done.has(r.our_media_id)) { report.push({ ...r, state: 'already-deleted' }); continue; }
const g = await get(`${r.our_media_id}?fields=id,caption,permalink&access_token=${TOKEN}`);
const live = g.status === 200 && g.body && g.body.id;
report.push({ ...r, state: live ? 'LIVE' : 'gone', caption: live ? (g.body.caption || '').replace(/\n/g, ' ').slice(0, 90) : null });
await sleep(120);
}
fs.writeFileSync(path.join(DATA, 'reshare-erase-list.json'), JSON.stringify({ ts: new Date().toISOString(), report }, null, 2));
const live = report.filter((x) => x.state === 'LIVE');
const byH = {}; live.forEach((x) => (byH[x.handle] = (byH[x.handle] || 0) + 1));
console.log('\n=== RESHARE ERASE (TK-10758) ===');
console.log('ledger rows:', report.length,
'| LIVE:', live.length,
'| already-deleted:', report.filter((x) => x.state === 'already-deleted').length,
'| gone:', report.filter((x) => x.state === 'gone').length);
console.log('LIVE by account:', JSON.stringify(byH, null, 2));
console.log('\nsample sassy captions (proof these are the reposts):');
live.slice(0, 6).forEach((x) => console.log(` @${x.handle} ${x.permalink}\n "${x.caption}"`));
if (!GO) { console.log('\nPROBE only — nothing deleted. Run --go to erase all LIVE reshares (paced, verified).'); return; }
let ok = 0, fail = 0;
for (const t of live) {
process.stdout.write(`DELETE @${t.handle} ${t.permalink} ... `);
const d = await del(`${t.our_media_id}?access_token=${TOKEN}`);
await sleep(1500);
const chk = await get(`${t.our_media_id}?fields=id&access_token=${TOKEN}`);
const gone = chk.status !== 200 || (chk.body && chk.body.error);
if (gone) {
fs.appendFileSync(TOMBS, JSON.stringify({ ts: new Date().toISOString(), handle: t.handle, media_id: t.our_media_id, permalink: t.permalink, method: 'graph-api', ticket: TICKET, reason: 'sassy-reshare-of-others-content', verified: true }) + '\n');
ok++; console.log('✓ deleted+verified');
} else { fail++; console.log(`✗ FAILED (${d.status})`); }
await sleep(PACE_MS);
}
console.log(`\nDONE: erased+verified ${ok}, failed ${fail}, of ${live.length}.`);
})();