← 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}.`);
})();