stfca 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-MIT +4 -0
- package/README.md +325 -0
- package/index.d.ts +615 -0
- package/index.js +1 -0
- package/module/config.js +33 -0
- package/module/login.js +48 -0
- package/module/loginHelper.js +722 -0
- package/module/options.js +44 -0
- package/package.json +69 -0
- package/src/api/action/addExternalModule.js +25 -0
- package/src/api/action/changeAvatar.js +137 -0
- package/src/api/action/changeBio.js +75 -0
- package/src/api/action/getCurrentUserID.js +7 -0
- package/src/api/action/handleFriendRequest.js +57 -0
- package/src/api/action/logout.js +76 -0
- package/src/api/action/refreshFb_dtsg.js +71 -0
- package/src/api/action/setPostReaction.js +106 -0
- package/src/api/action/unfriend.js +54 -0
- package/src/api/http/httpGet.js +46 -0
- package/src/api/http/httpPost.js +52 -0
- package/src/api/http/postFormData.js +47 -0
- package/src/api/messaging/addUserToGroup.js +68 -0
- package/src/api/messaging/changeAdminStatus.js +122 -0
- package/src/api/messaging/changeArchivedStatus.js +55 -0
- package/src/api/messaging/changeBlockedStatus.js +48 -0
- package/src/api/messaging/changeGroupImage.js +90 -0
- package/src/api/messaging/changeNickname.js +70 -0
- package/src/api/messaging/changeThreadColor.js +79 -0
- package/src/api/messaging/changeThreadEmoji.js +106 -0
- package/src/api/messaging/createNewGroup.js +88 -0
- package/src/api/messaging/createPoll.js +43 -0
- package/src/api/messaging/deleteMessage.js +56 -0
- package/src/api/messaging/deleteThread.js +56 -0
- package/src/api/messaging/editMessage.js +68 -0
- package/src/api/messaging/forwardAttachment.js +51 -0
- package/src/api/messaging/getEmojiUrl.js +29 -0
- package/src/api/messaging/getFriendsList.js +82 -0
- package/src/api/messaging/getMessage.js +829 -0
- package/src/api/messaging/handleMessageRequest.js +65 -0
- package/src/api/messaging/markAsDelivered.js +57 -0
- package/src/api/messaging/markAsRead.js +88 -0
- package/src/api/messaging/markAsReadAll.js +49 -0
- package/src/api/messaging/markAsSeen.js +61 -0
- package/src/api/messaging/muteThread.js +50 -0
- package/src/api/messaging/removeUserFromGroup.js +105 -0
- package/src/api/messaging/resolvePhotoUrl.js +43 -0
- package/src/api/messaging/searchForThread.js +52 -0
- package/src/api/messaging/sendMessage.js +379 -0
- package/src/api/messaging/sendMessageMqtt.js +323 -0
- package/src/api/messaging/sendTypingIndicator.js +67 -0
- package/src/api/messaging/setMessageReaction.js +75 -0
- package/src/api/messaging/setTitle.js +119 -0
- package/src/api/messaging/shareContact.js +49 -0
- package/src/api/messaging/threadColors.js +128 -0
- package/src/api/messaging/unsendMessage.js +81 -0
- package/src/api/messaging/uploadAttachment.js +95 -0
- package/src/api/socket/core/connectMqtt.js +179 -0
- package/src/api/socket/core/getSeqID.js +25 -0
- package/src/api/socket/core/getTaskResponseData.js +22 -0
- package/src/api/socket/core/markDelivery.js +12 -0
- package/src/api/socket/core/parseDelta.js +351 -0
- package/src/api/socket/detail/buildStream.js +208 -0
- package/src/api/socket/detail/constants.js +24 -0
- package/src/api/socket/listenMqtt.js +133 -0
- package/src/api/threads/getThreadHistory.js +664 -0
- package/src/api/threads/getThreadInfo.js +358 -0
- package/src/api/threads/getThreadList.js +248 -0
- package/src/api/threads/getThreadPictures.js +78 -0
- package/src/api/users/getUserID.js +65 -0
- package/src/api/users/getUserInfo.js +319 -0
- package/src/api/users/getUserInfoV2.js +133 -0
- package/src/core/sendReqMqtt.js +63 -0
- package/src/database/models/index.js +49 -0
- package/src/database/models/thread.js +31 -0
- package/src/database/models/user.js +32 -0
- package/src/database/threadData.js +98 -0
- package/src/database/userData.js +89 -0
- package/src/utils/client.js +214 -0
- package/src/utils/constants.js +23 -0
- package/src/utils/format.js +1111 -0
- package/src/utils/headers.js +41 -0
- package/src/utils/request.js +215 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { parseAndCheckLogin } = require("../../utils/client");
|
|
6
|
+
const { formatID, getType } = require("../../utils/format");
|
|
7
|
+
|
|
8
|
+
function formatEventReminders(reminder) {
|
|
9
|
+
return {
|
|
10
|
+
reminderID: reminder?.id,
|
|
11
|
+
eventCreatorID: reminder?.lightweight_event_creator?.id,
|
|
12
|
+
time: reminder?.time,
|
|
13
|
+
eventType: String(reminder?.lightweight_event_type || "").toLowerCase(),
|
|
14
|
+
locationName: reminder?.location_name,
|
|
15
|
+
locationCoordinates: reminder?.location_coordinates,
|
|
16
|
+
locationPage: reminder?.location_page,
|
|
17
|
+
eventStatus: String(reminder?.lightweight_event_status || "").toLowerCase(),
|
|
18
|
+
note: reminder?.note,
|
|
19
|
+
repeatMode: String(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: Array.isArray(reminder?.event_reminder_members?.edges) ? reminder.event_reminder_members.edges.map(m => ({
|
|
26
|
+
memberID: m?.node?.id,
|
|
27
|
+
state: String(m?.guest_list_state || "").toLowerCase(),
|
|
28
|
+
})) : [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatThreadGraphQLResponse(data) {
|
|
33
|
+
if (!data) return null;
|
|
34
|
+
if (data?.errors) return null;
|
|
35
|
+
const t = data.message_thread;
|
|
36
|
+
if (!t) return null;
|
|
37
|
+
const threadID = t?.thread_key?.thread_fbid || t?.thread_key?.other_user_id || null;
|
|
38
|
+
const lastM = t?.last_message;
|
|
39
|
+
const lastNode = Array.isArray(lastM?.nodes) && lastM.nodes[0] ? lastM.nodes[0] : null;
|
|
40
|
+
const snippetID = lastNode?.message_sender?.messaging_actor?.id || null;
|
|
41
|
+
const snippetText = lastNode?.snippet || null;
|
|
42
|
+
const lastRNode = Array.isArray(t?.last_read_receipt?.nodes) && t.last_read_receipt.nodes[0] ? t.last_read_receipt.nodes[0] : null;
|
|
43
|
+
const lastReadTimestamp = lastRNode?.timestamp_precise || null;
|
|
44
|
+
const participants = Array.isArray(t?.all_participants?.edges) ? t.all_participants.edges : [];
|
|
45
|
+
const approvals = Array.isArray(t?.group_approval_queue?.nodes) ? t.group_approval_queue.nodes : [];
|
|
46
|
+
const customInfo = t?.customization_info || {};
|
|
47
|
+
const bubble = customInfo?.outgoing_bubble_color;
|
|
48
|
+
const participantCustoms = Array.isArray(customInfo?.participant_customizations) ? customInfo.participant_customizations : [];
|
|
49
|
+
const nicknames = participantCustoms.reduce((res, val) => {
|
|
50
|
+
if (val?.nickname && val?.participant_id) res[val.participant_id] = val.nickname;
|
|
51
|
+
return res;
|
|
52
|
+
}, {});
|
|
53
|
+
return {
|
|
54
|
+
threadID,
|
|
55
|
+
threadName: t?.name || null,
|
|
56
|
+
participantIDs: participants.map(d => d?.node?.messaging_actor?.id).filter(Boolean),
|
|
57
|
+
userInfo: participants.map(d => ({
|
|
58
|
+
id: d?.node?.messaging_actor?.id || null,
|
|
59
|
+
name: d?.node?.messaging_actor?.name || null,
|
|
60
|
+
firstName: d?.node?.messaging_actor?.short_name || null,
|
|
61
|
+
vanity: d?.node?.messaging_actor?.username || null,
|
|
62
|
+
url: d?.node?.messaging_actor?.url || null,
|
|
63
|
+
thumbSrc: d?.node?.messaging_actor?.big_image_src?.uri || null,
|
|
64
|
+
profileUrl: d?.node?.messaging_actor?.big_image_src?.uri || null,
|
|
65
|
+
gender: d?.node?.messaging_actor?.gender || null,
|
|
66
|
+
type: d?.node?.messaging_actor?.__typename || null,
|
|
67
|
+
isFriend: !!d?.node?.messaging_actor?.is_viewer_friend,
|
|
68
|
+
isBirthday: !!d?.node?.messaging_actor?.is_birthday,
|
|
69
|
+
})),
|
|
70
|
+
unreadCount: t?.unread_count ?? 0,
|
|
71
|
+
messageCount: t?.messages_count ?? 0,
|
|
72
|
+
timestamp: t?.updated_time_precise || null,
|
|
73
|
+
muteUntil: t?.mute_until || null,
|
|
74
|
+
isGroup: t?.thread_type === "GROUP",
|
|
75
|
+
isSubscribed: !!t?.is_viewer_subscribed,
|
|
76
|
+
isArchived: !!t?.has_viewer_archived,
|
|
77
|
+
folder: t?.folder || null,
|
|
78
|
+
cannotReplyReason: t?.cannot_reply_reason || null,
|
|
79
|
+
eventReminders: Array.isArray(t?.event_reminders?.nodes) ? t.event_reminders.nodes.map(formatEventReminders) : [],
|
|
80
|
+
emoji: customInfo?.emoji || null,
|
|
81
|
+
color: bubble ? String(bubble).slice(2) : null,
|
|
82
|
+
threadTheme: t?.thread_theme || null,
|
|
83
|
+
nicknames,
|
|
84
|
+
adminIDs: Array.isArray(t?.thread_admins) ? t.thread_admins : [],
|
|
85
|
+
approvalMode: !!t?.approval_mode,
|
|
86
|
+
approvalQueue: approvals.map(a => ({
|
|
87
|
+
inviterID: a?.inviter?.id || null,
|
|
88
|
+
requesterID: a?.requester?.id || null,
|
|
89
|
+
timestamp: a?.request_timestamp || null,
|
|
90
|
+
request_source: a?.request_source || null,
|
|
91
|
+
})),
|
|
92
|
+
reactionsMuteMode: String(t?.reactions_mute_mode || "").toLowerCase(),
|
|
93
|
+
mentionsMuteMode: String(t?.mentions_mute_mode || "").toLowerCase(),
|
|
94
|
+
isPinProtected: !!t?.is_pin_protected,
|
|
95
|
+
relatedPageThread: t?.related_page_thread || null,
|
|
96
|
+
name: t?.name || null,
|
|
97
|
+
snippet: snippetText,
|
|
98
|
+
snippetSender: snippetID,
|
|
99
|
+
snippetAttachments: [],
|
|
100
|
+
serverTimestamp: t?.updated_time_precise || null,
|
|
101
|
+
imageSrc: t?.image?.uri || null,
|
|
102
|
+
isCanonicalUser: !!t?.is_canonical_neo_user,
|
|
103
|
+
isCanonical: t?.thread_type !== "GROUP",
|
|
104
|
+
recipientsLoadable: true,
|
|
105
|
+
hasEmailParticipant: false,
|
|
106
|
+
readOnly: false,
|
|
107
|
+
canReply: t?.cannot_reply_reason == null,
|
|
108
|
+
lastMessageTimestamp: t?.last_message ? t.last_message.timestamp_precise : null,
|
|
109
|
+
lastMessageType: "message",
|
|
110
|
+
lastReadTimestamp,
|
|
111
|
+
threadType: t?.thread_type === "GROUP" ? 2 : 1,
|
|
112
|
+
inviteLink: {
|
|
113
|
+
enable: t?.joinable_mode ? t.joinable_mode.mode == 1 : false,
|
|
114
|
+
link: t?.joinable_mode ? t.joinable_mode.link : null,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const queue = [];
|
|
120
|
+
let isProcessingQueue = false;
|
|
121
|
+
const processingThreads = new Set();
|
|
122
|
+
const queuedThreads = new Set();
|
|
123
|
+
const cooldown = new Map();
|
|
124
|
+
|
|
125
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
126
|
+
const getMultiInfo = async function (threadIDs) {
|
|
127
|
+
const buildQueries = () => {
|
|
128
|
+
const form = {};
|
|
129
|
+
threadIDs.forEach((x, y) => {
|
|
130
|
+
form["o" + y] = {
|
|
131
|
+
doc_id: "3449967031715030",
|
|
132
|
+
query_params: {
|
|
133
|
+
id: x,
|
|
134
|
+
message_limit: 0,
|
|
135
|
+
load_messages: false,
|
|
136
|
+
load_read_receipts: false,
|
|
137
|
+
before: null,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
queries: JSON.stringify(form),
|
|
143
|
+
batch_name: "MessengerGraphQLThreadFetcher",
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
const maxAttempts = 3;
|
|
147
|
+
let lastErr = null;
|
|
148
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
149
|
+
try {
|
|
150
|
+
const Submit = buildQueries();
|
|
151
|
+
const resData = await defaultFuncs.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, Submit).then(parseAndCheckLogin(ctx, defaultFuncs));
|
|
152
|
+
if (!Array.isArray(resData) || resData.length === 0) throw new Error("EmptyGraphBatch");
|
|
153
|
+
const tail = resData[resData.length - 1];
|
|
154
|
+
if (tail?.error_results && tail.error_results !== 0) throw new Error("GraphErrorResults");
|
|
155
|
+
const body = resData.slice(0, -1).sort((a, b) => Object.keys(a)[0].localeCompare(Object.keys(b)[0]));
|
|
156
|
+
const out = [];
|
|
157
|
+
body.forEach((x, y) => out.push(formatThreadGraphQLResponse(x["o" + y]?.data)));
|
|
158
|
+
const valid = out.some(d => !!d && !!d.threadID);
|
|
159
|
+
if (!valid) throw new Error("GraphNoData");
|
|
160
|
+
return { Success: true, Data: out };
|
|
161
|
+
} catch (e) {
|
|
162
|
+
lastErr = e;
|
|
163
|
+
if (attempt < maxAttempts) await new Promise(r => setTimeout(r, 300 * attempt));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { Success: false, Data: null, Error: lastErr ? String(lastErr.message || lastErr) : "Unknown" };
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const dbFiles = fs.readdirSync(path.join(__dirname, "../../database")).filter(f => path.extname(f) === ".js").reduce((acc, file) => {
|
|
170
|
+
acc[path.basename(file, ".js")] = require(path.join(__dirname, "../../database", file))(api);
|
|
171
|
+
return acc;
|
|
172
|
+
}, {});
|
|
173
|
+
const { threadData, userData } = dbFiles;
|
|
174
|
+
const { create, get, update, getAll } = threadData;
|
|
175
|
+
const { update: updateUser } = userData;
|
|
176
|
+
|
|
177
|
+
function isValidThread(d) {
|
|
178
|
+
return d && d.threadID;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function upsertUsersFromThreadInfo(info) {
|
|
182
|
+
try {
|
|
183
|
+
if (!info || !Array.isArray(info.userInfo) || info.userInfo.length === 0) return;
|
|
184
|
+
const tasks = info.userInfo.filter(u => u && u.id).map(u => {
|
|
185
|
+
const data = {
|
|
186
|
+
id: u.id,
|
|
187
|
+
name: u.name || null,
|
|
188
|
+
firstName: u.firstName || null,
|
|
189
|
+
vanity: u.vanity || null,
|
|
190
|
+
url: u.url || null,
|
|
191
|
+
thumbSrc: u.thumbSrc || null,
|
|
192
|
+
profileUrl: u.profileUrl || null,
|
|
193
|
+
gender: u.gender || null,
|
|
194
|
+
type: u.type || null,
|
|
195
|
+
isFriend: !!u.isFriend,
|
|
196
|
+
isBirthday: !!u.isBirthday,
|
|
197
|
+
};
|
|
198
|
+
return updateUser(u.id, { data });
|
|
199
|
+
});
|
|
200
|
+
await Promise.allSettled(tasks);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
console.warn(`[FCA-WARN] upsertUsers error: ${e?.message || e}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function fetchThreadInfo(tID, isNew) {
|
|
207
|
+
try {
|
|
208
|
+
const response = await getMultiInfo([tID]);
|
|
209
|
+
if (!response.Success || !response.Data || !isValidThread(response.Data[0])) {
|
|
210
|
+
cooldown.set(tID, Date.now() + 5 * 60 * 1000);
|
|
211
|
+
console.warn(`[FCA-WARN] GraphQL empty for ${tID}, cooldown applied`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const threadInfo = response.Data[0];
|
|
215
|
+
await upsertUsersFromThreadInfo(threadInfo);
|
|
216
|
+
if (isNew) {
|
|
217
|
+
await create(tID, { data: threadInfo });
|
|
218
|
+
console.info(`[FCA-INFO] Success create data thread: ${tID}`);
|
|
219
|
+
} else {
|
|
220
|
+
await update(tID, { data: threadInfo });
|
|
221
|
+
console.info(`[FCA-INFO] Success update data thread: ${tID}`);
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
cooldown.set(tID, Date.now() + 5 * 60 * 1000);
|
|
225
|
+
console.error(`[FCA-ERROR] fetchThreadInfo error ${tID}: ${err?.message || err}`);
|
|
226
|
+
} finally {
|
|
227
|
+
queuedThreads.delete(tID);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function checkAndUpdateThreads() {
|
|
232
|
+
try {
|
|
233
|
+
const allThreads = await getAll("threadID");
|
|
234
|
+
const existingThreadIDs = new Set(allThreads.map(t => t.threadID));
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
for (const t of existingThreadIDs) {
|
|
237
|
+
const cd = cooldown.get(t);
|
|
238
|
+
if (cd && now < cd) continue;
|
|
239
|
+
const result = await get(t);
|
|
240
|
+
if (!result) continue;
|
|
241
|
+
const lastUpdated = new Date(result.updatedAt).getTime();
|
|
242
|
+
if ((now - lastUpdated) / (1000 * 60) > 10 && !queuedThreads.has(t)) {
|
|
243
|
+
queuedThreads.add(t);
|
|
244
|
+
console.info(`[FCA-INFO] ThreadID ${t} queued for refresh`);
|
|
245
|
+
queue.push(() => fetchThreadInfo(t, false));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error(`[FCA-ERROR] checkAndUpdateThreads error: ${err?.message || err}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function processQueue() {
|
|
254
|
+
if (isProcessingQueue) return;
|
|
255
|
+
isProcessingQueue = true;
|
|
256
|
+
while (queue.length > 0) {
|
|
257
|
+
const task = queue.shift();
|
|
258
|
+
try {
|
|
259
|
+
await task();
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error(`[FCA-ERROR] Queue processing error: ${err?.message || err}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
isProcessingQueue = false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
setInterval(() => {
|
|
268
|
+
checkAndUpdateThreads();
|
|
269
|
+
processQueue();
|
|
270
|
+
}, 10000);
|
|
271
|
+
|
|
272
|
+
return async function getThreadInfoGraphQL(threadID, callback) {
|
|
273
|
+
let resolveFunc = function () { };
|
|
274
|
+
let rejectFunc = function () { };
|
|
275
|
+
const returnPromise = new Promise(function (resolve, reject) {
|
|
276
|
+
resolveFunc = resolve;
|
|
277
|
+
rejectFunc = reject;
|
|
278
|
+
});
|
|
279
|
+
if (getType(callback) != "Function" && getType(callback) != "AsyncFunction") {
|
|
280
|
+
callback = function (err, data) {
|
|
281
|
+
if (err) return rejectFunc(err);
|
|
282
|
+
resolveFunc(data);
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (getType(threadID) !== "Array") threadID = [threadID];
|
|
286
|
+
try {
|
|
287
|
+
const cached = await get(threadID[0]);
|
|
288
|
+
if (cached?.data && isValidThread(cached.data)) {
|
|
289
|
+
await upsertUsersFromThreadInfo(cached.data);
|
|
290
|
+
callback(null, cached.data);
|
|
291
|
+
return returnPromise;
|
|
292
|
+
}
|
|
293
|
+
if (!processingThreads.has(threadID[0])) {
|
|
294
|
+
processingThreads.add(threadID[0]);
|
|
295
|
+
console.info(`[FCA-INFO] Created new thread data: ${threadID[0]}`);
|
|
296
|
+
const response = await getMultiInfo(threadID);
|
|
297
|
+
if (response.Success && response.Data && isValidThread(response.Data[0])) {
|
|
298
|
+
const data = response.Data[0];
|
|
299
|
+
await upsertUsersFromThreadInfo(data);
|
|
300
|
+
await create(threadID[0], { data });
|
|
301
|
+
callback(null, data);
|
|
302
|
+
} else {
|
|
303
|
+
const stub = {
|
|
304
|
+
threadID: threadID[0],
|
|
305
|
+
threadName: null,
|
|
306
|
+
participantIDs: [],
|
|
307
|
+
userInfo: [],
|
|
308
|
+
unreadCount: 0,
|
|
309
|
+
messageCount: 0,
|
|
310
|
+
timestamp: null,
|
|
311
|
+
muteUntil: null,
|
|
312
|
+
isGroup: false,
|
|
313
|
+
isSubscribed: false,
|
|
314
|
+
isArchived: false,
|
|
315
|
+
folder: null,
|
|
316
|
+
cannotReplyReason: null,
|
|
317
|
+
eventReminders: [],
|
|
318
|
+
emoji: null,
|
|
319
|
+
color: null,
|
|
320
|
+
threadTheme: null,
|
|
321
|
+
nicknames: {},
|
|
322
|
+
adminIDs: [],
|
|
323
|
+
approvalMode: false,
|
|
324
|
+
approvalQueue: [],
|
|
325
|
+
reactionsMuteMode: "",
|
|
326
|
+
mentionsMuteMode: "",
|
|
327
|
+
isPinProtected: false,
|
|
328
|
+
relatedPageThread: null,
|
|
329
|
+
name: null,
|
|
330
|
+
snippet: null,
|
|
331
|
+
snippetSender: null,
|
|
332
|
+
snippetAttachments: [],
|
|
333
|
+
serverTimestamp: null,
|
|
334
|
+
imageSrc: null,
|
|
335
|
+
isCanonicalUser: false,
|
|
336
|
+
isCanonical: true,
|
|
337
|
+
recipientsLoadable: false,
|
|
338
|
+
hasEmailParticipant: false,
|
|
339
|
+
readOnly: false,
|
|
340
|
+
canReply: false,
|
|
341
|
+
lastMessageTimestamp: null,
|
|
342
|
+
lastMessageType: "message",
|
|
343
|
+
lastReadTimestamp: null,
|
|
344
|
+
threadType: 1,
|
|
345
|
+
inviteLink: { enable: false, link: null },
|
|
346
|
+
__status: "unavailable",
|
|
347
|
+
};
|
|
348
|
+
cooldown.set(threadID[0], Date.now() + 5 * 60 * 1000);
|
|
349
|
+
callback(null, stub);
|
|
350
|
+
}
|
|
351
|
+
processingThreads.delete(threadID[0]);
|
|
352
|
+
}
|
|
353
|
+
} catch (err) {
|
|
354
|
+
callback(err);
|
|
355
|
+
}
|
|
356
|
+
return returnPromise;
|
|
357
|
+
};
|
|
358
|
+
};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const log = require("npmlog");
|
|
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
|
+
if (resData[resData.length - 1].error_results > 0)
|
|
230
|
+
throw resData[0].o0.errors;
|
|
231
|
+
if (resData[resData.length - 1].successful_results === 0)
|
|
232
|
+
throw {
|
|
233
|
+
error: "getThreadList: there was no successful_results",
|
|
234
|
+
res: resData
|
|
235
|
+
};
|
|
236
|
+
if (timestamp) resData[0].o0.data.viewer.message_threads.nodes.shift();
|
|
237
|
+
callback(
|
|
238
|
+
null,
|
|
239
|
+
formatThreadList(resData[0].o0.data.viewer.message_threads.nodes)
|
|
240
|
+
);
|
|
241
|
+
})
|
|
242
|
+
.catch(err => {
|
|
243
|
+
log.error("getThreadList", err);
|
|
244
|
+
return callback(err);
|
|
245
|
+
});
|
|
246
|
+
return returnPromise;
|
|
247
|
+
};
|
|
248
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const log = require("npmlog");
|
|
4
|
+
const { parseAndCheckLogin } = require("../../utils/client");
|
|
5
|
+
module.exports = function(defaultFuncs, api, ctx) {
|
|
6
|
+
return function getThreadPictures(threadID, offset, limit, callback) {
|
|
7
|
+
let resolveFunc = function() {};
|
|
8
|
+
let rejectFunc = function() {};
|
|
9
|
+
const returnPromise = new Promise(function(resolve, reject) {
|
|
10
|
+
resolveFunc = resolve;
|
|
11
|
+
rejectFunc = reject;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (!callback) {
|
|
15
|
+
callback = function(err, friendList) {
|
|
16
|
+
if (err) {
|
|
17
|
+
return rejectFunc(err);
|
|
18
|
+
}
|
|
19
|
+
resolveFunc(friendList);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let form = {
|
|
24
|
+
thread_id: threadID,
|
|
25
|
+
offset: offset,
|
|
26
|
+
limit: limit
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
defaultFuncs
|
|
30
|
+
.post(
|
|
31
|
+
"https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
|
|
32
|
+
ctx.jar,
|
|
33
|
+
form
|
|
34
|
+
)
|
|
35
|
+
.then(parseAndCheckLogin(ctx, defaultFuncs))
|
|
36
|
+
.then(function(resData) {
|
|
37
|
+
if (resData.error) {
|
|
38
|
+
throw resData;
|
|
39
|
+
}
|
|
40
|
+
return Promise.all(
|
|
41
|
+
resData.payload.imagesData.map(function(image) {
|
|
42
|
+
form = {
|
|
43
|
+
thread_id: threadID,
|
|
44
|
+
image_id: image.fbid
|
|
45
|
+
};
|
|
46
|
+
return defaultFuncs
|
|
47
|
+
.post(
|
|
48
|
+
"https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
|
|
49
|
+
ctx.jar,
|
|
50
|
+
form
|
|
51
|
+
)
|
|
52
|
+
.then(parseAndCheckLogin(ctx, defaultFuncs))
|
|
53
|
+
.then(function(resData) {
|
|
54
|
+
if (resData.error) {
|
|
55
|
+
throw resData;
|
|
56
|
+
}
|
|
57
|
+
// the response is pretty messy
|
|
58
|
+
const queryThreadID =
|
|
59
|
+
resData.jsmods.require[0][3][1].query_metadata.query_path[0]
|
|
60
|
+
.message_thread;
|
|
61
|
+
const imageData =
|
|
62
|
+
resData.jsmods.require[0][3][1].query_results[queryThreadID]
|
|
63
|
+
.message_images.edges[0].node.image2;
|
|
64
|
+
return imageData;
|
|
65
|
+
});
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
})
|
|
69
|
+
.then(function(resData) {
|
|
70
|
+
callback(null, resData);
|
|
71
|
+
})
|
|
72
|
+
.catch(function(err) {
|
|
73
|
+
log.error("Error in getThreadPictures", err);
|
|
74
|
+
callback(err);
|
|
75
|
+
});
|
|
76
|
+
return returnPromise;
|
|
77
|
+
};
|
|
78
|
+
};
|