@classytic/social 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +368 -0
- package/dist/base-Bw7e52V8.mjs +246 -0
- package/dist/base-Bw7e52V8.mjs.map +1 -0
- package/dist/base-DBtKFiSX.d.mts +226 -0
- package/dist/base-DBtKFiSX.d.mts.map +1 -0
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/client/index.d.mts +44 -0
- package/dist/client/index.d.mts.map +1 -0
- package/dist/client/index.mjs +154 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/common/index.d.mts +3 -0
- package/dist/common/index.mjs +7 -0
- package/dist/contracts-Cdwa4zlg.d.mts +121 -0
- package/dist/contracts-Cdwa4zlg.d.mts.map +1 -0
- package/dist/contracts-lCa069IK.mjs +221 -0
- package/dist/contracts-lCa069IK.mjs.map +1 -0
- package/dist/env-Bl0cwwjC.mjs +955 -0
- package/dist/env-Bl0cwwjC.mjs.map +1 -0
- package/dist/env-DxOZHf0p.d.mts +394 -0
- package/dist/env-DxOZHf0p.d.mts.map +1 -0
- package/dist/errors-Cm6LeKf7.mjs +32 -0
- package/dist/errors-Cm6LeKf7.mjs.map +1 -0
- package/dist/facebook-l_4CghaA.mjs +95 -0
- package/dist/facebook-l_4CghaA.mjs.map +1 -0
- package/dist/http-DpcLSR1M.mjs +197 -0
- package/dist/http-DpcLSR1M.mjs.map +1 -0
- package/dist/index.d.mts +42 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +71 -0
- package/dist/index.mjs.map +1 -0
- package/dist/instagram-BGaeUFU2.mjs +90 -0
- package/dist/instagram-BGaeUFU2.mjs.map +1 -0
- package/dist/linkedin-70whtVKa.mjs +101 -0
- package/dist/linkedin-70whtVKa.mjs.map +1 -0
- package/dist/meta-D3vcJU1c.mjs +126 -0
- package/dist/meta-D3vcJU1c.mjs.map +1 -0
- package/dist/pkce-jq5II68b.mjs +72 -0
- package/dist/pkce-jq5II68b.mjs.map +1 -0
- package/dist/polling-DZ1apXtA.mjs +25 -0
- package/dist/polling-DZ1apXtA.mjs.map +1 -0
- package/dist/providers/facebook.d.mts +135 -0
- package/dist/providers/facebook.d.mts.map +1 -0
- package/dist/providers/facebook.mjs +450 -0
- package/dist/providers/facebook.mjs.map +1 -0
- package/dist/providers/instagram.d.mts +122 -0
- package/dist/providers/instagram.d.mts.map +1 -0
- package/dist/providers/instagram.mjs +496 -0
- package/dist/providers/instagram.mjs.map +1 -0
- package/dist/providers/linkedin.d.mts +145 -0
- package/dist/providers/linkedin.d.mts.map +1 -0
- package/dist/providers/linkedin.mjs +574 -0
- package/dist/providers/linkedin.mjs.map +1 -0
- package/dist/providers/reddit.d.mts +102 -0
- package/dist/providers/reddit.d.mts.map +1 -0
- package/dist/providers/reddit.mjs +657 -0
- package/dist/providers/reddit.mjs.map +1 -0
- package/dist/providers/telegram.d.mts +139 -0
- package/dist/providers/telegram.d.mts.map +1 -0
- package/dist/providers/telegram.mjs +517 -0
- package/dist/providers/telegram.mjs.map +1 -0
- package/dist/providers/tiktok.d.mts +116 -0
- package/dist/providers/tiktok.d.mts.map +1 -0
- package/dist/providers/tiktok.mjs +676 -0
- package/dist/providers/tiktok.mjs.map +1 -0
- package/dist/providers/twitter.d.mts +150 -0
- package/dist/providers/twitter.d.mts.map +1 -0
- package/dist/providers/twitter.mjs +628 -0
- package/dist/providers/twitter.mjs.map +1 -0
- package/dist/providers/whatsapp.d.mts +79 -0
- package/dist/providers/whatsapp.d.mts.map +1 -0
- package/dist/providers/whatsapp.mjs +376 -0
- package/dist/providers/whatsapp.mjs.map +1 -0
- package/dist/providers/youtube.d.mts +153 -0
- package/dist/providers/youtube.d.mts.map +1 -0
- package/dist/providers/youtube.mjs +902 -0
- package/dist/providers/youtube.mjs.map +1 -0
- package/dist/reddit-B10kS4Se.mjs +126 -0
- package/dist/reddit-B10kS4Se.mjs.map +1 -0
- package/dist/schemas/index.d.mts +819 -0
- package/dist/schemas/index.d.mts.map +1 -0
- package/dist/schemas/index.mjs +31 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/dist/security-BXhfebWm.d.mts +338 -0
- package/dist/security-BXhfebWm.d.mts.map +1 -0
- package/dist/shared-Fvc6xQku.mjs +100 -0
- package/dist/shared-Fvc6xQku.mjs.map +1 -0
- package/dist/telegram-FaUHpZgB.mjs +107 -0
- package/dist/telegram-FaUHpZgB.mjs.map +1 -0
- package/dist/tiktok-B_bMk4G-.mjs +94 -0
- package/dist/tiktok-B_bMk4G-.mjs.map +1 -0
- package/dist/twitter-BC22zfuc.mjs +98 -0
- package/dist/twitter-BC22zfuc.mjs.map +1 -0
- package/dist/types-BFE4psYI.d.mts +102 -0
- package/dist/types-BFE4psYI.d.mts.map +1 -0
- package/dist/types-Bv27tcT0.d.mts +230 -0
- package/dist/types-Bv27tcT0.d.mts.map +1 -0
- package/dist/types-BwkKyqpi.d.mts +253 -0
- package/dist/types-BwkKyqpi.d.mts.map +1 -0
- package/dist/types-CJrHMDV9.mjs +27 -0
- package/dist/types-CJrHMDV9.mjs.map +1 -0
- package/dist/types-ClbVc2rc.d.mts +117 -0
- package/dist/types-ClbVc2rc.d.mts.map +1 -0
- package/dist/types-D91N16Ym.d.mts +242 -0
- package/dist/types-D91N16Ym.d.mts.map +1 -0
- package/dist/types-DfLp_ibQ.d.mts +178 -0
- package/dist/types-DfLp_ibQ.d.mts.map +1 -0
- package/dist/types-DfjDgEoJ.d.mts +88 -0
- package/dist/types-DfjDgEoJ.d.mts.map +1 -0
- package/dist/types-Dp5Z9VBr.mjs +23 -0
- package/dist/types-Dp5Z9VBr.mjs.map +1 -0
- package/dist/types-hriBJTsU.d.mts +129 -0
- package/dist/types-hriBJTsU.d.mts.map +1 -0
- package/dist/types-rn6UuLL8.d.mts +184 -0
- package/dist/types-rn6UuLL8.d.mts.map +1 -0
- package/dist/whatsapp-CFp7ryR4.mjs +101 -0
- package/dist/whatsapp-CFp7ryR4.mjs.map +1 -0
- package/dist/youtube-Bs0fdY7H.mjs +98 -0
- package/dist/youtube-Bs0fdY7H.mjs.map +1 -0
- package/package.json +148 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { t as PlatformProvider } from "../base-Bw7e52V8.mjs";
|
|
2
|
+
import { t as SocialError } from "../errors-Cm6LeKf7.mjs";
|
|
3
|
+
import { c as META_GRAPH_VERSION, i as metaRefreshLongLivedInstagram, n as metaExchangeLongLived, o as META_AUTH_URL, s as META_GRAPH_BASE } from "../meta-D3vcJU1c.mjs";
|
|
4
|
+
import { t as pollUntilComplete } from "../polling-DZ1apXtA.mjs";
|
|
5
|
+
import { t as InstagramCredentialsSchema } from "../instagram-BGaeUFU2.mjs";
|
|
6
|
+
|
|
7
|
+
//#region src/providers/instagram/index.ts
|
|
8
|
+
/**
|
|
9
|
+
* Instagram Platform Provider
|
|
10
|
+
* ===========================
|
|
11
|
+
* Instagram integration via Facebook/Meta Graph API.
|
|
12
|
+
*
|
|
13
|
+
* Features:
|
|
14
|
+
* - OAuth2 via Facebook Login dialog
|
|
15
|
+
* - Two-step token exchange (short-lived → long-lived, 60 days)
|
|
16
|
+
* - Instagram Business Account discovery from Facebook Page
|
|
17
|
+
* - Reels upload (via public video URL)
|
|
18
|
+
* - Single photo post (via public image URL)
|
|
19
|
+
* - Carousel post (mixed images/videos)
|
|
20
|
+
* - Container status polling
|
|
21
|
+
* - Credential validation
|
|
22
|
+
*
|
|
23
|
+
* Instagram API Quirks:
|
|
24
|
+
* - Uses Facebook OAuth (not Instagram's own dialog)
|
|
25
|
+
* - Token exchange is two-step: code → short-lived → long-lived
|
|
26
|
+
* - Token refresh uses access_token itself (not a separate refresh_token)
|
|
27
|
+
* - Refresh endpoint is on graph.instagram.com (not graph.facebook.com)
|
|
28
|
+
* - Video/image upload requires publicly accessible URLs (no binary upload)
|
|
29
|
+
* - Publishing is two-step: create container → poll status → publish
|
|
30
|
+
* - Unpublished containers expire after 24 hours (not true drafts)
|
|
31
|
+
* - Only Business/Creator accounts work (personal accounts unsupported since Dec 2024)
|
|
32
|
+
* - IG Business Account must be linked to a Facebook Page
|
|
33
|
+
*/
|
|
34
|
+
const GRAPH_API_VERSION = META_GRAPH_VERSION;
|
|
35
|
+
const FB_AUTH_URL = META_AUTH_URL;
|
|
36
|
+
const GRAPH_API_BASE = META_GRAPH_BASE;
|
|
37
|
+
var InstagramProvider = class extends PlatformProvider {
|
|
38
|
+
defaultRedirectUri;
|
|
39
|
+
scopes;
|
|
40
|
+
constructor(cfg = {}) {
|
|
41
|
+
super(cfg);
|
|
42
|
+
this.name = "instagram";
|
|
43
|
+
this.displayName = "Instagram";
|
|
44
|
+
this.authType = "oauth2";
|
|
45
|
+
this.defaultRedirectUri = cfg.redirectUri || `http://localhost:${cfg.port || 8060}/api/oauth/instagram/callback`;
|
|
46
|
+
this.scopes = [
|
|
47
|
+
"business_management",
|
|
48
|
+
"instagram_business_basic",
|
|
49
|
+
"instagram_business_content_publish",
|
|
50
|
+
"instagram_business_manage_comments",
|
|
51
|
+
"pages_show_list",
|
|
52
|
+
"pages_read_engagement"
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get Facebook OAuth authorization URL
|
|
57
|
+
* Instagram uses Facebook's OAuth dialog for authorization
|
|
58
|
+
*/
|
|
59
|
+
getAuthUrl(state, credentials = {}, _options) {
|
|
60
|
+
return `${FB_AUTH_URL}?${new URLSearchParams({
|
|
61
|
+
client_id: credentials.appId,
|
|
62
|
+
redirect_uri: credentials.redirectUri || this.defaultRedirectUri,
|
|
63
|
+
scope: this.scopes.join(","),
|
|
64
|
+
response_type: "code",
|
|
65
|
+
state
|
|
66
|
+
}).toString()}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Exchange authorization code for tokens (two-step).
|
|
70
|
+
*
|
|
71
|
+
* Step 1: code → short-lived token (1 hour)
|
|
72
|
+
* Step 2: short-lived → long-lived token (60 days)
|
|
73
|
+
*/
|
|
74
|
+
async exchangeCode(code, credentials = {}) {
|
|
75
|
+
return metaExchangeLongLived("instagram", {
|
|
76
|
+
graphVersion: GRAPH_API_VERSION,
|
|
77
|
+
clientId: credentials.appId,
|
|
78
|
+
clientSecret: credentials.appSecret,
|
|
79
|
+
redirectUri: credentials.redirectUri || this.defaultRedirectUri,
|
|
80
|
+
code
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Refresh long-lived token. Instagram uses the access token itself as the
|
|
85
|
+
* refresh credential (must be ≥24h old, not yet expired) and hits the
|
|
86
|
+
* `graph.instagram.com` refresh endpoint.
|
|
87
|
+
*/
|
|
88
|
+
async refreshToken(refreshToken) {
|
|
89
|
+
return metaRefreshLongLivedInstagram({ refreshToken });
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get Instagram Business Account info
|
|
93
|
+
*
|
|
94
|
+
* Flow:
|
|
95
|
+
* 1. Get user's Facebook Pages via /me/accounts
|
|
96
|
+
* 2. For first page, get linked instagram_business_account
|
|
97
|
+
* 3. Get IG profile details
|
|
98
|
+
*/
|
|
99
|
+
async getAccountInfo(accessToken) {
|
|
100
|
+
const pagesData = await this._graphGet("/me/accounts", accessToken);
|
|
101
|
+
if (!pagesData.data || pagesData.data.length === 0) throw new SocialError("instagram", "No Facebook Pages found. Instagram Business accounts must be linked to a Facebook Page.", {
|
|
102
|
+
statusCode: 400,
|
|
103
|
+
hint: "Create a Facebook Page and link your Instagram Business account to it."
|
|
104
|
+
});
|
|
105
|
+
let igUserId = null;
|
|
106
|
+
for (const page of pagesData.data) {
|
|
107
|
+
const pageData = await this._graphGet(`/${page.id}?fields=instagram_business_account`, accessToken);
|
|
108
|
+
if (pageData.instagram_business_account?.id) {
|
|
109
|
+
igUserId = pageData.instagram_business_account.id;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!igUserId) throw new SocialError("instagram", "No Instagram Business Account found linked to your Facebook Pages. Make sure your Instagram account is a Business or Creator account connected to a Facebook Page.", {
|
|
114
|
+
statusCode: 400,
|
|
115
|
+
hint: "Switch your Instagram to a Business or Creator account and link it to a Facebook Page."
|
|
116
|
+
});
|
|
117
|
+
const profile = await this._graphGet(`/${igUserId}?fields=id,username,name,profile_picture_url,followers_count,media_count`, accessToken);
|
|
118
|
+
return {
|
|
119
|
+
id: igUserId,
|
|
120
|
+
ig_user_id: igUserId,
|
|
121
|
+
name: profile.name || profile.username,
|
|
122
|
+
username: profile.username,
|
|
123
|
+
profileImage: profile.profile_picture_url,
|
|
124
|
+
followersCount: profile.followers_count,
|
|
125
|
+
mediaCount: profile.media_count
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Test credential validity
|
|
130
|
+
*/
|
|
131
|
+
async testCredential(credentialData) {
|
|
132
|
+
try {
|
|
133
|
+
if (credentialData.oauthTokenData) {
|
|
134
|
+
const tokenData = typeof credentialData.oauthTokenData === "string" ? JSON.parse(credentialData.oauthTokenData) : credentialData.oauthTokenData;
|
|
135
|
+
const accountInfo = await this.getAccountInfo(tokenData.access_token);
|
|
136
|
+
return {
|
|
137
|
+
status: "OK",
|
|
138
|
+
message: "Instagram credential is valid",
|
|
139
|
+
data: {
|
|
140
|
+
channelId: accountInfo.id,
|
|
141
|
+
channelTitle: accountInfo.name,
|
|
142
|
+
profileImage: accountInfo.profileImage ?? void 0
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (!credentialData.appId || !credentialData.appSecret) return {
|
|
147
|
+
status: "Error",
|
|
148
|
+
message: "Instagram credentials not configured — Meta App ID and App Secret are required"
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
status: "Pending",
|
|
152
|
+
message: "Credential needs OAuth authorization. Click \"Connect Account\" to link your Instagram."
|
|
153
|
+
};
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return {
|
|
156
|
+
status: "Error",
|
|
157
|
+
message: error.message || "Failed to validate Instagram credential"
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Upload a video as a Reel to Instagram
|
|
163
|
+
*
|
|
164
|
+
* Instagram requires publicly accessible video URLs (no binary upload).
|
|
165
|
+
* Flow: create container → poll status → publish
|
|
166
|
+
*
|
|
167
|
+
* @param params
|
|
168
|
+
* @param params.videoUrl - Publicly accessible video URL (required)
|
|
169
|
+
* @param params.title - Used as caption
|
|
170
|
+
* @param params.description - Appended to caption if title is empty
|
|
171
|
+
* @param params.tokens - { access_token, ig_user_id }
|
|
172
|
+
* @param params.onProgress - Progress callback
|
|
173
|
+
*/
|
|
174
|
+
async uploadVideo(params) {
|
|
175
|
+
const { videoUrl, title = "", description = "", tokens, scheduledAt, onProgress } = params;
|
|
176
|
+
if (scheduledAt) throw new SocialError("instagram", "Instagram does not support scheduled publishing via the API.", {
|
|
177
|
+
statusCode: 400,
|
|
178
|
+
retryable: false,
|
|
179
|
+
hint: "Use the job queue scheduledFor field instead — the scheduler will execute the upload at the scheduled time."
|
|
180
|
+
});
|
|
181
|
+
const accessToken = tokens.access_token;
|
|
182
|
+
const igUserId = tokens.ig_user_id;
|
|
183
|
+
if (!igUserId) throw new SocialError("instagram", "Instagram User ID not found in tokens. Re-connect your Instagram account.", { statusCode: 400 });
|
|
184
|
+
if (!videoUrl) throw new SocialError("instagram", "Instagram requires a publicly accessible video URL. Provide videoUrl in params.", { statusCode: 400 });
|
|
185
|
+
const caption = (title || description || "").substring(0, 2200);
|
|
186
|
+
const containerData = await this._graphPost(`/${igUserId}/media`, {
|
|
187
|
+
media_type: "REELS",
|
|
188
|
+
video_url: videoUrl,
|
|
189
|
+
caption
|
|
190
|
+
}, accessToken);
|
|
191
|
+
if (containerData.error) throw new SocialError("instagram", containerData.error.message || "Instagram Reels container creation failed");
|
|
192
|
+
const containerId = containerData.id;
|
|
193
|
+
if (onProgress) onProgress(10);
|
|
194
|
+
await this._pollContainerStatus(accessToken, containerId);
|
|
195
|
+
if (onProgress) onProgress(80);
|
|
196
|
+
const publishData = await this._graphPost(`/${igUserId}/media_publish`, { creation_id: containerId }, accessToken);
|
|
197
|
+
if (publishData.error) throw new SocialError("instagram", publishData.error.message || "Instagram Reels publish failed");
|
|
198
|
+
if (onProgress) onProgress(100);
|
|
199
|
+
return {
|
|
200
|
+
platformVideoId: publishData.id,
|
|
201
|
+
platformUrl: `https://www.instagram.com/reel/${publishData.id}/`,
|
|
202
|
+
status: "published",
|
|
203
|
+
uploadedAt: /* @__PURE__ */ new Date(),
|
|
204
|
+
metadata: {
|
|
205
|
+
title: caption,
|
|
206
|
+
containerId,
|
|
207
|
+
mediaId: publishData.id
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Create a single photo post on Instagram
|
|
213
|
+
*
|
|
214
|
+
* @param params
|
|
215
|
+
* @param params.imageUrl - Publicly accessible image URL
|
|
216
|
+
* @param params.caption - Post caption (max 2200 chars)
|
|
217
|
+
* @param params.tokens - { access_token, ig_user_id }
|
|
218
|
+
*/
|
|
219
|
+
async uploadPhoto(params) {
|
|
220
|
+
const { imageUrl, caption = "", tokens } = params;
|
|
221
|
+
const accessToken = tokens.access_token;
|
|
222
|
+
const igUserId = tokens.ig_user_id;
|
|
223
|
+
if (!igUserId) throw new SocialError("instagram", "Instagram User ID not found in tokens. Re-connect your Instagram account.", { statusCode: 400 });
|
|
224
|
+
const containerData = await this._graphPost(`/${igUserId}/media`, {
|
|
225
|
+
image_url: imageUrl,
|
|
226
|
+
caption: caption.substring(0, 2200)
|
|
227
|
+
}, accessToken);
|
|
228
|
+
if (containerData.error) throw new SocialError("instagram", containerData.error.message || "Instagram photo container creation failed");
|
|
229
|
+
await this._pollContainerStatus(accessToken, containerData.id);
|
|
230
|
+
const publishData = await this._graphPost(`/${igUserId}/media_publish`, { creation_id: containerData.id }, accessToken);
|
|
231
|
+
if (publishData.error) throw new SocialError("instagram", publishData.error.message || "Instagram photo publish failed");
|
|
232
|
+
return {
|
|
233
|
+
mediaId: publishData.id,
|
|
234
|
+
status: "published"
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Create a carousel post on Instagram
|
|
239
|
+
*
|
|
240
|
+
* @param params
|
|
241
|
+
* @param params.items - 2-10 media items with type ('IMAGE'|'VIDEO') and url
|
|
242
|
+
* @param params.caption - Post caption (max 2200 chars)
|
|
243
|
+
* @param params.tokens - { access_token, ig_user_id }
|
|
244
|
+
*/
|
|
245
|
+
async uploadCarousel(params) {
|
|
246
|
+
const { items, caption = "", tokens } = params;
|
|
247
|
+
const accessToken = tokens.access_token;
|
|
248
|
+
const igUserId = tokens.ig_user_id;
|
|
249
|
+
if (!igUserId) throw new SocialError("instagram", "Instagram User ID not found in tokens. Re-connect your Instagram account.", { statusCode: 400 });
|
|
250
|
+
if (!items || items.length < 2 || items.length > 10) throw new SocialError("instagram", "Carousel requires 2-10 media items", { statusCode: 400 });
|
|
251
|
+
const childIds = [];
|
|
252
|
+
for (const item of items) {
|
|
253
|
+
const body = item.type === "VIDEO" ? {
|
|
254
|
+
media_type: "VIDEO",
|
|
255
|
+
video_url: item.url,
|
|
256
|
+
is_carousel_item: true
|
|
257
|
+
} : {
|
|
258
|
+
image_url: item.url,
|
|
259
|
+
is_carousel_item: true
|
|
260
|
+
};
|
|
261
|
+
const child = await this._graphPost(`/${igUserId}/media`, body, accessToken);
|
|
262
|
+
if (child.error) throw new SocialError("instagram", child.error.message || "Instagram carousel child container failed");
|
|
263
|
+
childIds.push(child.id);
|
|
264
|
+
}
|
|
265
|
+
for (const childId of childIds) await this._pollContainerStatus(accessToken, childId);
|
|
266
|
+
const carouselData = await this._graphPost(`/${igUserId}/media`, {
|
|
267
|
+
media_type: "CAROUSEL",
|
|
268
|
+
children: childIds.join(","),
|
|
269
|
+
caption: caption.substring(0, 2200)
|
|
270
|
+
}, accessToken);
|
|
271
|
+
if (carouselData.error) throw new SocialError("instagram", carouselData.error.message || "Instagram carousel container creation failed");
|
|
272
|
+
const publishData = await this._graphPost(`/${igUserId}/media_publish`, { creation_id: carouselData.id }, accessToken);
|
|
273
|
+
if (publishData.error) throw new SocialError("instagram", publishData.error.message || "Instagram carousel publish failed");
|
|
274
|
+
return {
|
|
275
|
+
mediaId: publishData.id,
|
|
276
|
+
status: "published"
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Get a single media object by ID
|
|
281
|
+
*
|
|
282
|
+
* @param accessToken - Valid access token
|
|
283
|
+
* @param mediaId - Instagram media ID
|
|
284
|
+
*/
|
|
285
|
+
async getMedia(accessToken, mediaId) {
|
|
286
|
+
try {
|
|
287
|
+
const data = await this._graphGet(`/${mediaId}?fields=id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,like_count,comments_count`, accessToken);
|
|
288
|
+
return {
|
|
289
|
+
id: data.id,
|
|
290
|
+
caption: data.caption ?? null,
|
|
291
|
+
mediaType: data.media_type,
|
|
292
|
+
mediaUrl: data.media_url ?? null,
|
|
293
|
+
thumbnailUrl: data.thumbnail_url ?? null,
|
|
294
|
+
permalink: data.permalink ?? null,
|
|
295
|
+
timestamp: data.timestamp,
|
|
296
|
+
likeCount: data.like_count ?? null,
|
|
297
|
+
commentsCount: data.comments_count ?? null
|
|
298
|
+
};
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (error instanceof SocialError) throw error;
|
|
301
|
+
throw new SocialError("instagram", error.message || "Failed to get media", { originalError: error });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* List media for an Instagram user
|
|
306
|
+
*
|
|
307
|
+
* @param accessToken - Valid access token
|
|
308
|
+
* @param userId - IG user ID (if omitted, fetched via getAccountInfo)
|
|
309
|
+
* @param options - Pagination options (limit, after, before)
|
|
310
|
+
*/
|
|
311
|
+
async listMedia(accessToken, userId, options = {}) {
|
|
312
|
+
try {
|
|
313
|
+
let path = `/${userId || (await this.getAccountInfo(accessToken)).id}/media?fields=id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,like_count,comments_count&limit=${Math.min(Math.max(options.limit || 25, 1), 100)}`;
|
|
314
|
+
if (options.after) path += `&after=${options.after}`;
|
|
315
|
+
if (options.before) path += `&before=${options.before}`;
|
|
316
|
+
const data = await this._graphGet(path, accessToken);
|
|
317
|
+
return {
|
|
318
|
+
media: (data.data || []).map((item) => ({
|
|
319
|
+
id: item.id,
|
|
320
|
+
caption: item.caption ?? null,
|
|
321
|
+
mediaType: item.media_type,
|
|
322
|
+
mediaUrl: item.media_url ?? null,
|
|
323
|
+
thumbnailUrl: item.thumbnail_url ?? null,
|
|
324
|
+
permalink: item.permalink ?? null,
|
|
325
|
+
timestamp: item.timestamp,
|
|
326
|
+
likeCount: item.like_count ?? null,
|
|
327
|
+
commentsCount: item.comments_count ?? null
|
|
328
|
+
})),
|
|
329
|
+
paging: {
|
|
330
|
+
after: data.paging?.cursors?.after ?? null,
|
|
331
|
+
before: data.paging?.cursors?.before ?? null
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (error instanceof SocialError) throw error;
|
|
336
|
+
throw new SocialError("instagram", error.message || "Failed to list media", { originalError: error });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Get insights for a specific media object
|
|
341
|
+
*
|
|
342
|
+
* @param accessToken - Valid access token
|
|
343
|
+
* @param mediaId - Instagram media ID
|
|
344
|
+
*/
|
|
345
|
+
async getMediaInsights(accessToken, mediaId) {
|
|
346
|
+
try {
|
|
347
|
+
const data = await this._graphGet(`/${mediaId}/insights?metric=reach,impressions,engagement,saved,likes,comments,shares`, accessToken);
|
|
348
|
+
const insights = {
|
|
349
|
+
reach: null,
|
|
350
|
+
impressions: null,
|
|
351
|
+
engagement: null,
|
|
352
|
+
saved: null,
|
|
353
|
+
likes: null,
|
|
354
|
+
comments: null,
|
|
355
|
+
shares: null
|
|
356
|
+
};
|
|
357
|
+
if (data.data) for (const entry of data.data) {
|
|
358
|
+
const key = entry.name;
|
|
359
|
+
if (key in insights) insights[key] = entry.values?.[0]?.value ?? null;
|
|
360
|
+
}
|
|
361
|
+
return insights;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (error instanceof SocialError) throw error;
|
|
364
|
+
throw new SocialError("instagram", error.message || "Failed to get media insights", { originalError: error });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
getCredentialZodSchema() {
|
|
368
|
+
return InstagramCredentialsSchema;
|
|
369
|
+
}
|
|
370
|
+
getCredentialSchema() {
|
|
371
|
+
return [{
|
|
372
|
+
name: "appId",
|
|
373
|
+
displayName: "Meta App ID",
|
|
374
|
+
type: "text",
|
|
375
|
+
required: true,
|
|
376
|
+
description: "App ID from Meta Developer Portal (developers.facebook.com)"
|
|
377
|
+
}, {
|
|
378
|
+
name: "appSecret",
|
|
379
|
+
displayName: "Meta App Secret",
|
|
380
|
+
type: "password",
|
|
381
|
+
required: true,
|
|
382
|
+
description: "App Secret from Meta Developer Portal"
|
|
383
|
+
}];
|
|
384
|
+
}
|
|
385
|
+
getMetadata() {
|
|
386
|
+
return {
|
|
387
|
+
name: this.name,
|
|
388
|
+
displayName: this.displayName,
|
|
389
|
+
authType: this.authType,
|
|
390
|
+
icon: "instagram",
|
|
391
|
+
brandColor: "#E4405F",
|
|
392
|
+
description: "Share reels and posts on Instagram",
|
|
393
|
+
scopes: this.scopes,
|
|
394
|
+
scopeDescriptions: {
|
|
395
|
+
instagram_business_basic: "View your Instagram profile info and media",
|
|
396
|
+
instagram_business_content_publish: "Publish posts and reels to your Instagram account",
|
|
397
|
+
instagram_business_manage_comments: "Read and manage comments on your Instagram posts",
|
|
398
|
+
pages_show_list: "View your Facebook Pages (required to find linked Instagram account)",
|
|
399
|
+
pages_read_engagement: "Read engagement data from your Facebook Pages"
|
|
400
|
+
},
|
|
401
|
+
setupGuide: [
|
|
402
|
+
{
|
|
403
|
+
step: 1,
|
|
404
|
+
title: "Create Meta Developer Account",
|
|
405
|
+
description: "Go to developers.facebook.com and create an account or log in"
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
step: 2,
|
|
409
|
+
title: "Create a New App",
|
|
410
|
+
description: "Click \"My Apps\" → \"Create App\". Enter your app name and email. On the use case step, filter by \"Content management\" and select \"Manage messaging & content on Instagram\""
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
step: 3,
|
|
414
|
+
title: "Connect Business Portfolio",
|
|
415
|
+
description: "Select or create a Business Portfolio when prompted. Click \"Create App\" to proceed to the dashboard"
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
step: 4,
|
|
419
|
+
title: "Add Permissions",
|
|
420
|
+
description: "Click \"Add all required permissions\" in step 1. Then go to \"Permissions and features\" and click \"+ Add\" for: instagram_business_content_publish, pages_show_list, pages_read_engagement. The use case auto-adds instagram_business_basic and instagram_business_manage_comments"
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
step: 5,
|
|
424
|
+
title: "Set Redirect URI",
|
|
425
|
+
description: `Go to "Facebook Login for Business" → "Settings". Add this as a Valid OAuth Redirect URI: ${this.defaultRedirectUri}`
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
step: 6,
|
|
429
|
+
title: "Copy Credentials",
|
|
430
|
+
description: "Go to App Settings → Basic. Copy the Facebook App ID and App Secret (not the Instagram-specific ones). These are used for the OAuth flow"
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
step: 7,
|
|
434
|
+
title: "Go Live",
|
|
435
|
+
description: "Switch app to \"Live\" mode. For development, Standard Access works with app role users. For production, request Advanced Access via App Review"
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
step: 8,
|
|
439
|
+
title: "Account Requirements",
|
|
440
|
+
description: "Your Instagram account must be a Business or Creator account linked to a Facebook Page. Personal accounts are not supported"
|
|
441
|
+
}
|
|
442
|
+
],
|
|
443
|
+
supportsScheduling: false,
|
|
444
|
+
supportsEnvironment: false,
|
|
445
|
+
redirectUriPattern: this.defaultRedirectUri,
|
|
446
|
+
credentialSchema: this.getCredentialSchema()
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Make a GET request to the Graph API
|
|
451
|
+
* @private
|
|
452
|
+
*/
|
|
453
|
+
async _graphGet(path, accessToken) {
|
|
454
|
+
const url = `${GRAPH_API_BASE}${path}${path.includes("?") ? "&" : "?"}access_token=${accessToken}`;
|
|
455
|
+
const data = await (await fetch(url)).json();
|
|
456
|
+
if (data.error) throw new SocialError("instagram", data.error.message || `Graph API GET failed: ${path}`);
|
|
457
|
+
return data;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Make a POST request to the Graph API
|
|
461
|
+
* @private
|
|
462
|
+
*/
|
|
463
|
+
async _graphPost(path, body, accessToken) {
|
|
464
|
+
const url = `${GRAPH_API_BASE}${path}`;
|
|
465
|
+
return (await fetch(url, {
|
|
466
|
+
method: "POST",
|
|
467
|
+
headers: { "Content-Type": "application/json" },
|
|
468
|
+
body: JSON.stringify({
|
|
469
|
+
...body,
|
|
470
|
+
access_token: accessToken
|
|
471
|
+
})
|
|
472
|
+
})).json();
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Poll container status until FINISHED, ERROR, or timeout
|
|
476
|
+
* @private
|
|
477
|
+
*/
|
|
478
|
+
async _pollContainerStatus(accessToken, containerId, maxAttempts = 30, intervalMs = 5e3) {
|
|
479
|
+
return pollUntilComplete({
|
|
480
|
+
fn: () => this._graphGet(`/${containerId}?fields=status_code,status`, accessToken),
|
|
481
|
+
isComplete: (data) => data.status_code === "FINISHED",
|
|
482
|
+
getError: (data) => {
|
|
483
|
+
if (data.status_code === "ERROR") return new SocialError("instagram", `Container processing failed: ${data.status || "Unknown error"}`, { retryable: true });
|
|
484
|
+
if (data.status_code === "EXPIRED") return new SocialError("instagram", "Container expired before publishing (24-hour limit)", { retryable: false });
|
|
485
|
+
return null;
|
|
486
|
+
},
|
|
487
|
+
maxAttempts,
|
|
488
|
+
intervalMs,
|
|
489
|
+
label: "InstagramProvider"
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
//#endregion
|
|
495
|
+
export { InstagramProvider };
|
|
496
|
+
//# sourceMappingURL=instagram.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instagram.mjs","names":[],"sources":["../../src/providers/instagram/index.ts"],"sourcesContent":["/**\n * Instagram Platform Provider\n * ===========================\n * Instagram integration via Facebook/Meta Graph API.\n *\n * Features:\n * - OAuth2 via Facebook Login dialog\n * - Two-step token exchange (short-lived → long-lived, 60 days)\n * - Instagram Business Account discovery from Facebook Page\n * - Reels upload (via public video URL)\n * - Single photo post (via public image URL)\n * - Carousel post (mixed images/videos)\n * - Container status polling\n * - Credential validation\n *\n * Instagram API Quirks:\n * - Uses Facebook OAuth (not Instagram's own dialog)\n * - Token exchange is two-step: code → short-lived → long-lived\n * - Token refresh uses access_token itself (not a separate refresh_token)\n * - Refresh endpoint is on graph.instagram.com (not graph.facebook.com)\n * - Video/image upload requires publicly accessible URLs (no binary upload)\n * - Publishing is two-step: create container → poll status → publish\n * - Unpublished containers expire after 24 hours (not true drafts)\n * - Only Business/Creator accounts work (personal accounts unsupported since Dec 2024)\n * - IG Business Account must be linked to a Facebook Page\n */\n\nimport {\n PlatformProvider,\n type AuthUrlOptions,\n type CredentialField,\n type ProviderMetadata,\n type UploadResult,\n type OAuthTokens,\n type ProviderConfig,\n} from '../../base.js';\nimport { SocialError } from '../../errors.js';\nimport { pollUntilComplete } from '../../utils/polling.js';\nimport { InstagramCredentialsSchema } from '../../schemas/instagram.js';\nimport { META_GRAPH_VERSION, META_GRAPH_BASE, META_AUTH_URL, META_TOKEN_URL } from '../../common/meta.js';\nimport {\n metaExchangeLongLived,\n metaRefreshLongLivedInstagram,\n} from '../../common/oauth/index.js';\nimport type { z } from 'zod';\nimport type {\n InstagramTokens,\n InstagramUploadVideoParams,\n InstagramUploadPhotoParams,\n InstagramUploadCarouselParams,\n CarouselItem,\n InstagramAccountInfo,\n InstagramCredentialData,\n InstagramTestResult,\n InstagramMedia,\n ListMediaOptions,\n ListMediaResult,\n MediaInsights,\n} from './types.js';\n\nexport type {\n InstagramTokens,\n InstagramUploadVideoParams,\n InstagramUploadPhotoParams,\n InstagramUploadCarouselParams,\n CarouselItem,\n InstagramAccountInfo,\n InstagramCredentialData,\n InstagramTestResult,\n InstagramMedia,\n InstagramMediaType,\n InstagramUserTag,\n ListMediaOptions,\n ListMediaResult,\n MediaInsights,\n ContainerStatusCode,\n ContainerStatus,\n InstagramSetupStep,\n} from './types.js';\n\n// ─── Graph API Endpoints (version in src/common/meta.ts) ───────────────────\n\nconst GRAPH_API_VERSION = META_GRAPH_VERSION;\nconst FB_AUTH_URL = META_AUTH_URL;\nconst FB_TOKEN_URL = META_TOKEN_URL;\nconst GRAPH_API_BASE = META_GRAPH_BASE;\nconst IG_REFRESH_URL = 'https://graph.instagram.com/refresh_access_token';\n\n// ─── Provider ───────────────────────────────────────────────────────────────\n\nexport class InstagramProvider extends PlatformProvider {\n private defaultRedirectUri: string;\n private scopes: string[];\n\n constructor(cfg: ProviderConfig = {}) {\n super(cfg);\n this.name = 'instagram';\n this.displayName = 'Instagram';\n this.authType = 'oauth2';\n\n this.defaultRedirectUri =\n cfg.redirectUri ||\n `http://localhost:${cfg.port || 8060}/api/oauth/instagram/callback`;\n\n this.scopes = [\n 'business_management',\n 'instagram_business_basic',\n 'instagram_business_content_publish',\n 'instagram_business_manage_comments',\n 'pages_show_list',\n 'pages_read_engagement',\n ];\n }\n\n // ─── OAuth ──────────────────────────────────────────────────────────────\n\n /**\n * Get Facebook OAuth authorization URL\n * Instagram uses Facebook's OAuth dialog for authorization\n */\n getAuthUrl(state: string, credentials: Record<string, any> = {}, _options?: AuthUrlOptions): string {\n const params = new URLSearchParams({\n client_id: credentials.appId,\n redirect_uri: credentials.redirectUri || this.defaultRedirectUri,\n scope: this.scopes.join(','),\n response_type: 'code',\n state,\n });\n\n return `${FB_AUTH_URL}?${params.toString()}`;\n }\n\n /**\n * Exchange authorization code for tokens (two-step).\n *\n * Step 1: code → short-lived token (1 hour)\n * Step 2: short-lived → long-lived token (60 days)\n */\n async exchangeCode(code: string, credentials: Record<string, any> = {}): Promise<OAuthTokens> {\n return metaExchangeLongLived('instagram', {\n graphVersion: GRAPH_API_VERSION,\n clientId: credentials.appId,\n clientSecret: credentials.appSecret,\n redirectUri: credentials.redirectUri || this.defaultRedirectUri,\n code,\n });\n }\n\n /**\n * Refresh long-lived token. Instagram uses the access token itself as the\n * refresh credential (must be ≥24h old, not yet expired) and hits the\n * `graph.instagram.com` refresh endpoint.\n */\n async refreshToken(refreshToken: string): Promise<OAuthTokens> {\n return metaRefreshLongLivedInstagram({ refreshToken });\n }\n\n // ─── Account Info ───────────────────────────────────────────────────────\n\n /**\n * Get Instagram Business Account info\n *\n * Flow:\n * 1. Get user's Facebook Pages via /me/accounts\n * 2. For first page, get linked instagram_business_account\n * 3. Get IG profile details\n */\n async getAccountInfo(accessToken: string): Promise<InstagramAccountInfo> {\n // Step 1: Get user's Facebook Pages\n const pagesData: any = await this._graphGet('/me/accounts', accessToken);\n\n if (!pagesData.data || pagesData.data.length === 0) {\n throw new SocialError('instagram',\n 'No Facebook Pages found. Instagram Business accounts must be linked to a Facebook Page.',\n { statusCode: 400, hint: 'Create a Facebook Page and link your Instagram Business account to it.' },\n );\n }\n\n // Step 2: Find a page with a linked Instagram Business Account\n let igUserId: string | null = null;\n for (const page of pagesData.data) {\n const pageData: any = await this._graphGet(\n `/${page.id}?fields=instagram_business_account`,\n accessToken,\n );\n\n if (pageData.instagram_business_account?.id) {\n igUserId = pageData.instagram_business_account.id;\n break;\n }\n }\n\n if (!igUserId) {\n throw new SocialError('instagram',\n 'No Instagram Business Account found linked to your Facebook Pages. ' +\n 'Make sure your Instagram account is a Business or Creator account connected to a Facebook Page.',\n { statusCode: 400, hint: 'Switch your Instagram to a Business or Creator account and link it to a Facebook Page.' },\n );\n }\n\n // Step 3: Get Instagram profile details\n const profile: any = await this._graphGet(\n `/${igUserId}?fields=id,username,name,profile_picture_url,followers_count,media_count`,\n accessToken,\n );\n\n return {\n id: igUserId,\n ig_user_id: igUserId,\n name: profile.name || profile.username,\n username: profile.username,\n profileImage: profile.profile_picture_url,\n followersCount: profile.followers_count,\n mediaCount: profile.media_count,\n };\n }\n\n /**\n * Test credential validity\n */\n async testCredential(credentialData: InstagramCredentialData): Promise<InstagramTestResult> {\n try {\n if (credentialData.oauthTokenData) {\n const tokenData: OAuthTokens = typeof credentialData.oauthTokenData === 'string'\n ? JSON.parse(credentialData.oauthTokenData)\n : credentialData.oauthTokenData;\n const accountInfo = await this.getAccountInfo(tokenData.access_token);\n\n return {\n status: 'OK',\n message: 'Instagram credential is valid',\n data: {\n channelId: accountInfo.id,\n channelTitle: accountInfo.name,\n profileImage: accountInfo.profileImage ?? undefined,\n },\n };\n }\n\n if (!credentialData.appId || !credentialData.appSecret) {\n return {\n status: 'Error',\n message: 'Instagram credentials not configured — Meta App ID and App Secret are required',\n };\n }\n\n return {\n status: 'Pending',\n message: 'Credential needs OAuth authorization. Click \"Connect Account\" to link your Instagram.',\n };\n } catch (error: any) {\n return {\n status: 'Error',\n message: error.message || 'Failed to validate Instagram credential',\n };\n }\n }\n\n // ─── Video Upload (Reels) ──────────────────────────────────────────────\n\n /**\n * Upload a video as a Reel to Instagram\n *\n * Instagram requires publicly accessible video URLs (no binary upload).\n * Flow: create container → poll status → publish\n *\n * @param params\n * @param params.videoUrl - Publicly accessible video URL (required)\n * @param params.title - Used as caption\n * @param params.description - Appended to caption if title is empty\n * @param params.tokens - { access_token, ig_user_id }\n * @param params.onProgress - Progress callback\n */\n async uploadVideo(params: InstagramUploadVideoParams): Promise<UploadResult> {\n const {\n videoUrl,\n title = '',\n description = '',\n tokens,\n scheduledAt,\n onProgress,\n } = params;\n\n // Instagram API does not support scheduled publishing\n if (scheduledAt) {\n throw new SocialError('instagram',\n 'Instagram does not support scheduled publishing via the API.',\n { statusCode: 400, retryable: false, hint: 'Use the job queue scheduledFor field instead — the scheduler will execute the upload at the scheduled time.' },\n );\n }\n\n const accessToken: string = tokens.access_token;\n const igUserId: string = tokens.ig_user_id;\n\n if (!igUserId) {\n throw new SocialError('instagram', 'Instagram User ID not found in tokens. Re-connect your Instagram account.', { statusCode: 400 });\n }\n\n if (!videoUrl) {\n throw new SocialError('instagram', 'Instagram requires a publicly accessible video URL. Provide videoUrl in params.', { statusCode: 400 });\n }\n\n const caption: string = (title || description || '').substring(0, 2200);\n\n // Step 1: Create Reels container\n const containerData: any = await this._graphPost(`/${igUserId}/media`, {\n media_type: 'REELS',\n video_url: videoUrl,\n caption,\n }, accessToken);\n\n if (containerData.error) {\n throw new SocialError('instagram', containerData.error.message || 'Instagram Reels container creation failed');\n }\n\n const containerId: string = containerData.id;\n if (onProgress) onProgress(10);\n\n // Step 2: Poll container status until FINISHED\n await this._pollContainerStatus(accessToken, containerId);\n if (onProgress) onProgress(80);\n\n // Step 3: Publish\n const publishData: any = await this._graphPost(`/${igUserId}/media_publish`, {\n creation_id: containerId,\n }, accessToken);\n\n if (publishData.error) {\n throw new SocialError('instagram', publishData.error.message || 'Instagram Reels publish failed');\n }\n\n if (onProgress) onProgress(100);\n\n return {\n platformVideoId: publishData.id,\n platformUrl: `https://www.instagram.com/reel/${publishData.id}/`,\n status: 'published',\n uploadedAt: new Date(),\n metadata: {\n title: caption,\n containerId,\n mediaId: publishData.id,\n },\n };\n }\n\n // ─── Photo Post ─────────────────────────────────────────────────────────\n\n /**\n * Create a single photo post on Instagram\n *\n * @param params\n * @param params.imageUrl - Publicly accessible image URL\n * @param params.caption - Post caption (max 2200 chars)\n * @param params.tokens - { access_token, ig_user_id }\n */\n async uploadPhoto(params: InstagramUploadPhotoParams): Promise<{ mediaId: string; status: string }> {\n const { imageUrl, caption = '', tokens } = params;\n\n const accessToken: string = tokens.access_token;\n const igUserId: string = tokens.ig_user_id;\n\n if (!igUserId) {\n throw new SocialError('instagram', 'Instagram User ID not found in tokens. Re-connect your Instagram account.', { statusCode: 400 });\n }\n\n // Step 1: Create image container\n const containerData: any = await this._graphPost(`/${igUserId}/media`, {\n image_url: imageUrl,\n caption: caption.substring(0, 2200),\n }, accessToken);\n\n if (containerData.error) {\n throw new SocialError('instagram', containerData.error.message || 'Instagram photo container creation failed');\n }\n\n // Step 2: Poll (images are usually instant, but check anyway)\n await this._pollContainerStatus(accessToken, containerData.id);\n\n // Step 3: Publish\n const publishData: any = await this._graphPost(`/${igUserId}/media_publish`, {\n creation_id: containerData.id,\n }, accessToken);\n\n if (publishData.error) {\n throw new SocialError('instagram', publishData.error.message || 'Instagram photo publish failed');\n }\n\n return {\n mediaId: publishData.id,\n status: 'published',\n };\n }\n\n // ─── Carousel Post ──────────────────────────────────────────────────────\n\n /**\n * Create a carousel post on Instagram\n *\n * @param params\n * @param params.items - 2-10 media items with type ('IMAGE'|'VIDEO') and url\n * @param params.caption - Post caption (max 2200 chars)\n * @param params.tokens - { access_token, ig_user_id }\n */\n async uploadCarousel(params: InstagramUploadCarouselParams): Promise<{ mediaId: string; status: string }> {\n const { items, caption = '', tokens } = params;\n\n const accessToken: string = tokens.access_token;\n const igUserId: string = tokens.ig_user_id;\n\n if (!igUserId) {\n throw new SocialError('instagram', 'Instagram User ID not found in tokens. Re-connect your Instagram account.', { statusCode: 400 });\n }\n\n if (!items || items.length < 2 || items.length > 10) {\n throw new SocialError('instagram', 'Carousel requires 2-10 media items', { statusCode: 400 });\n }\n\n // Step 1: Create child containers\n const childIds: string[] = [];\n for (const item of items) {\n const body: Record<string, any> = item.type === 'VIDEO'\n ? { media_type: 'VIDEO', video_url: item.url, is_carousel_item: true }\n : { image_url: item.url, is_carousel_item: true };\n\n const child: any = await this._graphPost(`/${igUserId}/media`, body, accessToken);\n if (child.error) {\n throw new SocialError('instagram', child.error.message || 'Instagram carousel child container failed');\n }\n childIds.push(child.id);\n }\n\n // Step 2: Wait for all children to finish processing\n for (const childId of childIds) {\n await this._pollContainerStatus(accessToken, childId);\n }\n\n // Step 3: Create carousel container\n const carouselData: any = await this._graphPost(`/${igUserId}/media`, {\n media_type: 'CAROUSEL',\n children: childIds.join(','),\n caption: caption.substring(0, 2200),\n }, accessToken);\n\n if (carouselData.error) {\n throw new SocialError('instagram', carouselData.error.message || 'Instagram carousel container creation failed');\n }\n\n // Step 4: Publish\n const publishData: any = await this._graphPost(`/${igUserId}/media_publish`, {\n creation_id: carouselData.id,\n }, accessToken);\n\n if (publishData.error) {\n throw new SocialError('instagram', publishData.error.message || 'Instagram carousel publish failed');\n }\n\n return {\n mediaId: publishData.id,\n status: 'published',\n };\n }\n\n // ─── Media Read ──────────────────────────────────────────────────────\n\n /**\n * Get a single media object by ID\n *\n * @param accessToken - Valid access token\n * @param mediaId - Instagram media ID\n */\n async getMedia(accessToken: string, mediaId: string): Promise<InstagramMedia> {\n try {\n const data: any = await this._graphGet(\n `/${mediaId}?fields=id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,like_count,comments_count`,\n accessToken,\n );\n\n return {\n id: data.id,\n caption: data.caption ?? null,\n mediaType: data.media_type,\n mediaUrl: data.media_url ?? null,\n thumbnailUrl: data.thumbnail_url ?? null,\n permalink: data.permalink ?? null,\n timestamp: data.timestamp,\n likeCount: data.like_count ?? null,\n commentsCount: data.comments_count ?? null,\n };\n } catch (error: any) {\n if (error instanceof SocialError) throw error;\n throw new SocialError('instagram', error.message || 'Failed to get media', { originalError: error });\n }\n }\n\n /**\n * List media for an Instagram user\n *\n * @param accessToken - Valid access token\n * @param userId - IG user ID (if omitted, fetched via getAccountInfo)\n * @param options - Pagination options (limit, after, before)\n */\n async listMedia(accessToken: string, userId?: string, options: ListMediaOptions = {}): Promise<ListMediaResult> {\n try {\n const igUserId = userId || (await this.getAccountInfo(accessToken)).id;\n const limit = Math.min(Math.max(options.limit || 25, 1), 100);\n\n let path = `/${igUserId}/media?fields=id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,like_count,comments_count&limit=${limit}`;\n if (options.after) path += `&after=${options.after}`;\n if (options.before) path += `&before=${options.before}`;\n\n const data: any = await this._graphGet(path, accessToken);\n\n const media: InstagramMedia[] = (data.data || []).map((item: any) => ({\n id: item.id,\n caption: item.caption ?? null,\n mediaType: item.media_type,\n mediaUrl: item.media_url ?? null,\n thumbnailUrl: item.thumbnail_url ?? null,\n permalink: item.permalink ?? null,\n timestamp: item.timestamp,\n likeCount: item.like_count ?? null,\n commentsCount: item.comments_count ?? null,\n }));\n\n return {\n media,\n paging: {\n after: data.paging?.cursors?.after ?? null,\n before: data.paging?.cursors?.before ?? null,\n },\n };\n } catch (error: any) {\n if (error instanceof SocialError) throw error;\n throw new SocialError('instagram', error.message || 'Failed to list media', { originalError: error });\n }\n }\n\n /**\n * Get insights for a specific media object\n *\n * @param accessToken - Valid access token\n * @param mediaId - Instagram media ID\n */\n async getMediaInsights(accessToken: string, mediaId: string): Promise<MediaInsights> {\n try {\n const data: any = await this._graphGet(\n `/${mediaId}/insights?metric=reach,impressions,engagement,saved,likes,comments,shares`,\n accessToken,\n );\n\n const insights: MediaInsights = {\n reach: null,\n impressions: null,\n engagement: null,\n saved: null,\n likes: null,\n comments: null,\n shares: null,\n };\n\n if (data.data) {\n for (const entry of data.data) {\n const key = entry.name as keyof MediaInsights;\n if (key in insights) {\n insights[key] = entry.values?.[0]?.value ?? null;\n }\n }\n }\n\n return insights;\n } catch (error: any) {\n if (error instanceof SocialError) throw error;\n throw new SocialError('instagram', error.message || 'Failed to get media insights', { originalError: error });\n }\n }\n\n // ─── Schema & Metadata ────────────────────────────────────────────────\n\n getCredentialZodSchema(): z.ZodType {\n return InstagramCredentialsSchema;\n }\n\n getCredentialSchema(): CredentialField[] {\n return [\n {\n name: 'appId',\n displayName: 'Meta App ID',\n type: 'text',\n required: true,\n description: 'App ID from Meta Developer Portal (developers.facebook.com)',\n },\n {\n name: 'appSecret',\n displayName: 'Meta App Secret',\n type: 'password',\n required: true,\n description: 'App Secret from Meta Developer Portal',\n },\n ];\n }\n\n getMetadata(): ProviderMetadata {\n return {\n name: this.name,\n displayName: this.displayName,\n authType: this.authType,\n icon: 'instagram',\n brandColor: '#E4405F',\n description: 'Share reels and posts on Instagram',\n scopes: this.scopes,\n scopeDescriptions: {\n instagram_business_basic: 'View your Instagram profile info and media',\n instagram_business_content_publish: 'Publish posts and reels to your Instagram account',\n instagram_business_manage_comments: 'Read and manage comments on your Instagram posts',\n pages_show_list: 'View your Facebook Pages (required to find linked Instagram account)',\n pages_read_engagement: 'Read engagement data from your Facebook Pages',\n },\n setupGuide: [\n {\n step: 1,\n title: 'Create Meta Developer Account',\n description: 'Go to developers.facebook.com and create an account or log in',\n },\n {\n step: 2,\n title: 'Create a New App',\n description: 'Click \"My Apps\" → \"Create App\". Enter your app name and email. On the use case step, filter by \"Content management\" and select \"Manage messaging & content on Instagram\"',\n },\n {\n step: 3,\n title: 'Connect Business Portfolio',\n description: 'Select or create a Business Portfolio when prompted. Click \"Create App\" to proceed to the dashboard',\n },\n {\n step: 4,\n title: 'Add Permissions',\n description: 'Click \"Add all required permissions\" in step 1. Then go to \"Permissions and features\" and click \"+ Add\" for: instagram_business_content_publish, pages_show_list, pages_read_engagement. The use case auto-adds instagram_business_basic and instagram_business_manage_comments',\n },\n {\n step: 5,\n title: 'Set Redirect URI',\n description: `Go to \"Facebook Login for Business\" → \"Settings\". Add this as a Valid OAuth Redirect URI: ${this.defaultRedirectUri}`,\n },\n {\n step: 6,\n title: 'Copy Credentials',\n description: 'Go to App Settings → Basic. Copy the Facebook App ID and App Secret (not the Instagram-specific ones). These are used for the OAuth flow',\n },\n {\n step: 7,\n title: 'Go Live',\n description: 'Switch app to \"Live\" mode. For development, Standard Access works with app role users. For production, request Advanced Access via App Review',\n },\n {\n step: 8,\n title: 'Account Requirements',\n description: 'Your Instagram account must be a Business or Creator account linked to a Facebook Page. Personal accounts are not supported',\n },\n ],\n supportsScheduling: false,\n supportsEnvironment: false,\n redirectUriPattern: this.defaultRedirectUri,\n credentialSchema: this.getCredentialSchema(),\n };\n }\n\n // ─── Private Helpers ──────────────────────────────────────────────────\n\n /**\n * Make a GET request to the Graph API\n * @private\n */\n private async _graphGet(path: string, accessToken: string): Promise<any> {\n const separator = path.includes('?') ? '&' : '?';\n const url = `${GRAPH_API_BASE}${path}${separator}access_token=${accessToken}`;\n\n const response = await fetch(url);\n const data: any = await response.json();\n\n if (data.error) {\n throw new SocialError('instagram', data.error.message || `Graph API GET failed: ${path}`);\n }\n\n return data;\n }\n\n /**\n * Make a POST request to the Graph API\n * @private\n */\n private async _graphPost(path: string, body: Record<string, any>, accessToken: string): Promise<any> {\n const url = `${GRAPH_API_BASE}${path}`;\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ ...body, access_token: accessToken }),\n });\n\n return response.json();\n }\n\n /**\n * Poll container status until FINISHED, ERROR, or timeout\n * @private\n */\n private async _pollContainerStatus(\n accessToken: string,\n containerId: string,\n maxAttempts: number = 30,\n intervalMs: number = 5000,\n ): Promise<any> {\n return pollUntilComplete({\n fn: () => this._graphGet(`/${containerId}?fields=status_code,status`, accessToken),\n isComplete: (data: any) => data.status_code === 'FINISHED',\n getError: (data: any) => {\n if (data.status_code === 'ERROR')\n return new SocialError('instagram', `Container processing failed: ${data.status || 'Unknown error'}`, { retryable: true });\n if (data.status_code === 'EXPIRED')\n return new SocialError('instagram', 'Container expired before publishing (24-hour limit)', { retryable: false });\n return null;\n },\n maxAttempts,\n intervalMs,\n label: 'InstagramProvider',\n });\n }\n}\n"],"mappings":"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"}
|