bb-fca 2.0.18 → 2.0.19

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,744 @@
1
+ import * as crypto from 'crypto';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import utils = require('../../../utils');
5
+
6
+ /**
7
+ * @namespace api.createReel
8
+ * @description Upload and publish a Facebook Reel with full copyright check flow.
9
+ * Flow: Start Session → rupload binary → Receive confirm → Copyright Check → Publish
10
+ * @license Ex-it
11
+ */
12
+
13
+ /**
14
+ * Generate a UUID v4 string for composer_session_id / waterfall_id.
15
+ */
16
+ function generateUUID(): string {
17
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
18
+ const r = (Math.random() * 16) | 0;
19
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
20
+ return v.toString(16);
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Sleep helper for polling.
26
+ */
27
+ function sleep(ms: number): Promise<void> {
28
+ return new Promise((resolve) => setTimeout(resolve, ms));
29
+ }
30
+
31
+ export default function(defaultFuncs: any, api: any, ctx: any) {
32
+ /**
33
+ * Upload and publish a video as a Facebook Reel.
34
+ *
35
+ * @param {object} options - The reel options.
36
+ * @param {string} options.videoPath - File path to the video file.
37
+ * @param {string} [options.message=""] - Caption text for the reel.
38
+ * @param {string} [options.privacy="EVERYONE"] - Privacy: "EVERYONE", "FRIENDS", or "SELF".
39
+ * @param {boolean} [options.skipCopyrightCheck=false] - Skip copyright check polling (faster but riskier).
40
+ * @param {number} [options.copyrightCheckTimeout=60000] - Max time to wait for copyright check (ms).
41
+ * @param {number} [options.copyrightCheckInterval=2000] - Polling interval for copyright check (ms).
42
+ * @param {Function} [callback] - Optional callback function.
43
+ * @returns {Promise<object>} Object containing postID, storyID and metadata.
44
+ */
45
+ async function createReel(options: any, callback?: Function) {
46
+ let resolveFunc: Function = function() {};
47
+ let rejectFunc: Function = function() {};
48
+
49
+ const returnPromise = new Promise<any>(function(resolve, reject) {
50
+ resolveFunc = resolve;
51
+ rejectFunc = reject;
52
+ });
53
+
54
+ callback =
55
+ callback ||
56
+ function(err, data) {
57
+ if (err) return rejectFunc(err);
58
+ resolveFunc(data);
59
+ };
60
+
61
+ try {
62
+ // ===== Validate Input =====
63
+ if (!options || typeof options !== 'object') {
64
+ throw new Error(
65
+ 'Options must be an object with at least { videoPath }.',
66
+ );
67
+ }
68
+
69
+ const videoPath = options.videoPath;
70
+ const message = options.message || '';
71
+ const privacy = options.privacy || 'EVERYONE';
72
+ const skipCopyrightCheck = options.skipCopyrightCheck || false;
73
+ const copyrightCheckTimeout = options.copyrightCheckTimeout || 60000;
74
+ const copyrightCheckInterval = options.copyrightCheckInterval || 2000;
75
+
76
+ if (!videoPath || typeof videoPath !== 'string') {
77
+ throw new Error('videoPath is required and must be a string.');
78
+ }
79
+ if (!fs.existsSync(videoPath)) {
80
+ throw new Error(`Video file not found: ${videoPath}`);
81
+ }
82
+
83
+ const validPrivacy = ['EVERYONE', 'FRIENDS', 'SELF', 'ALL_FRIENDS'];
84
+ if (!validPrivacy.includes(privacy)) {
85
+ throw new Error(
86
+ `Invalid privacy setting. Use one of: ${validPrivacy.join(', ')}`,
87
+ );
88
+ }
89
+
90
+ const fileBuffer = fs.readFileSync(videoPath);
91
+ const fileSize = fileBuffer.length;
92
+ const fileExtension = path
93
+ .extname(videoPath)
94
+ .replace('.', '')
95
+ .toLowerCase();
96
+
97
+ // Validate extension
98
+ const allowedExtensions = [
99
+ 'mov',
100
+ 'mp4',
101
+ 'avi',
102
+ 'mkv',
103
+ 'webm',
104
+ 'wmv',
105
+ 'flv',
106
+ 'mpg',
107
+ 'mpeg',
108
+ '3gp',
109
+ '3g2',
110
+ 'm4v',
111
+ ];
112
+ if (!allowedExtensions.includes(fileExtension)) {
113
+ throw new Error(
114
+ `Unsupported video format: .${fileExtension}. Supported: ${allowedExtensions.join(
115
+ ', ',
116
+ )}`,
117
+ );
118
+ }
119
+
120
+ const fileHash = crypto
121
+ .createHash('md5')
122
+ .update(fileBuffer)
123
+ .digest('hex');
124
+ const composerSessionId = generateUUID();
125
+
126
+ // CORS headers for cross-subdomain requests
127
+ const corsHeaders = {
128
+ Origin: 'https://www.facebook.com',
129
+ Referer: 'https://www.facebook.com/',
130
+ 'Sec-Fetch-Dest': 'empty',
131
+ 'Sec-Fetch-Mode': 'cors',
132
+ 'Sec-Fetch-Site': 'same-site',
133
+ };
134
+
135
+ // ====================================================================
136
+ // STEP 1: Initialize Upload Session (POST vupload-edge.facebook.com)
137
+ // Exact match with browser network capture
138
+ // ====================================================================
139
+ const startUrl =
140
+ 'https://vupload-edge.facebook.com/ajax/video/upload/requests/start/?av=' +
141
+ ctx.userID +
142
+ '&__a=1';
143
+
144
+ // Resolve lsd once — browser always sends a real token here
145
+ const lsd = ctx.lsd || ctx.fb_dtsg;
146
+
147
+ const startForm = {
148
+ file_size: String(fileSize),
149
+ file_extension: fileExtension,
150
+ target_id: ctx.userID,
151
+ source: 'reel_composer',
152
+ composer_dialog_version: '',
153
+ waterfall_id: composerSessionId,
154
+ composer_session_id: composerSessionId,
155
+ composer_entry_point_ref: 'comet_pp_plus_reel_composer_feed_sprout',
156
+ composer_work_shared_draft_mode: '',
157
+ has_file_been_replaced: 'false',
158
+ supports_chunking: 'true',
159
+ supports_file_api: 'true',
160
+ partition_start_offset: '0',
161
+ partition_end_offset: String(fileSize),
162
+ creator_product: '2',
163
+ spherical: 'false',
164
+ video_publisher_action_source: '',
165
+ // Auth params — must match browser session exactly
166
+ __aaid: '0',
167
+ __user: ctx.userID,
168
+ __a: '1',
169
+ __req: '1',
170
+ __hs: ctx.hs || '20577.HYP:comet_pkg.2.1...0',
171
+ dpr: '1',
172
+ __ccg: 'EXCELLENT',
173
+ __rev: ctx.revision || '',
174
+ __s: ctx.__s || '',
175
+ __hsi: ctx.__hsi || '',
176
+ __dyn: ctx.__dyn || '',
177
+ __csr: ctx.__csr || '',
178
+ __hsdp: ctx.__hsdp || '',
179
+ __hblp: ctx.__hblp || '',
180
+ __sjsp: ctx.__sjsp || '',
181
+ __comet_req: '15',
182
+ fb_dtsg: ctx.fb_dtsg,
183
+ jazoest: ctx.jazoest,
184
+ lsd,
185
+ __spin_r: ctx.revision || '',
186
+ __spin_b: 'trunk',
187
+ __spin_t: String(Math.floor(Date.now() / 1000)),
188
+ __crn: ctx.__crn || 'comet.fbweb.CometHomeRoute',
189
+ };
190
+
191
+ // Debug: log auth params to identify missing values
192
+ utils.log(
193
+ 'createReel',
194
+ `Start session params: fb_dtsg=${ctx.fb_dtsg ? 'OK(' + ctx.fb_dtsg.substring(0, 10) + '...)' : 'MISSING'}, ` +
195
+ `jazoest=${ctx.jazoest || 'MISSING'}, __rev=${ctx.revision || 'MISSING'}, ` +
196
+ `__hs=${ctx.hs || 'MISSING'}, lsd=${ctx.lsd || 'MISSING'}, ` +
197
+ `userID=${ctx.userID || 'MISSING'}`,
198
+ );
199
+
200
+ const startRaw = await utils
201
+ .post(startUrl, ctx.jar, startForm, ctx.globalOptions, ctx, {
202
+ X_fb_video_waterfall_id: composerSessionId,
203
+ ...corsHeaders,
204
+ 'Content-Type': 'application/x-www-form-urlencoded',
205
+ });
206
+
207
+ // Debug: log raw response before parsing
208
+ utils.log(
209
+ 'createReel',
210
+ `Start raw response status=${startRaw?.statusCode}, body=${JSON.stringify(startRaw?.body)?.substring(0, 500)}`,
211
+ );
212
+
213
+ const startRes = await utils.parseAndCheckLogin(ctx, defaultFuncs)(startRaw);
214
+
215
+ const videoId = startRes?.payload?.video_id || startRes?.video_id;
216
+ const uploadSessionId =
217
+ startRes?.payload?.upload_session_id || startRes?.upload_session_id;
218
+ const startOffset =
219
+ startRes?.payload?.start_offset ?? startRes?.start_offset ?? 0;
220
+ const endOffset =
221
+ startRes?.payload?.end_offset ?? startRes?.end_offset ?? fileSize;
222
+
223
+ if (!videoId) {
224
+ throw new Error(
225
+ 'Failed to get video_id from upload session. Response: ' +
226
+ JSON.stringify(startRes),
227
+ );
228
+ }
229
+
230
+ utils.log(
231
+ 'createReel',
232
+ `Upload session started. video_id=${videoId}, session=${uploadSessionId}`,
233
+ );
234
+
235
+ // ====================================================================
236
+ // STEP 2: Probe rupload server (GET)
237
+ // ====================================================================
238
+ const ruploadPath = `/fb_video/${fileHash}-${startOffset}-${endOffset}`;
239
+ const ruploadAuthParams = new URLSearchParams({
240
+ __aaid: '0',
241
+ __user: ctx.userID,
242
+ __a: '1',
243
+ __req: '1',
244
+ __hs: ctx.hs || '20577.HYP:comet_pkg.2.1...0',
245
+ dpr: '1',
246
+ __ccg: 'EXCELLENT',
247
+ __rev: ctx.revision || '',
248
+ __s: ctx.__s || '',
249
+ __hsi: ctx.__hsi || '',
250
+ __dyn: ctx.__dyn || '',
251
+ __csr: ctx.__csr || '',
252
+ __hsdp: ctx.__hsdp || '',
253
+ __hblp: ctx.__hblp || '',
254
+ __sjsp: ctx.__sjsp || '',
255
+ __comet_req: '15',
256
+ fb_dtsg_ag: ctx.fb_dtsg_ag || ctx.fb_dtsg,
257
+ jazoest: ctx.jazoest,
258
+ __spin_r: ctx.revision || '',
259
+ __spin_b: 'trunk',
260
+ __spin_t: String(Math.floor(Date.now() / 1000)),
261
+ __crn: ctx.__crn || 'comet.fbweb.CometHomeRoute',
262
+ }).toString();
263
+
264
+ // Try multiple rupload hosts (Facebook may route to different DCs)
265
+ const ruploadHosts = [
266
+ 'rupload-hkg4-2.up.facebook.com',
267
+ 'rupload-hkg1-2.up.facebook.com',
268
+ 'rupload.facebook.com',
269
+ ];
270
+
271
+ let ruploadBaseUrl = '';
272
+ let probeSuccess = false;
273
+
274
+ for (const host of ruploadHosts) {
275
+ ruploadBaseUrl = `https://${host}${ruploadPath}?${ruploadAuthParams}`;
276
+ try {
277
+ await utils.get(
278
+ ruploadBaseUrl,
279
+ ctx.jar,
280
+ undefined,
281
+ ctx.globalOptions,
282
+ ctx,
283
+ corsHeaders,
284
+ );
285
+ probeSuccess = true;
286
+ break;
287
+ } catch (e) {
288
+ // Try next host
289
+ }
290
+ }
291
+
292
+ if (!probeSuccess) {
293
+ // Fallback: use first host anyway
294
+ ruploadBaseUrl = `https://${ruploadHosts[0]}${ruploadPath}?${ruploadAuthParams}`;
295
+ }
296
+
297
+ // ====================================================================
298
+ // STEP 3: Upload Binary Data (POST rupload)
299
+ // ====================================================================
300
+ const fileName = path.basename(videoPath);
301
+ const mimeTypes = {
302
+ mov: 'video/quicktime',
303
+ mp4: 'video/mp4',
304
+ avi: 'video/x-msvideo',
305
+ mkv: 'video/x-matroska',
306
+ webm: 'video/webm',
307
+ wmv: 'video/x-ms-wmv',
308
+ flv: 'video/x-flv',
309
+ mpg: 'video/mpeg',
310
+ mpeg: 'video/mpeg',
311
+ '3gp': 'video/3gpp',
312
+ '3g2': 'video/3gpp2',
313
+ m4v: 'video/x-m4v',
314
+ };
315
+ const mimeType = mimeTypes[fileExtension] || 'video/mp4';
316
+
317
+ const uploadHeaders = {
318
+ 'Content-Type': 'application/octet-stream',
319
+ Product_media_id: String(videoId),
320
+ Id: String(uploadSessionId),
321
+ Start_offset: String(startOffset),
322
+ End_offset: String(endOffset),
323
+ Offset: String(startOffset),
324
+ 'X-Entity-Length': String(fileSize),
325
+ 'X-Total-Asset-Size': String(fileSize),
326
+ 'X-Entity-Type': mimeType,
327
+ 'X-Entity-Name': encodeURIComponent(fileName),
328
+ Composer_session_id: composerSessionId,
329
+ ...corsHeaders,
330
+ };
331
+
332
+ const uploadRes = await utils.postRaw(
333
+ ruploadBaseUrl,
334
+ ctx.jar,
335
+ fileBuffer,
336
+ ctx.globalOptions,
337
+ ctx,
338
+ uploadHeaders,
339
+ );
340
+
341
+ let fileHandle: string | null = null;
342
+ try {
343
+ const uploadBody =
344
+ typeof uploadRes.body === 'string'
345
+ ? JSON.parse(uploadRes.body)
346
+ : uploadRes.body;
347
+ fileHandle = uploadBody?.h || null;
348
+ } catch (_) {
349
+ /* parse error */
350
+ }
351
+
352
+ if (!fileHandle) {
353
+ throw new Error(
354
+ 'Failed to get file handle from binary upload. Response: ' +
355
+ JSON.stringify(uploadRes.body),
356
+ );
357
+ }
358
+
359
+ utils.log('createReel', `Binary upload complete. Handle obtained.`);
360
+
361
+ // ====================================================================
362
+ // STEP 4: Confirm Upload (POST receive)
363
+ // ====================================================================
364
+ const receiveParams = new URLSearchParams({
365
+ av: ctx.userID,
366
+ composer_session_id: composerSessionId,
367
+ video_id: String(videoId),
368
+ start_offset: '0',
369
+ end_offset: String(endOffset),
370
+ source: 'reel_composer',
371
+ target_id: ctx.userID,
372
+ waterfall_id: composerSessionId,
373
+ composer_entry_point_ref: 'comet_pp_plus_reel_composer_feed_sprout',
374
+ composer_work_shared_draft_mode: '',
375
+ composer_dialog_version: '',
376
+ has_file_been_replaced: 'false',
377
+ supports_chunking: 'true',
378
+ upload_speed: '',
379
+ partition_start_offset: '0',
380
+ partition_end_offset: String(endOffset),
381
+ __aaid: '0',
382
+ __user: ctx.userID,
383
+ __a: '1',
384
+ __req: '1',
385
+ __hs: ctx.hs || '20577.HYP:comet_pkg.2.1...0',
386
+ dpr: '1',
387
+ __ccg: 'EXCELLENT',
388
+ __rev: ctx.revision || '',
389
+ __s: ctx.__s || '',
390
+ __hsi: ctx.__hsi || '',
391
+ __dyn: ctx.__dyn || '',
392
+ __csr: ctx.__csr || '',
393
+ __hsdp: ctx.__hsdp || '',
394
+ __hblp: ctx.__hblp || '',
395
+ __sjsp: ctx.__sjsp || '',
396
+ __comet_req: '15',
397
+ fb_dtsg: ctx.fb_dtsg,
398
+ jazoest: ctx.jazoest,
399
+ lsd,
400
+ __spin_r: ctx.revision || '',
401
+ __spin_b: 'trunk',
402
+ __spin_t: String(Math.floor(Date.now() / 1000)),
403
+ __crn: ctx.__crn || 'comet.fbweb.CometHomeRoute',
404
+ }).toString();
405
+
406
+ const receiveUrl = `https://vupload-edge.facebook.com/ajax/video/upload/requests/receive/?${receiveParams}`;
407
+
408
+ // Send as multipart/form-data with the file handle
409
+ const receiveRes = await defaultFuncs
410
+ .postFormData(
411
+ receiveUrl,
412
+ ctx.jar,
413
+ {
414
+ fbuploader_video_file_chunk: fileHandle,
415
+ },
416
+ {
417
+ X_fb_video_waterfall_id: composerSessionId,
418
+ ...corsHeaders,
419
+ },
420
+ )
421
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
422
+
423
+ const recvStart =
424
+ receiveRes?.payload?.start_offset ?? receiveRes?.start_offset;
425
+ const recvEnd = receiveRes?.payload?.end_offset ?? receiveRes?.end_offset;
426
+
427
+ if (
428
+ recvStart !== undefined &&
429
+ recvEnd !== undefined &&
430
+ recvStart !== recvEnd
431
+ ) {
432
+ utils.warn(
433
+ 'createReel',
434
+ `Upload may be incomplete: start_offset=${recvStart}, end_offset=${recvEnd}`,
435
+ );
436
+ }
437
+
438
+ utils.log(
439
+ 'createReel',
440
+ `Upload confirmed. start=${recvStart}, end=${recvEnd}`,
441
+ );
442
+
443
+ // ====================================================================
444
+ // STEP 5: Copyright Check (Mutation + Polling)
445
+ // ====================================================================
446
+ if (!skipCopyrightCheck) {
447
+ utils.log('createReel', 'Starting copyright check...');
448
+
449
+ // 5a. Trigger copyright check mutation
450
+ const copyrightMutationForm = {
451
+ av: ctx.userID,
452
+ __user: ctx.userID,
453
+ __a: '1',
454
+ __req: '1',
455
+ __ccg: 'EXCELLENT',
456
+ __comet_req: '15',
457
+ fb_dtsg: ctx.fb_dtsg,
458
+ jazoest: ctx.jazoest,
459
+ lsd,
460
+ fb_api_caller_class: 'RelayModern',
461
+ fb_api_req_friendly_name: 'useCometVideoEditorCopyrightCheckMutation',
462
+ variables: JSON.stringify({
463
+ input: {
464
+ actor_id: ctx.userID,
465
+ client_mutation_id: '5',
466
+ from_mbs: false,
467
+ video_id: String(videoId),
468
+ },
469
+ __relay_internal__pv__CometVideoEditorCopyrightCheckDetails_shouldUseMatchlessrelayprovider: false,
470
+ }),
471
+ server_timestamps: 'true',
472
+ doc_id: '25171698979168867',
473
+ };
474
+
475
+ await defaultFuncs
476
+ .post(
477
+ 'https://www.facebook.com/api/graphql/',
478
+ ctx.jar,
479
+ copyrightMutationForm,
480
+ {},
481
+ )
482
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
483
+
484
+ // 5b. Poll for copyright check completion
485
+ const pollStartTime = Date.now();
486
+ let copyrightPassed = false;
487
+
488
+ while (Date.now() - pollStartTime < copyrightCheckTimeout) {
489
+ await sleep(copyrightCheckInterval);
490
+
491
+ const pollForm = {
492
+ av: ctx.userID,
493
+ __user: ctx.userID,
494
+ __a: '1',
495
+ __req: '1',
496
+ __ccg: 'EXCELLENT',
497
+ __comet_req: '15',
498
+ fb_dtsg: ctx.fb_dtsg,
499
+ jazoest: ctx.jazoest,
500
+ lsd,
501
+ fb_api_caller_class: 'RelayModern',
502
+ fb_api_req_friendly_name:
503
+ 'CometVideoEditorCopyrightCheckDetailsLiveQueryUpdaterQuery',
504
+ variables: JSON.stringify({
505
+ videoID: String(videoId),
506
+ __relay_internal__pv__CometVideoEditorCopyrightCheckDetails_shouldUseMatchlessrelayprovider: false,
507
+ }),
508
+ server_timestamps: 'true',
509
+ doc_id: '25488331727450621',
510
+ };
511
+
512
+ const pollRes = await defaultFuncs
513
+ .post(
514
+ 'https://www.facebook.com/api/graphql/',
515
+ ctx.jar,
516
+ pollForm,
517
+ {},
518
+ )
519
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
520
+
521
+ const video = pollRes?.data?.video || pollRes?.data?.node;
522
+ const precheck = video?.copyright_precheck_progress;
523
+ const percentage = precheck?.percentage;
524
+ const finished = video?.if_copyright_precheck_is_finished;
525
+
526
+ if (percentage >= 100 || finished !== null) {
527
+ copyrightPassed = true;
528
+ const earlyResult = precheck?.early_return_result;
529
+ if (earlyResult?.found_early_violation) {
530
+ utils.warn(
531
+ 'createReel',
532
+ 'Copyright violation detected! The reel may be blocked.',
533
+ );
534
+ }
535
+ break;
536
+ }
537
+
538
+ utils.log('createReel', `Copyright check: ${percentage || 0}%`);
539
+ }
540
+
541
+ if (!copyrightPassed) {
542
+ utils.warn(
543
+ 'createReel',
544
+ 'Copyright check timed out. Proceeding with publish anyway.',
545
+ );
546
+ } else {
547
+ utils.log('createReel', 'Copyright check passed!');
548
+ }
549
+ }
550
+
551
+ // ====================================================================
552
+ // STEP 6: Publish Reel (ComposerStoryCreateMutation)
553
+ // ====================================================================
554
+ utils.log('createReel', 'Publishing reel...');
555
+
556
+ const privacyBaseState = privacy === 'ALL_FRIENDS' ? 'FRIENDS' : privacy;
557
+
558
+ const publishVariables: any = {
559
+ input: {
560
+ composer_entry_point: 'comet_pp_plus_reel_composer_feed_sprout',
561
+ composer_source_surface: 'short_form_video',
562
+ idempotence_token: `${composerSessionId}_FEED`,
563
+ source: 'WWW',
564
+ attachments: [
565
+ {
566
+ video: {
567
+ audio_descriptions: null,
568
+ id: String(videoId),
569
+ additional_video_metadata: {
570
+ translatedAudioMetadata: [],
571
+ autoGenCaptionsSettings: {
572
+ autogenerate_captions_enabled: true,
573
+ should_review_all_captions: false,
574
+ },
575
+ },
576
+ notify_when_processed: true,
577
+ transcriptions: null,
578
+ was_created_via_unified_video_flow: {
579
+ was_created_via_unified_video_flow: true,
580
+ },
581
+ story_media_audio_data: {
582
+ raw_media_type: 'VIDEO',
583
+ },
584
+ video_media_metadata: {
585
+ audio: {
586
+ audio_type: 'original_audio',
587
+ start_time_s: 0,
588
+ volume_level: 1,
589
+ },
590
+ is_audio_muted: false,
591
+ length_in_sec: 0, // FB doesn't strictly require this
592
+ },
593
+ },
594
+ },
595
+ ],
596
+ fb_shorts: {
597
+ has_overridden_video_format: true,
598
+ is_fb_short: false,
599
+ remix_status: 'DISABLED',
600
+ },
601
+ post_publish_story_data: {
602
+ reshare_post_as_sticker: 'DISABLED',
603
+ },
604
+ message: {
605
+ ranges: [],
606
+ text: message,
607
+ },
608
+ audience: {
609
+ privacy: {
610
+ allow: [],
611
+ base_state: privacyBaseState,
612
+ deny: [],
613
+ tag_expansion_state: 'UNSPECIFIED',
614
+ },
615
+ },
616
+ with_tags_ids: null,
617
+ reels_remix: {
618
+ remix_status: 'DISABLED',
619
+ },
620
+ stars_receivable: {
621
+ is_receiving_stars_disabled: false,
622
+ },
623
+ logging: {
624
+ composer_session_id: composerSessionId,
625
+ },
626
+ navigation_data: {
627
+ attribution_id_v2: `CometHomeRoot.react,comet.home,logo,${Date.now()},631762,4748854339,,`,
628
+ },
629
+ tracking: [null],
630
+ event_share_metadata: {
631
+ surface: 'newsfeed',
632
+ },
633
+ actor_id: ctx.userID,
634
+ client_mutation_id: '1',
635
+ },
636
+ feedLocation: 'NEWSFEED',
637
+ feedbackSource: 1,
638
+ focusCommentID: null,
639
+ gridMediaWidth: null,
640
+ groupID: null,
641
+ scale: 1,
642
+ privacySelectorRenderLocation: 'COMET_STREAM',
643
+ checkPhotosToReelsUpsellEligibility: false,
644
+ referringStoryRenderLocation: null,
645
+ renderLocation: 'homepage_stream',
646
+ useDefaultActor: false,
647
+ inviteShortLinkKey: null,
648
+ isFeed: true,
649
+ isFundraiser: false,
650
+ isFunFactPost: false,
651
+ isGroup: false,
652
+ isEvent: false,
653
+ isTimeline: false,
654
+ isSocialLearning: false,
655
+ isPageNewsFeed: false,
656
+ isProfileReviews: false,
657
+ isWorkSharedDraft: false,
658
+ canUserManageOffers: false,
659
+ // Relay internal provider variables (required by ComposerStoryCreateMutation)
660
+ __relay_internal__pv__CometUFIShareActionMigrationrelayprovider: true,
661
+ __relay_internal__pv__GHLShouldChangeSponsoredDataFieldNamerelayprovider: true,
662
+ __relay_internal__pv__GHLShouldChangeAdIdFieldNamerelayprovider: true,
663
+ __relay_internal__pv__CometUFI_dedicated_comment_routable_dialog_gkrelayprovider: true,
664
+ __relay_internal__pv__CometUFICommentAutoTranslationTyperelayprovider: 'ORIGINAL',
665
+ __relay_internal__pv__CometUFICommentAvatarStickerAnimatedImagerelayprovider: false,
666
+ __relay_internal__pv__CometUFICommentActionLinksRewriteEnabledrelayprovider: false,
667
+ __relay_internal__pv__IsWorkUserrelayprovider: false,
668
+ __relay_internal__pv__CometUFIReactionsEnableShortNamerelayprovider: false,
669
+ __relay_internal__pv__CometUFISingleLineUFIrelayprovider: false,
670
+ __relay_internal__pv__CometFeedStory_enable_reactor_facepilerelayprovider: false,
671
+ __relay_internal__pv__CometFeedStory_enable_post_permalink_white_space_clickrelayprovider: false,
672
+ __relay_internal__pv__TestPilotShouldIncludeDemoAdUseCaserelayprovider: false,
673
+ __relay_internal__pv__FBReels_deprecate_short_form_video_context_gkrelayprovider: true,
674
+ __relay_internal__pv__FBReels_enable_view_dubbed_audio_type_gkrelayprovider: true,
675
+ __relay_internal__pv__CometImmersivePhotoCanUserDisable3DMotionrelayprovider: false,
676
+ __relay_internal__pv__WorkCometIsEmployeeGKProviderrelayprovider: false,
677
+ __relay_internal__pv__IsMergQAPollsrelayprovider: false,
678
+ __relay_internal__pv__FBReelsMediaFooter_comet_enable_reels_ads_gkrelayprovider: true,
679
+ __relay_internal__pv__FBReelsIFUTileContent_reelsIFUPlayOnHoverrelayprovider: true,
680
+ __relay_internal__pv__GroupsCometGYSJFeedItemHeightrelayprovider: 206,
681
+ __relay_internal__pv__ShouldEnableBakedInTextStoriesrelayprovider: false,
682
+ __relay_internal__pv__StoriesShouldIncludeFbNotesrelayprovider: false,
683
+ __relay_internal__pv__groups_comet_use_glvrelayprovider: false,
684
+ __relay_internal__pv__GHLShouldChangeSponsoredAuctionDistanceFieldNamerelayprovider: false,
685
+ __relay_internal__pv__GHLShouldUseSponsoredAuctionLabelFieldNameV1relayprovider: false,
686
+ __relay_internal__pv__GHLShouldUseSponsoredAuctionLabelFieldNameV2relayprovider: false,
687
+ };
688
+
689
+ const publishForm = {
690
+ av: ctx.userID,
691
+ __user: ctx.userID,
692
+ __a: '1',
693
+ __req: '1',
694
+ __hs: ctx.hs || '20577.HYP:comet_pkg.2.1...0',
695
+ __ccg: 'EXCELLENT',
696
+ __comet_req: '15',
697
+ fb_dtsg: ctx.fb_dtsg,
698
+ jazoest: ctx.jazoest,
699
+ lsd,
700
+ __spin_r: ctx.revision || '',
701
+ __spin_b: 'trunk',
702
+ __spin_t: Math.floor(Date.now() / 1000),
703
+ fb_api_caller_class: 'RelayModern',
704
+ fb_api_req_friendly_name: 'ComposerStoryCreateMutation',
705
+ variables: JSON.stringify(publishVariables),
706
+ server_timestamps: 'true',
707
+ doc_id: '26313541601679894',
708
+ };
709
+
710
+ const publishRes = await defaultFuncs
711
+ .post('https://www.facebook.com/api/graphql/', ctx.jar, publishForm, {})
712
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
713
+
714
+ if (publishRes.errors) {
715
+ throw new Error('Publish failed: ' + JSON.stringify(publishRes.errors));
716
+ }
717
+
718
+ const storyCreate = publishRes?.data?.story_create;
719
+ const result = {
720
+ success: true,
721
+ postID: storyCreate?.post_id || null,
722
+ storyID: storyCreate?.story_id || null,
723
+ videoID: String(videoId),
724
+ publishingFlow: storyCreate?.publishing_flow || null,
725
+ composerSessionId,
726
+ data: publishRes,
727
+ };
728
+
729
+ utils.log(
730
+ 'createReel',
731
+ `Reel published successfully! postID=${result.postID}`,
732
+ );
733
+
734
+ callback(null, result);
735
+ } catch (err) {
736
+ utils.error('createReel', err);
737
+ callback(err);
738
+ }
739
+
740
+ return returnPromise;
741
+ }
742
+
743
+ return createReel;
744
+ }