@dongdev/fca-unofficial 2.0.12 → 2.0.13

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
@@ -71,3 +71,6 @@ Too lazy to write changelog, sorry! (will write changelog in the next release, t
71
71
 
72
72
  ## v2.0.11 - 2025-10-07
73
73
  - Hotfix / auto bump
74
+
75
+ ## v2.0.12 - 2025-10-07
76
+ - Hotfix / auto bump
package/README.md CHANGED
@@ -8,6 +8,7 @@
8
8
  ## ⚠️ Important Disclaimer
9
9
 
10
10
  **We are not responsible if your account gets banned for spammy activities such as:**
11
+
11
12
  - Sending lots of messages to people you don't know
12
13
  - Sending messages very quickly
13
14
  - Sending spammy looking URLs
@@ -15,13 +16,14 @@
15
16
 
16
17
  **Recommendation:** Use Firefox browser or [this website](https://fca.dongdev.id.vn) to reduce logout issues, especially for iOS users.
17
18
 
18
- **Support:** If you encounter errors, contact us [here](https://www.facebook.com/minhdong.dev)
19
+ **Support:** If you encounter errors, contact us [here](https://www.facebook.com/mdong.dev)
19
20
 
20
21
  ## 🔍 Introduction
21
22
 
22
23
  Facebook now has an [official API for chat bots](https://developers.facebook.com/docs/messenger-platform), however it's only available for Facebook Pages.
23
24
 
24
25
  `@dongdev/fca-unofficial` is the only API that allows you to automate chat functionalities on a **user account** by emulating the browser. This means:
26
+
25
27
  - Making the exact same GET/POST requests as a browser
26
28
  - Does not work with auth tokens
27
29
  - Requires Facebook account credentials (email/password) or AppState
@@ -109,13 +111,13 @@ login({ appState: [] }, (err, api) => {
109
111
 
110
112
  ## 📝 Message Types
111
113
 
112
- | Type | Usage |
113
- | ---------------- | --------------------------------------------------------------- |
114
+ | Type | Usage |
115
+ | ---------------------- | ----------------------------------------------------------------- |
114
116
  | **Regular text** | `{ body: "message text" }` |
115
117
  | **Sticker** | `{ sticker: "sticker_id" }` |
116
118
  | **File/Image** | `{ attachment: fs.createReadStream(path) }` or array of streams |
117
119
  | **URL** | `{ url: "https://example.com" }` |
118
- | **Large emoji** | `{ emoji: "👍", emojiSize: "large" }` (small/medium/large) |
120
+ | **Large emoji** | `{ emoji: "👍", emojiSize: "large" }` (small/medium/large) |
119
121
 
120
122
  **Note:** A message can only be a regular message (which can be empty) and optionally **one of the following**: a sticker, an attachment, or a URL.
121
123
 
@@ -228,6 +230,7 @@ api.setOptions({
228
230
  ```
229
231
 
230
232
  **By default:**
233
+
231
234
  - `listenEvents` is `false` - won't receive events like joining/leaving chat, title changes
232
235
  - `selfListen` is `false` - will ignore messages sent by the current account
233
236
 
@@ -252,6 +255,7 @@ api.setOptions({
252
255
  ## 📚 Full API Documentation
253
256
 
254
257
  See [DOCS.md](./DOCS.md) for detailed information about:
258
+
255
259
  - All available API methods
256
260
  - Parameters and options
257
261
  - Event types
@@ -291,6 +295,7 @@ api.setMessageReaction(reaction, messageID, callback);
291
295
  ## 🤝 Contributing
292
296
 
293
297
  Contributions are welcome! Please:
298
+
294
299
  1. Fork the repository
295
300
  2. Create a new branch (`git checkout -b feature/AmazingFeature`)
296
301
  3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
@@ -1,3 +1,4 @@
1
+ // func/checkUpdate.js
1
2
  const logger = require("./logger");
2
3
  const fs = require("fs");
3
4
  const { exec } = require("child_process");
@@ -21,38 +22,41 @@ function getInstalledVersion() {
21
22
  }
22
23
  }
23
24
 
24
- async function checkAndUpdateVersion(callback) {
25
- try {
26
- logger("Checking version...", "info");
27
- const latest = (await execPromise(`npm view ${pkgName} version`)).stdout.trim();
28
- const installed = getInstalledVersion();
29
- if (!installed || installed !== latest) {
30
- logger(`New version available (${latest}). Current version (${installed || "not installed"}). Updating...`, "info");
25
+ async function _checkAndUpdateVersionImpl() {
26
+ logger("Checking version...", "info");
27
+ const latest = (await execPromise(`npm view ${pkgName} version`)).stdout.trim();
28
+ const installed = getInstalledVersion();
29
+ if (!installed || installed !== latest) {
30
+ logger(`New version available (${latest}). Current version (${installed || "not installed"}). Updating...`, "info");
31
+ try {
32
+ const { stderr } = await execPromise(`npm i ${pkgName}@latest`);
33
+ if (stderr) logger(stderr, "error");
34
+ logger(`Updated fca to the latest version: ${latest}, Restart to apply`, "info");
35
+ process.exit(1);
36
+ } catch (e) {
37
+ logger(`Error running npm install: ${e.error || e}. Trying to install from GitHub...`, "error");
31
38
  try {
32
- const { stderr } = await execPromise(`npm i ${pkgName}@latest`);
39
+ const { stderr } = await execPromise("npm i https://github.com/Donix-VN/fca-unofficial");
33
40
  if (stderr) logger(stderr, "error");
34
- logger(`Updated fca to the latest version: ${latest}, Restart to apply`, "info");
35
- callback(null);
36
- } catch (e) {
37
- logger(`Error running npm install: ${e.error || e}. Trying to install from GitHub...`, "error");
38
- try {
39
- const { stderr } = await execPromise("npm i https://github.com/Donix-VN/fca-unofficial");
40
- if (stderr) logger(stderr, "error");
41
- logger(`Installed from GitHub successfully: ${latest}`, "info");
42
- callback(null);
43
- } catch (gitErr) {
44
- logger(`Error installing from GitHub: ${gitErr.error || gitErr}`, "error");
45
- callback(gitErr.error || gitErr);
46
- }
41
+ logger(`Installed from GitHub successfully: ${latest}`, "info");
42
+ return;
43
+ } catch (gitErr) {
44
+ logger(`Error installing from GitHub: ${gitErr.error || gitErr}`, "error");
45
+ throw (gitErr.error || gitErr);
47
46
  }
48
- } else {
49
- logger(`You're already on the latest version - ${latest}`, "info");
50
- callback(null);
51
47
  }
52
- } catch (err) {
53
- logger(`Error checking version: ${err}`, "error");
54
- callback(err);
48
+ } else {
49
+ logger(`You're already on the latest version - ${latest}`, "info");
50
+ return;
51
+ }
52
+ }
53
+
54
+ function checkAndUpdateVersion(callback) {
55
+ if (typeof callback === "function") {
56
+ _checkAndUpdateVersionImpl().then(() => callback(null)).catch(err => callback(err));
57
+ return;
55
58
  }
59
+ return _checkAndUpdateVersionImpl();
56
60
  }
57
61
 
58
- module.exports.checkAndUpdateVersion = checkAndUpdateVersion;
62
+ module.exports = { checkAndUpdateVersion };
package/module/login.js CHANGED
@@ -1,11 +1,11 @@
1
- "use strict";
2
1
  const { getType } = require("../src/utils/format");
3
2
  const { setOptions } = require("./options");
4
3
  const { loadConfig } = require("./config");
4
+ const { checkAndUpdateVersion } = require("../func/checkUpdate");
5
+ const loginHelper = require("./loginHelper");
6
+
5
7
  const { config } = loadConfig();
6
- if (config.autoUpdate) require("../func/checkUpdate").checkAndUpdateVersion(() => {});
7
8
  global.fca = { config };
8
- const loginHelper = require("./loginHelper");
9
9
 
10
10
  function login(loginData, options, callback) {
11
11
  if (getType(options) === "Function" || getType(options) === "AsyncFunction") {
@@ -28,10 +28,11 @@ function login(loginData, options, callback) {
28
28
  };
29
29
  setOptions(globalOptions, options);
30
30
  let prCallback = null;
31
+ let rejectFunc = null;
32
+ let resolveFunc = null;
33
+ let returnPromise = null;
31
34
  if (getType(callback) !== "Function" && getType(callback) !== "AsyncFunction") {
32
- let rejectFunc = null;
33
- let resolveFunc = null;
34
- var returnPromise = new Promise(function (resolve, reject) {
35
+ returnPromise = new Promise(function (resolve, reject) {
35
36
  resolveFunc = resolve;
36
37
  rejectFunc = reject;
37
38
  });
@@ -41,7 +42,18 @@ function login(loginData, options, callback) {
41
42
  };
42
43
  callback = prCallback;
43
44
  }
44
- loginHelper(loginData.appState, loginData.Cookie,loginData.email, loginData.password, globalOptions, callback, prCallback);
45
+ const proceed = () => loginHelper(loginData.appState, loginData.Cookie, loginData.email, loginData.password, globalOptions, callback, prCallback);
46
+ if (config && config.autoUpdate) {
47
+ const p = checkAndUpdateVersion();
48
+ if (p && typeof p.then === "function") {
49
+ p.then(proceed).catch(err => callback(err));
50
+ } else {
51
+ proceed();
52
+ }
53
+ } else {
54
+ proceed();
55
+ }
45
56
  return returnPromise;
46
57
  }
58
+
47
59
  module.exports = login;
@@ -127,6 +127,35 @@ const genTotp = async secret => {
127
127
  return typeof r === "object" ? r.otp : r;
128
128
  };
129
129
 
130
+ function normalizeCookieHeaderString(s) {
131
+ let str = String(s || "").trim();
132
+ if (!str) return [];
133
+ if (/^cookie\s*:/i.test(str)) str = str.replace(/^cookie\s*:/i, "").trim();
134
+ str = str.replace(/\r?\n/g, " ").replace(/\s*;\s*/g, ";");
135
+ const parts = str.split(";").map(v => v.trim()).filter(Boolean);
136
+ const out = [];
137
+ for (const p of parts) {
138
+ const eq = p.indexOf("=");
139
+ if (eq <= 0) continue;
140
+ const k = p.slice(0, eq).trim();
141
+ const v = p.slice(eq + 1).trim().replace(/^"(.*)"$/, "$1");
142
+ if (!k) continue;
143
+ out.push(`${k}=${v}`);
144
+ }
145
+ return out;
146
+ }
147
+
148
+ function setJarFromPairs(j, pairs, domain) {
149
+ const expires = new Date(Date.now() + 31536e6).toUTCString();
150
+ for (const kv of pairs) {
151
+ const cookieStr = `${kv}; expires=${expires}; domain=${domain}; path=/;`;
152
+ try {
153
+ if (typeof j.setCookieSync === "function") j.setCookieSync(cookieStr, "https://www.facebook.com");
154
+ else j.setCookie(cookieStr, "https://www.facebook.com");
155
+ } catch { }
156
+ }
157
+ }
158
+
130
159
  function cookieHeaderFromJar(j) {
131
160
  const urls = ["https://www.facebook.com", "https://www.messenger.com"];
132
161
  const seen = new Set();
@@ -135,7 +164,7 @@ function cookieHeaderFromJar(j) {
135
164
  let s = "";
136
165
  try {
137
166
  s = typeof j.getCookieStringSync === "function" ? j.getCookieStringSync(u) : "";
138
- } catch {}
167
+ } catch { }
139
168
  if (!s) continue;
140
169
  for (const kv of s.split(";")) {
141
170
  const t = kv.trim();
@@ -174,7 +203,7 @@ async function ensureUniqueIndex(sequelize) {
174
203
  if (uniqueIndexEnsured) return;
175
204
  try {
176
205
  await sequelize.getQueryInterface().addIndex("app_state_backups", ["userID", "type"], { unique: true, name: "app_state_user_type_unique" });
177
- } catch {}
206
+ } catch { }
178
207
  uniqueIndexEnsured = true;
179
208
  }
180
209
 
@@ -231,7 +260,7 @@ async function getLatestBackupAny(type) {
231
260
  async function tokens(username, password, twofactor = null) {
232
261
  const t0 = process.hrtime.bigint();
233
262
  if (!username || !password) {
234
- logger("Missing email or password", {}, "error");
263
+ logger("Missing email or password", "error");
235
264
  return { status: false, message: "Please provide email and password" };
236
265
  }
237
266
  logger(`AUTO-LOGIN: Initialize login ${mask(username, 2)}`, "info");
@@ -263,13 +292,13 @@ async function tokens(username, password, twofactor = null) {
263
292
  if (code) logger(`AUTO-LOGIN: Error on request #1 ${code} ${message}`, "warn");
264
293
  logger("AUTO-LOGIN: Processing twofactor...", "info");
265
294
  if (code === 401) return { status: false, message: message || "Unauthorized" };
266
- if (twofactor === "0" || !twofactor) {
267
- logger("AUTO-LOGIN: 2FA required but secret missing", {}, "warn");
295
+ if (!config.credentials?.twofactor) {
296
+ logger("AUTO-LOGIN: 2FA required but secret missing", "warn");
268
297
  return { status: false, message: "Please provide the 2FA secret!" };
269
298
  }
270
299
  try {
271
300
  const dataErr = e && e.data && e.data.error && e.data.error.error_data ? e.data.error.error_data : {};
272
- const codeTotp = await genTotp(twofactor);
301
+ const codeTotp = await genTotp(config.credentials.twofactor);
273
302
  logger(`AUTO-LOGIN: Performing 2FA ${mask(codeTotp, 2)}`, "info");
274
303
  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" };
275
304
  form2.sig = encodeSig(sortObject(form2));
@@ -284,58 +313,12 @@ async function tokens(username, password, twofactor = null) {
284
313
  logger(`AUTO-LOGIN: Done success login with 2FA ${Math.round(t1)}ms`, "info");
285
314
  return { status: true, cookies };
286
315
  } catch {
287
- logger("AUTO-LOGIN: 2FA failed", {}, "error");
316
+ logger("AUTO-LOGIN: 2FA failed", "error");
288
317
  return { status: false, message: "Invalid two-factor code!" };
289
318
  }
290
319
  }
291
320
  }
292
321
 
293
- function normalizeCookieHeaderString(s) {
294
- let str = String(s || "").trim();
295
- if (!str) return [];
296
- if (/^cookie\s*:/i.test(str)) str = str.replace(/^cookie\s*:/i, "").trim();
297
- str = str.replace(/\r?\n/g, " ").replace(/\s*;\s*/g, ";");
298
- const parts = str.split(";").map(v => v.trim()).filter(Boolean);
299
- const out = [];
300
- for (const p of parts) {
301
- const eq = p.indexOf("=");
302
- if (eq <= 0) continue;
303
- const k = p.slice(0, eq).trim();
304
- const v = p.slice(eq + 1).trim().replace(/^"(.*)"$/, "$1");
305
- if (!k) continue;
306
- out.push(`${k}=${v}`);
307
- }
308
- return out;
309
- }
310
-
311
- function setJarFromPairs(j, pairs, domain) {
312
- const expires = new Date(Date.now() + 31536e6).toUTCString();
313
- for (const kv of pairs) {
314
- const cookieStr = `${kv}; expires=${expires}; domain=${domain}; path=/;`;
315
- try {
316
- if (typeof j.setCookieSync === "function") j.setCookieSync(cookieStr, "https://www.facebook.com");
317
- else j.setCookie(cookieStr, "https://www.facebook.com");
318
- } catch {}
319
- }
320
- }
321
-
322
- function makeLogin(j, email, password, globalOptions, callback, prCallback) {
323
- return async function () {
324
- const u = email || config.credentials.email;
325
- const p = password || config.credentials.password;
326
- const tf = config.credentials.twofactor || null;
327
- if (!u || !p) return;
328
- const r = await tokens(u, p, tf);
329
- if (r && r.status && Array.isArray(r.cookies)) {
330
- const pairs = r.cookies.map(c => `${c.key || c.name}=${c.value}`);
331
- setJarFromPairs(j, pairs, ".facebook.com");
332
- await get("https://www.facebook.com/", j, null, globalOptions).then(saveCookies(j));
333
- } else {
334
- throw new Error(r && r.message ? r.message : "Login failed");
335
- }
336
- };
337
- }
338
-
339
322
  async function hydrateJarFromDB(userID) {
340
323
  try {
341
324
  let ck = null;
@@ -358,7 +341,7 @@ async function hydrateJarFromDB(userID) {
358
341
  let parsed = null;
359
342
  try {
360
343
  parsed = JSON.parse(app);
361
- } catch {}
344
+ } catch { }
362
345
  if (Array.isArray(parsed)) {
363
346
  const pairs = parsed.map(c => [c.name || c.key, c.value].join("="));
364
347
  setJarFromPairs(jar, pairs, ".facebook.com");
@@ -377,10 +360,8 @@ async function tryAutoLoginIfNeeded(currentHtml, currentCookies, globalOptions)
377
360
  cs.find(c => c.key === "c_user")?.value ||
378
361
  cs.find(c => c.name === "i_user")?.value ||
379
362
  cs.find(c => c.name === "c_user")?.value;
380
-
381
363
  let userID = getUID(currentCookies);
382
364
  if (userID) return { html: currentHtml, cookies: currentCookies, userID };
383
-
384
365
  const hydrated = await hydrateJarFromDB(null);
385
366
  if (hydrated) {
386
367
  logger("AppState backup live — proceeding to login", "info");
@@ -391,12 +372,11 @@ async function tryAutoLoginIfNeeded(currentHtml, currentCookies, globalOptions)
391
372
  const uidB = getUID(cookiesB);
392
373
  if (uidB) return { html: htmlB, cookies: cookiesB, userID: uidB };
393
374
  }
394
-
395
375
  if (config.autoLogin !== true) throw new Error("AppState backup die — Auto-login is disabled");
396
376
  logger("AppState backup die — proceeding to email/password login", "warn");
397
- const u = config.credentials.email;
398
- const p = config.credentials.password;
399
- const tf = config.credentials.twofactor || null;
377
+ const u = config.credentials?.email;
378
+ const p = config.credentials?.password;
379
+ const tf = config.credentials?.twofactor || null;
400
380
  if (!u || !p) throw new Error("Missing user cookie");
401
381
  const r = await tokens(u, p, tf);
402
382
  if (!(r && r.status && Array.isArray(r.cookies))) throw new Error(r && r.message ? r.message : "Login failed");
@@ -411,9 +391,25 @@ async function tryAutoLoginIfNeeded(currentHtml, currentCookies, globalOptions)
411
391
  return { html: html2, cookies: cookies2, userID: uid2 };
412
392
  }
413
393
 
414
- function loginHelper(appState, Cookie, email, password, globalOptions, callback, prCallback) {
394
+ function makeLogin(j, email, password, globalOptions) {
395
+ return async function () {
396
+ const u = email || config.credentials?.email;
397
+ const p = password || config.credentials?.password;
398
+ const tf = config.credentials?.twofactor || null;
399
+ if (!u || !p) return;
400
+ const r = await tokens(u, p, tf);
401
+ if (r && r.status && Array.isArray(r.cookies)) {
402
+ const pairs = r.cookies.map(c => `${c.key || c.name}=${c.value}`);
403
+ setJarFromPairs(j, pairs, ".facebook.com");
404
+ await get("https://www.facebook.com/", j, null, globalOptions).then(saveCookies(j));
405
+ } else {
406
+ throw new Error(r && r.message ? r.message : "Login failed");
407
+ }
408
+ };
409
+ }
410
+
411
+ function loginHelper(appState, Cookie, email, password, globalOptions, callback) {
415
412
  try {
416
- let main;
417
413
  const domain = ".facebook.com";
418
414
  try {
419
415
  if (appState) {
@@ -421,7 +417,7 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback,
421
417
  let parsed = appState;
422
418
  try {
423
419
  parsed = JSON.parse(appState);
424
- } catch {}
420
+ } catch { }
425
421
  if (Array.isArray(parsed)) {
426
422
  const pairs = parsed.map(c => [c.name || c.key, c.value].join("="));
427
423
  setJarFromPairs(jar, pairs, domain);
@@ -449,7 +445,6 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback,
449
445
  } catch (e) {
450
446
  return callback(e);
451
447
  }
452
-
453
448
  (async () => {
454
449
  if (appState || Cookie) {
455
450
  return get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
@@ -462,7 +457,7 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback,
462
457
  logger("AppState backup die — proceeding to email/password login", "warn");
463
458
  return get("https://www.facebook.com/", null, null, globalOptions)
464
459
  .then(saveCookies(jar))
465
- .then(makeLogin(jar, email, password, globalOptions, callback, prCallback))
460
+ .then(makeLogin(jar, email, password, globalOptions))
466
461
  .then(function () {
467
462
  return get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
468
463
  });
@@ -475,34 +470,20 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback,
475
470
  cookies.find(c => c.key === "c_user")?.value ||
476
471
  cookies.find(c => c.name === "i_user")?.value ||
477
472
  cookies.find(c => c.name === "c_user")?.value;
478
-
479
- try {
480
- const userDataMatch = String(html).match(/\["CurrentUserInitialData",\[\],({.*?}),\d+\]/);
481
- if (userDataMatch) {
482
- const info = JSON.parse(userDataMatch[1]);
483
- logger(`Đăng nhập tài khoản: ${info.NAME} (${info.USER_ID})`, "info");
484
- } else if (userID) {
485
- logger(`ID người dùng: ${userID}`, "info");
486
- }
487
- } catch {}
488
-
489
473
  if (!userID) {
490
474
  const retried = await tryAutoLoginIfNeeded(html, cookies, globalOptions);
491
475
  html = retried.html;
492
476
  cookies = retried.cookies;
493
477
  userID = retried.userID;
494
478
  }
495
-
496
479
  if (html.includes("/checkpoint/block/?next")) {
497
480
  logger("Appstate die, vui lòng thay cái mới!", "error");
498
481
  throw new Error("Checkpoint");
499
482
  }
500
-
501
483
  let mqttEndpoint;
502
484
  let region = "PRN";
503
485
  let fb_dtsg;
504
486
  let irisSeqID;
505
-
506
487
  try {
507
488
  const m1 = html.match(/"endpoint":"([^"]+)"/);
508
489
  const m2 = m1 ? null : html.match(/endpoint\\":\\"([^\\"]+)\\"/);
@@ -526,11 +507,9 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback,
526
507
  } catch { }
527
508
  const tokenMatch = html.match(/DTSGInitialData.*?token":"(.*?)"/);
528
509
  if (tokenMatch) fb_dtsg = tokenMatch[1];
529
-
530
510
  try {
531
511
  if (userID) await backupAppStateSQL(jar, userID);
532
- } catch {}
533
-
512
+ } catch { }
534
513
  Promise.resolve()
535
514
  .then(function () {
536
515
  if (models && models.sequelize && typeof models.sequelize.authenticate === "function") {
@@ -546,9 +525,7 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback,
546
525
  console.error(error);
547
526
  console.error("Database connection failed:", error && error.message ? error.message : String(error));
548
527
  });
549
-
550
528
  logger("FCA fix/update by DongDev (Donix-VN)", "info");
551
-
552
529
  const ctx = {
553
530
  userID,
554
531
  jar,
@@ -568,7 +545,22 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback,
568
545
  wsReqNumber: 0,
569
546
  wsTaskNumber: 0
570
547
  };
571
-
548
+ ctx.performAutoLogin = async () => {
549
+ try {
550
+ const u = config.credentials?.email || email;
551
+ const p = config.credentials?.password || password;
552
+ const tf = config.credentials?.twofactor || null;
553
+ if (!u || !p) return false;
554
+ const r = await tokens(u, p, tf);
555
+ if (!(r && r.status && Array.isArray(r.cookies))) return false;
556
+ const pairs = r.cookies.map(c => `${c.key || c.name}=${c.value}`);
557
+ setJarFromPairs(jar, pairs, ".facebook.com");
558
+ await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
559
+ return true;
560
+ } catch {
561
+ return false;
562
+ }
563
+ };
572
564
  const api = {
573
565
  setOptions: require("./options").setOptions.bind(null, globalOptions),
574
566
  getCookies: function () {
@@ -585,7 +577,6 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback,
585
577
  return await getLatestBackup(uid, "cookie");
586
578
  }
587
579
  };
588
-
589
580
  const defaultFuncs = makeDefaults(html, userID, ctx);
590
581
  const srcRoot = path.join(__dirname, "../src/api");
591
582
  let loaded = 0;
@@ -605,23 +596,18 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback,
605
596
  loaded++;
606
597
  });
607
598
  });
608
-
609
599
  logger(`Loaded ${loaded} FCA API methods${skipped ? `, skipped ${skipped} duplicates` : ""}`);
610
-
611
- api.listen = api.listenMqtt;
612
-
613
- setInterval(function () {
614
- api
615
- .refreshFb_dtsg()
616
- .then(function () {
617
- logger("Successfully refreshed fb_dtsg", "[ FCA-UNO ] >");
618
- })
619
- .catch(function () {
600
+ if (api.listenMqtt) api.listen = api.listenMqtt;
601
+ if (api.refreshFb_dtsg) {
602
+ setInterval(function () {
603
+ api.refreshFb_dtsg().then(function () {
604
+ logger("Successfully refreshed fb_dtsg");
605
+ }).catch(function () {
620
606
  logger("An error occurred while refreshing fb_dtsg", "error");
621
607
  });
622
- }, 86400000);
623
-
624
- logger("Login successful!", "[ FCA-UNO ] >");
608
+ }, 86400000);
609
+ }
610
+ logger("Login successful!");
625
611
  callback(null, api);
626
612
  })
627
613
  .catch(function (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dongdev/fca-unofficial",
3
- "version": "2.0.12",
3
+ "version": "2.0.13",
4
4
  "description": "Unofficial Facebook Chat API for Node.js - Interact with Facebook Messenger programmatically",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -304,72 +304,62 @@ module.exports = function (defaultFuncs, api, ctx) {
304
304
  resolveFunc(data);
305
305
  };
306
306
  }
307
-
308
- try {
309
- const msgType = getType(msg);
310
- const threadIDType = getType(threadID);
311
- const messageIDType = getType(replyToMessage);
312
- if (msgType !== "String" && msgType !== "Object") return callback({ error: "Message should be of type string or object and not " + msgType + "." });
313
- if (threadIDType !== "Array" && threadIDType !== "Number" && threadIDType !== "String") return callback({ error: "ThreadID should be of type number, string, or array and not " + threadIDType + "." });
314
- if (replyToMessage && messageIDType !== "String") return callback({ error: "MessageID should be of type string and not " + threadIDType + "." });
315
- if (msgType === "String") msg = { body: msg };
316
-
317
- const disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
318
- if (disallowedProperties.length > 0) return callback({ error: "Disallowed props: `" + disallowedProperties.join(", ") + "`" });
319
-
320
- const messageAndOTID = generateOfflineThreadingID();
321
- const form = {
322
- client: "mercury",
323
- action_type: "ma-type:user-generated-message",
324
- author: "fbid:" + ctx.userID,
325
- timestamp: Date.now(),
326
- timestamp_absolute: "Today",
327
- timestamp_relative: generateTimestampRelative(),
328
- timestamp_time_passed: "0",
329
- is_unread: false,
330
- is_cleared: false,
331
- is_forward: false,
332
- is_filtered_content: false,
333
- is_filtered_content_bh: false,
334
- is_filtered_content_account: false,
335
- is_filtered_content_quasar: false,
336
- is_filtered_content_invalid_app: false,
337
- is_spoof_warning: false,
338
- source: "source:chat:web",
339
- "source_tags[0]": "source:chat",
340
- body: msg.body ? msg.body.toString() : "",
341
- html_body: false,
342
- ui_push_phase: "V3",
343
- status: "0",
344
- offline_threading_id: messageAndOTID,
345
- message_id: messageAndOTID,
346
- threading_id: generateThreadingID(ctx.clientID),
347
- ephemeral_ttl_mode: "0",
348
- manual_retry_cnt: "0",
349
- signatureID: getSignatureID(),
350
- replied_to_message_id: replyToMessage ? replyToMessage.toString() : ""
351
- };
352
- applyPageAuthor(form, msg);
353
- handleLocation(msg, form, callback, () =>
354
- handleSticker(msg, form, callback, () =>
355
- handleAttachment(msg, form, callback, () =>
356
- handleUrl(msg, form, callback, () =>
357
- handleEmoji(msg, form, callback, () =>
358
- handleMention(msg, form, callback, () => {
359
- finalizeHasAttachment(form);
360
- send(form, threadID, messageAndOTID, callback, isGroup);
361
- })
362
- )
307
+ const msgType = getType(msg);
308
+ const threadIDType = getType(threadID);
309
+ const messageIDType = getType(replyToMessage);
310
+ if (msgType !== "String" && msgType !== "Object") return callback({ error: "Message should be of type string or object and not " + msgType + "." });
311
+ if (threadIDType !== "Array" && threadIDType !== "Number" && threadIDType !== "String") return callback({ error: "ThreadID should be of type number, string, or array and not " + threadIDType + "." });
312
+ if (replyToMessage && messageIDType !== "String") return callback({ error: "MessageID should be of type string and not " + threadIDType + "." });
313
+ if (msgType === "String") msg = { body: msg };
314
+ const disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
315
+ if (disallowedProperties.length > 0) return callback({ error: "Disallowed props: `" + disallowedProperties.join(", ") + "`" });
316
+ const messageAndOTID = generateOfflineThreadingID();
317
+ const form = {
318
+ client: "mercury",
319
+ action_type: "ma-type:user-generated-message",
320
+ author: "fbid:" + ctx.userID,
321
+ timestamp: Date.now(),
322
+ timestamp_absolute: "Today",
323
+ timestamp_relative: generateTimestampRelative(),
324
+ timestamp_time_passed: "0",
325
+ is_unread: false,
326
+ is_cleared: false,
327
+ is_forward: false,
328
+ is_filtered_content: false,
329
+ is_filtered_content_bh: false,
330
+ is_filtered_content_account: false,
331
+ is_filtered_content_quasar: false,
332
+ is_filtered_content_invalid_app: false,
333
+ is_spoof_warning: false,
334
+ source: "source:chat:web",
335
+ "source_tags[0]": "source:chat",
336
+ body: msg.body ? msg.body.toString() : "",
337
+ html_body: false,
338
+ ui_push_phase: "V3",
339
+ status: "0",
340
+ offline_threading_id: messageAndOTID,
341
+ message_id: messageAndOTID,
342
+ threading_id: generateThreadingID(ctx.clientID),
343
+ ephemeral_ttl_mode: "0",
344
+ manual_retry_cnt: "0",
345
+ signatureID: getSignatureID(),
346
+ replied_to_message_id: replyToMessage ? replyToMessage.toString() : ""
347
+ };
348
+ applyPageAuthor(form, msg);
349
+ handleLocation(msg, form, callback, () =>
350
+ handleSticker(msg, form, callback, () =>
351
+ handleAttachment(msg, form, callback, () =>
352
+ handleUrl(msg, form, callback, () =>
353
+ handleEmoji(msg, form, callback, () =>
354
+ handleMention(msg, form, callback, () => {
355
+ finalizeHasAttachment(form);
356
+ send(form, threadID, messageAndOTID, callback, isGroup);
357
+ })
363
358
  )
364
359
  )
365
360
  )
366
- );
367
- } catch (e) {
368
- log.error("sendMessage", e);
369
- if (getType(e) === "Object" && e.error === "Not logged in.") ctx.loggedIn = false;
370
- return callback(e);
371
- }
372
-
361
+ )
362
+ );
373
363
  return returnPromise;
374
364
  };
375
365
 
@@ -75,17 +75,15 @@ module.exports = function createListenMqtt(deps) {
75
75
  const mqttClient = ctx.mqttClient;
76
76
  global.mqttClient = mqttClient;
77
77
  mqttClient.on("error", function (err) {
78
- logger("listenMqtt" + err, "error");
79
- mqttClient.end();
80
- if (ctx.globalOptions.autoReconnect) {
81
- listenMqtt(defaultFuncs, api, ctx, globalCallback);
82
- } else {
83
- // utils.checkLiveCookie(ctx, defaultFuncs).then(res => {
84
- // globalCallback({ type: "stop_listen", error: "Connection refused: Server unavailable" }, null);
85
- // }).catch(err => {
86
- // globalCallback({ type: "account_inactive", error: "Maybe your account is blocked by facebook, please login and check at https://facebook.com" }, null);
87
- // });
78
+ const msg = String(err && err.message ? err.message : err || "");
79
+ if (ctx._ending && /No subscription existed/i.test(msg)) {
80
+ logger("MQTT ignore unsubscribe error during shutdown", "warn");
81
+ return;
88
82
  }
83
+ logger(`MQTT error: ${msg}`, "error");
84
+ mqttClient.end();
85
+ logger("MQTT autoReconnect listenMqtt() in 2000ms", "warn");
86
+ setTimeout(() => listenMqtt(defaultFuncs, api, ctx, globalCallback), 2000);
89
87
  });
90
88
  mqttClient.on("connect", function () {
91
89
  if (process.env.OnStatus === undefined) {
@@ -174,7 +172,7 @@ module.exports = function createListenMqtt(deps) {
174
172
  return;
175
173
  }
176
174
  });
177
- mqttClient.on("close", function () {});
178
- mqttClient.on("disconnect", () => {});
175
+ mqttClient.on("close", function () { });
176
+ mqttClient.on("disconnect", () => { });
179
177
  };
180
178
  };
@@ -3,7 +3,7 @@ const mqtt = require("mqtt");
3
3
  const WebSocket = require("ws");
4
4
  const HttpsProxyAgent = require("https-proxy-agent");
5
5
  const EventEmitter = require("events");
6
- const logger = require("../../../func/logger.js");
6
+ const logger = require("../../../func/logger");
7
7
  const { parseAndCheckLogin } = require("../../utils/client");
8
8
  const { buildProxy, buildStream } = require("./detail/buildStream");
9
9
  const { topics } = require("./detail/constants");
@@ -14,63 +14,117 @@ const markDelivery = require("./core/markDelivery");
14
14
  const getTaskResponseData = require("./core/getTaskResponseData");
15
15
  const parseDelta = createParseDelta({ markDelivery, parseAndCheckLogin });
16
16
  const listenMqtt = createListenMqtt({ WebSocket, mqtt, HttpsProxyAgent, buildStream, buildProxy, topics, parseDelta, getTaskResponseData, logger });
17
- const getSeqIDFactory = createGetSeqID({ listenMqtt });
17
+ const getSeqIDFactory = createGetSeqID({ parseAndCheckLogin, listenMqtt, logger });
18
18
  module.exports = function (defaultFuncs, api, ctx) {
19
- const identity = function () {};
19
+ const identity = function () { };
20
20
  let globalCallback = identity;
21
21
  function getSeqIDWrapper() {
22
22
  const form = {
23
- av: ctx.globalOptions.pageID,
23
+ av: ctx.userID,
24
24
  queries: JSON.stringify({
25
- o0: {
26
- doc_id: "3336396659757871",
27
- query_params: {
28
- limit: 1,
29
- before: null,
30
- tags: ["INBOX"],
31
- includeDeliveryReceipts: false,
32
- includeSeqID: true
33
- }
34
- }
25
+ o0: { doc_id: "3336396659757871", query_params: { limit: 1, before: null, tags: ["INBOX"], includeDeliveryReceipts: false, includeSeqID: true } }
35
26
  })
36
27
  };
37
- return getSeqIDFactory(defaultFuncs, api, ctx, globalCallback, form);
28
+ logger("MQTT getSeqID call", "info");
29
+ return getSeqIDFactory(defaultFuncs, api, ctx, globalCallback, form).then(() => {
30
+ logger("MQTT getSeqID done", "info");
31
+ }).catch(e => {
32
+ logger(`MQTT getSeqID error: ${e && e.message ? e.message : e}`, "error");
33
+ });
34
+ }
35
+ function isConnected() {
36
+ return !!(ctx.mqttClient && ctx.mqttClient.connected);
37
+ }
38
+ function unsubAll(cb) {
39
+ if (!isConnected()) return cb && cb();
40
+ let pending = topics.length;
41
+ if (!pending) return cb && cb();
42
+ let done = false;
43
+ topics.forEach(t => {
44
+ ctx.mqttClient.unsubscribe(t, err => {
45
+ const msg = String(err && err.message ? err.message : err || "");
46
+ if (msg && /No subscription existed/i.test(msg)) err = null;
47
+ if (--pending === 0 && !done) {
48
+ done = true;
49
+ cb && cb();
50
+ }
51
+ });
52
+ });
53
+ }
54
+ function endQuietly(next) {
55
+ const finish = () => {
56
+ ctx.mqttClient = undefined;
57
+ ctx.lastSeqId = null;
58
+ ctx.syncToken = undefined;
59
+ ctx.t_mqttCalled = false;
60
+ ctx._ending = false;
61
+ next && next();
62
+ };
63
+ try {
64
+ if (ctx.mqttClient) {
65
+ if (isConnected()) {
66
+ try { ctx.mqttClient.publish("/browser_close", "{}"); } catch (_) { }
67
+ }
68
+ ctx.mqttClient.end(true, finish);
69
+ } else finish();
70
+ } catch (_) {
71
+ finish();
72
+ }
73
+ }
74
+ function delayedReconnect() {
75
+ logger("MQTT reconnect in 2000ms", "info");
76
+ setTimeout(() => getSeqIDWrapper(), 2000);
77
+ }
78
+ function forceCycle() {
79
+ ctx._ending = true;
80
+ logger("MQTT force cycle begin", "warn");
81
+ unsubAll(() => {
82
+ endQuietly(() => {
83
+ delayedReconnect();
84
+ });
85
+ });
38
86
  }
39
87
  return function (callback) {
40
88
  class MessageEmitter extends EventEmitter {
41
89
  stopListening(callback2) {
42
- const cb = callback2 || function () {};
90
+ const cb = callback2 || function () { };
91
+ logger("MQTT stop requested", "info");
43
92
  globalCallback = identity;
44
- if (ctx.mqttClient) {
45
- ctx.mqttClient.unsubscribe("/webrtc");
46
- ctx.mqttClient.unsubscribe("/rtc_multi");
47
- ctx.mqttClient.unsubscribe("/onevc");
48
- ctx.mqttClient.publish("/browser_close", "{}");
49
- ctx.mqttClient.end(false, function (...data) {
50
- cb(data);
51
- ctx.mqttClient = undefined;
52
- });
93
+ if (ctx._autoCycleTimer) {
94
+ clearInterval(ctx._autoCycleTimer);
95
+ ctx._autoCycleTimer = null;
96
+ logger("MQTT auto-cycle cleared", "info");
53
97
  }
98
+ ctx._ending = true;
99
+ unsubAll(() => {
100
+ endQuietly(() => {
101
+ logger("MQTT stopped", "info");
102
+ cb();
103
+ delayedReconnect();
104
+ });
105
+ });
54
106
  }
55
107
  async stopListeningAsync() {
56
- return new Promise(resolve => {
57
- this.stopListening(resolve);
58
- });
108
+ return new Promise(resolve => { this.stopListening(resolve); });
59
109
  }
60
110
  }
61
111
  const msgEmitter = new MessageEmitter();
62
112
  globalCallback = callback || function (error, message) {
63
- if (error) {
64
- return msgEmitter.emit("error", error);
65
- }
113
+ if (error) { logger("MQTT emit error", "error"); return msgEmitter.emit("error", error); }
66
114
  msgEmitter.emit("message", message);
67
115
  };
68
116
  if (!ctx.firstListen) ctx.lastSeqId = null;
69
117
  ctx.syncToken = undefined;
70
118
  ctx.t_mqttCalled = false;
71
- if (!ctx.firstListen || !ctx.lastSeqId) {
72
- getSeqIDWrapper();
73
- } else {
119
+ if (ctx._autoCycleTimer) {
120
+ clearInterval(ctx._autoCycleTimer);
121
+ ctx._autoCycleTimer = null;
122
+ }
123
+ ctx._autoCycleTimer = setInterval(forceCycle, 60 * 60 * 1000);
124
+ logger("MQTT auto-cycle enabled 3600000ms", "info");
125
+ if (!ctx.firstListen || !ctx.lastSeqId) getSeqIDWrapper();
126
+ else {
127
+ logger("MQTT starting listenMqtt", "info");
74
128
  listenMqtt(defaultFuncs, api, ctx, globalCallback);
75
129
  }
76
130
  api.stopListening = msgEmitter.stopListening;
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ const logger = require('../../func/logger');
2
3
 
3
4
  function saveCookies(jar) {
4
5
  return res => {
@@ -77,6 +78,41 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
77
78
  return cfg?.url || "";
78
79
  }
79
80
  };
81
+
82
+ const formatCookie = (arr, service) => {
83
+ const n = String(arr?.[0] || "");
84
+ const v = String(arr?.[1] || "");
85
+ return `${n}=${v}; Domain=.${service}.com; Path=/; Secure`;
86
+ };
87
+
88
+ const maybeAutoLogin = async (resData) => {
89
+ if (ctx.auto_login) {
90
+ const e = new Error("Not logged in.");
91
+ e.error = "Not logged in.";
92
+ e.res = resData;
93
+ throw e;
94
+ }
95
+ if (typeof ctx.performAutoLogin !== "function") {
96
+ const e = new Error("Not logged in.");
97
+ e.error = "Not logged in.";
98
+ e.res = resData;
99
+ throw e;
100
+ }
101
+ ctx.auto_login = true;
102
+ logL("Phiên đăng nhập hết hạn", "warn");
103
+ const ok = await ctx.performAutoLogin();
104
+ if (ok) {
105
+ logL("Auto login successful! Restarting...", "AUTO-LOGIN");
106
+ ctx.auto_login = false;
107
+ process.exit(1);
108
+ } else {
109
+ ctx.auto_login = false;
110
+ const e = new Error("Not logged in.");
111
+ e.error = "Not logged in.";
112
+ e.res = resData;
113
+ throw e;
114
+ }
115
+ };
80
116
  return async (res) => {
81
117
  const status = res?.status ?? 0;
82
118
  if (status >= 500 && status < 600) {
@@ -90,11 +126,16 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
90
126
  const retryTime = Math.floor(Math.random() * 5000);
91
127
  await delay(retryTime);
92
128
  const url = buildUrl(res?.config);
129
+ const method = String(res?.config?.method || "GET").toUpperCase();
93
130
  const ctype = String(headerOf(res?.config?.headers, "content-type") || "").toLowerCase();
94
131
  const isMultipart = ctype.includes("multipart/form-data");
95
132
  const payload = res?.config?.data;
96
133
  const params = res?.config?.params;
97
134
  retryCount += 1;
135
+ if (method === "GET") {
136
+ const newData = await http.get(url, ctx.jar, params || null, ctx.globalOptions, ctx);
137
+ return await parseAndCheckLogin(ctx, http, retryCount)(newData);
138
+ }
98
139
  if (isMultipart) {
99
140
  const newData = await http.postFormData(url, ctx.jar, payload, params, ctx.globalOptions, ctx);
100
141
  return await parseAndCheckLogin(ctx, http, retryCount)(newData);
@@ -123,17 +164,17 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
123
164
  throw err;
124
165
  }
125
166
  const method = String(res?.config?.method || "GET").toUpperCase();
126
- if (parsed.redirect && method === "GET") {
167
+ if (parsed?.redirect && method === "GET") {
127
168
  const redirectRes = await http.get(parsed.redirect, ctx.jar, null, ctx.globalOptions, ctx);
128
169
  return await parseAndCheckLogin(ctx, http)(redirectRes);
129
170
  }
130
- if (parsed.jsmods && parsed.jsmods.require && Array.isArray(parsed.jsmods.require[0]) && parsed.jsmods.require[0][0] === "Cookie") {
131
- parsed.jsmods.require[0][3][0] = parsed.jsmods.require[0][3][0].replace("_js_", "");
171
+ if (parsed?.jsmods && parsed.jsmods.require && Array.isArray(parsed.jsmods.require[0]) && parsed.jsmods.require[0][0] === "Cookie") {
172
+ parsed.jsmods.require[0][3][0] = String(parsed.jsmods.require[0][3][0] || "").replace("_js_", "");
132
173
  const requireCookie = parsed.jsmods.require[0][3];
133
- ctx.jar.setCookie(formatCookie(requireCookie, "facebook"), "https://www.facebook.com");
134
- ctx.jar.setCookie(formatCookie(requireCookie, "messenger"), "https://www.messenger.com");
174
+ await ctx.jar.setCookie(formatCookie(requireCookie, "facebook"), "https://www.facebook.com");
175
+ await ctx.jar.setCookie(formatCookie(requireCookie, "messenger"), "https://www.messenger.com");
135
176
  }
136
- if (parsed.jsmods && Array.isArray(parsed.jsmods.require)) {
177
+ if (parsed?.jsmods && Array.isArray(parsed.jsmods.require)) {
137
178
  for (const item of parsed.jsmods.require) {
138
179
  if (item[0] === "DTSG" && item[1] === "setToken") {
139
180
  ctx.fb_dtsg = item[3][0];
@@ -143,11 +184,26 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
143
184
  }
144
185
  }
145
186
  }
146
- if (parsed.error === 1357001) {
187
+ if (parsed?.error === 1357001) {
147
188
  const err = new Error("Facebook blocked the login");
148
189
  err.error = "Not logged in.";
149
190
  throw err;
150
191
  }
192
+ const resData = parsed;
193
+ const resStr = JSON.stringify(resData);
194
+ if (resStr.includes("XCheckpointFBScrapingWarningController") || resStr.includes("601051028565049")) {
195
+ await maybeAutoLogin(resData);
196
+ }
197
+ if (resStr.includes("https://www.facebook.com/login.php?") || String(parsed?.redirect || "").includes("login.php?")) {
198
+ await maybeAutoLogin(resData);
199
+ }
200
+ if (resStr.includes("1501092823525282")) {
201
+ logger("Bot checkpoint 282 detected, please check the account!", "error");
202
+ process.exit(0);
203
+ }
204
+ if (resStr.includes("828281030927956")) {
205
+ logger("Bot checkpoint 956 detected, please check the account!", "error");
206
+ }
151
207
  return parsed;
152
208
  };
153
209
  }
package/func/login.js DELETED
File without changes