← back to SmokeShop
lib/instagram-api.js
160 lines
/**
* Instagram Graph API — Meta OAuth + posting
* Handles authentication and media publishing for Instagram Business accounts
*/
const GRAPH_API_VERSION = 'v18.0';
const GRAPH_BASE = `https://graph.facebook.com/${GRAPH_API_VERSION}`;
/**
* Build the OAuth authorization URL for Meta login
*/
function getAuthUrl(appId, redirectUri) {
const scopes = [
'instagram_basic',
'instagram_content_publish',
'pages_show_list',
'pages_read_engagement',
].join(',');
return `https://www.facebook.com/${GRAPH_API_VERSION}/dialog/oauth?` +
`client_id=${appId}&redirect_uri=${encodeURIComponent(redirectUri)}` +
`&scope=${scopes}&response_type=code`;
}
/**
* Exchange authorization code for access token
*/
async function exchangeCodeForToken(code, appId, appSecret, redirectUri) {
const url = `${GRAPH_BASE}/oauth/access_token?` +
`client_id=${appId}&client_secret=${appSecret}` +
`&redirect_uri=${encodeURIComponent(redirectUri)}&code=${code}`;
const res = await fetch(url);
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(`Token exchange failed: ${JSON.stringify(err)}`);
}
return res.json();
}
/**
* Exchange short-lived token for long-lived token (60 days)
*/
async function getLongLivedToken(shortToken, appId, appSecret) {
const url = `${GRAPH_BASE}/oauth/access_token?` +
`grant_type=fb_exchange_token&client_id=${appId}` +
`&client_secret=${appSecret}&fb_exchange_token=${shortToken}`;
const res = await fetch(url);
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(`Long-lived token exchange failed: ${JSON.stringify(err)}`);
}
return res.json();
}
/**
* Get Instagram Business Account ID from Facebook Page
*/
async function getIGBusinessAccount(accessToken) {
// Get pages
const pagesRes = await fetch(`${GRAPH_BASE}/me/accounts?access_token=${accessToken}`);
const pages = await pagesRes.json();
if (!pages.data || pages.data.length === 0) {
throw new Error('No Facebook Pages found. You need a Facebook Page linked to an Instagram Business account.');
}
// Get IG account from first page
const pageId = pages.data[0].id;
const igRes = await fetch(
`${GRAPH_BASE}/${pageId}?fields=instagram_business_account&access_token=${accessToken}`
);
const igData = await igRes.json();
if (!igData.instagram_business_account) {
throw new Error('No Instagram Business account linked to this Facebook Page.');
}
return {
igAccountId: igData.instagram_business_account.id,
pageId,
pageName: pages.data[0].name,
};
}
/**
* Publish an image to Instagram
* Step 1: Create media container
* Step 2: Publish the container
*/
async function publishPost(igAccountId, accessToken, imageUrl, caption) {
// Step 1: Create media container
const containerRes = await fetch(
`${GRAPH_BASE}/${igAccountId}/media`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image_url: imageUrl,
caption,
access_token: accessToken,
}),
}
);
const container = await containerRes.json();
if (container.error) {
throw new Error(`Media container error: ${JSON.stringify(container.error)}`);
}
// Step 2: Publish
const publishRes = await fetch(
`${GRAPH_BASE}/${igAccountId}/media_publish`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
creation_id: container.id,
access_token: accessToken,
}),
}
);
const result = await publishRes.json();
if (result.error) {
throw new Error(`Publish error: ${JSON.stringify(result.error)}`);
}
return { mediaId: result.id, containerId: container.id };
}
/**
* Get insights for a media object
*/
async function getMediaInsights(mediaId, accessToken) {
const metrics = 'impressions,reach,engagement,saved';
const res = await fetch(
`${GRAPH_BASE}/${mediaId}/insights?metric=${metrics}&access_token=${accessToken}`
);
const data = await res.json();
if (data.error) return null;
const insights = {};
if (data.data) {
data.data.forEach(metric => {
insights[metric.name] = metric.values?.[0]?.value || 0;
});
}
return insights;
}
module.exports = {
getAuthUrl,
exchangeCodeForToken,
getLongLivedToken,
getIGBusinessAccount,
publishPost,
getMediaInsights,
};