@dongdev/fca-unofficial 3.0.29 → 3.0.30

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 (61) hide show
  1. package/CHANGELOG.md +229 -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 +88 -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/postFormData.js +1 -1
  17. package/src/api/messaging/changeArchivedStatus.js +1 -1
  18. package/src/api/messaging/changeBlockedStatus.js +1 -1
  19. package/src/api/messaging/changeGroupImage.js +1 -1
  20. package/src/api/messaging/changeNickname.js +1 -1
  21. package/src/api/messaging/changeThreadEmoji.js +1 -1
  22. package/src/api/messaging/createNewGroup.js +1 -1
  23. package/src/api/messaging/createThemeAI.js +1 -1
  24. package/src/api/messaging/deleteMessage.js +1 -1
  25. package/src/api/messaging/deleteThread.js +1 -1
  26. package/src/api/messaging/getFriendsList.js +1 -1
  27. package/src/api/messaging/getMessage.js +1 -1
  28. package/src/api/messaging/getThemePictures.js +1 -1
  29. package/src/api/messaging/handleMessageRequest.js +1 -1
  30. package/src/api/messaging/markAsDelivered.js +1 -1
  31. package/src/api/messaging/markAsRead.js +1 -1
  32. package/src/api/messaging/markAsReadAll.js +1 -1
  33. package/src/api/messaging/markAsSeen.js +1 -1
  34. package/src/api/messaging/muteThread.js +1 -1
  35. package/src/api/messaging/resolvePhotoUrl.js +1 -1
  36. package/src/api/messaging/sendMessage.js +1 -1
  37. package/src/api/messaging/setTitle.js +1 -1
  38. package/src/api/messaging/unsendMessage.js +1 -1
  39. package/src/api/messaging/uploadAttachment.js +1 -1
  40. package/src/api/socket/core/connectMqtt.js +16 -8
  41. package/src/api/socket/core/emitAuth.js +4 -0
  42. package/src/api/socket/core/getSeqID.js +6 -8
  43. package/src/api/socket/core/getTaskResponseData.js +3 -0
  44. package/src/api/socket/core/parseDelta.js +9 -0
  45. package/src/api/socket/detail/buildStream.js +11 -4
  46. package/src/api/socket/detail/constants.js +4 -0
  47. package/src/api/socket/listenMqtt.js +11 -5
  48. package/src/api/threads/getThreadHistory.js +1 -1
  49. package/src/api/threads/getThreadInfo.js +245 -388
  50. package/src/api/threads/getThreadList.js +1 -1
  51. package/src/api/threads/getThreadPictures.js +1 -1
  52. package/src/api/users/getUserID.js +1 -1
  53. package/src/api/users/getUserInfo.js +80 -8
  54. package/src/database/models/thread.js +5 -0
  55. package/src/remote/remoteClient.js +123 -0
  56. package/src/utils/broadcast.js +51 -0
  57. package/src/utils/loginParser.js +19 -1
  58. package/src/utils/request.js +33 -6
  59. package/.gitattributes +0 -2
  60. package/Fca_Database/database.sqlite +0 -0
  61. package/LICENSE-MIT +0 -21
