@dongdev/fca-unofficial 3.0.29 → 3.0.31

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 (100) hide show
  1. package/CHANGELOG.md +232 -132
  2. package/DOCS.md +82 -3
  3. package/README.md +524 -632
  4. package/func/logAdapter.js +33 -0
  5. package/index.d.ts +6 -0
  6. package/module/config.js +11 -1
  7. package/module/loginHelper.js +63 -4
  8. package/package.json +89 -81
  9. package/src/api/action/changeAvatar.js +1 -1
  10. package/src/api/action/changeBio.js +1 -1
  11. package/src/api/action/handleFriendRequest.js +1 -1
  12. package/src/api/action/logout.js +1 -1
  13. package/src/api/action/refreshFb_dtsg.js +1 -1
  14. package/src/api/action/setPostReaction.js +1 -1
  15. package/src/api/action/unfriend.js +1 -1
  16. package/src/api/http/httpGet.js +2 -2
  17. package/src/api/http/postFormData.js +1 -1
  18. package/src/api/messaging/addUserToGroup.js +1 -1
  19. package/src/api/messaging/changeArchivedStatus.js +1 -1
  20. package/src/api/messaging/changeBlockedStatus.js +1 -1
  21. package/src/api/messaging/changeGroupImage.js +2 -2
  22. package/src/api/messaging/changeNickname.js +2 -2
  23. package/src/api/messaging/changeThreadColor.js +1 -1
  24. package/src/api/messaging/changeThreadEmoji.js +1 -1
  25. package/src/api/messaging/createNewGroup.js +1 -1
  26. package/src/api/messaging/createThemeAI.js +1 -1
  27. package/src/api/messaging/deleteMessage.js +1 -1
  28. package/src/api/messaging/deleteThread.js +1 -1
  29. package/src/api/messaging/editMessage.js +1 -1
  30. package/src/api/messaging/getFriendsList.js +1 -1
  31. package/src/api/messaging/getMessage.js +1 -1
  32. package/src/api/messaging/getThemePictures.js +1 -1
  33. package/src/api/messaging/handleMessageRequest.js +1 -1
  34. package/src/api/messaging/markAsDelivered.js +1 -1
  35. package/src/api/messaging/markAsRead.js +1 -1
  36. package/src/api/messaging/markAsReadAll.js +1 -1
  37. package/src/api/messaging/markAsSeen.js +1 -1
  38. package/src/api/messaging/muteThread.js +1 -1
  39. package/src/api/messaging/resolvePhotoUrl.js +1 -1
  40. package/src/api/messaging/searchForThread.js +2 -1
  41. package/src/api/messaging/sendMessage.js +1 -1
  42. package/src/api/messaging/sendTypingIndicator.js +1 -1
  43. package/src/api/messaging/setMessageReaction.js +3 -4
  44. package/src/api/messaging/setTitle.js +1 -1
  45. package/src/api/messaging/unsendMessage.js +2 -2
  46. package/src/api/messaging/uploadAttachment.js +1 -1
  47. package/src/api/socket/core/connectMqtt.js +16 -8
  48. package/src/api/socket/core/emitAuth.js +4 -0
  49. package/src/api/socket/core/getSeqID.js +6 -8
  50. package/src/api/socket/core/getTaskResponseData.js +3 -0
  51. package/src/api/socket/core/parseDelta.js +9 -0
  52. package/src/api/socket/detail/buildStream.js +11 -4
  53. package/src/api/socket/detail/constants.js +4 -0
  54. package/src/api/socket/listenMqtt.js +11 -5
  55. package/src/api/threads/getThreadHistory.js +1 -1
  56. package/src/api/threads/getThreadInfo.js +246 -388
  57. package/src/api/threads/getThreadList.js +1 -1
  58. package/src/api/threads/getThreadPictures.js +1 -1
  59. package/src/api/users/getUserID.js +1 -1
  60. package/src/api/users/getUserInfo.js +87 -12
  61. package/src/database/helpers.js +53 -0
  62. package/src/database/models/index.js +2 -1
  63. package/src/database/models/thread.js +5 -0
  64. package/src/database/threadData.js +49 -53
  65. package/src/database/userData.js +46 -37
  66. package/src/remote/remoteClient.js +123 -0
  67. package/src/utils/broadcast.js +51 -0
  68. package/src/utils/format/attachment.js +357 -0
  69. package/src/utils/format/cookie.js +9 -0
  70. package/src/utils/format/date.js +50 -0
  71. package/src/utils/format/decode.js +44 -0
  72. package/src/utils/format/delta.js +194 -0
  73. package/src/utils/format/ids.js +64 -0
  74. package/src/utils/format/index.js +64 -0
  75. package/src/utils/format/message.js +88 -0
  76. package/src/utils/format/presence.js +132 -0
  77. package/src/utils/format/readTyp.js +44 -0
  78. package/src/utils/format/thread.js +42 -0
  79. package/src/utils/format/utils.js +141 -0
  80. package/src/utils/loginParser/autoLogin.js +125 -0
  81. package/src/utils/loginParser/helpers.js +43 -0
  82. package/src/utils/loginParser/index.js +10 -0
  83. package/src/utils/loginParser/parseAndCheckLogin.js +220 -0
  84. package/src/utils/loginParser/textUtils.js +28 -0
  85. package/src/utils/request/client.js +26 -0
  86. package/src/utils/request/config.js +23 -0
  87. package/src/utils/request/defaults.js +46 -0
  88. package/src/utils/request/helpers.js +46 -0
  89. package/src/utils/request/index.js +17 -0
  90. package/src/utils/request/methods.js +163 -0
  91. package/src/utils/request/proxy.js +21 -0
  92. package/src/utils/request/retry.js +77 -0
  93. package/src/utils/request/sanitize.js +49 -0
  94. package/.gitattributes +0 -2
  95. package/Fca_Database/database.sqlite +0 -0
  96. package/LICENSE-MIT +0 -21
  97. package/src/utils/format.js +0 -1174
  98. package/src/utils/loginParser.js +0 -347
  99. package/src/utils/messageFormat.js +0 -1173
  100. package/src/utils/request.js +0 -305
