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