@dongdev/fca-unofficial 3.0.27 → 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.
- package/.gitattributes +2 -0
- package/CHANGELOG.md +196 -193
- package/DOCS.md +3 -6
- package/README.md +1 -1
- package/index.d.ts +745 -746
- package/module/config.js +29 -29
- package/module/login.js +133 -134
- package/module/loginHelper.js +1240 -1240
- package/module/options.js +44 -45
- package/package.json +81 -81
- package/src/api/messaging/changeAdminStatus.js +56 -56
- package/src/api/messaging/changeThreadEmoji.js +47 -47
- package/src/api/messaging/createPoll.js +46 -46
- package/src/api/messaging/forwardAttachment.js +28 -28
- package/src/api/messaging/sendTypingIndicator.js +23 -23
- package/src/api/messaging/setMessageReaction.js +91 -91
- package/src/api/messaging/setTitle.js +47 -47
- package/src/api/socket/core/connectMqtt.js +250 -250
- package/src/api/socket/core/emitAuth.js +1 -1
- package/src/api/socket/core/getSeqID.js +322 -322
- package/src/api/socket/core/parseDelta.js +368 -377
- package/src/api/socket/listenMqtt.js +371 -372
- package/src/utils/cookies.js +68 -68
- package/src/utils/loginParser.js +347 -347
- package/src/utils/messageFormat.js +1173 -1173
- package/src/api/socket/core/markDelivery.js +0 -12
package/module/loginHelper.js
CHANGED
|
@@ -1,1240 +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 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;
|
|
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;
|