@@ -1,438 +1,296 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const logger = require("../../../func/logger");
4
- const { parseAndCheckLogin } = require("../../utils/client");
5
- const { formatID, getType } = require("../../utils/format");
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');
6
7
 
7
8
  function formatEventReminders(reminder) {
8
9
  return {
9
- reminderID: reminder?.id,
10
- eventCreatorID: reminder?.lightweight_event_creator?.id,
11
- time: reminder?.time,
12
- eventType: String(reminder?.lightweight_event_type || "").toLowerCase(),
13
- locationName: reminder?.location_name,
14
- locationCoordinates: reminder?.location_coordinates,
15
- locationPage: reminder?.location_page,
16
- eventStatus: String(reminder?.lightweight_event_status || "").toLowerCase(),
17
- note: reminder?.note,
18
- repeatMode: String(reminder?.repeat_mode || "").toLowerCase(),
19
- eventTitle: reminder?.event_title,
20
- triggerMessage: reminder?.trigger_message,
21
- secondsToNotifyBefore: reminder?.seconds_to_notify_before,
22
- allowsRsvp: reminder?.allows_rsvp,
23
- relatedEvent: reminder?.related_event,
24
- members: Array.isArray(reminder?.event_reminder_members?.edges) ? reminder.event_reminder_members.edges.map(m => ({
25
- memberID: m?.node?.id,
26
- state: String(m?.guest_list_state || "").toLowerCase(),
27
- })) : [],
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
+ })),
28
29
  };
29
30
  }
30
31
 
