bb-fca 2.0.8 → 2.0.9
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/core/models/buildAPI.js +3 -3
- package/dist/core/models/buildAPI.js.map +1 -1
- package/dist/core/models/loginHelper.js +29 -28
- package/dist/core/models/loginHelper.js.map +1 -1
- package/dist/deltas/apis/create.js +9 -0
- package/dist/deltas/apis/create.js.map +1 -1
- package/dist/deltas/apis/posting/group.js +30 -159
- package/dist/deltas/apis/posting/group.js.map +1 -1
- package/dist/deltas/apis/posting/post.js +805 -12
- package/dist/deltas/apis/posting/post.js.map +1 -1
- package/dist/index.d.ts +35 -58
- package/dist/types/deltas/apis/create.d.ts +9 -0
- package/dist/types/deltas/apis/posting/group.d.ts +1 -23
- package/dist/types/deltas/apis/posting/post.d.ts +23 -0
- package/dist/types/utils/constants.d.ts +26 -16
- package/dist/utils/constants.js +42 -29
- package/dist/utils/constants.js.map +1 -1
- package/package.json +1 -1
- package/request.txt +60 -0
- package/src/core/models/buildAPI.ts +3 -3
- package/src/core/models/loginHelper.ts +32 -30
- package/src/deltas/apis/create.ts +10 -0
- package/src/deltas/apis/posting/group.ts +64 -205
- package/src/deltas/apis/posting/post.ts +932 -33
- package/src/types/index.d.ts +35 -58
- package/src/utils/constants.ts +84 -57
- package/a.html +0 -537
- package/a.json +0 -5915
- package/src/utils/formatters.old.ts +0 -1049
- package/task.txt +0 -24
|
@@ -24,6 +24,7 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
24
24
|
let privacy = 'SELF';
|
|
25
25
|
let photos = [];
|
|
26
26
|
let videoId: string | null = null;
|
|
27
|
+
let groupID: string | null = null;
|
|
27
28
|
|
|
28
29
|
if (typeof options === 'string') {
|
|
29
30
|
postMessage = options;
|
|
@@ -32,6 +33,7 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
32
33
|
privacy = options.privacy || 'SELF';
|
|
33
34
|
photos = options.photos || [];
|
|
34
35
|
videoId = options.video || null;
|
|
36
|
+
groupID = options.groupID || null;
|
|
35
37
|
} else {
|
|
36
38
|
throw new Error('Invalid input: expected string or object.');
|
|
37
39
|
}
|
|
@@ -59,10 +61,12 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
59
61
|
.substring(2)}`;
|
|
60
62
|
const idempotenceToken = `${composerSessionId}_FEED`;
|
|
61
63
|
|
|
64
|
+
const isGroup = !!groupID;
|
|
65
|
+
|
|
62
66
|
const variables: any = {
|
|
63
67
|
input: {
|
|
64
68
|
composer_entry_point: 'inline_composer',
|
|
65
|
-
composer_source_surface: 'newsfeed',
|
|
69
|
+
composer_source_surface: isGroup ? 'group' : 'newsfeed',
|
|
66
70
|
composer_type: 'feed',
|
|
67
71
|
idempotence_token: idempotenceToken,
|
|
68
72
|
source: 'WWW',
|
|
@@ -97,11 +101,11 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
97
101
|
actor_id: ctx.userID,
|
|
98
102
|
client_mutation_id: '1',
|
|
99
103
|
},
|
|
100
|
-
feedLocation: 'NEWSFEED',
|
|
104
|
+
feedLocation: isGroup ? 'GROUP' : 'NEWSFEED',
|
|
101
105
|
feedbackSource: 1,
|
|
102
106
|
focusCommentID: null,
|
|
103
107
|
gridMediaWidth: null,
|
|
104
|
-
groupID:
|
|
108
|
+
groupID: groupID,
|
|
105
109
|
scale: 2,
|
|
106
110
|
privacySelectorRenderLocation: 'COMET_STREAM',
|
|
107
111
|
checkPhotosToReelsUpsellEligibility: true,
|
|
@@ -112,7 +116,7 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
112
116
|
isFeed: true,
|
|
113
117
|
isFundraiser: false,
|
|
114
118
|
isFunFactPost: false,
|
|
115
|
-
isGroup:
|
|
119
|
+
isGroup: isGroup,
|
|
116
120
|
isEvent: false,
|
|
117
121
|
isTimeline: false,
|
|
118
122
|
isSocialLearning: false,
|
|
@@ -123,7 +127,8 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
123
127
|
canUserManageOffers: false,
|
|
124
128
|
__relay_internal__pv__CometUFIShareActionMigrationrelayprovider: true,
|
|
125
129
|
__relay_internal__pv__CometUFI_dedicated_comment_routable_dialog_gkrelayprovider: false,
|
|
126
|
-
__relay_internal__pv__CometUFICommentAutoTranslationTyperelayprovider:
|
|
130
|
+
__relay_internal__pv__CometUFICommentAutoTranslationTyperelayprovider:
|
|
131
|
+
'ORIGINAL',
|
|
127
132
|
__relay_internal__pv__IsWorkUserrelayprovider: false,
|
|
128
133
|
__relay_internal__pv__CometUFIReactionsEnableShortNamerelayprovider: false,
|
|
129
134
|
__relay_internal__pv__FBReels_deprecate_short_form_video_context_gkrelayprovider: true,
|
|
@@ -304,7 +309,10 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
304
309
|
let paginationOpts: any = null;
|
|
305
310
|
if (typeof optionsOrCallback === 'function') {
|
|
306
311
|
callback = optionsOrCallback;
|
|
307
|
-
} else if (
|
|
312
|
+
} else if (
|
|
313
|
+
typeof optionsOrCallback === 'object' &&
|
|
314
|
+
optionsOrCallback !== null
|
|
315
|
+
) {
|
|
308
316
|
paginationOpts = optionsOrCallback;
|
|
309
317
|
}
|
|
310
318
|
|
|
@@ -327,11 +335,15 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
327
335
|
// ── Pagination mode: fetch next page via GraphQL ──────────────────
|
|
328
336
|
// Merge pagination from either postID (object) or second-arg options
|
|
329
337
|
const afterCursor: string | undefined =
|
|
330
|
-
paginationOpts?.after
|
|
331
|
-
|
|
338
|
+
paginationOpts?.after ||
|
|
339
|
+
(typeof postID === 'object' && postID !== null
|
|
340
|
+
? postID.after
|
|
341
|
+
: undefined);
|
|
332
342
|
const feedbackId: string | undefined =
|
|
333
|
-
paginationOpts?.feedback_id
|
|
334
|
-
|
|
343
|
+
paginationOpts?.feedback_id ||
|
|
344
|
+
(typeof postID === 'object' && postID !== null
|
|
345
|
+
? postID.feedback_id
|
|
346
|
+
: undefined);
|
|
335
347
|
|
|
336
348
|
if (afterCursor && feedbackId) {
|
|
337
349
|
// Use GraphQL to fetch next page of comments using the cursor
|
|
@@ -347,7 +359,8 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
347
359
|
scale: 1,
|
|
348
360
|
useDefaultActor: false,
|
|
349
361
|
id: feedbackId,
|
|
350
|
-
__relay_internal__pv__CometUFICommentAutoTranslationTyperelayprovider:
|
|
362
|
+
__relay_internal__pv__CometUFICommentAutoTranslationTyperelayprovider:
|
|
363
|
+
'ORIGINAL',
|
|
351
364
|
__relay_internal__pv__CometUFICommentAvatarStickerAnimatedImagerelayprovider: false,
|
|
352
365
|
__relay_internal__pv__CometUFICommentActionLinksRewriteEnabledrelayprovider: false,
|
|
353
366
|
__relay_internal__pv__IsWorkUserrelayprovider: false,
|
|
@@ -371,7 +384,11 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
371
384
|
);
|
|
372
385
|
|
|
373
386
|
let gqlData: any;
|
|
374
|
-
if (
|
|
387
|
+
if (
|
|
388
|
+
typeof gqlRes.body === 'object' &&
|
|
389
|
+
gqlRes.body !== null &&
|
|
390
|
+
!Buffer.isBuffer(gqlRes.body)
|
|
391
|
+
) {
|
|
375
392
|
gqlData = gqlRes.body;
|
|
376
393
|
} else {
|
|
377
394
|
const body = gqlRes.body.toString();
|
|
@@ -401,7 +418,8 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
401
418
|
const c: any = {
|
|
402
419
|
id: edge.node.legacy_fbid,
|
|
403
420
|
graphql_id: edge.node.id,
|
|
404
|
-
text:
|
|
421
|
+
text:
|
|
422
|
+
edge.node.body?.text || edge.node.preferred_body?.text || '',
|
|
405
423
|
created_time: edge.node.created_time,
|
|
406
424
|
author: {
|
|
407
425
|
id: edge.node.author?.id,
|
|
@@ -409,7 +427,8 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
409
427
|
avatar: edge.node.author?.profile_picture_depth_0?.uri,
|
|
410
428
|
},
|
|
411
429
|
reply_count: edge.node.feedback?.replies_fields?.count || 0,
|
|
412
|
-
total_reply_count:
|
|
430
|
+
total_reply_count:
|
|
431
|
+
edge.node.feedback?.replies_fields?.total_count || 0,
|
|
413
432
|
depth: edge.node.depth || 0,
|
|
414
433
|
attachments: edge.node.attachments || [],
|
|
415
434
|
};
|
|
@@ -419,7 +438,8 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
419
438
|
if (!re?.node) return null;
|
|
420
439
|
return {
|
|
421
440
|
id: re.node.legacy_fbid,
|
|
422
|
-
text:
|
|
441
|
+
text:
|
|
442
|
+
re.node.body?.text || re.node.preferred_body?.text || '',
|
|
423
443
|
created_time: re.node.created_time,
|
|
424
444
|
author: {
|
|
425
445
|
id: re.node.author?.id,
|
|
@@ -507,7 +527,9 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
507
527
|
* @private
|
|
508
528
|
* @returns {{ comments: any[], page_info: any | null }}
|
|
509
529
|
*/
|
|
510
|
-
function extractCommentsFromHTML(
|
|
530
|
+
function extractCommentsFromHTML(
|
|
531
|
+
htmlContent: string,
|
|
532
|
+
): { comments: any[]; page_info: any | null } {
|
|
511
533
|
const comments: any[] = [];
|
|
512
534
|
let page_info: any = null;
|
|
513
535
|
|
|
@@ -583,17 +605,20 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
583
605
|
if (commentData.page_info && !page_info) {
|
|
584
606
|
page_info = {
|
|
585
607
|
end_cursor: commentData.page_info.end_cursor || null,
|
|
586
|
-
has_next_page:
|
|
587
|
-
|
|
588
|
-
|
|
608
|
+
has_next_page:
|
|
609
|
+
commentData.page_info.has_next_page || false,
|
|
610
|
+
has_previous_page:
|
|
611
|
+
commentData.page_info.has_previous_page || false,
|
|
612
|
+
start_cursor:
|
|
613
|
+
commentData.page_info.start_cursor || null,
|
|
589
614
|
};
|
|
590
615
|
// Also try to capture feedback_id for GraphQL pagination
|
|
591
616
|
try {
|
|
592
617
|
const feedback =
|
|
593
618
|
result.data.node_v2?.comet_sections?.feedback?.story
|
|
594
619
|
?.story_ufi_container?.story?.feedback_context
|
|
595
|
-
?.feedback_target_with_context
|
|
596
|
-
?.feedback;
|
|
620
|
+
?.feedback_target_with_context
|
|
621
|
+
?.comment_list_renderer?.feedback;
|
|
597
622
|
if (feedback?.id && !page_info.feedback_id) {
|
|
598
623
|
page_info.feedback_id = feedback.id;
|
|
599
624
|
}
|
|
@@ -671,6 +696,217 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
671
696
|
return { comments, page_info };
|
|
672
697
|
}
|
|
673
698
|
|
|
699
|
+
/**
|
|
700
|
+
* Extract the numeric `post_id` (and related IDs) from a Facebook post HTML page.
|
|
701
|
+
*
|
|
702
|
+
* This follows the same pipeline as `getPostComments`:
|
|
703
|
+
* 1. Fetches the page HTML with session cookies (GET request).
|
|
704
|
+
* 2. Scans every `<script type="application/json" data-sjs>` block.
|
|
705
|
+
* 3. Searches each parsed JSON tree for `"post_id"` strings (and `"story_id"`,
|
|
706
|
+
* `"feedback_id"`, etc.) that appear alongside the post data.
|
|
707
|
+
*
|
|
708
|
+
* Works with **all** URL formats:
|
|
709
|
+
* - `/posts/pfbid{token}` ← pfbid (server resolves it during HTML render)
|
|
710
|
+
* - `/posts/{numericID}`
|
|
711
|
+
* - `permalink.php?story_fbid=...`
|
|
712
|
+
*
|
|
713
|
+
* @param {string} urlOrID - Full Facebook post URL, or a plain numeric post ID.
|
|
714
|
+
* If a numeric ID is passed, it is fetched as
|
|
715
|
+
* `facebook.com/permalink.php?story_fbid={id}&id={userID}`.
|
|
716
|
+
* @param {Function} [callback] - Optional node-style `(err, result)` callback.
|
|
717
|
+
* @returns {Promise<{
|
|
718
|
+
* post_id: string | null, // numeric post ID found in HTML ("post_id":"…")
|
|
719
|
+
* story_id: string | null, // numeric story/post ID (alias)
|
|
720
|
+
* feedback_id: string | null, // base64 GraphQL feedback ID
|
|
721
|
+
* post_url: string, // URL that was actually fetched
|
|
722
|
+
* }>}
|
|
723
|
+
*
|
|
724
|
+
* @example
|
|
725
|
+
* const { post_id } = await api.post.getPostIDFromURL(
|
|
726
|
+
* 'https://www.facebook.com/tuyetcollection/posts/pfbid0Yo6XDaKKnFKpENu3g2c2to1zJe7jFoZNr2NqjiXRiHmwjsy8dwbE84y4n7syCSfMl'
|
|
727
|
+
* );
|
|
728
|
+
* console.log(post_id); // '1510599910435387'
|
|
729
|
+
*
|
|
730
|
+
* // Then react using the real numeric ID
|
|
731
|
+
* await api.post.reactWithInfo(post_id, 'LOVE');
|
|
732
|
+
*/
|
|
733
|
+
async function getPostIDFromURL(
|
|
734
|
+
urlOrID: string,
|
|
735
|
+
callback?: Function,
|
|
736
|
+
): Promise<any> {
|
|
737
|
+
let resolveFunc: Function = function() {};
|
|
738
|
+
let rejectFunc: Function = function() {};
|
|
739
|
+
|
|
740
|
+
const returnPromise = new Promise<any>((resolve, reject) => {
|
|
741
|
+
resolveFunc = resolve;
|
|
742
|
+
rejectFunc = reject;
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
callback =
|
|
746
|
+
callback ||
|
|
747
|
+
function(err: any, data: any) {
|
|
748
|
+
if (err) return rejectFunc(err);
|
|
749
|
+
resolveFunc(data);
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
try {
|
|
753
|
+
if (!urlOrID) throw new Error('urlOrID is required.');
|
|
754
|
+
|
|
755
|
+
// ── Build fetch URL ────────────────────────────────────────────────
|
|
756
|
+
let fetchUrl: string;
|
|
757
|
+
|
|
758
|
+
if (urlOrID.startsWith('http://') || urlOrID.startsWith('https://')) {
|
|
759
|
+
fetchUrl = urlOrID;
|
|
760
|
+
} else if (urlOrID.startsWith('/')) {
|
|
761
|
+
fetchUrl = `https://www.facebook.com${urlOrID}`;
|
|
762
|
+
} else if (/^\d+$/.test(urlOrID)) {
|
|
763
|
+
// plain numeric ID → use permalink.php format (same as getPostComments)
|
|
764
|
+
fetchUrl = `https://www.facebook.com/permalink.php?story_fbid=${urlOrID}&id=${ctx.userID}`;
|
|
765
|
+
} else {
|
|
766
|
+
// pfbid token without full URL
|
|
767
|
+
fetchUrl = `https://www.facebook.com/permalink.php?story_fbid=${urlOrID}&id=${ctx.userID}`;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// ── Fetch HTML ─────────────────────────────────────────────────────
|
|
771
|
+
const resData = await utils.get(
|
|
772
|
+
fetchUrl,
|
|
773
|
+
ctx.jar,
|
|
774
|
+
{},
|
|
775
|
+
ctx.globalOptions,
|
|
776
|
+
ctx,
|
|
777
|
+
);
|
|
778
|
+
const html = resData.body.toString() as string;
|
|
779
|
+
|
|
780
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
781
|
+
/** Deep-search an object tree; call visitor(value, key) on every value. */
|
|
782
|
+
function deepSearch(
|
|
783
|
+
obj: any,
|
|
784
|
+
visitor: (v: any, k: string | number) => void,
|
|
785
|
+
): void {
|
|
786
|
+
if (!obj || typeof obj !== 'object') return;
|
|
787
|
+
for (const key of Object.keys(obj)) {
|
|
788
|
+
visitor(obj[key], key);
|
|
789
|
+
deepSearch(obj[key], visitor);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** Find the first string value of `fieldName` anywhere in `obj`. */
|
|
794
|
+
function findField(obj: any, fieldName: string): string | null {
|
|
795
|
+
let found: string | null = null;
|
|
796
|
+
deepSearch(obj, (v, k) => {
|
|
797
|
+
if (found) return;
|
|
798
|
+
if (k === fieldName && typeof v === 'string' && v.length > 0) {
|
|
799
|
+
found = v;
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
return found;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// ── Parse <script data-sjs> blocks (same as getPostComments) ───────
|
|
806
|
+
const scriptMatches = html.match(
|
|
807
|
+
/<script type="application\/json"\s+data-content-len="\d+"\s+data-sjs>(.*?)<\/script>/gs,
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
let post_id: string | null = null;
|
|
811
|
+
let story_id: string | null = null;
|
|
812
|
+
let feedback_id: string | null = null;
|
|
813
|
+
|
|
814
|
+
if (scriptMatches) {
|
|
815
|
+
outer: for (const scriptTag of scriptMatches) {
|
|
816
|
+
try {
|
|
817
|
+
const inner = scriptTag.match(/<script[^>]*>(.*?)<\/script>/s);
|
|
818
|
+
if (!inner) continue;
|
|
819
|
+
|
|
820
|
+
const jsonData = JSON.parse(inner[1]);
|
|
821
|
+
|
|
822
|
+
if (!jsonData.require || !Array.isArray(jsonData.require)) continue;
|
|
823
|
+
|
|
824
|
+
for (const req of jsonData.require) {
|
|
825
|
+
if (!Array.isArray(req)) continue;
|
|
826
|
+
|
|
827
|
+
if (req[0] === 'ScheduledServerJS' && req[1] === 'handle') {
|
|
828
|
+
const payload = req[3];
|
|
829
|
+
if (!payload || !Array.isArray(payload)) continue;
|
|
830
|
+
|
|
831
|
+
for (const item of payload) {
|
|
832
|
+
if (!item.__bbox?.require) continue;
|
|
833
|
+
|
|
834
|
+
for (const bboxReq of item.__bbox.require) {
|
|
835
|
+
if (!Array.isArray(bboxReq)) continue;
|
|
836
|
+
|
|
837
|
+
if (
|
|
838
|
+
bboxReq[0] === 'RelayPrefetchedStreamCache' &&
|
|
839
|
+
bboxReq[1] === 'next'
|
|
840
|
+
) {
|
|
841
|
+
const streamData = bboxReq[3];
|
|
842
|
+
if (!Array.isArray(streamData) || streamData.length < 2)
|
|
843
|
+
continue;
|
|
844
|
+
|
|
845
|
+
const bboxData = streamData[1];
|
|
846
|
+
if (!bboxData?.__bbox?.result) continue;
|
|
847
|
+
|
|
848
|
+
const result = bboxData.__bbox.result;
|
|
849
|
+
|
|
850
|
+
// ── Search for post_id / story_id ─────────────────
|
|
851
|
+
if (!post_id) post_id = findField(result, 'post_id');
|
|
852
|
+
if (!story_id) story_id = findField(result, 'story_id');
|
|
853
|
+
|
|
854
|
+
// ── Search for feedback_id (same path as getPostComments) ──
|
|
855
|
+
if (!feedback_id) {
|
|
856
|
+
try {
|
|
857
|
+
const feedback =
|
|
858
|
+
result.data?.node_v2?.comet_sections?.feedback
|
|
859
|
+
?.story?.story_ufi_container?.story
|
|
860
|
+
?.feedback_context?.feedback_target_with_context
|
|
861
|
+
?.comment_list_renderer?.feedback;
|
|
862
|
+
if (feedback?.id) feedback_id = feedback.id;
|
|
863
|
+
} catch (_) {}
|
|
864
|
+
|
|
865
|
+
// Fallback: deep-search for feedback_id string
|
|
866
|
+
if (!feedback_id)
|
|
867
|
+
feedback_id = findField(result, 'feedback_id');
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Stop once we have the numeric post_id
|
|
871
|
+
if (post_id) break outer;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
} catch (_) {
|
|
878
|
+
continue; // skip malformed JSON blocks
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// ── Fallback: regex scan on raw HTML ────────────────────────────────
|
|
884
|
+
// "post_id":"1510599910435387" or "post_id":1510599910435387
|
|
885
|
+
if (!post_id) {
|
|
886
|
+
const m = html.match(/"post_id"\s*:\s*"?(\d{10,20})"?/);
|
|
887
|
+
if (m) post_id = m[1];
|
|
888
|
+
}
|
|
889
|
+
if (!story_id) {
|
|
890
|
+
const m = html.match(/"story_id"\s*:\s*"?(\d{10,20})"?/);
|
|
891
|
+
if (m) story_id = m[1];
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const resultObj = {
|
|
895
|
+
post_id,
|
|
896
|
+
story_id: story_id || post_id, // they're often the same
|
|
897
|
+
feedback_id,
|
|
898
|
+
post_url: fetchUrl,
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
callback(null, resultObj);
|
|
902
|
+
} catch (err) {
|
|
903
|
+
utils.error('getPostIDFromURL', err);
|
|
904
|
+
callback(err);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return returnPromise;
|
|
908
|
+
}
|
|
909
|
+
|
|
674
910
|
/**
|
|
675
911
|
* Upload a photo to Facebook
|
|
676
912
|
* @param {string} photoPath - File path to the photo
|
|
@@ -906,8 +1142,6 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
906
1142
|
.post('https://www.facebook.com/api/graphql/', ctx.jar, configForm, {})
|
|
907
1143
|
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
908
1144
|
|
|
909
|
-
|
|
910
|
-
|
|
911
1145
|
// Extract config (endpoints, service info)
|
|
912
1146
|
// NOTE: config is returned as a JSON string under data.viewer
|
|
913
1147
|
let startUri =
|
|
@@ -953,8 +1187,6 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
953
1187
|
)
|
|
954
1188
|
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
955
1189
|
|
|
956
|
-
|
|
957
|
-
|
|
958
1190
|
// ===== Step 3: Initialize Upload Session =====
|
|
959
1191
|
// Browser sends: POST vupload2.facebook.com/ajax/video/upload/requests/start/?__a=1
|
|
960
1192
|
// Cross-subdomain: must manually extract cookies + send full auth params
|
|
@@ -990,8 +1222,6 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
990
1222
|
av: ctx.userID,
|
|
991
1223
|
};
|
|
992
1224
|
|
|
993
|
-
|
|
994
|
-
|
|
995
1225
|
const startRes = await utils
|
|
996
1226
|
.post(startRequestUrl, ctx.jar, startForm, ctx.globalOptions, ctx, {
|
|
997
1227
|
X_fb_video_waterfall_id: waterfallId,
|
|
@@ -1005,8 +1235,6 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
1005
1235
|
|
|
1006
1236
|
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
1007
1237
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
1238
|
const videoId = startRes?.payload?.video_id || startRes?.video_id;
|
|
1011
1239
|
const uploadSessionId =
|
|
1012
1240
|
startRes?.payload?.upload_session_id || startRes?.upload_session_id;
|
|
@@ -1049,7 +1277,6 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
1049
1277
|
'Sec-Fetch-Site': 'same-site',
|
|
1050
1278
|
};
|
|
1051
1279
|
|
|
1052
|
-
|
|
1053
1280
|
const probeRes = await utils.get(
|
|
1054
1281
|
ruploadBaseUrl,
|
|
1055
1282
|
ctx.jar,
|
|
@@ -1059,7 +1286,6 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
1059
1286
|
corsHeaders,
|
|
1060
1287
|
);
|
|
1061
1288
|
|
|
1062
|
-
|
|
1063
1289
|
// ===== Step 5: Upload Binary Data =====
|
|
1064
1290
|
const uploadHeaders = {
|
|
1065
1291
|
'Content-Type': 'application/octet-stream',
|
|
@@ -1076,7 +1302,6 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
1076
1302
|
...corsHeaders,
|
|
1077
1303
|
};
|
|
1078
1304
|
|
|
1079
|
-
|
|
1080
1305
|
const uploadRes = await utils.postRaw(
|
|
1081
1306
|
ruploadBaseUrl,
|
|
1082
1307
|
ctx.jar,
|
|
@@ -1135,7 +1360,6 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
1135
1360
|
lsd: ctx.lsd || '',
|
|
1136
1361
|
};
|
|
1137
1362
|
|
|
1138
|
-
|
|
1139
1363
|
const receiveRes = await utils
|
|
1140
1364
|
.post(
|
|
1141
1365
|
receiveUrl.toString(),
|
|
@@ -1184,11 +1408,686 @@ export default function(defaultFuncs: any, api: any, ctx: any) {
|
|
|
1184
1408
|
return returnPromise;
|
|
1185
1409
|
}
|
|
1186
1410
|
|
|
1411
|
+
/**
|
|
1412
|
+
* React to a Facebook post (like, love, haha, wow, sad, angry).
|
|
1413
|
+
*
|
|
1414
|
+
* @param {string} feedbackId - The feedback ID. Can be:
|
|
1415
|
+
* - A raw numeric post ID (e.g. "1624250182241514")
|
|
1416
|
+
* - A base64-encoded feedback ID (e.g. "ZmVlZGJhY2s6MTYyN...")
|
|
1417
|
+
* - The literal string "feedback:<postID>"
|
|
1418
|
+
* @param {string} [reaction="LIKE"] - Reaction type:
|
|
1419
|
+
* "LIKE" | "LOVE" | "HAHA" | "WOW" | "SAD" | "ANGRY" | "CARE"
|
|
1420
|
+
* or equivalent emojis: "👍" | "❤️" | "😂" | "😮" | "😢" | "😡" | "🤗"
|
|
1421
|
+
* @param {Function} [callback] - Optional callback function.
|
|
1422
|
+
* @returns {Promise<object>} The server response.
|
|
1423
|
+
*/
|
|
1424
|
+
async function reactToPost(
|
|
1425
|
+
feedbackId: string,
|
|
1426
|
+
reaction: string = 'LIKE',
|
|
1427
|
+
callback?: Function,
|
|
1428
|
+
) {
|
|
1429
|
+
try {
|
|
1430
|
+
if (!feedbackId) {
|
|
1431
|
+
throw new Error('feedbackId is required.');
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// ── Reaction ID map ────────────────────────────────────────────────
|
|
1435
|
+
// IDs sourced from live Facebook reaction metadata.
|
|
1436
|
+
// Only non-deprecated reactions are listed as primary keys.
|
|
1437
|
+
// Deprecated reactions kept for backward-compat.
|
|
1438
|
+
const REACTION_IDS: Record<string, string> = {
|
|
1439
|
+
// ── Active reactions ─────────────────────────────────────────────
|
|
1440
|
+
LIKE: '1635855486666999', // Thích (type:1) #0866FF
|
|
1441
|
+
LOVE: '1678524932434102', // Yêu thích (type:2) #f33e58
|
|
1442
|
+
WOW: '478547315650144', // Wow (type:3) #f7b125
|
|
1443
|
+
HAHA: '115940658764963', // Haha (type:4) #f7b125
|
|
1444
|
+
SAD: '908563459236466', // Buồn (type:7) #f7b125
|
|
1445
|
+
ANGRY: '444813342392137', // Phẫn nộ (type:8) #e9710f
|
|
1446
|
+
CARE: '613557422527858', // Thương thương (type:16) #f7b125
|
|
1447
|
+
SUPPORT: '613557422527858', // alias CARE
|
|
1448
|
+
DOROTHY: '1663186627268800', // Biết ơn (type:11) #7e64c4
|
|
1449
|
+
TOTO: '899779720071651', // Tự hào (type:12) #ec7ebd
|
|
1450
|
+
// ── Deprecated (kept for backward-compat) ────────────────────────
|
|
1451
|
+
YAY: '1667835766830853', // Chúc mừng (type:5 - deprecated)
|
|
1452
|
+
CONFUSED: '1536130110011063', // Khó hiểu (type:10 - deprecated)
|
|
1453
|
+
SELFIE: '869508936487422', // Ảnh selfie (type:13 - deprecated)
|
|
1454
|
+
FLAME: '938644726258608', // Bày tỏ (flame, type:14 - deprecated)
|
|
1455
|
+
PLANE: '1609920819308489', // Bày tỏ (plane, type:15 - deprecated)
|
|
1456
|
+
// ── Emoji aliases ────────────────────────────────────────────────
|
|
1457
|
+
'👍': '1635855486666999', // LIKE
|
|
1458
|
+
'❤️': '1678524932434102', // LOVE
|
|
1459
|
+
'😮': '478547315650144', // WOW
|
|
1460
|
+
'😂': '115940658764963', // HAHA
|
|
1461
|
+
'😢': '908563459236466', // SAD
|
|
1462
|
+
'😡': '444813342392137', // ANGRY
|
|
1463
|
+
'🤗': '613557422527858', // CARE/Support
|
|
1464
|
+
// ── Vietnamese name aliases ──────────────────────────────────────
|
|
1465
|
+
'THÍCH': '1635855486666999',
|
|
1466
|
+
'YÊU THÍCH': '1678524932434102',
|
|
1467
|
+
'BUỒN': '908563459236466',
|
|
1468
|
+
'PHẪN NỘ': '444813342392137',
|
|
1469
|
+
'THƯƠNG THƯƠNG': '613557422527858',
|
|
1470
|
+
'BIẾT ƠN': '1663186627268800',
|
|
1471
|
+
'TỰ HÀO': '899779720071651',
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
const reactionKey = reaction.toUpperCase
|
|
1475
|
+
? reaction.toUpperCase()
|
|
1476
|
+
: reaction;
|
|
1477
|
+
const reactionId = REACTION_IDS[reactionKey] || REACTION_IDS[reaction];
|
|
1478
|
+
if (!reactionId) {
|
|
1479
|
+
throw new Error(
|
|
1480
|
+
`Unknown reaction: "${reaction}". Valid values: LIKE, LOVE, WOW, HAHA, SAD, ANGRY, CARE, SUPPORT, DOROTHY, TOTO`,
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// ── Normalise feedbackId → base64 "feedback:<postID>" format ───────
|
|
1485
|
+
let encodedFeedbackId = feedbackId;
|
|
1486
|
+
|
|
1487
|
+
// Plain numeric post ID → encode
|
|
1488
|
+
if (/^\d+$/.test(feedbackId)) {
|
|
1489
|
+
encodedFeedbackId = Buffer.from(`feedback:${feedbackId}`).toString(
|
|
1490
|
+
'base64',
|
|
1491
|
+
);
|
|
1492
|
+
} else {
|
|
1493
|
+
// Try to detect if it's already a valid base64 string by decoding
|
|
1494
|
+
try {
|
|
1495
|
+
const decoded = Buffer.from(feedbackId, 'base64').toString('utf-8');
|
|
1496
|
+
if (!decoded.startsWith('feedback:')) {
|
|
1497
|
+
// Not yet a feedback base64 – wrap and encode
|
|
1498
|
+
const raw = feedbackId.startsWith('feedback:')
|
|
1499
|
+
? feedbackId
|
|
1500
|
+
: `feedback:${feedbackId}`;
|
|
1501
|
+
encodedFeedbackId = Buffer.from(raw).toString('base64');
|
|
1502
|
+
}
|
|
1503
|
+
// else already properly encoded – keep as-is
|
|
1504
|
+
} catch (_) {
|
|
1505
|
+
// keep original value
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// ── Build session IDs ──────────────────────────────────────────────
|
|
1510
|
+
const crypto = require('node:crypto');
|
|
1511
|
+
const sessionId = crypto.randomUUID
|
|
1512
|
+
? crypto.randomUUID()
|
|
1513
|
+
: [
|
|
1514
|
+
crypto.randomBytes(4).toString('hex'),
|
|
1515
|
+
crypto.randomBytes(2).toString('hex'),
|
|
1516
|
+
crypto.randomBytes(2).toString('hex'),
|
|
1517
|
+
crypto.randomBytes(2).toString('hex'),
|
|
1518
|
+
crypto.randomBytes(6).toString('hex'),
|
|
1519
|
+
].join('-');
|
|
1520
|
+
const downstreamSessionId = crypto.randomUUID
|
|
1521
|
+
? crypto.randomUUID()
|
|
1522
|
+
: [
|
|
1523
|
+
crypto.randomBytes(4).toString('hex'),
|
|
1524
|
+
crypto.randomBytes(2).toString('hex'),
|
|
1525
|
+
crypto.randomBytes(2).toString('hex'),
|
|
1526
|
+
crypto.randomBytes(2).toString('hex'),
|
|
1527
|
+
crypto.randomBytes(6).toString('hex'),
|
|
1528
|
+
].join('-');
|
|
1529
|
+
|
|
1530
|
+
// ── Variables payload (mirrors CometUFIFeedbackReactMutation) ──────
|
|
1531
|
+
const variables = {
|
|
1532
|
+
input: {
|
|
1533
|
+
// attribution_id_v2 identifies the React component tree that triggered
|
|
1534
|
+
// the reaction (format: "<Component>,<surface>,<trigger>,<ts>,<seq>,<groupID>,,").
|
|
1535
|
+
// Pass ctx.attribution_id_v2 when you have it from a page scrape.
|
|
1536
|
+
attribution_id_v2: ctx.attribution_id_v2 || '',
|
|
1537
|
+
feedback_id: encodedFeedbackId,
|
|
1538
|
+
feedback_reaction_id: reactionId,
|
|
1539
|
+
feedback_source: 'OBJECT',
|
|
1540
|
+
is_tracking_encrypted: true,
|
|
1541
|
+
// tracking is an encrypted server-generated token array.
|
|
1542
|
+
// Carry it through ctx when available; fall back to [].
|
|
1543
|
+
tracking:
|
|
1544
|
+
Array.isArray(ctx.tracking) && ctx.tracking.length
|
|
1545
|
+
? ctx.tracking
|
|
1546
|
+
: [],
|
|
1547
|
+
session_id: sessionId,
|
|
1548
|
+
downstream_share_session_id: downstreamSessionId,
|
|
1549
|
+
// The page URL where the reaction was performed
|
|
1550
|
+
downstream_share_session_origin_uri:
|
|
1551
|
+
ctx.currentUrl || 'https://www.facebook.com/',
|
|
1552
|
+
downstream_share_session_start_time: String(Date.now()),
|
|
1553
|
+
actor_id: ctx.userID,
|
|
1554
|
+
client_mutation_id: String(Math.floor(Math.random() * 10)),
|
|
1555
|
+
},
|
|
1556
|
+
useDefaultActor: false,
|
|
1557
|
+
__relay_internal__pv__CometUFIReactionsEnableShortNamerelayprovider: false,
|
|
1558
|
+
};
|
|
1559
|
+
|
|
1560
|
+
// ── POST to /api/graphql/ ──────────────────────────────────────────
|
|
1561
|
+
// Includes every form field present in the original captured request.
|
|
1562
|
+
const lsd = ctx.lsd || ctx.fb_dtsg;
|
|
1563
|
+
const form = {
|
|
1564
|
+
av: ctx.userID,
|
|
1565
|
+
__aaid: '0',
|
|
1566
|
+
__user: ctx.userID,
|
|
1567
|
+
__a: '1',
|
|
1568
|
+
__req: Math.floor(Math.random() * 99).toString(36),
|
|
1569
|
+
// Opaque session-state tokens generated by the browser on every page
|
|
1570
|
+
// load. Carry them through ctx when available so they match the live
|
|
1571
|
+
// session; otherwise fall back to empty strings (FB tolerates it).
|
|
1572
|
+
__hs: ctx.hs || '20536.HCSV2:comet_pkg.2.1...0',
|
|
1573
|
+
dpr: '1',
|
|
1574
|
+
__ccg: 'EXCELLENT',
|
|
1575
|
+
__rev: ctx.revision || '',
|
|
1576
|
+
__s: ctx.__s || '',
|
|
1577
|
+
__hsi: ctx.__hsi || '',
|
|
1578
|
+
__dyn: ctx.__dyn || '',
|
|
1579
|
+
__csr: ctx.__csr || '',
|
|
1580
|
+
__hsdp: ctx.__hsdp || '',
|
|
1581
|
+
__hblp: ctx.__hblp || '',
|
|
1582
|
+
__sjsp: ctx.__sjsp || '',
|
|
1583
|
+
__comet_req: '15',
|
|
1584
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
1585
|
+
jazoest: ctx.jazoest,
|
|
1586
|
+
lsd,
|
|
1587
|
+
__spin_r: ctx.revision || '',
|
|
1588
|
+
__spin_b: 'trunk',
|
|
1589
|
+
__spin_t: Math.floor(Date.now() / 1000).toString(),
|
|
1590
|
+
// __crn is the Comet route name (page type). Use ctx value or a safe
|
|
1591
|
+
// default that works for both feed and group pages.
|
|
1592
|
+
__crn: ctx.__crn || 'comet.fbweb.CometHomeRoute',
|
|
1593
|
+
fb_api_caller_class: 'RelayModern',
|
|
1594
|
+
fb_api_req_friendly_name: 'CometUFIFeedbackReactMutation',
|
|
1595
|
+
server_timestamps: 'true',
|
|
1596
|
+
variables: JSON.stringify(variables),
|
|
1597
|
+
doc_id: '34430477113234631',
|
|
1598
|
+
};
|
|
1599
|
+
|
|
1600
|
+
const response = await defaultFuncs
|
|
1601
|
+
.post('https://www.facebook.com/api/graphql/', ctx.jar, form, {})
|
|
1602
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
1603
|
+
|
|
1604
|
+
if (response.errors) {
|
|
1605
|
+
throw new Error(JSON.stringify(response.errors));
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
const result = {
|
|
1609
|
+
success: true,
|
|
1610
|
+
reaction: reactionKey,
|
|
1611
|
+
reactionId,
|
|
1612
|
+
feedbackId: encodedFeedbackId,
|
|
1613
|
+
data:
|
|
1614
|
+
response?.data?.feedback_react ||
|
|
1615
|
+
response?.data?.story_act ||
|
|
1616
|
+
response?.data ||
|
|
1617
|
+
response,
|
|
1618
|
+
};
|
|
1619
|
+
|
|
1620
|
+
if (callback) {
|
|
1621
|
+
callback(null, result);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
return result;
|
|
1625
|
+
} catch (err) {
|
|
1626
|
+
utils.error('reactToPost', err);
|
|
1627
|
+
if (callback) {
|
|
1628
|
+
callback(err);
|
|
1629
|
+
}
|
|
1630
|
+
throw err;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// ── Helper: parse CometSinglePostDialogContentQuery response ───────────
|
|
1635
|
+
function _extractTrackingFromNode(node: any): string[] {
|
|
1636
|
+
if (Array.isArray(node.tracking)) return node.tracking.filter(Boolean);
|
|
1637
|
+
if (node.tracking && typeof node.tracking === 'string')
|
|
1638
|
+
return [node.tracking];
|
|
1639
|
+
return [];
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
function _extractFeedbackTarget(
|
|
1643
|
+
feedbackStory: any,
|
|
1644
|
+
): { feedback_id: string | null; post_url: string | null } {
|
|
1645
|
+
const ufiCtx =
|
|
1646
|
+
feedbackStory.story_ufi_container?.story?.feedback_context ??
|
|
1647
|
+
feedbackStory.feedback_context ??
|
|
1648
|
+
null;
|
|
1649
|
+
if (!ufiCtx) return { feedback_id: null, post_url: null };
|
|
1650
|
+
|
|
1651
|
+
const feedbackTarget =
|
|
1652
|
+
ufiCtx.feedback_target_with_context ??
|
|
1653
|
+
ufiCtx.ufi_renderer?.feedback ??
|
|
1654
|
+
null;
|
|
1655
|
+
|
|
1656
|
+
return {
|
|
1657
|
+
feedback_id: feedbackTarget?.id ?? null,
|
|
1658
|
+
post_url: feedbackTarget?.url ?? null,
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function _extractPostInfoFields(
|
|
1663
|
+
response: any,
|
|
1664
|
+
userID: string,
|
|
1665
|
+
): {
|
|
1666
|
+
tracking: string[];
|
|
1667
|
+
feedback_id: string | null;
|
|
1668
|
+
attribution_id_v2: string;
|
|
1669
|
+
post_url: string | null;
|
|
1670
|
+
} {
|
|
1671
|
+
try {
|
|
1672
|
+
const node = response?.data?.node ?? response?.data?.node_v2 ?? null;
|
|
1673
|
+
if (!node)
|
|
1674
|
+
return {
|
|
1675
|
+
tracking: [],
|
|
1676
|
+
feedback_id: null,
|
|
1677
|
+
attribution_id_v2: '',
|
|
1678
|
+
post_url: null,
|
|
1679
|
+
};
|
|
1680
|
+
|
|
1681
|
+
let tracking = _extractTrackingFromNode(node);
|
|
1682
|
+
let feedback_id: string | null = null;
|
|
1683
|
+
let post_url: string | null = null;
|
|
1684
|
+
let attribution_id_v2 = '';
|
|
1685
|
+
|
|
1686
|
+
const feedbackStory =
|
|
1687
|
+
node.comet_sections?.feedback?.story ??
|
|
1688
|
+
node.comet_sections?.content?.story ??
|
|
1689
|
+
null;
|
|
1690
|
+
|
|
1691
|
+
if (feedbackStory) {
|
|
1692
|
+
// Prefer tracking from UFI story (more specific)
|
|
1693
|
+
const storyTracking = _extractTrackingFromNode(feedbackStory);
|
|
1694
|
+
if (storyTracking.length > 0) tracking = storyTracking;
|
|
1695
|
+
|
|
1696
|
+
post_url = feedbackStory.permalink ?? feedbackStory.url ?? null;
|
|
1697
|
+
|
|
1698
|
+
// Build attribution_id_v2 matching live browser format
|
|
1699
|
+
const ts = Date.now();
|
|
1700
|
+
const seq = Math.floor(Math.random() * 999999);
|
|
1701
|
+
attribution_id_v2 =
|
|
1702
|
+
`CometSinglePostDialogRoot.react,comet.post.single_dialog,unexpected,${ts},${seq},,,;` +
|
|
1703
|
+
`CometHomeRoot.react,comet.home,logo,${ts - 5000},${seq +
|
|
1704
|
+
1},${userID},229#230#301,`;
|
|
1705
|
+
|
|
1706
|
+
const target = _extractFeedbackTarget(feedbackStory);
|
|
1707
|
+
feedback_id = target.feedback_id;
|
|
1708
|
+
if (target.post_url) post_url = target.post_url;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// Fallback: deep path from node directly
|
|
1712
|
+
if (!feedback_id) {
|
|
1713
|
+
const fb =
|
|
1714
|
+
node.comet_sections?.feedback?.story?.story_ufi_container?.story
|
|
1715
|
+
?.feedback_context?.feedback_target_with_context ?? null;
|
|
1716
|
+
if (fb?.id) feedback_id = fb.id;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
return { tracking, feedback_id, attribution_id_v2, post_url };
|
|
1720
|
+
} catch {
|
|
1721
|
+
return {
|
|
1722
|
+
tracking: [],
|
|
1723
|
+
feedback_id: null,
|
|
1724
|
+
attribution_id_v2: '',
|
|
1725
|
+
post_url: null,
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/**
|
|
1731
|
+
|
|
1732
|
+
* Fetch post metadata using CometSinglePostDialogContentQuery.
|
|
1733
|
+
* Returns the `tracking` token array, `feedback_id`, and `attribution_id_v2`
|
|
1734
|
+
* required for accurate reactions via reactToPost().
|
|
1735
|
+
*
|
|
1736
|
+
* @param {string} storyID - The story node ID (base64-encoded, e.g. "UzpfS...").
|
|
1737
|
+
* Can also be a raw numeric post ID — will be auto-encoded.
|
|
1738
|
+
* @param {Function} [callback] - Optional callback function.
|
|
1739
|
+
* @returns {Promise<object>} Object with tracking, feedback_id, attribution_id_v2, and raw data.
|
|
1740
|
+
*
|
|
1741
|
+
* @example
|
|
1742
|
+
* // Get tracking token then react
|
|
1743
|
+
* const info = await api.create.getPostInfo('1510599910435387');
|
|
1744
|
+
* await api.create.reactPost(info.feedback_id, 'LIKE');
|
|
1745
|
+
*/
|
|
1746
|
+
async function getPostInfo(
|
|
1747
|
+
storyID: string,
|
|
1748
|
+
callback?: Function,
|
|
1749
|
+
): Promise<any> {
|
|
1750
|
+
let _resolve: (v: any) => void;
|
|
1751
|
+
let _reject: (e: any) => void;
|
|
1752
|
+
|
|
1753
|
+
const returnPromise = new Promise<any>((res, rej) => {
|
|
1754
|
+
_resolve = res;
|
|
1755
|
+
_reject = rej;
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
const done: Function =
|
|
1759
|
+
callback ??
|
|
1760
|
+
((err: any, data: any) => {
|
|
1761
|
+
if (err) return _reject(err);
|
|
1762
|
+
_resolve(data);
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
try {
|
|
1766
|
+
if (!storyID) {
|
|
1767
|
+
throw new Error('storyID is required.');
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// ── Encode storyID if it's a raw numeric post ID ──────────────────
|
|
1771
|
+
// Facebook storyID format: base64("S:<ownerID>:<postID>:<postID>") or
|
|
1772
|
+
// base64("UzpfS<ownerID>:<postID>:<postID>")
|
|
1773
|
+
// If user passes a plain numeric ID, encode it as feedback ID format
|
|
1774
|
+
// and let the server return the full story node.
|
|
1775
|
+
// Usually users pass the post numeric ID (story_fbid), so we build
|
|
1776
|
+
// the proper base64 story node ID: "S:f<userID>:<postID>:<postID>"
|
|
1777
|
+
let encodedStoryID = storyID;
|
|
1778
|
+
if (/^\d+$/.test(storyID)) {
|
|
1779
|
+
// Raw numeric post ID → build story node ID: S:f<actorID>:<postID>:<postID>
|
|
1780
|
+
const nodeString = `S:f${ctx.userID}:${storyID}:${storyID}`;
|
|
1781
|
+
const b64 = Buffer.from(nodeString)
|
|
1782
|
+
.toString('base64')
|
|
1783
|
+
.replaceAll('+', '-')
|
|
1784
|
+
.replaceAll('/', '_')
|
|
1785
|
+
.replaceAll('=', '');
|
|
1786
|
+
const pad = b64.length % 4;
|
|
1787
|
+
encodedStoryID = pad ? b64 + '='.repeat(4 - pad) : b64;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// ── Variables matching CometSinglePostDialogContentQuery ──────────
|
|
1791
|
+
const variables = {
|
|
1792
|
+
feedbackSource: 2, // POST_PERMALINK_DIALOG
|
|
1793
|
+
feedLocation: 'POST_PERMALINK_DIALOG',
|
|
1794
|
+
focusCommentID: null,
|
|
1795
|
+
privacySelectorRenderLocation: 'COMET_STREAM',
|
|
1796
|
+
renderLocation: 'permalink',
|
|
1797
|
+
scale: 1,
|
|
1798
|
+
shouldChangeNodeFieldName: true,
|
|
1799
|
+
storyID: encodedStoryID,
|
|
1800
|
+
useDefaultActor: false,
|
|
1801
|
+
// Relay provider flags (copy from live request)
|
|
1802
|
+
__relay_internal__pv__GHLShouldChangeAdIdFieldNamerelayprovider: true,
|
|
1803
|
+
__relay_internal__pv__GHLShouldChangeSponsoredDataFieldNamerelayprovider: true,
|
|
1804
|
+
__relay_internal__pv__CometFeedStory_enable_post_permalink_white_space_clickrelayprovider: false,
|
|
1805
|
+
__relay_internal__pv__CometUFICommentActionLinksRewriteEnabledrelayprovider: false,
|
|
1806
|
+
__relay_internal__pv__CometUFICommentAvatarStickerAnimatedImagerelayprovider: false,
|
|
1807
|
+
__relay_internal__pv__IsWorkUserrelayprovider: false,
|
|
1808
|
+
__relay_internal__pv__TestPilotShouldIncludeDemoAdUseCaserelayprovider: false,
|
|
1809
|
+
__relay_internal__pv__FBReels_deprecate_short_form_video_context_gkrelayprovider: true,
|
|
1810
|
+
__relay_internal__pv__FBReels_enable_view_dubbed_audio_type_gkrelayprovider: true,
|
|
1811
|
+
__relay_internal__pv__CometImmersivePhotoCanUserDisable3DMotionrelayprovider: false,
|
|
1812
|
+
__relay_internal__pv__WorkCometIsEmployeeGKProviderrelayprovider: false,
|
|
1813
|
+
__relay_internal__pv__IsMergQAPollsrelayprovider: false,
|
|
1814
|
+
__relay_internal__pv__FBReelsMediaFooter_comet_enable_reels_ads_gkrelayprovider: true,
|
|
1815
|
+
__relay_internal__pv__CometUFIReactionsEnableShortNamerelayprovider: false,
|
|
1816
|
+
__relay_internal__pv__CometUFICommentAutoTranslationTyperelayprovider:
|
|
1817
|
+
'ORIGINAL',
|
|
1818
|
+
__relay_internal__pv__CometUFIShareActionMigrationrelayprovider: true,
|
|
1819
|
+
__relay_internal__pv__CometUFISingleLineUFIrelayprovider: false,
|
|
1820
|
+
__relay_internal__pv__CometUFI_dedicated_comment_routable_dialog_gkrelayprovider: true,
|
|
1821
|
+
__relay_internal__pv__FBReelsIFUTileContent_reelsIFUPlayOnHoverrelayprovider: true,
|
|
1822
|
+
__relay_internal__pv__GroupsCometGYSJFeedItemHeightrelayprovider: 150,
|
|
1823
|
+
__relay_internal__pv__ShouldEnableBakedInTextStoriesrelayprovider: false,
|
|
1824
|
+
__relay_internal__pv__StoriesShouldIncludeFbNotesrelayprovider: false,
|
|
1825
|
+
};
|
|
1826
|
+
|
|
1827
|
+
const lsd = ctx.lsd || ctx.fb_dtsg;
|
|
1828
|
+
const form = {
|
|
1829
|
+
av: ctx.userID,
|
|
1830
|
+
__aaid: '0',
|
|
1831
|
+
__user: ctx.userID,
|
|
1832
|
+
__a: '1',
|
|
1833
|
+
__req: Math.floor(Math.random() * 99).toString(36),
|
|
1834
|
+
__hs: ctx.hs || '20536.HCSV2:comet_pkg.2.1...0',
|
|
1835
|
+
dpr: '1',
|
|
1836
|
+
__ccg: 'EXCELLENT',
|
|
1837
|
+
__rev: ctx.revision || '',
|
|
1838
|
+
__s: ctx.__s || '',
|
|
1839
|
+
__hsi: ctx.__hsi || '',
|
|
1840
|
+
__dyn: ctx.__dyn || '',
|
|
1841
|
+
__csr: ctx.__csr || '',
|
|
1842
|
+
__hsdp: ctx.__hsdp || '',
|
|
1843
|
+
__hblp: ctx.__hblp || '',
|
|
1844
|
+
__sjsp: ctx.__sjsp || '',
|
|
1845
|
+
__comet_req: '15',
|
|
1846
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
1847
|
+
jazoest: ctx.jazoest,
|
|
1848
|
+
lsd,
|
|
1849
|
+
__spin_r: ctx.revision || '',
|
|
1850
|
+
__spin_b: 'trunk',
|
|
1851
|
+
__spin_t: Math.floor(Date.now() / 1000).toString(),
|
|
1852
|
+
__crn: ctx.__crn || 'comet.fbweb.CometHomeRoute',
|
|
1853
|
+
fb_api_caller_class: 'RelayModern',
|
|
1854
|
+
fb_api_req_friendly_name: 'CometSinglePostDialogContentQuery',
|
|
1855
|
+
server_timestamps: 'true',
|
|
1856
|
+
variables: JSON.stringify(variables),
|
|
1857
|
+
doc_id: '26187019677574345',
|
|
1858
|
+
};
|
|
1859
|
+
|
|
1860
|
+
const response = await defaultFuncs
|
|
1861
|
+
.post('https://www.facebook.com/api/graphql/', ctx.jar, form, {})
|
|
1862
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
1863
|
+
|
|
1864
|
+
if (response.errors) {
|
|
1865
|
+
throw new Error(JSON.stringify(response.errors));
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// ── Extract tracking, feedback_id, attribution_id_v2 from response ─
|
|
1869
|
+
// The response structure for CometSinglePostDialogContentQuery:
|
|
1870
|
+
// data.node → story node
|
|
1871
|
+
// .comet_sections.feedback.story → UFI (Unified Feedback Interface)
|
|
1872
|
+
// .story_ufi_container.story.feedback_context
|
|
1873
|
+
// .feedback_target_with_context
|
|
1874
|
+
// .id → feedback_id (base64)
|
|
1875
|
+
// .url → post URL
|
|
1876
|
+
// .tracking → encrypted tracking token array
|
|
1877
|
+
// .id → story graphql node ID
|
|
1878
|
+
|
|
1879
|
+
const {
|
|
1880
|
+
tracking,
|
|
1881
|
+
feedback_id,
|
|
1882
|
+
attribution_id_v2,
|
|
1883
|
+
post_url,
|
|
1884
|
+
} = _extractPostInfoFields(response, ctx.userID);
|
|
1885
|
+
|
|
1886
|
+
const result = {
|
|
1887
|
+
success: true,
|
|
1888
|
+
storyID: encodedStoryID,
|
|
1889
|
+
feedback_id,
|
|
1890
|
+
tracking,
|
|
1891
|
+
attribution_id_v2,
|
|
1892
|
+
post_url,
|
|
1893
|
+
data: response,
|
|
1894
|
+
};
|
|
1895
|
+
|
|
1896
|
+
done(null, result);
|
|
1897
|
+
} catch (err) {
|
|
1898
|
+
utils.error('getPostInfo', err);
|
|
1899
|
+
done(err);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
return returnPromise;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* React to a post with full tracking accuracy.
|
|
1907
|
+
*
|
|
1908
|
+
* Combines `getPostInfo` + `reactToPost` in one call:
|
|
1909
|
+
* 1. Fetches real `tracking` tokens and `feedback_id` from Facebook via
|
|
1910
|
+
* `CometSinglePostDialogContentQuery`.
|
|
1911
|
+
* 2. Patches those values into `ctx` so `reactToPost` sends them correctly.
|
|
1912
|
+
* 3. Falls back gracefully if `getPostInfo` fails (uses original IDs).
|
|
1913
|
+
*
|
|
1914
|
+
* @param {string} storyID - Raw numeric post ID **or** base64 storyID.
|
|
1915
|
+
* Examples: `'1510599910435387'` or `'UzpfSTEwMDA0...'`
|
|
1916
|
+
* @param {string} [reaction='LIKE'] - Reaction type.
|
|
1917
|
+
* Valid: `'LIKE'|'LOVE'|'HAHA'|'WOW'|'SAD'|'ANGRY'|'CARE'`
|
|
1918
|
+
* or emoji aliases: `'👍'|'❤️'|'😂'|'😮'|'😢'|'😡'|'🤗'`
|
|
1919
|
+
* @param {Function} [callback] - Optional node-style callback `(err, result)`.
|
|
1920
|
+
* @returns {Promise<object>} Same shape as `reactToPost()`.
|
|
1921
|
+
*
|
|
1922
|
+
* @example
|
|
1923
|
+
* // Easiest usage — just pass the numeric post ID
|
|
1924
|
+
* const result = await api.post.reactWithInfo('1510599910435387', 'LOVE');
|
|
1925
|
+
*
|
|
1926
|
+
* // With callback
|
|
1927
|
+
* api.post.reactWithInfo('1510599910435387', 'HAHA', (err, res) => {
|
|
1928
|
+
* if (err) console.error(err);
|
|
1929
|
+
* else console.log('Reacted!', res);
|
|
1930
|
+
* });
|
|
1931
|
+
*/
|
|
1932
|
+
async function reactToPostWithTracking(
|
|
1933
|
+
storyID: string,
|
|
1934
|
+
reaction: string = 'LIKE',
|
|
1935
|
+
callback?: Function,
|
|
1936
|
+
): Promise<any> {
|
|
1937
|
+
try {
|
|
1938
|
+
if (!storyID) throw new Error('storyID is required.');
|
|
1939
|
+
|
|
1940
|
+
// ── Step 1: fetch tracking info from Facebook ──────────────────────
|
|
1941
|
+
let feedbackId = storyID; // fallback: use storyID as-is
|
|
1942
|
+
let infoErr: any = null;
|
|
1943
|
+
|
|
1944
|
+
try {
|
|
1945
|
+
const info = await getPostInfo(storyID);
|
|
1946
|
+
|
|
1947
|
+
// Patch tracking + attribution_id_v2 into ctx so reactToPost uses them
|
|
1948
|
+
if (info.tracking?.length) ctx.tracking = info.tracking;
|
|
1949
|
+
if (info.attribution_id_v2)
|
|
1950
|
+
ctx.attribution_id_v2 = info.attribution_id_v2;
|
|
1951
|
+
if (info.post_url) ctx.currentUrl = info.post_url;
|
|
1952
|
+
|
|
1953
|
+
// Prefer server-provided feedback_id; fall back to storyID
|
|
1954
|
+
if (info.feedback_id) feedbackId = info.feedback_id;
|
|
1955
|
+
} catch (err) {
|
|
1956
|
+
infoErr = err;
|
|
1957
|
+
utils.warn(
|
|
1958
|
+
'reactToPostWithTracking',
|
|
1959
|
+
`getPostInfo failed (falling back to storyID as feedbackId): ${err}`,
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// ── Step 2: react using the enriched ctx ───────────────────────────
|
|
1964
|
+
const result = await reactToPost(feedbackId, reaction);
|
|
1965
|
+
|
|
1966
|
+
// Attach info-fetch error to result so callers can inspect it
|
|
1967
|
+
if (infoErr) (result as any).infoError = String(infoErr);
|
|
1968
|
+
|
|
1969
|
+
if (callback) callback(null, result);
|
|
1970
|
+
return result;
|
|
1971
|
+
} catch (err) {
|
|
1972
|
+
utils.error('reactToPostWithTracking', err);
|
|
1973
|
+
if (callback) callback(err);
|
|
1974
|
+
throw err;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// ── pfbid (Permalink-Friendly Base ID) ───────────────────────────────────
|
|
1979
|
+
// pfbid is an opaque server-side token that Facebook generates.
|
|
1980
|
+
// It CANNOT be decoded client-side (it's internally hashed/encrypted).
|
|
1981
|
+
//
|
|
1982
|
+
// The correct workflow:
|
|
1983
|
+
// parsePostUrl(url) → extracts the pfbid token from the URL path
|
|
1984
|
+
// getPostInfo(token) → sends pfbid to Facebook's GraphQL; server resolves it
|
|
1985
|
+
// and returns the real numeric story_fbid in the response
|
|
1986
|
+
//
|
|
1987
|
+
// The returned storyID from parsePostUrl is either:
|
|
1988
|
+
// - A numeric string (e.g. "1510599910435387") → for /posts/{numericID} URLs
|
|
1989
|
+
// - A pfbid string (e.g. "pfbid0Yo6XDa...") → for /posts/pfbid{token} URLs
|
|
1990
|
+
//
|
|
1991
|
+
// Both work as input to getPostInfo / reactWithInfo.
|
|
1992
|
+
|
|
1993
|
+
/**
|
|
1994
|
+
* Parse any Facebook post URL into a storyID suitable for `getPostInfo`.
|
|
1995
|
+
*
|
|
1996
|
+
* Supports all common URL patterns:
|
|
1997
|
+
* - `https://www.facebook.com/{page}/posts/pfbid{token}` ← pfbid (new style)
|
|
1998
|
+
* - `https://www.facebook.com/{page}/posts/{numericID}` ← plain numeric
|
|
1999
|
+
* - `https://www.facebook.com/permalink.php?story_fbid={id}&id={page}`
|
|
2000
|
+
* - `https://www.facebook.com/photo?fbid={id}&...`
|
|
2001
|
+
* - `https://m.facebook.com/story.php?story_fbid={id}&...`
|
|
2002
|
+
*
|
|
2003
|
+
* @param {string} url - Any Facebook post URL.
|
|
2004
|
+
* @returns {{ storyID: string; pageSlug: string | null; format: 'pfbid'|'numeric'|'query' }}
|
|
2005
|
+
* `storyID` is either a numeric string or the full pfbid token (including "pfbid" prefix).
|
|
2006
|
+
* Pass it directly to `getPostInfo()` or `reactWithInfo()`.
|
|
2007
|
+
* @throws If no recognisable post ID can be extracted.
|
|
2008
|
+
*
|
|
2009
|
+
* @example
|
|
2010
|
+
* // pfbid URL → storyID is the pfbid token; pass it to getPostInfo which resolves it server-side
|
|
2011
|
+
* const { storyID } = parsePostUrl('https://www.facebook.com/tuyetcollection/posts/pfbid0Yo6XDaKKnFKpENu3g2c2to1zJe7jFoZNr2NqjiXRiHmwjsy8dwbE84y4n7syCSfMl');
|
|
2012
|
+
* // storyID = 'pfbid0Yo6XDaKKnFKpENu3g2c2to1zJe7jFoZNr2NqjiXRiHmwjsy8dwbE84y4n7syCSfMl'
|
|
2013
|
+
* const info = await api.post.getPostInfo(storyID); // Facebook resolves it → real ID in response
|
|
2014
|
+
* await api.post.reactToPost(info.feedback_id, 'LOVE');
|
|
2015
|
+
*
|
|
2016
|
+
* // Easiest: reactWithInfo handles all of this automatically
|
|
2017
|
+
* await api.post.reactWithInfo(storyID, 'LOVE');
|
|
2018
|
+
*
|
|
2019
|
+
* // Numeric URL → storyID is already the numeric ID
|
|
2020
|
+
* const { storyID } = parsePostUrl('https://www.facebook.com/tuyetcollection/posts/1510599910435387');
|
|
2021
|
+
* // storyID = '1510599910435387'
|
|
2022
|
+
*/
|
|
2023
|
+
function parsePostUrl(
|
|
2024
|
+
url: string,
|
|
2025
|
+
): { storyID: string; pageSlug: string | null; format: string } {
|
|
2026
|
+
let parsed: URL;
|
|
2027
|
+
try {
|
|
2028
|
+
parsed = new URL(url);
|
|
2029
|
+
} catch {
|
|
2030
|
+
throw new Error(`parsePostUrl: invalid URL "${url}"`);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
const path = parsed.pathname; // e.g. /tuyetcollection/posts/pfbid0Yo6...
|
|
2034
|
+
const segments = path.split('/').filter(Boolean);
|
|
2035
|
+
|
|
2036
|
+
// ── Pattern 1: /{slug}/posts/{id_or_pfbid} ───────────────────────────
|
|
2037
|
+
const postIdx = segments.indexOf('posts');
|
|
2038
|
+
if (postIdx !== -1 && segments[postIdx + 1]) {
|
|
2039
|
+
const token = segments[postIdx + 1];
|
|
2040
|
+
const pageSlug = postIdx > 0 ? segments[postIdx - 1] : null;
|
|
2041
|
+
|
|
2042
|
+
if (token.startsWith('pfbid')) {
|
|
2043
|
+
// pfbid is an opaque server token — cannot be decoded client-side.
|
|
2044
|
+
// Return the full pfbid as storyID; Facebook GraphQL resolves it server-side.
|
|
2045
|
+
return { storyID: token, pageSlug, format: 'pfbid' };
|
|
2046
|
+
}
|
|
2047
|
+
if (/^\d+$/.test(token)) {
|
|
2048
|
+
return { storyID: token, pageSlug, format: 'numeric' };
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// ── Pattern 2: /photo/{numericID} or /video/{numericID} ──────────────
|
|
2053
|
+
for (const seg of ['photo', 'video', 'reel']) {
|
|
2054
|
+
const idx = segments.indexOf(seg);
|
|
2055
|
+
if (idx !== -1 && segments[idx + 1] && /^\d+$/.test(segments[idx + 1])) {
|
|
2056
|
+
return {
|
|
2057
|
+
storyID: segments[idx + 1],
|
|
2058
|
+
pageSlug: null,
|
|
2059
|
+
format: 'numeric',
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// ── Pattern 3: query params ───────────────────────────────────────────
|
|
2065
|
+
// permalink.php?story_fbid=&id= | photo?fbid= | story.php?story_fbid=
|
|
2066
|
+
const storyFbid = parsed.searchParams.get('story_fbid');
|
|
2067
|
+
if (storyFbid && /^\d+$/.test(storyFbid)) {
|
|
2068
|
+
return { storyID: storyFbid, pageSlug: null, format: 'query' };
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
const fbid = parsed.searchParams.get('fbid');
|
|
2072
|
+
if (fbid && /^\d+$/.test(fbid)) {
|
|
2073
|
+
return { storyID: fbid, pageSlug: null, format: 'query' };
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
throw new Error(`parsePostUrl: could not extract post ID from "${url}"`);
|
|
2077
|
+
}
|
|
2078
|
+
|
|
1187
2079
|
return {
|
|
1188
2080
|
create: createPost,
|
|
1189
2081
|
createPost: createPost,
|
|
1190
2082
|
delete: deletePost,
|
|
1191
2083
|
getComments: getPostComments,
|
|
2084
|
+
getPostInfo: getPostInfo,
|
|
2085
|
+
getPostIDFromURL: getPostIDFromURL,
|
|
2086
|
+
parsePostUrl: parsePostUrl,
|
|
2087
|
+
react: reactToPost,
|
|
2088
|
+
reactToPost: reactToPost,
|
|
2089
|
+
reactWithInfo: reactToPostWithTracking,
|
|
2090
|
+
reactToPostWithTracking: reactToPostWithTracking,
|
|
1192
2091
|
uploadPhoto: uploadPhoto,
|
|
1193
2092
|
uploadVideo: uploadVideo,
|
|
1194
2093
|
};
|