@nextsparkjs/plugin-social-media-publisher 0.1.0-beta.1
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/.env.example +76 -0
- package/README.md +423 -0
- package/api/social/connect/callback/route.ts +669 -0
- package/api/social/connect/route.ts +327 -0
- package/api/social/disconnect/route.ts +187 -0
- package/api/social/publish/route.ts +402 -0
- package/docs/01-getting-started/01-introduction.md +471 -0
- package/docs/01-getting-started/02-installation.md +471 -0
- package/docs/01-getting-started/03-configuration.md +515 -0
- package/docs/02-core-features/01-oauth-integration.md +501 -0
- package/docs/02-core-features/02-publishing.md +527 -0
- package/docs/02-core-features/03-token-management.md +661 -0
- package/docs/02-core-features/04-audit-logging.md +646 -0
- package/docs/03-advanced-usage/01-provider-apis.md +764 -0
- package/docs/03-advanced-usage/02-custom-integrations.md +695 -0
- package/docs/03-advanced-usage/03-per-client-architecture.md +575 -0
- package/docs/04-use-cases/01-agency-management.md +661 -0
- package/docs/04-use-cases/02-content-publishing.md +668 -0
- package/docs/04-use-cases/03-analytics-reporting.md +748 -0
- package/entities/audit-logs/audit-logs.config.ts +150 -0
- package/lib/oauth-helper.ts +167 -0
- package/lib/providers/facebook.ts +672 -0
- package/lib/providers/index.ts +21 -0
- package/lib/providers/instagram.ts +791 -0
- package/lib/validation.ts +155 -0
- package/migrations/001_social_media_tables.sql +167 -0
- package/package.json +15 -0
- package/plugin.config.ts +81 -0
- package/tsconfig.json +47 -0
- package/types/social.types.ts +171 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facebook Graph API Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Provides methods for publishing to Facebook Pages
|
|
5
|
+
* Uses Facebook Graph API v18.0
|
|
6
|
+
*
|
|
7
|
+
* @see https://developers.facebook.com/docs/graph-api
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const GRAPH_API_VERSION = 'v18.0'
|
|
11
|
+
const GRAPH_API_BASE = `https://graph.facebook.com/${GRAPH_API_VERSION}`
|
|
12
|
+
|
|
13
|
+
export interface FacebookPublishOptions {
|
|
14
|
+
pageId: string
|
|
15
|
+
pageAccessToken: string
|
|
16
|
+
message: string
|
|
17
|
+
imageUrl?: string
|
|
18
|
+
imageUrls?: string[] // For carousels
|
|
19
|
+
videoUrl?: string
|
|
20
|
+
link?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FacebookPublishResult {
|
|
24
|
+
success: boolean
|
|
25
|
+
postId?: string
|
|
26
|
+
postUrl?: string
|
|
27
|
+
error?: string
|
|
28
|
+
errorDetails?: unknown
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FacebookPageInfo {
|
|
32
|
+
id: string
|
|
33
|
+
name: string
|
|
34
|
+
category: string
|
|
35
|
+
accessToken: string
|
|
36
|
+
tasks: string[]
|
|
37
|
+
pictureUrl?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface FacebookInsights {
|
|
41
|
+
impressions: number
|
|
42
|
+
reach: number
|
|
43
|
+
engagement: number
|
|
44
|
+
reactions: number
|
|
45
|
+
comments: number
|
|
46
|
+
shares: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FacebookPageStats {
|
|
50
|
+
id: string
|
|
51
|
+
name: string
|
|
52
|
+
fanCount: number
|
|
53
|
+
about?: string
|
|
54
|
+
category?: string
|
|
55
|
+
profilePictureUrl?: string
|
|
56
|
+
coverPhotoUrl?: string
|
|
57
|
+
link?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface FacebookAPIResponse<T> {
|
|
61
|
+
data?: T[]
|
|
62
|
+
paging?: {
|
|
63
|
+
next?: string
|
|
64
|
+
previous?: string
|
|
65
|
+
}
|
|
66
|
+
error?: {
|
|
67
|
+
message: string
|
|
68
|
+
type: string
|
|
69
|
+
code: number
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface FacebookPageData {
|
|
74
|
+
id: string
|
|
75
|
+
name: string
|
|
76
|
+
category: string
|
|
77
|
+
access_token: string
|
|
78
|
+
tasks?: string[]
|
|
79
|
+
picture?: {
|
|
80
|
+
data?: {
|
|
81
|
+
url: string
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class FacebookAPI {
|
|
87
|
+
/**
|
|
88
|
+
* Publish a text post to Facebook Page
|
|
89
|
+
*/
|
|
90
|
+
static async publishTextPost(options: FacebookPublishOptions): Promise<FacebookPublishResult> {
|
|
91
|
+
try {
|
|
92
|
+
const response: Response = await fetch(`${GRAPH_API_BASE}/${options.pageId}/feed`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: {
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
message: options.message,
|
|
99
|
+
access_token: options.pageAccessToken,
|
|
100
|
+
}),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const data: any = await response.json()
|
|
104
|
+
|
|
105
|
+
if (data.error) {
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
error: data.error.message,
|
|
109
|
+
errorDetails: data.error,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
postId: data.id,
|
|
116
|
+
postUrl: `https://www.facebook.com/${data.id}`,
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return {
|
|
120
|
+
success: false,
|
|
121
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
122
|
+
errorDetails: error,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Publish a photo post to Facebook Page
|
|
129
|
+
*/
|
|
130
|
+
static async publishPhotoPost(options: FacebookPublishOptions): Promise<FacebookPublishResult> {
|
|
131
|
+
if (!options.imageUrl) {
|
|
132
|
+
return {
|
|
133
|
+
success: false,
|
|
134
|
+
error: 'Image URL is required for photo posts',
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const response: Response = await fetch(`${GRAPH_API_BASE}/${options.pageId}/photos`, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: {
|
|
142
|
+
'Content-Type': 'application/json',
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
url: options.imageUrl,
|
|
146
|
+
message: options.message,
|
|
147
|
+
access_token: options.pageAccessToken,
|
|
148
|
+
}),
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const data: any = await response.json()
|
|
152
|
+
|
|
153
|
+
if (data.error) {
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
error: data.error.message,
|
|
157
|
+
errorDetails: data.error,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
success: true,
|
|
163
|
+
postId: data.id,
|
|
164
|
+
postUrl: `https://www.facebook.com/${data.post_id || data.id}`,
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return {
|
|
168
|
+
success: false,
|
|
169
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
170
|
+
errorDetails: error,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Publish a link post to Facebook Page
|
|
177
|
+
*/
|
|
178
|
+
static async publishLinkPost(options: FacebookPublishOptions): Promise<FacebookPublishResult> {
|
|
179
|
+
if (!options.link) {
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
error: 'Link URL is required for link posts',
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const response: Response = await fetch(`${GRAPH_API_BASE}/${options.pageId}/feed`, {
|
|
188
|
+
method: 'POST',
|
|
189
|
+
headers: {
|
|
190
|
+
'Content-Type': 'application/json',
|
|
191
|
+
},
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
message: options.message,
|
|
194
|
+
link: options.link,
|
|
195
|
+
access_token: options.pageAccessToken,
|
|
196
|
+
}),
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const data: any = await response.json()
|
|
200
|
+
|
|
201
|
+
if (data.error) {
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
error: data.error.message,
|
|
205
|
+
errorDetails: data.error,
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
success: true,
|
|
211
|
+
postId: data.id,
|
|
212
|
+
postUrl: `https://www.facebook.com/${data.id}`,
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return {
|
|
216
|
+
success: false,
|
|
217
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
218
|
+
errorDetails: error,
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get Facebook Pages managed by user
|
|
225
|
+
*/
|
|
226
|
+
static async getUserPages(userAccessToken: string): Promise<FacebookPageInfo[]> {
|
|
227
|
+
try {
|
|
228
|
+
console.log('[FacebookAPI] 🔍 Fetching user pages...')
|
|
229
|
+
let allPages: FacebookPageInfo[] = []
|
|
230
|
+
let nextUrl: string | null = `${GRAPH_API_BASE}/me/accounts?fields=id,name,category,access_token,tasks,picture&access_token=${userAccessToken}`
|
|
231
|
+
let pageCount = 0
|
|
232
|
+
|
|
233
|
+
// Fetch all pages following pagination
|
|
234
|
+
while (nextUrl && pageCount < 10) { // Safety limit of 10 pages
|
|
235
|
+
pageCount++
|
|
236
|
+
console.log(`[FacebookAPI] 🔍 Fetching page batch ${pageCount}...`)
|
|
237
|
+
|
|
238
|
+
const response: Response = await fetch(nextUrl)
|
|
239
|
+
const data: FacebookAPIResponse<FacebookPageData> = await response.json()
|
|
240
|
+
|
|
241
|
+
if (data.error) {
|
|
242
|
+
throw new Error(data.error.message)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Map and add pages from this batch
|
|
246
|
+
const batchPages = (data.data || []).map((page: any) => ({
|
|
247
|
+
id: page.id,
|
|
248
|
+
name: page.name,
|
|
249
|
+
category: page.category,
|
|
250
|
+
accessToken: page.access_token,
|
|
251
|
+
tasks: page.tasks || [],
|
|
252
|
+
pictureUrl: page.picture?.data?.url,
|
|
253
|
+
}))
|
|
254
|
+
|
|
255
|
+
allPages = allPages.concat(batchPages)
|
|
256
|
+
console.log(`[FacebookAPI] 🔍 Batch ${pageCount}: Found ${batchPages.length} pages`)
|
|
257
|
+
|
|
258
|
+
// Check for next page
|
|
259
|
+
nextUrl = data.paging?.next || null
|
|
260
|
+
if (nextUrl) {
|
|
261
|
+
console.log(`[FacebookAPI] 🔍 Pagination detected - fetching next batch...`)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log('[FacebookAPI] ✅ Total pages found across all batches:', allPages.length)
|
|
266
|
+
console.log('[FacebookAPI] 🔍 Page names:', allPages.map((p: any) => p.name))
|
|
267
|
+
|
|
268
|
+
return allPages
|
|
269
|
+
} catch (error) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`Failed to fetch Facebook Pages: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get page insights (analytics)
|
|
278
|
+
*/
|
|
279
|
+
static async getPageInsights(
|
|
280
|
+
pageId: string,
|
|
281
|
+
pageAccessToken: string
|
|
282
|
+
): Promise<FacebookInsights> {
|
|
283
|
+
try {
|
|
284
|
+
const response: Response = await fetch(
|
|
285
|
+
`${GRAPH_API_BASE}/${pageId}/insights?` +
|
|
286
|
+
`metric=page_impressions,page_engaged_users,page_views_total&` +
|
|
287
|
+
`period=day&` +
|
|
288
|
+
`access_token=${pageAccessToken}`
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
const data: any = await response.json()
|
|
292
|
+
|
|
293
|
+
if (data.error) {
|
|
294
|
+
throw new Error(data.error.message)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Extract metrics from response
|
|
298
|
+
const insights: FacebookInsights = {
|
|
299
|
+
impressions: 0,
|
|
300
|
+
reach: 0,
|
|
301
|
+
engagement: 0,
|
|
302
|
+
reactions: 0,
|
|
303
|
+
comments: 0,
|
|
304
|
+
shares: 0,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
data.data?.forEach((metric: any) => {
|
|
308
|
+
const value = metric.values?.[0]?.value || 0
|
|
309
|
+
if (metric.name === 'page_impressions') {
|
|
310
|
+
insights.impressions = value
|
|
311
|
+
} else if (metric.name === 'page_engaged_users') {
|
|
312
|
+
insights.engagement = value
|
|
313
|
+
} else if (metric.name === 'page_views_total') {
|
|
314
|
+
insights.reach = value
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
return insights
|
|
319
|
+
} catch (error) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Failed to fetch page insights: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Validate page access token permissions
|
|
328
|
+
*/
|
|
329
|
+
static async validatePagePermissions(
|
|
330
|
+
pageId: string,
|
|
331
|
+
pageAccessToken: string
|
|
332
|
+
): Promise<{ valid: boolean; permissions: string[]; missing: string[] }> {
|
|
333
|
+
try {
|
|
334
|
+
const response: Response = await fetch(
|
|
335
|
+
`${GRAPH_API_BASE}/${pageId}?fields=tasks&access_token=${pageAccessToken}`
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
const data: any = await response.json()
|
|
339
|
+
|
|
340
|
+
if (data.error) {
|
|
341
|
+
return {
|
|
342
|
+
valid: false,
|
|
343
|
+
permissions: [],
|
|
344
|
+
missing: ['pages_manage_posts', 'pages_read_engagement'],
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const requiredTasks = ['CREATE_CONTENT', 'MODERATE']
|
|
349
|
+
const grantedTasks = data.tasks || []
|
|
350
|
+
const missingTasks = requiredTasks.filter(task => !grantedTasks.includes(task))
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
valid: missingTasks.length === 0,
|
|
354
|
+
permissions: grantedTasks,
|
|
355
|
+
missing: missingTasks,
|
|
356
|
+
}
|
|
357
|
+
} catch (error) {
|
|
358
|
+
return {
|
|
359
|
+
valid: false,
|
|
360
|
+
permissions: [],
|
|
361
|
+
missing: ['pages_manage_posts', 'pages_read_engagement'],
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Get Instagram Business Account connected to a Facebook Page
|
|
368
|
+
*
|
|
369
|
+
* IMPORTANT: This is Instagram Graph API (NOT Basic Display API)
|
|
370
|
+
* - Requires Facebook Page to have Instagram Business Account linked
|
|
371
|
+
* - Returns null if Page has no Instagram connected
|
|
372
|
+
* - Requires instagram_basic permission
|
|
373
|
+
*
|
|
374
|
+
* @param pageId - Facebook Page ID
|
|
375
|
+
* @param pageAccessToken - Page Access Token (NOT user token)
|
|
376
|
+
* @returns Instagram Business Account info or null if not connected
|
|
377
|
+
*/
|
|
378
|
+
static async getInstagramBusinessAccount(
|
|
379
|
+
pageId: string,
|
|
380
|
+
pageAccessToken: string
|
|
381
|
+
): Promise<{
|
|
382
|
+
id: string
|
|
383
|
+
username: string
|
|
384
|
+
name?: string
|
|
385
|
+
profilePictureUrl?: string
|
|
386
|
+
followersCount?: number
|
|
387
|
+
followsCount?: number
|
|
388
|
+
mediaCount?: number
|
|
389
|
+
biography?: string
|
|
390
|
+
website?: string
|
|
391
|
+
} | null> {
|
|
392
|
+
try {
|
|
393
|
+
console.log('[FacebookAPI] Checking if Page has Instagram Business Account...')
|
|
394
|
+
console.log('[FacebookAPI] Page ID:', pageId)
|
|
395
|
+
console.log('[FacebookAPI] Page Access Token (first 20 chars):', pageAccessToken.substring(0, 20) + '...')
|
|
396
|
+
|
|
397
|
+
// Step 0: Check Page Access Token permissions
|
|
398
|
+
console.log('[FacebookAPI] 🔍 Checking Page Access Token permissions...')
|
|
399
|
+
const debugResponse: Response = await fetch(
|
|
400
|
+
`${GRAPH_API_BASE}/debug_token?input_token=${pageAccessToken}&access_token=${pageAccessToken}`
|
|
401
|
+
)
|
|
402
|
+
const debugData: any = await debugResponse.json()
|
|
403
|
+
|
|
404
|
+
// Log token info in single lines for easier debugging
|
|
405
|
+
if (debugData.data) {
|
|
406
|
+
console.log('[FacebookAPI] 🔍 Token Type:', debugData.data.type)
|
|
407
|
+
console.log('[FacebookAPI] 🔍 Token App ID:', debugData.data.app_id)
|
|
408
|
+
console.log('[FacebookAPI] 🔍 Token Valid:', debugData.data.is_valid)
|
|
409
|
+
console.log('[FacebookAPI] 🔍 Token Scopes:', JSON.stringify(debugData.data.scopes || []))
|
|
410
|
+
console.log('[FacebookAPI] 🔍 Token Granular Scopes:', JSON.stringify(debugData.data.granular_scopes || []))
|
|
411
|
+
} else if (debugData.error) {
|
|
412
|
+
console.log('[FacebookAPI] ❌ Token Debug Error:', debugData.error.message)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Step 1: Check if Page has Instagram Business Account linked
|
|
416
|
+
const pageResponse: Response = await fetch(
|
|
417
|
+
`${GRAPH_API_BASE}/${pageId}?fields=instagram_business_account&access_token=${pageAccessToken}`
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
const pageData: any = await pageResponse.json()
|
|
421
|
+
|
|
422
|
+
// DEBUG: Log the full response
|
|
423
|
+
console.log('[FacebookAPI] 🔍 Page API Response:', JSON.stringify(pageData, null, 2))
|
|
424
|
+
console.log('[FacebookAPI] 🔍 Has instagram_business_account field?', !!pageData.instagram_business_account)
|
|
425
|
+
console.log('[FacebookAPI] 🔍 Response status:', pageResponse.status)
|
|
426
|
+
|
|
427
|
+
if (pageData.error) {
|
|
428
|
+
console.error('[FacebookAPI] Error fetching Page data:', pageData.error)
|
|
429
|
+
return null
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!pageData.instagram_business_account) {
|
|
433
|
+
console.log('[FacebookAPI] Page does not have Instagram Business Account linked')
|
|
434
|
+
console.log('[FacebookAPI] Available fields in response:', Object.keys(pageData))
|
|
435
|
+
return null
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const igAccountId = pageData.instagram_business_account.id
|
|
439
|
+
console.log('[FacebookAPI] Found Instagram Business Account:', igAccountId)
|
|
440
|
+
|
|
441
|
+
// Step 2: Get Instagram Business Account details
|
|
442
|
+
const igResponse: Response = await fetch(
|
|
443
|
+
`${GRAPH_API_BASE}/${igAccountId}?` +
|
|
444
|
+
`fields=id,username,name,profile_picture_url,followers_count,follows_count,media_count,biography,website&` +
|
|
445
|
+
`access_token=${pageAccessToken}`
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
const igData: any = await igResponse.json()
|
|
449
|
+
|
|
450
|
+
if (igData.error) {
|
|
451
|
+
console.error('[FacebookAPI] Error fetching Instagram data:', igData.error)
|
|
452
|
+
throw new Error(igData.error.message)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
console.log('[FacebookAPI] ✅ Instagram Business Account retrieved:', {
|
|
456
|
+
id: igData.id,
|
|
457
|
+
username: igData.username,
|
|
458
|
+
followersCount: igData.followers_count
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
id: igData.id,
|
|
463
|
+
username: igData.username,
|
|
464
|
+
name: igData.name,
|
|
465
|
+
profilePictureUrl: igData.profile_picture_url,
|
|
466
|
+
followersCount: igData.followers_count,
|
|
467
|
+
followsCount: igData.follows_count,
|
|
468
|
+
mediaCount: igData.media_count,
|
|
469
|
+
biography: igData.biography,
|
|
470
|
+
website: igData.website,
|
|
471
|
+
}
|
|
472
|
+
} catch (error) {
|
|
473
|
+
console.error('[FacebookAPI] Exception getting Instagram Business Account:', error)
|
|
474
|
+
throw new Error(
|
|
475
|
+
`Failed to fetch Instagram Business Account: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Get Facebook Page statistics and information
|
|
482
|
+
*
|
|
483
|
+
* Similar to InstagramAPI.getAccountInfo(), this fetches public page data
|
|
484
|
+
* including follower count, about info, and profile picture
|
|
485
|
+
*
|
|
486
|
+
* @param pageId - Facebook Page ID
|
|
487
|
+
* @param pageAccessToken - Page Access Token
|
|
488
|
+
* @returns Page statistics and info
|
|
489
|
+
*/
|
|
490
|
+
static async getPageInfo(
|
|
491
|
+
pageId: string,
|
|
492
|
+
pageAccessToken: string
|
|
493
|
+
): Promise<FacebookPageStats> {
|
|
494
|
+
try {
|
|
495
|
+
console.log('[FacebookAPI] Fetching Page info and stats...')
|
|
496
|
+
|
|
497
|
+
const response: Response = await fetch(
|
|
498
|
+
`${GRAPH_API_BASE}/${pageId}?` +
|
|
499
|
+
`fields=id,name,fan_count,about,category,picture{url},cover{source},link&` +
|
|
500
|
+
`access_token=${pageAccessToken}`
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
const data: any = await response.json()
|
|
504
|
+
|
|
505
|
+
if (data.error) {
|
|
506
|
+
console.error('[FacebookAPI] ❌ API Error:', data.error)
|
|
507
|
+
throw new Error(data.error.message || 'Failed to fetch Page info')
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
console.log('[FacebookAPI] ✅ Page info retrieved:', {
|
|
511
|
+
id: data.id,
|
|
512
|
+
name: data.name,
|
|
513
|
+
fanCount: data.fan_count
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
id: data.id,
|
|
518
|
+
name: data.name,
|
|
519
|
+
fanCount: data.fan_count || 0,
|
|
520
|
+
about: data.about,
|
|
521
|
+
category: data.category,
|
|
522
|
+
profilePictureUrl: data.picture?.url,
|
|
523
|
+
coverPhotoUrl: data.cover?.source,
|
|
524
|
+
link: data.link,
|
|
525
|
+
}
|
|
526
|
+
} catch (error) {
|
|
527
|
+
console.error('[FacebookAPI] ❌ Exception while fetching Page info:', error)
|
|
528
|
+
throw new Error(
|
|
529
|
+
`Failed to fetch Facebook Page info: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
530
|
+
)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Publish a carousel post (multiple images) to Facebook Page
|
|
536
|
+
*
|
|
537
|
+
* Facebook carousel publishing is a 2-step process:
|
|
538
|
+
* 1. Upload each photo as unpublished
|
|
539
|
+
* 2. Create post with attached_media array
|
|
540
|
+
*/
|
|
541
|
+
static async publishCarouselPost(options: FacebookPublishOptions): Promise<FacebookPublishResult> {
|
|
542
|
+
const { pageId, pageAccessToken, message, imageUrls } = options
|
|
543
|
+
|
|
544
|
+
// Validation
|
|
545
|
+
if (!imageUrls || imageUrls.length < 2) {
|
|
546
|
+
return {
|
|
547
|
+
success: false,
|
|
548
|
+
error: 'Carousel requires at least 2 images',
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
console.log(`[FacebookAPI] Creating carousel with ${imageUrls.length} images...`)
|
|
554
|
+
|
|
555
|
+
// Step 1: Upload each photo as unpublished
|
|
556
|
+
const mediaFbIds: string[] = []
|
|
557
|
+
const errors: { index: number; error: string }[] = []
|
|
558
|
+
|
|
559
|
+
for (let i = 0; i < imageUrls.length; i++) {
|
|
560
|
+
console.log(`[FacebookAPI] Uploading image ${i + 1}/${imageUrls.length}...`)
|
|
561
|
+
|
|
562
|
+
const photoResult = await this.uploadUnpublishedPhoto(
|
|
563
|
+
pageId,
|
|
564
|
+
pageAccessToken,
|
|
565
|
+
imageUrls[i],
|
|
566
|
+
message // ✅ Pass caption to each image for better reach and UX
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
if (photoResult.success && photoResult.mediaFbId) {
|
|
570
|
+
mediaFbIds.push(photoResult.mediaFbId)
|
|
571
|
+
console.log(`[FacebookAPI] ✅ Image ${i + 1} uploaded: ${photoResult.mediaFbId}`)
|
|
572
|
+
} else {
|
|
573
|
+
console.error(`[FacebookAPI] ❌ Failed to upload image ${i + 1}: ${photoResult.error}`)
|
|
574
|
+
errors.push({ index: i + 1, error: photoResult.error || 'Unknown error' })
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (errors.length > 0) {
|
|
579
|
+
const errorMessage = `Failed to upload images: ${errors.map(e => `#${e.index}: ${e.error}`).join(', ')}`
|
|
580
|
+
console.error(`[FacebookAPI] ${errorMessage}`)
|
|
581
|
+
return {
|
|
582
|
+
success: false,
|
|
583
|
+
error: errorMessage,
|
|
584
|
+
errorDetails: errors,
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
console.log(`[FacebookAPI] All ${mediaFbIds.length} images uploaded`)
|
|
589
|
+
|
|
590
|
+
// Step 2: Create post with attached_media
|
|
591
|
+
console.log('[FacebookAPI] Creating carousel post...')
|
|
592
|
+
const attachedMedia = mediaFbIds.map(fbId => ({ media_fbid: fbId }))
|
|
593
|
+
|
|
594
|
+
const response: Response = await fetch(`${GRAPH_API_BASE}/${pageId}/feed`, {
|
|
595
|
+
method: 'POST',
|
|
596
|
+
headers: {
|
|
597
|
+
'Content-Type': 'application/json',
|
|
598
|
+
},
|
|
599
|
+
body: JSON.stringify({
|
|
600
|
+
message,
|
|
601
|
+
attached_media: attachedMedia,
|
|
602
|
+
access_token: pageAccessToken,
|
|
603
|
+
}),
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
const data: any = await response.json()
|
|
607
|
+
|
|
608
|
+
if (data.error) {
|
|
609
|
+
console.error('[FacebookAPI] Carousel post creation failed:', data.error.message)
|
|
610
|
+
return {
|
|
611
|
+
success: false,
|
|
612
|
+
error: data.error.message,
|
|
613
|
+
errorDetails: data.error,
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
console.log(`[FacebookAPI] ✅ Carousel published successfully: ${data.id}`)
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
success: true,
|
|
621
|
+
postId: data.id,
|
|
622
|
+
postUrl: `https://www.facebook.com/${data.id}`,
|
|
623
|
+
}
|
|
624
|
+
} catch (error) {
|
|
625
|
+
console.error('[FacebookAPI] Exception during carousel publish:', error)
|
|
626
|
+
return {
|
|
627
|
+
success: false,
|
|
628
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
629
|
+
errorDetails: error,
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Upload a photo as unpublished (for carousel creation)
|
|
636
|
+
* @private
|
|
637
|
+
*/
|
|
638
|
+
private static async uploadUnpublishedPhoto(
|
|
639
|
+
pageId: string,
|
|
640
|
+
pageAccessToken: string,
|
|
641
|
+
imageUrl: string,
|
|
642
|
+
caption?: string
|
|
643
|
+
): Promise<{ success: boolean; mediaFbId?: string; error?: string }> {
|
|
644
|
+
try {
|
|
645
|
+
const response: Response = await fetch(`${GRAPH_API_BASE}/${pageId}/photos`, {
|
|
646
|
+
method: 'POST',
|
|
647
|
+
headers: {
|
|
648
|
+
'Content-Type': 'application/json',
|
|
649
|
+
},
|
|
650
|
+
body: JSON.stringify({
|
|
651
|
+
url: imageUrl,
|
|
652
|
+
published: false,
|
|
653
|
+
message: caption || '', // ✅ Add individual caption to each carousel image
|
|
654
|
+
access_token: pageAccessToken,
|
|
655
|
+
}),
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
const data: any = await response.json()
|
|
659
|
+
|
|
660
|
+
if (data.error) {
|
|
661
|
+
return { success: false, error: data.error.message }
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return { success: true, mediaFbId: data.id }
|
|
665
|
+
} catch (error) {
|
|
666
|
+
return {
|
|
667
|
+
success: false,
|
|
668
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social Media Provider Exports
|
|
3
|
+
*
|
|
4
|
+
* Export all provider API wrappers
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { FacebookAPI } from './facebook'
|
|
8
|
+
export type {
|
|
9
|
+
FacebookPublishOptions,
|
|
10
|
+
FacebookPublishResult,
|
|
11
|
+
FacebookPageInfo,
|
|
12
|
+
FacebookInsights,
|
|
13
|
+
} from './facebook'
|
|
14
|
+
|
|
15
|
+
export { InstagramAPI } from './instagram'
|
|
16
|
+
export type {
|
|
17
|
+
InstagramPublishOptions,
|
|
18
|
+
InstagramPublishResult,
|
|
19
|
+
InstagramAccountInfo,
|
|
20
|
+
InstagramInsights,
|
|
21
|
+
} from './instagram'
|