@dongdev/fca-unofficial 3.0.25 → 3.0.28

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.
Files changed (35) hide show
  1. package/.gitattributes +2 -0
  2. package/CHANGELOG.md +196 -190
  3. package/DOCS.md +3 -6
  4. package/Fca_Database/database.sqlite +0 -0
  5. package/LICENSE-MIT +1 -1
  6. package/README.md +1 -1
  7. package/index.d.ts +745 -746
  8. package/module/config.js +29 -33
  9. package/module/login.js +133 -136
  10. package/module/loginHelper.js +1240 -1048
  11. package/module/options.js +44 -45
  12. package/package.json +81 -82
  13. package/src/api/messaging/changeAdminStatus.js +56 -56
  14. package/src/api/messaging/changeGroupImage.js +2 -1
  15. package/src/api/messaging/changeThreadEmoji.js +47 -47
  16. package/src/api/messaging/createPoll.js +25 -25
  17. package/src/api/messaging/deleteMessage.js +110 -30
  18. package/src/api/messaging/forwardAttachment.js +28 -28
  19. package/src/api/messaging/removeUserFromGroup.js +28 -73
  20. package/src/api/messaging/sendMessage.js +15 -17
  21. package/src/api/messaging/sendTypingIndicator.js +23 -23
  22. package/src/api/messaging/setMessageReaction.js +57 -60
  23. package/src/api/messaging/setTitle.js +47 -47
  24. package/src/api/messaging/uploadAttachment.js +471 -73
  25. package/src/api/socket/core/connectMqtt.js +250 -250
  26. package/src/api/socket/core/emitAuth.js +1 -1
  27. package/src/api/socket/core/getSeqID.js +322 -40
  28. package/src/api/socket/core/parseDelta.js +368 -377
  29. package/src/api/socket/listenMqtt.js +371 -360
  30. package/src/utils/client.js +2 -312
  31. package/src/utils/cookies.js +68 -0
  32. package/src/utils/format.js +117 -90
  33. package/src/utils/loginParser.js +347 -0
  34. package/src/utils/messageFormat.js +1173 -0
  35. package/src/api/socket/core/markDelivery.js +0 -12
