@lazyneoaz/metachat 1.0.10 → 1.1.0

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.
Files changed (53) hide show
  1. package/package.json +1 -1
  2. package/src/apis/addUserToGroup.js +1 -1
  3. package/src/apis/changeAdminStatus.js +46 -55
  4. package/src/apis/changeArchivedStatus.js +16 -8
  5. package/src/apis/changeAvatar.js +2 -2
  6. package/src/apis/changeBio.js +1 -1
  7. package/src/apis/changeBlockedStatus.js +1 -1
  8. package/src/apis/changeGroupImage.js +23 -15
  9. package/src/apis/changeThreadColor.js +1 -1
  10. package/src/apis/changeThreadEmoji.js +14 -8
  11. package/src/apis/createNewGroup.js +13 -5
  12. package/src/apis/createPoll.js +14 -5
  13. package/src/apis/deleteMessage.js +11 -8
  14. package/src/apis/deleteThread.js +1 -1
  15. package/src/apis/editMessage.js +59 -48
  16. package/src/apis/emoji.js +2 -2
  17. package/src/apis/follow.js +26 -30
  18. package/src/apis/forwardAttachment.js +3 -2
  19. package/src/apis/forwardMessage.js +1 -1
  20. package/src/apis/gcmember.js +4 -3
  21. package/src/apis/gcname.js +3 -3
  22. package/src/apis/gcrule.js +4 -3
  23. package/src/apis/getFriendsList.js +2 -2
  24. package/src/apis/getThreadInfo.js +1 -1
  25. package/src/apis/getThreadPictures.js +1 -1
  26. package/src/apis/handleMessageRequest.js +1 -1
  27. package/src/apis/listenMqtt.js +160 -6
  28. package/src/apis/markAsRead.js +15 -9
  29. package/src/apis/muteThread.js +1 -1
  30. package/src/apis/nickname.js +62 -48
  31. package/src/apis/pinMessage.js +110 -130
  32. package/src/apis/removeUserFromGroup.js +9 -2
  33. package/src/apis/resolvePhotoUrl.js +1 -1
  34. package/src/apis/searchForThread.js +1 -1
  35. package/src/apis/sendEffect.js +3 -2
  36. package/src/apis/sendMessage.js +13 -2
  37. package/src/apis/setMessageReaction.js +58 -19
  38. package/src/apis/setMessageReactionMqtt.js +96 -56
  39. package/src/apis/setThreadThemeMqtt.js +70 -75
  40. package/src/apis/shareContact.js +37 -31
  41. package/src/apis/theme.js +1 -1
  42. package/src/apis/unfriend.js +1 -1
  43. package/src/apis/unsendMessage.js +71 -9
  44. package/src/app/state.js +1 -1
  45. package/src/engine/models/loginHelper.js +5 -0
  46. package/src/types/index.d.ts +40 -1
  47. package/src/utils/antiSuspension.js +5 -5
  48. package/src/utils/auth-helpers.js +1 -1
  49. package/src/utils/clients.js +21 -0
  50. package/src/utils/headers.js +1 -1
  51. package/src/utils/lsRequest.js +175 -0
  52. package/src/utils/rateLimiter.js +6 -2
  53. package/src/utils/tokenRefresh.js +24 -2
@@ -1,70 +1,81 @@
1
1
  "use strict";
2
- /**
3
- * @author RFS-ADRENO
4
- * @rewrittenBy Isai Ivanov
5
- */
6
2
 
7
3
  const utils = require('../utils');
8
-
9
- function canBeCalled(func) {
10
- try {
11
- Reflect.apply(func, null, []);
12
- return true;
13
- } catch (error) {
14
- return false;
15
- }
16
- }
4
+ const { publishLsRequestWithAck } = require('../utils/lsRequest');
17
5
 
