@lazyneoaz/metachat 1.0.9 → 1.0.11

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 (46) hide show
  1. package/index.js +55 -1
  2. package/package.json +1 -1
  3. package/src/apis/addUserToGroup.js +1 -1
  4. package/src/apis/changeAdminStatus.js +11 -9
  5. package/src/apis/changeGroupImage.js +1 -1
  6. package/src/apis/changeThreadColor.js +1 -1
  7. package/src/apis/createNewGroup.js +12 -4
  8. package/src/apis/createPoll.js +5 -2
  9. package/src/apis/editMessage.js +59 -48
  10. package/src/apis/emoji.js +2 -2
  11. package/src/apis/follow.js +1 -1
  12. package/src/apis/forwardAttachment.js +3 -2
  13. package/src/apis/gcmember.js +20 -12
  14. package/src/apis/gcname.js +2 -2
  15. package/src/apis/gcrule.js +16 -10
  16. package/src/apis/getAccess.js +3 -1
  17. package/src/apis/getBotInitialData.js +1 -1
  18. package/src/apis/getThreadInfo.js +1 -1
  19. package/src/apis/listenMqtt.js +72 -3
  20. package/src/apis/markAsSeen.js +7 -9
  21. package/src/apis/nickname.js +67 -50
  22. package/src/apis/pinMessage.js +1 -1
  23. package/src/apis/removeUserFromGroup.js +7 -0
  24. package/src/apis/sendEffect.js +7 -6
  25. package/src/apis/sendMessage.js +6 -2
  26. package/src/apis/setMessageReaction.js +59 -20
  27. package/src/apis/setMessageReactionMqtt.js +96 -56
  28. package/src/apis/setThreadThemeMqtt.js +4 -2
  29. package/src/apis/shareContact.js +37 -31
  30. package/src/apis/theme.js +1 -1
  31. package/src/app/MessengerBot.js +239 -0
  32. package/src/app/broadcast.js +48 -0
  33. package/src/app/config.js +97 -0
  34. package/src/app/createFcaClient.js +179 -0
  35. package/src/app/state.js +117 -0
  36. package/src/app/threadSync.js +106 -0
  37. package/src/app/updateCheck.js +92 -0
  38. package/src/engine/client.js +76 -28
  39. package/src/engine/models/loginHelper.js +5 -0
  40. package/src/utils/antiSuspension.js +10 -10
  41. package/src/utils/auth-helpers.js +17 -1
  42. package/src/utils/clients.js +21 -0
  43. package/src/utils/headers.js +1 -1
  44. package/src/utils/lsRequest.js +176 -0
  45. package/src/utils/rateLimiter.js +7 -3
  46. package/src/utils/tokenRefresh.js +14 -1
