← back to Norma

agents/instagram-agent/skills/story.js

100 lines

/**
 * Instagram Story Skill
 *
 * Creates an Instagram Story via the Instagram API with Instagram Login.
 * Uses media_type=STORIES. Stories expire after 24 hours.
 * Video stories are polled until processing finishes before publishing.
 *
 * Runs in simulation mode when IG_USER_ID + IG_ACCESS_TOKEN are not set.
 * All calls go to graph.instagram.com (see skills/_ig-api.js for why).
 */

const ig = require('./_ig-api');

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

/**
 * @param {Object} params - Request body
 * @param {string} [params.image_url] - Public URL of the image (for image stories)
 * @param {string} [params.video_url] - Public URL of the video (for video stories)
 * @param {string} [params.pipeline_id] - Pipeline entry ID to link
 * @returns {Promise<Object>}
 */
module.exports = async function story(params) {
  const imageUrl = params.image_url || '';
  const videoUrl = params.video_url || '';
  const mediaSource = videoUrl ? 'video' : 'image';
  const simulated = !ig.hasCredentials();

  console.log(`[${AGENT}] Story skill invoked — simulation=${simulated}, source=${mediaSource}`);
  if (imageUrl) console.log(`[${AGENT}] Image URL: ${imageUrl}`);
  if (videoUrl) console.log(`[${AGENT}] Video URL: ${videoUrl}`);

  if (simulated) {
    // --- Simulation mode ---
    const simulatedContainerId = `ig_story_container_${Date.now()}`;
    const simulatedMediaId = `ig_story_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
    const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();

    console.log(`[${AGENT}] SIMULATED story created: container=${simulatedContainerId}, media=${simulatedMediaId}`);
    console.log(`[${AGENT}] Story expires at: ${expiresAt}`);

    return {
      media_id: simulatedMediaId,
      container_id: simulatedContainerId,
      image_url: imageUrl || null,
      video_url: videoUrl || null,
      media_source: mediaSource,
      permalink: `https://www.instagram.com/stories/${simulatedMediaId.substring(0, 11)}/`,
      media_type: 'STORIES',
      expires_at: expiresAt,
      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 (!videoUrl && !imageUrl) {
    throw new Error('Either image_url or video_url is required for a story');
  }

  const igUserId = process.env.IG_USER_ID;
  const accessToken = process.env.IG_ACCESS_TOKEN;

  // Step 1: create the story container
  const containerBody = { media_type: 'STORIES' };
  if (videoUrl) containerBody.video_url = videoUrl;
  else containerBody.image_url = imageUrl;

  const containerId = await ig.createContainer(igUserId, accessToken, containerBody);
  console.log(`[${AGENT}] Story container created: ${containerId}`);

  // Step 2: video stories must finish processing before publishing
  if (videoUrl) await ig.waitForContainer(containerId, accessToken);

  // Step 3: publish
  const mediaId = await ig.publishContainer(igUserId, accessToken, containerId);
  const permalink = await ig.permalinkOf(mediaId, accessToken);
  console.log(`[${AGENT}] Story published: media=${mediaId} ${permalink || ''}`);

  return {
    media_id: mediaId,
    container_id: containerId,
    image_url: imageUrl || null,
    video_url: videoUrl || null,
    media_source: mediaSource,
    permalink,
    media_type: 'STORIES',
    expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
    simulated: false,
    posted: true,
    platform: PLATFORM,
    pipeline_id: params.pipeline_id || null,
    created_at: new Date().toISOString(),
  };
};