fca-phantom 1.0.0
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/LICENSE +58 -0
- package/README.md +534 -0
- package/index.js +35 -0
- package/package.json +101 -0
- package/phantom/core/builder/bootstrap.js +334 -0
- package/phantom/core/builder/config.js +78 -0
- package/phantom/core/builder/forge.js +113 -0
- package/phantom/core/builder/ignite.js +386 -0
- package/phantom/core/builder/options.js +61 -0
- package/phantom/core/engine.js +71 -0
- package/phantom/core/reactor.js +2 -0
- package/phantom/datastore/appState.js +2 -0
- package/phantom/datastore/appStateBackup.js +34 -0
- package/phantom/datastore/models/cipher/e2ee.js +48 -0
- package/phantom/datastore/models/cipher/vault.js +153 -0
- package/phantom/datastore/models/index.js +3 -0
- package/phantom/datastore/models/matrix/auth.js +151 -0
- package/phantom/datastore/models/matrix/cache.js +3 -0
- package/phantom/datastore/models/matrix/checker.js +2 -0
- package/phantom/datastore/models/matrix/clients.js +2 -0
- package/phantom/datastore/models/matrix/constants.js +2 -0
- package/phantom/datastore/models/matrix/credentials.js +2 -0
- package/phantom/datastore/models/matrix/cycle.js +2 -0
- package/phantom/datastore/models/matrix/gate.js +282 -0
- package/phantom/datastore/models/matrix/ghost.js +332 -0
- package/phantom/datastore/models/matrix/headers.js +193 -0
- package/phantom/datastore/models/matrix/heartbeat.js +298 -0
- package/phantom/datastore/models/matrix/identity.js +235 -0
- package/phantom/datastore/models/matrix/logger.js +271 -0
- package/phantom/datastore/models/matrix/monitor.js +2 -0
- package/phantom/datastore/models/matrix/net.js +316 -0
- package/phantom/datastore/models/matrix/response.js +193 -0
- package/phantom/datastore/models/matrix/revive.js +255 -0
- package/phantom/datastore/models/matrix/signals.js +2 -0
- package/phantom/datastore/models/matrix/store.js +263 -0
- package/phantom/datastore/models/matrix/telemetry.js +272 -0
- package/phantom/datastore/models/matrix/tools.js +93 -0
- package/phantom/datastore/models/matrix/transform/cookieParser.js +2 -0
- package/phantom/datastore/models/matrix/transform/cookies.js +114 -0
- package/phantom/datastore/models/matrix/transform/index.js +203 -0
- package/phantom/datastore/models/matrix/validator.js +157 -0
- package/phantom/datastore/models/types/index.d.ts +498 -0
- package/phantom/datastore/schema.js +167 -0
- package/phantom/datastore/session.js +129 -0
- package/phantom/datastore/threads.js +22 -0
- package/phantom/datastore/users.js +26 -0
- package/phantom/dispatch/addExternalModule.js +239 -0
- package/phantom/dispatch/addUserToGroup.js +161 -0
- package/phantom/dispatch/changeAdminStatus.js +142 -0
- package/phantom/dispatch/changeArchivedStatus.js +135 -0
- package/phantom/dispatch/changeAvatar.js +123 -0
- package/phantom/dispatch/changeBio.js +86 -0
- package/phantom/dispatch/changeBlockedStatus.js +86 -0
- package/phantom/dispatch/changeGroupImage.js +145 -0
- package/phantom/dispatch/changeThreadColor.js +172 -0
- package/phantom/dispatch/changeThreadEmoji.js +130 -0
- package/phantom/dispatch/comment.js +136 -0
- package/phantom/dispatch/createAITheme.js +333 -0
- package/phantom/dispatch/createNewGroup.js +99 -0
- package/phantom/dispatch/createPoll.js +148 -0
- package/phantom/dispatch/deleteMessage.js +131 -0
- package/phantom/dispatch/deleteThread.js +155 -0
- package/phantom/dispatch/e2ee.js +101 -0
- package/phantom/dispatch/editMessage.js +158 -0
- package/phantom/dispatch/emoji.js +143 -0
- package/phantom/dispatch/fetchThemeData.js +233 -0
- package/phantom/dispatch/follow.js +111 -0
- package/phantom/dispatch/forwardMessage.js +110 -0
- package/phantom/dispatch/friend.js +189 -0
- package/phantom/dispatch/gcmember.js +138 -0
- package/phantom/dispatch/gcname.js +131 -0
- package/phantom/dispatch/gcrule.js +111 -0
- package/phantom/dispatch/getAccess.js +109 -0
- package/phantom/dispatch/getBotInfo.js +81 -0
- package/phantom/dispatch/getBotInitialData.js +110 -0
- package/phantom/dispatch/getFriendsList.js +118 -0
- package/phantom/dispatch/getMessage.js +199 -0
- package/phantom/dispatch/getTheme.js +199 -0
- package/phantom/dispatch/getThemeInfo.js +160 -0
- package/phantom/dispatch/getThreadHistory.js +139 -0
- package/phantom/dispatch/getThreadInfo.js +153 -0
- package/phantom/dispatch/getThreadList.js +132 -0
- package/phantom/dispatch/getThreadPictures.js +93 -0
- package/phantom/dispatch/getUserID.js +147 -0
- package/phantom/dispatch/getUserInfo.js +513 -0
- package/phantom/dispatch/getUserInfoV2.js +146 -0
- package/phantom/dispatch/handleMessageRequest.js +50 -0
- package/phantom/dispatch/httpGet.js +63 -0
- package/phantom/dispatch/httpPost.js +89 -0
- package/phantom/dispatch/httpPostFormData.js +69 -0
- package/phantom/dispatch/listenMqtt.js +1236 -0
- package/phantom/dispatch/listenSpeed.js +179 -0
- package/phantom/dispatch/logout.js +93 -0
- package/phantom/dispatch/markAsDelivered.js +92 -0
- package/phantom/dispatch/markAsRead.js +119 -0
- package/phantom/dispatch/markAsReadAll.js +215 -0
- package/phantom/dispatch/markAsSeen.js +70 -0
- package/phantom/dispatch/mqttDeltaValue.js +278 -0
- package/phantom/dispatch/muteThread.js +253 -0
- package/phantom/dispatch/nickname.js +132 -0
- package/phantom/dispatch/notes.js +263 -0
- package/phantom/dispatch/pinMessage.js +238 -0
- package/phantom/dispatch/produceMetaTheme.js +335 -0
- package/phantom/dispatch/realtime.js +291 -0
- package/phantom/dispatch/removeUserFromGroup.js +248 -0
- package/phantom/dispatch/resolvePhotoUrl.js +217 -0
- package/phantom/dispatch/searchForThread.js +258 -0
- package/phantom/dispatch/sendMessage.js +354 -0
- package/phantom/dispatch/sendMessageMqtt.js +249 -0
- package/phantom/dispatch/sendTypingIndicator.js +206 -0
- package/phantom/dispatch/setMessageReaction.js +188 -0
- package/phantom/dispatch/setMessageReactionMqtt.js +248 -0
- package/phantom/dispatch/setThreadTheme.js +330 -0
- package/phantom/dispatch/setThreadThemeMqtt.js +207 -0
- package/phantom/dispatch/share.js +200 -0
- package/phantom/dispatch/shareContact.js +216 -0
- package/phantom/dispatch/stickers.js +395 -0
- package/phantom/dispatch/story.js +240 -0
- package/phantom/dispatch/theme.js +296 -0
- package/phantom/dispatch/unfriend.js +199 -0
- package/phantom/dispatch/unsendMessage.js +124 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
const { formatAttachment: _formatAttachment } = require('../datastore/models/matrix/transform/index');
|
|
5
|
+
|
|
6
|
+
const HISTORY_CACHE = new Map();
|
|
7
|
+
const CACHE_TTL = 90 * 1000;
|
|
8
|
+
const MAX_FETCH_PER_CALL = 1000;
|
|
9
|
+
|
|
10
|
+
async function retryOp(fn, retries = 3, base = 700) {
|
|
11
|
+
for (let i = 0; i < retries; i++) {
|
|
12
|
+
try { return await fn(); } catch (err) {
|
|
13
|
+
if (i === retries - 1) throw err;
|
|
14
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 300));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatAttachmentsGraphQLResponse(attachment) {
|
|
20
|
+
switch (attachment.__typename) {
|
|
21
|
+
case "MessageImage":
|
|
22
|
+
return { type: "photo", ID: attachment.legacy_attachment_id, filename: attachment.filename, thumbnailUrl: attachment.thumbnail?.uri, previewUrl: attachment.preview?.uri, previewWidth: attachment.preview?.width, previewHeight: attachment.preview?.height, largePreviewUrl: attachment.large_preview?.uri, largePreviewHeight: attachment.large_preview?.height, largePreviewWidth: attachment.large_preview?.width, url: attachment.large_preview?.uri, width: attachment.original_dimensions?.x, height: attachment.original_dimensions?.y, name: attachment.filename };
|
|
23
|
+
case "MessageAnimatedImage":
|
|
24
|
+
return { type: "animated_image", ID: attachment.legacy_attachment_id, filename: attachment.filename, previewUrl: attachment.preview_image?.uri, previewWidth: attachment.preview_image?.width, previewHeight: attachment.preview_image?.height, url: attachment.animated_image?.uri, width: attachment.animated_image?.width, height: attachment.animated_image?.height, name: attachment.filename };
|
|
25
|
+
case "MessageVideo":
|
|
26
|
+
return { type: "video", ID: attachment.legacy_attachment_id, filename: attachment.filename, duration: attachment.playable_duration_in_ms, thumbnailUrl: attachment.large_image?.uri, previewUrl: attachment.large_image?.uri, previewWidth: attachment.large_image?.width, previewHeight: attachment.large_image?.height, url: attachment.playable_url, width: attachment.original_dimensions?.x, height: attachment.original_dimensions?.y, videoType: attachment.video_type?.toLowerCase() };
|
|
27
|
+
case "MessageFile":
|
|
28
|
+
return { type: "file", ID: attachment.message_file_fbid, filename: attachment.filename, url: attachment.url, isMalicious: attachment.is_malicious, contentType: attachment.content_type, name: attachment.filename };
|
|
29
|
+
case "MessageAudio":
|
|
30
|
+
return { type: "audio", ID: attachment.url_shimhash, filename: attachment.filename, duration: attachment.playable_duration_in_ms, audioType: attachment.audio_type, url: attachment.playable_url, isVoiceMail: attachment.is_voicemail };
|
|
31
|
+
default:
|
|
32
|
+
return { type: "unknown", error: "Unknown attachment type " + attachment.__typename };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatExtensibleAttachment(attachment) {
|
|
37
|
+
if (attachment.story_attachment) {
|
|
38
|
+
const sa = attachment.story_attachment;
|
|
39
|
+
return { type: "share", ID: attachment.legacy_attachment_id, url: sa.url, title: sa.title_with_entities?.text, description: sa.description?.text, source: sa.source?.text, image: sa.media?.image?.uri, width: sa.media?.image?.width, height: sa.media?.image?.height, playable: sa.media?.is_playable, duration: sa.media?.playable_duration_in_ms, playableUrl: sa.media?.playable_url, subattachments: sa.subattachments, properties: (sa.properties || []).reduce((obj, cur) => { obj[cur.key] = cur.value?.text; return obj; }, {}) };
|
|
40
|
+
}
|
|
41
|
+
return { type: "unknown", error: "Unknown extensible attachment" };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatMessagesGraphQLResponse(data) {
|
|
45
|
+
const messageThread = data.o0?.data?.message_thread;
|
|
46
|
+
if (!messageThread) return [];
|
|
47
|
+
|
|
48
|
+
const threadID = messageThread.thread_key?.thread_fbid || messageThread.thread_key?.other_user_id;
|
|
49
|
+
const isGroup = messageThread.thread_type === "GROUP";
|
|
50
|
+
|
|
51
|
+
return messageThread.messages.nodes.map(d => {
|
|
52
|
+
switch (d.__typename) {
|
|
53
|
+
case "UserMessage": {
|
|
54
|
+
const mentions = {};
|
|
55
|
+
if (d.message?.ranges) {
|
|
56
|
+
for (const e of d.message.ranges) {
|
|
57
|
+
if (e.entity?.id && d.message.text) mentions[e.entity.id] = d.message.text.substring(e.offset, e.offset + e.length);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const attachments = d.sticker
|
|
61
|
+
? [{ type: "sticker", ID: d.sticker.id, url: d.sticker.url, packID: d.sticker.pack?.id, frameCount: d.sticker.frame_count, frameRate: d.sticker.frame_rate }]
|
|
62
|
+
: (d.blob_attachments || []).map(formatAttachmentsGraphQLResponse).concat(d.extensible_attachment ? [formatExtensibleAttachment(d.extensible_attachment)] : []);
|
|
63
|
+
|
|
64
|
+
return { type: "message", attachments, body: d.message?.text || "", isGroup, messageID: d.message_id, senderID: d.message_sender?.id, threadID, timestamp: d.timestamp_precise, mentions, isUnread: d.unread, isUnsent: !!d.is_unsent, messageReactions: (d.message_reactions || []).map(r => ({ reaction: r.reaction, userID: r.user?.id })) };
|
|
65
|
+
}
|
|
66
|
+
case "ThreadNameMessage":
|
|
67
|
+
case "ThreadImageMessage":
|
|
68
|
+
case "ParticipantLeftMessage":
|
|
69
|
+
case "ParticipantsAddedMessage":
|
|
70
|
+
case "GenericAdminTextMessage":
|
|
71
|
+
return { type: "event", messageID: d.message_id, threadID, isGroup, senderID: d.message_sender?.id, author: d.message_sender?.id, timestamp: d.timestamp_precise, snippet: d.snippet, logMessageType: utils.getAdminTextMessageType(d.extensible_message_admin_text_type || d.__typename), logMessageData: d.extensible_message_admin_text || {} };
|
|
72
|
+
default:
|
|
73
|
+
return { type: "unknown", error: "Unknown message type " + d.__typename, raw: d, messageID: d.message_id, threadID, timestamp: d.timestamp_precise };
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
79
|
+
return async function getThreadHistory(threadID, amount, timestamp, callback) {
|
|
80
|
+
let resolveFunc, rejectFunc;
|
|
81
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
82
|
+
resolveFunc = resolve;
|
|
83
|
+
rejectFunc = reject;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (typeof timestamp === "function") { callback = timestamp; timestamp = null; }
|
|
87
|
+
if (typeof callback !== "function") {
|
|
88
|
+
callback = (err, data) => {
|
|
89
|
+
if (err) return rejectFunc(err);
|
|
90
|
+
resolveFunc(data);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
if (!threadID) throw new Error("getThreadHistory: threadID is required");
|
|
96
|
+
if (!amount || typeof amount !== "number" || amount <= 0) throw new Error("getThreadHistory: amount must be a positive number");
|
|
97
|
+
|
|
98
|
+
amount = Math.min(amount, MAX_FETCH_PER_CALL);
|
|
99
|
+
|
|
100
|
+
const cacheKey = `hist_${threadID}_${amount}_${timestamp || "latest"}`;
|
|
101
|
+
const cached = HISTORY_CACHE.get(cacheKey);
|
|
102
|
+
if (cached && (Date.now() - cached.ts < CACHE_TTL)) return callback(null, cached.data);
|
|
103
|
+
|
|
104
|
+
const form = {
|
|
105
|
+
av: ctx.globalOptions?.pageID || ctx.userID,
|
|
106
|
+
queries: JSON.stringify({
|
|
107
|
+
o0: {
|
|
108
|
+
doc_id: "1498317363570230",
|
|
109
|
+
query_params: {
|
|
110
|
+
id: String(threadID),
|
|
111
|
+
message_limit: amount,
|
|
112
|
+
load_messages: 1,
|
|
113
|
+
load_read_receipts: false,
|
|
114
|
+
before: timestamp || null
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const resData = await retryOp(() =>
|
|
121
|
+
defaultFuncs.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
|
122
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (resData.error || (Array.isArray(resData) && resData[resData.length - 1].error_results !== 0)) throw resData;
|
|
126
|
+
if (!Array.isArray(resData) || !resData[0]?.o0?.data) throw { error: "getThreadHistory: malformed response" };
|
|
127
|
+
|
|
128
|
+
const messages = formatMessagesGraphQLResponse(resData[0]);
|
|
129
|
+
|
|
130
|
+
HISTORY_CACHE.set(cacheKey, { data: messages, ts: Date.now() });
|
|
131
|
+
callback(null, messages);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
utils.error("getThreadHistory", err);
|
|
134
|
+
callback(err);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return returnPromise;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
|
|
5
|
+
const THREAD_CACHE = new Map();
|
|
6
|
+
const CACHE_TTL = 3 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
async function retryOp(fn, retries = 3, base = 700) {
|
|
9
|
+
for (let i = 0; i < retries; i++) {
|
|
10
|
+
try { return await fn(); } catch (err) {
|
|
11
|
+
if (i === retries - 1) throw err;
|
|
12
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 300));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatEventReminders(reminder) {
|
|
18
|
+
return {
|
|
19
|
+
reminderID: reminder.id,
|
|
20
|
+
eventCreatorID: reminder.lightweight_event_creator?.id,
|
|
21
|
+
time: reminder.time,
|
|
22
|
+
eventType: reminder.lightweight_event_type?.toLowerCase(),
|
|
23
|
+
locationName: reminder.location_name,
|
|
24
|
+
eventStatus: reminder.lightweight_event_status?.toLowerCase(),
|
|
25
|
+
note: reminder.note,
|
|
26
|
+
repeatMode: reminder.repeat_mode?.toLowerCase(),
|
|
27
|
+
eventTitle: reminder.event_title,
|
|
28
|
+
members: (reminder.event_reminder_members?.edges || []).map(member => ({ memberID: member.node?.id, state: member.guest_list_state?.toLowerCase() }))
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatThreadGraphQLResponse(data) {
|
|
33
|
+
if (data.errors) {
|
|
34
|
+
const details = data.errors.map(e => e.message || e).join(", ");
|
|
35
|
+
throw Object.assign(new Error(`GraphQL error in getThreadInfo: ${details}`), { details, fullErrors: data.errors });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const messageThread = data.message_thread;
|
|
39
|
+
if (!messageThread) throw new Error("No message_thread in GraphQL response");
|
|
40
|
+
|
|
41
|
+
const threadID = messageThread.thread_key?.thread_fbid || messageThread.thread_key?.other_user_id;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
threadID,
|
|
45
|
+
threadName: messageThread.name,
|
|
46
|
+
participantIDs: messageThread.all_participants.edges.map(d => d.node.messaging_actor.id),
|
|
47
|
+
userInfo: messageThread.all_participants.edges.map(d => {
|
|
48
|
+
const p = d.node.messaging_actor;
|
|
49
|
+
return { id: p.id, name: p.name, firstName: p.short_name, vanity: p.username, url: p.url, thumbSrc: p.big_image_src?.uri, profileUrl: p.big_image_src?.uri, gender: p.gender, type: p.__typename, isFriend: p.is_viewer_friend, isBirthday: !!p.is_birthday };
|
|
50
|
+
}),
|
|
51
|
+
unreadCount: messageThread.unread_count,
|
|
52
|
+
messageCount: messageThread.messages_count,
|
|
53
|
+
timestamp: messageThread.updated_time_precise,
|
|
54
|
+
muteUntil: messageThread.mute_until,
|
|
55
|
+
isGroup: messageThread.thread_type === "GROUP",
|
|
56
|
+
isSubscribed: messageThread.is_viewer_subscribed,
|
|
57
|
+
isArchived: messageThread.has_viewer_archived,
|
|
58
|
+
folder: messageThread.folder,
|
|
59
|
+
cannotReplyReason: messageThread.cannot_reply_reason,
|
|
60
|
+
eventReminders: messageThread.event_reminders ? messageThread.event_reminders.nodes.map(formatEventReminders) : null,
|
|
61
|
+
emoji: messageThread.customization_info?.emoji || null,
|
|
62
|
+
color: messageThread.customization_info?.outgoing_bubble_color ? messageThread.customization_info.outgoing_bubble_color.slice(2) : null,
|
|
63
|
+
threadTheme: messageThread.thread_theme,
|
|
64
|
+
nicknames: (messageThread.customization_info?.participant_customizations || []).reduce((res, val) => { if (val.nickname) res[val.participant_id] = val.nickname; return res; }, {}),
|
|
65
|
+
adminIDs: messageThread.thread_admins || [],
|
|
66
|
+
approvalMode: Boolean(messageThread.approval_mode),
|
|
67
|
+
approvalQueue: (messageThread.group_approval_queue?.nodes || []).map(a => ({ inviterID: a.inviter?.id, requesterID: a.requester?.id, timestamp: a.request_timestamp, request_source: a.request_source })),
|
|
68
|
+
reactionsMuteMode: messageThread.reactions_mute_mode?.toLowerCase() || "all_reactions",
|
|
69
|
+
mentionsMuteMode: messageThread.mentions_mute_mode?.toLowerCase() || "all_mentions",
|
|
70
|
+
isPinProtected: messageThread.is_pin_protected,
|
|
71
|
+
relatedPageThread: messageThread.related_page_thread,
|
|
72
|
+
name: messageThread.name,
|
|
73
|
+
snippet: messageThread.last_message?.nodes?.[0]?.snippet || null,
|
|
74
|
+
snippetSender: messageThread.last_message?.nodes?.[0]?.message_sender?.messaging_actor?.id || null,
|
|
75
|
+
snippetAttachments: [],
|
|
76
|
+
serverTimestamp: messageThread.updated_time_precise,
|
|
77
|
+
imageSrc: messageThread.image?.uri || null,
|
|
78
|
+
isCanonicalUser: messageThread.is_canonical_neo_user,
|
|
79
|
+
isCanonical: messageThread.thread_type !== "GROUP",
|
|
80
|
+
recipientsLoadable: true,
|
|
81
|
+
hasEmailParticipant: false,
|
|
82
|
+
readOnly: false,
|
|
83
|
+
canReply: messageThread.cannot_reply_reason == null,
|
|
84
|
+
lastMessageTimestamp: messageThread.last_message?.timestamp_precise || null,
|
|
85
|
+
lastReadTimestamp: messageThread.last_read_receipt?.nodes?.[0]?.timestamp_precise || null,
|
|
86
|
+
threadType: messageThread.thread_type === "GROUP" ? 2 : 1,
|
|
87
|
+
inviteLink: { enable: messageThread.joinable_mode?.mode === 1, link: messageThread.joinable_mode?.link || null }
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
92
|
+
return async function getThreadInfo(threadID) {
|
|
93
|
+
const threadIDs = Array.isArray(threadID) ? threadID : [threadID];
|
|
94
|
+
|
|
95
|
+
if (threadIDs.length === 0) throw new Error("getThreadInfo: at least one threadID is required");
|
|
96
|
+
|
|
97
|
+
const cachedResults = {};
|
|
98
|
+
const toFetch = [];
|
|
99
|
+
|
|
100
|
+
for (const tid of threadIDs) {
|
|
101
|
+
const cached = THREAD_CACHE.get(`thread_${tid}`);
|
|
102
|
+
if (cached && (Date.now() - cached.ts < CACHE_TTL)) {
|
|
103
|
+
cachedResults[tid] = cached.data;
|
|
104
|
+
} else {
|
|
105
|
+
toFetch.push(tid);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (toFetch.length === 0) {
|
|
110
|
+
return Array.isArray(threadID) ? cachedResults : Object.values(cachedResults)[0] || null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (ctx.validator && !ctx.validator.validateIDArray(toFetch, ctx.validator.isValidThreadID)) {
|
|
114
|
+
throw new Error("Invalid thread ID(s)");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const queriesObj = {};
|
|
118
|
+
toFetch.forEach((t, i) => {
|
|
119
|
+
queriesObj[`o${i}`] = {
|
|
120
|
+
doc_id: "3449967031715030",
|
|
121
|
+
query_params: { id: t, message_limit: 0, load_messages: false, load_read_receipts: false, before: null }
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const form = { queries: JSON.stringify(queriesObj), batch_name: "MessengerGraphQLThreadFetcher" };
|
|
126
|
+
|
|
127
|
+
const resData = await retryOp(() =>
|
|
128
|
+
defaultFuncs.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
|
129
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (resData.error) throw resData;
|
|
133
|
+
|
|
134
|
+
const threadInfos = { ...cachedResults };
|
|
135
|
+
for (let i = resData.length - 2; i >= 0; i--) {
|
|
136
|
+
const res = resData[i];
|
|
137
|
+
if (res.error_results) throw Object.assign(new Error(`Facebook error_results for thread query`), { error_count: res.error_results });
|
|
138
|
+
const oKey = Object.keys(res)[0];
|
|
139
|
+
const responseData = res[oKey];
|
|
140
|
+
if (responseData.errors) throw Object.assign(new Error(JSON.stringify(responseData.errors)), { fullErrors: responseData.errors });
|
|
141
|
+
|
|
142
|
+
const info = formatThreadGraphQLResponse(responseData.data);
|
|
143
|
+
if (info) {
|
|
144
|
+
const tid = info.threadID || toFetch[toFetch.length - 1 - i];
|
|
145
|
+
threadInfos[tid] = info;
|
|
146
|
+
THREAD_CACHE.set(`thread_${tid}`, { data: info, ts: Date.now() });
|
|
147
|
+
if (ctx.cache) ctx.cache.set(`thread_${tid}`, info);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return Array.isArray(threadID) ? threadInfos : Object.values(threadInfos)[0] || null;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
|
|
5
|
+
const LIST_CACHE = new Map();
|
|
6
|
+
const CACHE_TTL = 60 * 1000;
|
|
7
|
+
|
|
8
|
+
async function retryOp(fn, retries = 3, base = 700) {
|
|
9
|
+
for (let i = 0; i < retries; i++) {
|
|
10
|
+
try { return await fn(); } catch (err) {
|
|
11
|
+
if (i === retries - 1) throw err;
|
|
12
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 300));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatEventReminders(reminder) {
|
|
18
|
+
return {
|
|
19
|
+
reminderID: reminder.id,
|
|
20
|
+
eventCreatorID: reminder.lightweight_event_creator?.id,
|
|
21
|
+
time: reminder.time,
|
|
22
|
+
eventType: reminder.lightweight_event_type?.toLowerCase(),
|
|
23
|
+
eventTitle: reminder.event_title,
|
|
24
|
+
members: (reminder.event_reminder_members?.edges || []).map(m => ({ memberID: m.node?.id, state: m.guest_list_state?.toLowerCase() }))
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatThread(messageThread) {
|
|
29
|
+
if (!messageThread?.thread_key) return null;
|
|
30
|
+
|
|
31
|
+
const threadID = messageThread.thread_key.thread_fbid || messageThread.thread_key.other_user_id;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
threadID,
|
|
35
|
+
threadName: messageThread.name,
|
|
36
|
+
participantIDs: messageThread.all_participants.edges.map(d => d.node.messaging_actor.id),
|
|
37
|
+
userInfo: messageThread.all_participants.edges.map(d => {
|
|
38
|
+
const p = d.node.messaging_actor;
|
|
39
|
+
return { id: p.id, name: p.name, firstName: p.short_name, vanity: p.username, url: p.url, thumbSrc: p.big_image_src?.uri, profileUrl: p.big_image_src?.uri, gender: p.gender, type: p.__typename, isFriend: p.is_viewer_friend, isBirthday: !!p.is_birthday, isMessengerUser: p.is_messenger_user, isVerified: p.is_verified };
|
|
40
|
+
}),
|
|
41
|
+
unreadCount: messageThread.unread_count,
|
|
42
|
+
messageCount: messageThread.messages_count,
|
|
43
|
+
timestamp: messageThread.updated_time_precise,
|
|
44
|
+
muteUntil: messageThread.mute_until,
|
|
45
|
+
isGroup: messageThread.thread_type === "GROUP",
|
|
46
|
+
isSubscribed: messageThread.is_viewer_subscribed,
|
|
47
|
+
isArchived: messageThread.has_viewer_archived,
|
|
48
|
+
folder: messageThread.folder,
|
|
49
|
+
cannotReplyReason: messageThread.cannot_reply_reason,
|
|
50
|
+
eventReminders: messageThread.event_reminders ? messageThread.event_reminders.nodes.map(formatEventReminders) : null,
|
|
51
|
+
emoji: messageThread.customization_info?.emoji || null,
|
|
52
|
+
color: messageThread.customization_info?.outgoing_bubble_color ? messageThread.customization_info.outgoing_bubble_color.slice(2) : null,
|
|
53
|
+
threadTheme: messageThread.thread_theme,
|
|
54
|
+
nicknames: (messageThread.customization_info?.participant_customizations || []).reduce((res, val) => { if (val.nickname) res[val.participant_id] = val.nickname; return res; }, {}),
|
|
55
|
+
adminIDs: (messageThread.thread_admins || []).map(a => a.id || a),
|
|
56
|
+
approvalMode: Boolean(messageThread.approval_mode),
|
|
57
|
+
approvalQueue: (messageThread.group_approval_queue?.nodes || []).map(a => ({ inviterID: a.inviter?.id, requesterID: a.requester?.id, timestamp: a.request_timestamp })),
|
|
58
|
+
reactionsMuteMode: messageThread.reactions_mute_mode?.toLowerCase() || "all_reactions",
|
|
59
|
+
mentionsMuteMode: messageThread.mentions_mute_mode?.toLowerCase() || "all_mentions",
|
|
60
|
+
isPinProtected: messageThread.is_pin_protected,
|
|
61
|
+
name: messageThread.name,
|
|
62
|
+
snippet: messageThread.last_message?.nodes?.[0]?.snippet || null,
|
|
63
|
+
snippetSender: messageThread.last_message?.nodes?.[0]?.message_sender?.messaging_actor?.id || null,
|
|
64
|
+
snippetAttachments: [],
|
|
65
|
+
serverTimestamp: messageThread.updated_time_precise,
|
|
66
|
+
imageSrc: messageThread.image?.uri || null,
|
|
67
|
+
isCanonical: messageThread.thread_type !== "GROUP",
|
|
68
|
+
canReply: messageThread.cannot_reply_reason == null,
|
|
69
|
+
lastMessageTimestamp: messageThread.last_message?.timestamp_precise || null,
|
|
70
|
+
lastReadTimestamp: messageThread.last_read_receipt?.nodes?.[0]?.timestamp_precise || null,
|
|
71
|
+
threadType: messageThread.thread_type === "GROUP" ? 2 : 1,
|
|
72
|
+
inviteLink: { enable: messageThread.joinable_mode?.mode === 1, link: messageThread.joinable_mode?.link || null }
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const VALID_FOLDERS = new Set(["INBOX", "ARCHIVED", "PENDING", "OTHER", "SPAM", "UNREAD"]);
|
|
77
|
+
|
|
78
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
79
|
+
return async function getThreadList(limit, timestamp, tags) {
|
|
80
|
+
if (typeof timestamp === "undefined") timestamp = null;
|
|
81
|
+
if (typeof tags === "undefined") tags = ["INBOX"];
|
|
82
|
+
if (typeof tags === "string") tags = [tags];
|
|
83
|
+
|
|
84
|
+
if (!Number.isInteger(limit) || limit <= 0) throw new Error("getThreadList: limit must be a positive integer");
|
|
85
|
+
if (timestamp !== null && !Number.isInteger(timestamp)) throw new Error("getThreadList: timestamp must be integer or null");
|
|
86
|
+
if (!Array.isArray(tags)) throw new Error("getThreadList: tags must be an array");
|
|
87
|
+
|
|
88
|
+
const normalizedTags = tags.map(t => t.toUpperCase()).filter(t => VALID_FOLDERS.has(t));
|
|
89
|
+
if (normalizedTags.length === 0) throw new Error(`getThreadList: invalid tags. Valid values: ${[...VALID_FOLDERS].join(", ")}`);
|
|
90
|
+
|
|
91
|
+
const cacheKey = `threadlist_${limit}_${timestamp}_${normalizedTags.join(",")}`;
|
|
92
|
+
const cached = LIST_CACHE.get(cacheKey);
|
|
93
|
+
if (cached && (Date.now() - cached.ts < CACHE_TTL)) return cached.data;
|
|
94
|
+
|
|
95
|
+
const form = {
|
|
96
|
+
av: ctx.i_userID || ctx.userID,
|
|
97
|
+
queries: JSON.stringify({
|
|
98
|
+
o0: {
|
|
99
|
+
doc_id: "3336396659757871",
|
|
100
|
+
query_params: {
|
|
101
|
+
limit: limit + (timestamp ? 1 : 0),
|
|
102
|
+
before: timestamp,
|
|
103
|
+
tags: normalizedTags,
|
|
104
|
+
includeDeliveryReceipts: true,
|
|
105
|
+
includeSeqID: false
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}),
|
|
109
|
+
batch_name: "MessengerGraphQLThreadlistFetcher"
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const resData = await retryOp(() =>
|
|
113
|
+
defaultFuncs.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
|
114
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (!Array.isArray(resData) || !resData.length) throw new Error("getThreadList: invalid server response");
|
|
118
|
+
|
|
119
|
+
const lastResult = resData[resData.length - 1];
|
|
120
|
+
if (lastResult.error_results > 0) throw new Error(JSON.stringify(resData[0]?.o0?.errors || "Unknown error"));
|
|
121
|
+
if (lastResult.successful_results === 0) throw new Error("getThreadList: no successful results");
|
|
122
|
+
if (!resData[0]?.o0?.data) throw new Error("getThreadList: invalid data structure");
|
|
123
|
+
|
|
124
|
+
let nodes = resData[0].o0.data.viewer.message_threads.nodes;
|
|
125
|
+
if (timestamp) nodes = nodes.slice(1);
|
|
126
|
+
|
|
127
|
+
const result = nodes.map(formatThread).filter(Boolean);
|
|
128
|
+
|
|
129
|
+
LIST_CACHE.set(cacheKey, { data: result, ts: Date.now() });
|
|
130
|
+
return result;
|
|
131
|
+
};
|
|
132
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
|
|
5
|
+
const PIC_CACHE = new Map();
|
|
6
|
+
const CACHE_TTL = 5 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
async function retryOp(fn, retries = 3, base = 600) {
|
|
9
|
+
for (let i = 0; i < retries; i++) {
|
|
10
|
+
try { return await fn(); } catch (err) {
|
|
11
|
+
if (i === retries - 1) throw err;
|
|
12
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 200));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function fetchViaGraphQL(defaultFuncs, ctx, threadID, offset, limit) {
|
|
18
|
+
const form = {
|
|
19
|
+
fb_api_caller_class: "RelayModern",
|
|
20
|
+
fb_api_req_friendly_name: "MessengerThreadSharedMediaQuery",
|
|
21
|
+
doc_id: "4629398273799357",
|
|
22
|
+
variables: JSON.stringify({
|
|
23
|
+
threadID: String(threadID),
|
|
24
|
+
first: limit,
|
|
25
|
+
after: offset > 0 ? String(offset) : null,
|
|
26
|
+
mediaTypes: ["photo", "video"]
|
|
27
|
+
})
|
|
28
|
+
};
|
|
29
|
+
const res = await defaultFuncs.post("https://www.facebook.com/api/graphql/", ctx.jar, form)
|
|
30
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
31
|
+
if (res.errors) throw new Error(JSON.stringify(res.errors));
|
|
32
|
+
return res.data?.message_thread?.shared_media?.edges?.map(e => ({
|
|
33
|
+
imageID: e.node?.id,
|
|
34
|
+
url: e.node?.image?.uri,
|
|
35
|
+
thumbnailUrl: e.node?.image?.uri,
|
|
36
|
+
width: e.node?.image?.width,
|
|
37
|
+
height: e.node?.image?.height,
|
|
38
|
+
messageID: e.node?.message_id,
|
|
39
|
+
timestamp: e.node?.timestamp_precise
|
|
40
|
+
})) || [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
44
|
+
return async function getThreadPictures(threadID, offset, limit, callback) {
|
|
45
|
+
let resolveFunc, rejectFunc;
|
|
46
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
47
|
+
resolveFunc = resolve;
|
|
48
|
+
rejectFunc = reject;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (typeof limit === "function") { callback = limit; limit = 50; }
|
|
52
|
+
else if (typeof offset === "function") { callback = offset; offset = 0; limit = 50; }
|
|
53
|
+
if (typeof callback !== "function") {
|
|
54
|
+
callback = (err, result) => {
|
|
55
|
+
if (err) return rejectFunc(err);
|
|
56
|
+
resolveFunc(result);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
offset = typeof offset === "number" ? offset : 0;
|
|
61
|
+
limit = typeof limit === "number" ? Math.min(limit, 200) : 50;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
if (!threadID) throw new Error("getThreadPictures: threadID is required");
|
|
65
|
+
|
|
66
|
+
const cacheKey = `pics_${threadID}_${offset}_${limit}`;
|
|
67
|
+
const cached = PIC_CACHE.get(cacheKey);
|
|
68
|
+
if (cached && (Date.now() - cached.ts < CACHE_TTL)) return callback(null, cached.data);
|
|
69
|
+
|
|
70
|
+
let result;
|
|
71
|
+
try {
|
|
72
|
+
const form = { thread_id: String(threadID), offset, limit };
|
|
73
|
+
const res = await retryOp(() =>
|
|
74
|
+
defaultFuncs.post("https://www.facebook.com/ajax/mercury/thread_images.php", ctx.jar, form)
|
|
75
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
76
|
+
);
|
|
77
|
+
if (res?.error) throw res;
|
|
78
|
+
result = res?.payload || res;
|
|
79
|
+
} catch (legacyErr) {
|
|
80
|
+
utils.warn("getThreadPictures", "Legacy API failed, trying GraphQL:", legacyErr.message || legacyErr.error);
|
|
81
|
+
result = await retryOp(() => fetchViaGraphQL(defaultFuncs, ctx, threadID, offset, limit));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
PIC_CACHE.set(cacheKey, { data: result, ts: Date.now() });
|
|
85
|
+
callback(null, result);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
utils.error("getThreadPictures", err);
|
|
88
|
+
callback(err);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return returnPromise;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
|
|
5
|
+
const UID_CACHE = new Map();
|
|
6
|
+
const CACHE_TTL = 10 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
async function retryOp(fn, retries = 3, base = 600) {
|
|
9
|
+
for (let i = 0; i < retries; i++) {
|
|
10
|
+
try { return await fn(); } catch (err) {
|
|
11
|
+
if (i === retries - 1) throw err;
|
|
12
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 200));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatData(data) {
|
|
18
|
+
return {
|
|
19
|
+
userID: utils.formatID(String(data.uid)),
|
|
20
|
+
photoUrl: data.photo,
|
|
21
|
+
indexRank: data.index_rank,
|
|
22
|
+
name: data.text,
|
|
23
|
+
isVerified: !!data.is_verified,
|
|
24
|
+
profileUrl: data.path,
|
|
25
|
+
category: data.category,
|
|
26
|
+
score: data.score,
|
|
27
|
+
type: data.type
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function extractIDFromUrl(url) {
|
|
32
|
+
if (!url) return null;
|
|
33
|
+
|
|
34
|
+
const profileMatch = url.match(/profile\.php\?id=(\d+)/);
|
|
35
|
+
if (profileMatch) return profileMatch[1];
|
|
36
|
+
|
|
37
|
+
const fbidMatch = url.match(/[?&]fbid=(\d+)/);
|
|
38
|
+
if (fbidMatch) return fbidMatch[1];
|
|
39
|
+
|
|
40
|
+
const idMatch = url.match(/\/(\d{10,})/);
|
|
41
|
+
if (idMatch) return idMatch[1];
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function resolveVanityURL(defaultFuncs, ctx, url) {
|
|
47
|
+
const cleanUrl = url.replace(/\?.*$/, "").replace(/\/$/, "");
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const res = await retryOp(() => defaultFuncs.get(cleanUrl, ctx.jar));
|
|
51
|
+
const html = res.body || (typeof res === "string" ? res : "");
|
|
52
|
+
|
|
53
|
+
const patterns = [
|
|
54
|
+
/"userID":"(\d+)"/,
|
|
55
|
+
/"USER_ID":"(\d+)"/,
|
|
56
|
+
/"id":"(\d+)"/,
|
|
57
|
+
/entity_id[":\s]+(\d+)/,
|
|
58
|
+
/profile_id=(\d+)/,
|
|
59
|
+
/"actorID":"(\d+)"/,
|
|
60
|
+
/"ownerID":"(\d+)"/
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
for (const pattern of patterns) {
|
|
64
|
+
const match = html.match(pattern);
|
|
65
|
+
if (match && /^\d{8,}$/.test(match[1])) return match[1];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const extracted = extractIDFromUrl(url);
|
|
69
|
+
if (extracted) return extracted;
|
|
70
|
+
|
|
71
|
+
return null;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
78
|
+
return async function getUID(link, callback) {
|
|
79
|
+
let resolveFunc, rejectFunc;
|
|
80
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
81
|
+
resolveFunc = resolve;
|
|
82
|
+
rejectFunc = reject;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (typeof callback !== "function") {
|
|
86
|
+
callback = (err, result) => {
|
|
87
|
+
if (err) return rejectFunc(err);
|
|
88
|
+
resolveFunc(result);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
if (!link || typeof link !== "string") throw { error: "getUserID: link must be a non-empty string" };
|
|
94
|
+
|
|
95
|
+
const cacheKey = `uid_${link.trim().toLowerCase()}`;
|
|
96
|
+
const cached = UID_CACHE.get(cacheKey);
|
|
97
|
+
if (cached && (Date.now() - cached.ts < CACHE_TTL)) return callback(null, cached.data);
|
|
98
|
+
|
|
99
|
+
const isURL = link.includes(".com") || link.startsWith("http");
|
|
100
|
+
|
|
101
|
+
if (isURL) {
|
|
102
|
+
let uid = extractIDFromUrl(link);
|
|
103
|
+
|
|
104
|
+
if (!uid) {
|
|
105
|
+
uid = await resolveVanityURL(defaultFuncs, ctx, link);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!uid || !/^\d+$/.test(uid)) throw new Error("getUserID: could not extract user ID from URL");
|
|
109
|
+
|
|
110
|
+
UID_CACHE.set(cacheKey, { data: uid, ts: Date.now() });
|
|
111
|
+
return callback(null, uid);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const form = {
|
|
115
|
+
value: link.toLowerCase().trim(),
|
|
116
|
+
viewer: ctx.userID,
|
|
117
|
+
rsp: "search",
|
|
118
|
+
context: "search",
|
|
119
|
+
path: "/home.php",
|
|
120
|
+
request_id: ctx.clientID || utils.getGUID()
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const res = await retryOp(() =>
|
|
124
|
+
defaultFuncs.get("https://www.facebook.com/ajax/typeahead/search.php", ctx.jar, form)
|
|
125
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (res.error) throw res;
|
|
129
|
+
|
|
130
|
+
if (!res.payload?.entries) {
|
|
131
|
+
throw {
|
|
132
|
+
error: "getUserID: no results found. Account may require checkpoint verification.",
|
|
133
|
+
details: "Visit facebook.com to verify your account."
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const data = res.payload.entries.map(formatData);
|
|
138
|
+
UID_CACHE.set(cacheKey, { data, ts: Date.now() });
|
|
139
|
+
callback(null, data);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
utils.error("getUserID", err);
|
|
142
|
+
callback(err);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return returnPromise;
|
|
146
|
+
};
|
|
147
|
+
};
|