← back to Norma

agents/instagram-agent/skills/post.js

102 lines

/**
 * Instagram Post Skill
 *
 * Creates an Instagram feed post via Meta Graph API (Content Publishing API).
 * Two-step flow: create media container, then publish it.
 * Runs in simulation mode when IG_ACCESS_TOKEN is not set.
 *
 * Real endpoints:
 *   Step 1 — Create container:
 *     POST https://graph.facebook.com/v19.0/{ig_user_id}/media
 *     Params: image_url, caption, access_token
 *
 *   Step 2 — Publish:
 *     POST https://graph.facebook.com/v19.0/{ig_user_id}/media_publish
 *     Params: creation_id, access_token
 */

const { loadCredentials } = require('../../shared/credentials');
const ig = require('./_ig-api');

const AGENT = 'instagram-agent';
const PLATFORM = 'instagram';

async function getCredentials() {
  const creds = await loadCredentials('instagram');
  return {
    has: !!(creds?.IG_USER_ID && creds?.IG_ACCESS_TOKEN),
    userId: creds?.IG_USER_ID || '',
    accessToken: creds?.IG_ACCESS_TOKEN || '',
  };
}

/**
 * @param {Object} params - Request body
 * @param {string} params.image_url - Public URL of the image to post
 * @param {string} [params.caption] - Post caption text
 * @param {string} [params.petition_url] - Petition URL to include in caption
 * @param {string} [params.pipeline_id] - Pipeline entry ID to link
 * @returns {Promise<Object>}
 */
module.exports = async function post(params) {
  const imageUrl = params.image_url || '';
  const caption = params.caption || params.text || '';
  const { has: hasCreds, userId, accessToken } = await getCredentials();
  const simulated = !hasCreds;

  console.log(`[${AGENT}] Post skill invoked — simulation=${simulated}`);
  console.log(`[${AGENT}] Caption: ${caption.substring(0, 100)}${caption.length > 100 ? '...' : ''}`);
  if (imageUrl) console.log(`[${AGENT}] Image URL: ${imageUrl}`);

  if (simulated) {
    // --- Simulation mode ---
    const simulatedContainerId = `ig_container_${Date.now()}`;
    const simulatedMediaId = `ig_media_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;

    console.log(`[${AGENT}] SIMULATED post created: container=${simulatedContainerId}, media=${simulatedMediaId}`);

    return {
      media_id: simulatedMediaId,
      container_id: simulatedContainerId,
      caption,
      image_url: imageUrl || null,
      permalink: `https://www.instagram.com/p/${simulatedMediaId.substring(0, 11)}/`,
      media_type: 'IMAGE',
      simulated: true,
      posted: false,
      platform: PLATFORM,
      pipeline_id: params.pipeline_id || null,
      created_at: new Date().toISOString(),
    };
  }

  // --- Real API call (Instagram API with Instagram Login) ---
  if (!imageUrl) throw new Error('image_url is required to post to the feed');

  // Step 1: create the image container
  const containerId = await ig.createContainer(userId, accessToken, {
    image_url: imageUrl,
    caption,
  });
  console.log(`[${AGENT}] Post container created: ${containerId}`);

  // Step 2: publish (image containers are ready immediately)
  const mediaId = await ig.publishContainer(userId, accessToken, containerId);
  const permalink = await ig.permalinkOf(mediaId, accessToken);
  console.log(`[${AGENT}] Post published: media=${mediaId} ${permalink || ''}`);

  return {
    media_id: mediaId,
    container_id: containerId,
    caption,
    image_url: imageUrl,
    permalink,
    media_type: 'IMAGE',
    simulated: false,
    posted: true,
    platform: PLATFORM,
    pipeline_id: params.pipeline_id || null,
    created_at: new Date().toISOString(),
  };
};