31
32
  function formatThreadGraphQLResponse(data) {
32
- if (!data) return null;
33
- if (data?.errors) return null;
34
- const t = data.message_thread;
35
- if (!t) return null;
36
- const threadID = t?.thread_key?.thread_fbid || t?.thread_key?.other_user_id || null;
37
- const lastM = t?.last_message;
38
- const lastNode = Array.isArray(lastM?.nodes) && lastM.nodes[0] ? lastM.nodes[0] : null;
39
- const snippetID = lastNode?.message_sender?.messaging_actor?.id || null;
40
- const snippetText = lastNode?.snippet || null;
41
- const lastRNode = Array.isArray(t?.last_read_receipt?.nodes) && t.last_read_receipt.nodes[0] ? t.last_read_receipt.nodes[0] : null;
42
- const lastReadTimestamp = lastRNode?.timestamp_precise || null;
43
- const participants = Array.isArray(t?.all_participants?.edges) ? t.all_participants.edges : [];
44
- const approvals = Array.isArray(t?.group_approval_queue?.nodes) ? t.group_approval_queue.nodes : [];
45
- const customInfo = t?.customization_info || {};
46
- const bubble = customInfo?.outgoing_bubble_color;
47
- const participantCustoms = Array.isArray(customInfo?.participant_customizations) ? customInfo.participant_customizations : [];
48
- const nicknames = participantCustoms.reduce((res, val) => {
49
- if (val?.nickname && val?.participant_id) res[val.participant_id] = val.nickname;
50
- return res;
51
- }, {});
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
+
52
55
  return {
53
56
  threadID,
54
- threadName: t?.name || null,
55
- participantIDs: participants.map(d => d?.node?.messaging_actor?.id).filter(Boolean),
56
- userInfo: participants.map(d => ({
57
- id: d?.node?.messaging_actor?.id || null,
58
- name: d?.node?.messaging_actor?.name || null,
59
- firstName: d?.node?.messaging_actor?.short_name || null,
60
- vanity: d?.node?.messaging_actor?.username || null,
61
- url: d?.node?.messaging_actor?.url || null,
62
- thumbSrc: d?.node?.messaging_actor?.big_image_src?.uri || null,
63
- profileUrl: d?.node?.messaging_actor?.big_image_src?.uri || null,
64
- gender: d?.node?.messaging_actor?.gender || null,
65
- type: d?.node?.messaging_actor?.__typename || null,
66
- isFriend: !!d?.node?.messaging_actor?.is_viewer_friend,
67
- isBirthday: !!d?.node?.messaging_actor?.is_birthday,
68
- })),
69
- unreadCount: t?.unread_count ?? 0,
70
- messageCount: t?.messages_count ?? 0,
71
- timestamp: t?.updated_time_precise || null,
72
- muteUntil: t?.mute_until || null,
73
- isGroup: t?.thread_type === "GROUP",
74
- isSubscribed: !!t?.is_viewer_subscribed,
75
- isArchived: !!t?.has_viewer_archived,
76
- folder: t?.folder || null,
77
- cannotReplyReason: t?.cannot_reply_reason || null,
78
- eventReminders: Array.isArray(t?.event_reminders?.nodes) ? t.event_reminders.nodes.map(formatEventReminders) : [],
79
- emoji: customInfo?.emoji || null,
80
- color: bubble ? String(bubble).slice(2) : null,
81
- threadTheme: t?.thread_theme || null,
82
- nicknames,
83
- adminIDs: Array.isArray(t?.thread_admins) ? t.thread_admins : [],
84
- approvalMode: !!t?.approval_mode,
85
- approvalQueue: approvals.map(a => ({
86
- inviterID: a?.inviter?.id || null,
87
- requesterID: a?.requester?.id || null,
88
- timestamp: a?.request_timestamp || null,
89
- request_source: a?.request_source || null,
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,
90
73
  })),
91
- reactionsMuteMode: String(t?.reactions_mute_mode || "").toLowerCase(),
92
- mentionsMuteMode: String(t?.mentions_mute_mode || "").toLowerCase(),
93
- isPinProtected: !!t?.is_pin_protected,
94
- relatedPageThread: t?.related_page_thread || null,
95
- name: t?.name || null,
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,
96
113
  snippet: snippetText,
97
114
  snippetSender: snippetID,
98
115
  snippetAttachments: [],
99
- serverTimestamp: t?.updated_time_precise || null,
100
- imageSrc: t?.image?.uri || null,
101
- isCanonicalUser: !!t?.is_canonical_neo_user,
102
- isCanonical: t?.thread_type !== "GROUP",
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",
103
120
  recipientsLoadable: true,
104
121
  hasEmailParticipant: false,
105
122
  readOnly: false,
106
- canReply: t?.cannot_reply_reason == null,
107
- lastMessageTimestamp: t?.last_message ? t.last_message.timestamp_precise : null,
123
+ canReply: messageThread.cannot_reply_reason == null,
124
+ lastMessageTimestamp:
125
+ messageThread.last_message?.timestamp_precise || null,
108
126
  lastMessageType: "message",
109
127
  lastReadTimestamp,
110
- threadType: t?.thread_type === "GROUP" ? 2 : 1,
128
+ threadType: messageThread.thread_type === "GROUP" ? 2 : 1,
111
129
  inviteLink: {
112
- enable: t?.joinable_mode ? t.joinable_mode.mode == 1 : false,
113
- link: t?.joinable_mode ? t.joinable_mode.link : null,
130
+ enable: messageThread.joinable_mode?.mode === 1,
131
+ link: messageThread.joinable_mode?.link || null,
114
132
  },
115
133
  };
116
134
  }
