@dongdev/fca-unofficial 2.0.24 → 2.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/module/loginHelper.js +166 -56
- package/package.json +1 -1
- package/src/api/messaging/getThemePictures.js +62 -0
- package/src/api/messaging/removeUserFromGroup.js +2 -1
- package/src/api/socket/core/connectMqtt.js +13 -6
- package/src/api/socket/core/emitAuth.js +22 -0
- package/src/api/socket/core/getSeqID.js +31 -17
- package/src/api/socket/listenMqtt.js +38 -2
- package/src/api/threads/getThreadInfo.js +105 -26
package/CHANGELOG.md
CHANGED
package/module/loginHelper.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
const fs = require("fs");
|
|
2
3
|
const path = require("path");
|
|
3
4
|
const models = require("../src/database/models");
|
|
@@ -230,6 +231,10 @@ async function backupAppStateSQL(j, userID) {
|
|
|
230
231
|
const ck = cookieHeaderFromJar(j);
|
|
231
232
|
await upsertBackup(Model, userID, "appstate", JSON.stringify(appJson));
|
|
232
233
|
await upsertBackup(Model, userID, "cookie", ck);
|
|
234
|
+
try {
|
|
235
|
+
const out = path.join(process.cwd(), "appstate.json");
|
|
236
|
+
fs.writeFileSync(out, JSON.stringify(appJson, null, 2));
|
|
237
|
+
} catch { }
|
|
233
238
|
logger("Backup stored (overwrite mode)", "info");
|
|
234
239
|
} catch (e) {
|
|
235
240
|
logger(`Failed to save appstate backup ${e && e.message ? e.message : String(e)}`, "warn");
|
|
@@ -258,66 +263,171 @@ async function getLatestBackupAny(type) {
|
|
|
258
263
|
}
|
|
259
264
|
}
|
|
260
265
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
+
const MESSENGER_USER_AGENT = "Dalvik/2.1.0 (Linux; U; Android 9; ASUS_Z01QD Build/PQ3A.190605.003) [FBAN/Orca-Android;FBAV/391.2.0.20.404;FBPN/com.facebook.orca;FBLC/vi_VN;FBBV/437533963;FBCR/Viettel Telecom;FBMF/asus;FBBD/asus;FBDV/ASUS_Z01QD;FBSV/9;FBCA/x86:armeabi-v7a;FBDM/{density=1.5,width=1600,height=900};FB_FW/1;]";
|
|
267
|
+
|
|
268
|
+
function encodesig(obj) {
|
|
269
|
+
let data = "";
|
|
270
|
+
Object.keys(obj).forEach(k => { data += `${k}=${obj[k]}`; });
|
|
271
|
+
return md5(data + "62f8ce9f74b12f84c123cc23437a4a32");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function sort(obj) {
|
|
275
|
+
const keys = Object.keys(obj).sort();
|
|
276
|
+
const out = {};
|
|
277
|
+
for (const k of keys) out[k] = obj[k];
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function setJarCookies(j, appstate) {
|
|
282
|
+
const tasks = [];
|
|
283
|
+
for (const c of appstate) {
|
|
284
|
+
const dom = (c.domain || ".facebook.com").replace(/^\./, "");
|
|
285
|
+
const path = c.path || "/";
|
|
286
|
+
const base1 = `https://${dom}${path}`;
|
|
287
|
+
const base2 = `https://www.${dom}${path}`;
|
|
288
|
+
const str = `${c.key}=${c.value}; Domain=${c.domain || ".facebook.com"}; Path=${path};`;
|
|
289
|
+
tasks.push(j.setCookie(str, base1));
|
|
290
|
+
tasks.push(j.setCookie(str, base2));
|
|
266
291
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
292
|
+
await Promise.all(tasks);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function loginViaGraph(username, password, twofactorSecretOrCode, i_user, externalJar) {
|
|
296
|
+
const cookieJar = externalJar instanceof CookieJar ? externalJar : new CookieJar();
|
|
297
|
+
const client = wrapper(axiosBase.create({ jar: cookieJar, withCredentials: true, timeout: 30000, validateStatus: () => true }));
|
|
298
|
+
const device_id = uuidv4();
|
|
299
|
+
const family_device_id = device_id;
|
|
300
|
+
const machine_id = randomString(24);
|
|
301
|
+
const base = {
|
|
302
|
+
adid: "00000000-0000-0000-0000-000000000000",
|
|
303
|
+
format: "json",
|
|
304
|
+
device_id,
|
|
305
|
+
email: username,
|
|
306
|
+
password,
|
|
307
|
+
generate_analytics_claim: "1",
|
|
308
|
+
community_id: "",
|
|
309
|
+
cpl: "true",
|
|
310
|
+
try_num: "1",
|
|
311
|
+
family_device_id,
|
|
312
|
+
secure_family_device_id: "",
|
|
313
|
+
credentials_type: "password",
|
|
314
|
+
enroll_misauth: "false",
|
|
315
|
+
generate_session_cookies: "1",
|
|
316
|
+
source: "login",
|
|
317
|
+
generate_machine_id: "1",
|
|
318
|
+
jazoest: "22297",
|
|
319
|
+
meta_inf_fbmeta: "NO_FILE",
|
|
320
|
+
advertiser_id: "00000000-0000-0000-0000-000000000000",
|
|
321
|
+
currently_logged_in_userid: "0",
|
|
322
|
+
locale: "vi_VN",
|
|
323
|
+
client_country_code: "VN",
|
|
324
|
+
fb_api_req_friendly_name: "authenticate",
|
|
325
|
+
fb_api_caller_class: "AuthOperations$PasswordAuthOperation",
|
|
326
|
+
api_key: "256002347743983",
|
|
327
|
+
access_token: "256002347743983|374e60f8b9bb6b8cbb30f78030438895"
|
|
328
|
+
};
|
|
329
|
+
const headers = {
|
|
330
|
+
"User-Agent": MESSENGER_USER_AGENT,
|
|
331
|
+
"Accept-Encoding": "gzip, deflate",
|
|
332
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
333
|
+
"x-fb-connection-quality": "EXCELLENT",
|
|
334
|
+
"x-fb-sim-hni": "45204",
|
|
335
|
+
"x-fb-net-hni": "45204",
|
|
336
|
+
"x-fb-connection-type": "WIFI",
|
|
337
|
+
"x-tigon-is-retry": "False",
|
|
338
|
+
"x-fb-friendly-name": "authenticate",
|
|
339
|
+
"x-fb-request-analytics-tags": '{"network_tags":{"retry_attempt":"0"},"application_tags":"unknown"}',
|
|
340
|
+
"x-fb-http-engine": "Liger",
|
|
341
|
+
"x-fb-client-ip": "True",
|
|
342
|
+
"x-fb-server-cluster": "True",
|
|
343
|
+
authorization: `OAuth ${base.access_token}`
|
|
344
|
+
};
|
|
345
|
+
const form1 = { ...base };
|
|
346
|
+
form1.sig = encodesig(sort(form1));
|
|
347
|
+
const res1 = await client.request({ url: "https://b-graph.facebook.com/auth/login", method: "post", data: qs.stringify(form1), headers });
|
|
348
|
+
if (res1.status === 200 && res1.data && res1.data.session_cookies) {
|
|
349
|
+
const appstate = res1.data.session_cookies.map(c => ({ key: c.name, value: c.value, domain: c.domain, path: c.path }));
|
|
350
|
+
const cUserCookie = appstate.find(c => c.key === "c_user");
|
|
351
|
+
if (i_user) appstate.push({ key: "i_user", value: i_user, domain: ".facebook.com", path: "/" });
|
|
352
|
+
else if (cUserCookie) appstate.push({ key: "i_user", value: cUserCookie.value, domain: ".facebook.com", path: "/" });
|
|
353
|
+
await setJarCookies(cookieJar, appstate);
|
|
354
|
+
let eaau = null;
|
|
355
|
+
let eaad6v7 = null;
|
|
356
|
+
try {
|
|
357
|
+
const r1 = await client.request({ url: `https://api.facebook.com/method/auth.getSessionforApp?format=json&access_token=${res1.data.access_token}&new_app_id=350685531728`, method: "get", headers: { "user-agent": MESSENGER_USER_AGENT, "x-fb-connection-type": "WIFI", authorization: `OAuth ${res1.data.access_token}` } });
|
|
358
|
+
eaau = r1.data && r1.data.access_token ? r1.data.access_token : null;
|
|
359
|
+
} catch { }
|
|
300
360
|
try {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
361
|
+
const r2 = await client.request({ url: `https://api.facebook.com/method/auth.getSessionforApp?format=json&access_token=${res1.data.access_token}&new_app_id=275254692598279`, method: "get", headers: { "user-agent": MESSENGER_USER_AGENT, "x-fb-connection-type": "WIFI", authorization: `OAuth ${res1.data.access_token}` } });
|
|
362
|
+
eaad6v7 = r2.data && r2.data.access_token ? r2.data.access_token : null;
|
|
363
|
+
} catch { }
|
|
364
|
+
return { ok: true, cookies: appstate.map(c => ({ key: c.key, value: c.value })), jar: cookieJar, access_token_mess: res1.data.access_token || null, access_token: eaau, access_token_eaad6v7: eaad6v7, uid: res1.data.uid || cUserCookie?.value || null, session_key: res1.data.session_key || null };
|
|
365
|
+
}
|
|
366
|
+
const err = res1 && res1.data && res1.data.error ? res1.data.error : {};
|
|
367
|
+
if (err && err.code === 406) {
|
|
368
|
+
const data = err.error_data || {};
|
|
369
|
+
let code = null;
|
|
370
|
+
if (twofactorSecretOrCode && /^\d{6}$/.test(String(twofactorSecretOrCode))) code = String(twofactorSecretOrCode);
|
|
371
|
+
else if (twofactorSecretOrCode) {
|
|
372
|
+
try {
|
|
373
|
+
const clean = decodeURI(twofactorSecretOrCode).replace(/\s+/g, "").toUpperCase();
|
|
374
|
+
const { otp } = await TOTP.generate(clean);
|
|
375
|
+
code = otp;
|
|
376
|
+
} catch { }
|
|
377
|
+
} else if (config.credentials?.twofactor) {
|
|
378
|
+
const { otp } = await TOTP.generate(String(config.credentials.twofactor).replace(/\s+/g, "").toUpperCase());
|
|
379
|
+
code = otp;
|
|
319
380
|
}
|
|
381
|
+
if (!code) return { ok: false, message: "2FA required" };
|
|
382
|
+
const form2 = {
|
|
383
|
+
...base,
|
|
384
|
+
credentials_type: "two_factor",
|
|
385
|
+
twofactor_code: code,
|
|
386
|
+
userid: data.uid || username,
|
|
387
|
+
first_factor: data.login_first_factor || "",
|
|
388
|
+
machine_id: data.machine_id || machine_id
|
|
389
|
+
};
|
|
390
|
+
form2.sig = encodesig(sort(form2));
|
|
391
|
+
const res2 = await client.request({ url: "https://b-graph.facebook.com/auth/login", method: "post", data: qs.stringify(form2), headers });
|
|
392
|
+
if (res2.status === 200 && res2.data && res2.data.session_cookies) {
|
|
393
|
+
const appstate = res2.data.session_cookies.map(c => ({ key: c.name, value: c.value, domain: c.domain, path: c.path }));
|
|
394
|
+
const cUserCookie = appstate.find(c => c.key === "c_user");
|
|
395
|
+
if (i_user) appstate.push({ key: "i_user", value: i_user, domain: ".facebook.com", path: "/" });
|
|
396
|
+
else if (cUserCookie) appstate.push({ key: "i_user", value: cUserCookie.value, domain: ".facebook.com", path: "/" });
|
|
397
|
+
await setJarCookies(cookieJar, appstate);
|
|
398
|
+
let eaau = null;
|
|
399
|
+
let eaad6v7 = null;
|
|
400
|
+
try {
|
|
401
|
+
const r1 = await client.request({ url: `https://api.facebook.com/method/auth.getSessionforApp?format=json&access_token=${res2.data.access_token}&new_app_id=350685531728`, method: "get", headers: { "user-agent": MESSENGER_USER_AGENT, "x-fb-connection-type": "WIFI", authorization: `OAuth ${res2.data.access_token}` } });
|
|
402
|
+
eaau = r1.data && r1.data.access_token ? r1.data.access_token : null;
|
|
403
|
+
} catch { }
|
|
404
|
+
try {
|
|
405
|
+
const r2 = await client.request({ url: `https://api.facebook.com/method/auth.getSessionforApp?format=json&access_token=${res2.data.access_token}&new_app_id=275254692598279`, method: "get", headers: { "user-agent": MESSENGER_USER_AGENT, "x-fb-connection-type": "WIFI", authorization: `OAuth ${res2.data.access_token}` } });
|
|
406
|
+
eaad6v7 = r2.data && r2.data.access_token ? r2.data.access_token : null;
|
|
407
|
+
} catch { }
|
|
408
|
+
return { ok: true, cookies: appstate.map(c => ({ key: c.key, value: c.value })), jar: cookieJar, access_token_mess: res2.data.access_token || null, access_token: eaau, access_token_eaad6v7: eaad6v7, uid: res2.data.uid || cUserCookie?.value || null, session_key: res2.data.session_key || null };
|
|
409
|
+
}
|
|
410
|
+
return { ok: false, message: "2FA failed" };
|
|
411
|
+
}
|
|
412
|
+
return { ok: false, message: "Login failed" };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function tokens(username, password, twofactor = null) {
|
|
416
|
+
const t0 = process.hrtime.bigint();
|
|
417
|
+
if (!username || !password) return { status: false, message: "Please provide email and password" };
|
|
418
|
+
logger(`AUTO-LOGIN: Initialize login ${mask(username, 2)}`, "info");
|
|
419
|
+
const res = await loginViaGraph(username, password, twofactor, null, jar);
|
|
420
|
+
if (res && res.ok && Array.isArray(res.cookies)) {
|
|
421
|
+
logger(`AUTO-LOGIN: Login success ${res.cookies.length} cookies`, "info");
|
|
422
|
+
const t1 = Number(process.hrtime.bigint() - t0) / 1e6;
|
|
423
|
+
logger(`Done success login ${Math.round(t1)}ms`, "info");
|
|
424
|
+
return { status: true, cookies: res.cookies };
|
|
425
|
+
}
|
|
426
|
+
if (res && res.message === "2FA required") {
|
|
427
|
+
logger("AUTO-LOGIN: 2FA required but secret missing", "warn");
|
|
428
|
+
return { status: false, message: "Please provide the 2FA secret!" };
|
|
320
429
|
}
|
|
430
|
+
return { status: false, message: res && res.message ? res.message : "Login failed" };
|
|
321
431
|
}
|
|
322
432
|
|
|
323
433
|
async function hydrateJarFromDB(userID) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const log = require("npmlog");
|
|
4
|
+
const { parseAndCheckLogin } = require("../../utils/client");
|
|
5
|
+
const { getType } = require("../../utils/format");
|
|
6
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
7
|
+
return function getThemePictures(id, callback) {
|
|
8
|
+
let resolveFunc = function () { };
|
|
9
|
+
let rejectFunc = function () { };
|
|
10
|
+
const returnPromise = new Promise(function (resolve, reject) {
|
|
11
|
+
resolveFunc = resolve;
|
|
12
|
+
rejectFunc = reject;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (!callback) {
|
|
16
|
+
if (
|
|
17
|
+
getType(callback) == "Function" ||
|
|
18
|
+
getType(callback) == "AsyncFunction"
|
|
19
|
+
) {
|
|
20
|
+
callback = callback;
|
|
21
|
+
} else {
|
|
22
|
+
callback = function (err, data) {
|
|
23
|
+
if (err) {
|
|
24
|
+
return rejectFunc(err);
|
|
25
|
+
}
|
|
26
|
+
resolveFunc(data);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (getType(id) != "String") {
|
|
32
|
+
id = "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const form = {
|
|
36
|
+
fb_api_caller_class: "RelayModern",
|
|
37
|
+
fb_api_req_friendly_name: "MWPThreadThemeProviderQuery",
|
|
38
|
+
doc_id: "9734829906576883",
|
|
39
|
+
server_timestamps: true,
|
|
40
|
+
variables: JSON.stringify({
|
|
41
|
+
id
|
|
42
|
+
}),
|
|
43
|
+
av: ctx.userID
|
|
44
|
+
};
|
|
45
|
+
defaultFuncs
|
|
46
|
+
.post("https://www.facebook.com/api/graphql/", ctx.jar, form)
|
|
47
|
+
.then(parseAndCheckLogin(ctx, defaultFuncs))
|
|
48
|
+
.then(function (resData) {
|
|
49
|
+
if (resData.errors) {
|
|
50
|
+
throw resData;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return callback(null, resData);
|
|
54
|
+
})
|
|
55
|
+
.catch(function (err) {
|
|
56
|
+
log.error("getThemePictures", err);
|
|
57
|
+
return callback(err);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return returnPromise;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { getType } = require("../../utils/format");
|
|
4
|
+
const { parseAndCheckLogin } = require("../../utils/client");
|
|
4
5
|
const log = require("npmlog");
|
|
5
6
|
|
|
6
7
|
module.exports = function (defaultFuncs, api, ctx) {
|
|
@@ -80,23 +80,30 @@ module.exports = function createListenMqtt(deps) {
|
|
|
80
80
|
const mqttClient = ctx.mqttClient;
|
|
81
81
|
global.mqttClient = mqttClient;
|
|
82
82
|
|
|
83
|
-
|
|
84
83
|
mqttClient.on("error", function (err) {
|
|
85
84
|
const msg = String(err && err.message ? err.message : err || "");
|
|
86
|
-
|
|
87
|
-
if ((ctx._ending || ctx._cycling) && isEndingLikeError(msg)) {
|
|
85
|
+
if ((ctx._ending || ctx._cycling) && /No subscription existed|client disconnecting/i.test(msg)) {
|
|
88
86
|
logger(`mqtt expected during shutdown: ${msg}`, "info");
|
|
89
87
|
return;
|
|
90
88
|
}
|
|
89
|
+
|
|
90
|
+
if (/Not logged in|Not logged in.|blocked the login|401|403/i.test(msg)) {
|
|
91
|
+
try { mqttClient.end(true); } catch (_) { }
|
|
92
|
+
return emitAuth(ctx, api, globalCallback,
|
|
93
|
+
/blocked/i.test(msg) ? "login_blocked" : "not_logged_in",
|
|
94
|
+
msg
|
|
95
|
+
);
|
|
96
|
+
}
|
|
91
97
|
logger(`mqtt error: ${msg}`, "error");
|
|
92
98
|
try { mqttClient.end(true); } catch (_) { }
|
|
93
99
|
if (ctx._ending || ctx._cycling) return;
|
|
94
100
|
|
|
95
101
|
if (ctx.globalOptions.autoReconnect) {
|
|
96
|
-
|
|
102
|
+
const d = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
|
|
103
|
+
logger(`mqtt autoReconnect listenMqtt() in ${d}ms`, "warn");
|
|
104
|
+
setTimeout(() => listenMqtt(defaultFuncs, api, ctx, globalCallback), d);
|
|
97
105
|
} else {
|
|
98
|
-
|
|
99
|
-
globalCallback({ type: "stop_listen", error: msg || "Connection refused: Server unavailable" }, null);
|
|
106
|
+
globalCallback({ type: "stop_listen", error: msg || "Connection refused" }, null);
|
|
100
107
|
}
|
|
101
108
|
});
|
|
102
109
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
module.exports = function createEmitAuth({ logger }) {
|
|
3
|
+
return function emitAuth(ctx, api, globalCallback, reason, detail) {
|
|
4
|
+
try { if (ctx._autoCycleTimer) clearInterval(ctx._autoCycleTimer); } catch (_) { }
|
|
5
|
+
try { ctx._ending = true; } catch (_) { }
|
|
6
|
+
try { if (ctx.mqttClient) ctx.mqttClient.end(true); } catch (_) { }
|
|
7
|
+
ctx.mqttClient = undefined;
|
|
8
|
+
ctx.loggedIn = false;
|
|
9
|
+
|
|
10
|
+
const msg = detail || reason;
|
|
11
|
+
logger(`auth change -> ${reason}: ${msg}`, "error");
|
|
12
|
+
|
|
13
|
+
if (typeof globalCallback === "function") {
|
|
14
|
+
globalCallback({
|
|
15
|
+
type: "account_inactive",
|
|
16
|
+
reason,
|
|
17
|
+
error: msg,
|
|
18
|
+
timestamp: Date.now()
|
|
19
|
+
}, null);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
};
|
|
@@ -2,24 +2,38 @@
|
|
|
2
2
|
const { getType } = require("../../../utils/format");
|
|
3
3
|
const { parseAndCheckLogin } = require("../../../utils/client");
|
|
4
4
|
module.exports = function createGetSeqID(deps) {
|
|
5
|
-
const { listenMqtt } = deps;
|
|
5
|
+
const { listenMqtt, logger, emitAuth } = deps;
|
|
6
|
+
|
|
6
7
|
return function getSeqID(defaultFuncs, api, ctx, globalCallback, form) {
|
|
7
8
|
ctx.t_mqttCalled = false;
|
|
8
|
-
return defaultFuncs
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
9
|
+
return defaultFuncs
|
|
10
|
+
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
|
11
|
+
.then(parseAndCheckLogin(ctx, defaultFuncs))
|
|
12
|
+
.then(resData => {
|
|
13
|
+
if (getType(resData) !== "Array") throw { error: "Not logged in" };
|
|
14
|
+
if (!Array.isArray(resData) || !resData.length) return;
|
|
15
|
+
const lastRes = resData[resData.length - 1];
|
|
16
|
+
if (lastRes && lastRes.successful_results === 0) return;
|
|
17
|
+
|
|
18
|
+
const syncSeqId = resData[0]?.o0?.data?.viewer?.message_threads?.sync_sequence_id;
|
|
19
|
+
if (syncSeqId) {
|
|
20
|
+
ctx.lastSeqId = syncSeqId;
|
|
21
|
+
logger("mqtt getSeqID ok -> listenMqtt()", "info");
|
|
22
|
+
listenMqtt(defaultFuncs, api, ctx, globalCallback);
|
|
23
|
+
} else {
|
|
24
|
+
throw { error: "getSeqId: no sync_sequence_id found." };
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
.catch(err => {
|
|
28
|
+
const msg = (err && err.error) || (err && err.message) || String(err || "");
|
|
29
|
+
if (/Not logged in/i.test(msg)) {
|
|
30
|
+
return emitAuth(ctx, api, globalCallback, "not_logged_in", msg);
|
|
31
|
+
}
|
|
32
|
+
if (/blocked the login/i.test(msg)) {
|
|
33
|
+
return emitAuth(ctx, api, globalCallback, "login_blocked", msg);
|
|
34
|
+
}
|
|
35
|
+
logger(`getSeqID error: ${msg}`, "error");
|
|
36
|
+
return emitAuth(ctx, api, globalCallback, "auth_error", msg);
|
|
37
|
+
});
|
|
24
38
|
};
|
|
25
39
|
};
|
|
@@ -12,9 +12,11 @@ const createListenMqtt = require("./core/connectMqtt");
|
|
|
12
12
|
const createGetSeqID = require("./core/getSeqID");
|
|
13
13
|
const markDelivery = require("./core/markDelivery");
|
|
14
14
|
const getTaskResponseData = require("./core/getTaskResponseData");
|
|
15
|
+
const createEmitAuth = require("./core/emitAuth");
|
|
15
16
|
const parseDelta = createParseDelta({ markDelivery, parseAndCheckLogin });
|
|
16
17
|
const listenMqtt = createListenMqtt({ WebSocket, mqtt, HttpsProxyAgent, buildStream, buildProxy, topics, parseDelta, getTaskResponseData, logger });
|
|
17
18
|
const getSeqIDFactory = createGetSeqID({ parseAndCheckLogin, listenMqtt, logger });
|
|
19
|
+
const emitAuth = createEmitAuth({ logger });
|
|
18
20
|
|
|
19
21
|
const MQTT_DEFAULTS = { cycleMs: 60 * 60 * 1000, reconnectDelayMs: 2000, autoReconnect: true, reconnectAfterStop: false };
|
|
20
22
|
function mqttConf(ctx, overrides) {
|
|
@@ -26,6 +28,33 @@ function mqttConf(ctx, overrides) {
|
|
|
26
28
|
module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
27
29
|
const identity = function () { };
|
|
28
30
|
let globalCallback = identity;
|
|
31
|
+
|
|
32
|
+
function installPostGuard() {
|
|
33
|
+
if (ctx._postGuarded) return defaultFuncs.post;
|
|
34
|
+
const rawPost = defaultFuncs.post && defaultFuncs.post.bind(defaultFuncs);
|
|
35
|
+
if (!rawPost) return defaultFuncs.post;
|
|
36
|
+
|
|
37
|
+
function postSafe(...args) {
|
|
38
|
+
return rawPost(...args).catch(err => {
|
|
39
|
+
const msg = (err && err.error) || (err && err.message) || String(err || "");
|
|
40
|
+
if (/Not logged in|blocked the login/i.test(msg)) {
|
|
41
|
+
emitAuth(
|
|
42
|
+
ctx,
|
|
43
|
+
api,
|
|
44
|
+
globalCallback,
|
|
45
|
+
/blocked/i.test(msg) ? "login_blocked" : "not_logged_in",
|
|
46
|
+
msg
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
throw err;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
defaultFuncs.post = postSafe;
|
|
53
|
+
ctx._postGuarded = true;
|
|
54
|
+
logger("postSafe guard installed for defaultFuncs.post", "info");
|
|
55
|
+
return postSafe;
|
|
56
|
+
}
|
|
57
|
+
|
|
29
58
|
let conf = mqttConf(ctx, opts);
|
|
30
59
|
|
|
31
60
|
function getSeqIDWrapper() {
|
|
@@ -34,7 +63,10 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
34
63
|
queries: JSON.stringify({
|
|
35
64
|
o0: {
|
|
36
65
|
doc_id: "3336396659757871",
|
|
37
|
-
query_params: {
|
|
66
|
+
query_params: {
|
|
67
|
+
limit: 1, before: null, tags: ["INBOX"],
|
|
68
|
+
includeDeliveryReceipts: false, includeSeqID: true
|
|
69
|
+
}
|
|
38
70
|
}
|
|
39
71
|
})
|
|
40
72
|
};
|
|
@@ -85,7 +117,7 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
85
117
|
}
|
|
86
118
|
|
|
87
119
|
function forceCycle() {
|
|
88
|
-
if (ctx._cycling) return;
|
|
120
|
+
if (ctx._cycling) return;
|
|
89
121
|
ctx._cycling = true;
|
|
90
122
|
ctx._ending = true;
|
|
91
123
|
logger("mqtt force cycle begin", "warn");
|
|
@@ -119,12 +151,16 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
119
151
|
}
|
|
120
152
|
|
|
121
153
|
const msgEmitter = new MessageEmitter();
|
|
154
|
+
|
|
122
155
|
globalCallback = callback || function (error, message) {
|
|
123
156
|
if (error) { logger("mqtt emit error", "error"); return msgEmitter.emit("error", error); }
|
|
124
157
|
msgEmitter.emit("message", message);
|
|
125
158
|
};
|
|
126
159
|
|
|
127
160
|
conf = mqttConf(ctx, conf);
|
|
161
|
+
|
|
162
|
+
installPostGuard();
|
|
163
|
+
|
|
128
164
|
if (!ctx.firstListen) ctx.lastSeqId = null;
|
|
129
165
|
ctx.syncToken = undefined;
|
|
130
166
|
ctx.t_mqttCalled = false;
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
1
|
const fs = require("fs");
|
|
4
2
|
const path = require("path");
|
|
5
3
|
const logger = require("../../../func/logger");
|
|
@@ -122,6 +120,8 @@ let isProcessingQueue = false;
|
|
|
122
120
|
const processingThreads = new Set();
|
|
123
121
|
const queuedThreads = new Set();
|
|
124
122
|
const cooldown = new Map();
|
|
123
|
+
const inflight = new Map();
|
|
124
|
+
let loopStarted = false;
|
|
125
125
|
|
|
126
126
|
module.exports = function (defaultFuncs, api, ctx) {
|
|
127
127
|
const getMultiInfo = async function (threadIDs) {
|
|
@@ -204,6 +204,17 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
async function createOrUpdateThread(id, data) {
|
|
208
|
+
const existing = await get(id);
|
|
209
|
+
if (existing) {
|
|
210
|
+
await update(id, { data });
|
|
211
|
+
return "update";
|
|
212
|
+
} else {
|
|
213
|
+
await create(id, { data });
|
|
214
|
+
return "create";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
207
218
|
async function fetchThreadInfo(tID, isNew) {
|
|
208
219
|
try {
|
|
209
220
|
const response = await getMultiInfo([tID]);
|
|
@@ -214,13 +225,8 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
214
225
|
}
|
|
215
226
|
const threadInfo = response.Data[0];
|
|
216
227
|
await upsertUsersFromThreadInfo(threadInfo);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
logger(`Success create data thread: ${tID}`, "info");
|
|
220
|
-
} else {
|
|
221
|
-
await update(tID, { data: threadInfo });
|
|
222
|
-
logger(`Success update data thread: ${tID}`, "info");
|
|
223
|
-
}
|
|
228
|
+
const op = await createOrUpdateThread(tID, threadInfo);
|
|
229
|
+
logger(`${op === "create" ? "Success create data thread" : "Success update data thread"}: ${tID}`, "info");
|
|
224
230
|
} catch (err) {
|
|
225
231
|
cooldown.set(tID, Date.now() + 5 * 60 * 1000);
|
|
226
232
|
logger(`fetchThreadInfo error ${tID}: ${err?.message || err}`, "error");
|
|
@@ -242,7 +248,6 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
242
248
|
const lastUpdated = new Date(result.updatedAt).getTime();
|
|
243
249
|
if ((now - lastUpdated) / (1000 * 60) > 10 && !queuedThreads.has(t)) {
|
|
244
250
|
queuedThreads.add(t);
|
|
245
|
-
logger(`ThreadID ${t} queued for refresh`, "info");
|
|
246
251
|
queue.push(() => fetchThreadInfo(t, false));
|
|
247
252
|
}
|
|
248
253
|
}
|
|
@@ -265,10 +270,13 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
265
270
|
isProcessingQueue = false;
|
|
266
271
|
}
|
|
267
272
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
273
|
+
if (!loopStarted) {
|
|
274
|
+
loopStarted = true;
|
|
275
|
+
setInterval(() => {
|
|
276
|
+
checkAndUpdateThreads();
|
|
277
|
+
processQueue();
|
|
278
|
+
}, 10000);
|
|
279
|
+
}
|
|
272
280
|
|
|
273
281
|
return async function getThreadInfoGraphQL(threadID, callback) {
|
|
274
282
|
let resolveFunc = function () { };
|
|
@@ -284,25 +292,89 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
284
292
|
};
|
|
285
293
|
}
|
|
286
294
|
if (getType(threadID) !== "Array") threadID = [threadID];
|
|
295
|
+
const tid = String(threadID[0]);
|
|
287
296
|
try {
|
|
288
|
-
const
|
|
297
|
+
const cd = cooldown.get(tid);
|
|
298
|
+
if (cd && Date.now() < cd) {
|
|
299
|
+
const cachedCd = await get(tid);
|
|
300
|
+
if (cachedCd?.data && isValidThread(cachedCd.data)) {
|
|
301
|
+
await upsertUsersFromThreadInfo(cachedCd.data);
|
|
302
|
+
callback(null, cachedCd.data);
|
|
303
|
+
return returnPromise;
|
|
304
|
+
}
|
|
305
|
+
const stub = {
|
|
306
|
+
threadID: tid,
|
|
307
|
+
threadName: null,
|
|
308
|
+
participantIDs: [],
|
|
309
|
+
userInfo: [],
|
|
310
|
+
unreadCount: 0,
|
|
311
|
+
messageCount: 0,
|
|
312
|
+
timestamp: null,
|
|
313
|
+
muteUntil: null,
|
|
314
|
+
isGroup: false,
|
|
315
|
+
isSubscribed: false,
|
|
316
|
+
isArchived: false,
|
|
317
|
+
folder: null,
|
|
318
|
+
cannotReplyReason: null,
|
|
319
|
+
eventReminders: [],
|
|
320
|
+
emoji: null,
|
|
321
|
+
color: null,
|
|
322
|
+
threadTheme: null,
|
|
323
|
+
nicknames: {},
|
|
324
|
+
adminIDs: [],
|
|
325
|
+
approvalMode: false,
|
|
326
|
+
approvalQueue: [],
|
|
327
|
+
reactionsMuteMode: "",
|
|
328
|
+
mentionsMuteMode: "",
|
|
329
|
+
isPinProtected: false,
|
|
330
|
+
relatedPageThread: null,
|
|
331
|
+
name: null,
|
|
332
|
+
snippet: null,
|
|
333
|
+
snippetSender: null,
|
|
334
|
+
snippetAttachments: [],
|
|
335
|
+
serverTimestamp: null,
|
|
336
|
+
imageSrc: null,
|
|
337
|
+
isCanonicalUser: false,
|
|
338
|
+
isCanonical: true,
|
|
339
|
+
recipientsLoadable: false,
|
|
340
|
+
hasEmailParticipant: false,
|
|
341
|
+
readOnly: false,
|
|
342
|
+
canReply: false,
|
|
343
|
+
lastMessageTimestamp: null,
|
|
344
|
+
lastMessageType: "message",
|
|
345
|
+
lastReadTimestamp: null,
|
|
346
|
+
threadType: 1,
|
|
347
|
+
inviteLink: { enable: false, link: null },
|
|
348
|
+
__status: "cooldown",
|
|
349
|
+
};
|
|
350
|
+
await createOrUpdateThread(tid, stub);
|
|
351
|
+
callback(null, stub);
|
|
352
|
+
return returnPromise;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const cached = await get(tid);
|
|
289
356
|
if (cached?.data && isValidThread(cached.data)) {
|
|
290
357
|
await upsertUsersFromThreadInfo(cached.data);
|
|
291
358
|
callback(null, cached.data);
|
|
292
359
|
return returnPromise;
|
|
293
360
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
361
|
+
|
|
362
|
+
if (inflight.has(tid)) {
|
|
363
|
+
inflight.get(tid).then(data => callback(null, data)).catch(err => callback(err));
|
|
364
|
+
return returnPromise;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const p = (async () => {
|
|
368
|
+
processingThreads.add(tid);
|
|
369
|
+
const response = await getMultiInfo([tid]);
|
|
298
370
|
if (response.Success && response.Data && isValidThread(response.Data[0])) {
|
|
299
371
|
const data = response.Data[0];
|
|
300
372
|
await upsertUsersFromThreadInfo(data);
|
|
301
|
-
await
|
|
302
|
-
|
|
373
|
+
await createOrUpdateThread(tid, data);
|
|
374
|
+
return data;
|
|
303
375
|
} else {
|
|
304
376
|
const stub = {
|
|
305
|
-
threadID:
|
|
377
|
+
threadID: tid,
|
|
306
378
|
threadName: null,
|
|
307
379
|
participantIDs: [],
|
|
308
380
|
userInfo: [],
|
|
@@ -346,11 +418,18 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
346
418
|
inviteLink: { enable: false, link: null },
|
|
347
419
|
__status: "unavailable",
|
|
348
420
|
};
|
|
349
|
-
cooldown.set(
|
|
350
|
-
|
|
421
|
+
cooldown.set(tid, Date.now() + 5 * 60 * 1000);
|
|
422
|
+
await createOrUpdateThread(tid, stub);
|
|
423
|
+
return stub;
|
|
351
424
|
}
|
|
352
|
-
|
|
353
|
-
|
|
425
|
+
})()
|
|
426
|
+
.finally(() => {
|
|
427
|
+
processingThreads.delete(tid);
|
|
428
|
+
inflight.delete(tid);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
inflight.set(tid, p);
|
|
432
|
+
p.then(data => callback(null, data)).catch(err => callback(err));
|
|
354
433
|
} catch (err) {
|
|
355
434
|
callback(err);
|
|
356
435
|
}
|