18
6
  /**
19
- * A function for editing bot's messages.
20
- * @param {string} text - The text with which the bot will edit its messages.
21
- * @param {string} messageID - The message ID of the message the bot will edit.
22
- * @param {Object} callback - Callback for the function.
23
- */
24
-
7
+ * Edits a previously sent bot message via MQTT with ACK confirmation.
8
+ *
9
+ * @param {string} text New text content for the message.
10
+ * @param {string} messageID The ID of the message to edit.
11
+ * @param {Function} [callback] Optional callback(err, result).
12
+ * @returns {Promise}
13
+ */
25
14
  module.exports = function (defaultFuncs, api, ctx) {
26
15
  return function editMessage(text, messageID, callback) {
27
- if (!ctx.mqttClient) {
28
- throw new Error('Not connected to MQTT');
16
+ let resolveFunc, rejectFunc;
17
+ const returnPromise = new Promise((resolve, reject) => {
18
+ resolveFunc = resolve;
19
+ rejectFunc = reject;
20
+ });
21
+
22
+ const cb = (typeof callback === "function")
23
+ ? (err, data) => { callback(err, data); if (err) rejectFunc(err); else resolveFunc(data); }
24
+ : (err, data) => { if (err) rejectFunc(err); else resolveFunc(data); };
25
+
26
+ if (!ctx.mqttClient || !ctx.mqttClient.connected) {
27
+ return cb(new Error("Not connected to MQTT — call listenMqtt() first")), returnPromise;
29
28
  }
30
29
 
31
- ctx.wsReqNumber += 1;
32
- ctx.wsTaskNumber += 1;
30
+ if (typeof ctx.wsReqNumber !== "number") ctx.wsReqNumber = 0;
31
+ if (typeof ctx.wsTaskNumber !== "number") ctx.wsTaskNumber = 0;
33
32
 
34
- const queryPayload = {
35
- message_id: messageID,
36
- text
37
- };
33
+ const requestId = ++ctx.wsReqNumber;
34
+ const taskId = ++ctx.wsTaskNumber;
38
35
 
39
- const query = {
36
+ const task = {
40
37
  failure_count: null,
41
- label: '742',
42
- payload: JSON.stringify(queryPayload),
43
- queue_name: 'edit_message',
44
- task_id: ctx.wsTaskNumber
38
+ label: "742",
39
+ payload: JSON.stringify({ message_id: messageID, text }),
40
+ queue_name: "edit_message",
41
+ task_id: taskId
45
42
  };
46
43
 
47
- const context = {
48
- app_id: '2220391788200892',
49
- payload: {
44
+ const content = {
45
+ app_id: "2220391788200892",
46
+ payload: JSON.stringify({
50
47
  data_trace_id: null,
51
48
  epoch_id: parseInt(utils.generateOfflineThreadingID()),
52
- tasks: [query],
53
- version_id: '6903494529735864'
54
- },
55
- request_id: ctx.wsReqNumber,
49
+ tasks: [task],
50
+ version_id: "6903494529735864"
51
+ }),
52
+ request_id: requestId,
56
53
  type: 3
57
54
  };
58
55
 
59
- context.payload = JSON.stringify(context.payload);
60
-
61
-
62
-
63
-
64
-
65
- ctx.mqttClient.publish('/ls_req', JSON.stringify(context), {
66
- qos: 1, retain: false
56
+ publishLsRequestWithAck({
57
+ client: ctx.mqttClient,
58
+ content,
59
+ requestId,
60
+ timeoutMs: 10000,
61
+ extract: (message) => ({
62
+ success: true,
63
+ messageID,
64
+ text,
65
+ ack: message.payload
66
+ })
67
+ }).then((result) => {
68
+ cb(null, result);
69
+ }).catch((err) => {
70
+ if (err && err.message && err.message.includes("Timeout waiting for LS ACK")) {
71
+ utils.warn("editMessage", "ACK timed out — edit may still have been applied");
72
+ cb(null, { success: true, messageID, text, ackTimeout: true });
73
+ } else {
74
+ utils.error("editMessage", err && err.message ? err.message : err);
75
+ cb(err instanceof Error ? err : new Error(String(err && err.message ? err.message : err)));
76
+ }
67
77
  });
78
+
79
+ return returnPromise;
68
80
  };
69
81
  };
70
-
package/src/apis/emoji.js CHANGED
@@ -69,7 +69,7 @@ module.exports = function (defaultFuncs, api, ctx) {
69
69
  return _callback(new Error("An emoji character is required."));
70
70
  }
71
71
 
72
- if (!ctx.mqttClient) {
72
+ if (!ctx.mqttClient || !ctx.mqttClient.connected) {
73
73
  return _callback(new Error("Not connected to MQTT"));
74
74
  }
75
75
 
@@ -92,7 +92,7 @@ module.exports = function (defaultFuncs, api, ctx) {
92
92
  };
93
93
 
94
94
  const context = {
95
- app_id: ctx.appID,
95
+ app_id: ctx.appID || '2220391788200892',
96
96
  payload: {
97
97
  epoch_id: parseInt(utils.generateOfflineThreadingID()),
98
98
  tasks: [query],
@@ -1,27 +1,28 @@
1
1
  "use strict";
2
2
 
3
- /**
4
- * Author @YanMaglinte
5
- * https://github.com/YANDEVA
6
- * * Example:
7
- * api.follow("100090794779367", true);
8
- * jsdocs @ChoruOfficial
9
- */
10
-
11
- /**
12
- * @param {object} defaultFuncs The default functions for making API requests.
13
- * @param {object} api The full API object.
14
- * @param {object} ctx The context object.
15
- * @returns {function(senderID: string, boolean: boolean, callback?: (err: any, data?: any) => void): void} The follow function.
16
- */
17
3
  module.exports = function (defaultFuncs, api, ctx) {
18
- /**
19
- * Follows or unfollows a user on Facebook.
20
- * @param {string} senderID The ID of the user to follow or unfollow.
21
- * @param {boolean} boolean Set to `true` to follow the user, `false` to unfollow.
22
- * @param {(err: any, data?: any) => void} [callback] An optional callback function.
23
- */
24
4
  return function follow(senderID, boolean, callback) {
5
+ let resolveFunc = () => {};
6
+ let rejectFunc = () => {};
7
+ const returnPromise = new Promise((resolve, reject) => {
8
+ resolveFunc = resolve;
9
+ rejectFunc = reject;
10
+ });
11
+
12
+ if (!callback) {
13
+ callback = (err, data) => {
14
+ if (err) return rejectFunc(err);
15
+ resolveFunc(data);
16
+ };
17
+ } else {
18
+ const _userCb = callback;
19
+ callback = (err, data) => {
20
+ if (err) { _userCb(err); return rejectFunc(err); }
21
+ _userCb(null, data);
22
+ resolveFunc(data);
23
+ };
24
+ }
25
+
25
26
  let form;
26
27
  if (boolean) {
27
28
  form = {
@@ -67,15 +68,10 @@ module.exports = function (defaultFuncs, api, ctx) {
67
68
  }
68
69
 
69
70
  api.httpPost("https://www.facebook.com/api/graphql/", form, (err, data) => {
70
- if (err) {
71
- if (typeof callback === "function") {
72
- callback(err);
73
- }
74
- } else {
75
- if (typeof callback === "function") {
76
- callback(null, data);
77
- }
78
- }
71
+ if (err) return callback(err instanceof Error ? err : new Error(String(err && err.message ? err.message : err)));
72
+ callback(null, data);
79
73
  });
74
+
75
+ return returnPromise;
80
76
  };
81
- };
77
+ };
@@ -48,6 +48,7 @@ module.exports = function (defaultFuncs, api, ctx) {
48
48
  const timestamp = Date.now();
49
49
  const tid = String(threadID);
50
50
  if (typeof ctx.wsReqNumber !== 'number') ctx.wsReqNumber = 0;
51
+ if (typeof ctx.wsTaskNumber !== 'number') ctx.wsTaskNumber = 0;
51
52
  const reqID = ++ctx.wsReqNumber;
52
53
 
53
54
  const sendPayload = {
@@ -73,7 +74,7 @@ module.exports = function (defaultFuncs, api, ctx) {
73
74
  label: "46",
74
75
  payload: JSON.stringify(sendPayload),
75
76
  queue_name: tid,
76
- task_id: 400,
77
+ task_id: ++ctx.wsTaskNumber,
77
78
  failure_count: null,
78
79
  },
79
80
  {
@@ -84,7 +85,7 @@ module.exports = function (defaultFuncs, api, ctx) {
84
85
  sync_group: 1,
85
86
  }),
86
87
  queue_name: tid,
87
- task_id: 401,
88
+ task_id: ++ctx.wsTaskNumber,
88
89
  failure_count: null,
89
90
  },
90
91
  ],
@@ -38,7 +38,7 @@ module.exports = (defaultFuncs, api, ctx) => {
38
38
  ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
39
39
 
40
40
  if (res && res.error) {
41
- throw res;
41
+ throw new Error(res.error_msg || res.errorSummary || String(res.error));
42
42
  }
43
43
 
44
44
  callback(null, { success: true, forwardedTo: threadIDs });
@@ -54,7 +54,7 @@ module.exports = function (defaultFuncs, api, ctx) {
54
54
  _callback(null, { type: "error_gc", error: "threadID is required." });
55
55
  return returnPromise;
56
56
  }
57
- if (!ctx.mqttClient) {
57
+ if (!ctx.mqttClient || !ctx.mqttClient.connected) {
58
58
  _callback(null, { type: "error_gc", error: "Not connected to MQTT" });
59
59
  return returnPromise;
60
60
  }
@@ -99,7 +99,7 @@ module.exports = function (defaultFuncs, api, ctx) {
99
99
  }
100
100
 
101
101
  const context = {
102
- app_id: ctx.appID,
102
+ app_id: ctx.appID || '2220391788200892',
103
103
  payload: { epoch_id: parseInt(utils.generateOfflineThreadingID()), tasks: [query], version_id: "24631415369801570" },
104
104
  request_id: ctx.wsReqNumber,
105
105
  type: 3
@@ -121,7 +121,8 @@ module.exports = function (defaultFuncs, api, ctx) {
121
121
  return _callback(null, gcmemberInfo);
122
122
  });
123
123
  } catch (err) {
124
- _callback(null, { type: "error_gc", error: err.message || "An unknown error occurred." });
124
+ utils.error("gcmember", err);
125
+ _callback(err instanceof Error ? err : new Error(err.message || "An unknown error occurred."));
125
126
  }
126
127
 
127
128
  return returnPromise;
@@ -69,7 +69,7 @@ module.exports = function (defaultFuncs, api, ctx) {
69
69
  return _callback(new Error("newName must be a string."));
70
70
  }
71
71
 
72
- if (!ctx.mqttClient) {
72
+ if (!ctx.mqttClient || !ctx.mqttClient.connected) {
73
73
  return _callback(new Error("Not connected to MQTT"));
74
74
  }
75
75
 
@@ -91,11 +91,11 @@ module.exports = function (defaultFuncs, api, ctx) {
91
91
  };
92
92
 
93
93
  const context = {
94
- app_id: ctx.appID,
94
+ app_id: ctx.appID || '2220391788200892',
95
95
  payload: {
96
96
  epoch_id: parseInt(utils.generateOfflineThreadingID()),
97
97
  tasks: [query],
98
- version_id: '24631415369801570',
98
+ version_id: '8798795233522156',
99
99
  },
100
100
  request_id: ctx.wsReqNumber,
101
101
  type: 3,
@@ -46,7 +46,7 @@ module.exports = function (defaultFuncs, api, ctx) {
46
46
  }
47
47
  if (!userID) { _callback(null, { type: "error_gc_rule", error: "userID is required." }); return returnPromise; }
48
48
  if (!threadID) { _callback(null, { type: "error_gc_rule", error: "threadID is required." }); return returnPromise; }
49
- if (!ctx.mqttClient) { _callback(null, { type: "error_gc_rule", error: "Not connected to MQTT" }); return returnPromise; }
49
+ if (!ctx.mqttClient || !ctx.mqttClient.connected) { _callback(null, { type: "error_gc_rule", error: "Not connected to MQTT" }); return returnPromise; }
50
50
 
51
51
  const threadInfo = await api.getThreadInfo(threadID);
52
52
  if (!threadInfo) {
@@ -90,7 +90,7 @@ module.exports = function (defaultFuncs, api, ctx) {
90
90
  task_id: ctx.wsTaskNumber
91
91
  };
92
92
  const context = {
93
- app_id: ctx.appID,
93
+ app_id: ctx.appID || '2220391788200892',
94
94
  payload: {
95
95
  epoch_id: parseInt(utils.generateOfflineThreadingID()),
96
96
  tasks: [query],
@@ -116,7 +116,8 @@ module.exports = function (defaultFuncs, api, ctx) {
116
116
  });
117
117
 
118
118
  } catch (err) {
119
- _callback(null, { type: "error_gc_rule", error: err.message || "An unknown error occurred." });
119
+ utils.error("gcrule", err);
120
+ _callback(err instanceof Error ? err : new Error(err.message || "An unknown error occurred."));
120
121
  }
121
122
 
122
123
  return returnPromise;
@@ -61,11 +61,11 @@ module.exports = (defaultFuncs, api, ctx) => {
61
61
  ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
62
62
 
63
63
  if (!res) {
64
- throw { error: "getFriendsList returned empty object." };
64
+ throw new Error("getFriendsList returned empty object.");
65
65
  }
66
66
 
67
67
  if (res.error) {
68
- throw res;
68
+ throw new Error(res.error_msg || res.errorSummary || String(res.error));
69
69
  }
70
70
 
71
71
  callback(null, formatData(res.payload));
@@ -225,7 +225,7 @@ module.exports = function (defaultFuncs, api, ctx) {
225
225
  .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
226
226
 
227
227
  if (resData.error) {
228
- throw resData;
228
+ throw new Error(resData.error_msg || resData.errorSummary || String(resData.error));
229
229
  }
230
230
 
231
231
  const threadInfos = {};
@@ -44,7 +44,7 @@ module.exports = (defaultFuncs, api, ctx) => {
44
44
  ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
45
45
 
46
46
  if (res && res.error) {
47
- throw res;
47
+ throw new Error(res.error_msg || res.errorSummary || String(res.error));
48
48
  }
49
49
 
50
50
  return callback(null, res?.payload || res);
@@ -36,7 +36,7 @@ module.exports = (defaultFuncs, api, ctx) => {
36
36
  ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
37
37
 
38
38
  if (res && res.error) {
39
- throw res;
39
+ throw new Error(res.error_msg || res.errorSummary || String(res.error));
40
40
  }
41
41
 
42
42
  return callback(null, { success: true, accepted: accept });
@@ -331,6 +331,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
331
331
  }));
332
332
 
333
333
  mqttClient.on('connect', guard("connect", async () => {
334
+ const wasReconnect = !ctx._mqttConnected && (ctx._reconnectAttempts || 0) > 0;
334
335
  if (!ctx._mqttConnected) {
335
336
  utils.log("MQTT connected successfully");
336
337
  ctx._mqttConnected = true;
@@ -338,6 +339,17 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
338
339
  ctx._cycling = false;
339
340
  ctx._reconnectAttempts = 0;
340
341
  ctx._mqttQuickCloseCount = 0;
342
+
343
+ // Update reconnect stats and emit lifecycle events
344
+ if (!ctx._reconnectStats) ctx._reconnectStats = { totalAttempts: 0, lastAttemptAt: null, nextAttemptAt: null, lastSuccessAt: null };
345
+ ctx._reconnectStats.nextAttemptAt = null;
346
+ ctx._reconnectStats.lastSuccessAt = Date.now();
347
+ try {
348
+ if (ctx._emitter) {
349
+ const eventName = wasReconnect ? 'reconnected' : 'connected';
350
+ ctx._emitter.emit(eventName, { timestamp: Date.now(), totalReconnects: ctx._reconnectStats.totalAttempts });
351
+ }
352
+ } catch (_) {}
341
353
  if (ctx._reconnectTimer) {
342
354
  clearTimeout(ctx._reconnectTimer);
343
355
  ctx._reconnectTimer = null;
@@ -440,7 +452,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
440
452
  ctx.syncToken = jsonMessage.syncToken;
441
453
  }
442
454
  if (jsonMessage.lastIssuedSeqId) {
443
- ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId);
455
+ ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId, 10);
444
456
  }
445
457
 
446
458
  if (jsonMessage.deltas) {
@@ -487,6 +499,9 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
487
499
  }
488
500
  // Save connected state BEFORE clearing it — used for quick-close detection.
489
501
  const wasConnected = ctx._mqttConnected;
502
+ try {
503
+ if (ctx._emitter) ctx._emitter.emit('disconnected', { timestamp: Date.now(), wasConnected });
504
+ } catch (_) {}
490
505
  ctx._mqttConnected = false;
491
506
  if (ctx._ending || ctx._cycling) return;
492
507
 
@@ -552,6 +567,15 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
552
567
  ctx._mqttWatchdog = null;
553
568
  }
554
569
  ctx._mqttConnected = false;
570
+
571
+ if (!ctx._ending && !ctx._cycling && ctx.globalOptions.autoReconnect) {
572
+ try { mqttClient.end(true); } catch (_) {}
573
+ ctx._reconnectAttempts = (ctx._reconnectAttempts || 0) + 1;
574
+ const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
575
+ const d = computeBackoffDelay(ctx, baseDelay, MQTT_MAX_BACKOFF, MQTT_JITTER_MAX);
576
+ utils.warn("MQTT", `Disconnect received — reconnecting in ${d}ms (attempt ${ctx._reconnectAttempts})`);
577
+ scheduleReconnect(d);
578
+ }
555
579
  }));
556
580
 
557
581
  mqttClient.on('offline', guard("offline", () => {
@@ -583,7 +607,8 @@ const MQTT_DEFAULTS = {
583
607
  autoReconnect: true,
584
608
  watchdogIntervalMs: 60000,
585
609
  staleMs: 300000,
586
- reconnectAfterStop: false
610
+ reconnectAfterStop: false,
611
+ maxReconnectAttempts: 100
587
612
  };
588
613
 
589
614
  function mqttConf(ctx, overrides) {
@@ -756,6 +781,22 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
756
781
  if (ctx._ending) return;
757
782
  if (ctx._reconnectTimer) return;
758
783
  utils.warn("MQTT", `Will reconnect in ${ms}ms`);
784
+
785
+ if (!ctx._reconnectStats) ctx._reconnectStats = { totalAttempts: 0, lastAttemptAt: null, nextAttemptAt: null, lastSuccessAt: null };
786
+ ctx._reconnectStats.totalAttempts = (ctx._reconnectStats.totalAttempts || 0) + 1;
787
+ ctx._reconnectStats.lastAttemptAt = Date.now();
788
+ ctx._reconnectStats.nextAttemptAt = Date.now() + ms;
789
+ try {
790
+ if (ctx._emitter) {
791
+ ctx._emitter.emit('reconnecting', {
792
+ attempt: ctx._reconnectAttempts || 0,
793
+ delayMs: ms,
794
+ nextAttemptAt: ctx._reconnectStats.nextAttemptAt,
795
+ timestamp: Date.now()
796
+ });
797
+ }
798
+ } catch (_) {}
799
+
759
800
  ctx._reconnectTimer = setTimeout(() => {
760
801
  ctx._reconnectTimer = null;
761
802
  getSeqIDWrapper();
@@ -812,11 +853,67 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
812
853
  const msg = ((err && err.error) || (err && err.message) || String(err || "")) + detail;
813
854
 
814
855
  if (/Not logged in/i.test(msg)) {
815
- utils.error("MQTT", "Auth error in getSeqID: Not logged in");
856
+ utils.error("MQTT", "Auth error in getSeqID: Not logged in — attempting recovery...");
857
+
858
+ // Step 1: Try token refresh first (fastest, least invasive)
859
+ let tokenRefreshed = false;
860
+ try {
861
+ if (api.tokenRefreshManager && typeof api.tokenRefreshManager.refreshTokens === 'function') {
862
+ utils.log("MQTT", "getSeqID: refreshing tokens before giving up...");
863
+ await api.tokenRefreshManager.refreshTokens(ctx, defaultFuncs, 'https://www.facebook.com');
864
+ tokenRefreshed = true;
865
+ utils.log("MQTT", "getSeqID: token refresh succeeded, scheduling reconnect");
866
+ }
867
+ } catch (refreshErr) {
868
+ utils.warn("MQTT", `getSeqID: token refresh failed: ${refreshErr && refreshErr.message ? refreshErr.message : refreshErr}`);
869
+ }
870
+
871
+ if (tokenRefreshed && ctx.globalOptions.autoReconnect) {
872
+ ctx._reconnectAttempts = Math.max(0, (ctx._reconnectAttempts || 0) - 1);
873
+ const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 3000;
874
+ return scheduleReconnect(baseDelay);
875
+ }
876
+
877
+ // Step 2: Try full auto re-login (email+password) as last resort
878
+ let reloginOk = false;
879
+ try {
880
+ const { globalAutoReLoginManager } = require('../utils/autoReLogin');
881
+ if (globalAutoReLoginManager && globalAutoReLoginManager.isEnabled && globalAutoReLoginManager.isEnabled()) {
882
+ utils.log("MQTT", "getSeqID: attempting auto re-login...");
883
+ reloginOk = await globalAutoReLoginManager.handleSessionExpiry(api, 'https://www.facebook.com', "MQTT getSeqID Not logged in");
884
+ if (reloginOk) {
885
+ utils.log("MQTT", "getSeqID: re-login succeeded, scheduling MQTT reconnect");
886
+ }
887
+ }
888
+ } catch (reloginErr) {
889
+ utils.warn("MQTT", `getSeqID: auto re-login failed: ${reloginErr && reloginErr.message ? reloginErr.message : reloginErr}`);
890
+ }
891
+
892
+ if (reloginOk && ctx.globalOptions.autoReconnect) {
893
+ ctx._reconnectAttempts = 0;
894
+ const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 5000;
895
+ return scheduleReconnect(baseDelay);
896
+ }
897
+
898
+ // Both recovery paths exhausted — emit auth error to signal the user
816
899
  return emitAuthError("not_logged_in", msg);
817
900
  }
818
901
  if (/blocked the login|checkpoint|security check|session.*expir|invalid.*session|authentication.*fail|auth.*fail|login.*block|account.*lock|verification.*requir/i.test(msg)) {
819
902
  utils.error("MQTT", "Auth error in getSeqID: Session/Login blocked");
903
+
904
+ // Still try token refresh for session expiry before giving up
905
+ try {
906
+ if (api.tokenRefreshManager && typeof api.tokenRefreshManager.refreshTokens === 'function') {
907
+ utils.log("MQTT", "getSeqID: refreshing tokens on session expiry...");
908
+ await api.tokenRefreshManager.refreshTokens(ctx, defaultFuncs, 'https://www.facebook.com');
909
+ if (ctx.globalOptions.autoReconnect) {
910
+ ctx._reconnectAttempts = 0;
911
+ const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 5000;
912
+ return scheduleReconnect(baseDelay);
913
+ }
914
+ }
915
+ } catch (_) {}
916
+
820
917
  return emitAuthError("login_blocked", msg);
821
918
  }
822
919
 
@@ -1011,7 +1108,8 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
1011
1108
  try {
1012
1109
  const LIFECYCLE_EVENTS = [
1013
1110
  'sessionExpired', 'checkpoint', 'relogin', 'ready',
1014
- 'account_inactive', 'checkpoint_282', 'checkpoint_956', 'error'
1111
+ 'account_inactive', 'checkpoint_282', 'checkpoint_956', 'error',
1112
+ 'reconnecting', 'reconnected', 'connected', 'disconnected'
1015
1113
  ];
1016
1114
  for (const event of LIFECYCLE_EVENTS) {
1017
1115
  // Use listeners() NOT rawListeners() — rawListeners() returns the
@@ -1080,8 +1178,64 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
1080
1178
 
1081
1179
  ctx.firstListen = false;
1082
1180
 
1083
- api.stopListening = msgEmitter.stopListening;
1084
- api.stopListeningAsync = msgEmitter.stopListeningAsync;
1181
+ api.stopListening = msgEmitter.stopListening.bind(msgEmitter);
1182
+ api.stopListeningAsync = msgEmitter.stopListeningAsync.bind(msgEmitter);
1183
+
1184
+ api.reconnect = function(callback) {
1185
+ let resolveFunc, rejectFunc;
1186
+ const returnPromise = new Promise((resolve, reject) => {
1187
+ resolveFunc = resolve;
1188
+ rejectFunc = reject;
1189
+ });
1190
+ const cb = typeof callback === "function"
1191
+ ? (err) => { callback(err); if (err) rejectFunc(err); else resolveFunc(); }
1192
+ : (err) => { if (err) rejectFunc(err); else resolveFunc(); };
1193
+
1194
+ if (ctx._ending) {
1195
+ return cb(new Error("listenMqtt is not active — call listenMqtt() first")), returnPromise;
1196
+ }
1197
+ utils.log("MQTT", "api.reconnect() — force-reconnecting");
1198
+ try { if (ctx.mqttClient) ctx.mqttClient.end(true); } catch (_) {}
1199
+ ctx._mqttConnected = false;
1200
+ ctx._reconnectAttempts = 0;
1201
+ if (ctx._reconnectTimer) {
1202
+ clearTimeout(ctx._reconnectTimer);
1203
+ ctx._reconnectTimer = null;
1204
+ }
1205
+ try {
1206
+ if (ctx._emitter) ctx._emitter.emit('reconnecting', { forced: true, attempt: 0, delayMs: 0, timestamp: Date.now() });
1207
+ } catch (_) {}
1208
+ getSeqIDWrapper().then(() => cb(null)).catch(cb);
1209
+ return returnPromise;
1210
+ };
1211
+
1212
+ api.getReconnectStatus = function() {
1213
+ const stats = ctx._reconnectStats || {};
1214
+ return {
1215
+ connected: !!(ctx.mqttClient && ctx.mqttClient.connected),
1216
+ reconnectAttempts: ctx._reconnectAttempts || 0,
1217
+ totalReconnects: stats.totalAttempts || 0,
1218
+ lastAttemptAt: stats.lastAttemptAt || null,
1219
+ nextAttemptAt: ctx._reconnectTimer ? (stats.nextAttemptAt || null) : null,
1220
+ lastSuccessAt: stats.lastSuccessAt || null,
1221
+ reconnectPending: !!ctx._reconnectTimer,
1222
+ autoReconnect: !!ctx.globalOptions.autoReconnect,
1223
+ options: Object.assign({}, ctx._mqttOpt || {})
1224
+ };
1225
+ };
1226
+
1227
+ api.setReconnectOptions = function(newOpts) {
1228
+ if (!newOpts || typeof newOpts !== "object") return;
1229
+ ctx._mqttOpt = Object.assign({}, MQTT_DEFAULTS, ctx._mqttOpt || {}, newOpts);
1230
+ if (typeof ctx._mqttOpt.autoReconnect === "boolean") {
1231
+ ctx.globalOptions.autoReconnect = ctx._mqttOpt.autoReconnect;
1232
+ }
1233
+ if (typeof ctx._mqttOpt.cycleMs === "number") {
1234
+ conf = Object.assign({}, conf, { cycleMs: ctx._mqttOpt.cycleMs });
1235
+ }
1236
+ utils.log("MQTT", "setReconnectOptions applied:", JSON.stringify(ctx._mqttOpt));
1237
+ return ctx._mqttOpt;
1238
+ };
1085
1239
 
1086
1240
  api.useMiddleware = function(nameOrFn, fn) {
1087
1241
  const remove = ctx._middleware.use(nameOrFn, fn);
@@ -21,7 +21,18 @@ module.exports = function (defaultFuncs, api, ctx) {
21
21
 
22
22
  if (!callback) {
23
23
  callback = (err) => {
24
- if (err) return rejectFunc(err instanceof Error ? err : new Error(String(err.error || err.message || err)));
24
+ if (err) return rejectFunc(err instanceof Error ? err : new Error(String(err && (err.error || err.message) || err)));
25
+ resolveFunc(null);
26
+ };
27
+ } else {
28
+ const _userCb = callback;
29
+ callback = (err) => {
30
+ if (err) {
31
+ const e = err instanceof Error ? err : new Error(String(err && (err.error || err.message) || err));
32
+ _userCb(e);
33
+ return rejectFunc(e);
34
+ }
35
+ _userCb(null);
25
36
  resolveFunc(null);
26
37
  };
27
38
  }
@@ -43,9 +54,7 @@ module.exports = function (defaultFuncs, api, ctx) {
43
54
  .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
44
55
 
45
56
  if (resData && resData.error) {
46
- const err = new Error(String(resData.error.message || resData.error));
47
- callback(err);
48
- return returnPromise;
57
+ return callback(new Error(String(resData.error.message || resData.error)));
49
58
  }
50
59
 
51
60
  callback(null);
@@ -62,16 +71,13 @@ module.exports = function (defaultFuncs, api, ctx) {
62
71
  .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
63
72
 
64
73
  if (resData && resData.error) {
65
- const err = new Error(String(resData.error.message || resData.error));
66
- callback(err);
67
- return returnPromise;
74
+ return callback(new Error(String(resData.error.message || resData.error)));
68
75
  }
69
76
 
70
77
  callback(null);
71
78
  }
72
79
  } catch (e) {
73
- const err = e instanceof Error ? e : new Error(String(e.error || e.message || e));
74
- callback(err);
80
+ callback(e instanceof Error ? e : new Error(String(e && (e.error || e.message) || e)));
75
81
  }
76
82
 
77
83
  return returnPromise;