117
135
 
118
- const queue = [];
119
- let isProcessingQueue = false;
120
- const processingThreads = new Set();
121
- const queuedThreads = new Set();
122
- const cooldown = new Map();
123
- const inflight = new Map();
124
- let loopStarted = false;
125
-
126
136
  module.exports = function (defaultFuncs, api, ctx) {
127
- const getMultiInfo = async function (threadIDs) {
128
- const buildQueries = () => {
129
- const form = {};
130
- threadIDs.forEach((x, y) => {
131
- form["o" + y] = {
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] = {
132
197
  doc_id: "3449967031715030",
133
198
  query_params: {
134
- id: x,
199
+ id: t,
135
200
  message_limit: 0,
136
201
  load_messages: false,
137
202
  load_read_receipts: false,
138
- before: null,
139
- },
203
+ before: null
204
+ }
140
205
  };
141
206
  });
142
- return {
143
- queries: JSON.stringify(form),
144
- batch_name: "MessengerGraphQLThreadFetcher",
145
- };
146
- };
147
- const maxAttempts = 3;
148
- let lastErr = null;
149
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
150
- try {
151
- const Submit = buildQueries();
152
- const resData = await defaultFuncs.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, Submit).then(parseAndCheckLogin(ctx, defaultFuncs));
153
- if (!Array.isArray(resData) || resData.length === 0) throw new Error("EmptyGraphBatch");
154
- const tail = resData[resData.length - 1];
155
- if (tail?.error_results && tail.error_results !== 0) throw new Error("GraphErrorResults");
156
- const body = resData.slice(0, -1).sort((a, b) => Object.keys(a)[0].localeCompare(Object.keys(b)[0]));
157
- const out = [];
158
- body.forEach((x, y) => out.push(formatThreadGraphQLResponse(x["o" + y]?.data)));
159
- const valid = out.some(d => !!d && !!d.threadID);
160
- if (!valid) throw new Error("GraphNoData");
161
- return { Success: true, Data: out };
162
- } catch (e) {
163
- lastErr = e;
164
- if (attempt < maxAttempts) await new Promise(r => setTimeout(r, 300 * attempt));
165
- }
166
- }
167
- return { Success: false, Data: null, Error: lastErr ? String(lastErr.message || lastErr) : "Unknown" };
168
- };
169
-
170
- const dbFiles = fs.readdirSync(path.join(__dirname, "../../database")).filter(f => path.extname(f) === ".js").reduce((acc, file) => {
171
- acc[path.basename(file, ".js")] = require(path.join(__dirname, "../../database", file))(api);
172
- return acc;
173
- }, {});
174
- const { threadData, userData } = dbFiles;
175
- const { create, get, update, getAll } = threadData;
176
- const { update: updateUser } = userData;
177
207
 
178
- function isValidThread(d) {
179
- return d && d.threadID;
180
- }
181
-
182
- async function upsertUsersFromThreadInfo(info) {
183
- try {
184
- if (!info || !Array.isArray(info.userInfo) || info.userInfo.length === 0) return;
185
- const tasks = info.userInfo.filter(u => u && u.id).map(u => {
186
- const data = {
187
- id: u.id,
188
- name: u.name || null,
189
- firstName: u.firstName || null,
190
- vanity: u.vanity || null,
191
- url: u.url || null,
192
- thumbSrc: u.thumbSrc || null,
193
- profileUrl: u.profileUrl || null,
194
- gender: u.gender || null,
195
- type: u.type || null,
196
- isFriend: !!u.isFriend,
197
- isBirthday: !!u.isBirthday,
198
- };
199
- return updateUser(u.id, { data });
200
- });
201
- await Promise.allSettled(tasks);
202
- } catch (e) {
203
- logger(`upsertUsers error: ${e?.message || e}`, "warn");
204
- }
205
- }
208
+ const form = {
209
+ queries: JSON.stringify(queries),
210
+ batch_name: "MessengerGraphQLThreadFetcher"
211
+ };
206
212
 
