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.
- package/dist/deltas/apis/create.js +22 -0
- package/dist/deltas/apis/create.js.map +1 -1
- package/dist/deltas/apis/posting/group.js +607 -0
- package/dist/deltas/apis/posting/group.js.map +1 -0
- package/dist/deltas/apis/posting/post.js +346 -1
- package/dist/deltas/apis/posting/post.js.map +1 -1
- package/dist/deltas/apis/posting/story.js +147 -0
- package/dist/deltas/apis/posting/story.js.map +1 -1
- package/dist/deltas/apis/threads/searchGroup.js +159 -0
- package/dist/deltas/apis/threads/searchGroup.js.map +1 -0
- package/dist/deltas/apis/users/searchGroups.js +221 -0
- package/dist/deltas/apis/users/searchGroups.js.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/types/deltas/apis/create.d.ts +22 -0
- package/dist/types/deltas/apis/posting/group.d.ts +90 -0
- package/dist/types/deltas/apis/posting/post.d.ts +7 -0
- package/dist/types/deltas/apis/posting/story.d.ts +21 -0
- package/dist/types/deltas/apis/threads/searchGroup.d.ts +15 -0
- package/dist/types/deltas/apis/users/searchGroups.d.ts +17 -0
- package/dist/types/utils/axios.d.ts +1 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/utils/axios.js +12 -0
- package/dist/utils/axios.js.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/request.txt +60 -0
- package/src/deltas/apis/create.ts +25 -0
- package/src/deltas/apis/posting/group.ts +754 -0
- package/src/deltas/apis/posting/post.ts +439 -1
- package/src/deltas/apis/posting/story.ts +147 -0
- package/src/types/index.d.ts +66 -0
- package/src/utils/axios.ts +19 -0
- package/src/utils/index.ts +1 -0
- package/LICENSE-MIT +0 -21
- package/examples/post.example.js +0 -149
- package/friend.html +0 -534
- 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.
|