132 lines
3.5 KiB
JavaScript
132 lines
3.5 KiB
JavaScript
const axios = require('axios')
|
|
|
|
const AUTHORIZE_TOKEN_URL = 'https://api.instagram.com/oauth/access_token'
|
|
const GRAPH_BASE_URL = 'https://graph.instagram.com'
|
|
|
|
/**
|
|
* Exchange the short-lived OAuth `code` (from the Instagram Login redirect)
|
|
* for a short-lived (1hr) user access token.
|
|
*/
|
|
async function exchangeCodeForToken (code) {
|
|
const clientId = process.env.INSTAGRAM_CLIENT_ID
|
|
const clientSecret = process.env.INSTAGRAM_APP_SECRET
|
|
const redirectUri = process.env.INSTAGRAM_REDIRECT_URI
|
|
|
|
if (!clientId || !clientSecret || !redirectUri) {
|
|
throw new Error('Instagram OAuth is not configured (INSTAGRAM_CLIENT_ID/APP_SECRET/REDIRECT_URI)')
|
|
}
|
|
|
|
const params = new URLSearchParams({
|
|
client_id: clientId,
|
|
client_secret: clientSecret,
|
|
grant_type: 'authorization_code',
|
|
redirect_uri: redirectUri,
|
|
code,
|
|
})
|
|
|
|
const { data } = await axios.post(AUTHORIZE_TOKEN_URL, params, {
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
timeout: 15000,
|
|
})
|
|
|
|
return {
|
|
accessToken: data?.access_token,
|
|
userId: data?.user_id,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exchange a short-lived token for a long-lived one (~60 days).
|
|
*/
|
|
async function exchangeForLongLivedToken (shortLivedToken) {
|
|
const clientSecret = process.env.INSTAGRAM_APP_SECRET
|
|
if (!clientSecret) {
|
|
throw new Error('INSTAGRAM_APP_SECRET is not configured')
|
|
}
|
|
|
|
const { data } = await axios.get(`${GRAPH_BASE_URL}/access_token`, {
|
|
params: {
|
|
grant_type: 'ig_exchange_token',
|
|
client_secret: clientSecret,
|
|
access_token: shortLivedToken,
|
|
},
|
|
timeout: 15000,
|
|
})
|
|
|
|
const expiresInSeconds = Number(data?.expires_in) || 60 * 24 * 60 * 60
|
|
return {
|
|
accessToken: data?.access_token,
|
|
expiresAt: new Date(Date.now() + expiresInSeconds * 1000),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh a long-lived token before it expires (must be at least 24h old, valid).
|
|
*/
|
|
async function refreshLongLivedToken (longLivedToken) {
|
|
const { data } = await axios.get(`${GRAPH_BASE_URL}/refresh_access_token`, {
|
|
params: {
|
|
grant_type: 'ig_refresh_token',
|
|
access_token: longLivedToken,
|
|
},
|
|
timeout: 15000,
|
|
})
|
|
|
|
const expiresInSeconds = Number(data?.expires_in) || 60 * 24 * 60 * 60
|
|
return {
|
|
accessToken: data?.access_token,
|
|
expiresAt: new Date(Date.now() + expiresInSeconds * 1000),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Basic profile info for the connected Instagram Business/Creator account.
|
|
*/
|
|
async function getProfile (accessToken) {
|
|
const { data } = await axios.get(`${GRAPH_BASE_URL}/me`, {
|
|
params: {
|
|
fields: 'id,username,account_type,name,biography,profile_picture_url',
|
|
access_token: accessToken,
|
|
},
|
|
timeout: 15000,
|
|
})
|
|
|
|
return {
|
|
id: data?.id,
|
|
username: data?.username,
|
|
accountType: data?.account_type || null,
|
|
name: data?.name || null,
|
|
biography: data?.biography || null,
|
|
profilePictureUrl: data?.profile_picture_url || null,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Paginated media list (posts + reels). `after` is a Graph API paging cursor.
|
|
*/
|
|
async function getMedia (accessToken, { after } = {}) {
|
|
const { data } = await axios.get(`${GRAPH_BASE_URL}/me/media`, {
|
|
params: {
|
|
fields: 'id,caption,media_type,media_product_type,media_url,permalink,timestamp',
|
|
limit: 25,
|
|
access_token: accessToken,
|
|
...(after ? { after } : {}),
|
|
},
|
|
timeout: 15000,
|
|
})
|
|
|
|
return {
|
|
items: Array.isArray(data?.data) ? data.data : [],
|
|
nextCursor: data?.paging?.cursors?.after || null,
|
|
hasNext: Boolean(data?.paging?.next),
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
exchangeCodeForToken,
|
|
exchangeForLongLivedToken,
|
|
refreshLongLivedToken,
|
|
getProfile,
|
|
getMedia,
|
|
}
|