@dongdev/fca-unofficial 3.0.23 → 3.0.27

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.
@@ -8,13 +8,8 @@ const { saveCookies, getAppState } = require("../src/utils/client");
8
8
  const { getFrom } = require("../src/utils/constants");
9
9
  const { loadConfig } = require("./config");
10
10
  const { config } = loadConfig();
11
- const { v4: uuidv4 } = require("uuid");
12
- const { CookieJar } = require("tough-cookie");
13
- const { wrapper } = require("axios-cookiejar-support");
14
11
  const axiosBase = require("axios");
15
- const qs = require("querystring");
16
- const crypto = require("crypto");
17
- const { TOTP } = require("totp-generator");
12
+ const parseUserHtml = require("./parseUseerHtml");
18
13
 
19
14
  const regions = [
20
15
  { code: "PRN", name: "Pacific Northwest Region", location: "Khu vực Tây Bắc Thái Bình Dương" },
@@ -53,81 +48,167 @@ function mask(s, keep = 3) {
53
48
  return n <= keep ? "*".repeat(n) : s.slice(0, keep) + "*".repeat(Math.max(0, n - keep));
54
49
  }
55
50
 
56
- function md5(s) {
57
- return crypto.createHash("md5").update(s).digest("hex");
58
- }
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;
59
65
 
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
+ // Build request body
67
+ const body = {
68
+ email,
69
+ password
70
+ };
66
71
 
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
- }
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
+ }
73
77
 
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
- }
78
+ // Build headers
79
+ const headers = {
80
+ "Content-Type": "application/json",
81
+ "Accept": "application/json"
82
+ };
79
83
 
80
- function rand(min, max) {
81
- return Math.floor(Math.random() * (max - min + 1)) + min;
82
- }
84
+ // Add x-api-key header if provided
85
+ if (xApiKey) {
86
+ headers["x-api-key"] = xApiKey;
87
+ }
83
88
 
84
- function choice(arr) {
85
- return arr[Math.floor(Math.random() * arr.length)];
86
- }
89
+ logger(`API-LOGIN: Attempting login for ${mask(email, 2)} via iOS API`, "info");
87
90
 
88
- function randomBuildId() {
89
- const prefixes = ["QP1A", "RP1A", "SP1A", "TP1A", "UP1A", "AP4A"];
90
- return `${choice(prefixes)}.${rand(180000, 250000)}.${rand(10, 99)}`;
91
- }
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;
92
101
 
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
- }
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
+ }
97
107
 
98
- function randomFbav() {
99
- return `${rand(390, 499)}.${rand(0, 3)}.${rand(0, 2)}.${rand(10, 60)}.${rand(100, 999)}`;
100
- }
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;
101
112
 
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
- }
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");
118
119
 
119
- const MOBILE_UA = randomOrcaUA();
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}`;
120
163
 
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 };
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
+ }
124
172
  }
125
173
 
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
- };
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
+ }
131
212
 
132
213
  function normalizeCookieHeaderString(s) {
133
214
  let str = String(s || "").trim();
@@ -149,17 +230,36 @@ function normalizeCookieHeaderString(s) {
149
230
 
150
231
  function setJarFromPairs(j, pairs, domain) {
151
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
+
152
243
  for (const kv of pairs) {
153
244
  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 { }
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
+ }
158
258
  }
159
259
  }
160
260
 
161
261
  function cookieHeaderFromJar(j) {
162
- const urls = ["https://www.facebook.com", "https://www.messenger.com"];
262
+ const urls = ["https://www.facebook.com"];
163
263
  const seen = new Set();
164
264
  const parts = [];
165
265
  for (const u of urls) {
@@ -249,10 +349,6 @@ async function backupAppStateSQL(j, userID) {
249
349
  const ck = cookieHeaderFromJar(j);
250
350
  await upsertBackup(Model, userID, "appstate", JSON.stringify(appJson));
251
351
  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
352
  logger("Backup stored (overwrite mode)", "info");
257
353
  } catch (e) {
258
354
  logger(`Failed to save appstate backup ${e && e.message ? e.message : String(e)}`, "warn");
@@ -281,20 +377,7 @@ async function getLatestBackupAny(type) {
281
377
  }
282
378
  }
283
379
 
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
380
 
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
381
 
299
382
  async function setJarCookies(j, appstate) {
300
383
  const tasks = [];
@@ -357,48 +440,24 @@ async function setJarCookies(j, appstate) {
357
440
 
358
441
  return cookieParts.join("; ");
359
442
  };
360
-
361
- // Determine target URLs and cookie strings based on domain
362
443
  const cookieConfigs = [];
363
-
364
- // For .facebook.com domain, set for both facebook.com and messenger.com
365
444
  if (cookieDomain === ".facebook.com" || cookieDomain === "facebook.com") {
366
- // Set for facebook.com with .facebook.com domain
367
445
  cookieConfigs.push({ url: `http://${dom}${cookiePath}`, cookieStr: buildCookieString() });
