← back to Norma
agents/instagram-agent/skills/post.js
125 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 accounts = require('../accounts');
const AGENT = 'instagram-agent';
const PLATFORM = 'instagram';
/**
* Resolve credentials. If `account` is given (a handle / page name / ig id),
* use the multi-account registry + shared META_ACCESS_TOKEN; otherwise fall
* back to the legacy single-account creds (DB / env).
*/
async function getCredentials(account) {
if (account) {
const a = accounts.resolve(account);
if (!a) throw new Error(`Unknown Instagram account "${account}" (run build-registry.js / see accounts.json)`);
return { has: a.has_token, userId: a.ig_user_id, accessToken: a.access_token, handle: a.handle };
}
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, handle } = await getCredentials(params.account);
const simulated = !hasCreds;
console.log(`[${AGENT}] Post skill invoked — account=${handle || params.account || 'default'} 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: wait for the container to finish, THEN publish. Image containers
// are USUALLY ready immediately, but Meta occasionally hasn't finished
// processing when publish is called and returns "Media ID is not available".
// Polling status_code -> FINISHED first fixes that race. Publishing only ever
// fires once, after readiness, so this can never cause a double-post.
try {
await ig.waitForContainer(containerId, accessToken, { tries: 12, delayMs: 2000 });
} catch (e) {
// A real processing ERROR is surfaced clearly (mirrors the reel path); a
// bare timeout still falls through to one publish attempt as a last resort.
if (/processing error/i.test(e.message)) throw e;
console.log(`[${AGENT}] container wait note: ${e.message} — attempting publish anyway`);
}
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(),
};
};