bb-fca 2.0.4 → 2.0.7

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.
Files changed (38) hide show
  1. package/dist/deltas/apis/create.js +22 -0
  2. package/dist/deltas/apis/create.js.map +1 -1
  3. package/dist/deltas/apis/posting/group.js +607 -0
  4. package/dist/deltas/apis/posting/group.js.map +1 -0
  5. package/dist/deltas/apis/posting/post.js +346 -1
  6. package/dist/deltas/apis/posting/post.js.map +1 -1
  7. package/dist/deltas/apis/posting/story.js +147 -0
  8. package/dist/deltas/apis/posting/story.js.map +1 -1
  9. package/dist/deltas/apis/threads/searchGroup.js +159 -0
  10. package/dist/deltas/apis/threads/searchGroup.js.map +1 -0
  11. package/dist/deltas/apis/users/searchGroups.js +221 -0
  12. package/dist/deltas/apis/users/searchGroups.js.map +1 -0
  13. package/dist/index.d.ts +66 -0
  14. package/dist/types/deltas/apis/create.d.ts +22 -0
  15. package/dist/types/deltas/apis/posting/group.d.ts +90 -0
  16. package/dist/types/deltas/apis/posting/post.d.ts +7 -0
  17. package/dist/types/deltas/apis/posting/story.d.ts +21 -0
  18. package/dist/types/deltas/apis/threads/searchGroup.d.ts +15 -0
  19. package/dist/types/deltas/apis/users/searchGroups.d.ts +17 -0
  20. package/dist/types/utils/axios.d.ts +1 -0
  21. package/dist/types/utils/index.d.ts +1 -0
  22. package/dist/utils/axios.js +12 -0
  23. package/dist/utils/axios.js.map +1 -1
  24. package/dist/utils/index.js +1 -0
  25. package/dist/utils/index.js.map +1 -1
  26. package/package.json +1 -1
  27. package/request.txt +60 -0
  28. package/src/deltas/apis/create.ts +25 -0
  29. package/src/deltas/apis/posting/group.ts +754 -0
  30. package/src/deltas/apis/posting/post.ts +439 -1
  31. package/src/deltas/apis/posting/story.ts +147 -0
  32. package/src/types/index.d.ts +66 -0
  33. package/src/utils/axios.ts +19 -0
  34. package/src/utils/index.ts +1 -0
  35. package/LICENSE-MIT +0 -21
  36. package/examples/post.example.js +0 -149
  37. package/friend.html +0 -534
  38. package/proflie.html +0 -527
@@ -13,6 +13,7 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
13
13
  * @param {string} options.message - The text content of the post.
14
14
  * @param {string} [options.privacy="SELF"] - Privacy setting: "EVERYONE", "FRIENDS", or "SELF".
15
15
  * @param {Array<string>} [options.photos] - Array of photo IDs to attach.
16
+ * @param {string} [options.video] - Video ID from uploadVideo() to attach.
16
17
  * @param {Function} [callback] - Optional callback function.
17
18
  * @returns {Promise<object>} The server's response containing post details.
18
19
  */
@@ -22,6 +23,7 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
22
23
  let postMessage = '';
23
24
  let privacy = 'SELF';
24
25
  let photos = [];
26
+ let videoId: string | null = null;
25
27
 
26
28
  if (typeof options === 'string') {
27
29
  postMessage = options;
@@ -29,6 +31,7 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
29
31
  postMessage = options.message || '';
30
32
  privacy = options.privacy || 'SELF';
31
33
  photos = options.photos || [];
34
+ videoId = options.video || null;
32
35
  } else {
33
36
  throw new Error('Invalid input: expected string or object.');
34
37
  }
@@ -118,6 +121,15 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
118
121
  isWorkSharedDraft: false,
119
122
  hashtag: null,
120
123
  canUserManageOffers: false,
