@dongdev/fca-unofficial 2.0.24 → 2.0.25

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 CHANGED
@@ -107,3 +107,6 @@ Too lazy to write changelog, sorry! (will write changelog in the next release, t
107
107
 
108
108
  ## v2.0.23 - 2025-10-11
109
109
  - Hotfix / auto bump
110
+
111
+ ## v2.0.24 - 2025-10-11
112
+ - Hotfix / auto bump
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dongdev/fca-unofficial",
3
- "version": "2.0.24",
3
+ "version": "2.0.25",
4
4
  "description": "Unofficial Facebook Chat API for Node.js - Interact with Facebook Messenger programmatically",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+
3
+ const log = require("npmlog");
4
+ const { parseAndCheckLogin } = require("../../utils/client");
5
+ const { getType } = require("../../utils/format");
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function getThemePictures(id, callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+
15
+ if (!callback) {
16
+ if (
17
+ getType(callback) == "Function" ||
18
+ getType(callback) == "AsyncFunction"
19
+ ) {
20
+ callback = callback;
21
+ } else {
22
+ callback = function (err, data) {
23
+ if (err) {
24
+ return rejectFunc(err);
25
+ }
26
+ resolveFunc(data);
27
+ };
28
+ }
29
+ }
30
+
31
+ if (getType(id) != "String") {
32
+ id = "";
33
+ }
34
+
35
+ const form = {
36
+ fb_api_caller_class: "RelayModern",
37
+ fb_api_req_friendly_name: "MWPThreadThemeProviderQuery",
38
+ doc_id: "9734829906576883",
39
+ server_timestamps: true,
40
+ variables: JSON.stringify({
41
+ id
42
+ }),
43
+ av: ctx.userID
44
+ };
45
+ defaultFuncs
46
+ .post("https://www.facebook.com/api/graphql/", ctx.jar, form)
47
+ .then(parseAndCheckLogin(ctx, defaultFuncs))
48
+ .then(function (resData) {
49
+ if (resData.errors) {
50
+ throw resData;
51
+ }
52
+
53
+ return callback(null, resData);
54
+ })
55
+ .catch(function (err) {
56
+ log.error("getThemePictures", err);
57
+ return callback(err);
58
+ });
59
+
60
+ return returnPromise;
61
+ };
62
+ };
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
- const { parseAndCheckLogin, getType } = require("../../utils/format");
3
+ const { getType } = require("../../utils/format");
4
+ const { parseAndCheckLogin } = require("../../utils/client");
4
5
  const log = require("npmlog");
5
6
 
