@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyneoaz/metachat",
3
- "version": "1.0.6",
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.",
@@ -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").then((ok) => {
694
- if (ok && ctx._listeningActive && typeof api.listenMqtt === 'function') {
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
- const listeners = prevEmitter.rawListeners ? prevEmitter.rawListeners(event) : [];
1024
- for (const listener of listeners) {
1025
- msgEmitter.on(event, listener);
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
- const updated = { fb_dtsg };
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);
@@ -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
- if (mqttReady && !isMultiRecipient && api.sendMessageMqtt) {
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
- const mqttReady = ctx.mqttClient && ctx.mqttClient.connected;
315
- if (mqttReady && !Array.isArray(threadID) && api.sendMessageMqtt) {
316
- try {
317
- const mqttRes = await api.sendMessageMqtt(msg, threadID, replyToMessage);
318
- callback(null, mqttRes);
319
- } catch (_mqttErr) {
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 >= cb.cooldownMs) {
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
- return Math.max(0, cb.cooldownMs - (Date.now() - cb.trippedAt));
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
- if (durationMs) cb.cooldownMs = durationMs;
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: ${(cb.cooldownMs / 60000).toFixed(1)} min`);
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) {
@@ -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
- // CRITICAL FIX: Update the live ctx object IN-PLACE instead of
125
- // replacing api.ctx. Every API method closure (sendMessage,
126
- // listenMqtt, etc.) captured the original ctx variable replacing
127
- // the reference leaves all of them with stale tokens forever.
128
- // Updating in-place means all closures immediately see fresh values.
129
- const liveCtx = api.ctx;
130
- if (liveCtx && newApi.ctx) {
131
- const sessionFields = [
132
- 'fb_dtsg', 'jazoest', 'lsd', 'ttstamp',
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 (api && api.listenMqtt && api.ctx && api.ctx._listeningActive) {
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
- const cb = api.ctx._lastListenCallback || null;
185
- if (cb) {
186
- api.listenMqtt(cb);
176
+ if (savedListenCallback) {
177
+ api.listenMqtt(savedListenCallback);
187
178
  } else {
188
179
  api.listenMqtt();
189
180
  }