full
This commit is contained in:
131
services/InstagramGraphService.js
Normal file
131
services/InstagramGraphService.js
Normal file
@@ -0,0 +1,131 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user