← back to SDCC Stories

lib/instagram-api.js

105 lines

/**
 * Instagram API — Meta Graph API integration (cloned from SmokeShop pattern)
 * OAuth 2.0 flow → long-lived token → 2-step media publishing
 */
const https = require('https');

const META_API_VERSION = 'v18.0';

// Exchange short-lived code for access token
async function exchangeCodeForToken(appId, appSecret, redirectUri, code) {
  const params = new URLSearchParams({
    client_id: appId,
    client_secret: appSecret,
    grant_type: 'authorization_code',
    redirect_uri: redirectUri,
    code
  });

  return apiGet(`https://graph.facebook.com/${META_API_VERSION}/oauth/access_token?${params}`);
}

// Exchange short-lived token for 60-day long-lived token
async function getLongLivedToken(appId, appSecret, shortToken) {
  const params = new URLSearchParams({
    grant_type: 'fb_exchange_token',
    client_id: appId,
    client_secret: appSecret,
    fb_exchange_token: shortToken
  });

  return apiGet(`https://graph.facebook.com/${META_API_VERSION}/oauth/access_token?${params}`);
}

// Get Instagram Business Account ID from Facebook Page
async function getIGBusinessAccount(accessToken) {
  const pages = await apiGet(`https://graph.facebook.com/${META_API_VERSION}/me/accounts?access_token=${accessToken}`);
  if (!pages.data || pages.data.length === 0) return null;

  const pageId = pages.data[0].id;
  const igAccount = await apiGet(`https://graph.facebook.com/${META_API_VERSION}/${pageId}?fields=instagram_business_account&access_token=${accessToken}`);
  return igAccount.instagram_business_account?.id || null;
}

// Publish to Instagram (2-step: create container → publish)
async function publishToInstagram(igAccountId, accessToken, imageUrl, caption) {
  // Step 1: Create media container
  const container = await apiPost(`https://graph.facebook.com/${META_API_VERSION}/${igAccountId}/media`, {
    image_url: imageUrl,
    caption,
    access_token: accessToken
  });

  if (!container.id) throw new Error('Failed to create media container: ' + JSON.stringify(container));

  // Wait for processing
  await new Promise(r => setTimeout(r, 3000));

  // Step 2: Publish
  const published = await apiPost(`https://graph.facebook.com/${META_API_VERSION}/${igAccountId}/media_publish`, {
    creation_id: container.id,
    access_token: accessToken
  });

  return published;
}

// ── HTTP helpers ──
function apiGet(url) {
  return new Promise((resolve, reject) => {
    https.get(url, (res) => {
      let data = '';
      res.on('data', c => data += c);
      res.on('end', () => {
        try { resolve(JSON.parse(data)); }
        catch (e) { resolve({ error: data }); }
      });
    }).on('error', reject);
  });
}

function apiPost(url, body) {
  return new Promise((resolve, reject) => {
    const payload = JSON.stringify(body);
    const parsed = new URL(url);
    const req = https.request({
      hostname: parsed.hostname,
      path: parsed.pathname + parsed.search,
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }
    }, (res) => {
      let data = '';
      res.on('data', c => data += c);
      res.on('end', () => {
        try { resolve(JSON.parse(data)); }
        catch (e) { resolve({ error: data }); }
      });
    });
    req.on('error', reject);
    req.write(payload);
    req.end();
  });
}

module.exports = { exchangeCodeForToken, getLongLivedToken, getIGBusinessAccount, publishToInstagram };