207
- async function createOrUpdateThread(id, data) {
208
- const existing = await get(id);
209
- if (existing) {
210
- await update(id, { data });
211
- return "update";
212
- } else {
213
- await create(id, { data });
214
- return "create";
215
- }
216
- }
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));
217
220
 
218
- async function fetchThreadInfo(tID, isNew) {
219
- try {
220
- const response = await getMultiInfo([tID]);
221
- if (!response.Success || !response.Data || !isValidThread(response.Data[0])) {
222
- cooldown.set(tID, Date.now() + 5 * 60 * 1000);
223
- logger(`GraphQL empty for ${tID}, cooldown applied`, "warn");
224
- return;
221
+ if (resData.error) {
222
+ throw resData;
225
223
  }
226
- const threadInfo = response.Data[0];
227
- await upsertUsersFromThreadInfo(threadInfo);
228
- const op = await createOrUpdateThread(tID, threadInfo);
229
- logger(`${op === "create" ? "Success create data thread" : "Success update data thread"}: ${tID}`, "info");
230
- } catch (err) {
231
- cooldown.set(tID, Date.now() + 5 * 60 * 1000);
232
- logger(`fetchThreadInfo error ${tID}: ${err?.message || err}`, "error");
233
- } finally {
234
- queuedThreads.delete(tID);
235
- }
236
- }
237
224
 
238
- async function checkAndUpdateThreads() {
239
- try {
240
- const allThreads = await getAll("threadID");
241
- const existingThreadIDs = new Set(allThreads.map(t => t.threadID));
242
- const now = Date.now();
243
- for (const t of existingThreadIDs) {
244
- const cd = cooldown.get(t);
245
- if (cd && now < cd) continue;
246
- const result = await get(t);
247
- if (!result) continue;
248
- const lastUpdated = new Date(result.updatedAt).getTime();
249
- if ((now - lastUpdated) / (1000 * 60) > 10 && !queuedThreads.has(t)) {
250
- queuedThreads.add(t);
251
- queue.push(() => fetchThreadInfo(t, false));
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));
252
238
  }
253
239
  }
254
- } catch (err) {
255
- logger(`checkAndUpdateThreads error: ${err?.message || err}`, "error");
256
- }
257
- }
240
+ return out;
241
+ };
258
242
 
