← back to Beverlyhillsvideos

social/poster/ig-graph.mjs

70 lines

// ig-graph.mjs — Instagram Graph API Reel publishing (3-step async flow).
// Docs: developers.facebook.com/docs/instagram-platform/content-publishing
import { cfg } from './env.mjs';

const base = () => `https://graph.facebook.com/${cfg.graphVersion}`;

async function gpost(pathPart, params) {
  const url = new URL(`${base()}/${pathPart}`);
  const body = new URLSearchParams({ ...params, access_token: cfg.accessToken });
  const res = await fetch(url, { method: 'POST', body });
  const json = await res.json();
  if (!res.ok || json.error) {
    throw new Error(`Graph POST ${pathPart} failed: ${JSON.stringify(json.error || json)}`);
  }
  return json;
}

async function gget(pathPart, fields) {
  const url = new URL(`${base()}/${pathPart}`);
  url.searchParams.set('fields', fields);
  url.searchParams.set('access_token', cfg.accessToken);
  const res = await fetch(url);
  const json = await res.json();
  if (!res.ok || json.error) {
    throw new Error(`Graph GET ${pathPart} failed: ${JSON.stringify(json.error || json)}`);
  }
  return json;
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// Step 1: create a REELS container pointing at the public video URL.
export async function createReelContainer({ videoUrl, caption }) {
  const json = await gpost(`${cfg.igUserId}/media`, {
    media_type: 'REELS',
    video_url: videoUrl,
    caption,
    share_to_feed: 'true',
  });
  return json.id; // container id
}

// Step 2: poll the container until Instagram finishes transcoding.
export async function waitForContainer(containerId, { timeoutMs = 300000, everyMs = 8000 } = {}) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const { status_code, status } = await gget(containerId, 'status_code,status');
    if (status_code === 'FINISHED') return true;
    if (status_code === 'ERROR' || status_code === 'EXPIRED') {
      throw new Error(`Container ${containerId} ${status_code}: ${status}`);
    }
    await sleep(everyMs);
  }
  throw new Error(`Container ${containerId} not FINISHED within ${timeoutMs}ms`);
}

// Step 3: publish the finished container to the feed.
export async function publishContainer(containerId) {
  const json = await gpost(`${cfg.igUserId}/media_publish`, { creation_id: containerId });
  return json.id; // published media id
}

// Full flow.
export async function postReel({ videoUrl, caption }) {
  const containerId = await createReelContainer({ videoUrl, caption });
  await waitForContainer(containerId);
  const mediaId = await publishContainer(containerId);
  return { containerId, mediaId };
}