@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.
@@ -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'