ayman-fca 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.
Files changed (124) hide show
  1. package/README.md +81 -0
  2. package/func/checkUpdate.js +13 -0
  3. package/func/logAdapter.js +30 -0
  4. package/func/logger.js +66 -0
  5. package/index.d.ts +751 -0
  6. package/index.js +15 -0
  7. package/module/config.js +38 -0
  8. package/module/login.js +111 -0
  9. package/module/loginHelper.js +1296 -0
  10. package/module/options.js +37 -0
  11. package/package.json +78 -0
  12. package/src/api/action/addExternalModule.js +19 -0
  13. package/src/api/action/changeAvatar.js +137 -0
  14. package/src/api/action/changeBio.js +48 -0
  15. package/src/api/action/enableAutoSaveAppState.js +72 -0
  16. package/src/api/action/getCurrentUserID.js +11 -0
  17. package/src/api/action/handleFriendRequest.js +33 -0
  18. package/src/api/action/logout.js +76 -0
  19. package/src/api/action/refreshFb_dtsg.js +62 -0
  20. package/src/api/action/setPostReaction.js +106 -0
  21. package/src/api/action/story.js +118 -0
  22. package/src/api/action/unfriend.js +30 -0
  23. package/src/api/http/httpGet.js +28 -0
  24. package/src/api/http/httpPost.js +32 -0
  25. package/src/api/http/postFormData.js +23 -0
  26. package/src/api/messaging/J +1 -0
  27. package/src/api/messaging/addUserToGroup.js +70 -0
  28. package/src/api/messaging/changeAdminStatus.js +72 -0
  29. package/src/api/messaging/changeArchivedStatus.js +31 -0
  30. package/src/api/messaging/changeBlockedStatus.js +27 -0
  31. package/src/api/messaging/changeGroupImage.js +91 -0
  32. package/src/api/messaging/changeNickname.js +70 -0
  33. package/src/api/messaging/changeThreadColor.js +44 -0
  34. package/src/api/messaging/changeThreadEmoji.js +111 -0
  35. package/src/api/messaging/createNewGroup.js +50 -0
  36. package/src/api/messaging/createPoll.js +52 -0
  37. package/src/api/messaging/createThemeAI.js +98 -0
  38. package/src/api/messaging/deleteMessage.js +73 -0
  39. package/src/api/messaging/deleteThread.js +29 -0
  40. package/src/api/messaging/editMessage.js +67 -0
  41. package/src/api/messaging/forwardAttachment.js +55 -0
  42. package/src/api/messaging/forwardMessage.js +73 -0
  43. package/src/api/messaging/getEmojiUrl.js +29 -0
  44. package/src/api/messaging/getFriendsList.js +82 -0
  45. package/src/api/messaging/getMessage.js +829 -0
  46. package/src/api/messaging/getThemePictures.js +62 -0
  47. package/src/api/messaging/groupActions.js +119 -0
  48. package/src/api/messaging/handleMessageRequest.js +31 -0
  49. package/src/api/messaging/markAsDelivered.js +31 -0
  50. package/src/api/messaging/markAsRead.js +88 -0
  51. package/src/api/messaging/markAsReadAll.js +28 -0
  52. package/src/api/messaging/markAsSeen.js +30 -0
  53. package/src/api/messaging/muteThread.js +27 -0
  54. package/src/api/messaging/notes.js +101 -0
  55. package/src/api/messaging/removeUserFromGroup.js +51 -0
  56. package/src/api/messaging/resolvePhotoUrl.js +29 -0
  57. package/src/api/messaging/scheduler.js +100 -0
  58. package/src/api/messaging/searchForThread.js +32 -0
  59. package/src/api/messaging/sendMessage.js +270 -0
  60. package/src/api/messaging/sendTypingIndicator.js +62 -0
  61. package/src/api/messaging/setMessageReaction.js +91 -0
  62. package/src/api/messaging/setTitle.js +86 -0
  63. package/src/api/messaging/shareContact.js +47 -0
  64. package/src/api/messaging/threadColors.js +128 -0
  65. package/src/api/messaging/unsendMessage.js +73 -0
  66. package/src/api/messaging/uploadAttachment.js +492 -0
  67. package/src/api/socket/core/connectMqtt.js +259 -0
  68. package/src/api/socket/core/emitAuth.js +79 -0
  69. package/src/api/socket/core/getSeqID.js +170 -0
  70. package/src/api/socket/core/getTaskResponseData.js +27 -0
  71. package/src/api/socket/core/parseDelta.js +377 -0
  72. package/src/api/socket/detail/buildStream.js +215 -0
  73. package/src/api/socket/detail/constants.js +29 -0
  74. package/src/api/socket/listenMqtt.js +377 -0
  75. package/src/api/socket/middleware/index.js +80 -0
  76. package/src/api/threads/getThreadHistory.js +664 -0
  77. package/src/api/threads/getThreadInfo.js +296 -0
  78. package/src/api/threads/getThreadList.js +293 -0
  79. package/src/api/threads/getThreadPictures.js +43 -0
  80. package/src/api/user/J +1 -0
  81. package/src/api/user/getUserID.js +48 -0
  82. package/src/api/user/getUserInfo.js +402 -0
  83. package/src/api/user/getUserInfoV2.js +134 -0
  84. package/src/core/sendReqMqtt.js +69 -0
  85. package/src/database/helpers.js +36 -0
  86. package/src/database/models/index.js +55 -0
  87. package/src/database/models/thread.js +44 -0
  88. package/src/database/models/user.js +39 -0
  89. package/src/database/threadData.js +92 -0
  90. package/src/database/userData.js +88 -0
  91. package/src/remote/remoteClient.js +71 -0
  92. package/src/utils/broadcast.js +62 -0
  93. package/src/utils/client.js +10 -0
  94. package/src/utils/constants.js +53 -0
  95. package/src/utils/cookies.js +73 -0
  96. package/src/utils/format/attachment.js +357 -0
  97. package/src/utils/format/cookie.js +9 -0
  98. package/src/utils/format/date.js +50 -0
  99. package/src/utils/format/decode.js +44 -0
  100. package/src/utils/format/delta.js +194 -0
  101. package/src/utils/format/ids.js +64 -0
  102. package/src/utils/format/index.js +64 -0
  103. package/src/utils/format/message.js +88 -0
  104. package/src/utils/format/presence.js +132 -0
  105. package/src/utils/format/readTyp.js +44 -0
  106. package/src/utils/format/thread.js +42 -0
  107. package/src/utils/format/utils.js +141 -0
  108. package/src/utils/headers.js +96 -0
  109. package/src/utils/loginParser/autoLogin.js +125 -0
  110. package/src/utils/loginParser/helpers.js +43 -0
  111. package/src/utils/loginParser/index.js +10 -0
  112. package/src/utils/loginParser/parseAndCheckLogin.js +220 -0
  113. package/src/utils/loginParser/textUtils.js +28 -0
  114. package/src/utils/request/H +1 -0
  115. package/src/utils/request/client.js +33 -0
  116. package/src/utils/request/config.js +25 -0
  117. package/src/utils/request/defaults.js +40 -0
  118. package/src/utils/request/helpers.js +31 -0
  119. package/src/utils/request/index.js +12 -0
  120. package/src/utils/request/methods.js +92 -0
  121. package/src/utils/request/proxy.js +23 -0
  122. package/src/utils/request/retry.js +87 -0
  123. package/src/utils/request/sanitize.js +41 -0
  124. package/src/utils/sessionKeeper.js +275 -0