@@ -1,1048 +1,1240 @@
1
- "use strict";
2
- const fs = require("fs");
3
- const path = require("path");
4
- const models = require("../src/database/models");
5
- const logger = require("../func/logger");
6
- const { get, post, jar, makeDefaults } = require("../src/utils/request");
7
- const { saveCookies, getAppState } = require("../src/utils/client");
8
- const { getFrom } = require("../src/utils/constants");
9
- const { loadConfig } = require("./config");
10
- const { config } = loadConfig();
11
- const { v4: uuidv4 } = require("uuid");
12
- const { CookieJar } = require("tough-cookie");
13
- const { wrapper } = require("axios-cookiejar-support");
14
- const axiosBase = require("axios");
15
- const qs = require("querystring");
16
- const crypto = require("crypto");
17
- const { TOTP } = require("totp-generator");
18
-
19
- const regions = [
20
- { code: "PRN", name: "Pacific Northwest Region", location: "Khu vực Tây Bắc Thái Bình Dương" },
21
- { code: "VLL", name: "Valley Region", location: "Valley" },
22
- { code: "ASH", name: "Ashburn Region", location: "Ashburn" },
23
- { code: "DFW", name: "Dallas/Fort Worth Region", location: "Dallas/Fort Worth" },
24
- { code: "LLA", name: "Los Angeles Region", location: "Los Angeles" },
25
- { code: "FRA", name: "Frankfurt", location: "Frankfurt" },
26
- { code: "SIN", name: "Singapore", location: "Singapore" },
27
- { code: "NRT", name: "Tokyo", location: "Japan" },
28
- { code: "HKG", name: "Hong Kong", location: "Hong Kong" },
29
- { code: "SYD", name: "Sydney", location: "Sydney" },
30
- { code: "PNB", name: "Pacific Northwest - Beta", location: "Pacific Northwest " }
31
- ];
32
-
33
- const REGION_MAP = new Map(regions.map(r => [r.code, r]));
34
-
35
- function parseRegion(html) {
36
- try {
37
- const m1 = html.match(/"endpoint":"([^"]+)"/);
38
- const m2 = m1 ? null : html.match(/endpoint\\":\\"([^\\"]+)\\"/);
39
- const raw = (m1 && m1[1]) || (m2 && m2[1]);
40
- if (!raw) return "PRN";
41
- const endpoint = raw.replace(/\\\//g, "/");
42
- const url = new URL(endpoint);
43
- const rp = url.searchParams ? url.searchParams.get("region") : null;
44
- return rp ? rp.toUpperCase() : "PRN";
45
- } catch {
46
- return "PRN";
47
- }
48
- }
49
-
50
- function mask(s, keep = 3) {
51
- if (!s) return "";
52
- const n = s.length;
53
- return n <= keep ? "*".repeat(n) : s.slice(0, keep) + "*".repeat(Math.max(0, n - keep));
54
- }
55
-
56
- function md5(s) {
57
- return crypto.createHash("md5").update(s).digest("hex");
58
- }
59
-
60
- function randomString(length = 24) {
61
- let s = "abcdefghijklmnopqrstuvwxyz";
62
- let out = s.charAt(Math.floor(Math.random() * s.length));
63
- for (let i = 1; i < length; i++) out += "abcdefghijklmnopqrstuvwxyz0123456789".charAt(Math.floor(36 * Math.random()));
64
- return out;
65
- }
66
-
67
- function sortObject(o) {
68
- const keys = Object.keys(o).sort();
69
- const x = {};
70
- for (const k of keys) x[k] = o[k];
71
- return x;
72
- }
73
-
74
- function encodeSig(obj) {
75
- let data = "";
76
- for (const k of Object.keys(obj)) data += `${k}=${obj[k]}`;
77
- return md5(data + "62f8ce9f74b12f84c123cc23437a4a32");
78
- }
79
-
80
- function rand(min, max) {
81
- return Math.floor(Math.random() * (max - min + 1)) + min;
82
- }
83
-
84
- function choice(arr) {
85
- return arr[Math.floor(Math.random() * arr.length)];
86
- }
87
-
88
- function randomBuildId() {
89
- const prefixes = ["QP1A", "RP1A", "SP1A", "TP1A", "UP1A", "AP4A"];
90
- return `${choice(prefixes)}.${rand(180000, 250000)}.${rand(10, 99)}`;
91
- }
92
-
93
- function randomResolution() {
94
- const presets = [{ w: 720, h: 1280, d: 2.0 }, { w: 1080, h: 1920, d: 2.625 }, { w: 1080, h: 2400, d: 3.0 }, { w: 1440, h: 3040, d: 3.5 }, { w: 1440, h: 3200, d: 4.0 }];
95
- return choice(presets);
96
- }
97
-
98
- function randomFbav() {
99
- return `${rand(390, 499)}.${rand(0, 3)}.${rand(0, 2)}.${rand(10, 60)}.${rand(100, 999)}`;
100
- }
101
-
102
- function randomOrcaUA() {
103
- const androidVersions = ["8.1.0", "9", "10", "11", "12", "13", "14"];
104
- const devices = [{ brand: "samsung", model: "SM-G996B" }, { brand: "samsung", model: "SM-S908E" }, { brand: "Xiaomi", model: "M2101K9AG" }, { brand: "OPPO", model: "CPH2219" }, { brand: "vivo", model: "V2109" }, { brand: "HUAWEI", model: "VOG-L29" }, { brand: "asus", model: "ASUS_I001DA" }, { brand: "Google", model: "Pixel 6" }, { brand: "realme", model: "RMX2170" }];
105
- const carriers = ["Viettel Telecom", "Mobifone", "Vinaphone", "T-Mobile", "Verizon", "AT&T", "Telkomsel", "Jio", "NTT DOCOMO", "Vodafone", "Orange"];
106
- const locales = ["vi_VN", "en_US", "en_GB", "id_ID", "th_TH", "fr_FR", "de_DE", "es_ES", "pt_BR"];
107
- const archs = ["arm64-v8a", "armeabi-v7a"];
108
- const a = choice(androidVersions);
109
- const d = choice(devices);
110
- const b = randomBuildId();
111
- const r = randomResolution();
112
- const fbav = randomFbav();
113
- const fbbv = rand(320000000, 520000000);
114
- const arch = `${choice(archs)}:${choice(archs)}`;
115
- const ua = `Dalvik/2.1.0 (Linux; U; Android ${a}; ${d.model} Build/${b}) [FBAN/Orca-Android;FBAV/${fbav};FBPN/com.facebook.orca;FBLC/${choice(locales)};FBBV/${fbbv};FBCR/${choice(carriers)};FBMF/${d.brand};FBBD/${d.brand};FBDV/${d.model};FBSV/${a};FBCA/${arch};FBDM/{density=${r.d.toFixed(1)},width=${r.w},height=${r.h}};FB_FW/1;]`;
116
- return ua;
117
- }
118
-
119
- const MOBILE_UA = randomOrcaUA();
120
-
121
- function buildHeaders(url, extra = {}) {
122
- const u = new URL(url);
123
- return { "content-type": "application/x-www-form-urlencoded", "x-fb-http-engine": "Liger", "user-agent": MOBILE_UA, Host: u.host, Origin: "https://www.facebook.com", Referer: "https://www.facebook.com/", Connection: "keep-alive", ...extra };
124
- }
125
-
126
- const genTotp = async secret => {
127
- const cleaned = String(secret || "").replace(/\s+/g, "").toUpperCase();
128
- const r = await TOTP.generate(cleaned);
129
- return typeof r === "object" ? r.otp : r;
130
- };
131
-
132
- function normalizeCookieHeaderString(s) {
133
- let str = String(s || "").trim();
134
- if (!str) return [];
135
- if (/^cookie\s*:/i.test(str)) str = str.replace(/^cookie\s*:/i, "").trim();
136
- str = str.replace(/\r?\n/g, " ").replace(/\s*;\s*/g, ";");
137
- const parts = str.split(";").map(v => v.trim()).filter(Boolean);
138
- const out = [];
139
- for (const p of parts) {
140
- const eq = p.indexOf("=");
141
- if (eq <= 0) continue;
142
- const k = p.slice(0, eq).trim();
143
- const v = p.slice(eq + 1).trim().replace(/^"(.*)"$/, "$1");
144
- if (!k) continue;
145
- out.push(`${k}=${v}`);
146
- }
147
- return out;
148
- }
149
-
150
- function setJarFromPairs(j, pairs, domain) {
151
- const expires = new Date(Date.now() + 31536e6).toUTCString();
152
- for (const kv of pairs) {
153
- const cookieStr = `${kv}; expires=${expires}; domain=${domain}; path=/;`;
154
- try {
155
- if (typeof j.setCookieSync === "function") j.setCookieSync(cookieStr, "https://www.facebook.com");
156
- else j.setCookie(cookieStr, "https://www.facebook.com");
157
- } catch { }
158
- }
159
- }
160
-
161
- function cookieHeaderFromJar(j) {
162
- const urls = ["https://www.facebook.com", "https://www.messenger.com"];
163
- const seen = new Set();
164
- const parts = [];
165
- for (const u of urls) {
166
- let s = "";
167
- try {
168
- s = typeof j.getCookieStringSync === "function" ? j.getCookieStringSync(u) : "";
169
- } catch { }
170
- if (!s) continue;
171
- for (const kv of s.split(";")) {
172
- const t = kv.trim();
173
- const name = t.split("=")[0];
174
- if (!name || seen.has(name)) continue;
175
- seen.add(name);
176
- parts.push(t);
177
- }
178
- }
179
- return parts.join("; ");
180
- }
181
-
182
- let uniqueIndexEnsured = false;
183
-
184
- function getBackupModel() {
185
- try {
186
- if (!models || !models.sequelize || !models.Sequelize) return null;
187
- const sequelize = models.sequelize;
188
-
189
- // Validate that sequelize is a proper Sequelize instance
190
- if (!sequelize || typeof sequelize.define !== "function") return null;
191
-
192
- const { DataTypes } = models.Sequelize;
193
- if (sequelize.models && sequelize.models.AppStateBackup) return sequelize.models.AppStateBackup;
194
- const dialect = typeof sequelize.getDialect === "function" ? sequelize.getDialect() : "sqlite";
195
- const LongText = (dialect === "mysql" || dialect === "mariadb") ? DataTypes.TEXT("long") : DataTypes.TEXT;
196
-
197
- try {
198
- const AppStateBackup = sequelize.define(
199
- "AppStateBackup",
200
- {
201
- id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
202
- userID: { type: DataTypes.STRING, allowNull: false },
203
- type: { type: DataTypes.STRING, allowNull: false },
204
- data: { type: LongText }
205
- },
206
- { tableName: "app_state_backups", timestamps: true, indexes: [{ unique: true, fields: ["userID", "type"] }] }
207
- );
208
- return AppStateBackup;
209
- } catch (defineError) {
210
- // If define fails, log and return null
211
- logger(`Failed to define AppStateBackup model: ${defineError && defineError.message ? defineError.message : String(defineError)}`, "warn");
212
- return null;
213
- }
214
- } catch (e) {
215
- // Silently handle any errors in getBackupModel
216
- return null;
217
- }
218
- }
219
-
220
- async function ensureUniqueIndex(sequelize) {
221
- if (uniqueIndexEnsured || !sequelize) return;
222
- try {
223
- if (typeof sequelize.getQueryInterface !== "function") return;
224
- await sequelize.getQueryInterface().addIndex("app_state_backups", ["userID", "type"], { unique: true, name: "app_state_user_type_unique" });
225
- } catch { }
226
- uniqueIndexEnsured = true;
227
- }
228
-
229
- async function upsertBackup(Model, userID, type, data) {
230
- const where = { userID: String(userID || ""), type };
231
- const row = await Model.findOne({ where });
232
- if (row) {
233
- await row.update({ data });
234
- logger(`Overwrote existing ${type} backup for user ${where.userID}`, "info");
235
- return;
236
- }
237
- await Model.create({ ...where, data });
238
- logger(`Created new ${type} backup for user ${where.userID}`, "info");
239
- }
240
-
241
- async function backupAppStateSQL(j, userID) {
242
- try {
243
- const Model = getBackupModel();
244
- if (!Model) return;
245
- if (!models || !models.sequelize) return;
246
- await Model.sync();
247
- await ensureUniqueIndex(models.sequelize);
248
- const appJson = getAppState(j);
249
- const ck = cookieHeaderFromJar(j);
250
- await upsertBackup(Model, userID, "appstate", JSON.stringify(appJson));
251
- await upsertBackup(Model, userID, "cookie", ck);
252
- try {
253
- const out = path.join(process.cwd(), "appstate.json");
254
- fs.writeFileSync(out, JSON.stringify(appJson, null, 2));
255
- } catch { }
256
- logger("Backup stored (overwrite mode)", "info");
257
- } catch (e) {
258
- logger(`Failed to save appstate backup ${e && e.message ? e.message : String(e)}`, "warn");
259
- }
260
- }
261
-
262
- async function getLatestBackup(userID, type) {
263
- try {
264
- const Model = getBackupModel();
265
- if (!Model) return null;
266
- const row = await Model.findOne({ where: { userID: String(userID || ""), type } });
267
- return row ? row.data : null;
268
- } catch {
269
- return null;
270
- }
271
- }
272
-
273
- async function getLatestBackupAny(type) {
274
- try {
275
- const Model = getBackupModel();
276
- if (!Model) return null;
277
- const row = await Model.findOne({ where: { type }, order: [["updatedAt", "DESC"]] });
278
- return row ? row.data : null;
279
- } catch {
280
- return null;
281
- }
282
- }
283
-
284
- 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;]";
285
-
286
- function encodesig(obj) {
287
- let data = "";
288
- Object.keys(obj).forEach(k => { data += `${k}=${obj[k]}`; });
289
- return md5(data + "62f8ce9f74b12f84c123cc23437a4a32");
290
- }
291
-
292
- function sort(obj) {
293
- const keys = Object.keys(obj).sort();
294
- const out = {};
295
- for (const k of keys) out[k] = obj[k];
296
- return out;
297
- }
298
-
299
- async function setJarCookies(j, appstate) {
300
- const tasks = [];
301
- for (const c of appstate) {
302
- const cookieName = c.name || c.key;
303
- const cookieValue = c.value;
304
- if (!cookieName || cookieValue === undefined) continue;
305
-
306
- const cookieDomain = c.domain || ".facebook.com";
307
- const cookiePath = c.path || "/";
308
- const dom = cookieDomain.replace(/^\./, "");
309
-
310
- // Handle expirationDate (can be in seconds or milliseconds)
311
- let expiresStr = "";
312
- if (c.expirationDate !== undefined) {
313
- let expiresDate;
314
- if (typeof c.expirationDate === "number") {
315
- // If expirationDate is less than a year from now in seconds, treat as seconds
316
- // Otherwise treat as milliseconds
317
- const now = Date.now();
318
- const oneYearInMs = 365 * 24 * 60 * 60 * 1000;
319
- if (c.expirationDate < (now + oneYearInMs) / 1000) {
320
- expiresDate = new Date(c.expirationDate * 1000);
321
- } else {
322
- expiresDate = new Date(c.expirationDate);
323
- }
324
- } else {
325
- expiresDate = new Date(c.expirationDate);
326
- }
327
- expiresStr = `; expires=${expiresDate.toUTCString()}`;
328
- } else if (c.expires) {
329
- const expiresDate = typeof c.expires === "number" ? new Date(c.expires) : new Date(c.expires);
330
- expiresStr = `; expires=${expiresDate.toUTCString()}`;
331
- }
332
-
333
- // Helper function to build cookie string
334
- const buildCookieString = (domainOverride = null) => {
335
- const domain = domainOverride || cookieDomain;
336
- let cookieParts = [`${cookieName}=${cookieValue}${expiresStr}`];
337
- cookieParts.push(`Domain=${domain}`);
338
- cookieParts.push(`Path=${cookiePath}`);
339
-
340
- // Add Secure flag if secure is true
341
- if (c.secure === true) {
342
- cookieParts.push("Secure");
343
- }
344
-
345
- // Add HttpOnly flag if httpOnly is true
346
- if (c.httpOnly === true) {
347
- cookieParts.push("HttpOnly");
348
- }
349
-
350
- // Add SameSite attribute if provided
351
- if (c.sameSite) {
352
- const sameSiteValue = String(c.sameSite).toLowerCase();
353
- if (["strict", "lax", "none"].includes(sameSiteValue)) {
354
- cookieParts.push(`SameSite=${sameSiteValue.charAt(0).toUpperCase() + sameSiteValue.slice(1)}`);
355
- }
356
- }
357
-
358
- return cookieParts.join("; ");
359
- };
360
-
361
- // Determine target URLs and cookie strings based on domain
362
- const cookieConfigs = [];
363
-
364
- // For .facebook.com domain, set for both facebook.com and messenger.com
365
- if (cookieDomain === ".facebook.com" || cookieDomain === "facebook.com") {
366
- // Set for facebook.com with .facebook.com domain
367
- cookieConfigs.push({ url: `http://${dom}${cookiePath}`, cookieStr: buildCookieString() });
368
- cookieConfigs.push({ url: `https://${dom}${cookiePath}`, cookieStr: buildCookieString() });
369
- cookieConfigs.push({ url: `http://www.${dom}${cookiePath}`, cookieStr: buildCookieString() });
370
- cookieConfigs.push({ url: `https://www.${dom}${cookiePath}`, cookieStr: buildCookieString() });
371
-
372
- // Set for messenger.com with .messenger.com domain (or without domain for host-only)
373
- // Use .messenger.com domain to allow cross-subdomain sharing
374
- cookieConfigs.push({ url: `http://messenger.com${cookiePath}`, cookieStr: buildCookieString(".messenger.com") });
375
- cookieConfigs.push({ url: `https://messenger.com${cookiePath}`, cookieStr: buildCookieString(".messenger.com") });
376
- cookieConfigs.push({ url: `http://www.messenger.com${cookiePath}`, cookieStr: buildCookieString(".messenger.com") });
377
- cookieConfigs.push({ url: `https://www.messenger.com${cookiePath}`, cookieStr: buildCookieString(".messenger.com") });
378
- } else if (cookieDomain === ".messenger.com" || cookieDomain === "messenger.com") {
379
- // Set for messenger.com only
380
- const messengerDom = cookieDomain.replace(/^\./, "");
381
- cookieConfigs.push({ url: `http://${messengerDom}${cookiePath}`, cookieStr: buildCookieString() });
382
- cookieConfigs.push({ url: `https://${messengerDom}${cookiePath}`, cookieStr: buildCookieString() });
383
- cookieConfigs.push({ url: `http://www.${messengerDom}${cookiePath}`, cookieStr: buildCookieString() });
384
- cookieConfigs.push({ url: `https://www.${messengerDom}${cookiePath}`, cookieStr: buildCookieString() });
385
- } else {
386
- // For other domains, set normally
387
- cookieConfigs.push({ url: `http://${dom}${cookiePath}`, cookieStr: buildCookieString() });
388
- cookieConfigs.push({ url: `https://${dom}${cookiePath}`, cookieStr: buildCookieString() });
389
- cookieConfigs.push({ url: `http://www.${dom}${cookiePath}`, cookieStr: buildCookieString() });
390
- cookieConfigs.push({ url: `https://www.${dom}${cookiePath}`, cookieStr: buildCookieString() });
391
- }
392
-
393
- // Set cookie for all target URLs, silently catch domain errors
394
- for (const config of cookieConfigs) {
395
- tasks.push(j.setCookie(config.cookieStr, config.url).catch((err) => {
396
- // Silently ignore domain mismatch errors for cross-domain cookies
397
- // These are expected when setting cookies across domains
398
- if (err && err.message && err.message.includes("Cookie not in this host's domain")) {
399
- return; // Expected error, ignore
400
- }
401
- // Log other errors but don't throw
402
- return;
403
- }));
404
- }
405
- }
406
- await Promise.all(tasks);
407
- }
408
-
409
- async function loginViaGraph(username, password, twofactorSecretOrCode, i_user, externalJar) {
410
- const cookieJar = externalJar instanceof CookieJar ? externalJar : new CookieJar();
411
- const client = wrapper(axiosBase.create({ jar: cookieJar, withCredentials: true, timeout: 30000, validateStatus: () => true }));
412
- const device_id = uuidv4();
413
- const family_device_id = device_id;
414
- const machine_id = randomString(24);
415
- const base = {
416
- adid: "00000000-0000-0000-0000-000000000000",
417
- format: "json",
418
- device_id,
419
- email: username,
420
- password,
421
- generate_analytics_claim: "1",
422
- community_id: "",
423
- cpl: "true",
424
- try_num: "1",
425
- family_device_id,
426
- secure_family_device_id: "",
427
- credentials_type: "password",
428
- enroll_misauth: "false",
429
- generate_session_cookies: "1",
430
- source: "login",
431
- generate_machine_id: "1",
432
- jazoest: "22297",
433
- meta_inf_fbmeta: "NO_FILE",
434
- advertiser_id: "00000000-0000-0000-0000-000000000000",
435
- currently_logged_in_userid: "0",
436
- locale: "vi_VN",
437
- client_country_code: "VN",
438
- fb_api_req_friendly_name: "authenticate",
439
- fb_api_caller_class: "AuthOperations$PasswordAuthOperation",
440
- api_key: "256002347743983",
441
- access_token: "256002347743983|374e60f8b9bb6b8cbb30f78030438895"
442
- };
443
- const headers = {
444
- "User-Agent": MESSENGER_USER_AGENT,
445
- "Accept-Encoding": "gzip, deflate",
446
- "Content-Type": "application/x-www-form-urlencoded",
447
- "x-fb-connection-quality": "EXCELLENT",
448
- "x-fb-sim-hni": "45204",
449
- "x-fb-net-hni": "45204",
450
- "x-fb-connection-type": "WIFI",
451
- "x-tigon-is-retry": "False",
452
- "x-fb-friendly-name": "authenticate",
453
- "x-fb-request-analytics-tags": '{"network_tags":{"retry_attempt":"0"},"application_tags":"unknown"}',
454
- "x-fb-http-engine": "Liger",
455
- "x-fb-client-ip": "True",
456
- "x-fb-server-cluster": "True",
457
- authorization: `OAuth ${base.access_token}`
458
- };
459
- const form1 = { ...base };
460
- form1.sig = encodesig(sort(form1));
461
- const res1 = await client.request({ url: "https://b-graph.facebook.com/auth/login", method: "post", data: qs.stringify(form1), headers });
462
- if (res1.status === 200 && res1.data && res1.data.session_cookies) {
463
- const appstate = res1.data.session_cookies.map(c => ({ key: c.name, value: c.value, domain: c.domain, path: c.path }));
464
- const cUserCookie = appstate.find(c => c.key === "c_user");
465
- if (i_user) appstate.push({ key: "i_user", value: i_user, domain: ".facebook.com", path: "/" });
466
- else if (cUserCookie) appstate.push({ key: "i_user", value: cUserCookie.value, domain: ".facebook.com", path: "/" });
467
- await setJarCookies(cookieJar, appstate);
468
- let eaau = null;
469
- let eaad6v7 = null;
470
- try {
471
- 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}` } });
472
- eaau = r1.data && r1.data.access_token ? r1.data.access_token : null;
473
- } catch { }
474
- try {
475
- 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}` } });
476
- eaad6v7 = r2.data && r2.data.access_token ? r2.data.access_token : null;
477
- } catch { }
478
- 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 };
479
- }
480
- const err = res1 && res1.data && res1.data.error ? res1.data.error : {};
481
- if (err && err.code === 406) {
482
- const data = err.error_data || {};
483
- let code = null;
484
- if (twofactorSecretOrCode && /^\d{6}$/.test(String(twofactorSecretOrCode))) code = String(twofactorSecretOrCode);
485
- else if (twofactorSecretOrCode) {
486
- try {
487
- const clean = decodeURI(twofactorSecretOrCode).replace(/\s+/g, "").toUpperCase();
488
- const { otp } = await TOTP.generate(clean);
489
- code = otp;
490
- } catch { }
491
- } else if (config.credentials?.twofactor) {
492
- const { otp } = await TOTP.generate(String(config.credentials.twofactor).replace(/\s+/g, "").toUpperCase());
493
- code = otp;
494
- }
495
- if (!code) return { ok: false, message: "2FA required" };
496
- const form2 = {
497
- ...base,
498
- credentials_type: "two_factor",
499
- twofactor_code: code,
500
- userid: data.uid || username,
501
- first_factor: data.login_first_factor || "",
502
- machine_id: data.machine_id || machine_id
503
- };
504
- form2.sig = encodesig(sort(form2));
505
- const res2 = await client.request({ url: "https://b-graph.facebook.com/auth/login", method: "post", data: qs.stringify(form2), headers });
506
- if (res2.status === 200 && res2.data && res2.data.session_cookies) {
507
- const appstate = res2.data.session_cookies.map(c => ({ key: c.name, value: c.value, domain: c.domain, path: c.path }));
508
- const cUserCookie = appstate.find(c => c.key === "c_user");
509
- if (i_user) appstate.push({ key: "i_user", value: i_user, domain: ".facebook.com", path: "/" });
510
- else if (cUserCookie) appstate.push({ key: "i_user", value: cUserCookie.value, domain: ".facebook.com", path: "/" });
511
- await setJarCookies(cookieJar, appstate);
512
- let eaau = null;
513
- let eaad6v7 = null;
514
- try {
515
- 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}` } });
516
- eaau = r1.data && r1.data.access_token ? r1.data.access_token : null;
517
- } catch { }
518
- try {
519
- 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}` } });
520
- eaad6v7 = r2.data && r2.data.access_token ? r2.data.access_token : null;
521
- } catch { }
522
- 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 };
523
- }
524
- return { ok: false, message: "2FA failed" };
525
- }
526
- return { ok: false, message: "Login failed" };
527
- }
528
-
529
- async function tokens(username, password, twofactor = null) {
530
- const t0 = process.hrtime.bigint();
531
- if (!username || !password) return { status: false, message: "Please provide email and password" };
532
- logger(`AUTO-LOGIN: Initialize login ${mask(username, 2)}`, "info");
533
- const res = await loginViaGraph(username, password, twofactor, null, jar);
534
- if (res && res.ok && Array.isArray(res.cookies)) {
535
- logger(`AUTO-LOGIN: Login success ${res.cookies.length} cookies`, "info");
536
- const t1 = Number(process.hrtime.bigint() - t0) / 1e6;
537
- logger(`Done success login ${Math.round(t1)}ms`, "info");
538
- return { status: true, cookies: res.cookies };
539
- }
540
- if (res && res.message === "2FA required") {
541
- logger("AUTO-LOGIN: 2FA required but secret missing", "warn");
542
- return { status: false, message: "Please provide the 2FA secret!" };
543
- }
544
- return { status: false, message: res && res.message ? res.message : "Login failed" };
545
- }
546
-
547
- async function hydrateJarFromDB(userID) {
548
- try {
549
- let ck = null;
550
- let app = null;
551
- if (userID) {
552
- ck = await getLatestBackup(userID, "cookie");
553
- app = await getLatestBackup(userID, "appstate");
554
- } else {
555
- ck = await getLatestBackupAny("cookie");
556
- app = await getLatestBackupAny("appstate");
557
- }
558
- if (ck) {
559
- const pairs = normalizeCookieHeaderString(ck);
560
- if (pairs.length) {
561
- setJarFromPairs(jar, pairs, ".facebook.com");
562
- return true;
563
- }
564
- }
565
- if (app) {
566
- let parsed = null;
567
- try {
568
- parsed = JSON.parse(app);
569
- } catch { }
570
- if (Array.isArray(parsed)) {
571
- const pairs = parsed.map(c => [c.name || c.key, c.value].join("="));
572
- setJarFromPairs(jar, pairs, ".facebook.com");
573
- return true;
574
- }
575
- }
576
- return false;
577
- } catch {
578
- return false;
579
- }
580
- }
581
-
582
- async function tryAutoLoginIfNeeded(currentHtml, currentCookies, globalOptions, ctxRef, hadAppStateInput = false) {
583
- const getUID = cs =>
584
- cs.find(c => c.key === "i_user")?.value ||
585
- cs.find(c => c.key === "c_user")?.value ||
586
- cs.find(c => c.name === "i_user")?.value ||
587
- cs.find(c => c.name === "c_user")?.value;
588
- const htmlUID = body => {
589
- const s = typeof body === "string" ? body : String(body ?? "");
590
- return s.match(/"USER_ID"\s*:\s*"(\d+)"/)?.[1] || s.match(/\["CurrentUserInitialData",\[\],\{.*?"USER_ID":"(\d+)".*?\},\d+\]/)?.[1];
591
- };
592
- let userID = getUID(currentCookies);
593
- // Also try to extract userID from HTML if not found in cookies
594
- if (!userID) {
595
- userID = htmlUID(currentHtml);
596
- }
597
- if (userID) return { html: currentHtml, cookies: currentCookies, userID };
598
- // If appState/Cookie was provided and is not dead (not checkpointed), skip backup
599
- if (hadAppStateInput) {
600
- const isCheckpoint = currentHtml.includes("/checkpoint/block/?next");
601
- if (!isCheckpoint) {
602
- // AppState provided and not checkpointed, but userID not found
603
- // This might be a temporary issue - try to refresh cookies from jar
604
- try {
605
- const refreshedCookies = await Promise.resolve(jar.getCookies("https://www.facebook.com"));
606
- userID = getUID(refreshedCookies);
607
- if (userID) {
608
- return { html: currentHtml, cookies: refreshedCookies, userID };
609
- }
610
- } catch { }
611
- // If still no userID, skip backup and throw error
612
- throw new Error("Missing user cookie from provided appState");
613
- }
614
- // AppState is dead (checkpointed), proceed to backup/email login
615
- }
616
- const hydrated = await hydrateJarFromDB(null);
617
- if (hydrated) {
618
- logger("AppState backup live proceeding to login", "info");
619
- const initial = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
620
- const resB = (await ctxRef.bypassAutomation(initial, jar)) || initial;
621
- const htmlB = resB && resB.data ? resB.data : "";
622
- if (htmlB.includes("/checkpoint/block/?next")) throw new Error("Checkpoint");
623
- const cookiesB = await Promise.resolve(jar.getCookies("https://www.facebook.com"));
624
- const uidB = getUID(cookiesB);
625
- if (uidB) return { html: htmlB, cookies: cookiesB, userID: uidB };
626
- }
627
- if (config.autoLogin !== true) throw new Error("AppState backup die — Auto-login is disabled");
628
- logger("AppState backup die — proceeding to email/password login", "warn");
629
- const u = config.credentials?.email;
630
- const p = config.credentials?.password;
631
- const tf = config.credentials?.twofactor || null;
632
- if (!u || !p) throw new Error("Missing user cookie");
633
- const r = await tokens(u, p, tf);
634
- if (!(r && r.status && Array.isArray(r.cookies))) throw new Error(r && r.message ? r.message : "Login failed");
635
- const pairs = r.cookies.map(c => `${c.key || c.name}=${c.value}`);
636
- setJarFromPairs(jar, pairs, ".facebook.com");
637
- const initial2 = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
638
- const res2 = (await ctxRef.bypassAutomation(initial2, jar)) || initial2;
639
- const html2 = res2 && res2.data ? res2.data : "";
640
- if (html2.includes("/checkpoint/block/?next")) throw new Error("Checkpoint");
641
- const cookies2 = await Promise.resolve(jar.getCookies("https://www.facebook.com"));
642
- const uid2 = getUID(cookies2);
643
- if (!uid2) throw new Error("Login failed");
644
- return { html: html2, cookies: cookies2, userID: uid2 };
645
- }
646
-
647
- function makeLogin(j, email, password, globalOptions) {
648
- return async function () {
649
- const u = email || config.credentials?.email;
650
- const p = password || config.credentials?.password;
651
- const tf = config.credentials?.twofactor || null;
652
- if (!u || !p) return;
653
- const r = await tokens(u, p, tf);
654
- if (r && r.status && Array.isArray(r.cookies)) {
655
- const pairs = r.cookies.map(c => `${c.key || c.name}=${c.value}`);
656
- setJarFromPairs(j, pairs, ".facebook.com");
657
- await get("https://www.facebook.com/", j, null, globalOptions).then(saveCookies(j));
658
- } else {
659
- throw new Error(r && r.message ? r.message : "Login failed");
660
- }
661
- };
662
- }
663
-
664
- function loginHelper(appState, Cookie, email, password, globalOptions, callback) {
665
- try {
666
- const domain = ".facebook.com";
667
- // Helper to extract userID from appState input
668
- const extractUIDFromAppState = (appStateInput) => {
669
- if (!appStateInput) return null;
670
- let parsed = appStateInput;
671
- if (typeof appStateInput === "string") {
672
- try {
673
- parsed = JSON.parse(appStateInput);
674
- } catch {
675
- return null;
676
- }
677
- }
678
- if (Array.isArray(parsed)) {
679
- const cUser = parsed.find(c => (c.key === "c_user" || c.name === "c_user"));
680
- if (cUser) return cUser.value;
681
- const iUser = parsed.find(c => (c.key === "i_user" || c.name === "i_user"));
682
- if (iUser) return iUser.value;
683
- }
684
- return null;
685
- };
686
- let userIDFromAppState = extractUIDFromAppState(appState);
687
- (async () => {
688
- try {
689
- if (appState) {
690
- // Check and convert cookie to appState format
691
- if (Array.isArray(appState) && appState.some(c => c.name)) {
692
- // Convert name to key if needed
693
- appState = appState.map(c => {
694
- if (c.name && !c.key) {
695
- c.key = c.name;
696
- delete c.name;
697
- }
698
- return c;
699
- });
700
- } else if (typeof appState === "string") {
701
- // Try to parse as JSON first
702
- let parsed = appState;
703
- try {
704
- parsed = JSON.parse(appState);
705
- } catch { }
706
-
707
- if (Array.isArray(parsed)) {
708
- // Already parsed as array, use it
709
- appState = parsed;
710
- } else {
711
- // Parse string cookie format (key=value; key2=value2)
712
- const arrayAppState = [];
713
- appState.split(';').forEach(c => {
714
- const [key, value] = c.split('=');
715
- if (key && value) {
716
- arrayAppState.push({
717
- key: key.trim(),
718
- value: value.trim(),
719
- domain: ".facebook.com",
720
- path: "/",
721
- expires: new Date().getTime() + 1000 * 60 * 60 * 24 * 365
722
- });
723
- }
724
- });
725
- appState = arrayAppState;
726
- }
727
- }
728
-
729
- // Set cookies into jar with individual domain/path
730
- if (Array.isArray(appState)) {
731
- await setJarCookies(jar, appState);
732
- } else {
733
- throw new Error("Invalid appState format");
734
- }
735
- }
736
- if (Cookie) {
737
- let cookiePairs = [];
738
- if (typeof Cookie === "string") cookiePairs = normalizeCookieHeaderString(Cookie);
739
- else if (Array.isArray(Cookie)) cookiePairs = Cookie.map(String).filter(Boolean);
740
- else if (Cookie && typeof Cookie === "object") cookiePairs = Object.entries(Cookie).map(([k, v]) => `${k}=${v}`);
741
- if (cookiePairs.length) setJarFromPairs(jar, cookiePairs, domain);
742
- }
743
- } catch (e) {
744
- return callback(e);
745
- }
746
- const ctx = { globalOptions, options: globalOptions, reconnectAttempts: 0 };
747
- ctx.bypassAutomation = async function (resp, j) {
748
- global.fca = global.fca || {};
749
- global.fca.BypassAutomationNotification = this.bypassAutomation.bind(this);
750
- const s = x => (typeof x === "string" ? x : String(x ?? ""));
751
- const u = r => r?.request?.res?.responseUrl || (r?.config?.baseURL ? new URL(r.config.url || "/", r.config.baseURL).toString() : r?.config?.url || "");
752
- const isCp = r => typeof u(r) === "string" && u(r).includes("checkpoint/601051028565049");
753
- const cookieUID = async () => {
754
- try {
755
- const cookies = typeof j?.getCookies === "function" ? await j.getCookies("https://www.facebook.com") : [];
756
- return cookies.find(c => c.key === "i_user")?.value || cookies.find(c => c.key === "c_user")?.value;
757
- } catch { return undefined; }
758
- };
759
- const htmlUID = body => s(body).match(/"USER_ID"\s*:\s*"(\d+)"/)?.[1] || s(body).match(/\["CurrentUserInitialData",\[\],\{.*?"USER_ID":"(\d+)".*?\},\d+\]/)?.[1];
760
- const getUID = async body => (await cookieUID()) || htmlUID(body);
761
- const refreshJar = async () => get("https://www.facebook.com/", j, null, this.options).then(saveCookies(j));
762
- const bypass = async body => {
763
- const b = s(body);
764
- const UID = await getUID(b);
765
- const fb_dtsg = getFrom(b, '"DTSGInitData",[],{"token":"', '",') || b.match(/name="fb_dtsg"\s+value="([^"]+)"/)?.[1];
766
- const jazoest = getFrom(b, 'name="jazoest" value="', '"') || getFrom(b, "jazoest=", '",') || b.match(/name="jazoest"\s+value="([^"]+)"/)?.[1];
767
- const lsd = getFrom(b, '["LSD",[],{"token":"', '"}') || b.match(/name="lsd"\s+value="([^"]+)"/)?.[1];
768
- const form = { av: UID, fb_dtsg, jazoest, lsd, fb_api_caller_class: "RelayModern", fb_api_req_friendly_name: "FBScrapingWarningMutation", variables: "{}", server_timestamps: true, doc_id: 6339492849481770 };
769
- await post("https://www.facebook.com/api/graphql/", j, form, null, this.options).then(saveCookies(j));
770
- logger("Facebook automation warning detected, handling...", "warn");
771
- this.reconnectAttempts = 0;
772
- };
773
- try {
774
- if (resp) {
775
- if (isCp(resp)) {
776
- await bypass(s(resp.data));
777
- const refreshed = await refreshJar();
778
- if (isCp(refreshed)) logger("Checkpoint still present after refresh", "warn");
779
- else logger("Bypass complete, cookies refreshed", "info");
780
- return refreshed;
781
- }
782
- return resp;
783
- }
784
- const first = await get("https://www.facebook.com/", j, null, this.options).then(saveCookies(j));
785
- if (isCp(first)) {
786
- await bypass(s(first.data));
787
- const refreshed = await refreshJar();
788
- if (!isCp(refreshed)) logger("Bypass complete, cookies refreshed", "info");
789
- else logger("Checkpoint still present after refresh", "warn");
790
- return refreshed;
791
- }
792
- return first;
793
- } catch (e) {
794
- logger(`Bypass automation error: ${e && e.message ? e.message : String(e)}`, "error");
795
- return resp;
796
- }
797
- };
798
- if (appState || Cookie) {
799
- const initial = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
800
- return (await ctx.bypassAutomation(initial, jar)) || initial;
801
- }
802
- const hydrated = await hydrateJarFromDB(null);
803
- if (hydrated) {
804
- logger("AppState backup live — proceeding to login", "info");
805
- const initial = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
806
- return (await ctx.bypassAutomation(initial, jar)) || initial;
807
- }
808
- logger("AppState backup die — proceeding to email/password login", "warn");
809
- return get("https://www.facebook.com/", null, null, globalOptions)
810
- .then(saveCookies(jar))
811
- .then(makeLogin(jar, email, password, globalOptions))
812
- .then(function () {
813
- return get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
814
- });
815
- })()
816
- .then(async function (res) {
817
- const ctx = {};
818
- ctx.options = globalOptions;
819
- ctx.bypassAutomation = async function (resp, j) {
820
- global.fca = global.fca || {};
821
- global.fca.BypassAutomationNotification = this.bypassAutomation.bind(this);
822
- const s = x => (typeof x === "string" ? x : String(x ?? ""));
823
- const u = r => r?.request?.res?.responseUrl || (r?.config?.baseURL ? new URL(r.config.url || "/", r.config.baseURL).toString() : r?.config?.url || "");
824
- const isCp = r => typeof u(r) === "string" && u(r).includes("checkpoint/601051028565049");
825
- const cookieUID = async () => {
826
- try {
827
- const cookies = typeof j?.getCookies === "function" ? await j.getCookies("https://www.facebook.com") : [];
828
- return cookies.find(c => c.key === "i_user")?.value || cookies.find(c => c.key === "c_user")?.value;
829
- } catch { return undefined; }
830
- };
831
- const htmlUID = body => s(body).match(/"USER_ID"\s*:\s*"(\d+)"/)?.[1] || s(body).match(/\["CurrentUserInitialData",\[\],\{.*?"USER_ID":"(\d+)".*?\},\d+\]/)?.[1];
832
- const getUID = async body => (await cookieUID()) || htmlUID(body);
833
- const refreshJar = async () => get("https://www.facebook.com/", j, null, this.options).then(saveCookies(j));
834
- const bypass = async body => {
835
- const b = s(body);
836
- const UID = await getUID(b);
837
- const fb_dtsg = getFrom(b, '"DTSGInitData",[],{"token":"', '",') || b.match(/name="fb_dtsg"\s+value="([^"]+)"/)?.[1];
838
- const jazoest = getFrom(b, 'name="jazoest" value="', '"') || getFrom(b, "jazoest=", '",') || b.match(/name="jazoest"\s+value="([^"]+)"/)?.[1];
839
- const lsd = getFrom(b, '["LSD",[],{"token":"', '"}') || b.match(/name="lsd"\s+value="([^"]+)"/)?.[1];
840
- const form = { av: UID, fb_dtsg, jazoest, lsd, fb_api_caller_class: "RelayModern", fb_api_req_friendly_name: "FBScrapingWarningMutation", variables: "{}", server_timestamps: true, doc_id: 6339492849481770 };
841
- await post("https://www.facebook.com/api/graphql/", j, form, null, this.options).then(saveCookies(j));
842
- logger("Facebook automation warning detected, handling...", "warn");
843
- };
844
- try {
845
- if (res && isCp(res)) {
846
- await bypass(s(res.data));
847
- const refreshed = await refreshJar();
848
- if (!isCp(refreshed)) logger("Bypass complete, cookies refreshed", "info");
849
- return refreshed;
850
- }
851
- logger("No checkpoint detected", "info");
852
- return res;
853
- } catch {
854
- return res;
855
- }
856
- };
857
- const processed = (await ctx.bypassAutomation(res, jar)) || res;
858
- let html = processed && processed.data ? processed.data : "";
859
- let cookies = await Promise.resolve(jar.getCookies("https://www.facebook.com"));
860
- const getUIDFromCookies = cs =>
861
- cs.find(c => c.key === "i_user")?.value ||
862
- cs.find(c => c.key === "c_user")?.value ||
863
- cs.find(c => c.name === "i_user")?.value ||
864
- cs.find(c => c.name === "c_user")?.value;
865
- const getUIDFromHTML = body => {
866
- const s = typeof body === "string" ? body : String(body ?? "");
867
- return s.match(/"USER_ID"\s*:\s*"(\d+)"/)?.[1] || s.match(/\["CurrentUserInitialData",\[\],\{.*?"USER_ID":"(\d+)".*?\},\d+\]/)?.[1];
868
- };
869
- let userID = getUIDFromCookies(cookies);
870
- // Also try to extract userID from HTML if not found in cookies
871
- if (!userID) {
872
- userID = getUIDFromHTML(html);
873
- }
874
- // If still not found and appState was provided, use userID from appState input as fallback
875
- if (!userID && userIDFromAppState) {
876
- userID = userIDFromAppState;
877
- }
878
- if (!userID) {
879
- // Pass hadAppStateInput=true if appState/Cookie was originally provided
880
- const retried = await tryAutoLoginIfNeeded(html, cookies, globalOptions, ctx, !!(appState || Cookie));
881
- html = retried.html;
882
- cookies = retried.cookies;
883
- userID = retried.userID;
884
- }
885
- if (html.includes("/checkpoint/block/?next")) {
886
- logger("Appstate die, vui lòng thay cái mới!", "error");
887
- throw new Error("Checkpoint");
888
- }
889
- let mqttEndpoint;
890
- let region = "PRN";
891
- let fb_dtsg;
892
- let irisSeqID;
893
- try {
894
- const m1 = html.match(/"endpoint":"([^"]+)"/);
895
- const m2 = m1 ? null : html.match(/endpoint\\":\\"([^\\"]+)\\"/);
896
- const raw = (m1 && m1[1]) || (m2 && m2[1]);
897
- if (raw) mqttEndpoint = raw.replace(/\\\//g, "/");
898
- region = parseRegion(html);
899
- const rinfo = REGION_MAP.get(region);
900
- if (rinfo) logger(`Server region ${region} - ${rinfo.name}`, "info");
901
- else logger(`Server region ${region}`, "info");
902
- } catch {
903
- logger("Not MQTT endpoint", "warn");
904
- }
905
- try {
906
- const userDataMatch = String(html).match(/\["CurrentUserInitialData",\[\],({.*?}),\d+\]/);
907
- if (userDataMatch) {
908
- const info = JSON.parse(userDataMatch[1]);
909
- logger(`Đăng nhập tài khoản: ${info.NAME} (${info.USER_ID})`, "info");
910
- } else if (userID) {
911
- logger(`ID người dùng: ${userID}`, "info");
912
- }
913
- } catch { }
914
- const tokenMatch = html.match(/DTSGInitialData.*?token":"(.*?)"/);
915
- if (tokenMatch) fb_dtsg = tokenMatch[1];
916
- try {
917
- if (userID) await backupAppStateSQL(jar, userID);
918
- } catch { }
919
- Promise.resolve()
920
- .then(function () {
921
- if (models && models.sequelize && typeof models.sequelize.authenticate === "function") {
922
- return models.sequelize.authenticate();
923
- }
924
- })
925
- .then(function () {
926
- if (models && typeof models.syncAll === "function") {
927
- return models.syncAll();
928
- }
929
- })
930
- .catch(function (error) {
931
- // Silently handle database errors - they're not critical for login
932
- const errorMsg = error && error.message ? error.message : String(error);
933
- if (!errorMsg.includes("No Sequelize instance passed")) {
934
- // Only log non-Sequelize instance errors
935
- logger(`Database connection failed: ${errorMsg}`, "warn");
936
- }
937
- });
938
- logger("FCA fix/update by DongDev (Donix-VN)", "info");
939
- const ctxMain = {
940
- userID,
941
- jar,
942
- globalOptions,
943
- loggedIn: true,
944
- access_token: "NONE",
945
- clientMutationId: 0,
946
- mqttClient: undefined,
947
- lastSeqId: irisSeqID,
948
- syncToken: undefined,
949
- mqttEndpoint,
950
- region,
951
- firstListen: true,
952
- fb_dtsg,
953
- clientID: ((Math.random() * 2147483648) | 0).toString(16),
954
- clientId: getFrom(html, '["MqttWebDeviceID",[],{"clientID":"', '"}') || "",
955
- wsReqNumber: 0,
956
- wsTaskNumber: 0,
957
- tasks: new Map()
958
- };
959
- ctxMain.options = globalOptions;
960
- ctxMain.bypassAutomation = ctx.bypassAutomation.bind(ctxMain);
961
- ctxMain.performAutoLogin = async () => {
962
- try {
963
- const u = config.credentials?.email || email;
964
- const p = config.credentials?.password || password;
965
- const tf = config.credentials?.twofactor || null;
966
- if (!u || !p) return false;
967
- const r = await tokens(u, p, tf);
968
- if (!(r && r.status && Array.isArray(r.cookies))) return false;
969
- const pairs = r.cookies.map(c => `${c.key || c.name}=${c.value}`);
970
- setJarFromPairs(jar, pairs, ".facebook.com");
971
- await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
972
- return true;
973
- } catch {
974
- return false;
975
- }
976
- };
977
- const api = {
978
- setOptions: require("./options").setOptions.bind(null, globalOptions),
979
- getCookies: function () {
980
- return cookieHeaderFromJar(jar);
981
- },
982
- getAppState: function () {
983
- return getAppState(jar);
984
- },
985
- getLatestAppStateFromDB: async function (uid = userID) {
986
- const data = await getLatestBackup(uid, "appstate");
987
- return data ? JSON.parse(data) : null;
988
- },
989
- getLatestCookieFromDB: async function (uid = userID) {
990
- return await getLatestBackup(uid, "cookie");
991
- }
992
- };
993
- const defaultFuncs = makeDefaults(html, userID, ctxMain);
994
- const srcRoot = path.join(__dirname, "../src/api");
995
- let loaded = 0;
996
- let skipped = 0;
997
- fs.readdirSync(srcRoot, { withFileTypes: true }).forEach((sub) => {
998
- if (!sub.isDirectory()) return;
999
- const subDir = path.join(srcRoot, sub.name);
1000
- fs.readdirSync(subDir, { withFileTypes: true }).forEach((entry) => {
1001
- if (!entry.isFile() || !entry.name.endsWith(".js")) return;
1002
- const p = path.join(subDir, entry.name);
1003
- const key = path.basename(entry.name, ".js");
1004
- if (api[key]) {
1005
- skipped++;
1006
- return;
1007
- }
1008
- let mod;
1009
- try {
1010
- mod = require(p);
1011
- } catch (e) {
1012
- logger(`Failed to require API module ${p}: ${e && e.message ? e.message : String(e)}`, "warn");
1013
- skipped++;
1014
- return;
1015
- }
1016
- const factory = typeof mod === "function" ? mod : (mod && typeof mod.default === "function" ? mod.default : null);
1017
- if (!factory) {
1018
- logger(`API module ${p} does not export a function, skipping`, "warn");
1019
- skipped++;
1020
- return;
1021
- }
1022
- api[key] = factory(defaultFuncs, api, ctxMain);
1023
- loaded++;
1024
- });
1025
- });
1026
- logger(`Loaded ${loaded} FCA API methods${skipped ? `, skipped ${skipped} duplicates` : ""}`);
1027
- if (api.listenMqtt) api.listen = api.listenMqtt;
1028
- if (api.refreshFb_dtsg) {
1029
- setInterval(function () {
1030
- api.refreshFb_dtsg().then(function () {
1031
- logger("Successfully refreshed fb_dtsg");
1032
- }).catch(function () {
1033
- logger("An error occurred while refreshing fb_dtsg", "error");
1034
- });
1035
- }, 86400000);
1036
- }
1037
- logger("Login successful!");
1038
- callback(null, api);
1039
- })
1040
- .catch(function (e) {
1041
- callback(e);
1042
- });
1043
- } catch (e) {
1044
- callback(e);
1045
- }
1046
- }
1047
-
1048
- module.exports = loginHelper;
1
+ "use strict";
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const models = require("../src/database/models");
5
+ const logger = require("../func/logger");
6
+ const { get, post, jar, makeDefaults } = require("../src/utils/request");
7
+ const { saveCookies, getAppState } = require("../src/utils/client");
8
+ const { getFrom } = require("../src/utils/constants");
9
+ const { loadConfig } = require("./config");
10
+ const { config } = loadConfig();
11
+ const axiosBase = require("axios");
12
+ const parseUserHtml = require("./parseUseerHtml");
13
+
14
+ const regions = [
15
+ { code: "PRN", name: "Pacific Northwest Region", location: "Khu vực Tây Bắc Thái Bình Dương" },
16
+ { code: "VLL", name: "Valley Region", location: "Valley" },
17
+ { code: "ASH", name: "Ashburn Region", location: "Ashburn" },
18
+ { code: "DFW", name: "Dallas/Fort Worth Region", location: "Dallas/Fort Worth" },
19
+ { code: "LLA", name: "Los Angeles Region", location: "Los Angeles" },
20
+ { code: "FRA", name: "Frankfurt", location: "Frankfurt" },
21
+ { code: "SIN", name: "Singapore", location: "Singapore" },
22
+ { code: "NRT", name: "Tokyo", location: "Japan" },
23
+ { code: "HKG", name: "Hong Kong", location: "Hong Kong" },
24
+ { code: "SYD", name: "Sydney", location: "Sydney" },
25
+ { code: "PNB", name: "Pacific Northwest - Beta", location: "Pacific Northwest " }
26
+ ];
27
+
28
+ const REGION_MAP = new Map(regions.map(r => [r.code, r]));
29
+
30
+ function parseRegion(html) {
31
+ try {
32
+ const m1 = html.match(/"endpoint":"([^"]+)"/);
33
+ const m2 = m1 ? null : html.match(/endpoint\\":\\"([^\\"]+)\\"/);
34
+ const raw = (m1 && m1[1]) || (m2 && m2[1]);
35
+ if (!raw) return "PRN";
36
+ const endpoint = raw.replace(/\\\//g, "/");
37
+ const url = new URL(endpoint);
38
+ const rp = url.searchParams ? url.searchParams.get("region") : null;
39
+ return rp ? rp.toUpperCase() : "PRN";
40
+ } catch {
41
+ return "PRN";
42
+ }
43
+ }
44
+
45
+ function mask(s, keep = 3) {
46
+ if (!s) return "";
47
+ const n = s.length;
48
+ return n <= keep ? "*".repeat(n) : s.slice(0, keep) + "*".repeat(Math.max(0, n - keep));
49
+ }
50
+
51
+ /**
52
+ * Login via external API endpoint (iOS method)
53
+ * @param {string} email - Email hoặc số điện thoại
54
+ * @param {string} password - Mật khẩu
55
+ * @param {string|null} twoFactor - Secret Base32 cho 2FA (không phải mã 6 số)
56
+ * @param {string|null} apiBaseUrl - Base URL của API server (mặc định: https://minhdong.site)
57
+ * @param {string|null} apiKey - API key để xác thực (x-api-key header)
58
+ * @returns {Promise<{ok: boolean, uid?: string, access_token?: string, cookies?: Array, cookie?: string, message?: string}>}
59
+ */
60
+ async function loginViaAPI(email, password, twoFactor = null, apiBaseUrl = null, apiKey = null) {
61
+ try {
62
+ const baseUrl = apiBaseUrl || config.apiServer || "https://minhdong.site";
63
+ const endpoint = `${baseUrl}/api/v1/facebook/login_ios`;
64
+ const xApiKey = apiKey || config.apiKey || null;
65
+
66
+ // Build request body
67
+ const body = {
68
+ email,
69
+ password
70
+ };
71
+
72
+ // Only include twoFactor if provided (must be Base32 secret, not 6-digit code)
73
+ if (twoFactor && typeof twoFactor === "string" && twoFactor.trim()) {
74
+ // Clean up the secret - remove spaces and convert to uppercase
75
+ body.twoFactor = twoFactor.replace(/\s+/g, "").toUpperCase();
76
+ }
77
+
78
+ // Build headers
79
+ const headers = {
80
+ "Content-Type": "application/json",
81
+ "Accept": "application/json"
82
+ };
83
+
84
+ // Add x-api-key header if provided
85
+ if (xApiKey) {
86
+ headers["x-api-key"] = xApiKey;
87
+ }
88
+
89
+ logger(`API-LOGIN: Attempting login for ${mask(email, 2)} via iOS API`, "info");
90
+
91
+ const response = await axiosBase({
92
+ method: "POST",
93
+ url: endpoint,
94
+ headers,
95
+ data: body,
96
+ timeout: 60000,
97
+ validateStatus: () => true
98
+ });
99
+ if (response.status === 200 && response.data) {
100
+ const data = response.data;
101
+
102
+ // Check if login was successful
103
+ if (data.error) {
104
+ logger(`API-LOGIN: Login failed - ${data.error}`, "error");
105
+ return { ok: false, message: data.error };
106
+ }
107
+
108
+ // Extract response data
109
+ const uid = data.uid || data.user_id || data.userId || null;
110
+ const accessToken = data.access_token || data.accessToken || null;
111
+ const cookie = data.cookie || data.cookies || null;
112
+
113
+ if (!uid && !accessToken && !cookie) {
114
+ logger("API-LOGIN: Response missing required fields (uid, access_token, cookie)", "warn");
115
+ return { ok: false, message: "Invalid response from API" };
116
+ }
117
+
118
+ logger(`API-LOGIN: Login successful for UID: ${uid || "unknown"}`, "info");
119
+
120
+ // Parse cookies if provided as string
121
+ let cookies = [];
122
+ if (typeof cookie === "string") {
123
+ // Parse cookie string format: "key1=value1; key2=value2"
124
+ const pairs = cookie.split(";").map(p => p.trim()).filter(Boolean);
125
+ for (const pair of pairs) {
126
+ const eq = pair.indexOf("=");
127
+ if (eq > 0) {
128
+ const key = pair.slice(0, eq).trim();
129
+ const value = pair.slice(eq + 1).trim();
130
+ cookies.push({
131
+ key,
132
+ value,
133
+ domain: ".facebook.com",
134
+ path: "/"
135
+ });
136
+ }
137
+ }
138
+ } else if (Array.isArray(cookie)) {
139
+ // Already in array format
140
+ cookies = cookie.map(c => ({
141
+ key: c.key || c.name,
142
+ value: c.value,
143
+ domain: c.domain || ".facebook.com",
144
+ path: c.path || "/"
145
+ }));
146
+ }
147
+
148
+ return {
149
+ ok: true,
150
+ uid,
151
+ access_token: accessToken,
152
+ cookies,
153
+ cookie: typeof cookie === "string" ? cookie : null
154
+ };
155
+ }
156
+
157
+ // Handle error responses
158
+ const errorMsg = response.data && response.data.error
159
+ ? response.data.error
160
+ : response.data && response.data.message
161
+ ? response.data.message
162
+ : `HTTP ${response.status}`;
163
+
164
+ logger(`API-LOGIN: Login failed - ${errorMsg}`, "error");
165
+ return { ok: false, message: errorMsg };
166
+
167
+ } catch (error) {
168
+ const errMsg = error && error.message ? error.message : String(error);
169
+ logger(`API-LOGIN: Request failed - ${errMsg}`, "error");
170
+ return { ok: false, message: errMsg };
171
+ }
172
+ }
173
+
174
+ /**
175
+ * High-level login function that uses the API endpoint
176
+ * @param {string} email - Email hoặc số điện thoại
177
+ * @param {string} password - Mật khẩu
178
+ * @param {string|null} twoFactor - Secret Base32 cho 2FA (không phải mã 6 số)
179
+ * @param {string|null} apiBaseUrl - Base URL của API server
180
+ * @returns {Promise<{status: boolean, cookies?: Array, uid?: string, access_token?: string, message?: string}>}
181
+ */
182
+ async function tokensViaAPI(email, password, twoFactor = null, apiBaseUrl = null) {
183
+ const t0 = process.hrtime.bigint();
184
+
185
+ if (!email || !password) {
186
+ return { status: false, message: "Please provide email and password" };
187
+ }
188
+
189
+ logger(`API-LOGIN: Initialize login ${mask(email, 2)}`, "info");
190
+
191
+ const res = await loginViaAPI(email, password, twoFactor, apiBaseUrl);
192
+
193
+ if (res && res.ok) {
194
+ logger(`API-LOGIN: Login success - UID: ${res.uid}`, "info");
195
+ const t1 = Number(process.hrtime.bigint() - t0) / 1e6;
196
+ logger(`Done API login ${Math.round(t1)}ms`, "info");
197
+
198
+ return {
199
+ status: true,
200
+ cookies: res.cookies,
201
+ uid: res.uid,
202
+ access_token: res.access_token,
203
+ cookie: res.cookie
204
+ };
205
+ }
206
+
207
+ return {
208
+ status: false,
209
+ message: res && res.message ? res.message : "Login failed"
210
+ };
211
+ }
212
+
213
+ function normalizeCookieHeaderString(s) {
214
+ let str = String(s || "").trim();
215
+ if (!str) return [];
216
+ if (/^cookie\s*:/i.test(str)) str = str.replace(/^cookie\s*:/i, "").trim();
217
+ str = str.replace(/\r?\n/g, " ").replace(/\s*;\s*/g, ";");
218
+ const parts = str.split(";").map(v => v.trim()).filter(Boolean);
219
+ const out = [];
220
+ for (const p of parts) {
221
+ const eq = p.indexOf("=");
222
+ if (eq <= 0) continue;
223
+ const k = p.slice(0, eq).trim();
224
+ const v = p.slice(eq + 1).trim().replace(/^"(.*)"$/, "$1");
225
+ if (!k) continue;
226
+ out.push(`${k}=${v}`);
227
+ }
228
+ return out;
229
+ }
230
+
231
+ function setJarFromPairs(j, pairs, domain) {
232
+ const expires = new Date(Date.now() + 31536e6).toUTCString();
233
+ // URLs to set cookies for - include both desktop and mobile versions
234
+ const urls = [
235
+ "https://www.facebook.com",
236
+ "https://facebook.com",
237
+ "https://m.facebook.com",
238
+ "http://www.facebook.com",
239
+ "http://facebook.com",
240
+ "http://m.facebook.com"
241
+ ];
242
+
243
+ for (const kv of pairs) {
244
+ const cookieStr = `${kv}; expires=${expires}; domain=${domain}; path=/;`;
245
+ // Set cookie for all URLs to ensure it works on both desktop and mobile
246
+ for (const url of urls) {
247
+ try {
248
+ if (typeof j.setCookieSync === "function") {
249
+ j.setCookieSync(cookieStr, url);
250
+ } else if (typeof j.setCookie === "function") {
251
+ j.setCookie(cookieStr, url);
252
+ }
253
+ } catch (err) {
254
+ // Silently ignore domain mismatch errors
255
+ // These can happen when setting cookies across different subdomains
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ function cookieHeaderFromJar(j) {
262
+ const urls = ["https://www.facebook.com"];
263
+ const seen = new Set();
264
+ const parts = [];
265
+ for (const u of urls) {
266
+ let s = "";
267
+ try {
268
+ s = typeof j.getCookieStringSync === "function" ? j.getCookieStringSync(u) : "";
269
+ } catch { }
270
+ if (!s) continue;
271
+ for (const kv of s.split(";")) {
272
+ const t = kv.trim();
273
+ const name = t.split("=")[0];
274
+ if (!name || seen.has(name)) continue;
275
+ seen.add(name);
276
+ parts.push(t);
277
+ }
278
+ }
279
+ return parts.join("; ");
280
+ }
281
+
282
+ let uniqueIndexEnsured = false;
283
+
284
+ function getBackupModel() {
285
+ try {
286
+ if (!models || !models.sequelize || !models.Sequelize) return null;
287
+ const sequelize = models.sequelize;
288
+
289
+ // Validate that sequelize is a proper Sequelize instance
290
+ if (!sequelize || typeof sequelize.define !== "function") return null;
291
+
292
+ const { DataTypes } = models.Sequelize;
293
+ if (sequelize.models && sequelize.models.AppStateBackup) return sequelize.models.AppStateBackup;
294
+ const dialect = typeof sequelize.getDialect === "function" ? sequelize.getDialect() : "sqlite";
295
+ const LongText = (dialect === "mysql" || dialect === "mariadb") ? DataTypes.TEXT("long") : DataTypes.TEXT;
296
+
297
+ try {
298
+ const AppStateBackup = sequelize.define(
299
+ "AppStateBackup",
300
+ {
301
+ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
302
+ userID: { type: DataTypes.STRING, allowNull: false },
303
+ type: { type: DataTypes.STRING, allowNull: false },
304
+ data: { type: LongText }
305
+ },
306
+ { tableName: "app_state_backups", timestamps: true, indexes: [{ unique: true, fields: ["userID", "type"] }] }
307
+ );
308
+ return AppStateBackup;
309
+ } catch (defineError) {
310
+ // If define fails, log and return null
311
+ logger(`Failed to define AppStateBackup model: ${defineError && defineError.message ? defineError.message : String(defineError)}`, "warn");
312
+ return null;
313
+ }
314
+ } catch (e) {
315
+ // Silently handle any errors in getBackupModel
316
+ return null;
317
+ }
318
+ }
319
+
320
+ async function ensureUniqueIndex(sequelize) {
321
+ if (uniqueIndexEnsured || !sequelize) return;
322
+ try {
323
+ if (typeof sequelize.getQueryInterface !== "function") return;
324
+ await sequelize.getQueryInterface().addIndex("app_state_backups", ["userID", "type"], { unique: true, name: "app_state_user_type_unique" });
325
+ } catch { }
326
+ uniqueIndexEnsured = true;
327
+ }
328
+
329
+ async function upsertBackup(Model, userID, type, data) {
330
+ const where = { userID: String(userID || ""), type };
331
+ const row = await Model.findOne({ where });
332
+ if (row) {
333
+ await row.update({ data });
334
+ logger(`Overwrote existing ${type} backup for user ${where.userID}`, "info");
335
+ return;
336
+ }
337
+ await Model.create({ ...where, data });
338
+ logger(`Created new ${type} backup for user ${where.userID}`, "info");
339
+ }
340
+
341
+ async function backupAppStateSQL(j, userID) {
342
+ try {
343
+ const Model = getBackupModel();
344
+ if (!Model) return;
345
+ if (!models || !models.sequelize) return;
346
+ await Model.sync();
347
+ await ensureUniqueIndex(models.sequelize);
348
+ const appJson = getAppState(j);
349
+ const ck = cookieHeaderFromJar(j);
350
+ await upsertBackup(Model, userID, "appstate", JSON.stringify(appJson));
351
+ await upsertBackup(Model, userID, "cookie", ck);
352
+ logger("Backup stored (overwrite mode)", "info");
353
+ } catch (e) {
354
+ logger(`Failed to save appstate backup ${e && e.message ? e.message : String(e)}`, "warn");
355
+ }
356
+ }
357
+
358
+ async function getLatestBackup(userID, type) {
359
+ try {
360
+ const Model = getBackupModel();
361
+ if (!Model) return null;
362
+ const row = await Model.findOne({ where: { userID: String(userID || ""), type } });
363
+ return row ? row.data : null;
364
+ } catch {
365
+ return null;
366
+ }
367
+ }
368
+
369
+ async function getLatestBackupAny(type) {
370
+ try {
371
+ const Model = getBackupModel();
372
+ if (!Model) return null;
373
+ const row = await Model.findOne({ where: { type }, order: [["updatedAt", "DESC"]] });
374
+ return row ? row.data : null;
375
+ } catch {
376
+ return null;
377
+ }
378
+ }
379
+
380
+
381
+
382
+ async function setJarCookies(j, appstate) {
383
+ const tasks = [];
384
+ for (const c of appstate) {
385
+ const cookieName = c.name || c.key;
386
+ const cookieValue = c.value;
387
+ if (!cookieName || cookieValue === undefined) continue;
388
+
389
+ const cookieDomain = c.domain || ".facebook.com";
390
+ const cookiePath = c.path || "/";
391
+ const dom = cookieDomain.replace(/^\./, "");
392
+
393
+ // Handle expirationDate (can be in seconds or milliseconds)
394
+ let expiresStr = "";
395
+ if (c.expirationDate !== undefined) {
396
+ let expiresDate;
397
+ if (typeof c.expirationDate === "number") {
398
+ // If expirationDate is less than a year from now in seconds, treat as seconds
399
+ // Otherwise treat as milliseconds
400
+ const now = Date.now();
401
+ const oneYearInMs = 365 * 24 * 60 * 60 * 1000;
402
+ if (c.expirationDate < (now + oneYearInMs) / 1000) {
403
+ expiresDate = new Date(c.expirationDate * 1000);
404
+ } else {
405
+ expiresDate = new Date(c.expirationDate);
406
+ }
407
+ } else {
408
+ expiresDate = new Date(c.expirationDate);
409
+ }
410
+ expiresStr = `; expires=${expiresDate.toUTCString()}`;
411
+ } else if (c.expires) {
412
+ const expiresDate = typeof c.expires === "number" ? new Date(c.expires) : new Date(c.expires);
413
+ expiresStr = `; expires=${expiresDate.toUTCString()}`;
414
+ }
415
+
416
+ // Helper function to build cookie string
417
+ const buildCookieString = (domainOverride = null) => {
418
+ const domain = domainOverride || cookieDomain;
419
+ let cookieParts = [`${cookieName}=${cookieValue}${expiresStr}`];
420
+ cookieParts.push(`Domain=${domain}`);
421
+ cookieParts.push(`Path=${cookiePath}`);
422
+
423
+ // Add Secure flag if secure is true
424
+ if (c.secure === true) {
425
+ cookieParts.push("Secure");
426
+ }
427
+
428
+ // Add HttpOnly flag if httpOnly is true
429
+ if (c.httpOnly === true) {
430
+ cookieParts.push("HttpOnly");
431
+ }
432
+
433
+ // Add SameSite attribute if provided
434
+ if (c.sameSite) {
435
+ const sameSiteValue = String(c.sameSite).toLowerCase();
436
+ if (["strict", "lax", "none"].includes(sameSiteValue)) {
437
+ cookieParts.push(`SameSite=${sameSiteValue.charAt(0).toUpperCase() + sameSiteValue.slice(1)}`);
438
+ }
439
+ }
440
+
441
+ return cookieParts.join("; ");
442
+ };
443
+ const cookieConfigs = [];
444
+ if (cookieDomain === ".facebook.com" || cookieDomain === "facebook.com") {
445
+ cookieConfigs.push({ url: `http://${dom}${cookiePath}`, cookieStr: buildCookieString() });
446
+ cookieConfigs.push({ url: `https://${dom}${cookiePath}`, cookieStr: buildCookieString() });
447
+ cookieConfigs.push({ url: `http://www.${dom}${cookiePath}`, cookieStr: buildCookieString() });
448
+ cookieConfigs.push({ url: `https://www.${dom}${cookiePath}`, cookieStr: buildCookieString() });
449
+ } else {
450
+ cookieConfigs.push({ url: `http://${dom}${cookiePath}`, cookieStr: buildCookieString() });
451
+ cookieConfigs.push({ url: `https://${dom}${cookiePath}`, cookieStr: buildCookieString() });
452
+ cookieConfigs.push({ url: `http://www.${dom}${cookiePath}`, cookieStr: buildCookieString() });
453
+ cookieConfigs.push({ url: `https://www.${dom}${cookiePath}`, cookieStr: buildCookieString() });
454
+ }
455
+
456
+ for (const config of cookieConfigs) {
457
+ tasks.push(j.setCookie(config.cookieStr, config.url).catch((err) => {
458
+ if (err && err.message && err.message.includes("Cookie not in this host's domain")) {
459
+ return;
460
+ }
461
+ return;
462
+ }));
463
+ }
464
+ }
465
+ await Promise.all(tasks);
466
+ }
467
+
468
+ // tokens function - alias to tokensViaAPI for backward compatibility
469
+ async function tokens(username, password, twofactor = null) {
470
+ return tokensViaAPI(username, password, twofactor);
471
+ }
472
+
473
+ async function hydrateJarFromDB(userID) {
474
+ try {
475
+ let ck = null;
476
+ let app = null;
477
+ if (userID) {
478
+ ck = await getLatestBackup(userID, "cookie");
479
+ app = await getLatestBackup(userID, "appstate");
480
+ } else {
481
+ ck = await getLatestBackupAny("cookie");
482
+ app = await getLatestBackupAny("appstate");
483
+ }
484
+ if (ck) {
485
+ const pairs = normalizeCookieHeaderString(ck);
486
+ if (pairs.length) {
487
+ setJarFromPairs(jar, pairs, ".facebook.com");
488
+ return true;
489
+ }
490
+ }
491
+ if (app) {
492
+ let parsed = null;
493
+ try {
494
+ parsed = JSON.parse(app);
495
+ } catch { }
496
+ if (Array.isArray(parsed)) {
497
+ const pairs = parsed.map(c => [c.name || c.key, c.value].join("="));
498
+ setJarFromPairs(jar, pairs, ".facebook.com");
499
+ return true;
500
+ }
501
+ }
502
+ return false;
503
+ } catch {
504
+ return false;
505
+ }
506
+ }
507
+
508
+ async function tryAutoLoginIfNeeded(currentHtml, currentCookies, globalOptions, ctxRef, hadAppStateInput = false) {
509
+ // Helper to validate UID - must be a non-zero positive number string
510
+ const isValidUID = uid => uid && uid !== "0" && /^\d+$/.test(uid) && parseInt(uid, 10) > 0;
511
+
512
+ const getUID = cs =>
513
+ cs.find(c => c.key === "i_user")?.value ||
514
+ cs.find(c => c.key === "c_user")?.value ||
515
+ cs.find(c => c.name === "i_user")?.value ||
516
+ cs.find(c => c.name === "c_user")?.value;
517
+ const htmlUID = body => {
518
+ const s = typeof body === "string" ? body : String(body ?? "");
519
+ return s.match(/"USER_ID"\s*:\s*"(\d+)"/)?.[1] || s.match(/\["CurrentUserInitialData",\[\],\{.*?"USER_ID":"(\d+)".*?\},\d+\]/)?.[1];
520
+ };
521
+
522
+ let userID = getUID(currentCookies);
523
+ // Also try to extract userID from HTML if cookie userID is invalid
524
+ if (!isValidUID(userID)) {
525
+ userID = htmlUID(currentHtml);
526
+ }
527
+ // If we have a valid userID, return success
528
+ if (isValidUID(userID)) {
529
+ return { html: currentHtml, cookies: currentCookies, userID };
530
+ }
531
+
532
+ // No valid userID found - need to try auto-login
533
+ logger("tryAutoLoginIfNeeded: No valid userID found, attempting recovery...", "warn");
534
+
535
+ // If appState/Cookie was provided and is not checkpointed, try refresh
536
+ if (hadAppStateInput) {
537
+ const isCheckpoint = currentHtml.includes("/checkpoint/block/?next");
538
+ if (!isCheckpoint) {
539
+ try {
540
+ const refreshedCookies = await Promise.resolve(jar.getCookies("https://www.facebook.com"));
541
+ userID = getUID(refreshedCookies);
542
+ if (isValidUID(userID)) {
543
+ return { html: currentHtml, cookies: refreshedCookies, userID };
544
+ }
545
+ } catch { }
546
+ }
547
+ }
548
+
549
+ // Try to hydrate from DB backup
550
+ const hydrated = await hydrateJarFromDB(null);
551
+ if (hydrated) {
552
+ logger("tryAutoLoginIfNeeded: Trying backup from DB...", "info");
553
+ try {
554
+ const initial = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
555
+ const resB = (await ctxRef.bypassAutomation(initial, jar)) || initial;
556
+ const htmlB = resB && resB.data ? resB.data : "";
557
+ if (!htmlB.includes("/checkpoint/block/?next")) {
558
+ const htmlUserID = htmlUID(htmlB);
559
+ if (isValidUID(htmlUserID)) {
560
+ const cookiesB = await Promise.resolve(jar.getCookies("https://www.facebook.com"));
561
+ logger(`tryAutoLoginIfNeeded: DB backup session valid, USER_ID=${htmlUserID}`, "info");
562
+ return { html: htmlB, cookies: cookiesB, userID: htmlUserID };
563
+ } else {
564
+ logger(`tryAutoLoginIfNeeded: DB backup session dead (HTML USER_ID=${htmlUserID || "empty"}), will try API login...`, "warn");
565
+ }
566
+ }
567
+ } catch (dbErr) {
568
+ logger(`tryAutoLoginIfNeeded: DB backup failed - ${dbErr && dbErr.message ? dbErr.message : String(dbErr)}`, "warn");
569
+ }
570
+ }
571
+
572
+ // Check if auto-login is enabled (support both true and "true")
573
+ if (config.autoLogin === false || config.autoLogin === "false") {
574
+ throw new Error("AppState backup die — Auto-login is disabled");
575
+ }
576
+
577
+ // Try API login
578
+ const u = config.credentials?.email || config.email;
579
+ const p = config.credentials?.password || config.password;
580
+ const tf = config.credentials?.twofactor || config.twofactor || null;
581
+
582
+ if (!u || !p) {
583
+ logger("tryAutoLoginIfNeeded: No credentials configured for auto-login!", "error");
584
+ throw new Error("Missing credentials for auto-login (email/password not configured in fca-config.json)");
585
+ }
586
+
587
+ logger(`tryAutoLoginIfNeeded: Attempting API login for ${u.slice(0, 3)}***...`, "info");
588
+
589
+ const r = await tokens(u, p, tf);
590
+ if (!r || !r.status) {
591
+ throw new Error(r && r.message ? r.message : "API Login failed");
592
+ }
593
+
594
+ logger(`tryAutoLoginIfNeeded: API login successful! UID: ${r.uid}`, "info");
595
+
596
+ // Handle cookies - can be array, cookie string header, or both
597
+ let cookiePairs = [];
598
+
599
+ // If cookies is a string (cookie header format), parse it
600
+ if (typeof r.cookies === "string") {
601
+ cookiePairs = normalizeCookieHeaderString(r.cookies);
602
+ }
603
+ // If cookies is an array, convert to pairs
604
+ else if (Array.isArray(r.cookies)) {
605
+ cookiePairs = r.cookies.map(c => {
606
+ if (typeof c === "string") {
607
+ // Already in "key=value" format
608
+ return c;
609
+ } else if (c && typeof c === "object") {
610
+ // Object format {key, value} or {name, value}
611
+ return `${c.key || c.name}=${c.value}`;
612
+ }
613
+ return null;
614
+ }).filter(Boolean);
615
+ }
616
+
617
+ // Also check for cookie field (alternative field name)
618
+ if (cookiePairs.length === 0 && r.cookie) {
619
+ if (typeof r.cookie === "string") {
620
+ cookiePairs = normalizeCookieHeaderString(r.cookie);
621
+ } else if (Array.isArray(r.cookie)) {
622
+ cookiePairs = r.cookie.map(c => {
623
+ if (typeof c === "string") return c;
624
+ if (c && typeof c === "object") return `${c.key || c.name}=${c.value}`;
625
+ return null;
626
+ }).filter(Boolean);
627
+ }
628
+ }
629
+
630
+ if (cookiePairs.length === 0) {
631
+ logger("tryAutoLoginIfNeeded: No cookies found in API response", "warn");
632
+ throw new Error("API login returned no cookies");
633
+ } else {
634
+ logger(`tryAutoLoginIfNeeded: Parsed ${cookiePairs.length} cookies from API response`, "info");
635
+ setJarFromPairs(jar, cookiePairs, ".facebook.com");
636
+ }
637
+
638
+ // Wait a bit for cookies to be set
639
+ await new Promise(resolve => setTimeout(resolve, 500));
640
+
641
+ // Refresh Facebook page with new cookies - try multiple times if needed
642
+ // Try both www.facebook.com and m.facebook.com to ensure session is established
643
+ let html2 = "";
644
+ let res2 = null;
645
+ let retryCount = 0;
646
+ const maxRetries = 3;
647
+ const urlsToTry = ["https://m.facebook.com/", "https://www.facebook.com/"];
648
+
649
+ while (retryCount < maxRetries) {
650
+ try {
651
+ // Try m.facebook.com first (mobile version often works better for API login)
652
+ const urlToUse = retryCount === 0 ? urlsToTry[0] : urlsToTry[retryCount % urlsToTry.length];
653
+ logger(`tryAutoLoginIfNeeded: Refreshing ${urlToUse} (attempt ${retryCount + 1}/${maxRetries})...`, "info");
654
+
655
+ const initial2 = await get(urlToUse, jar, null, globalOptions).then(saveCookies(jar));
656
+ res2 = (await ctxRef.bypassAutomation(initial2, jar)) || initial2;
657
+ html2 = res2 && res2.data ? res2.data : "";
658
+
659
+ if (html2.includes("/checkpoint/block/?next")) {
660
+ throw new Error("Checkpoint after API login");
661
+ }
662
+
663
+ // Check if HTML contains valid USER_ID
664
+ const htmlUserID = htmlUID(html2);
665
+ if (isValidUID(htmlUserID)) {
666
+ logger(`tryAutoLoginIfNeeded: Found valid USER_ID in HTML from ${urlToUse}: ${htmlUserID}`, "info");
667
+ break;
668
+ }
669
+
670
+ // If no valid USER_ID found, wait and retry with different URL
671
+ if (retryCount < maxRetries - 1) {
672
+ logger(`tryAutoLoginIfNeeded: No valid USER_ID in HTML from ${urlToUse} (attempt ${retryCount + 1}/${maxRetries}), retrying...`, "warn");
673
+ await new Promise(resolve => setTimeout(resolve, 1000 * (retryCount + 1)));
674
+ retryCount++;
675
+ } else {
676
+ logger("tryAutoLoginIfNeeded: No valid USER_ID found in HTML after retries", "warn");
677
+ break;
678
+ }
679
+ } catch (err) {
680
+ if (err.message && err.message.includes("Checkpoint")) {
681
+ throw err;
682
+ }
683
+ if (retryCount < maxRetries - 1) {
684
+ logger(`tryAutoLoginIfNeeded: Error refreshing page (attempt ${retryCount + 1}/${maxRetries}): ${err && err.message ? err.message : String(err)}`, "warn");
685
+ await new Promise(resolve => setTimeout(resolve, 1000 * (retryCount + 1)));
686
+ retryCount++;
687
+ } else {
688
+ throw err;
689
+ }
690
+ }
691
+ }
692
+
693
+ const cookies2 = await Promise.resolve(jar.getCookies("https://www.facebook.com"));
694
+ const uid2 = getUID(cookies2);
695
+ const htmlUserID2 = htmlUID(html2);
696
+
697
+ // Prioritize USER_ID from HTML over cookies (more reliable)
698
+ let finalUID = null;
699
+ if (isValidUID(htmlUserID2)) {
700
+ finalUID = htmlUserID2;
701
+ logger(`tryAutoLoginIfNeeded: Using USER_ID from HTML: ${finalUID}`, "info");
702
+ } else if (isValidUID(uid2)) {
703
+ finalUID = uid2;
704
+ logger(`tryAutoLoginIfNeeded: Using USER_ID from cookies: ${finalUID}`, "info");
705
+ } else if (isValidUID(r.uid)) {
706
+ finalUID = r.uid;
707
+ logger(`tryAutoLoginIfNeeded: Using USER_ID from API response: ${finalUID}`, "info");
708
+ }
709
+
710
+ if (!isValidUID(finalUID)) {
711
+ logger(`tryAutoLoginIfNeeded: HTML check - USER_ID from HTML: ${htmlUserID2 || "none"}, from cookies: ${uid2 || "none"}, from API: ${r.uid || "none"}`, "error");
712
+ throw new Error("Login failed - could not get valid userID after API login. HTML may indicate session is not established.");
713
+ }
714
+
715
+ // Final validation: ensure HTML shows we're logged in
716
+ if (!isValidUID(htmlUserID2)) {
717
+ logger("tryAutoLoginIfNeeded: WARNING - HTML does not show valid USER_ID, but proceeding with cookie-based UID", "warn");
718
+ }
719
+
720
+ return { html: html2, cookies: cookies2, userID: finalUID };
721
+ }
722
+
723
+ function makeLogin(j, email, password, globalOptions) {
724
+ return async function () {
725
+ const u = email || config.credentials?.email;
726
+ const p = password || config.credentials?.password;
727
+ const tf = config.credentials?.twofactor || null;
728
+ if (!u || !p) return;
729
+ const r = await tokens(u, p, tf);
730
+ if (r && r.status && Array.isArray(r.cookies)) {
731
+ const pairs = r.cookies.map(c => `${c.key || c.name}=${c.value}`);
732
+ setJarFromPairs(j, pairs, ".facebook.com");
733
+ await get("https://www.facebook.com/", j, null, globalOptions).then(saveCookies(j));
734
+ } else {
735
+ throw new Error(r && r.message ? r.message : "Login failed");
736
+ }
737
+ };
738
+ }
739
+
740
+ function loginHelper(appState, Cookie, email, password, globalOptions, callback) {
741
+ try {
742
+ const domain = ".facebook.com";
743
+ // Helper to extract userID from appState input
744
+ const extractUIDFromAppState = (appStateInput) => {
745
+ if (!appStateInput) return null;
746
+ let parsed = appStateInput;
747
+ if (typeof appStateInput === "string") {
748
+ try {
749
+ parsed = JSON.parse(appStateInput);
750
+ } catch {
751
+ return null;
752
+ }
753
+ }
754
+ if (Array.isArray(parsed)) {
755
+ const cUser = parsed.find(c => (c.key === "c_user" || c.name === "c_user"));
756
+ if (cUser) return cUser.value;
757
+ const iUser = parsed.find(c => (c.key === "i_user" || c.name === "i_user"));
758
+ if (iUser) return iUser.value;
759
+ }
760
+ return null;
761
+ };
762
+ let userIDFromAppState = extractUIDFromAppState(appState);
763
+ (async () => {
764
+ try {
765
+ if (appState) {
766
+ // Check and convert cookie to appState format
767
+ if (Array.isArray(appState) && appState.some(c => c.name)) {
768
+ // Convert name to key if needed
769
+ appState = appState.map(c => {
770
+ if (c.name && !c.key) {
771
+ c.key = c.name;
772
+ delete c.name;
773
+ }
774
+ return c;
775
+ });
776
+ } else if (typeof appState === "string") {
777
+ // Try to parse as JSON first
778
+ let parsed = appState;
779
+ try {
780
+ parsed = JSON.parse(appState);
781
+ } catch { }
782
+
783
+ if (Array.isArray(parsed)) {
784
+ // Already parsed as array, use it
785
+ appState = parsed;
786
+ } else {
787
+ // Parse string cookie format (key=value; key2=value2)
788
+ const arrayAppState = [];
789
+ appState.split(';').forEach(c => {
790
+ const [key, value] = c.split('=');
791
+ if (key && value) {
792
+ arrayAppState.push({
793
+ key: key.trim(),
794
+ value: value.trim(),
795
+ domain: ".facebook.com",
796
+ path: "/",
797
+ expires: new Date().getTime() + 1000 * 60 * 60 * 24 * 365
798
+ });
799
+ }
800
+ });
801
+ appState = arrayAppState;
802
+ }
803
+ }
804
+
805
+ // Set cookies into jar with individual domain/path
806
+ if (Array.isArray(appState)) {
807
+ await setJarCookies(jar, appState);
808
+ } else {
809
+ throw new Error("Invalid appState format");
810
+ }
811
+ }
812
+ if (Cookie) {
813
+ let cookiePairs = [];
814
+ if (typeof Cookie === "string") cookiePairs = normalizeCookieHeaderString(Cookie);
815
+ else if (Array.isArray(Cookie)) cookiePairs = Cookie.map(String).filter(Boolean);
816
+ else if (Cookie && typeof Cookie === "object") cookiePairs = Object.entries(Cookie).map(([k, v]) => `${k}=${v}`);
817
+ if (cookiePairs.length) setJarFromPairs(jar, cookiePairs, domain);
818
+ }
819
+ } catch (e) {
820
+ return callback(e);
821
+ }
822
+ const ctx = { globalOptions, options: globalOptions, reconnectAttempts: 0 };
823
+ ctx.bypassAutomation = async function (resp, j) {
824
+ global.fca = global.fca || {};
825
+ global.fca.BypassAutomationNotification = this.bypassAutomation.bind(this);
826
+ const s = x => (typeof x === "string" ? x : String(x ?? ""));
827
+ const u = r => r?.request?.res?.responseUrl || (r?.config?.baseURL ? new URL(r.config.url || "/", r.config.baseURL).toString() : r?.config?.url || "");
828
+ const isCp = r => typeof u(r) === "string" && u(r).includes("checkpoint/601051028565049");
829
+ const cookieUID = async () => {
830
+ try {
831
+ const cookies = typeof j?.getCookies === "function" ? await j.getCookies("https://www.facebook.com") : [];
832
+ return cookies.find(c => c.key === "i_user")?.value || cookies.find(c => c.key === "c_user")?.value;
833
+ } catch { return undefined; }
834
+ };
835
+ const htmlUID = body => s(body).match(/"USER_ID"\s*:\s*"(\d+)"/)?.[1] || s(body).match(/\["CurrentUserInitialData",\[\],\{.*?"USER_ID":"(\d+)".*?\},\d+\]/)?.[1];
836
+ const getUID = async body => (await cookieUID()) || htmlUID(body);
837
+ const refreshJar = async () => get("https://www.facebook.com/", j, null, this.options).then(saveCookies(j));
838
+ const bypass = async body => {
839
+ const b = s(body);
840
+ const UID = await getUID(b);
841
+ const fb_dtsg = getFrom(b, '"DTSGInitData",[],{"token":"', '",') || b.match(/name="fb_dtsg"\s+value="([^"]+)"/)?.[1];
842
+ const jazoest = getFrom(b, 'name="jazoest" value="', '"') || getFrom(b, "jazoest=", '",') || b.match(/name="jazoest"\s+value="([^"]+)"/)?.[1];
843
+ const lsd = getFrom(b, '["LSD",[],{"token":"', '"}') || b.match(/name="lsd"\s+value="([^"]+)"/)?.[1];
844
+ const form = { av: UID, fb_dtsg, jazoest, lsd, fb_api_caller_class: "RelayModern", fb_api_req_friendly_name: "FBScrapingWarningMutation", variables: "{}", server_timestamps: true, doc_id: 6339492849481770 };
845
+ await post("https://www.facebook.com/api/graphql/", j, form, null, this.options).then(saveCookies(j));
846
+ logger("Facebook automation warning detected, handling...", "warn");
847
+ this.reconnectAttempts = 0;
848
+ };
849
+ try {
850
+ if (resp) {
851
+ if (isCp(resp)) {
852
+ await bypass(s(resp.data));
853
+ const refreshed = await refreshJar();
854
+ if (isCp(refreshed)) logger("Checkpoint still present after refresh", "warn");
855
+ else logger("Bypass complete, cookies refreshed", "info");
856
+ return refreshed;
857
+ }
858
+ return resp;
859
+ }
860
+ const first = await get("https://www.facebook.com/", j, null, this.options).then(saveCookies(j));
861
+ if (isCp(first)) {
862
+ await bypass(s(first.data));
863
+ const refreshed = await refreshJar();
864
+ if (!isCp(refreshed)) logger("Bypass complete, cookies refreshed", "info");
865
+ else logger("Checkpoint still present after refresh", "warn");
866
+ return refreshed;
867
+ }
868
+ return first;
869
+ } catch (e) {
870
+ logger(`Bypass automation error: ${e && e.message ? e.message : String(e)}`, "error");
871
+ return resp;
872
+ }
873
+ };
874
+ if (appState || Cookie) {
875
+ const initial = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
876
+ return (await ctx.bypassAutomation(initial, jar)) || initial;
877
+ }
878
+ const hydrated = await hydrateJarFromDB(null);
879
+ if (hydrated) {
880
+ logger("AppState backup live proceeding to login", "info");
881
+ const initial = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
882
+ return (await ctx.bypassAutomation(initial, jar)) || initial;
883
+ }
884
+ logger("AppState backup die — proceeding to email/password login", "warn");
885
+ return get("https://www.facebook.com/", null, null, globalOptions)
886
+ .then(saveCookies(jar))
887
+ .then(makeLogin(jar, email, password, globalOptions))
888
+ .then(function () {
889
+ return get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
890
+ });
891
+ })()
892
+ .then(async function (res) {
893
+ const ctx = {};
894
+ ctx.options = globalOptions;
895
+ ctx.bypassAutomation = async function (resp, j) {
896
+ global.fca = global.fca || {};
897
+ global.fca.BypassAutomationNotification = this.bypassAutomation.bind(this);
898
+ const s = x => (typeof x === "string" ? x : String(x ?? ""));
899
+ const u = r => r?.request?.res?.responseUrl || (r?.config?.baseURL ? new URL(r.config.url || "/", r.config.baseURL).toString() : r?.config?.url || "");
900
+ const isCp = r => typeof u(r) === "string" && u(r).includes("checkpoint/601051028565049");
901
+ const cookieUID = async () => {
902
+ try {
903
+ const cookies = typeof j?.getCookies === "function" ? await j.getCookies("https://www.facebook.com") : [];
904
+ return cookies.find(c => c.key === "i_user")?.value || cookies.find(c => c.key === "c_user")?.value;
905
+ } catch { return undefined; }
906
+ };
907
+ const htmlUID = body => s(body).match(/"USER_ID"\s*:\s*"(\d+)"/)?.[1] || s(body).match(/\["CurrentUserInitialData",\[\],\{.*?"USER_ID":"(\d+)".*?\},\d+\]/)?.[1];
908
+ const getUID = async body => (await cookieUID()) || htmlUID(body);
909
+ const refreshJar = async () => get("https://www.facebook.com/", j, null, this.options).then(saveCookies(j));
910
+ const bypass = async body => {
911
+ const b = s(body);
912
+ const UID = await getUID(b);
913
+ const fb_dtsg = getFrom(b, '"DTSGInitData",[],{"token":"', '",') || b.match(/name="fb_dtsg"\s+value="([^"]+)"/)?.[1];
914
+ const jazoest = getFrom(b, 'name="jazoest" value="', '"') || getFrom(b, "jazoest=", '",') || b.match(/name="jazoest"\s+value="([^"]+)"/)?.[1];
915
+ const lsd = getFrom(b, '["LSD",[],{"token":"', '"}') || b.match(/name="lsd"\s+value="([^"]+)"/)?.[1];
916
+ const form = { av: UID, fb_dtsg, jazoest, lsd, fb_api_caller_class: "RelayModern", fb_api_req_friendly_name: "FBScrapingWarningMutation", variables: "{}", server_timestamps: true, doc_id: 6339492849481770 };
917
+ await post("https://www.facebook.com/api/graphql/", j, form, null, this.options).then(saveCookies(j));
918
+ logger("Facebook automation warning detected, handling...", "warn");
919
+ };
920
+ try {
921
+ if (res && isCp(res)) {
922
+ await bypass(s(res.data));
923
+ const refreshed = await refreshJar();
924
+ if (!isCp(refreshed)) logger("Bypass complete, cookies refreshed", "info");
925
+ return refreshed;
926
+ }
927
+ logger("No checkpoint detected", "info");
928
+ return res;
929
+ } catch {
930
+ return res;
931
+ }
932
+ };
933
+ const processed = (await ctx.bypassAutomation(res, jar)) || res;
934
+ let html = processed && processed.data ? processed.data : "";
935
+ let cookies = await Promise.resolve(jar.getCookies("https://www.facebook.com"));
936
+ const getUIDFromCookies = cs =>
937
+ cs.find(c => c.key === "i_user")?.value ||
938
+ cs.find(c => c.key === "c_user")?.value ||
939
+ cs.find(c => c.name === "i_user")?.value ||
940
+ cs.find(c => c.name === "c_user")?.value;
941
+ const getUIDFromHTML = body => {
942
+ const s = typeof body === "string" ? body : String(body ?? "");
943
+ return s.match(/"USER_ID"\s*:\s*"(\d+)"/)?.[1] || s.match(/\["CurrentUserInitialData",\[\],\{.*?"USER_ID":"(\d+)".*?\},\d+\]/)?.[1];
944
+ };
945
+ // Helper to validate UID - must be a non-zero positive number string
946
+ const isValidUID = uid => uid && uid !== "0" && /^\d+$/.test(uid) && parseInt(uid, 10) > 0;
947
+
948
+ let userID = getUIDFromCookies(cookies);
949
+ // Also try to extract userID from HTML if not found in cookies
950
+ if (!isValidUID(userID)) {
951
+ userID = getUIDFromHTML(html);
952
+ }
953
+ // If still not found and appState was provided, use userID from appState input as fallback
954
+ if (!isValidUID(userID) && userIDFromAppState && isValidUID(userIDFromAppState)) {
955
+ userID = userIDFromAppState;
956
+ }
957
+ // Trigger auto-login if userID is invalid (missing or "0")
958
+ if (!isValidUID(userID)) {
959
+ logger("Invalid userID detected (missing or 0), attempting auto-login...", "warn");
960
+ // Pass hadAppStateInput=true if appState/Cookie was originally provided
961
+ const retried = await tryAutoLoginIfNeeded(html, cookies, globalOptions, ctx, !!(appState || Cookie));
962
+ html = retried.html;
963
+ cookies = retried.cookies;
964
+ userID = retried.userID;
965
+
966
+ // Validate HTML after auto-login - ensure it contains valid USER_ID
967
+ const htmlUserIDAfterLogin = getUIDFromHTML(html);
968
+ if (!isValidUID(htmlUserIDAfterLogin)) {
969
+ logger("After auto-login, HTML still does not contain valid USER_ID. Session may not be established.", "error");
970
+ // Try one more refresh
971
+ try {
972
+ const refreshRes = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
973
+ const refreshedHtml = refreshRes && refreshRes.data ? refreshRes.data : "";
974
+ const refreshedHtmlUID = getUIDFromHTML(refreshedHtml);
975
+ if (isValidUID(refreshedHtmlUID)) {
976
+ html = refreshedHtml;
977
+ userID = refreshedHtmlUID;
978
+ logger(`After refresh, found valid USER_ID in HTML: ${userID}`, "info");
979
+ } else {
980
+ throw new Error("Login failed - HTML does not show valid USER_ID after auto-login and refresh");
981
+ }
982
+ } catch (refreshErr) {
983
+ throw new Error(`Login failed - Could not establish valid session. HTML USER_ID check failed: ${refreshErr && refreshErr.message ? refreshErr.message : String(refreshErr)}`);
984
+ }
985
+ } else {
986
+ // Use USER_ID from HTML as it's more reliable
987
+ userID = htmlUserIDAfterLogin;
988
+ logger(`After auto-login, using USER_ID from HTML: ${userID}`, "info");
989
+ }
990
+ }
991
+ if (html.includes("/checkpoint/block/?next")) {
992
+ logger("Appstate die, vui lòng thay cái mới!", "error");
993
+ throw new Error("Checkpoint");
994
+ }
995
+
996
+ // Final validation: ensure HTML shows we're logged in before proceeding
997
+ let finalHtmlUID = getUIDFromHTML(html);
998
+ if (!isValidUID(finalHtmlUID)) {
999
+ // If cookies have valid UID but HTML doesn't, try to "activate" session
1000
+ if (isValidUID(userID)) {
1001
+ logger(`HTML shows USER_ID=${finalHtmlUID || "none"} but cookies have valid UID=${userID}. Attempting to activate session...`, "warn");
1002
+
1003
+ // Try making requests to activate the session
1004
+ try {
1005
+ // Wait a bit first for cookies to propagate
1006
+ await new Promise(resolve => setTimeout(resolve, 1000));
1007
+
1008
+ // Try refreshing with m.facebook.com/home.php (mobile home page)
1009
+ logger("Trying to activate session via m.facebook.com/home.php...", "info");
1010
+ const activateRes = await get("https://m.facebook.com/home.php", jar, null, globalOptions).then(saveCookies(jar));
1011
+ const activateHtml = activateRes && activateRes.data ? activateRes.data : "";
1012
+ const activateUID = getUIDFromHTML(activateHtml);
1013
+
1014
+ if (isValidUID(activateUID)) {
1015
+ html = activateHtml;
1016
+ finalHtmlUID = activateUID;
1017
+ userID = activateUID;
1018
+ logger(`Session activated! Found valid USER_ID in HTML: ${userID}`, "info");
1019
+ } else {
1020
+ // Try one more time with www.facebook.com/home.php after delay
1021
+ await new Promise(resolve => setTimeout(resolve, 1500));
1022
+ logger("Trying to activate session via www.facebook.com/home.php...", "info");
1023
+ const activateRes2 = await get("https://www.facebook.com/home.php", jar, null, globalOptions).then(saveCookies(jar));
1024
+ const activateHtml2 = activateRes2 && activateRes2.data ? activateRes2.data : "";
1025
+ const activateUID2 = getUIDFromHTML(activateHtml2);
1026
+
1027
+ if (isValidUID(activateUID2)) {
1028
+ html = activateHtml2;
1029
+ finalHtmlUID = activateUID2;
1030
+ userID = activateUID2;
1031
+ logger(`Session activated on second try! Found valid USER_ID in HTML: ${userID}`, "info");
1032
+ } else {
1033
+ // If cookies have valid UID, we can proceed with cookie-based UID but warn
1034
+ logger(`WARNING: HTML still shows USER_ID=${finalHtmlUID || "none"} but cookies have valid UID=${userID}. Proceeding with cookie-based UID.`, "warn");
1035
+ // Don't throw error, proceed with cookie-based UID
1036
+ }
1037
+ }
1038
+ } catch (activateErr) {
1039
+ logger(`Failed to activate session: ${activateErr && activateErr.message ? activateErr.message : String(activateErr)}. Proceeding with cookie-based UID.`, "warn");
1040
+ // Don't throw error, proceed with cookie-based UID
1041
+ }
1042
+ } else {
1043
+ // No valid UID in either cookies or HTML
1044
+ logger(`Final HTML validation failed - USER_ID from HTML: ${finalHtmlUID || "none"}, from cookies: ${userID || "none"}`, "error");
1045
+ throw new Error("Login validation failed - HTML does not contain valid USER_ID. Session may not be properly established.");
1046
+ }
1047
+ }
1048
+
1049
+ // Final check: ensure we have a valid userID (either from HTML or cookies)
1050
+ if (!isValidUID(userID)) {
1051
+ logger(`No valid USER_ID found - HTML: ${finalHtmlUID || "none"}, Cookies: ${userID || "none"}`, "error");
1052
+ throw new Error("Login validation failed - No valid USER_ID found in HTML or cookies.");
1053
+ }
1054
+ let mqttEndpoint;
1055
+ let region = "PRN";
1056
+ let fb_dtsg;
1057
+ let irisSeqID;
1058
+ try {
1059
+ parseUserHtml(html);
1060
+ const m1 = html.match(/"endpoint":"([^"]+)"/);
1061
+ const m2 = m1 ? null : html.match(/endpoint\\":\\"([^\\"]+)\\"/);
1062
+ const raw = (m1 && m1[1]) || (m2 && m2[1]);
1063
+ if (raw) mqttEndpoint = raw.replace(/\\\//g, "/");
1064
+ region = parseRegion(html);
1065
+ const rinfo = REGION_MAP.get(region);
1066
+ if (rinfo) logger(`Server region ${region} - ${rinfo.name}`, "info");
1067
+ else logger(`Server region ${region}`, "info");
1068
+ } catch {
1069
+ logger("Not MQTT endpoint", "warn");
1070
+ }
1071
+ try {
1072
+ const userDataMatch = String(html).match(/\["CurrentUserInitialData",\[\],({.*?}),\d+\]/);
1073
+ if (userDataMatch) {
1074
+ const info = JSON.parse(userDataMatch[1]);
1075
+ logger(`Đăng nhập tài khoản: ${info.NAME} (${info.USER_ID})`, "info");
1076
+
1077
+ // Check if Facebook response shows USER_ID = 0 (session dead)
1078
+ if (!isValidUID(info.USER_ID)) {
1079
+ logger("Facebook response shows invalid USER_ID (0 or empty), session is dead!", "warn");
1080
+ // Force trigger auto-login
1081
+ const retried = await tryAutoLoginIfNeeded(html, cookies, globalOptions, ctx, !!(appState || Cookie));
1082
+ html = retried.html;
1083
+ cookies = retried.cookies;
1084
+ userID = retried.userID;
1085
+ // Re-check after auto-login
1086
+ if (!isValidUID(userID)) {
1087
+ throw new Error("Auto-login failed - could not get valid userID");
1088
+ }
1089
+ }
1090
+ } else if (userID) {
1091
+ logger(`ID người dùng: ${userID}`, "info");
1092
+ }
1093
+ } catch (userDataErr) {
1094
+ // If error is from our validation, rethrow it
1095
+ if (userDataErr && userDataErr.message && userDataErr.message.includes("Auto-login failed")) {
1096
+ throw userDataErr;
1097
+ }
1098
+ // Otherwise ignore parsing errors
1099
+ }
1100
+ const tokenMatch = html.match(/DTSGInitialData.*?token":"(.*?)"/);
1101
+ if (tokenMatch) fb_dtsg = tokenMatch[1];
1102
+ try {
1103
+ if (userID) await backupAppStateSQL(jar, userID);
1104
+ } catch { }
1105
+ Promise.resolve()
1106
+ .then(function () {
1107
+ if (models && models.sequelize && typeof models.sequelize.authenticate === "function") {
1108
+ return models.sequelize.authenticate();
1109
+ }
1110
+ })
1111
+ .then(function () {
1112
+ if (models && typeof models.syncAll === "function") {
1113
+ return models.syncAll();
1114
+ }
1115
+ })
1116
+ .catch(function (error) {
1117
+ // Silently handle database errors - they're not critical for login
1118
+ const errorMsg = error && error.message ? error.message : String(error);
1119
+ if (!errorMsg.includes("No Sequelize instance passed")) {
1120
+ // Only log non-Sequelize instance errors
1121
+ logger(`Database connection failed: ${errorMsg}`, "warn");
1122
+ }
1123
+ });
1124
+ logger("FCA fix/update by DongDev (Donix-VN)", "info");
1125
+ const ctxMain = {
1126
+ userID,
1127
+ jar,
1128
+ globalOptions,
1129
+ loggedIn: true,
1130
+ access_token: "NONE",
1131
+ clientMutationId: 0,
1132
+ mqttClient: undefined,
1133
+ lastSeqId: irisSeqID,
1134
+ syncToken: undefined,
1135
+ mqttEndpoint,
1136
+ region,
1137
+ firstListen: true,
1138
+ fb_dtsg,
1139
+ clientID: ((Math.random() * 2147483648) | 0).toString(16),
1140
+ clientId: getFrom(html, '["MqttWebDeviceID",[],{"clientID":"', '"}') || "",
1141
+ wsReqNumber: 0,
1142
+ wsTaskNumber: 0,
1143
+ tasks: new Map()
1144
+ };
1145
+ ctxMain.options = globalOptions;
1146
+ ctxMain.bypassAutomation = ctx.bypassAutomation.bind(ctxMain);
1147
+ ctxMain.performAutoLogin = async () => {
1148
+ try {
1149
+ const u = config.credentials?.email || email;
1150
+ const p = config.credentials?.password || password;
1151
+ const tf = config.credentials?.twofactor || null;
1152
+ if (!u || !p) return false;
1153
+ const r = await tokens(u, p, tf);
1154
+ if (!(r && r.status && Array.isArray(r.cookies))) return false;
1155
+ const pairs = r.cookies.map(c => `${c.key || c.name}=${c.value}`);
1156
+ setJarFromPairs(jar, pairs, ".facebook.com");
1157
+ await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
1158
+ return true;
1159
+ } catch {
1160
+ return false;
1161
+ }
1162
+ };
1163
+ const api = {
1164
+ setOptions: require("./options").setOptions.bind(null, globalOptions),
1165
+ getCookies: function () {
1166
+ return cookieHeaderFromJar(jar);
1167
+ },
1168
+ getAppState: function () {
1169
+ return getAppState(jar);
1170
+ },
1171
+ getLatestAppStateFromDB: async function (uid = userID) {
1172
+ const data = await getLatestBackup(uid, "appstate");
1173
+ return data ? JSON.parse(data) : null;
1174
+ },
1175
+ getLatestCookieFromDB: async function (uid = userID) {
1176
+ return await getLatestBackup(uid, "cookie");
1177
+ }
1178
+ };
1179
+ const defaultFuncs = makeDefaults(html, userID, ctxMain);
1180
+ const srcRoot = path.join(__dirname, "../src/api");
1181
+ let loaded = 0;
1182
+ let skipped = 0;
1183
+ fs.readdirSync(srcRoot, { withFileTypes: true }).forEach((sub) => {
1184
+ if (!sub.isDirectory()) return;
1185
+ const subDir = path.join(srcRoot, sub.name);
1186
+ fs.readdirSync(subDir, { withFileTypes: true }).forEach((entry) => {
1187
+ if (!entry.isFile() || !entry.name.endsWith(".js")) return;
1188
+ const p = path.join(subDir, entry.name);
1189
+ const key = path.basename(entry.name, ".js");
1190
+ if (api[key]) {
1191
+ skipped++;
1192
+ return;
1193
+ }
1194
+ let mod;
1195
+ try {
1196
+ mod = require(p);
1197
+ } catch (e) {
1198
+ logger(`Failed to require API module ${p}: ${e && e.message ? e.message : String(e)}`, "warn");
1199
+ skipped++;
1200
+ return;
1201
+ }
1202
+ const factory = typeof mod === "function" ? mod : (mod && typeof mod.default === "function" ? mod.default : null);
1203
+ if (!factory) {
1204
+ logger(`API module ${p} does not export a function, skipping`, "warn");
1205
+ skipped++;
1206
+ return;
1207
+ }
1208
+ api[key] = factory(defaultFuncs, api, ctxMain);
1209
+ loaded++;
1210
+ });
1211
+ });
1212
+ logger(`Loaded ${loaded} FCA API methods${skipped ? `, skipped ${skipped} duplicates` : ""}`);
1213
+ if (api.listenMqtt) api.listen = api.listenMqtt;
1214
+ if (api.refreshFb_dtsg) {
1215
+ setInterval(function () {
1216
+ api.refreshFb_dtsg().then(function () {
1217
+ logger("Successfully refreshed fb_dtsg");
1218
+ }).catch(function () {
1219
+ logger("An error occurred while refreshing fb_dtsg", "error");
1220
+ });
1221
+ }, 86400000);
1222
+ }
1223
+ logger("Login successful!");
1224
+ callback(null, api);
1225
+ })
1226
+ .catch(function (e) {
1227
+ callback(e);
1228
+ });
1229
+ } catch (e) {
1230
+ callback(e);
1231
+ }
1232
+ }
1233
+
1234
+ module.exports = loginHelper;
1235
+ module.exports.loginHelper = loginHelper;
1236
+ module.exports.tokensViaAPI = tokensViaAPI;
1237
+ module.exports.loginViaAPI = loginViaAPI;
1238
+ module.exports.tokens = tokens;
1239
+ module.exports.normalizeCookieHeaderString = normalizeCookieHeaderString;
1240
+ module.exports.setJarFromPairs = setJarFromPairs;