Files
modstagram-back/services/InstagramGraphService.js
payacom 3f416a7a60 full
2026-08-31 20:45:41 +03:30

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,
}