368
446
  cookieConfigs.push({ url: `https://${dom}${cookiePath}`, cookieStr: buildCookieString() });
369
447
  cookieConfigs.push({ url: `http://www.${dom}${cookiePath}`, cookieStr: buildCookieString() });
370
448
  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
449
  } else {
386
- // For other domains, set normally
387
450
  cookieConfigs.push({ url: `http://${dom}${cookiePath}`, cookieStr: buildCookieString() });
388
451
  cookieConfigs.push({ url: `https://${dom}${cookiePath}`, cookieStr: buildCookieString() });
389
452
  cookieConfigs.push({ url: `http://www.${dom}${cookiePath}`, cookieStr: buildCookieString() });
390
453
  cookieConfigs.push({ url: `https://www.${dom}${cookiePath}`, cookieStr: buildCookieString() });
391
454
  }
392
455
 
393
- // Set cookie for all target URLs, silently catch domain errors
394
456
  for (const config of cookieConfigs) {
395
457
  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
458
  if (err && err.message && err.message.includes("Cookie not in this host's domain")) {
399
- return; // Expected error, ignore
459
+ return;
400
460
  }
401
- // Log other errors but don't throw
402
461
  return;
403
462
  }));
404
463
  }
@@ -406,142 +465,9 @@ async function setJarCookies(j, appstate) {
406
465
  await Promise.all(tasks);
407
466
  }
408
467
 
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
-
468
+ // tokens function - alias to tokensViaAPI for backward compatibility
529
469
  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" };
470
+ return tokensViaAPI(username, password, twofactor);
545
471
  }
546
472
 
547
473
  async function hydrateJarFromDB(userID) {
@@ -580,6 +506,9 @@ async function hydrateJarFromDB(userID) {
580
506
  }
581
507
 
582
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
+
583
512
  const getUID = cs =>
584
513
  cs.find(c => c.key === "i_user")?.value ||
585
514
  cs.find(c => c.key === "c_user")?.value ||
@@ -589,59 +518,206 @@ async function tryAutoLoginIfNeeded(currentHtml, currentCookies, globalOptions,
589
518
  const s = typeof body === "string" ? body : String(body ?? "");
590
519
  return s.match(/"USER_ID"\s*:\s*"(\d+)"/)?.[1] || s.match(/\["CurrentUserInitialData",\[\],\{.*?"USER_ID":"(\d+)".*?\},\d+\]/)?.[1];
591
520
  };
521
+
592
522
  let userID = getUID(currentCookies);
593
- // Also try to extract userID from HTML if not found in cookies
594
- if (!userID) {
523
+ // Also try to extract userID from HTML if cookie userID is invalid
524
+ if (!isValidUID(userID)) {
595
525
  userID = htmlUID(currentHtml);
596
526
  }
597
- if (userID) return { html: currentHtml, cookies: currentCookies, userID };
598
- // If appState/Cookie was provided and is not dead (not checkpointed), skip backup
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
599
536
  if (hadAppStateInput) {
600
537
  const isCheckpoint = currentHtml.includes("/checkpoint/block/?next");
601
538
  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
539
  try {
605
540
  const refreshedCookies = await Promise.resolve(jar.getCookies("https://www.facebook.com"));
606
541
  userID = getUID(refreshedCookies);
607
- if (userID) {
542
+ if (isValidUID(userID)) {
608
543
  return { html: currentHtml, cookies: refreshedCookies, userID };
609
544
  }
610
545
  } catch { }
611
- // If still no userID, skip backup and throw error
612
- throw new Error("Missing user cookie from provided appState");
613
546
  }
614
- // AppState is dead (checkpointed), proceed to backup/email login
615
547
  }
548
+
549
+ // Try to hydrate from DB backup
616
550
  const hydrated = await hydrateJarFromDB(null);
617
551
  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 };
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)");
626
585
  }
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");
586
+
587
+ logger(`tryAutoLoginIfNeeded: Attempting API login for ${u.slice(0, 3)}***...`, "info");
588
+
633
589
  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");
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
+
641
693
  const cookies2 = await Promise.resolve(jar.getCookies("https://www.facebook.com"));
