@dongdev/fca-unofficial 3.0.28 → 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.
- package/CHANGELOG.md +229 -132
- package/DOCS.md +82 -3
- package/README.md +524 -632
- package/func/logAdapter.js +33 -0
- package/index.d.ts +6 -0
- package/module/config.js +11 -1
- package/module/loginHelper.js +63 -7
- package/package.json +88 -81
- package/src/api/action/changeAvatar.js +1 -1
- package/src/api/action/changeBio.js +1 -1
- package/src/api/action/handleFriendRequest.js +1 -1
- package/src/api/action/logout.js +1 -1
- package/src/api/action/refreshFb_dtsg.js +1 -1
- package/src/api/action/setPostReaction.js +1 -1
- package/src/api/action/unfriend.js +1 -1
- package/src/api/http/postFormData.js +1 -1
- package/src/api/messaging/changeArchivedStatus.js +1 -1
- package/src/api/messaging/changeBlockedStatus.js +1 -1
- package/src/api/messaging/changeGroupImage.js +1 -1
- package/src/api/messaging/changeNickname.js +1 -1
- package/src/api/messaging/changeThreadEmoji.js +1 -1
- package/src/api/messaging/createNewGroup.js +1 -1
- package/src/api/messaging/createThemeAI.js +1 -1
- package/src/api/messaging/deleteMessage.js +1 -1
- package/src/api/messaging/deleteThread.js +1 -1
- package/src/api/messaging/getFriendsList.js +1 -1
- package/src/api/messaging/getMessage.js +1 -1
- package/src/api/messaging/getThemePictures.js +1 -1
- package/src/api/messaging/handleMessageRequest.js +1 -1
- package/src/api/messaging/markAsDelivered.js +1 -1
- package/src/api/messaging/markAsRead.js +1 -1
- package/src/api/messaging/markAsReadAll.js +1 -1
- package/src/api/messaging/markAsSeen.js +1 -1
- package/src/api/messaging/muteThread.js +1 -1
- package/src/api/messaging/resolvePhotoUrl.js +1 -1
- package/src/api/messaging/sendMessage.js +1 -1
- package/src/api/messaging/setTitle.js +1 -1
- package/src/api/messaging/unsendMessage.js +1 -1
- package/src/api/messaging/uploadAttachment.js +1 -1
- package/src/api/socket/core/connectMqtt.js +16 -8
- package/src/api/socket/core/emitAuth.js +4 -0
- package/src/api/socket/core/getSeqID.js +6 -8
- package/src/api/socket/core/getTaskResponseData.js +3 -0
- package/src/api/socket/core/parseDelta.js +9 -0
- package/src/api/socket/detail/buildStream.js +11 -4
- package/src/api/socket/detail/constants.js +4 -0
- package/src/api/socket/listenMqtt.js +11 -5
- package/src/api/threads/getThreadHistory.js +1 -1
- package/src/api/threads/getThreadInfo.js +245 -388
- package/src/api/threads/getThreadList.js +1 -1
- package/src/api/threads/getThreadPictures.js +1 -1
- package/src/api/users/getUserID.js +1 -1
- package/src/api/users/getUserInfo.js +80 -8
- package/src/database/models/thread.js +5 -0
- package/src/remote/remoteClient.js +123 -0
- package/src/utils/broadcast.js +51 -0
- package/src/utils/loginParser.js +19 -1
- package/src/utils/request.js +33 -6
- package/.gitattributes +0 -2
- package/Fca_Database/database.sqlite +0 -0
- package/LICENSE-MIT +0 -21
|
@@ -1,438 +1,295 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const {
|
|
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
|
|
10
|
-
eventCreatorID: reminder
|
|
11
|
-
time: reminder
|
|
12
|
-
eventType:
|
|
13
|
-
locationName: reminder
|
|
14
|
-
locationCoordinates: reminder
|
|
15
|
-
locationPage: reminder
|
|
16
|
-
eventStatus:
|
|
17
|
-
note: reminder
|
|
18
|
-
repeatMode:
|
|
19
|
-
eventTitle: reminder
|
|
20
|
-
triggerMessage: reminder
|
|
21
|
-
secondsToNotifyBefore: reminder
|
|
22
|
-
allowsRsvp: reminder
|
|
23
|
-
relatedEvent: reminder
|
|
24
|
-
members:
|
|
25
|
-
memberID:
|
|
26
|
-
state:
|
|
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 (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
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:
|
|
55
|
-
participantIDs:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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:
|
|
100
|
-
imageSrc:
|
|
101
|
-
isCanonicalUser:
|
|
102
|
-
isCanonical:
|
|
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:
|
|
107
|
-
lastMessageTimestamp:
|
|
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:
|
|
128
|
+
threadType: messageThread.thread_type === "GROUP" ? 2 : 1,
|
|
111
129
|
inviteLink: {
|
|
112
|
-
enable:
|
|
113
|
-
link:
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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:
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
}
|
|
239
|
+
return out;
|
|
240
|
+
};
|
|
258
241
|
|
|
259
|
-
|
|
260
|
-
if (isProcessingQueue) return;
|
|
261
|
-
isProcessingQueue = true;
|
|
262
|
-
while (queue.length > 0) {
|
|
263
|
-
const task = queue.shift();
|
|
242
|
+
(async () => {
|
|
264
243
|
try {
|
|
265
|
-
await
|
|
266
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
setInterval(() => {
|
|
276
|
-
checkAndUpdateThreads();
|
|
277
|
-
processQueue();
|
|
278
|
-
}, 10000);
|
|
279
|
-
}
|
|
247
|
+
if (stale.length) {
|
|
248
|
+
fetched = await fetchFromGraphQL(stale);
|
|
280
249
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
278
|
+
const result = Array.isArray(threadID)
|
|
279
|
+
? resultMap
|
|
280
|
+
: resultMap[threadIDs[0]] || null;
|
|
366
281
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
};
|