124
+ __relay_internal__pv__CometUFIShareActionMigrationrelayprovider: true,
125
+ __relay_internal__pv__CometUFI_dedicated_comment_routable_dialog_gkrelayprovider: false,
126
+ __relay_internal__pv__CometUFICommentAutoTranslationTyperelayprovider: 'ORIGINAL',
127
+ __relay_internal__pv__IsWorkUserrelayprovider: false,
128
+ __relay_internal__pv__CometUFIReactionsEnableShortNamerelayprovider: false,
129
+ __relay_internal__pv__FBReels_deprecate_short_form_video_context_gkrelayprovider: true,
130
+ __relay_internal__pv__StoriesArmadilloReplyEnabledrelayprovider: true,
131
+ __relay_internal__pv__WorkCometIsEmployeeGKProviderrelayprovider: false,
132
+ __relay_internal__pv__IsMergQAPollsrelayprovider: false,
121
133
  };
122
134
  // Add photo attachments if provided
123
135
  if (photos && photos.length > 0) {
@@ -129,6 +141,26 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
129
141
  },
130
142
  }));
131
143
  }
144
+
145
+ // Add video attachment if provided
146
+ if (videoId) {
147
+ variables.input.attachments = [
148
+ {
149
+ video: {
150
+ audio_descriptions: null,
151
+ id: String(videoId),
152
+ additional_video_metadata: {
153
+ translatedAudioMetadata: [],
154
+ },
155
+ notify_when_processed: true,
156
+ transcriptions: null,
157
+ was_created_via_unified_video_flow: {
158
+ was_created_via_unified_video_flow: true,
159
+ },
160
+ },
161
+ },
162
+ ];
163
+ }
132
164
  const form = {
133
165
  av: ctx.userID,
134
166
  __user: ctx.userID,
@@ -147,7 +179,7 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
147
179
  fb_api_req_friendly_name: 'ComposerStoryCreateMutation',
148
180
  variables: JSON.stringify(variables),
149
181
  server_timestamps: 'true',
150
- doc_id: '35233514182914739',
182
+ doc_id: videoId ? '26937332182536553' : '35233514182914739',
151
183
  };
152
184
 
153
185
  const postResult = await defaultFuncs
@@ -590,10 +622,416 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
590
622
  return returnPromise;
591
623
  }
592
624
 