642
694
  const uid2 = getUID(cookies2);
643
- if (!uid2) throw new Error("Login failed");
644
- return { html: html2, cookies: cookies2, userID: uid2 };
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 };
645
721
  }
646
722
 
647
723
  function makeLogin(j, email, password, globalOptions) {
@@ -866,31 +942,121 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
866
942
  const s = typeof body === "string" ? body : String(body ?? "");
867
943
  return s.match(/"USER_ID"\s*:\s*"(\d+)"/)?.[1] || s.match(/\["CurrentUserInitialData",\[\],\{.*?"USER_ID":"(\d+)".*?\},\d+\]/)?.[1];
868
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
+
869
948
  let userID = getUIDFromCookies(cookies);
870
949
  // Also try to extract userID from HTML if not found in cookies
871
- if (!userID) {
950
+ if (!isValidUID(userID)) {
872
951
  userID = getUIDFromHTML(html);
873
952
  }
874
953
  // If still not found and appState was provided, use userID from appState input as fallback
875
- if (!userID && userIDFromAppState) {
954
+ if (!isValidUID(userID) && userIDFromAppState && isValidUID(userIDFromAppState)) {
876
955
  userID = userIDFromAppState;
877
956
  }
878
- if (!userID) {
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");
879
960
  // Pass hadAppStateInput=true if appState/Cookie was originally provided
880
961
  const retried = await tryAutoLoginIfNeeded(html, cookies, globalOptions, ctx, !!(appState || Cookie));
881
962
  html = retried.html;
882
963
  cookies = retried.cookies;
883
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
+ }
884
990
  }
885
991
  if (html.includes("/checkpoint/block/?next")) {
886
992
  logger("Appstate die, vui lòng thay cái mới!", "error");
887
993
  throw new Error("Checkpoint");
888
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
+ }
889
1054
  let mqttEndpoint;
890
1055
  let region = "PRN";
891
1056
  let fb_dtsg;
892
1057
  let irisSeqID;
893
1058
  try {
1059
+ parseUserHtml(html);
894
1060
  const m1 = html.match(/"endpoint":"([^"]+)"/);
895
1061
  const m2 = m1 ? null : html.match(/endpoint\\":\\"([^\\"]+)\\"/);
896
1062
  const raw = (m1 && m1[1]) || (m2 && m2[1]);
@@ -907,10 +1073,30 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
907
1073
  if (userDataMatch) {
908
1074
  const info = JSON.parse(userDataMatch[1]);
909
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
+ }
910
1090
  } else if (userID) {
911
1091
  logger(`ID người dùng: ${userID}`, "info");
912
1092
  }
913
- } catch { }
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
+ }
914
1100
  const tokenMatch = html.match(/DTSGInitialData.*?token":"(.*?)"/);
915
1101
  if (tokenMatch) fb_dtsg = tokenMatch[1];
916
1102
  try {
@@ -1046,3 +1232,9 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
1046
1232
  }
1047
1233
 
1048
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;