← back to Norma
agents/instagram-agent/comment-reply.js
69 lines
#!/usr/bin/env node
/**
* comment-reply.js — the ONE API-allowed "interaction": warmly reply to comments on OUR OWN
* posts (Meta has no endpoint to like/comment on other accounts' media). Builds vendor/designer
* relationships off the reshare-credit strategy — a vendor comments on their feature, we reply.
*
* node comment-reply.js <account> # DRAFT: show comments + drafted replies, send nothing
* node comment-reply.js <account> --go # LIVE: send replies
*
* SAFETY — only auto-replies to clearly POSITIVE engagement. Skips questions (need a real
* answer), anything negative, and spam — those are logged as "needs a human" instead of a
* canned reply. Dedups via data/comment-replies-<account>.jsonl so we never double-reply.
*/
const accounts = require('./accounts');
const fs = require('fs');
const path = require('path');
const args = process.argv.slice(2);
const ACCOUNT = (args.find((a) => !a.startsWith('--')) || 'fabric_fridays').replace(/^@/, '');
const GO = args.includes('--go');
const acct = accounts.resolve(ACCOUNT);
if (!acct) { console.error(`Unknown account "${ACCOUNT}".`); process.exit(1); }
const { ig_user_id: ID, username: UNAME, access_token: T, graph_host: H, graph_version: V } = acct;
const REPLIES = [
'Thank you so much! 🙌', 'So glad you love it too! ✨', 'Right?? Obsessed. 🧵',
'Appreciate you! 💛', 'Thank you — made our day. 🙏', 'Couldn\'t agree more! 👏',
'You get it. 😍 Thanks for the love!', 'Thank you! Swoon-worthy, right? ✨',
];
// Skip anything that needs a real human: a question, a negative, or spam.
const SKIP = /\?|price|cost|how much|where.*(buy|get)|available|shipping|dm|link|follow me|check my|send me|send us|send this|repost|can we|collab|hate|ugly|awful|worst|scam|fake|spam|http/i;
const NEG = /hate|ugly|awful|worst|scam|fake|trash|disgusting/i;
const g = async (url) => (await fetch(url)).json();
(async () => {
const LOG = path.join(__dirname, 'data', `comment-replies-${ACCOUNT}.jsonl`);
const replied = new Set(fs.existsSync(LOG) ? fs.readFileSync(LOG, 'utf8').trim().split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l).comment_id; } catch { return null; } }).filter(Boolean) : []);
const media2 = await g(`${H}/${V}/${ID}/media?fields=id,permalink,comments_count&limit=20&access_token=${encodeURIComponent(T)}`);
const withComments = (media2.data || []).filter((m) => (m.comments_count || 0) > 0);
console.log(`@${ACCOUNT} — ${GO ? 'LIVE' : 'DRAFT'} — ${withComments.length} post(s) with comments\n`);
let sent = 0, skipped = 0; const needsHuman = [];
for (const m of (media2.data || [])) {
if (!(m.comments_count > 0)) continue;
const cj = await g(`${H}/${V}/${m.id}/comments?fields=id,text,username,timestamp&access_token=${encodeURIComponent(T)}`);
for (const c of (cj.data || [])) {
if (replied.has(c.id)) continue;
const cu = (c.username || '').toLowerCase();
if (cu === (UNAME || '').toLowerCase() || cu === ACCOUNT.toLowerCase()) continue; // never reply to ourselves
const text = c.text || '';
if (SKIP.test(text)) { needsHuman.push({ post: m.permalink, from: c.username, text, why: NEG.test(text) ? 'negative' : 'question/spam' }); skipped++; continue; }
const reply = REPLIES[(c.id.charCodeAt(c.id.length - 1) + sent) % REPLIES.length];
console.log(`↳ @${c.username}: "${text.slice(0, 60)}"\n reply: "${reply}" (${m.permalink})`);
if (GO) {
try {
const r = await (await fetch(`${H}/${V}/${c.id}/replies`, { method: 'POST', body: new URLSearchParams({ message: reply, access_token: T }) })).json();
if (r.id) { fs.appendFileSync(LOG, JSON.stringify({ ts: new Date().toISOString(), comment_id: c.id, from: c.username, reply, reply_id: r.id }) + '\n'); console.log(' ✓ sent'); sent++; }
else console.log(' ✗ ' + JSON.stringify(r).slice(0, 120));
} catch (e) { console.log(' ✗ ' + e.message); }
await new Promise((res) => setTimeout(res, 3000));
} else sent++;
}
}
if (needsHuman.length) { console.log(`\n⚠ ${needsHuman.length} comment(s) need a HUMAN (question/negative/spam) — not auto-replied:`); needsHuman.forEach((n) => console.log(` @${n.from} [${n.why}]: "${n.text.slice(0, 70)}" ${n.post}`)); }
console.log(`\n@${ACCOUNT}: ${GO ? 'sent' : 'would send'} ${sent} · skipped ${skipped}.`);
})().catch((e) => { console.error('FAILED:', e.message); process.exit(1); });