259
- async function processQueue() {
260
- if (isProcessingQueue) return;
261
- isProcessingQueue = true;
262
- while (queue.length > 0) {
263
- const task = queue.shift();
243
+ (async () => {
264
244
  try {
265
- await task();
266
- } catch (err) {
267
- logger(`Queue processing error: ${err?.message || err}`, "error");
268
- }
269
- }
270
- isProcessingQueue = false;
271
- }
245
+ const { fresh, stale } = await loadFromDb(threadIDs);
246
+ let fetched = {};
272
247
 
273
- if (!loopStarted) {
274
- loopStarted = true;
275
- setInterval(() => {
276
- checkAndUpdateThreads();
277
- processQueue();
278
- }, 10000);
279
- }
248
+ if (stale.length) {
249
+ fetched = await fetchFromGraphQL(stale);
280
250
 
281
- return async function getThreadInfoGraphQL(threadID, callback) {
282
- let resolveFunc = function () { };
283
- let rejectFunc = function () { };
284
- const returnPromise = new Promise(function (resolve, reject) {
285
- resolveFunc = resolve;
286
- rejectFunc = reject;
287
- });
288
- if (getType(callback) != "Function" && getType(callback) != "AsyncFunction") {
289
- callback = function (err, data) {
290
- if (err) return rejectFunc(err);
291
- resolveFunc(data);
292
- };
293
- }
294
- if (getType(threadID) !== "Array") threadID = [threadID];
295
- const tid = String(threadID[0]);
296
- try {
297
- const cd = cooldown.get(tid);
298
- if (cd && Date.now() < cd) {
299
- const cachedCd = await get(tid);
300
- if (cachedCd?.data && isValidThread(cachedCd.data)) {
301
- await upsertUsersFromThreadInfo(cachedCd.data);
302
- callback(null, cachedCd.data);
303
- return returnPromise;
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
+ }
304
272
  }
305
- const stub = {
306
- threadID: tid,
307
- threadName: null,
308
- participantIDs: [],
309
- userInfo: [],
310
- unreadCount: 0,
311
- messageCount: 0,
312
- timestamp: null,
313
- muteUntil: null,
314
- isGroup: false,
315
- isSubscribed: false,
316
- isArchived: false,
317
- folder: null,
318
- cannotReplyReason: null,
319
- eventReminders: [],
320
- emoji: null,
321
- color: null,
322
- threadTheme: null,
323
- nicknames: {},
324
- adminIDs: [],
325
- approvalMode: false,
326
- approvalQueue: [],
327
- reactionsMuteMode: "",
328
- mentionsMuteMode: "",
329
- isPinProtected: false,
330
- relatedPageThread: null,
331
- name: null,
332
- snippet: null,
333
- snippetSender: null,
334
- snippetAttachments: [],
335
- serverTimestamp: null,
336
- imageSrc: null,
337
- isCanonicalUser: false,
338
- isCanonical: true,
339
- recipientsLoadable: false,
340
- hasEmailParticipant: false,
341
- readOnly: false,
342
- canReply: false,
343
- lastMessageTimestamp: null,
344
- lastMessageType: "message",
345
- lastReadTimestamp: null,
346
- threadType: 1,
347
- inviteLink: { enable: false, link: null },
348
- __status: "cooldown",
349
- };
350
- await createOrUpdateThread(tid, stub);
351
- callback(null, stub);
352
- return returnPromise;
353
- }
354
273
 
355
- const cached = await get(tid);
356
- if (cached?.data && isValidThread(cached.data)) {
357
- await upsertUsersFromThreadInfo(cached.data);
358
- callback(null, cached.data);
359
- return returnPromise;
360
- }
274
+ const resultMap = {};
275
+ for (const id of threadIDs) {
276
+ resultMap[id] = fresh[id] || fetched[id] || null;
277
+ }
361
278
 
362
- if (inflight.has(tid)) {
363
- inflight.get(tid).then(data => callback(null, data)).catch(err => callback(err));
364
- return returnPromise;
365
- }
279
+ const result = Array.isArray(threadID)
280
+ ? resultMap
281
+ : resultMap[threadIDs[0]] || null;
366
282
 
367
- const p = (async () => {
368
- processingThreads.add(tid);
369
- const response = await getMultiInfo([tid]);
370
- if (response.Success && response.Data && isValidThread(response.Data[0])) {
371
- const data = response.Data[0];
372
- await upsertUsersFromThreadInfo(data);
373
- await createOrUpdateThread(tid, data);
374
- return data;
375
- } else {
376
- const stub = {
377
- threadID: tid,
378
- threadName: null,
379
- participantIDs: [],
380
- userInfo: [],
381
- unreadCount: 0,
382
- messageCount: 0,
383
- timestamp: null,
384
- muteUntil: null,
385
- isGroup: false,
386
- isSubscribed: false,
387
- isArchived: false,
388
- folder: null,
389
- cannotReplyReason: null,
390
- eventReminders: [],
391
- emoji: null,
392
- color: null,
393
- threadTheme: null,
394
- nicknames: {},
395
- adminIDs: [],
396
- approvalMode: false,
397
- approvalQueue: [],
398
- reactionsMuteMode: "",
399
- mentionsMuteMode: "",
400
- isPinProtected: false,
401
- relatedPageThread: null,
402
- name: null,
403
- snippet: null,
404
- snippetSender: null,
405
- snippetAttachments: [],
406
- serverTimestamp: null,
407
- imageSrc: null,
408
- isCanonicalUser: false,
409
- isCanonical: true,
410
- recipientsLoadable: false,
411
- hasEmailParticipant: false,
412
- readOnly: false,
413
- canReply: false,
414
- lastMessageTimestamp: null,
415
- lastMessageType: "message",
416
- lastReadTimestamp: null,
417
- threadType: 1,
418
- inviteLink: { enable: false, link: null },
419
- __status: "unavailable",
420
- };
421
- cooldown.set(tid, Date.now() + 5 * 60 * 1000);
422
- await createOrUpdateThread(tid, stub);
423
- return stub;
424
- }
425
- })()
426
- .finally(() => {
427
- processingThreads.delete(tid);
428
- inflight.delete(tid);
429
- });
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
+ })();
430
293
 
431
- inflight.set(tid, p);
432
- p.then(data => callback(null, data)).catch(err => callback(err));
433
- } catch (err) {
434
- callback(err);
435
- }
436
294
  return returnPromise;
437
295
  };
438
296
  };