@lazyneoaz/metachat 1.0.6 → 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/listenMqtt.js +15 -18
- package/src/apis/refreshFb_dtsg.js +7 -1
- package/src/apis/sendMessage.js +23 -11
- package/src/utils/antiSuspension.js +21 -4
- package/src/utils/autoReLogin.js +22 -31
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.",
|
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
|
}
|
|
@@ -1020,9 +1012,14 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
1020
1012
|
'account_inactive', 'checkpoint_282', 'checkpoint_956', 'error'
|
|
1021
1013
|
];
|
|
1022
1014
|
for (const event of LIFECYCLE_EVENTS) {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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);
|
|
1026
1023
|
}
|
|
1027
1024
|
}
|
|
1028
1025
|
} catch (_) {}
|
|
@@ -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);
|
|
@@ -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,35 +128,17 @@ class AutoReLoginManager {
|
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
if (api) {
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
'mqttEndpoint', 'lastSeqId', 'appID', 'clientID',
|
|
134
|
-
'userAppID', 'mqttAppID', 'userID', 'region',
|
|
135
|
-
'access_token'
|
|
136
|
-
];
|
|
137
|
-
for (const field of sessionFields) {
|
|
138
|
-
if (newApi.ctx[field] !== undefined) {
|
|
139
|
-
liveCtx[field] = newApi.ctx[field];
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// Reset flags that block MQTT reconnection after re-login
|
|
143
|
-
liveCtx.loggedIn = true;
|
|
144
|
-
liveCtx._ending = false;
|
|
145
|
-
liveCtx._cycling = false;
|
|
146
|
-
liveCtx._mqttReauthing = false;
|
|
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;
|
|
147
140
|
}
|
|
148
141
|
|
|
149
|
-
// Do NOT replace api.defaultFuncs — the existing closures already
|
|
150
|
-
// reference the live (now-updated) ctx, so they will pick up fresh
|
|
151
|
-
// tokens without needing new function objects.
|
|
152
|
-
|
|
153
142
|
if (api.tokenRefreshManager) {
|
|
154
143
|
api.tokenRefreshManager.resetFailureCount();
|
|
155
144
|
}
|
|
@@ -175,15 +164,17 @@ class AutoReLoginManager {
|
|
|
175
164
|
this.onReLoginSuccess();
|
|
176
165
|
}
|
|
177
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.
|
|
178
170
|
try {
|
|
179
|
-
if (
|
|
171
|
+
if (wasListening && api && api.listenMqtt) {
|
|
180
172
|
try {
|
|
181
173
|
if (typeof api.stopListening === 'function') {
|
|
182
174
|
try { api.stopListening(); } catch (_) {}
|
|
183
175
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
api.listenMqtt(cb);
|
|
176
|
+
if (savedListenCallback) {
|
|
177
|
+
api.listenMqtt(savedListenCallback);
|
|
187
178
|
} else {
|
|
188
179
|
api.listenMqtt();
|
|
189
180
|
}
|