import { type MetaAccountParams } from './accounts.ts' import { graphItems, graphRequest, requireConfirmOrDryRun, type JsonObject } from './core.ts' import { listPages } from './pages.ts' import { boundedLimit } from './validation.ts' export type InstagramAccount = { pageId: string pageName: string | null igUserId: string } export type InstagramMediaParams = MetaAccountParams & { igUserId: string limit?: number after?: string fields?: string } export type InstagramPublishParams = MetaAccountParams & { igUserId: string imageUrl?: string videoUrl?: string caption?: string dryRun?: boolean confirm?: boolean } const IG_USER_FIELDS = 'id,username,name,profile_picture_url,followers_count,media_count,website' const IG_MEDIA_FIELDS = 'id,caption,media_type,media_url,permalink,timestamp,like_count,comments_count' function igUserIdOf(value: unknown): string | null { if (!value || typeof value !== 'object') return null const id = (value as JsonObject).id return typeof id === 'string' ? id : null } /** Discover Instagram professional accounts linked to Pages the token can access. */ export async function listInstagramAccounts(params: MetaAccountParams & { limit?: number } = {}) { const pages = await listPages({ ...params, limit: params.limit, fields: 'id,name,instagram_business_account', }) const items: InstagramAccount[] = [] for (const page of pages.items) { const igUserId = igUserIdOf(page.instagram_business_account) if (!igUserId) continue items.push({ pageId: typeof page.id === 'string' ? page.id : '', pageName: typeof page.name === 'string' ? page.name : null, igUserId, }) } return { items, next: pages.next } } /** Read an Instagram professional account by caller-supplied id. */ export async function getInstagramUser(params: MetaAccountParams & { igUserId: string; fields?: string }) { const igUserId = params.igUserId?.trim() if (!igUserId) { throw new Error('getInstagramUser requires igUserId from listInstagramAccounts (or the account the caller named).') } return graphRequest({ ...params, path: '/' + encodeURIComponent(igUserId), query: { fields: params.fields ?? IG_USER_FIELDS }, }) } /** List recent Instagram media. */ export async function listMedia(params: InstagramMediaParams) { const igUserId = params.igUserId?.trim() if (!igUserId) throw new Error('listMedia requires igUserId.') const data = await graphRequest({ ...params, path: '/' + encodeURIComponent(igUserId) + '/media', query: { fields: params.fields ?? IG_MEDIA_FIELDS, limit: boundedLimit(params.limit, 10, 50), after: params.after, }, }) return graphItems(data) } /** Read one Instagram media object. */ export async function getMedia(params: MetaAccountParams & { mediaId: string; fields?: string }) { const mediaId = params.mediaId?.trim() if (!mediaId) throw new Error('getMedia requires mediaId.') return graphRequest({ ...params, path: '/' + encodeURIComponent(mediaId), query: { fields: params.fields ?? IG_MEDIA_FIELDS }, }) } /** List comments on an Instagram media object. */ export async function listComments( params: MetaAccountParams & { mediaId: string; limit?: number; after?: string }, ) { const mediaId = params.mediaId?.trim() if (!mediaId) throw new Error('listComments requires mediaId.') const data = await graphRequest({ ...params, path: '/' + encodeURIComponent(mediaId) + '/comments', query: { fields: 'id,text,timestamp,username,like_count', limit: boundedLimit(params.limit, 10, 50), after: params.after, }, }) return graphItems(data) } /** * Preview or publish Instagram media (container + publish). * Live publish requires confirm: true. Never posts without that confirmation. */ export async function publishMedia(params: InstagramPublishParams) { const igUserId = params.igUserId?.trim() if (!igUserId) throw new Error('publishMedia requires igUserId from listInstagramAccounts.') const imageUrl = params.imageUrl?.trim() const videoUrl = params.videoUrl?.trim() if (!imageUrl && !videoUrl) throw new Error('publishMedia requires imageUrl or videoUrl.') const containerBody: JsonObject = {} if (imageUrl) containerBody.image_url = imageUrl if (videoUrl) { containerBody.media_type = 'REELS' containerBody.video_url = videoUrl } if (params.caption) containerBody.caption = params.caption const containerPath = '/' + encodeURIComponent(igUserId) + '/media' const publishPath = '/' + encodeURIComponent(igUserId) + '/media_publish' const mode = requireConfirmOrDryRun({ dryRun: params.dryRun, confirm: params.confirm, action: 'publishMedia', }) if (mode.dryRun) { return { dryRun: true as const, steps: [ { method: 'POST', path: containerPath, body: containerBody }, { method: 'POST', path: publishPath, body: { creation_id: '' } }, ], } } const container = await graphRequest({ ...params, method: 'POST', path: containerPath, body: containerBody, }) const creationId = typeof container.id === 'string' ? container.id : null if (!creationId) throw new Error('Instagram media container did not return an id.') const published = await graphRequest({ ...params, method: 'POST', path: publishPath, body: { creation_id: creationId }, }) return { published: true as const, containerId: creationId, mediaId: published.id ?? null } } /** * Instagram Graph helpers (professional account linked to a Page). * @example * import { listInstagramAccounts } from 'kody:@kody/meta/instagram' * const { items } = await listInstagramAccounts() */ export default function instagram() { return { listInstagramAccounts, getInstagramUser, listMedia, getMedia, listComments, publishMedia, } }