← 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(),
};
};