@@ -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) {
@@ -583,7 +595,8 @@ const MQTT_DEFAULTS = {
583
595
  autoReconnect: true,
584
596
  watchdogIntervalMs: 60000,
585
597
  staleMs: 300000,
586
- reconnectAfterStop: false
598
+ reconnectAfterStop: false,
599
+ maxReconnectAttempts: 100
587
600
  };
588
601
 
589
602
  function mqttConf(ctx, overrides) {
@@ -812,11 +825,67 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
812
825
  const msg = ((err && err.error) || (err && err.message) || String(err || "")) + detail;
813
826
 
814
827
  if (/Not logged in/i.test(msg)) {
815
- utils.error("MQTT", "Auth error in getSeqID: Not logged in");
828
+ utils.error("MQTT", "Auth error in getSeqID: Not logged in — attempting recovery...");
829
+
830
+ // Step 1: Try token refresh first (fastest, least invasive)
831
+ let tokenRefreshed = false;
832
+ try {
833
+ if (api.tokenRefreshManager && typeof api.tokenRefreshManager.refreshTokens === 'function') {
834
+ utils.log("MQTT", "getSeqID: refreshing tokens before giving up...");
835
+ await api.tokenRefreshManager.refreshTokens(ctx, defaultFuncs, 'https://www.facebook.com');
836
+ tokenRefreshed = true;
837
+ utils.log("MQTT", "getSeqID: token refresh succeeded, scheduling reconnect");
838
+ }
839
+ } catch (refreshErr) {
840
+ utils.warn("MQTT", `getSeqID: token refresh failed: ${refreshErr && refreshErr.message ? refreshErr.message : refreshErr}`);
841
+ }
842
+
843
+ if (tokenRefreshed && ctx.globalOptions.autoReconnect) {
844
+ ctx._reconnectAttempts = Math.max(0, (ctx._reconnectAttempts || 0) - 1);
845
+ const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 3000;
846
+ return scheduleReconnect(baseDelay);
847
+ }
848
+
849
+ // Step 2: Try full auto re-login (email+password) as last resort
850
+ let reloginOk = false;
851
+ try {
852
+ const { globalAutoReLoginManager } = require('../utils/autoReLogin');
853
+ if (globalAutoReLoginManager && globalAutoReLoginManager.isEnabled && globalAutoReLoginManager.isEnabled()) {
854
+ utils.log("MQTT", "getSeqID: attempting auto re-login...");
855
+ reloginOk = await globalAutoReLoginManager.handleSessionExpiry(api, 'https://www.facebook.com', "MQTT getSeqID Not logged in");
856
+ if (reloginOk) {
857
+ utils.log("MQTT", "getSeqID: re-login succeeded, scheduling MQTT reconnect");
858
+ }
859
+ }
860
+ } catch (reloginErr) {
861
+ utils.warn("MQTT", `getSeqID: auto re-login failed: ${reloginErr && reloginErr.message ? reloginErr.message : reloginErr}`);
862
+ }
863
+
864
+ if (reloginOk && ctx.globalOptions.autoReconnect) {
865
+ ctx._reconnectAttempts = 0;
866
+ const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 5000;
867
+ return scheduleReconnect(baseDelay);
868
+ }
869
+
870
+ // Both recovery paths exhausted — emit auth error to signal the user
816
871
  return emitAuthError("not_logged_in", msg);
817
872
  }
818
873
  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
874
  utils.error("MQTT", "Auth error in getSeqID: Session/Login blocked");
875
+
876
+ // Still try token refresh for session expiry before giving up
877
+ try {
878
+ if (api.tokenRefreshManager && typeof api.tokenRefreshManager.refreshTokens === 'function') {
879
+ utils.log("MQTT", "getSeqID: refreshing tokens on session expiry...");
880
+ await api.tokenRefreshManager.refreshTokens(ctx, defaultFuncs, 'https://www.facebook.com');
881
+ if (ctx.globalOptions.autoReconnect) {
882
+ ctx._reconnectAttempts = 0;
883
+ const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 5000;
884
+ return scheduleReconnect(baseDelay);
885
+ }
886
+ }
887
+ } catch (_) {}
888
+
820
889
  return emitAuthError("login_blocked", msg);
821
890
  }
822
891
 
@@ -10,8 +10,8 @@ const utils = require('../utils');
10
10
  module.exports = function (defaultFuncs, api, ctx) {
11
11
  /**
12
12
  * Marks all messages as "seen" up to a specific timestamp.
13
- * @param {number} [seen_timestamp=Date.now()] - The timestamp (in milliseconds) up to which messages should be marked as seen. If a function is provided, it's treated as the callback and the timestamp defaults to the current time.
14
- * @param {Function} [callback] - The callback function.
13
+ * @param {number} [seen_timestamp=Date.now()] - The timestamp (in ms) up to which messages should be marked as seen.
14
+ * @param {Function} [callback] - Optional callback function.
15
15
  * @returns {Promise<void>} A Promise that resolves on success or rejects with an error.
16
16
  */
17
17
  return async function markAsSeen(seen_timestamp, callback) {
@@ -30,11 +30,9 @@ module.exports = function (defaultFuncs, api, ctx) {
30
30
  }
31
31
 
32
32
  if (!callback) {
33
- callback = function (err, friendList) {
34
- if (err) {
35
- return rejectFunc(err);
36
- }
37
- resolveFunc(friendList);
33
+ callback = function (err) {
34
+ if (err) return rejectFunc(err);
35
+ resolveFunc();
38
36
  };
39
37
  }
40
38
 
@@ -56,13 +54,13 @@ module.exports = function (defaultFuncs, api, ctx) {
56
54
  throw resData;
57
55
  }
58
56
 
59
- return callback();
57
+ callback(null);
60
58
  } catch (err) {
61
59
  utils.error("markAsSeen", err);
62
60
  if (utils.getType(err) == "Object" && err.error === "Not logged in.") {
63
61
  ctx.loggedIn = false;
64
62
  }
65
- return callback(err);
63
+ callback(err);
66
64
  }
67
65
 
68
66
  return returnPromise;
@@ -1,19 +1,21 @@
1
1
  "use strict";
2
2
 
3
3
  const utils = require('../utils');
4
+ const { publishLsRequestWithAck, buildLsTask, generateEpochId } = require('../utils/lsRequest');
5
+
6
+ const NICKNAME_VERSION_ID = "25459622483894963";
4
7
 
5
8
  module.exports = function (defaultFuncs, api, ctx) {
6
9
  /**
7
- * Made by Choru Official
8
- * Mqtt
9
- * Sets a nickname for a participant in a Facebook thread via MQTT.
10
+ * Sets a nickname for a participant in a Facebook thread via MQTT with ACK.
11
+ * Uses publishLsRequestWithAck for reliable delivery confirmation.
10
12
  *
11
- * @param {string} nickname The new nickname to set.
13
+ * @param {string} nickname The new nickname to set (empty string to clear).
12
14
  * @param {string} threadID The ID of the thread.
13
- * @param {string} participantID The ID of the participant whose nickname will be changed. Defaults to the current user's ID if not provided or a function.
14
- * @param {Function} [callback] Optional callback function to be invoked upon completion.
15
- * @param {string} [initiatorID] The ID of the user who initiated the nickname change (e.g., from event.senderID).
16
- * @returns {Promise<object>} A promise that resolves with a structured event object on success or rejects on error.
15
+ * @param {string} participantID The ID of the participant. Defaults to the bot's ID.
16
+ * @param {Function} [callback] Optional callback(err, result).
17
+ * @param {string} [initiatorID] The senderID who triggered the change (for event tracking).
18
+ * @returns {Promise<object>}
17
19
  */
18
20
  return function setNickname(nickname, threadID, participantID, callback, initiatorID) {
19
21
  let _callback;
@@ -37,8 +39,7 @@ module.exports = function (defaultFuncs, api, ctx) {
37
39
  _callback = participantID;
38
40
  participantID = ctx.userID;
39
41
  _initiatorID = callback;
40
- }
41
- else if (utils.getType(callback) === "string") {
42
+ } else if (utils.getType(callback) === "string") {
42
43
  _initiatorID = callback;
43
44
  _callback = undefined;
44
45
  } else {
@@ -65,66 +66,82 @@ module.exports = function (defaultFuncs, api, ctx) {
65
66
  }
66
67
 
67
68
  _initiatorID = _initiatorID || ctx.userID;
68
-
69
69
  threadID = threadID || ctx.threadID;
70
70
  participantID = participantID || ctx.userID;
71
71
 
72
72
  if (!threadID) {
73
- return _callback(new Error("threadID is required to set a nickname."));
73
+ _callback(new Error("threadID is required to set a nickname."));
74
+ return returnPromise;
74
75
  }
75
76
  if (typeof nickname !== 'string') {
76
- return _callback(new Error("nickname must be a string."));
77
+ _callback(new Error("nickname must be a string."));
78
+ return returnPromise;
77
79
  }
78
-
79
- if (!ctx.mqttClient) {
80
- return _callback(new Error("Not connected to MQTT"));
80
+ if (!ctx.mqttClient || !ctx.mqttClient.connected) {
81
+ _callback(new Error("Not connected to MQTT — call listenMqtt() first before using setNickname."));
82
+ return returnPromise;
81
83
  }
82
84
 
83
- ctx.wsReqNumber += 1;
84
- ctx.wsTaskNumber += 1;
85
+ if (typeof ctx.wsReqNumber !== "number") ctx.wsReqNumber = 0;
86
+ if (typeof ctx.wsTaskNumber !== "number") ctx.wsTaskNumber = 0;
85
87
 
86
- const queryPayload = {
88
+ const requestId = ++ctx.wsReqNumber;
89
+ const task = buildLsTask(ctx, "44", "thread_participant_nickname", {
87
90
  thread_key: threadID.toString(),
88
91
  contact_id: participantID.toString(),
89
92
  nickname: nickname,
90
- sync_group: 1,
91
- };
93
+ sync_group: 1
94
+ });
92
95
 
93
- const query = {
94
- failure_count: null,
95
- label: '44',
96
- payload: JSON.stringify(queryPayload),
97
- queue_name: 'thread_participant_nickname',
98
- task_id: ctx.wsTaskNumber,
96
+ const envelope = {
97
+ app_id: String(ctx.appID || ctx.mqttAppID || "2220391788200892"),
98
+ payload: JSON.stringify({
99
+ epoch_id: generateEpochId(),
100
+ tasks: [task],
101
+ version_id: NICKNAME_VERSION_ID
102
+ }),
103
+ request_id: requestId,
104
+ type: 3
99
105
  };
100
106
 
101
- const context = {
102
- app_id: ctx.appID || "2220391788200892",
103
- payload: {
104
- epoch_id: parseInt(utils.generateOfflineThreadingID()),
105
- tasks: [query],
106
- version_id: '24631415369801570',
107
- },
108
- request_id: ctx.wsReqNumber,
109
- type: 3,
110
- };
111
- context.payload = JSON.stringify(context.payload);
112
-
113
- ctx.mqttClient.publish('/ls_req', JSON.stringify(context), { qos: 1, retain: false }, (err) => {
114
- if (err) {
115
- return _callback(new Error(`MQTT publish failed for setNickname: ${err.message || err}`));
116
- }
117
-
118
- const nicknameChangeEvent = {
119
- type: "thread_nickname_update",
120
- threadID: threadID,
121
- participantID: participantID,
107
+ publishLsRequestWithAck({
108
+ client: ctx.mqttClient,
109
+ content: envelope,
110
+ requestId,
111
+ timeoutMs: 12000,
112
+ extract: (message) => ({
113
+ type: "thread_nickname_update",
114
+ threadID,
115
+ participantID,
122
116
  newNickname: nickname,
123
117
  senderID: _initiatorID,
124
118
  BotID: ctx.userID,
125
119
  timestamp: Date.now(),
126
- };
127
- _callback(null, nicknameChangeEvent);
120
+ success: true,
121
+ ack: message.payload
122
+ })
123
+ }).then((result) => {
124
+ _callback(null, result);
125
+ }).catch((err) => {
126
+ // ACK timeout is non-fatal — the nickname was likely applied anyway.
127
+ // Build a best-effort result so callers still get a useful object.
128
+ if (err && err.message && err.message.includes("Timeout waiting for LS ACK")) {
129
+ utils.warn("setNickname", "ACK timed out — nickname may still have been applied");
130
+ _callback(null, {
131
+ type: "thread_nickname_update",
132
+ threadID,
133
+ participantID,
134
+ newNickname: nickname,
135
+ senderID: _initiatorID,
136
+ BotID: ctx.userID,
137
+ timestamp: Date.now(),
138
+ success: true,
139
+ ackTimeout: true
140
+ });
141
+ } else {
142
+ utils.error("setNickname", err && err.message ? err.message : err);
143
+ _callback(err instanceof Error ? err : new Error(String(err && err.message ? err.message : err)));
144
+ }
128
145
  });
129
146
 
130
147
  return returnPromise;
@@ -83,7 +83,7 @@ module.exports = function (defaultFuncs, api, ctx) {
83
83
  }
84
84
  }
85
85
 
86
- if (!ctx.mqttClient) throw new Error("MQTT not connected.");
86
+ if (!ctx.mqttClient || !ctx.mqttClient.connected) throw new Error("MQTT not connected.");
87
87
  if (!threadID || !messageID) throw new Error(`"${action}" requires threadID and messageID.`);
88
88
 
89
89
  const epoch_id = parseInt(utils.generateOfflineThreadingID());
@@ -16,6 +16,13 @@ module.exports = (defaultFuncs, api, ctx) => {
16
16
  if (err) return rejectFunc(err);
17
17
  resolveFunc(result);
18
18
  };
19
+ } else {
20
+ const _userCb = callback;
21
+ callback = (err, result) => {
22
+ if (err) { _userCb(err); return rejectFunc(err); }
23
+ _userCb(null, result);
24
+ resolveFunc(result);
25
+ };
19
26
  }
20
27
 
21
28
  try {
@@ -135,6 +135,7 @@ function buildMqttPayload(effectTag, msgObj, threadID, ctx) {
135
135
  }
136
136
 
137
137
  if (typeof ctx.wsReqNumber !== 'number') ctx.wsReqNumber = 0;
138
+ if (typeof ctx.wsTaskNumber !== 'number') ctx.wsTaskNumber = 0;
138
139
  const request_id = ++ctx.wsReqNumber;
139
140
 
140
141
  const content = {
@@ -145,7 +146,7 @@ function buildMqttPayload(effectTag, msgObj, threadID, ctx) {
145
146
  label: '46',
146
147
  payload: JSON.stringify(sendPayload),
147
148
  queue_name: tid,
148
- task_id: 400,
149
+ task_id: ++ctx.wsTaskNumber,
149
150
  failure_count: null,
150
151
  },
151
152
  {
@@ -156,7 +157,7 @@ function buildMqttPayload(effectTag, msgObj, threadID, ctx) {
156
157
  sync_group: 1,
157
158
  }),
158
159
  queue_name: tid,
159
- task_id: 401,
160
+ task_id: ++ctx.wsTaskNumber,
160
161
  failure_count: null,
161
162
  },
162
163
  ],
@@ -213,9 +214,9 @@ module.exports = function (defaultFuncs, api, ctx) {
213
214
  }
214
215
 
215
216
  try {
216
- if (!effectName) return callback(new Error('effectName is required.'));
217
- if (message === undefined || message === null) return callback(new Error('message is required.'));
218
- if (!threadID) return callback(new Error('threadID is required.'));
217
+ if (!effectName) { callback(new Error('effectName is required.')); return returnPromise; }
218
+ if (message === undefined || message === null) { callback(new Error('message is required.')); return returnPromise; }
219
+ if (!threadID) { callback(new Error('threadID is required.')); return returnPromise; }
219
220
 
220
221
  const effectTag = resolveEffect(effectName);
221
222
  utils.log('sendEffect', `Effect "${effectTag}" → thread ${threadID}`);
@@ -280,7 +281,7 @@ module.exports = function (defaultFuncs, api, ctx) {
280
281
  timestamp: v.timestamp || p.timestamp,
281
282
  }), { threadID, messageID: messageAndOTID, timestamp: httpTs });
282
283
 
283
- return callback(null, { ...msgInfo, effect: effectTag, method: 'http' });
284
+ callback(null, { ...msgInfo, effect: effectTag, method: 'http' });
284
285
 
285
286
  } catch (err) {
286
287
  utils.error('sendEffect', err.message || err);
@@ -220,7 +220,11 @@ module.exports = (defaultFuncs, api, ctx) => {
220
220
  throw new Error(JSON.stringify(resData));
221
221
  }
222
222
  const messageInfo = resData.payload.actions.reduce((p, v) => {
223
- return { threadID: v.thread_fbid, messageID: v.message_id, timestamp: v.timestamp } || p;
223
+ return {
224
+ threadID: v.thread_fbid || (p && p.threadID),
225
+ messageID: v.message_id || (p && p.messageID),
226
+ timestamp: v.timestamp || (p && p.timestamp),
227
+ };
224
228
  }, null);
225
229
  return messageInfo;
226
230
  }
@@ -259,7 +263,7 @@ module.exports = (defaultFuncs, api, ctx) => {
259
263
  return callback(new Error("ThreadID should be of type number, string, or array and not " + threadIDType + "."));
260
264
  }
261
265
  if (replyToMessage && messageIDType !== 'String') {
262
- return callback(new Error("MessageID should be of type string and not " + threadIDType + "."));
266
+ return callback(new Error("MessageID should be of type string and not " + messageIDType + "."));
263
267
  }
264
268
 
265
269
  if (ctx.validator && !ctx.validator.isValidMessage(msg)) {
@@ -2,27 +2,66 @@
2
2
 
3
3
  const utils = require('../utils');
4
4
 
5
-
6
5
  module.exports = function (defaultFuncs, api, ctx) {
7
- return async (reaction, messageID) => {
8
- if (reaction === undefined || reaction === null) throw new Error("Please enter a valid emoji.");
9
- const action = reaction === "" ? "REMOVE_REACTION" : "ADD_REACTION";
10
- const defData = await defaultFuncs.postFormData("https://www.facebook.com/webgraphql/mutation/", ctx.jar, {
11
- doc_id: "1491398900900362",
12
- variables: JSON.stringify({
13
- data: {
14
- client_mutation_id: ctx.clientMutationId++,
15
- actor_id: ctx.userID,
16
- action,
17
- message_id: messageID,
18
- reaction
19
- }
20
- }),
21
- dpr: 1
22
- }, {});
23
- const resData = await utils.parseAndCheckLogin(ctx, defaultFuncs)(defData);
24
- if (!resData) {
25
- throw new Error("setMessageReactionLegacy returned empty object.");
6
+ return async function setMessageReaction(reaction, messageID, callback) {
7
+ let resolveFunc = () => {};
8
+ let rejectFunc = () => {};
9
+ const returnPromise = new Promise((resolve, reject) => {
10
+ resolveFunc = resolve;
11
+ rejectFunc = reject;
12
+ });
13
+
14
+ if (!callback) {
15
+ callback = (err, data) => {
16
+ if (err) return rejectFunc(err);
17
+ resolveFunc(data);
18
+ };
19
+ } else {
20
+ const _userCb = callback;
21
+ callback = (err, data) => {
22
+ if (err) { _userCb(err); return rejectFunc(err); }
23
+ _userCb(null, data);
24
+ resolveFunc(data);
25
+ };
26
26
  }
27
+
28
+ try {
29
+ if (reaction === undefined || reaction === null) {
30
+ throw new Error("Please enter a valid emoji.");
31
+ }
32
+
33
+ const action = reaction === "" ? "REMOVE_REACTION" : "ADD_REACTION";
34
+
35
+ const defData = await defaultFuncs.postFormData(
36
+ "https://www.facebook.com/webgraphql/mutation/",
37
+ ctx.jar,
38
+ {
39
+ doc_id: "3360163564070970",
40
+ variables: JSON.stringify({
41
+ data: {
42
+ client_mutation_id: ctx.clientMutationId++,
43
+ actor_id: ctx.userID,
44
+ action,
45
+ message_id: messageID,
46
+ reaction
47
+ }
48
+ }),
49
+ dpr: 1
50
+ },
51
+ {}
52
+ );
53
+
54
+ const resData = await utils.parseAndCheckLogin(ctx, defaultFuncs)(defData);
55
+ if (!resData) {
56
+ throw new Error("setMessageReaction returned empty object.");
57
+ }
58
+
59
+ callback(null, { success: true, action, messageID });
60
+ } catch (err) {
61
+ utils.error("setMessageReaction", err);
62
+ callback(err instanceof Error ? err : new Error(String(err)));
63
+ }
64
+
65
+ return returnPromise;
27
66
  };
28
67
  };
@@ -1,61 +1,101 @@
1
+ "use strict";
1
2
 
2
- 'use strict';
3
-
3
+ const { publishLsRequestWithAck } = require('../utils/lsRequest');
4
4
  const utils = require('../utils');
5
5
 
6
- function isCallable(func) {
7
-   try {
8
-     Reflect.apply(func, null, []);
9
-     return true;
10
-   } catch (error) {
11
-     return false;
12
-   }
13
- }
14
-
15
6
  module.exports = function (defaultFuncs, api, ctx) {
16
-   return function setMessageReactionMqtt(reaction, messageID, threadID) {
17
-     if (!ctx.mqttClient) {
18
-       throw new Error('Not connected to MQTT');
19
-     }
20
-
21
-     ctx.wsReqNumber += 1;
22
-     ctx.wsTaskNumber += 1;
23
-
24
-     const taskPayload = {
25
-       thread_key: threadID,
26
-       timestamp_ms: Date.now(),
27
-       message_id: messageID,
28
-       reaction,
29
-       actor_id: ctx.userID,
30
-       reaction_style: null,
31
-       sync_group: 1,
32
-       send_attribution: Math.random() < 0.5 ? 65537 : 524289
33
-     };
34
-
35
-     const task = {
36
-       failure_count: null,
37
-       label: '29',
38
-       payload: JSON.stringify(taskPayload),
39
-       queue_name: JSON.stringify(['reaction', messageID]),
40
-       task_id: ctx.wsTaskNumber
41
-     };
42
-
43
-     const content = {
44
-       app_id: '2220391788200892',
45
-       payload: JSON.stringify({
46
-         data_trace_id: null,
47
-         epoch_id: parseInt(utils.generateOfflineThreadingID()),
48
-         tasks: [task],
49
-         version_id: '7158486590867448',
50
-       }),
51
-       request_id: ctx.wsReqNumber,
52
-       type: 3,
53
-     };
54
-
55
-     /*if (isCallable(callback)) {
56
-       ctx.reqCallbacks[ctx.wsReqNumber] = callback;
57
-     }*/
58
-
59
-     ctx.mqttClient.publish('/ls_req', JSON.stringify(content), { qos: 1, retain: false });
60
-   };
7
+ /**
8
+ * Sets or clears a reaction on a Messenger message via MQTT with ACK.
9
+ *
10
+ * @param {string} reaction Emoji string (e.g. "😍") or empty string to clear.
11
+ * @param {string} messageID The message ID.
12
+ * @param {string} threadID The thread ID.
13
+ * @param {Function} [callback] Optional callback(err, result).
14
+ * @returns {Promise}
15
+ */
16
+ return function setMessageReactionMqtt(reaction, messageID, threadID, callback) {
17
+ let resolveFunc, rejectFunc;
18
+ const returnPromise = new Promise((resolve, reject) => {
19
+ resolveFunc = resolve;
20
+ rejectFunc = reject;
21
+ });
22
+
23
+ const cb = (typeof callback === "function")
24
+ ? (err, data) => { callback(err, data); if (err) rejectFunc(err); else resolveFunc(data); }
25
+ : (err, data) => { if (err) rejectFunc(err); else resolveFunc(data); };
26
+
27
+ if (!ctx.mqttClient || !ctx.mqttClient.connected) {
28
+ const err = new Error("Not connected to MQTT — call listenMqtt() first");
29
+ return cb(err), returnPromise;
30
+ }
31
+
32
+ if (typeof ctx.wsReqNumber !== "number") ctx.wsReqNumber = 0;
33
+ if (typeof ctx.wsTaskNumber !== "number") ctx.wsTaskNumber = 0;
34
+
35
+ const requestId = ++ctx.wsReqNumber;
36
+ const taskId = ++ctx.wsTaskNumber;
37
+
38
+ const taskPayload = {
39
+ thread_key: threadID,
40
+ timestamp_ms: Date.now(),
41
+ message_id: messageID,
42
+ reaction: reaction || "",
43
+ actor_id: ctx.userID,
44
+ reaction_style: null,
45
+ sync_group: 1,
46
+ send_attribution: Math.random() < 0.5 ? 65537 : 524289
47
+ };
48
+
49
+ const task = {
50
+ failure_count: null,
51
+ label: "29",
52
+ payload: JSON.stringify(taskPayload),
53
+ queue_name: JSON.stringify(["reaction", messageID]),
54
+ task_id: taskId
55
+ };
56
+
57
+ const content = {
58
+ app_id: "2220391788200892",
59
+ payload: JSON.stringify({
60
+ data_trace_id: null,
61
+ epoch_id: parseInt(utils.generateOfflineThreadingID()),
62
+ tasks: [task],
63
+ version_id: "7158486590867448"
64
+ }),
65
+ request_id: requestId,
66
+ type: 3
67
+ };
68
+
69
+ publishLsRequestWithAck({
70
+ client: ctx.mqttClient,
71
+ content,
72
+ requestId,
73
+ timeoutMs: 10000,
74
+ extract: (message) => ({
75
+ success: true,
76
+ messageID,
77
+ threadID,
78
+ reaction: reaction || "",
79
+ ack: message.payload
80
+ })
81
+ }).then((result) => {
82
+ cb(null, result);
83
+ }).catch((err) => {
84
+ if (err && err.message && err.message.includes("Timeout waiting for LS ACK")) {
85
+ utils.warn("setMessageReactionMqtt", "ACK timed out — reaction may still have been applied");
86
+ cb(null, {
87
+ success: true,
88
+ messageID,
89
+ threadID,
90
+ reaction: reaction || "",
91
+ ackTimeout: true
92
+ });
93
+ } else {
94
+ utils.error("setMessageReactionMqtt", err && err.message ? err.message : err);
95
+ cb(err instanceof Error ? err : new Error(String(err && err.message ? err.message : err)));
96
+ }
97
+ });
98
+
99
+ return returnPromise;
100
+ };
61
101
  };