← back to Norma
agents/instagram-agent/scan-spoonflower.js
68 lines
#!/usr/bin/env node
/**
* scan-spoonflower.js — READ-ONLY fleet-wide scan for any post whose caption mentions
* "spoonflower" across all 35 IG accounts. Uses the shared META token + each account's
* ig_user_id. Paginates up to MAXPAGES per account. Writes data/spoonflower-hitlist.jsonl.
* No writes to Instagram. $0 (Graph API reads are free).
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const A = require('./accounts');
const HOST = 'graph.facebook.com';
const VER = 'v21.0';
const MAXPAGES = 6; // ~6*50 = up to 300 recent posts/account
const RE = /spoonflower/i;
const OUT = path.join(__dirname, 'data', 'spoonflower-hitlist.jsonl');
function get(url) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
let d = '';
res.on('data', (c) => (d += c));
res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(new Error('bad json: ' + d.slice(0, 200))); } });
}).on('error', reject);
});
}
async function scanAccount(acct) {
const hits = [];
let url = `https://${HOST}/${VER}/${acct.ig_user_id}/media?fields=id,caption,permalink,timestamp,media_type&limit=50&access_token=${encodeURIComponent(acct.access_token)}`;
let scanned = 0;
for (let page = 0; page < MAXPAGES && url; page++) {
const j = await get(url);
if (j.error) return { error: j.error.message, scanned, hits };
for (const m of (j.data || [])) {
scanned++;
if (m.caption && RE.test(m.caption)) {
hits.push({ handle: acct.handle, page_name: acct.page_name, ig_user_id: acct.ig_user_id,
media_id: m.id, permalink: m.permalink, timestamp: m.timestamp, media_type: m.media_type,
caption: m.caption });
}
}
url = (j.paging && j.paging.next) || null;
}
return { scanned, hits };
}
(async () => {
const accounts = A.list().map((a) => A.resolve(a.handle)).filter((a) => a && a.ig_user_id && a.access_token);
console.log(`Scanning ${accounts.length} accounts (up to ${MAXPAGES * 50} posts each)…\n`);
const all = [];
const errs = [];
for (const acct of accounts) {
try {
const r = await scanAccount(acct);
if (r.error) { errs.push({ handle: acct.handle, error: r.error }); process.stdout.write(` @${acct.handle}: ERR ${r.error.slice(0,60)}\n`); continue; }
if (r.hits.length) { all.push(...r.hits); console.log(` @${acct.handle}: ${r.hits.length} SPOONFLOWER hit(s) / ${r.scanned} scanned`); }
else process.stdout.write(` @${acct.handle}: clean (${r.scanned})\n`);
} catch (e) { errs.push({ handle: acct.handle, error: e.message }); console.log(` @${acct.handle}: EXC ${e.message.slice(0,60)}`); }
}
fs.writeFileSync(OUT, all.map((h) => JSON.stringify(h)).join('\n') + (all.length ? '\n' : ''));
console.log(`\n=== TOTAL: ${all.length} Spoonflower posts across ${new Set(all.map(h=>h.handle)).size} accounts ===`);
console.log(`hitlist → ${OUT}`);
if (errs.length) console.log(`errors on ${errs.length} accounts:`, errs.map(e=>e.handle).join(', '));
for (const h of all) console.log(` • @${h.handle} ${h.permalink} (${h.timestamp}) "${(h.caption||'').replace(/\n/g,' ').slice(0,70)}…"`);
})();