625
+ /**
626
+ * Upload a video to Facebook using the 7-step resumable upload flow.
627
+ * Returns video_id which can be attached to a post via createPost.
628
+ * @param {string} videoPath - File path to the video file
629
+ * @param {Function} [callback] - Optional callback function
630
+ * @returns {Promise<object>} Object containing video ID and metadata
631
+ */
632
+ async function uploadVideo(videoPath, callback) {
633
+ let resolveFunc: Function = function() {};
634
+ let rejectFunc: Function = function() {};
635
+
636
+ const returnPromise = new Promise<any>(function(resolve, reject) {
637
+ resolveFunc = resolve;
638
+ rejectFunc = reject;
639
+ });
640
+
641
+ callback =
642
+ callback ||
643
+ function(err, data) {
644
+ if (err) return rejectFunc(err);
645
+ resolveFunc(data);
646
+ };
647
+
648
+ try {
649
+ if (!videoPath) {
650
+ throw new Error('Video path is required.');
651
+ }
652
+ if (typeof videoPath !== 'string') {
653
+ throw new Error('Video path must be a string.');
654
+ }
655
+ if (!fs.existsSync(videoPath)) {
656
+ throw new Error(`Video file not found: ${videoPath}`);
657
+ }
658
+
659
+ const path = require('path');
660
+ const crypto = require('crypto');
661
+
662
+ const fileBuffer = fs.readFileSync(videoPath);
663
+ const fileSize = fileBuffer.length;
664
+ const fileName = path.basename(videoPath);
665
+ const fileExtension = path
666
+ .extname(videoPath)
667
+ .replace('.', '')
668
+ .toLowerCase();
669
+
670
+ // Validate extension
671
+ const allowedExtensions = [
672
+ 'gif',
673
+ 'mov',
674
+ 'mp4',
675
+ 'avi',
676
+ 'mkv',
677
+ 'webm',
678
+ 'wmv',
679
+ 'flv',
680
+ 'mpg',
681
+ 'mpeg',
682
+ '3gp',
683
+ '3g2',
684
+ 'm4v',
685
+ 'ogv',
686
+ ];
687
+ if (!allowedExtensions.includes(fileExtension)) {
688
+ throw new Error(
689
+ `Unsupported video format: .${fileExtension}. Supported: ${allowedExtensions.join(
690
+ ', ',
691
+ )}`,
692
+ );
693
+ }
694
+
695
+ // Determine MIME type
696
+ const mimeTypes = {
697
+ mov: 'video/quicktime',
698
+ mp4: 'video/mp4',
699
+ avi: 'video/x-msvideo',
700
+ mkv: 'video/x-matroska',
701
+ webm: 'video/webm',
702
+ wmv: 'video/x-ms-wmv',
703
+ flv: 'video/x-flv',
704
+ mpg: 'video/mpeg',
705
+ mpeg: 'video/mpeg',
706
+ '3gp': 'video/3gpp',
707
+ '3g2': 'video/3gpp2',
708
+ m4v: 'video/x-m4v',
709
+ gif: 'image/gif',
710
+ ogv: 'video/ogg',
711
+ };
712
+ const mimeType = mimeTypes[fileExtension] || 'video/mp4';
713
+
714
+ // Generate IDs
715
+ const waterfallId = crypto.randomBytes(16).toString('hex');
716
+ const fileHash = crypto
717
+ .createHash('md5')
718
+ .update(fileBuffer)
719
+ .digest('hex');
720
+
721
+ // Common form params
722
+ const baseForm = {
723
+ av: ctx.userID,
724
+ __user: ctx.userID,
725
+ __a: '1',
726
+ __req: '1',
727
+ __ccg: 'EXCELLENT',
728
+ __comet_req: '15',
729
+ fb_dtsg: ctx.fb_dtsg,
730
+ jazoest: ctx.jazoest,
731
+ lsd: ctx.fb_dtsg,
732
+ fb_api_caller_class: 'RelayModern',
733
+ };
734
+
735
+ // ===== Step 1: Get Composer Video Uploader Config =====
736
+ const configForm = {
737
+ ...baseForm,
738
+ fb_api_req_friendly_name: 'useComposerVideoUploaderConfigQuery',
739
+ variables: JSON.stringify({
740
+ actorID: ctx.userID,
741
+ entryPoint: 'feed',
742
+ targetID: '',
743
+ }),
744
+ doc_id: '9734072893355148',
745
+ };
746
+
747
+ const configRes = await defaultFuncs
748
+ .post('https://www.facebook.com/api/graphql/', ctx.jar, configForm, {})
749
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
750
+
751
+
752
+
753
+ // Extract config (endpoints, service info)
754
+ // NOTE: config is returned as a JSON string under data.viewer
755
+ let startUri =
756
+ 'https://vupload2.facebook.com/ajax/video/upload/requests/start/';
757
+ let receiveUri =
758
+ 'https://vupload2.facebook.com/ajax/video/upload/requests/receive/';
759
+ let ruploadServiceName = 'rupload-hkg1-2.up';
760
+ let ruploadServiceDomain = 'facebook.com';
761
+
762
+ try {
763
+ const configRaw =
764
+ configRes?.data?.viewer?.comet_composer_video_uploader_config;
765
+ if (configRaw) {
766
+ const configData =
767
+ typeof configRaw === 'string' ? JSON.parse(configRaw) : configRaw;
768
+ startUri = configData.start_uri || startUri;
769
+ receiveUri = configData.receive_uri || receiveUri;
770
+ ruploadServiceName =
771
+ configData.resumable_service_name || ruploadServiceName;
772
+ ruploadServiceDomain =
773
+ configData.resumable_service_domain || ruploadServiceDomain;
774
+ } else {
775
+ }
776
+ } catch (e) {
777
+ // Config parse error, use defaults
778
+ }
779
+
780
+ // ===== Step 2: Get Server Validation Config (optional, for validation) =====
781
+ const validationForm = {
782
+ ...baseForm,
783
+ fb_api_req_friendly_name:
784
+ 'MediaUploadFBDefaultServerConfigurationRetrieverQuery',
785
+ variables: JSON.stringify({ source_type: 'newsfeed_composer' }),
786
+ doc_id: '24229633186643574',
787
+ };
788
+
789
+ const validationRes = await defaultFuncs
790
+ .post(
791
+ 'https://www.facebook.com/api/graphql/',
792
+ ctx.jar,
793
+ validationForm,
794
+ {},
795
+ )
796
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
797
+
798
+
799
+
800
+ // ===== Step 3: Initialize Upload Session =====
801
+ // Browser sends: POST vupload2.facebook.com/ajax/video/upload/requests/start/?__a=1
802
+ // Cross-subdomain: must manually extract cookies + send full auth params
803
+ const startRequestUrl = startUri + '?__a=1';
804
+
805
+ const startForm = {
806
+ // Upload-specific params
807
+ waterfall_id: waterfallId,
808
+ target_id: ctx.userID,
809
+ source: 'newsfeed_composer',
810
+ composer_entry_point_ref: 'feed',
811
+ supports_chunking: 'true',
812
+ supports_file_api: 'true',
813
+ file_size: String(fileSize),
814
+ file_extension: fileExtension,
815
+ partition_start_offset: '0',
816
+ partition_end_offset: String(fileSize),
817
+ composer_dialog_version: 'V2',
818
+ video_publisher_action_source: '',
819
+ // Auth params (must be explicit for cross-subdomain)
820
+ __aaid: '0',
821
+ __user: ctx.userID,
822
+ __a: '1',
823
+ __req: '1',
824
+ __hs: ctx.hs || '',
825
+ dpr: '2',
826
+ __ccg: 'EXCELLENT',
827
+ __rev: ctx.revision,
828
+ __comet_req: '15',
829
+ fb_dtsg: ctx.fb_dtsg,
830
+ jazoest: ctx.jazoest,
831
+ lsd: ctx.lsd,
832
+ av: ctx.userID,
833
+ };
834
+
835
+
836
+
837
+ const startRes = await utils
838
+ .post(startRequestUrl, ctx.jar, startForm, ctx.globalOptions, ctx, {
839
+ X_fb_video_waterfall_id: waterfallId,
840
+ Origin: 'https://www.facebook.com',
841
+ Referer: 'https://www.facebook.com/',
842
+ 'Content-Type': 'application/x-www-form-urlencoded',
843
+ 'Sec-Fetch-Dest': 'empty',
844
+ 'Sec-Fetch-Mode': 'cors',
845
+ 'Sec-Fetch-Site': 'same-site',
846
+ })
847
+
848
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
849
+
850
+
851
+
852
+ const videoId = startRes?.payload?.video_id || startRes?.video_id;
853
+ const uploadSessionId =
854
+ startRes?.payload?.upload_session_id || startRes?.upload_session_id;
855
+ const startOffset =
856
+ startRes?.payload?.start_offset ?? startRes?.start_offset ?? 0;
857
+ const endOffset =
858
+ startRes?.payload?.end_offset ?? startRes?.end_offset ?? fileSize;
859
+
860
+ if (!videoId) {
861
+ throw new Error(
862
+ 'Failed to get video_id from upload session init. Response: ' +
863
+ JSON.stringify(startRes),
864
+ );
865
+ }
866
+
867
+ // ===== Step 4: Probe rupload server =====
868
+ // Browser sends ALL auth params as query string on rupload URLs
869
+ const ruploadHost = `${ruploadServiceName}.${ruploadServiceDomain}`;
870
+ const ruploadPath = `/fb_video/${fileHash}-${startOffset}-${endOffset}`;
871
+ const ruploadAuthParams = new URLSearchParams({
872
+ __aaid: '0',
873
+ __user: ctx.userID,
874
+ __a: '1',
875
+ __req: '1',
876
+ dpr: '2',
877
+ __ccg: 'EXCELLENT',
878
+ __rev: ctx.revision || '',
879
+ __comet_req: '15',
880
+ fb_dtsg: ctx.fb_dtsg,
881
+ jazoest: ctx.jazoest,
882
+ lsd: ctx.lsd || '',
883
+ }).toString();
884
+ const ruploadBaseUrl = `https://${ruploadHost}${ruploadPath}?${ruploadAuthParams}`;
885
+
886
+ const corsHeaders = {
887
+ Origin: 'https://www.facebook.com',
888
+ Referer: 'https://www.facebook.com/',
889
+ 'Sec-Fetch-Dest': 'empty',
890
+ 'Sec-Fetch-Mode': 'cors',
891
+ 'Sec-Fetch-Site': 'same-site',
892
+ };
893
+
894
+
895
+ const probeRes = await utils.get(
896
+ ruploadBaseUrl,
897
+ ctx.jar,
898
+ undefined,
899
+ ctx.globalOptions,
900
+ ctx,
901
+ corsHeaders,
902
+ );
903
+
904
+
905
+ // ===== Step 5: Upload Binary Data =====
906
+ const uploadHeaders = {
907
+ 'Content-Type': 'application/octet-stream',
908
+ Product_media_id: String(videoId),
909
+ Id: String(uploadSessionId),
910
+ Start_offset: String(startOffset),
911
+ End_offset: String(endOffset),
912
+ Offset: String(startOffset),
913
+ 'X-Entity-Length': String(fileSize),
914
+ 'X-Total-Asset-Size': String(fileSize),
915
+ 'X-Entity-Type': mimeType,
916
+ 'X-Entity-Name': encodeURIComponent(fileName),
917
+ Composer_session_id: waterfallId,
918
+ ...corsHeaders,
919
+ };
920
+
921
+
922
+ const uploadRes = await utils.postRaw(
923
+ ruploadBaseUrl,
924
+ ctx.jar,
925
+ fileBuffer,
926
+ ctx.globalOptions,
927
+ ctx,
928
+ uploadHeaders,
929
+ );
930
+
931
+ let fileHandle: string | null = null;
932
+ try {
933
+ const uploadBody =
934
+ typeof uploadRes.body === 'string'
935
+ ? JSON.parse(uploadRes.body)
936
+ : uploadRes.body;
937
+ fileHandle = uploadBody?.h || null;
938
+ } catch (_) {
939
+ /* parse error */
940
+ }
941
+
942
+ if (!fileHandle) {
943
+ throw new Error(
944
+ 'Failed to get file handle from binary upload. Response: ' +
945
+ JSON.stringify(uploadRes.body),
946
+ );
947
+ }
948
+
949
+ // ===== Step 6: Confirm Upload (Receive) =====
950
+ // Browser sends auth in form body, URL just has ?__a=1
951
+ const receiveUrl = new URL(receiveUri);
952
+ receiveUrl.searchParams.append('__a', '1');
953
+
954
+ const receiveForm = {
955
+ waterfall_id: waterfallId,
956
+ target_id: ctx.userID,
957
+ video_id: String(videoId),
958
+ source: 'newsfeed_composer',
959
+ composer_entry_point_ref: 'feed',
960
+ supports_chunking: 'true',
961
+ supports_upload_service: 'true',
962
+ partition_start_offset: String(startOffset),
963
+ partition_end_offset: String(endOffset),
964
+ start_offset: String(startOffset),
965
+ end_offset: String(endOffset),
966
+ upload_speed: '0',
967
+ fbuploader_video_file_chunk: fileHandle,
968
+ composer_dialog_version: 'V2',
969
+ // Auth params in form body (matching browser)
970
+ __aaid: '0',
971
+ __user: ctx.userID,
972
+ __a: '1',
973
+ __ccg: 'EXCELLENT',
974
+ __comet_req: '15',
975
+ fb_dtsg: ctx.fb_dtsg,
976
+ jazoest: ctx.jazoest,
977
+ lsd: ctx.lsd || '',
978
+ };
979
+
980
+
981
+ const receiveRes = await utils
982
+ .post(
983
+ receiveUrl.toString(),
984
+ ctx.jar,
985
+ receiveForm,
986
+ ctx.globalOptions,
987
+ ctx,
988
+ {
989
+ X_fb_video_waterfall_id: waterfallId,
990
+ ...corsHeaders,
991
+ },
992
+ )
993
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
994
+
995
+ // Verify upload complete: start_offset should equal end_offset
996
+ const recvStart =
997
+ receiveRes?.payload?.start_offset ?? receiveRes?.start_offset;
998
+ const recvEnd = receiveRes?.payload?.end_offset ?? receiveRes?.end_offset;
999
+ if (
1000
+ recvStart !== undefined &&
1001
+ recvEnd !== undefined &&
1002
+ recvStart !== recvEnd
1003
+ ) {
1004
+ utils.warn(
1005
+ 'uploadVideo',
1006
+ `Upload may be incomplete: start_offset=${recvStart}, end_offset=${recvEnd}`,
1007
+ );
1008
+ }
1009
+
1010
+ const result = {
1011
+ success: true,
1012
+ videoID: String(videoId),
1013
+ videoId: String(videoId),
1014
+ uploadID: waterfallId,
1015
+ uploadSessionID: String(uploadSessionId),
1016
+ fileHandle: fileHandle,
1017
+ data: receiveRes,
1018
+ };
1019
+
1020
+ callback(null, result);
1021
+ } catch (err) {
1022
+ utils.error('uploadVideo', err);
1023
+ callback(err);
1024
+ }
1025
+
1026
+ return returnPromise;
1027
+ }
1028
+
593
1029
  return {
594
1030
  create: createPost,
1031
+ createPost: createPost,
595
1032
  delete: deletePost,
596
1033
  getComments: getPostComments,
597
1034
  uploadPhoto: uploadPhoto,
1035
+ uploadVideo: uploadVideo,
598
1036
  };