@@ -0,0 +1,296 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { parseAndCheckLogin } = require('../../utils/client');
6
+ const log = require('../../../func/logAdapter');
7
+
8
+ function formatEventReminders(reminder) {
9
+ return {
10
+ reminderID: reminder.id,
11
+ eventCreatorID: reminder.lightweight_event_creator.id,
12
+ time: reminder.time,
13
+ eventType: reminder.lightweight_event_type.toLowerCase(),
14
+ locationName: reminder.location_name,
15
+ locationCoordinates: reminder.location_coordinates,
16
+ locationPage: reminder.location_page,
17
+ eventStatus: reminder.lightweight_event_status.toLowerCase(),
18
+ note: reminder.note,
19
+ repeatMode: reminder.repeat_mode.toLowerCase(),
20
+ eventTitle: reminder.event_title,
21
+ triggerMessage: reminder.trigger_message,
22
+ secondsToNotifyBefore: reminder.seconds_to_notify_before,
23
+ allowsRsvp: reminder.allows_rsvp,
24
+ relatedEvent: reminder.related_event,
25
+ members: reminder.event_reminder_members.edges.map((member) => ({
26
+ memberID: member.node.id,
27
+ state: member.guest_list_state.toLowerCase(),
28
+ })),
29
+ };
30
+ }
31
+
32
+ function formatThreadGraphQLResponse(data) {
33
+ if (data.errors) {
34
+ const details = data.errors.map(e => e.message || e).join(", ");
35
+ const error = new Error(`GraphQL error in getThreadInfo: ${details}`);
36
+ throw error;
37
+ }
38
+
39
+ const messageThread = data.message_thread;
40
+ if (!messageThread) {
41
+ throw new Error("No message_thread in GraphQL response");
42
+ }
43
+
44
+ const threadID =
45
+ messageThread.thread_key.thread_fbid ||
46
+ messageThread.thread_key.other_user_id;
47
+
48
+ const lastM = messageThread.last_message;
49
+ const snippetID =
50
+ lastM?.nodes?.[0]?.message_sender?.messaging_actor?.id || null;
51
+ const snippetText = lastM?.nodes?.[0]?.snippet || null;
52
+ const lastReadTimestamp =
53
+ messageThread.last_read_receipt?.nodes?.[0]?.timestamp_precise || null;
54
+
55
+ return {
56
+ threadID,
57
+ threadName: messageThread.name,
58
+ participantIDs: messageThread.all_participants.edges.map(
59
+ d => d.node.messaging_actor.id
60
+ ),
61
+ userInfo: messageThread.all_participants.edges.map(d => ({
62
+ id: d.node.messaging_actor.id,
63
+ name: d.node.messaging_actor.name,
64
+ firstName: d.node.messaging_actor.short_name,
65
+ vanity: d.node.messaging_actor.username,
66
+ url: d.node.messaging_actor.url,
67
+ thumbSrc: d.node.messaging_actor.big_image_src.uri,
68
+ profileUrl: d.node.messaging_actor.big_image_src.uri,
69
+ gender: d.node.messaging_actor.gender,
70
+ type: d.node.messaging_actor.__typename,
71
+ isFriend: d.node.messaging_actor.is_viewer_friend,
72
+ isBirthday: !!d.node.messaging_actor.is_birthday,
73
+ })),
74
+ unreadCount: messageThread.unread_count,
75
+ messageCount: messageThread.messages_count,
76
+ timestamp: messageThread.updated_time_precise,
77
+ muteUntil: messageThread.mute_until,
78
+ isGroup: messageThread.thread_type === "GROUP",
79
+ isSubscribed: messageThread.is_viewer_subscribed,
80
+ isArchived: messageThread.has_viewer_archived,
81
+ folder: messageThread.folder,
82
+ cannotReplyReason: messageThread.cannot_reply_reason,
83
+ eventReminders: messageThread.event_reminders
84
+ ? messageThread.event_reminders.nodes.map(formatEventReminders)
85
+ : null,
86
+ emoji: messageThread.customization_info?.emoji || null,
87
+ color: messageThread.customization_info?.outgoing_bubble_color
88
+ ? messageThread.customization_info.outgoing_bubble_color.slice(2)
89
+ : null,
90
+ threadTheme: messageThread.thread_theme,
91
+ nicknames:
92
+ messageThread.customization_info?.participant_customizations?.reduce(
93
+ (res, val) => {
94
+ if (val.nickname) res[val.participant_id] = val.nickname;
95
+ return res;
96
+ },
97
+ {}
98
+ ) || {},
99
+ adminIDs: messageThread.thread_admins,
100
+ approvalMode: Boolean(messageThread.approval_mode),
101
+ approvalQueue:
102
+ messageThread.group_approval_queue?.nodes?.map(a => ({
103
+ inviterID: a.inviter.id,
104
+ requesterID: a.requester.id,
105
+ timestamp: a.request_timestamp,
106
+ request_source: a.request_source,
107
+ })) || [],
108
+ reactionsMuteMode: messageThread.reactions_mute_mode?.toLowerCase(),
109
+ mentionsMuteMode: messageThread.mentions_mute_mode?.toLowerCase(),
110
+ isPinProtected: messageThread.is_pin_protected,
111
+ relatedPageThread: messageThread.related_page_thread,
112
+ name: messageThread.name,
113
+ snippet: snippetText,
114
+ snippetSender: snippetID,
115
+ snippetAttachments: [],
116
+ serverTimestamp: messageThread.updated_time_precise,
117
+ imageSrc: messageThread.image?.uri || null,
118
+ isCanonicalUser: messageThread.is_canonical_neo_user,
119
+ isCanonical: messageThread.thread_type !== "GROUP",
120
+ recipientsLoadable: true,
121
+ hasEmailParticipant: false,
122
+ readOnly: false,
123
+ canReply: messageThread.cannot_reply_reason == null,
124
+ lastMessageTimestamp:
125
+ messageThread.last_message?.timestamp_precise || null,
126
+ lastMessageType: "message",
127
+ lastReadTimestamp,
128
+ threadType: messageThread.thread_type === "GROUP" ? 2 : 1,
129
+ inviteLink: {
130
+ enable: messageThread.joinable_mode?.mode === 1,
131
+ link: messageThread.joinable_mode?.link || null,
132
+ },
133
+ };
134
+ }
135
+
136
+ module.exports = function (defaultFuncs, api, ctx) {
137
+ const dbFiles = fs.readdirSync(path.join(__dirname, "../../database"))
138
+ .filter(f => path.extname(f) === ".js")
139
+ .reduce((acc, file) => {
140
+ const mod = require(path.join(__dirname, "../../database", file));
141
+ acc[path.basename(file, ".js")] = typeof mod === "function" ? mod(api) : mod;
142
+ return acc;
143
+ }, {});
144
+
145
+ const { threadData } = dbFiles;
146
+ const { create, get, update } = threadData || {};
147
+ const FRESH_MS = 10 * 60 * 1000;
148
+ return function getThreadInfo(threadID, callback) {
149
+ let resolveFunc;
150
+ let rejectFunc;
151
+
152
+ const returnPromise = new Promise((resolve, reject) => {
153
+ resolveFunc = resolve;
154
+ rejectFunc = reject;
155
+ });
156
+
157
+ if (typeof callback !== "function") {
158
+ callback = (err, data) => {
159
+ if (err) {
160
+ return rejectFunc(err);
161
+ }
162
+ return resolveFunc(data);
163
+ };
164
+ }
165
+
166
+ const threadIDs = Array.isArray(threadID) ? threadID.map(String) : [String(threadID)];
167
+
168
+ const now = Date.now();
169
+
170
+ const loadFromDb = async ids => {
171
+ if (!threadData || typeof get !== "function") return { fresh: {}, stale: ids };
172
+ const fresh = {};
173
+ const stale = [];
174
+ const rows = await Promise.all(ids.map(id => get(id).catch(() => null)));
175
+ for (let i = 0; i < ids.length; i++) {
176
+ const id = ids[i];
177
+ const row = rows[i];
178
+ if (row && row.data) {
179
+ const updatedAt = row.updatedAt ? new Date(row.updatedAt).getTime() : 0;
180
+ if (updatedAt && now - updatedAt <= FRESH_MS) {
181
+ fresh[id] = row.data;
182
+ } else {
183
+ stale.push(id);
184
+ }
185
+ } else {
186
+ stale.push(id);
187
+ }
188
+ }
189
+ return { fresh, stale };
190
+ };
191
+
192
+ const fetchFromGraphQL = async ids => {
193
+ if (!ids.length) return {};
194
+ const queries = {};
195
+ ids.forEach((t, i) => {
196
+ queries["o" + i] = {
197
+ doc_id: "3449967031715030",
198
+ query_params: {
199
+ id: t,
200
+ message_limit: 0,
201
+ load_messages: false,
202
+ load_read_receipts: false,
203
+ before: null
204
+ }
205
+ };
206
+ });
207
+
208
+ const form = {
209
+ queries: JSON.stringify(queries),
210
+ batch_name: "MessengerGraphQLThreadFetcher"
211
+ };
212
+
213
+ const resData = await defaultFuncs
214
+ .post(
215
+ "https://www.facebook.com/api/graphqlbatch/",
216
+ ctx.jar,
217
+ form
218
+ )
219
+ .then(parseAndCheckLogin(ctx, defaultFuncs));
220
+
221
+ if (resData.error) {
222
+ throw resData;
223
+ }
224
+
225
+ const out = {};
226
+ for (let i = resData.length - 2; i >= 0; i--) {
227
+ const res = resData[i];
228
+ const oKey = Object.keys(res)[0];
229
+ const responseData = res[oKey];
230
+ try {
231
+ const info = formatThreadGraphQLResponse(responseData.data);
232
+ if (info && info.threadID) {
233
+ out[info.threadID] = info;
234
+ }
235
+ } catch (e) {
236
+ // Skip malformed entries but continue processing others
237
+ log.error("getThreadInfoGraphQL", e && e.message ? e.message : String(e));
238
+ }
239
+ }
240
+ return out;
241
+ };
242
+
243
+ (async () => {
244
+ try {
245
+ const { fresh, stale } = await loadFromDb(threadIDs);
246
+ let fetched = {};
247
+
248
+ if (stale.length) {
249
+ fetched = await fetchFromGraphQL(stale);
250
+
251
+ // Persist fetched data back to DB
252
+ if (threadData && (typeof create === "function" || typeof update === "function")) {
253
+ const tasks = [];
254
+ for (const id of stale) {
255
+ const info = fetched[id];
256
+ if (!info) continue;
257
+ const payload = { data: info };
258
+ if (typeof update === "function") {
259
+ tasks.push(update(id, payload).catch(() => null));
260
+ } else if (typeof create === "function") {
261
+ tasks.push(create(id, payload).catch(() => null));
262
+ }
263
+ }
264
+ if (tasks.length) {
265
+ try {
266
+ await Promise.all(tasks);
267
+ } catch {
268
+ // Swallow DB errors – not critical for API behavior
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ const resultMap = {};
275
+ for (const id of threadIDs) {
276
+ resultMap[id] = fresh[id] || fetched[id] || null;
277
+ }
278
+
279
+ const result = Array.isArray(threadID)
280
+ ? resultMap
281
+ : resultMap[threadIDs[0]] || null;
282
+
283
+ return callback(null, result);
284
+ } catch (err) {
285
+ // Horizon-style anti-get-info message to hint possible spam/limit
286
+ log.error(
287
+ "getThreadInfoGraphQL",
288
+ "Lỗi: getThreadInfoGraphQL Có Thể Do Bạn Spam Quá Nhiều, Hãy Thử Lại !"
289
+ );
290
+ return callback(err);
291
+ }
292
+ })();
293
+
294
+ return returnPromise;
295
+ };
296
+ };
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+
3
+ const log = require("../../../func/logAdapter");
4
+ const { parseAndCheckLogin } = require("../../utils/client");
5
+ const { formatID, getType } = require("../../utils/format");
6
+ function createProfileUrl(url, username, id) {
7
+ if (url) return url;
8
+ return (
9
+ "https://www.facebook.com/" + (username || formatID(id.toString()))
10
+ );
11
+ }
12
+
13
+ function formatParticipants(participants) {
14
+ return participants.edges.map(p => {
15
+ p = p.node.messaging_actor;
16
+ switch (p["__typename"]) {
17
+ case "User":
18
+ return {
19
+ accountType: p["__typename"],
20
+ userID: formatID(p.id.toString()), // do we need .toString()? when it is not a string?
21
+ name: p.name,
22
+ shortName: p.short_name,
23
+ gender: p.gender,
24
+ url: p.url, // how about making it profileURL
25
+ profilePicture: p.big_image_src.uri,
26
+ username: p.username || null,
27
+ // TODO: maybe better names for these?
28
+ isViewerFriend: p.is_viewer_friend, // true/false
29
+ isMessengerUser: p.is_messenger_user, // true/false
30
+ isVerified: p.is_verified, // true/false
31
+ isMessageBlockedByViewer: p.is_message_blocked_by_viewer, // true/false
32
+ isViewerCoworker: p.is_viewer_coworker, // true/false
33
+ isEmployee: p.is_employee // null? when it is something other? can someone check?
34
+ };
35
+ case "Page":
36
+ return {
37
+ accountType: p["__typename"],
38
+ userID: formatID(p.id.toString()), // or maybe... pageID?
39
+ name: p.name,
40
+ url: p.url,
41
+ profilePicture: p.big_image_src.uri,
42
+ username: p.username || null,
43
+ // uhm... better names maybe?
44
+ acceptsMessengerUserFeedback: p.accepts_messenger_user_feedback, // true/false
45
+ isMessengerUser: p.is_messenger_user, // true/false
46
+ isVerified: p.is_verified, // true/false
47
+ isMessengerPlatformBot: p.is_messenger_platform_bot, // true/false
48
+ isMessageBlockedByViewer: p.is_message_blocked_by_viewer // true/false
49
+ };
50
+ case "ReducedMessagingActor":
51
+ case "UnavailableMessagingActor":
52
+ return {
53
+ accountType: p["__typename"],
54
+ userID: formatID(p.id.toString()),
55
+ name: p.name,
56
+ url: createProfileUrl(p.url, p.username, p.id), // in this case p.url is null all the time
57
+ profilePicture: p.big_image_src.uri, // in this case it is default facebook photo, we could determine gender using it
58
+ username: p.username || null, // maybe we could use it to generate profile URL?
59
+ isMessageBlockedByViewer: p.is_message_blocked_by_viewer // true/false
60
+ };
61
+ default:
62
+ log.warn(
63
+ "getThreadList",
64
+ "Found participant with unsupported typename. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues\n" +
65
+ JSON.stringify(p, null, 2)
66
+ );
67
+ return {
68
+ accountType: p["__typename"],
69
+ userID: formatID(p.id.toString()),
70
+ name: p.name || `[unknown ${p["__typename"]}]` // probably it will always be something... but fallback to [unknown], just in case
71
+ };
72
+ }
73
+ });
74
+ }
75
+
76
+ // "FF8C0077" -> "8C0077"
77
+ function formatColor(color) {
78
+ if (color && color.match(/^(?:[0-9a-fA-F]{8})$/g)) return color.slice(2);
79
+ return color;
80
+ }
81
+
82
+ function getThreadName(t) {
83
+ if (t.name || t.thread_key.thread_fbid) return t.name;
84
+
85
+ for (let po of t.all_participants.edges) {
86
+ let p = po.node;
87
+ if (p.messaging_actor.id === t.thread_key.other_user_id)
88
+ return p.messaging_actor.name;
89
+ }
90
+ }
91
+
92
+ function mapNicknames(customizationInfo) {
93
+ return customizationInfo && customizationInfo.participant_customizations
94
+ ? customizationInfo.participant_customizations.map(u => {
95
+ return {
96
+ userID: u.participant_id,
97
+ nickname: u.nickname
98
+ };
99
+ })
100
+ : [];
101
+ }
102
+
103
+ function formatThreadList(data) {
104
+ return data.map(t => {
105
+ let lastMessageNode =
106
+ t.last_message && t.last_message.nodes && t.last_message.nodes.length > 0
107
+ ? t.last_message.nodes[0]
108
+ : null;
109
+ return {
110
+ threadID: t.thread_key
111
+ ? formatID(t.thread_key.thread_fbid || t.thread_key.other_user_id)
112
+ : null, // shall never be null
113
+ name: getThreadName(t),
114
+ unreadCount: t.unread_count,
115
+ messageCount: t.messages_count,
116
+ imageSrc: t.image ? t.image.uri : null,
117
+ emoji: t.customization_info ? t.customization_info.emoji : null,
118
+ color: formatColor(
119
+ t.customization_info ? t.customization_info.outgoing_bubble_color : null
120
+ ),
121
+ threadTheme: t.thread_theme,
122
+ nicknames: mapNicknames(t.customization_info),
123
+ muteUntil: t.mute_until,
124
+ participants: formatParticipants(t.all_participants),
125
+ adminIDs: t.thread_admins.map(a => a.id),
126
+ folder: t.folder,
127
+ isGroup: t.thread_type === "GROUP",
128
+ customizationEnabled: t.customization_enabled, // false for ONE_TO_ONE with Page or ReducedMessagingActor
129
+ participantAddMode: t.participant_add_mode_as_string, // "ADD" if "GROUP" and null if "ONE_TO_ONE"
130
+ montageThread: t.montage_thread
131
+ ? Buffer.from(t.montage_thread.id, "base64").toString()
132
+ : null, // base64 encoded string "message_thread:0000000000000000"
133
+ reactionsMuteMode: t.reactions_mute_mode,
134
+ mentionsMuteMode: t.mentions_mute_mode,
135
+ isArchived: t.has_viewer_archived,
136
+ isSubscribed: t.is_viewer_subscribed,
137
+ timestamp: t.updated_time_precise, // in miliseconds
138
+ snippet: lastMessageNode ? lastMessageNode.snippet : null,
139
+ snippetAttachments: lastMessageNode
140
+ ? lastMessageNode.extensible_attachment
141
+ : null, // TODO: not sure if it works
142
+ snippetSender: lastMessageNode
143
+ ? formatID(
144
+ (lastMessageNode.message_sender.messaging_actor.id || "").toString()
145
+ )
146
+ : null,
147
+ lastMessageTimestamp: lastMessageNode
148
+ ? lastMessageNode.timestamp_precise
149
+ : null, // timestamp in miliseconds
150
+ lastReadTimestamp:
151
+ t.last_read_receipt && t.last_read_receipt.nodes.length > 0
152
+ ? t.last_read_receipt.nodes[0]
153
+ ? t.last_read_receipt.nodes[0].timestamp_precise
154
+ : null
155
+ : null,
156
+ cannotReplyReason: t.cannot_reply_reason,
157
+ approvalMode: Boolean(t.approval_mode),
158
+ participantIDs: formatParticipants(t.all_participants).map(
159
+ participant => participant.userID
160
+ ),
161
+ threadType: t.thread_type === "GROUP" ? 2 : 1, // "GROUP" or "ONE_TO_ONE"
162
+ inviteLink: {
163
+ enable: t.joinable_mode ? t.joinable_mode.mode == 1 : false,
164
+ link: t.joinable_mode ? t.joinable_mode.link : null
165
+ }
166
+ };
167
+ });
168
+ }
169
+
170
+ module.exports = function(defaultFuncs, api, ctx) {
171
+ return function getThreadList(limit, timestamp, tags, callback) {
172
+ if (
173
+ !callback &&
174
+ (getType(tags) === "Function" ||
175
+ getType(tags) === "AsyncFunction")
176
+ ) {
177
+ callback = tags;
178
+ tags = [""];
179
+ }
180
+ if (
181
+ getType(limit) !== "Number" ||
182
+ !Number.isInteger(limit) ||
183
+ limit <= 0
184
+ )
185
+ throw { error: "getThreadList: limit must be a positive integer" };
186
+ if (
187
+ getType(timestamp) !== "Null" &&
188
+ (getType(timestamp) !== "Number" || !Number.isInteger(timestamp))
189
+ )
190
+ throw { error: "getThreadList: timestamp must be an integer or null" };
191
+ if (getType(tags) === "String") tags = [tags];
192
+ if (getType(tags) !== "Array")
193
+ throw { error: "getThreadList: tags must be an array" };
194
+ var resolveFunc = function() {};
195
+ var rejectFunc = function() {};
196
+ var returnPromise = new Promise(function(resolve, reject) {
197
+ resolveFunc = resolve;
198
+ rejectFunc = reject;
199
+ });
200
+ if (
201
+ getType(callback) !== "Function" &&
202
+ getType(callback) !== "AsyncFunction"
203
+ ) {
204
+ callback = function(err, data) {
205
+ if (err) return rejectFunc(err);
206
+ resolveFunc(data);
207
+ };
208
+ }
209
+ const form = {
210
+ av: ctx.userID,
211
+ queries: JSON.stringify({
212
+ o0: {
213
+ doc_id: "3336396659757871",
214
+ query_params: {
215
+ limit: limit + (timestamp ? 1 : 0),
216
+ before: timestamp,
217
+ tags: tags,
218
+ includeDeliveryReceipts: true,
219
+ includeSeqID: false
220
+ }
221
+ }
222
+ }),
223
+ batch_name: "MessengerGraphQLThreadlistFetcher"
224
+ };
225
+ defaultFuncs
226
+ .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
227
+ .then(parseAndCheckLogin(ctx, defaultFuncs))
228
+ .then(resData => {
229
+ // Validate resData is an array and has elements
230
+ if (!resData || !Array.isArray(resData) || resData.length === 0) {
231
+ throw {
232
+ error: "getThreadList: Invalid response data - resData is not a valid array",
233
+ res: resData
234
+ };
235
+ }
236
+
237
+ // Validate last element exists and has required properties
238
+ const lastElement = resData[resData.length - 1];
239
+ if (!lastElement || typeof lastElement !== "object") {
240
+ throw {
241
+ error: "getThreadList: Invalid response data - last element is missing or invalid",
242
+ res: resData
243
+ };
244
+ }
245
+
246
+ if (lastElement.error_results > 0) {
247
+ // Check if first element and o0 exist before accessing errors
248
+ if (resData[0] && resData[0].o0 && resData[0].o0.errors) {
249
+ throw resData[0].o0.errors;
250
+ } else {
251
+ throw {
252
+ error: "getThreadList: Error results > 0 but error details not available",
253
+ res: resData
254
+ };
255
+ }
256
+ }
257
+
258
+ if (lastElement.successful_results === 0) {
259
+ throw {
260
+ error: "getThreadList: there was no successful_results",
261
+ res: resData
262
+ };
263
+ }
264
+
265
+ // Validate first element and nested data structure
266
+ if (!resData[0] || !resData[0].o0 || !resData[0].o0.data ||
267
+ !resData[0].o0.data.viewer || !resData[0].o0.data.viewer.message_threads ||
268
+ !Array.isArray(resData[0].o0.data.viewer.message_threads.nodes)) {
269
+ throw {
270
+ error: "getThreadList: Invalid response data structure - missing required fields",
271
+ res: resData
272
+ };
273
+ }
274
+
275
+ if (timestamp) {
276
+ const nodes = resData[0].o0.data.viewer.message_threads.nodes;
277
+ if (Array.isArray(nodes) && nodes.length > 0) {
278
+ nodes.shift();
279
+ }
280
+ }
281
+
282
+ callback(
283
+ null,
284
+ formatThreadList(resData[0].o0.data.viewer.message_threads.nodes)
285
+ );
286
+ })
287
+ .catch(err => {
288
+ log.error("getThreadList", err);
289
+ return callback(err);
290
+ });
291
+ return returnPromise;
292
+ };
293
+ };
@@ -0,0 +1,43 @@
1
+ // ============================================================
2
+ // AYMAN-FCA v2.0 — Get Thread Pictures
3
+ // © 2025 Ayman. All Rights Reserved.
4
+ // ============================================================
5
+ "use strict";
6
+
7
+ const log = require("../../../func/logAdapter");
8
+ const { parseAndCheckLogin } = require("../../utils/client");
9
+
10
+ module.exports = function(defaultFuncs, api, ctx) {
11
+ return function getThreadPictures(threadID, offset, limit, callback) {
12
+ let resolve, reject;
13
+ const p = new Promise((res, rej) => { resolve = res; reject = rej; });
14
+ callback = callback || ((err, data) => err ? reject(err) : resolve(data));
15
+
16
+ defaultFuncs.post(
17
+ "https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
18
+ ctx.jar,
19
+ { thread_id: threadID, offset, limit }
20
+ )
21
+ .then(parseAndCheckLogin(ctx, defaultFuncs))
22
+ .then(res => {
23
+ if (res.error) throw res;
24
+ return Promise.all(
25
+ (res.payload?.imagesData || []).map(image =>
26
+ defaultFuncs.post(
27
+ "https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
28
+ ctx.jar, { thread_id: threadID, image_id: image.fbid }
29
+ )
30
+ .then(parseAndCheckLogin(ctx, defaultFuncs))
31
+ .then(r => {
32
+ const qID = r.jsmods?.require?.[0]?.[3]?.[1]?.query_metadata?.query_path?.[0]?.message_thread;
33
+ return r.jsmods?.require?.[0]?.[3]?.[1]?.query_results?.[qID]?.message_images?.edges?.[0]?.node?.image2;
34
+ })
35
+ )
36
+ );
37
+ })
38
+ .then(data => callback(null, data))
39
+ .catch(err => { log.error("getThreadPictures", err); callback(err); });
40
+
41
+ return p;
42
+ };
43
+ };
package/src/api/user/J ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,48 @@
1
+ // ============================================================
2
+ // AYMAN-FCA v2.0 — Get User ID
3
+ // © 2025 Ayman. All Rights Reserved.
4
+ // ============================================================
5
+ "use strict";
6
+
7
+ const log = require("../../../func/logAdapter");
8
+ const { formatID } = require("../../utils/format");
9
+ const { parseAndCheckLogin } = require("../../utils/client");
10
+
11
+ function formatData(data) {
12
+ return {
13
+ userID: formatID(String(data.uid)),
14
+ photoUrl: data.photo,
15
+ indexRank: data.index_rank,
16
+ name: data.text,
17
+ isVerified: data.is_verified,
18
+ profileUrl: data.path,
19
+ category: data.category,
20
+ score: data.score,
21
+ type: data.type
22
+ };
23
+ }
24
+
25
+ module.exports = function(defaultFuncs, api, ctx) {
26
+ return function getUserID(name, callback) {
27
+ let resolve, reject;
28
+ const p = new Promise((res, rej) => { resolve = res; reject = rej; });
29
+ callback = callback || ((err, data) => err ? reject(err) : resolve(data));
30
+
31
+ defaultFuncs.get("https://www.facebook.com/ajax/typeahead/search.php", ctx.jar, {
32
+ value: name.toLowerCase(),
33
+ viewer: ctx.userID,
34
+ rsp: "search",
35
+ context: "search",
36
+ path: "/home.php",
37
+ request_id: ctx.clientId
38
+ })
39
+ .then(parseAndCheckLogin(ctx, defaultFuncs))
40
+ .then(res => {
41
+ if (res.error) throw res;
42
+ callback(null, (res.payload?.entries || []).map(formatData));
43
+ })
44
+ .catch(err => { log.error("getUserID", err); callback(err); });
45
+
46
+ return p;
47
+ };
48
+ };