@@ -1,438 +1,295 @@
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
+ acc[path.basename(file, ".js")] = require(path.join(__dirname, "../../database", file))(api);
141
+ return acc;
142
+ }, {});
143
+
144
+ const { threadData } = dbFiles;
145
+ const { create, get, update } = threadData || {};
146
+ const FRESH_MS = 10 * 60 * 1000;
147
+ return function getThreadInfo(threadID, callback) {
148
+ let resolveFunc;
149
+ let rejectFunc;
150
+
151
+ const returnPromise = new Promise((resolve, reject) => {
152
+ resolveFunc = resolve;
153
+ rejectFunc = reject;
154
+ });
155
+
156
+ if (typeof callback !== "function") {
157
+ callback = (err, data) => {
158
+ if (err) {
159
+ return rejectFunc(err);
160
+ }
161
+ return resolveFunc(data);
162
+ };
163
+ }
164
+
165
+ const threadIDs = Array.isArray(threadID) ? threadID.map(String) : [String(threadID)];
166
+
167
+ const now = Date.now();
168
+
169
+ const loadFromDb = async ids => {
170
+ if (!threadData || typeof get !== "function") return { fresh: {}, stale: ids };
171
+ const fresh = {};
172
+ const stale = [];
173
+ const rows = await Promise.all(ids.map(id => get(id).catch(() => null)));
174
+ for (let i = 0; i < ids.length; i++) {
175
+ const id = ids[i];
176
+ const row = rows[i];
177
+ if (row && row.data) {
178
+ const updatedAt = row.updatedAt ? new Date(row.updatedAt).getTime() : 0;
179
+ if (updatedAt && now - updatedAt <= FRESH_MS) {
180
+ fresh[id] = row.data;
181
+ } else {
182
+ stale.push(id);
183
+ }
184
+ } else {
185
+ stale.push(id);
186
+ }
187
+ }
188
+ return { fresh, stale };
189
+ };
190
+
191
+ const fetchFromGraphQL = async ids => {
192
+ if (!ids.length) return {};
193
+ const queries = {};
194
+ ids.forEach((t, i) => {
195
+ queries["o" + i] = {
132
196
  doc_id: "3449967031715030",
133
197
  query_params: {
134
- id: x,
198
+ id: t,
135
199
  message_limit: 0,
136
200
  load_messages: false,
137
201
  load_read_receipts: false,
138
- before: null,
139
- },
202
+ before: null
203
+ }
140
204
  };
141
205
  });
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
206
 
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
- }
207
+ const form = {
208
+ queries: JSON.stringify(queries),
209
+ batch_name: "MessengerGraphQLThreadFetcher"
210
+ };
206
211
 
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
- }
212
+ const resData = await defaultFuncs
213
+ .post(
214
+ "https://www.facebook.com/api/graphqlbatch/",
215
+ ctx.jar,
216
+ form
217
+ )
218
+ .then(parseAndCheckLogin(ctx, defaultFuncs));
217
219
 
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;
220
+ if (resData.error) {
221
+ throw resData;
225
222
  }
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
223
 
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));
224
+ const out = {};
225
+ for (let i = resData.length - 2; i >= 0; i--) {
226
+ const res = resData[i];
227
+ const oKey = Object.keys(res)[0];
228
+ const responseData = res[oKey];
229
+ try {
230
+ const info = formatThreadGraphQLResponse(responseData.data);
231
+ if (info && info.threadID) {
232
+ out[info.threadID] = info;
233
+ }
234
+ } catch (e) {
235
+ // Skip malformed entries but continue processing others
236
+ log.error("getThreadInfoGraphQL", e && e.message ? e.message : String(e));
252
237
  }
253
238
  }
254
- } catch (err) {
255
- logger(`checkAndUpdateThreads error: ${err?.message || err}`, "error");
256
- }
257
- }
239
+ return out;
240
+ };
258
241
 
259
- async function processQueue() {
260
- if (isProcessingQueue) return;
261
- isProcessingQueue = true;
262
- while (queue.length > 0) {
263
- const task = queue.shift();
242
+ (async () => {
264
243
  try {
265
- await task();
266
- } catch (err) {
267
- logger(`Queue processing error: ${err?.message || err}`, "error");
268
- }
269
- }
270
- isProcessingQueue = false;
271
- }
244
+ const { fresh, stale } = await loadFromDb(threadIDs);
245
+ let fetched = {};
272
246
 
273
- if (!loopStarted) {
274
- loopStarted = true;
275
- setInterval(() => {
276
- checkAndUpdateThreads();
277
- processQueue();
278
- }, 10000);
279
- }
247
+ if (stale.length) {
248
+ fetched = await fetchFromGraphQL(stale);
280
249
 
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;
250
+ // Persist fetched data back to DB
251
+ if (threadData && (typeof create === "function" || typeof update === "function")) {
252
+ const tasks = [];
253
+ for (const id of stale) {
254
+ const info = fetched[id];
255
+ if (!info) continue;
256
+ const payload = { data: info };
257
+ if (typeof update === "function") {
258
+ tasks.push(update(id, payload).catch(() => null));
259
+ } else if (typeof create === "function") {
260
+ tasks.push(create(id, payload).catch(() => null));
261
+ }
262
+ }
263
+ if (tasks.length) {
264
+ try {
265
+ await Promise.all(tasks);
266
+ } catch {
267
+ // Swallow DB errors not critical for API behavior
268
+ }
269
+ }
270
+ }
304
271
  }
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
272
 
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
- }
273
+ const resultMap = {};
274
+ for (const id of threadIDs) {
275
+ resultMap[id] = fresh[id] || fetched[id] || null;
276
+ }
361
277
 
362
- if (inflight.has(tid)) {
363
- inflight.get(tid).then(data => callback(null, data)).catch(err => callback(err));
364
- return returnPromise;
365
- }
278
+ const result = Array.isArray(threadID)
279
+ ? resultMap
280
+ : resultMap[threadIDs[0]] || null;
366
281
 
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
- });
282
+ return callback(null, result);
283
+ } catch (err) {
284
+ // Horizon-style anti-get-info message to hint possible spam/limit
285
+ log.error(
286
+ "getThreadInfoGraphQL",
287
+ "Lỗi: getThreadInfoGraphQL Có Thể Do Bạn Spam Quá Nhiều, Hãy Thử Lại !"
288
+ );
289
+ return callback(err);
290
+ }
291
+ })();
430
292
 
431
- inflight.set(tid, p);
432
- p.then(data => callback(null, data)).catch(err => callback(err));
433
- } catch (err) {
434
- callback(err);
435
- }
436
293
  return returnPromise;
437
294
  };
438
295
  };