599
1037
  }
@@ -1,3 +1,4 @@
1
+ import * as fs from 'fs';
1
2
  import utils = require('../../../utils');
2
3
  import { URL } from 'url';
3
4
 
@@ -178,6 +179,140 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
178
179
  }
179
180
  }
180
181
 
182
+ /**
183
+ * Uploads a photo for story usage.
184
+ * Uses waterfallxapp=comet_stories to target the stories upload endpoint.
185
+ * @param {string} photoPath Absolute path to the photo file.
186
+ * @returns {Promise<{success: boolean, photoID: string, photoId: string, imageSrc: string, data: object}>}
187
+ */
188
+ async function uploadStoryPhoto(photoPath) {
189
+ if (!photoPath) {
190
+ throw new Error('Photo path is required.');
191
+ }
192
+ if (typeof photoPath !== 'string') {
193
+ throw new Error('Photo path must be a string.');
194
+ }
195
+ if (!fs.existsSync(photoPath)) {
196
+ throw new Error(`Photo file not found: ${photoPath}`);
197
+ }
198
+
199
+ const photoStream = fs.createReadStream(photoPath);
200
+
201
+ const url = new URL(
202
+ 'https://upload.facebook.com/ajax/react_composer/attachments/photo/upload',
203
+ );
204
+ url.searchParams.append('av', ctx.userID);
205
+ url.searchParams.append('__aaid', '0');
206
+ url.searchParams.append('__user', ctx.userID);
207
+ url.searchParams.append('__a', '1');
208
+ url.searchParams.append('dpr', '1');
209
+ url.searchParams.append('__ccg', 'EXCELLENT');
210
+ url.searchParams.append('__comet_req', '15');
211
+ url.searchParams.append('fb_dtsg', ctx.fb_dtsg);
212
+ url.searchParams.append('jazoest', ctx.jazoest);
213
+ url.searchParams.append('lsd', ctx.fb_dtsg);
214
+
215
+ const form = {
216
+ source: '8',
217
+ profile_id: ctx.userID,
218
+ waterfallxapp: 'comet_stories',
219
+ farr: photoStream,
220
+ };
221
+
222
+ const uploadResponse = await utils.postFormData(
223
+ url.toString(),
224
+ ctx.jar,
225
+ form,
226
+ ctx.globalOptions,
227
+ ctx,
228
+ );
229
+
230
+ const uploadResult = JSON.parse(
231
+ uploadResponse.body.toString().replace(/^for \(;;\);/, ''),
232
+ );
233
+
234
+ if (uploadResult.error || uploadResult.errors) {
235
+ throw new Error(
236
+ JSON.stringify(uploadResult.error || uploadResult.errors),
237
+ );
238
+ }
239
+
240
+ const photoID =
241
+ uploadResult.payload?.photoID || uploadResult.payload?.fbid || null;
242
+
243
+ if (!photoID) {
244
+ throw new Error('Could not extract photoID from upload response.');
245
+ }
246
+
247
+ return {
248
+ success: true,
249
+ photoID: String(photoID),
250
+ photoId: String(photoID),
251
+ imageSrc: uploadResult.payload?.imageSrc || null,
252
+ data: uploadResult.payload,
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Creates a new image-based story.
258
+ * @param {string} photoPath Absolute path to the photo file.
259
+ * @returns {Promise<{success: boolean, storyID: string}>} A promise that resolves with the new story's ID.
260
+ */
261
+ async function createImageStory(photoPath) {
262
+ // Step 1: Upload photo for story
263
+ const uploadResult = await uploadStoryPhoto(photoPath);
264
+
265
+ // Step 2: Create story with the uploaded photo
266
+ const variables = {
267
+ input: {
268
+ audiences: [{ stories: { self: { target_id: ctx.userID } } }],
269
+ audiences_is_complete: true,
270
+ logging: {
271
+ composer_session_id: 'createStoriesImage-' + Date.now(),
272
+ },
273
+ navigation_data: {
274
+ attribution_id_v2: 'StoriesCreateRoot.react,comet.stories.create',
275
+ },
276
+ source: 'WWW',
277
+ attachments: [
278
+ {
279
+ photo: {
280
+ id: uploadResult.photoID,
281
+ overlays: [],
282
+ },
283
+ },
284
+ ],
285
+ tracking: [null],
286
+ actor_id: ctx.userID,
287
+ client_mutation_id: '1',
288
+ },
289
+ };
290
+
291
+ const form = {
292
+ __a: '1',
293
+ fb_api_caller_class: 'RelayModern',
294
+ fb_api_req_friendly_name: 'StoriesCreateMutation',
295
+ variables: JSON.stringify(variables),
296
+ doc_id: '24226878183562473',
297
+ };
298
+
299
+ const res = await defaultFuncs.post(
300
+ 'https://www.facebook.com/api/graphql/',
301
+ ctx.jar,
302
+ form,
303
+ {},
304
+ );
305
+ if (res.data.errors) throw new Error(JSON.stringify(res.data.errors));
306
+
307
+ const storyNode =
308
+ res.data?.data?.story_create?.viewer?.actor?.story_bucket?.nodes[0]
309
+ ?.first_story_to_show;
310
+ if (!storyNode || !storyNode.id)
311
+ throw new Error('Could not find the storyCardID in the response.');
312
+
313
+ return { success: true, storyID: storyNode.id };
314
+ }
315
+
181
316
  return {
182
317
  /**
183
318
  * Creates a new text-based story.
@@ -187,6 +322,18 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
187
322
  * @returns {Promise<{success: boolean, storyID: string}>}
188
323
  */
189
324
  create,
325
+ /**
326
+ * Uploads a photo for use in an image story.
327
+ * @param {string} photoPath Absolute path to the photo file.
328
+ * @returns {Promise<{success: boolean, photoID: string, photoId: string, imageSrc: string, data: object}>}
329
+ */
330
+ uploadStoryPhoto,
331
+ /**
332
+ * Creates a new image-based story. Handles upload + publish in one call.
333
+ * @param {string} photoPath Absolute path to the photo file.
334
+ * @returns {Promise<{success: boolean, storyID: string}>}
335
+ */
336
+ createImageStory,
190
337
  /**
191
338
  * Reacts to a story with a specific emoji.
192
339
  * @param {string} storyIdOrUrl The ID or full URL of the story to react to.