@lazyneoaz/metachat 1.0.5 → 1.0.7
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/package.json +1 -1
- package/src/apis/getThreadList.js +7 -2
- package/src/apis/listenMqtt.js +37 -15
- package/src/apis/logout.js +60 -23
- package/src/apis/refreshFb_dtsg.js +7 -1
- package/src/apis/sendMessage.js +23 -11
- package/src/engine/models/buildAPI.js +8 -0
- package/src/engine/models/loginHelper.js +12 -7
- package/src/utils/antiSuspension.js +21 -4
- package/src/utils/autoReLogin.js +28 -10
- package/src/utils/clients.js +10 -5
- package/src/utils/formatters/data/formatAttachment.js +49 -35
- package/src/utils/formatters/data/formatDelta.js +28 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyneoaz/metachat",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"types": "src/types/index.d.ts",
|
|
6
6
|
"description": "Advanced Facebook Chat API client for building Messenger bots — real-time messaging, thread management, MQTT, and session stability.",
|
|
@@ -53,13 +53,18 @@ function formatThreadGraphQLResponse(messageThread) {
|
|
|
53
53
|
const lastR = messageThread.last_read_receipt;
|
|
54
54
|
const lastReadTimestamp = lastR?.nodes?.[0]?.timestamp_precise || null;
|
|
55
55
|
|
|
56
|
+
// Guard: all_participants or edges can be absent on restricted/deleted threads
|
|
57
|
+
const edges = (messageThread.all_participants && Array.isArray(messageThread.all_participants.edges))
|
|
58
|
+
? messageThread.all_participants.edges
|
|
59
|
+
: [];
|
|
60
|
+
|
|
56
61
|
return {
|
|
57
62
|
threadID: threadID,
|
|
58
63
|
threadName: messageThread.name,
|
|
59
|
-
participantIDs:
|
|
64
|
+
participantIDs: edges.map(
|
|
60
65
|
(d) => d.node.messaging_actor.id,
|
|
61
66
|
),
|
|
62
|
-
userInfo:
|
|
67
|
+
userInfo: edges.map((d) => {
|
|
63
68
|
const p = d.node.messaging_actor;
|
|
64
69
|
return {
|
|
65
70
|
id: p.id,
|
package/src/apis/listenMqtt.js
CHANGED
|
@@ -688,23 +688,15 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
688
688
|
timestamp: Date.now()
|
|
689
689
|
}, null);
|
|
690
690
|
}
|
|
691
|
+
// handleSessionExpiry owns the listenMqtt restart after re-login —
|
|
692
|
+
// it captures wasListening from the OLD ctx before calling loginHelper,
|
|
693
|
+
// so it is the only place that reliably knows whether listening was active.
|
|
694
|
+
// DO NOT restart listenMqtt here; doing so races with handleSessionExpiry
|
|
695
|
+
// and creates two simultaneous MQTT connections.
|
|
691
696
|
try {
|
|
692
697
|
if (globalAutoReLoginManager && globalAutoReLoginManager.isEnabled && globalAutoReLoginManager.isEnabled()) {
|
|
693
|
-
globalAutoReLoginManager.handleSessionExpiry(api, 'https://www.facebook.com', "Session expired")
|
|
694
|
-
|
|
695
|
-
try {
|
|
696
|
-
if (typeof api.stopListening === 'function') {
|
|
697
|
-
try { api.stopListening(); } catch (_) {}
|
|
698
|
-
}
|
|
699
|
-
const cb = ctx._lastListenCallback || null;
|
|
700
|
-
if (cb) {
|
|
701
|
-
api.listenMqtt(cb);
|
|
702
|
-
} else {
|
|
703
|
-
api.listenMqtt();
|
|
704
|
-
}
|
|
705
|
-
} catch (_) {}
|
|
706
|
-
}
|
|
707
|
-
}).catch(() => {});
|
|
698
|
+
globalAutoReLoginManager.handleSessionExpiry(api, 'https://www.facebook.com', "Session expired")
|
|
699
|
+
.catch(() => {});
|
|
708
700
|
}
|
|
709
701
|
} catch (_) {}
|
|
710
702
|
}
|
|
@@ -1007,7 +999,37 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
1007
999
|
? ctx._middleware.wrapCallback(baseCallback)
|
|
1008
1000
|
: baseCallback;
|
|
1009
1001
|
|
|
1002
|
+
// Replace ctx._emitter with the new MessageEmitter, but first migrate
|
|
1003
|
+
// any existing listeners so they are not silently orphaned.
|
|
1004
|
+
// Listeners added via api.on() before listenMqtt() was called live on the
|
|
1005
|
+
// old emitter; without migration they would never fire again.
|
|
1006
|
+
const prevEmitter = ctx._emitter;
|
|
1010
1007
|
ctx._emitter = msgEmitter;
|
|
1008
|
+
if (prevEmitter && prevEmitter !== msgEmitter && typeof prevEmitter.eventNames === 'function') {
|
|
1009
|
+
try {
|
|
1010
|
+
const LIFECYCLE_EVENTS = [
|
|
1011
|
+
'sessionExpired', 'checkpoint', 'relogin', 'ready',
|
|
1012
|
+
'account_inactive', 'checkpoint_282', 'checkpoint_956', 'error'
|
|
1013
|
+
];
|
|
1014
|
+
for (const event of LIFECYCLE_EVENTS) {
|
|
1015
|
+
// Use listeners() NOT rawListeners() — rawListeners() returns the
|
|
1016
|
+
// internal once-wrapper functions. When re-registered with .on()
|
|
1017
|
+
// those wrappers fire the handler once then remove themselves from
|
|
1018
|
+
// the OLD emitter, but stay on the NEW emitter forever, causing a
|
|
1019
|
+
// memory leak and never-again-firing once handlers.
|
|
1020
|
+
const fns = prevEmitter.listeners ? prevEmitter.listeners(event) : [];
|
|
1021
|
+
for (const fn of fns) {
|
|
1022
|
+
msgEmitter.on(event, fn);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
} catch (_) {}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Reset reconnect-blocking flags — calling listenMqtt() always means
|
|
1029
|
+
// "start fresh". Without this, ctx._ending left behind by stopListening()
|
|
1030
|
+
// or emitAuthError() would permanently block scheduleReconnect().
|
|
1031
|
+
ctx._ending = false;
|
|
1032
|
+
ctx._cycling = false;
|
|
1011
1033
|
|
|
1012
1034
|
ctx._listeningActive = true;
|
|
1013
1035
|
ctx._lastListenCallback = callback || null;
|
package/src/apis/logout.js
CHANGED
|
@@ -11,50 +11,87 @@ const utils = require('../utils');
|
|
|
11
11
|
module.exports = function (defaultFuncs, api, ctx) {
|
|
12
12
|
/**
|
|
13
13
|
* Logs the current user out of Facebook.
|
|
14
|
-
*
|
|
14
|
+
*
|
|
15
|
+
* Strategy:
|
|
16
|
+
* 1. Try to fetch the settings-menu endpoint and parse the logout form from
|
|
17
|
+
* the jsmods response (legacy path, may break if Facebook restructures).
|
|
18
|
+
* 2. If that fails for any reason, fall back to posting directly to
|
|
19
|
+
* /logout.php using the session token already in ctx.fb_dtsg. This is
|
|
20
|
+
* reliable as long as the session is still valid.
|
|
21
|
+
*
|
|
22
|
+
* @returns {Promise<void>}
|
|
15
23
|
*/
|
|
16
24
|
return async function logout() {
|
|
17
|
-
|
|
18
|
-
pmid: "0",
|
|
19
|
-
};
|
|
20
|
-
|
|
25
|
+
// ── Path 1: Parse logout form from settings menu ──────────────────────
|
|
21
26
|
try {
|
|
22
27
|
const resData = await defaultFuncs
|
|
23
28
|
.post(
|
|
24
29
|
"https://www.facebook.com/bluebar/modern_settings_menu/?help_type=364455653583099&show_contextual_help=1",
|
|
25
30
|
ctx.jar,
|
|
26
|
-
|
|
31
|
+
{ pmid: "0" },
|
|
27
32
|
)
|
|
28
33
|
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
// Guard every access — Facebook restructures this response frequently.
|
|
36
|
+
const instances = resData?.jsmods?.instances;
|
|
37
|
+
const firstGroup = Array.isArray(instances) && instances[0] && instances[0][2] && instances[0][2][0];
|
|
38
|
+
const elem = firstGroup && Array.isArray(firstGroup) ? firstGroup.find(v => v.value === "logout") : null;
|
|
39
|
+
|
|
40
|
+
if (elem) {
|
|
41
|
+
const markup = resData?.jsmods?.markup;
|
|
42
|
+
const markupEntry = Array.isArray(markup) ? markup.find(v => v[0] === elem.markup?.__m) : null;
|
|
43
|
+
const html = markupEntry?.[1]?.__html;
|
|
44
|
+
|
|
45
|
+
if (html) {
|
|
46
|
+
const logoutForm = {
|
|
47
|
+
fb_dtsg: utils.getFrom(html, '"fb_dtsg" value="', '"') || ctx.fb_dtsg,
|
|
48
|
+
ref: utils.getFrom(html, '"ref" value="', '"'),
|
|
49
|
+
h: utils.getFrom(html, '"h" value="', '"'),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const logoutRes = await defaultFuncs
|
|
53
|
+
.post("https://www.facebook.com/logout.php", ctx.jar, logoutForm)
|
|
54
|
+
.then(utils.saveCookies(ctx.jar));
|
|
55
|
+
|
|
56
|
+
if (logoutRes.headers && logoutRes.headers.location) {
|
|
57
|
+
await defaultFuncs
|
|
58
|
+
.get(logoutRes.headers.location, ctx.jar)
|
|
59
|
+
.then(utils.saveCookies(ctx.jar));
|
|
60
|
+
ctx.loggedIn = false;
|
|
61
|
+
utils.log("logout", "Logged out successfully (path 1).");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// If no redirect location, fall through to path 2
|
|
65
|
+
}
|
|
33
66
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
67
|
+
} catch (_) {
|
|
68
|
+
// Settings-menu path failed — continue to fallback
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Path 2: Direct logout using ctx.fb_dtsg ───────────────────────────
|
|
72
|
+
// This path works as long as the session token in ctx is valid.
|
|
73
|
+
// It does NOT require parsing the settings-menu HTML.
|
|
74
|
+
try {
|
|
37
75
|
const logoutForm = {
|
|
38
|
-
fb_dtsg:
|
|
39
|
-
ref:
|
|
40
|
-
h:
|
|
76
|
+
fb_dtsg: ctx.fb_dtsg || "",
|
|
77
|
+
ref: "mb",
|
|
78
|
+
h: "",
|
|
41
79
|
};
|
|
42
80
|
|
|
43
81
|
const logoutRes = await defaultFuncs
|
|
44
82
|
.post("https://www.facebook.com/logout.php", ctx.jar, logoutForm)
|
|
45
83
|
.then(utils.saveCookies(ctx.jar));
|
|
46
84
|
|
|
47
|
-
if (
|
|
48
|
-
|
|
85
|
+
if (logoutRes.headers && logoutRes.headers.location) {
|
|
86
|
+
try {
|
|
87
|
+
await defaultFuncs
|
|
88
|
+
.get(logoutRes.headers.location, ctx.jar)
|
|
89
|
+
.then(utils.saveCookies(ctx.jar));
|
|
90
|
+
} catch (_) {}
|
|
49
91
|
}
|
|
50
92
|
|
|
51
|
-
await defaultFuncs
|
|
52
|
-
.get(logoutRes.headers.location, ctx.jar)
|
|
53
|
-
.then(utils.saveCookies(ctx.jar));
|
|
54
|
-
|
|
55
93
|
ctx.loggedIn = false;
|
|
56
|
-
utils.log("logout", "Logged out successfully.");
|
|
57
|
-
|
|
94
|
+
utils.log("logout", "Logged out successfully (path 2).");
|
|
58
95
|
} catch (err) {
|
|
59
96
|
utils.error("logout", err);
|
|
60
97
|
throw err;
|
|
@@ -60,7 +60,13 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
60
60
|
|
|
61
61
|
if (!fb_dtsg) throw new Error("Could not find fb_dtsg in HTML. Session may have expired.");
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
// Recalculate ttstamp whenever fb_dtsg changes — ttstamp = "2" followed
|
|
64
|
+
// by the concatenation of each character's char code (NOT their sum).
|
|
65
|
+
// Leaving ttstamp stale after a token refresh causes request signature
|
|
66
|
+
// mismatches that Facebook detects as bot-like behaviour.
|
|
67
|
+
const ttstamp = "2" + Array.from(fb_dtsg).map(c => c.charCodeAt(0)).join("");
|
|
68
|
+
|
|
69
|
+
const updated = { fb_dtsg, ttstamp };
|
|
64
70
|
if (jazoest) updated.jazoest = jazoest;
|
|
65
71
|
|
|
66
72
|
Object.assign(ctx, updated);
|
package/src/apis/sendMessage.js
CHANGED
|
@@ -300,32 +300,44 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
try {
|
|
303
|
-
let result;
|
|
304
303
|
const mqttReady = ctx.mqttClient && ctx.mqttClient.connected;
|
|
305
304
|
const isMultiRecipient = Array.isArray(threadID);
|
|
305
|
+
// Track which transport was used so the catch block can fall back to
|
|
306
|
+
// the OTHER transport — retrying the same one that just failed is useless.
|
|
307
|
+
const usedMqtt = mqttReady && !isMultiRecipient && api.sendMessageMqtt;
|
|
306
308
|
|
|
307
|
-
|
|
309
|
+
let result;
|
|
310
|
+
if (usedMqtt) {
|
|
308
311
|
result = await api.sendMessageMqtt(msg, threadID, replyToMessage);
|
|
309
312
|
} else {
|
|
310
313
|
result = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
|
|
311
314
|
}
|
|
312
315
|
callback(null, result);
|
|
313
316
|
} catch (sendErr) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
callback(sendErr);
|
|
321
|
-
}
|
|
322
|
-
} else {
|
|
317
|
+
// Fall back to the OTHER transport, not the one that just failed.
|
|
318
|
+
const mqttNowReady = ctx.mqttClient && ctx.mqttClient.connected;
|
|
319
|
+
const primaryWasMqtt = mqttNowReady && !Array.isArray(threadID) && api.sendMessageMqtt;
|
|
320
|
+
|
|
321
|
+
if (primaryWasMqtt) {
|
|
322
|
+
// MQTT was used (or is available) — fall back to HTTP
|
|
323
323
|
try {
|
|
324
324
|
const httpRes = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
|
|
325
325
|
callback(null, httpRes);
|
|
326
326
|
} catch (_httpErr) {
|
|
327
327
|
callback(sendErr);
|
|
328
328
|
}
|
|
329
|
+
} else {
|
|
330
|
+
// HTTP was used — try MQTT if it has since become available
|
|
331
|
+
if (mqttNowReady && !Array.isArray(threadID) && api.sendMessageMqtt) {
|
|
332
|
+
try {
|
|
333
|
+
const mqttRes = await api.sendMessageMqtt(msg, threadID, replyToMessage);
|
|
334
|
+
callback(null, mqttRes);
|
|
335
|
+
} catch (_mqttErr) {
|
|
336
|
+
callback(sendErr);
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
callback(sendErr);
|
|
340
|
+
}
|
|
329
341
|
}
|
|
330
342
|
} finally {
|
|
331
343
|
if (typingTimeout) clearTimeout(typingTimeout);
|
|
@@ -84,6 +84,13 @@ async function buildAPI(html, jar, netData, globalOptions, fbLinkFunc, errorRetr
|
|
|
84
84
|
const emitter = new EventEmitter();
|
|
85
85
|
emitter.setMaxListeners(50);
|
|
86
86
|
|
|
87
|
+
// Pre-compute ttstamp so it is present from the very first request.
|
|
88
|
+
// tokenRefresh.js sets this field on every refresh — initialising it here
|
|
89
|
+
// keeps ctx consistent and prevents undefined reads before the first refresh.
|
|
90
|
+
const ttstamp = dtsgResult.fb_dtsg
|
|
91
|
+
? "2" + Array.from(dtsgResult.fb_dtsg).map(c => c.charCodeAt(0)).join("")
|
|
92
|
+
: "2";
|
|
93
|
+
|
|
87
94
|
const ctx = {
|
|
88
95
|
userID,
|
|
89
96
|
jar,
|
|
@@ -108,6 +115,7 @@ async function buildAPI(html, jar, netData, globalOptions, fbLinkFunc, errorRetr
|
|
|
108
115
|
cache: new SimpleCache(),
|
|
109
116
|
validator: globalValidator,
|
|
110
117
|
_emitter: emitter,
|
|
118
|
+
ttstamp,
|
|
111
119
|
...dtsgResult,
|
|
112
120
|
};
|
|
113
121
|
const defaultFuncs = utils.makeDefaults(html, userID, ctx);
|
|
@@ -317,13 +317,18 @@ async function loginHelper(credentials, globalOptions, callback, setOptionsFunc,
|
|
|
317
317
|
|
|
318
318
|
// Expose EventEmitter interface on the API so consumers can subscribe to
|
|
319
319
|
// key lifecycle events: 'sessionExpired', 'checkpoint', 'relogin', 'ready'
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
320
|
+
//
|
|
321
|
+
// IMPORTANT: Use ctx._emitter dynamically (not a captured snapshot).
|
|
322
|
+
// listenMqtt() replaces ctx._emitter with a fresh MessageEmitter on every
|
|
323
|
+
// call. Capturing the initial emitter here would orphan any listeners added
|
|
324
|
+
// via api.on() after listenMqtt() runs — they would never receive events
|
|
325
|
+
// because the emitter they registered on is no longer the active one.
|
|
326
|
+
if (ctx._emitter) {
|
|
327
|
+
api.on = (event, listener) => ctx._emitter.on(event, listener);
|
|
328
|
+
api.once = (event, listener) => ctx._emitter.once(event, listener);
|
|
329
|
+
api.off = (event, listener) => ctx._emitter.removeListener(event, listener);
|
|
330
|
+
api.emit = (event, ...args) => ctx._emitter.emit(event, ...args);
|
|
331
|
+
api.removeAllListeners = (event) => ctx._emitter.removeAllListeners(event);
|
|
327
332
|
}
|
|
328
333
|
|
|
329
334
|
const { TokenRefreshManager } = require('../../utils/tokenRefresh');
|
|
@@ -205,6 +205,14 @@ class AntiSuspension {
|
|
|
205
205
|
detectSuspensionSignal(text) {
|
|
206
206
|
if (!text || typeof text !== 'string') return false;
|
|
207
207
|
const lower = text.toLowerCase();
|
|
208
|
+
|
|
209
|
+
// Session expiry is NOT a suspension event — never trip the circuit
|
|
210
|
+
// breaker for it. A false positive here blocks all sends AND re-login
|
|
211
|
+
// attempts for up to 45 minutes, turning a normal logout into a
|
|
212
|
+
// permanent lockout until the cooldown expires.
|
|
213
|
+
const isSessionExpiry = SESSION_EXPIRY_SIGNALS.some(signal => lower.includes(signal));
|
|
214
|
+
if (isSessionExpiry) return false;
|
|
215
|
+
|
|
208
216
|
const found = SUSPENSION_SIGNALS.some(signal => lower.includes(signal));
|
|
209
217
|
if (found) {
|
|
210
218
|
this._onSuspensionSignalDetected();
|
|
@@ -240,11 +248,14 @@ class AntiSuspension {
|
|
|
240
248
|
isCircuitBreakerTripped() {
|
|
241
249
|
const cb = this.suspensionCircuitBreaker;
|
|
242
250
|
if (!cb.tripped) return false;
|
|
251
|
+
// Use per-trip override duration if set, otherwise fall back to default.
|
|
252
|
+
const activeCooldown = cb.overrideCooldownMs || cb.cooldownMs;
|
|
243
253
|
const elapsed = Date.now() - cb.trippedAt;
|
|
244
|
-
if (elapsed >=
|
|
254
|
+
if (elapsed >= activeCooldown) {
|
|
245
255
|
cb.tripped = false;
|
|
246
256
|
cb.signalCount = 0;
|
|
247
257
|
cb.trippedAt = null;
|
|
258
|
+
cb.overrideCooldownMs = null;
|
|
248
259
|
return false;
|
|
249
260
|
}
|
|
250
261
|
return true;
|
|
@@ -253,25 +264,31 @@ class AntiSuspension {
|
|
|
253
264
|
getCircuitBreakerRemainingMs() {
|
|
254
265
|
const cb = this.suspensionCircuitBreaker;
|
|
255
266
|
if (!cb.tripped) return 0;
|
|
256
|
-
|
|
267
|
+
const activeCooldown = cb.overrideCooldownMs || cb.cooldownMs;
|
|
268
|
+
return Math.max(0, activeCooldown - (Date.now() - cb.trippedAt));
|
|
257
269
|
}
|
|
258
270
|
|
|
259
271
|
tripCircuitBreaker(reason, durationMs) {
|
|
260
272
|
const cb = this.suspensionCircuitBreaker;
|
|
261
273
|
cb.tripped = true;
|
|
262
274
|
cb.trippedAt = Date.now();
|
|
263
|
-
|
|
275
|
+
// Store custom duration as a per-trip override so the DEFAULT cooldownMs
|
|
276
|
+
// is never permanently mutated — future organic-signal trips will still
|
|
277
|
+
// use the original 45-minute default.
|
|
278
|
+
cb.overrideCooldownMs = durationMs || null;
|
|
264
279
|
cb.signalCount = cb.maxSignalsBeforeTrip;
|
|
280
|
+
const activeCooldown = durationMs || cb.cooldownMs;
|
|
265
281
|
const { utils } = this._getUtils();
|
|
266
282
|
utils && utils.warn && utils.warn("AntiSuspension",
|
|
267
283
|
`Circuit breaker manually tripped: ${reason || 'manual'}. ` +
|
|
268
|
-
`Cooldown: ${(
|
|
284
|
+
`Cooldown: ${(activeCooldown / 60000).toFixed(1)} min`);
|
|
269
285
|
}
|
|
270
286
|
|
|
271
287
|
resetCircuitBreaker() {
|
|
272
288
|
this.suspensionCircuitBreaker.tripped = false;
|
|
273
289
|
this.suspensionCircuitBreaker.signalCount = 0;
|
|
274
290
|
this.suspensionCircuitBreaker.trippedAt = null;
|
|
291
|
+
this.suspensionCircuitBreaker.overrideCooldownMs = null;
|
|
275
292
|
}
|
|
276
293
|
|
|
277
294
|
async simulateTyping(threadID, messageLength = 50) {
|
package/src/utils/autoReLogin.js
CHANGED
|
@@ -110,6 +110,13 @@ class AutoReLoginManager {
|
|
|
110
110
|
|
|
111
111
|
const fbLinkFunc = typeof fbLink === 'function' ? fbLink : () => fbLink;
|
|
112
112
|
|
|
113
|
+
// Capture the listening state from the CURRENT ctx BEFORE loginHelper
|
|
114
|
+
// replaces api.ctx with a fresh context object. By the time the login
|
|
115
|
+
// callback fires, api.ctx already points to the new ctx, so checking
|
|
116
|
+
// api.ctx._listeningActive in the callback would always be undefined.
|
|
117
|
+
const wasListening = !!(api && api.ctx && api.ctx._listeningActive);
|
|
118
|
+
const savedListenCallback = (api && api.ctx && api.ctx._lastListenCallback) || null;
|
|
119
|
+
|
|
113
120
|
await new Promise((resolve, reject) => {
|
|
114
121
|
loginHelperModel(
|
|
115
122
|
this.credentials,
|
|
@@ -121,9 +128,17 @@ class AutoReLoginManager {
|
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
if (api) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
// loginHelper calls loadApiModules() which recreates all API
|
|
132
|
+
// method closures on the new ctx, so subsequent calls automatically
|
|
133
|
+
// use fresh tokens. Also reset flags on the new ctx so MQTT can
|
|
134
|
+
// reconnect cleanly.
|
|
135
|
+
if (api.ctx) {
|
|
136
|
+
api.ctx.loggedIn = true;
|
|
137
|
+
api.ctx._ending = false;
|
|
138
|
+
api.ctx._cycling = false;
|
|
139
|
+
api.ctx._mqttReauthing = false;
|
|
140
|
+
}
|
|
141
|
+
|
|
127
142
|
if (api.tokenRefreshManager) {
|
|
128
143
|
api.tokenRefreshManager.resetFailureCount();
|
|
129
144
|
}
|
|
@@ -149,15 +164,17 @@ class AutoReLoginManager {
|
|
|
149
164
|
this.onReLoginSuccess();
|
|
150
165
|
}
|
|
151
166
|
|
|
167
|
+
// Restart MQTT listening if it was active before re-login.
|
|
168
|
+
// wasListening was captured from the OLD ctx before loginHelper replaced
|
|
169
|
+
// api.ctx — this is the only reliable way to know if listening was on.
|
|
152
170
|
try {
|
|
153
|
-
if (
|
|
171
|
+
if (wasListening && api && api.listenMqtt) {
|
|
154
172
|
try {
|
|
155
173
|
if (typeof api.stopListening === 'function') {
|
|
156
174
|
try { api.stopListening(); } catch (_) {}
|
|
157
175
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
api.listenMqtt(cb);
|
|
176
|
+
if (savedListenCallback) {
|
|
177
|
+
api.listenMqtt(savedListenCallback);
|
|
161
178
|
} else {
|
|
162
179
|
api.listenMqtt();
|
|
163
180
|
}
|
|
@@ -216,9 +233,10 @@ class AutoReLoginManager {
|
|
|
216
233
|
updateAppState(appState) {
|
|
217
234
|
if (!this.credentials) return;
|
|
218
235
|
if (!Array.isArray(appState) || appState.length === 0) return;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
236
|
+
// Always overwrite with the freshest cookies — the old condition was too
|
|
237
|
+
// restrictive and silently skipped updates when credentials.appState was
|
|
238
|
+
// already a valid object, leaving stale cookies in re-login credentials.
|
|
239
|
+
this.credentials.appState = appState;
|
|
222
240
|
}
|
|
223
241
|
|
|
224
242
|
disable() {
|
package/src/utils/clients.js
CHANGED
|
@@ -49,22 +49,26 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
49
49
|
|
|
50
50
|
await delay(retryTime);
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
// Guard against undefined Content-Type header before splitting
|
|
53
|
+
const contentType = (data.request.headers && data.request.headers["content-type"]) || "";
|
|
54
|
+
if (contentType.split(";")[0].trim() === "multipart/form-data") {
|
|
53
55
|
const newData = await http.postFormData(
|
|
54
56
|
url,
|
|
55
57
|
ctx.jar,
|
|
56
58
|
data.request.formData,
|
|
57
59
|
data.request.qs,
|
|
58
|
-
ctx.globalOptions,
|
|
59
60
|
ctx
|
|
60
61
|
);
|
|
61
62
|
return await parseAndCheckLogin(ctx, http, retryCount)(newData);
|
|
62
63
|
} else {
|
|
64
|
+
// defaultFuncs.post signature: (url, jar, form, ctxx, customHeader)
|
|
65
|
+
// The 4th arg must be ctx (not ctx.globalOptions) — passing globalOptions
|
|
66
|
+
// here caused the retry to be treated as a raw network call without
|
|
67
|
+
// session context, missing auth headers and session inspection.
|
|
63
68
|
const newData = await http.post(
|
|
64
69
|
url,
|
|
65
70
|
ctx.jar,
|
|
66
71
|
data.request.form,
|
|
67
|
-
ctx.globalOptions,
|
|
68
72
|
ctx
|
|
69
73
|
);
|
|
70
74
|
return await parseAndCheckLogin(ctx, http, retryCount)(newData);
|
|
@@ -261,9 +265,10 @@ function saveCookies(jar) {
|
|
|
261
265
|
function getAccessFromBusiness(jar, Options) {
|
|
262
266
|
return async function (res) {
|
|
263
267
|
const html = res ? res.body : null;
|
|
264
|
-
|
|
268
|
+
// Use the same axios wrapper used everywhere else — "request" module does not exist
|
|
269
|
+
const { get } = require("./axios");
|
|
265
270
|
try {
|
|
266
|
-
const businessRes = await get("https://business.facebook.com/content_management", jar, null, Options,
|
|
271
|
+
const businessRes = await get("https://business.facebook.com/content_management", jar, null, Options, { noRef: true });
|
|
267
272
|
const token = /"accessToken":"([^.]+)","clientID":/g.exec(businessRes.body)[1];
|
|
268
273
|
return [html, token];
|
|
269
274
|
} catch (e) {
|
|
@@ -177,58 +177,72 @@ function _formatAttachment(attachment1, attachment2) {
|
|
|
177
177
|
attachment1: attachment1,
|
|
178
178
|
attachment2: attachment2
|
|
179
179
|
};
|
|
180
|
-
case "MessageImage":
|
|
180
|
+
case "MessageImage": {
|
|
181
|
+
// Guard nested objects — Facebook occasionally returns MessageImage with
|
|
182
|
+
// missing preview/thumbnail/large_preview/original_dimensions objects,
|
|
183
|
+
// which causes a crash on property access (e.g. blob.preview.uri).
|
|
184
|
+
const imgThumb = blob.thumbnail || {};
|
|
185
|
+
const imgPrev = blob.preview || {};
|
|
186
|
+
const imgLarge = blob.large_preview || {};
|
|
187
|
+
const imgDims = blob.original_dimensions || {};
|
|
181
188
|
return {
|
|
182
189
|
type: "photo",
|
|
183
190
|
ID: blob.legacy_attachment_id,
|
|
184
191
|
filename: blob.filename,
|
|
185
|
-
thumbnailUrl:
|
|
186
|
-
previewUrl:
|
|
187
|
-
previewWidth:
|
|
188
|
-
previewHeight:
|
|
189
|
-
largePreviewUrl:
|
|
190
|
-
largePreviewWidth:
|
|
191
|
-
largePreviewHeight:
|
|
192
|
-
url:
|
|
193
|
-
width:
|
|
194
|
-
height:
|
|
192
|
+
thumbnailUrl: imgThumb.uri || null,
|
|
193
|
+
previewUrl: imgPrev.uri || null,
|
|
194
|
+
previewWidth: imgPrev.width || 0,
|
|
195
|
+
previewHeight: imgPrev.height || 0,
|
|
196
|
+
largePreviewUrl: imgLarge.uri || null,
|
|
197
|
+
largePreviewWidth: imgLarge.width || 0,
|
|
198
|
+
largePreviewHeight: imgLarge.height || 0,
|
|
199
|
+
url: imgLarge.uri || null,
|
|
200
|
+
width: imgDims.x || 0,
|
|
201
|
+
height: imgDims.y || 0,
|
|
195
202
|
name: blob.filename
|
|
196
203
|
};
|
|
197
|
-
|
|
204
|
+
}
|
|
205
|
+
case "MessageAnimatedImage": {
|
|
206
|
+
const aniPrev = blob.preview_image || {};
|
|
207
|
+
const aniImg = blob.animated_image || {};
|
|
198
208
|
return {
|
|
199
209
|
type: "animated_image",
|
|
200
210
|
ID: blob.legacy_attachment_id,
|
|
201
211
|
filename: blob.filename,
|
|
202
|
-
previewUrl:
|
|
203
|
-
previewWidth:
|
|
204
|
-
previewHeight:
|
|
205
|
-
url:
|
|
206
|
-
width:
|
|
207
|
-
height:
|
|
208
|
-
thumbnailUrl:
|
|
212
|
+
previewUrl: aniPrev.uri || null,
|
|
213
|
+
previewWidth: aniPrev.width || 0,
|
|
214
|
+
previewHeight: aniPrev.height || 0,
|
|
215
|
+
url: aniImg.uri || null,
|
|
216
|
+
width: aniImg.width || 0,
|
|
217
|
+
height: aniImg.height || 0,
|
|
218
|
+
thumbnailUrl: aniPrev.uri || null,
|
|
209
219
|
name: blob.filename,
|
|
210
|
-
facebookUrl:
|
|
211
|
-
rawGifImage:
|
|
212
|
-
animatedGifUrl:
|
|
213
|
-
animatedGifPreviewUrl:
|
|
214
|
-
animatedWebpUrl:
|
|
215
|
-
animatedWebpPreviewUrl:
|
|
220
|
+
facebookUrl: aniImg.uri || null,
|
|
221
|
+
rawGifImage: aniImg.uri || null,
|
|
222
|
+
animatedGifUrl: aniImg.uri || null,
|
|
223
|
+
animatedGifPreviewUrl: aniPrev.uri || null,
|
|
224
|
+
animatedWebpUrl: aniImg.uri || null,
|
|
225
|
+
animatedWebpPreviewUrl: aniPrev.uri || null
|
|
216
226
|
};
|
|
217
|
-
|
|
227
|
+
}
|
|
228
|
+
case "MessageVideo": {
|
|
229
|
+
const vidImg = blob.large_image || {};
|
|
230
|
+
const vidDims = blob.original_dimensions || {};
|
|
218
231
|
return {
|
|
219
232
|
type: "video",
|
|
220
233
|
filename: blob.filename,
|
|
221
234
|
ID: blob.legacy_attachment_id,
|
|
222
|
-
previewUrl:
|
|
223
|
-
previewWidth:
|
|
224
|
-
previewHeight:
|
|
225
|
-
url: blob.playable_url,
|
|
226
|
-
width:
|
|
227
|
-
height:
|
|
228
|
-
duration: blob.playable_duration_in_ms,
|
|
229
|
-
videoType: blob.video_type.toLowerCase(),
|
|
230
|
-
thumbnailUrl:
|
|
235
|
+
previewUrl: vidImg.uri || null,
|
|
236
|
+
previewWidth: vidImg.width || 0,
|
|
237
|
+
previewHeight: vidImg.height || 0,
|
|
238
|
+
url: blob.playable_url || null,
|
|
239
|
+
width: vidDims.x || 0,
|
|
240
|
+
height: vidDims.y || 0,
|
|
241
|
+
duration: blob.playable_duration_in_ms || 0,
|
|
242
|
+
videoType: blob.video_type ? blob.video_type.toLowerCase() : "unknown",
|
|
243
|
+
thumbnailUrl: vidImg.uri || null
|
|
231
244
|
};
|
|
245
|
+
}
|
|
232
246
|
case "MessageFile":
|
|
233
247
|
return {
|
|
234
248
|
type: "file",
|
|
@@ -37,17 +37,24 @@ function formatDeltaMessage(m) {
|
|
|
37
37
|
isReply: true
|
|
38
38
|
} : null;
|
|
39
39
|
|
|
40
|
+
// Guard: actorFbId can be null on system/admin messages; threadKey can be
|
|
41
|
+
// missing on malformed deltas from Facebook — both crash with .toString().
|
|
42
|
+
const senderID = md.actorFbId != null ? formatID(md.actorFbId.toString()) : "0";
|
|
43
|
+
const threadKey = md.threadKey || {};
|
|
44
|
+
const threadRaw = threadKey.threadFbId || threadKey.otherUserFbId;
|
|
45
|
+
const threadID = threadRaw != null ? formatID(threadRaw.toString()) : "0";
|
|
46
|
+
|
|
40
47
|
return {
|
|
41
48
|
type: "message",
|
|
42
|
-
senderID
|
|
49
|
+
senderID,
|
|
43
50
|
body: m.delta.body || "",
|
|
44
|
-
threadID
|
|
51
|
+
threadID,
|
|
45
52
|
messageID: md.messageId,
|
|
46
53
|
offlineThreadingId: md.offlineThreadingId,
|
|
47
54
|
attachments: (m.delta.attachments || []).map(v => _formatAttachment(v)),
|
|
48
55
|
mentions: mentions,
|
|
49
56
|
timestamp: md.timestamp,
|
|
50
|
-
isGroup: !!
|
|
57
|
+
isGroup: !!threadKey.threadFbId,
|
|
51
58
|
participantIDs: m.delta.participants,
|
|
52
59
|
messageReply: messageReply
|
|
53
60
|
};
|
|
@@ -79,24 +86,35 @@ function formatDeltaEvent(m) {
|
|
|
79
86
|
logMessageData = m;
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
// Guard: messageMetadata or threadKey may be absent on some delta variants
|
|
90
|
+
const meta = m.messageMetadata || {};
|
|
91
|
+
const evtKey = meta.threadKey || {};
|
|
92
|
+
const evtThreadRaw = evtKey.threadFbId || evtKey.otherUserFbId;
|
|
93
|
+
const evtThreadID = evtThreadRaw != null ? formatID(evtThreadRaw.toString()) : "0";
|
|
94
|
+
const evtMessageID = meta.messageId != null ? meta.messageId.toString() : "";
|
|
95
|
+
|
|
82
96
|
return {
|
|
83
97
|
type: "event",
|
|
84
|
-
threadID:
|
|
85
|
-
messageID:
|
|
98
|
+
threadID: evtThreadID,
|
|
99
|
+
messageID: evtMessageID,
|
|
86
100
|
logMessageType,
|
|
87
101
|
logMessageData,
|
|
88
|
-
logMessageBody:
|
|
89
|
-
timestamp:
|
|
90
|
-
author:
|
|
102
|
+
logMessageBody: meta.adminText,
|
|
103
|
+
timestamp: meta.timestamp,
|
|
104
|
+
author: meta.actorFbId,
|
|
91
105
|
participantIDs: m.participants
|
|
92
106
|
};
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
function formatDeltaReadReceipt(delta) {
|
|
110
|
+
// Guard: threadKey or its sub-fields may be missing in some receipt variants
|
|
111
|
+
const tk = delta.threadKey || {};
|
|
112
|
+
const reader = (tk.otherUserFbId || delta.actorFbId);
|
|
113
|
+
const threadRaw = tk.otherUserFbId || tk.threadFbId;
|
|
96
114
|
return {
|
|
97
|
-
reader:
|
|
115
|
+
reader: reader != null ? reader.toString() : "0",
|
|
98
116
|
time: delta.actionTimestampMs,
|
|
99
|
-
threadID: formatID(
|
|
117
|
+
threadID: threadRaw != null ? formatID(threadRaw.toString()) : "0",
|
|
100
118
|
type: "read_receipt"
|
|
101
119
|
};
|
|
102
120
|
}
|