@dongdev/fca-unofficial 2.0.24 → 2.0.26

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,9 @@ 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
113
+
114
+ ## v2.0.25 - 2025-10-12
115
+ - Hotfix / auto bump
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  const fs = require("fs");
2
3
  const path = require("path");
3
4
  const models = require("../src/database/models");
@@ -230,6 +231,10 @@ async function backupAppStateSQL(j, userID) {
230
231
  const ck = cookieHeaderFromJar(j);
231
232
  await upsertBackup(Model, userID, "appstate", JSON.stringify(appJson));
232
233
  await upsertBackup(Model, userID, "cookie", ck);
234
+ try {
235
+ const out = path.join(process.cwd(), "appstate.json");
236
+ fs.writeFileSync(out, JSON.stringify(appJson, null, 2));
237
+ } catch { }
233
238
  logger("Backup stored (overwrite mode)", "info");
234
239
  } catch (e) {
235
240
  logger(`Failed to save appstate backup ${e && e.message ? e.message : String(e)}`, "warn");
@@ -258,66 +263,171 @@ async function getLatestBackupAny(type) {
258
263
  }
259
264
  }
260
265
 
261
- async function tokens(username, password, twofactor = null) {
262
- const t0 = process.hrtime.bigint();
263
- if (!username || !password) {
264
- logger("Missing email or password", "error");
265
- return { status: false, message: "Please provide email and password" };
266
+ const MESSENGER_USER_AGENT = "Dalvik/2.1.0 (Linux; U; Android 9; ASUS_Z01QD Build/PQ3A.190605.003) [FBAN/Orca-Android;FBAV/391.2.0.20.404;FBPN/com.facebook.orca;FBLC/vi_VN;FBBV/437533963;FBCR/Viettel Telecom;FBMF/asus;FBBD/asus;FBDV/ASUS_Z01QD;FBSV/9;FBCA/x86:armeabi-v7a;FBDM/{density=1.5,width=1600,height=900};FB_FW/1;]";
267
+
268
+ function encodesig(obj) {
269
+ let data = "";
270
+ Object.keys(obj).forEach(k => { data += `${k}=${obj[k]}`; });
271
+ return md5(data + "62f8ce9f74b12f84c123cc23437a4a32");
272
+ }
273
+
274
+ function sort(obj) {
275
+ const keys = Object.keys(obj).sort();
276
+ const out = {};
277
+ for (const k of keys) out[k] = obj[k];
278
+ return out;
279
+ }
280
+
281
+ async function setJarCookies(j, appstate) {
282
+ const tasks = [];
283
+ for (const c of appstate) {
284
+ const dom = (c.domain || ".facebook.com").replace(/^\./, "");
285
+ const path = c.path || "/";
286
+ const base1 = `https://${dom}${path}`;
287
+ const base2 = `https://www.${dom}${path}`;
288
+ const str = `${c.key}=${c.value}; Domain=${c.domain || ".facebook.com"}; Path=${path};`;
289
+ tasks.push(j.setCookie(str, base1));
290
+ tasks.push(j.setCookie(str, base2));
266
291
  }
267
- logger(`AUTO-LOGIN: Initialize login ${mask(username, 2)}`, "info");
268
- const cj = new CookieJar();
269
- const axios = wrapper(axiosBase.create({ jar: cj, withCredentials: true, validateStatus: () => true, timeout: 30000 }));
270
- const loginUrl = "https://b-graph.facebook.com/auth/login";
271
- const baseForm = { adid: uuidv4(), email: username, password: password, format: "json", device_id: uuidv4(), cpl: "true", family_device_id: uuidv4(), locale: "en_US", client_country_code: "US", credentials_type: "device_based_login_password", generate_session_cookies: "1", generate_analytics_claim: "1", generate_machine_id: "1", currently_logged_in_userid: "0", irisSeqID: 1, try_num: "1", enroll_misauth: "false", meta_inf_fbmeta: "", source: "login", machine_id: randomString(24), fb_api_req_friendly_name: "authenticate", fb_api_caller_class: "com.facebook.account.login.protocol.Fb4aAuthHandler", api_key: "882a8490361da98702bf97a021ddc14d", access_token: "350685531728%7C62f8ce9f74b12f84c123cc23437a4a32" };
272
- try {
273
- const form1 = { ...baseForm };
274
- form1.sig = encodeSig(sortObject(form1));
275
- logger("AUTO-LOGIN: Send login request", "info");
276
- const r0 = process.hrtime.bigint();
277
- const res1 = await axios.post(loginUrl, qs.stringify(form1), { headers: buildHeaders(loginUrl, { "x-fb-friendly-name": form1.fb_api_req_friendly_name }) });
278
- const dt1 = Number(process.hrtime.bigint() - r0) / 1e6;
279
- logger(`AUTO-LOGIN: Received response ${res1.status} ${Math.round(dt1)}ms`, "info");
280
- if (res1.status >= 400) throw { response: res1 };
281
- if (res1.data && res1.data.session_cookies) {
282
- const cookies = res1.data.session_cookies.map(e => ({ key: e.name, value: e.value, domain: "facebook.com", path: e.path, hostOnly: false }));
283
- logger(`AUTO-LOGIN: Login success (first attempt) ${cookies.length} cookies`, "info");
284
- const t1 = Number(process.hrtime.bigint() - t0) / 1e6;
285
- logger(`Done success login ${Math.round(t1)}ms`, "info");
286
- return { status: true, cookies };
287
- }
288
- throw { response: res1 };
289
- } catch (err) {
290
- const e = err && err.response ? err.response : null;
291
- const code = e && e.data && e.data.error ? e.data.error.code : null;
292
- const message = e && e.data && e.data.error ? e.data.error.message : "";
293
- if (code) logger(`AUTO-LOGIN: Error on request #1 ${code} ${message}`, "warn");
294
- logger("AUTO-LOGIN: Processing twofactor...", "info");
295
- if (code === 401) return { status: false, message: message || "Unauthorized" };
296
- if (!config.credentials?.twofactor) {
297
- logger("AUTO-LOGIN: 2FA required but secret missing", "warn");
298
- return { status: false, message: "Please provide the 2FA secret!" };
299
- }
292
+ await Promise.all(tasks);
293
+ }
294
+
295
+ async function loginViaGraph(username, password, twofactorSecretOrCode, i_user, externalJar) {
296
+ const cookieJar = externalJar instanceof CookieJar ? externalJar : new CookieJar();
297
+ const client = wrapper(axiosBase.create({ jar: cookieJar, withCredentials: true, timeout: 30000, validateStatus: () => true }));
298
+ const device_id = uuidv4();
299
+ const family_device_id = device_id;
300
+ const machine_id = randomString(24);
301
+ const base = {
302
+ adid: "00000000-0000-0000-0000-000000000000",
303
+ format: "json",
304
+ device_id,
305
+ email: username,
306
+ password,
307
+ generate_analytics_claim: "1",
308
+ community_id: "",
309
+ cpl: "true",
310
+ try_num: "1",
311
+ family_device_id,
312
+ secure_family_device_id: "",
313
+ credentials_type: "password",
314
+ enroll_misauth: "false",
315
+ generate_session_cookies: "1",
316
+ source: "login",
317
+ generate_machine_id: "1",
318
+ jazoest: "22297",
319
+ meta_inf_fbmeta: "NO_FILE",
320
+ advertiser_id: "00000000-0000-0000-0000-000000000000",
321
+ currently_logged_in_userid: "0",
322
+ locale: "vi_VN",
323
+ client_country_code: "VN",
324
+ fb_api_req_friendly_name: "authenticate",
325
+ fb_api_caller_class: "AuthOperations$PasswordAuthOperation",
326
+ api_key: "256002347743983",
327
+ access_token: "256002347743983|374e60f8b9bb6b8cbb30f78030438895"
328
+ };
329
+ const headers = {
330
+ "User-Agent": MESSENGER_USER_AGENT,
331
+ "Accept-Encoding": "gzip, deflate",
332
+ "Content-Type": "application/x-www-form-urlencoded",
333
+ "x-fb-connection-quality": "EXCELLENT",
334
+ "x-fb-sim-hni": "45204",
335
+ "x-fb-net-hni": "45204",
336
+ "x-fb-connection-type": "WIFI",
337
+ "x-tigon-is-retry": "False",
338
+ "x-fb-friendly-name": "authenticate",
339
+ "x-fb-request-analytics-tags": '{"network_tags":{"retry_attempt":"0"},"application_tags":"unknown"}',
340
+ "x-fb-http-engine": "Liger",
341
+ "x-fb-client-ip": "True",
342
+ "x-fb-server-cluster": "True",
343
+ authorization: `OAuth ${base.access_token}`
344
+ };
345
+ const form1 = { ...base };
346
+ form1.sig = encodesig(sort(form1));
347
+ const res1 = await client.request({ url: "https://b-graph.facebook.com/auth/login", method: "post", data: qs.stringify(form1), headers });
348
+ if (res1.status === 200 && res1.data && res1.data.session_cookies) {
349
+ const appstate = res1.data.session_cookies.map(c => ({ key: c.name, value: c.value, domain: c.domain, path: c.path }));
350
+ const cUserCookie = appstate.find(c => c.key === "c_user");
351
+ if (i_user) appstate.push({ key: "i_user", value: i_user, domain: ".facebook.com", path: "/" });
352
+ else if (cUserCookie) appstate.push({ key: "i_user", value: cUserCookie.value, domain: ".facebook.com", path: "/" });
353
+ await setJarCookies(cookieJar, appstate);
354
+ let eaau = null;
355
+ let eaad6v7 = null;
356
+ try {
357
+ const r1 = await client.request({ url: `https://api.facebook.com/method/auth.getSessionforApp?format=json&access_token=${res1.data.access_token}&new_app_id=350685531728`, method: "get", headers: { "user-agent": MESSENGER_USER_AGENT, "x-fb-connection-type": "WIFI", authorization: `OAuth ${res1.data.access_token}` } });
358
+ eaau = r1.data && r1.data.access_token ? r1.data.access_token : null;
359
+ } catch { }
300
360
  try {
301
- const dataErr = e && e.data && e.data.error && e.data.error.error_data ? e.data.error_data : {};
302
- const codeTotp = await genTotp(config.credentials.twofactor);
303
- logger(`AUTO-LOGIN: Performing 2FA ${mask(codeTotp, 2)}`, "info");
304
- const form2 = { ...baseForm, twofactor_code: codeTotp, encrypted_msisdn: "", userid: dataErr.uid || "", machine_id: dataErr.machine_id || baseForm.machine_id, first_factor: dataErr.login_first_factor || "", credentials_type: "two_factor" };
305
- form2.sig = encodeSig(sortObject(form2));
306
- const r1 = process.hrtime.bigint();
307
- const res2 = await axios.post(loginUrl, qs.stringify(form2), { headers: buildHeaders(loginUrl, { "x-fb-friendly-name": form2.fb_api_req_friendly_name }) });
308
- const dt2 = Number(process.hrtime.bigint() - r1) / 1e6;
309
- logger(`AUTO-LOGIN: Received 2FA response ${res2.status} ${Math.round(dt2)}ms`, "info");
310
- if (res2.status >= 400 || !(res2.data && res2.data.session_cookies)) throw new Error("2FA failed");
311
- const cookies = res2.data.session_cookies.map(e => ({ key: e.name, value: e.value, domain: "facebook.com", path: e.path, hostOnly: false }));
312
- logger(`AUTO-LOGIN: Login success with 2FA ${cookies.length} cookies`, "info");
313
- const t1 = Number(process.hrtime.bigint() - t0) / 1e6;
314
- logger(`AUTO-LOGIN: Done success login with 2FA ${Math.round(t1)}ms`, "info");
315
- return { status: true, cookies };
316
- } catch {
317
- logger("AUTO-LOGIN: 2FA failed", "error");
318
- return { status: false, message: "Invalid two-factor code!" };
361
+ const r2 = await client.request({ url: `https://api.facebook.com/method/auth.getSessionforApp?format=json&access_token=${res1.data.access_token}&new_app_id=275254692598279`, method: "get", headers: { "user-agent": MESSENGER_USER_AGENT, "x-fb-connection-type": "WIFI", authorization: `OAuth ${res1.data.access_token}` } });
362
+ eaad6v7 = r2.data && r2.data.access_token ? r2.data.access_token : null;
363
+ } catch { }
364
+ return { ok: true, cookies: appstate.map(c => ({ key: c.key, value: c.value })), jar: cookieJar, access_token_mess: res1.data.access_token || null, access_token: eaau, access_token_eaad6v7: eaad6v7, uid: res1.data.uid || cUserCookie?.value || null, session_key: res1.data.session_key || null };
365
+ }
366
+ const err = res1 && res1.data && res1.data.error ? res1.data.error : {};
367
+ if (err && err.code === 406) {
368
+ const data = err.error_data || {};
369
+ let code = null;
370
+ if (twofactorSecretOrCode && /^\d{6}$/.test(String(twofactorSecretOrCode))) code = String(twofactorSecretOrCode);
371
+ else if (twofactorSecretOrCode) {
372
+ try {
373
+ const clean = decodeURI(twofactorSecretOrCode).replace(/\s+/g, "").toUpperCase();
374
+ const { otp } = await TOTP.generate(clean);
375
+ code = otp;
376
+ } catch { }
377
+ } else if (config.credentials?.twofactor) {
378
+ const { otp } = await TOTP.generate(String(config.credentials.twofactor).replace(/\s+/g, "").toUpperCase());
379
+ code = otp;
319
380
  }
381
+ if (!code) return { ok: false, message: "2FA required" };
382
+ const form2 = {
383
+ ...base,
384
+ credentials_type: "two_factor",
385
+ twofactor_code: code,
386
+ userid: data.uid || username,
387
+ first_factor: data.login_first_factor || "",
388
+ machine_id: data.machine_id || machine_id
389
+ };
390
+ form2.sig = encodesig(sort(form2));
391
+ const res2 = await client.request({ url: "https://b-graph.facebook.com/auth/login", method: "post", data: qs.stringify(form2), headers });
392
+ if (res2.status === 200 && res2.data && res2.data.session_cookies) {
393
+ const appstate = res2.data.session_cookies.map(c => ({ key: c.name, value: c.value, domain: c.domain, path: c.path }));
394
+ const cUserCookie = appstate.find(c => c.key === "c_user");
395
+ if (i_user) appstate.push({ key: "i_user", value: i_user, domain: ".facebook.com", path: "/" });
396
+ else if (cUserCookie) appstate.push({ key: "i_user", value: cUserCookie.value, domain: ".facebook.com", path: "/" });
397
+ await setJarCookies(cookieJar, appstate);
398
+ let eaau = null;
399
+ let eaad6v7 = null;
400
+ try {
401
+ const r1 = await client.request({ url: `https://api.facebook.com/method/auth.getSessionforApp?format=json&access_token=${res2.data.access_token}&new_app_id=350685531728`, method: "get", headers: { "user-agent": MESSENGER_USER_AGENT, "x-fb-connection-type": "WIFI", authorization: `OAuth ${res2.data.access_token}` } });
402
+ eaau = r1.data && r1.data.access_token ? r1.data.access_token : null;
403
+ } catch { }
404
+ try {
405
+ const r2 = await client.request({ url: `https://api.facebook.com/method/auth.getSessionforApp?format=json&access_token=${res2.data.access_token}&new_app_id=275254692598279`, method: "get", headers: { "user-agent": MESSENGER_USER_AGENT, "x-fb-connection-type": "WIFI", authorization: `OAuth ${res2.data.access_token}` } });
406
+ eaad6v7 = r2.data && r2.data.access_token ? r2.data.access_token : null;
407
+ } catch { }
408
+ return { ok: true, cookies: appstate.map(c => ({ key: c.key, value: c.value })), jar: cookieJar, access_token_mess: res2.data.access_token || null, access_token: eaau, access_token_eaad6v7: eaad6v7, uid: res2.data.uid || cUserCookie?.value || null, session_key: res2.data.session_key || null };
409
+ }
410
+ return { ok: false, message: "2FA failed" };
411
+ }
412
+ return { ok: false, message: "Login failed" };
413
+ }
414
+
415
+ async function tokens(username, password, twofactor = null) {
416
+ const t0 = process.hrtime.bigint();
417
+ if (!username || !password) return { status: false, message: "Please provide email and password" };
418
+ logger(`AUTO-LOGIN: Initialize login ${mask(username, 2)}`, "info");
419
+ const res = await loginViaGraph(username, password, twofactor, null, jar);
420
+ if (res && res.ok && Array.isArray(res.cookies)) {
421
+ logger(`AUTO-LOGIN: Login success ${res.cookies.length} cookies`, "info");
422
+ const t1 = Number(process.hrtime.bigint() - t0) / 1e6;
423
+ logger(`Done success login ${Math.round(t1)}ms`, "info");
424
+ return { status: true, cookies: res.cookies };
425
+ }
426
+ if (res && res.message === "2FA required") {
427
+ logger("AUTO-LOGIN: 2FA required but secret missing", "warn");
428
+ return { status: false, message: "Please provide the 2FA secret!" };
320
429
  }
430
+ return { status: false, message: res && res.message ? res.message : "Login failed" };
321
431
  }
322
432
 
323
433
  async function hydrateJarFromDB(userID) {
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.26",
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
  }