6
7
  module.exports = function (defaultFuncs, api, ctx) {
@@ -80,23 +80,30 @@ module.exports = function createListenMqtt(deps) {
80
80
  const mqttClient = ctx.mqttClient;
81
81
  global.mqttClient = mqttClient;
82
82
 
83
-
84
83
  mqttClient.on("error", function (err) {
85
84
  const msg = String(err && err.message ? err.message : err || "");
86
-
87
- if ((ctx._ending || ctx._cycling) && isEndingLikeError(msg)) {
85
+ if ((ctx._ending || ctx._cycling) && /No subscription existed|client disconnecting/i.test(msg)) {
88
86
  logger(`mqtt expected during shutdown: ${msg}`, "info");
89
87
  return;
90
88
  }
89
+
90
+ if (/Not logged in|Not logged in.|blocked the login|401|403/i.test(msg)) {
91
+ try { mqttClient.end(true); } catch (_) { }
92
+ return emitAuth(ctx, api, globalCallback,
93
+ /blocked/i.test(msg) ? "login_blocked" : "not_logged_in",
94
+ msg
95
+ );
96
+ }
91
97
  logger(`mqtt error: ${msg}`, "error");
92
98
  try { mqttClient.end(true); } catch (_) { }
93
99
  if (ctx._ending || ctx._cycling) return;
94
100
 
95
101
  if (ctx.globalOptions.autoReconnect) {
96
- scheduleReconnect();
102
+ const d = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
103
+ logger(`mqtt autoReconnect listenMqtt() in ${d}ms`, "warn");
104
+ setTimeout(() => listenMqtt(defaultFuncs, api, ctx, globalCallback), d);
97
105
  } else {
98
-
99
- globalCallback({ type: "stop_listen", error: msg || "Connection refused: Server unavailable" }, null);
106
+ globalCallback({ type: "stop_listen", error: msg || "Connection refused" }, null);
100
107
  }
101
108
  });
102
109
 
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ module.exports = function createEmitAuth({ logger }) {
3
+ return function emitAuth(ctx, api, globalCallback, reason, detail) {
4
+ try { if (ctx._autoCycleTimer) clearInterval(ctx._autoCycleTimer); } catch (_) { }
5
+ try { ctx._ending = true; } catch (_) { }
6
+ try { if (ctx.mqttClient) ctx.mqttClient.end(true); } catch (_) { }
7
+ ctx.mqttClient = undefined;
8
+ ctx.loggedIn = false;
9
+
10
+ const msg = detail || reason;
11
+ logger(`auth change -> ${reason}: ${msg}`, "error");
12
+
13
+ if (typeof globalCallback === "function") {
14
+ globalCallback({
15
+ type: "account_inactive",
16
+ reason,
17
+ error: msg,
18
+ timestamp: Date.now()
19
+ }, null);
20
+ }
21
+ };
22
+ };
@@ -2,24 +2,38 @@
2
2
  const { getType } = require("../../../utils/format");
3
3
  const { parseAndCheckLogin } = require("../../../utils/client");
4
4
  module.exports = function createGetSeqID(deps) {
5
- const { listenMqtt } = deps;
5
+ const { listenMqtt, logger, emitAuth } = deps;
6
+
6
7
  return function getSeqID(defaultFuncs, api, ctx, globalCallback, form) {
7
8
  ctx.t_mqttCalled = false;
8
- return defaultFuncs.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form).then(parseAndCheckLogin(ctx, defaultFuncs)).then(resData => {
9
- if (getType(resData) !== "Array") throw { error: "Not logged in", res: resData };
10
- if (!Array.isArray(resData) || !resData.length) return;
11
- const lastRes = resData[resData.length - 1];
12
- if (lastRes && lastRes.successful_results === 0) return;
13
- const syncSeqId = resData[0] && resData[0].o0 && resData[0].o0.data && resData[0].o0.data.viewer && resData[0].o0.data.viewer.message_threads && resData[0].o0.data.viewer.message_threads.sync_sequence_id;
14
- if (syncSeqId) {
15
- ctx.lastSeqId = syncSeqId;
16
- listenMqtt(defaultFuncs, api, ctx, globalCallback);
17
- } else {
18
- throw { error: "getSeqId: no sync_sequence_id found.", res: resData };
19
- }
20
- }).catch(err => {
21
- if (getType(err) === "Object" && err.error === "Not logged in") ctx.loggedIn = false;
22
- return globalCallback(err);
23
- });
9
+ return defaultFuncs
10
+ .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
11
+ .then(parseAndCheckLogin(ctx, defaultFuncs))
12
+ .then(resData => {
13
+ if (getType(resData) !== "Array") throw { error: "Not logged in" };
14
+ if (!Array.isArray(resData) || !resData.length) return;
15
+ const lastRes = resData[resData.length - 1];
16
+ if (lastRes && lastRes.successful_results === 0) return;
17
+
18
+ const syncSeqId = resData[0]?.o0?.data?.viewer?.message_threads?.sync_sequence_id;
19
+ if (syncSeqId) {
20
+ ctx.lastSeqId = syncSeqId;
21
+ logger("mqtt getSeqID ok -> listenMqtt()", "info");
22
+ listenMqtt(defaultFuncs, api, ctx, globalCallback);
23
+ } else {
24
+ throw { error: "getSeqId: no sync_sequence_id found." };
25
+ }
26
+ })
27
+ .catch(err => {
28
+ const msg = (err && err.error) || (err && err.message) || String(err || "");
29
+ if (/Not logged in/i.test(msg)) {
30
+ return emitAuth(ctx, api, globalCallback, "not_logged_in", msg);
31
+ }
32
+ if (/blocked the login/i.test(msg)) {
33
+ return emitAuth(ctx, api, globalCallback, "login_blocked", msg);
34
+ }
35
+ logger(`getSeqID error: ${msg}`, "error");
36
+ return emitAuth(ctx, api, globalCallback, "auth_error", msg);
37
+ });
24
38
  };
25
39
  };
@@ -12,9 +12,11 @@ const createListenMqtt = require("./core/connectMqtt");
12
12
  const createGetSeqID = require("./core/getSeqID");
13
13
  const markDelivery = require("./core/markDelivery");
14
14
  const getTaskResponseData = require("./core/getTaskResponseData");
15
+ const createEmitAuth = require("./core/emitAuth");
15
16
  const parseDelta = createParseDelta({ markDelivery, parseAndCheckLogin });
16
17
  const listenMqtt = createListenMqtt({ WebSocket, mqtt, HttpsProxyAgent, buildStream, buildProxy, topics, parseDelta, getTaskResponseData, logger });
17
18
  const getSeqIDFactory = createGetSeqID({ parseAndCheckLogin, listenMqtt, logger });
19
+ const emitAuth = createEmitAuth({ logger });
18
20
 
19
21
  const MQTT_DEFAULTS = { cycleMs: 60 * 60 * 1000, reconnectDelayMs: 2000, autoReconnect: true, reconnectAfterStop: false };
20
22
  function mqttConf(ctx, overrides) {
@@ -26,6 +28,33 @@ function mqttConf(ctx, overrides) {
26
28
  module.exports = function (defaultFuncs, api, ctx, opts) {
27
29
  const identity = function () { };
28
30
  let globalCallback = identity;
31
+
32
+ function installPostGuard() {
33
+ if (ctx._postGuarded) return defaultFuncs.post;
34
+ const rawPost = defaultFuncs.post && defaultFuncs.post.bind(defaultFuncs);
35
+ if (!rawPost) return defaultFuncs.post;
36
+
37
+ function postSafe(...args) {
38
+ return rawPost(...args).catch(err => {
39
+ const msg = (err && err.error) || (err && err.message) || String(err || "");
40
+ if (/Not logged in|blocked the login/i.test(msg)) {
41
+ emitAuth(
42
+ ctx,
43
+ api,
44
+ globalCallback,
45
+ /blocked/i.test(msg) ? "login_blocked" : "not_logged_in",
46
+ msg
47
+ );
48
+ }
49
+ throw err;
50
+ });
51
+ }
52
+ defaultFuncs.post = postSafe;
53
+ ctx._postGuarded = true;
54
+ logger("postSafe guard installed for defaultFuncs.post", "info");
55
+ return postSafe;
56
+ }
57
+
29
58
  let conf = mqttConf(ctx, opts);
30
59
 
31
60
  function getSeqIDWrapper() {
@@ -34,7 +63,10 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
34
63
  queries: JSON.stringify({
35
64
  o0: {
36
65
  doc_id: "3336396659757871",
37
- query_params: { limit: 1, before: null, tags: ["INBOX"], includeDeliveryReceipts: false, includeSeqID: true }
66
+ query_params: {
67
+ limit: 1, before: null, tags: ["INBOX"],
68
+ includeDeliveryReceipts: false, includeSeqID: true
69
+ }
38
70
  }
39
71
  })
40
72
  };
@@ -85,7 +117,7 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
85
117
  }
86
118
 
87
119
  function forceCycle() {
88
- if (ctx._cycling) return; // đừng cycle chồng
120
+ if (ctx._cycling) return;
89
121
  ctx._cycling = true;
90
122
  ctx._ending = true;
91
123
  logger("mqtt force cycle begin", "warn");
@@ -119,12 +151,16 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
119
151
  }
120
152
 
121
153
  const msgEmitter = new MessageEmitter();
154
+
122
155
  globalCallback = callback || function (error, message) {
123
156
  if (error) { logger("mqtt emit error", "error"); return msgEmitter.emit("error", error); }
124
157
  msgEmitter.emit("message", message);
125
158
  };
126
159
 
127
160
  conf = mqttConf(ctx, conf);
161
+
162
+ installPostGuard();
163
+
128
164
  if (!ctx.firstListen) ctx.lastSeqId = null;
129
165
  ctx.syncToken = undefined;
130
166
  ctx.t_mqttCalled = false;
@@ -1,5 +1,3 @@
1
- "use strict";
2
-
3
1
  const fs = require("fs");
4
2
  const path = require("path");
5
3
  const logger = require("../../../func/logger");
@@ -122,6 +120,8 @@ let isProcessingQueue = false;
122
120
  const processingThreads = new Set();
123
121
  const queuedThreads = new Set();
124
122
  const cooldown = new Map();
123
+ const inflight = new Map();
124
+ let loopStarted = false;
125
125
 
126
126
  module.exports = function (defaultFuncs, api, ctx) {
127
127
  const getMultiInfo = async function (threadIDs) {
@@ -204,6 +204,17 @@ module.exports = function (defaultFuncs, api, ctx) {
204
204
  }
205
205
  }
206
206
 
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
+ }
217
+
207
218
  async function fetchThreadInfo(tID, isNew) {
208
219
  try {
209
220
  const response = await getMultiInfo([tID]);
@@ -214,13 +225,8 @@ module.exports = function (defaultFuncs, api, ctx) {
214
225
  }
215
226
  const threadInfo = response.Data[0];
216
227
  await upsertUsersFromThreadInfo(threadInfo);
217
- if (isNew) {
218
- await create(tID, { data: threadInfo });
219
- logger(`Success create data thread: ${tID}`, "info");
220
- } else {
221
- await update(tID, { data: threadInfo });
222
- logger(`Success update data thread: ${tID}`, "info");
223
- }
228
+ const op = await createOrUpdateThread(tID, threadInfo);
229
+ logger(`${op === "create" ? "Success create data thread" : "Success update data thread"}: ${tID}`, "info");
224
230
  } catch (err) {
225
231
  cooldown.set(tID, Date.now() + 5 * 60 * 1000);
226
232
  logger(`fetchThreadInfo error ${tID}: ${err?.message || err}`, "error");
@@ -242,7 +248,6 @@ module.exports = function (defaultFuncs, api, ctx) {
242
248
  const lastUpdated = new Date(result.updatedAt).getTime();
243
249
  if ((now - lastUpdated) / (1000 * 60) > 10 && !queuedThreads.has(t)) {
244
250
  queuedThreads.add(t);
245
- logger(`ThreadID ${t} queued for refresh`, "info");
246
251
  queue.push(() => fetchThreadInfo(t, false));
247
252
  }
248
253
  }
@@ -265,10 +270,13 @@ module.exports = function (defaultFuncs, api, ctx) {
265
270
  isProcessingQueue = false;
266
271
  }
267
272
 
268
- setInterval(() => {
269
- checkAndUpdateThreads();
270
- processQueue();
271
- }, 10000);
273
+ if (!loopStarted) {
274
+ loopStarted = true;
275
+ setInterval(() => {
276
+ checkAndUpdateThreads();
277
+ processQueue();
278
+ }, 10000);
279
+ }
272
280
 
273
281
  return async function getThreadInfoGraphQL(threadID, callback) {
274
282
  let resolveFunc = function () { };
@@ -284,25 +292,89 @@ module.exports = function (defaultFuncs, api, ctx) {
284
292
  };
285
293
  }
286
294
  if (getType(threadID) !== "Array") threadID = [threadID];
295
+ const tid = String(threadID[0]);
287
296
  try {
288
- const cached = await get(threadID[0]);
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;
304
+ }
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
+
355
+ const cached = await get(tid);
289
356
  if (cached?.data && isValidThread(cached.data)) {
290
357
  await upsertUsersFromThreadInfo(cached.data);
291
358
  callback(null, cached.data);
292
359
  return returnPromise;
293
360
  }
294
- if (!processingThreads.has(threadID[0])) {
295
- processingThreads.add(threadID[0]);
296
- logger(`Created new thread data: ${threadID[0]}`, "info");
297
- const response = await getMultiInfo(threadID);
361
+
362
+ if (inflight.has(tid)) {
363
+ inflight.get(tid).then(data => callback(null, data)).catch(err => callback(err));
364
+ return returnPromise;
365
+ }
366
+
367
+ const p = (async () => {
368
+ processingThreads.add(tid);
369
+ const response = await getMultiInfo([tid]);
298
370
  if (response.Success && response.Data && isValidThread(response.Data[0])) {
299
371
  const data = response.Data[0];
300
372
  await upsertUsersFromThreadInfo(data);
301
- await create(threadID[0], { data });
302
- callback(null, data);
373
+ await createOrUpdateThread(tid, data);
374
+ return data;
303
375
  } else {
304
376
  const stub = {
305
- threadID: threadID[0],
377
+ threadID: tid,
306
378
  threadName: null,
307
379
  participantIDs: [],
308
380
  userInfo: [],
@@ -346,11 +418,18 @@ module.exports = function (defaultFuncs, api, ctx) {
346
418
  inviteLink: { enable: false, link: null },
347
419
  __status: "unavailable",
348
420
  };
349
- cooldown.set(threadID[0], Date.now() + 5 * 60 * 1000);
350
- callback(null, stub);
421
+ cooldown.set(tid, Date.now() + 5 * 60 * 1000);
422
+ await createOrUpdateThread(tid, stub);
423
+ return stub;
351
424
  }
352
- processingThreads.delete(threadID[0]);
353
- }
425
+ })()
426
+ .finally(() => {
427
+ processingThreads.delete(tid);
428
+ inflight.delete(tid);
429
+ });
430
+
431
+ inflight.set(tid, p);
432
+ p.then(data => callback(null, data)).catch(err => callback(err));
354
433
  } catch (err) {
355
434
  callback(err);
356
435
  }