← back to Norma

agents/instagram-agent/accounts.js

110 lines

/**
 * accounts.js — multi-account resolver for the Instagram posting layer.
 *
 * Maps an account name/handle → the credentials needed to publish to it:
 *   { ig_user_id, access_token, graph_host, graph_version, handle, page_name }
 *
 * All 35 IG-linked accounts share ONE never-expiring token (META_ACCESS_TOKEN),
 * so posting to any of them is just "resolve the ig_user_id, reuse the token."
 * A per-account `token` field in accounts.json overrides the shared token if a
 * given account ever needs its own (e.g. an Instagram-Login flow account).
 *
 * Resolution order for the token:
 *   1. account.token in accounts.json (per-account override)
 *   2. process.env.META_ACCESS_TOKEN
 *   3. ~/Projects/secrets-manager/.env  META_ACCESS_TOKEN
 *   4. process.env.IG_ACCESS_TOKEN      (legacy single-account fallback)
 */

const fs = require('fs');
const path = require('path');
const os = require('os');

const REGISTRY_PATH = path.join(__dirname, 'accounts.json');

function sharedToken() {
  if (process.env.META_ACCESS_TOKEN) return process.env.META_ACCESS_TOKEN;
  try {
    const envPath = path.join(os.homedir(), 'Projects/secrets-manager/.env');
    const line = fs.readFileSync(envPath, 'utf8')
      .split('\n').find((l) => l.startsWith('META_ACCESS_TOKEN='));
    if (line) return line.slice('META_ACCESS_TOKEN='.length).trim();
  } catch { /* ignore */ }
  return process.env.IG_ACCESS_TOKEN || '';
}

function loadRegistry() {
  try { return JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8')); }
  catch { return { accounts: {} }; }
}

/** Normalize "@Velvet Wallpaper" / "velvetwallpaper" / an ig_user_id → a lookup key. */
function normalize(name) {
  return String(name || '').trim().replace(/^@/, '').toLowerCase();
}

/** List all postable account handles (with counts when the registry has them). */
function list() {
  return Object.values(loadRegistry().accounts)
    .map((a) => ({ handle: a.handle, ig_user_id: a.ig_user_id, page_name: a.page_name,
      followers: a.followers_count, posts: a.media_count }));
}

/**
 * Resolve one account → publishing credentials, or null if unknown.
 * Accepts a handle (@x / x), a page_name, or a raw ig_user_id.
 */
function resolve(name) {
  const reg = loadRegistry();
  const key = normalize(name);

  let acct = reg.accounts[key];
  if (!acct) {
    // fall back to matching by page_name or by ig_user_id
    acct = Object.values(reg.accounts).find(
      (a) => normalize(a.page_name) === key || a.ig_user_id === String(name).trim(),
    );
  }
  if (!acct) return null;

  const token = acct.token || sharedToken();
  return {
    handle: acct.handle,
    page_name: acct.page_name,
    ig_user_id: acct.ig_user_id,
    access_token: token,
    graph_host: acct.graph_host || 'https://graph.facebook.com',
    graph_version: acct.graph_version || 'v21.0',
    has_token: !!token,
  };
}

/**
 * Skill-facing resolver used by post/reel/story. Merges TWO addressing schemes:
 *   1. handle-registry (this file's accounts.json) → shared META_ACCESS_TOKEN
 *   2. legacy env-suffix scheme used by dw-marketing-reels:
 *        'dw'              → IG_USER_ID + IG_ACCESS_TOKEN
 *        'phillipe-romano' → IG_USER_ID_PHILLIPE_ROMANO + IG_ACCESS_TOKEN_PHILLIPE_ROMANO
 * Registry wins first; the env scheme is the fallback so existing reel callers
 * keep working unchanged. Returns { account, userId, accessToken, source }.
 */
function resolveSkillAccount(account) {
  const acct = (account || 'dw').toString().trim() || 'dw';
  const reg = resolve(acct);
  if (reg && reg.has_token) {
    return { account: reg.handle || acct, userId: reg.ig_user_id, accessToken: reg.access_token, source: 'registry' };
  }
  if (acct === 'dw') {
    return { account: 'dw', userId: process.env.IG_USER_ID, accessToken: process.env.IG_ACCESS_TOKEN, source: 'env' };
  }
  const suffix = acct.toUpperCase().replace(/-/g, '_');
  return {
    account: acct,
    userId: process.env[`IG_USER_ID_${suffix}`],
    accessToken: process.env[`IG_ACCESS_TOKEN_${suffix}`],
    source: 'env',
  };
}

module.exports = { resolve, resolveSkillAccount, list, normalize, loadRegistry };