@lazyneoaz/nkxchat 1.0.2 → 1.0.4

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/nkxchat",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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.",
@@ -39,6 +39,7 @@
39
39
  "nkxchat"
40
40
  ],
41
41
  "dependencies": {
42
+ "@dongdev/fca-unofficial": "^4.0.3",
42
43
  "axios": "^1.13.5",
43
44
  "axios-cookiejar-support": "^4.0.7",
44
45
  "bluebird": "^3.7.2",
@@ -593,9 +593,72 @@ function mqttConf(ctx, overrides) {
593
593
  return ctx._mqttOpt;
594
594
  }
595
595
 
596
+ function createMiddlewareSystem() {
597
+ const stack = [];
598
+ let nextId = 0;
599
+ function use(nameOrFn, fn) {
600
+ let name, middlewareFn;
601
+ if (typeof nameOrFn === "string" && typeof fn === "function") {
602
+ name = nameOrFn; middlewareFn = fn;
603
+ } else if (typeof nameOrFn === "function") {
604
+ middlewareFn = nameOrFn; name = `middleware_${nextId++}`;
605
+ } else throw new Error("Middleware must be a function or (name, function)");
606
+ const entry = { name, fn: middlewareFn, enabled: true };
607
+ stack.push(entry);
608
+ return function remove() {
609
+ const i = stack.indexOf(entry);
610
+ if (i !== -1) stack.splice(i, 1);
611
+ };
612
+ }
613
+ function remove(identifier) {
614
+ const i = typeof identifier === "string"
615
+ ? stack.findIndex(e => e.name === identifier)
616
+ : stack.findIndex(e => e.fn === identifier);
617
+ if (i !== -1) { stack.splice(i, 1); return true; }
618
+ return false;
619
+ }
620
+ function clear() { stack.length = 0; }
621
+ function list() { return stack.filter(e => e.enabled).map(e => e.name); }
622
+ function setEnabled(name, enabled) {
623
+ const e = stack.find(e => e.name === name);
624
+ if (e) { e.enabled = enabled; return true; }
625
+ return false;
626
+ }
627
+ function process(event, finalCallback) {
628
+ const active = stack.filter(e => e.enabled);
629
+ if (!active.length) return finalCallback(null, event);
630
+ let idx = 0;
631
+ function next(err) {
632
+ if (err && err !== false && err !== null) return finalCallback(err, null);
633
+ if (err === false || err === null) return finalCallback(null, null);
634
+ if (idx >= active.length) return finalCallback(null, event);
635
+ const mw = active[idx++];
636
+ try {
637
+ const r = mw.fn(event, next);
638
+ if (r && typeof r.then === "function") r.then(() => next()).catch(e => next(e));
639
+ else if (r === false || r === null) finalCallback(null, null);
640
+ } catch (e) { next(e); }
641
+ }
642
+ next();
643
+ }
644
+ function wrapCallback(callback) {
645
+ return function(err, event) {
646
+ if (err) return callback(err, null);
647
+ if (!event) return callback(null, null);
648
+ process(event, (mwErr, processed) => {
649
+ if (mwErr) return callback(mwErr, null);
650
+ if (processed === null) return;
651
+ callback(null, processed);
652
+ });
653
+ };
654
+ }
655
+ return { use, remove, clear, list, setEnabled, process, wrapCallback, get count() { return stack.filter(e => e.enabled).length; } };
656
+ }
657
+
596
658
  module.exports = (defaultFuncs, api, ctx, opts) => {
597
659
  const identity = () => {};
598
660
  let globalCallback = identity;
661
+ if (!ctx._middleware) ctx._middleware = createMiddlewareSystem();
599
662
 
600
663
  function emitAuthError(reason, detail) {
601
664
  try { if (ctx._autoCycleTimer) clearTimeout(ctx._autoCycleTimer); } catch (_) { }
@@ -930,7 +993,7 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
930
993
 
931
994
  const msgEmitter = new MessageEmitter();
932
995
 
933
- globalCallback = callback || function(error, message) {
996
+ const baseCallback = callback || function(error, message) {
934
997
  if (error) {
935
998
  utils.error("MQTT", "Emit error");
936
999
  return msgEmitter.emit("error", error);
@@ -941,6 +1004,12 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
941
1004
  msgEmitter.emit("message", message);
942
1005
  };
943
1006
 
1007
+ globalCallback = ctx._middleware && ctx._middleware.count
1008
+ ? ctx._middleware.wrapCallback(baseCallback)
1009
+ : baseCallback;
1010
+
1011
+ ctx._emitter = msgEmitter;
1012
+
944
1013
  ctx._listeningActive = true;
945
1014
  ctx._lastListenCallback = callback || null;
946
1015
 
@@ -990,6 +1059,24 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
990
1059
 
991
1060
  api.stopListening = msgEmitter.stopListening;
992
1061
  api.stopListeningAsync = msgEmitter.stopListeningAsync;
1062
+
1063
+ api.useMiddleware = function(nameOrFn, fn) {
1064
+ const remove = ctx._middleware.use(nameOrFn, fn);
1065
+ globalCallback = ctx._middleware.wrapCallback(baseCallback || identity);
1066
+ return remove;
1067
+ };
1068
+ api.removeMiddleware = function(identifier) {
1069
+ const ok = ctx._middleware.remove(identifier);
1070
+ if (!ctx._middleware.count) globalCallback = baseCallback || identity;
1071
+ return ok;
1072
+ };
1073
+ api.clearMiddleware = function() {
1074
+ ctx._middleware.clear();
1075
+ globalCallback = baseCallback || identity;
1076
+ };
1077
+ api.listMiddleware = function() { return ctx._middleware.list(); };
1078
+ api.setMiddlewareEnabled = function(name, enabled) { return ctx._middleware.setEnabled(name, enabled); };
1079
+
993
1080
  return msgEmitter;
994
1081
  };
995
1082
  };
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+
3
+ module.exports = function (defaultFuncs, api, ctx) {
4
+ const scheduledMessages = new Map();
5
+ let nextId = 1;
6
+
7
+ function toTimestamp(when) {
8
+ if (when instanceof Date) return when.getTime();
9
+ if (typeof when === "number") return when;
10
+ if (typeof when === "string") return new Date(when).getTime();
11
+ return NaN;
12
+ }
13
+
14
+ function scheduleMessage(message, threadID, when, options) {
15
+ options = options || {};
16
+ const timestamp = toTimestamp(when);
17
+ if (isNaN(timestamp)) throw new Error("Invalid 'when'. Must be Date, number (ms timestamp), or ISO string.");
18
+ const now = Date.now();
19
+ if (timestamp <= now) throw new Error("Scheduled time must be in the future.");
20
+
21
+ const id = `scheduled_${nextId++}_${now}`;
22
+ const delay = timestamp - now;
23
+
24
+ const scheduled = {
25
+ id,
26
+ message,
27
+ threadID,
28
+ timestamp,
29
+ createdAt: now,
30
+ options: {
31
+ replyMessageID: options.replyMessageID || null,
32
+ callback: options.callback || null,
33
+ },
34
+ cancelled: false,
35
+ timeout: null,
36
+ };
37
+
38
+ scheduled.timeout = setTimeout(() => {
39
+ if (scheduled.cancelled) return;
40
+ const sendFn = api.sendMessage || api.sendMessageMqtt;
41
+ if (!sendFn) return;
42
+ Promise.resolve(
43
+ sendFn(message, threadID, scheduled.options.callback || (() => {}), scheduled.options.replyMessageID)
44
+ ).then(() => {
45
+ scheduledMessages.delete(id);
46
+ }).catch(() => {
47
+ scheduledMessages.delete(id);
48
+ });
49
+ }, delay);
50
+
51
+ scheduledMessages.set(id, scheduled);
52
+ return id;
53
+ }
54
+
55
+ function cancelScheduledMessage(id) {
56
+ const s = scheduledMessages.get(id);
57
+ if (!s || s.cancelled) return false;
58
+ clearTimeout(s.timeout);
59
+ s.cancelled = true;
60
+ scheduledMessages.delete(id);
61
+ return true;
62
+ }
63
+
64
+ function getScheduledMessage(id) {
65
+ const s = scheduledMessages.get(id);
66
+ if (!s || s.cancelled) return null;
67
+ return {
68
+ id: s.id,
69
+ message: s.message,
70
+ threadID: s.threadID,
71
+ timestamp: s.timestamp,
72
+ createdAt: s.createdAt,
73
+ options: { ...s.options },
74
+ timeUntilSend: s.timestamp - Date.now(),
75
+ };
76
+ }
77
+
78
+ function listScheduledMessages() {
79
+ const now = Date.now();
80
+ return Array.from(scheduledMessages.values())
81
+ .filter(s => !s.cancelled)
82
+ .map(s => ({
83
+ id: s.id,
84
+ message: s.message,
85
+ threadID: s.threadID,
86
+ timestamp: s.timestamp,
87
+ createdAt: s.createdAt,
88
+ options: { ...s.options },
89
+ timeUntilSend: s.timestamp - now,
90
+ }))
91
+ .sort((a, b) => a.timestamp - b.timestamp);
92
+ }
93
+
94
+ function cancelAllScheduledMessages() {
95
+ let count = 0;
96
+ for (const id of Array.from(scheduledMessages.keys())) {
97
+ if (cancelScheduledMessage(id)) count++;
98
+ }
99
+ return count;
100
+ }
101
+
102
+ function getScheduledCount() {
103
+ return scheduledMessages.size;
104
+ }
105
+
106
+ const cleanupInterval = setInterval(() => {
107
+ const now = Date.now();
108
+ for (const [id, s] of scheduledMessages.entries()) {
109
+ if (s.cancelled || s.timestamp < now) scheduledMessages.delete(id);
110
+ }
111
+ }, 5 * 60 * 1000);
112
+
113
+ function destroy() {
114
+ clearInterval(cleanupInterval);
115
+ return cancelAllScheduledMessages();
116
+ }
117
+
118
+ ctx._scheduler = { destroy };
119
+
120
+ return {
121
+ scheduleMessage,
122
+ cancelScheduledMessage,
123
+ getScheduledMessage,
124
+ listScheduledMessages,
125
+ cancelAllScheduledMessages,
126
+ getScheduledCount,
127
+ destroy,
128
+ };
129
+ };
@@ -13,38 +13,26 @@ const allowedProperties = {
13
13
  mentions: true,
14
14
  location: true,
15
15
  effect: true,
16
+ replyToMessage: true,
16
17
  };
17
18
 
18
19
  module.exports = (defaultFuncs, api, ctx) => {
19
- function getThreadCache() {
20
- if (!ctx.threadTypeCache) ctx.threadTypeCache = Object.create(null);
21
- return ctx.threadTypeCache;
22
- }
23
-
24
- async function isGroupThread(threadID, explicitIsGroup) {
25
- if (utils.getType(explicitIsGroup) === "Boolean") return !!explicitIsGroup;
26
- const tid = threadID.toString();
27
- const cache = getThreadCache();
28
- if (Object.prototype.hasOwnProperty.call(cache, tid)) return !!cache[tid];
29
- try {
30
- const info = await api.getThreadInfo(tid);
31
- cache[tid] = !!info.isGroup;
32
- return !!info.isGroup;
33
- } catch (_) {
34
- const fallback = tid.length >= 16;
35
- cache[tid] = fallback;
36
- return fallback;
37
- }
20
+ async function getUrl(url) {
21
+ const resData = await defaultFuncs.post(
22
+ "https://www.facebook.com/message_share_attachment/fromURI/",
23
+ ctx.jar,
24
+ { image_height: 960, image_width: 960, uri: url }
25
+ ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
26
+ if (!resData || resData.error || !resData.payload) throw new Error("Invalid url");
27
+ return resData.payload.share_data.share_params;
38
28
  }
39
29
 
40
30
  function detectAttachmentType(attachment) {
41
- const path = attachment.path || '';
42
- const ext = path.toLowerCase().split('.').pop();
43
-
31
+ const p = attachment.path || '';
32
+ const ext = p.toLowerCase().split('.').pop();
44
33
  const audioTypes = ['mp3', 'wav', 'aac', 'm4a', 'ogg', 'opus', 'flac'];
45
34
  const videoTypes = ['mp4', 'mov', 'avi', 'mkv', 'webm', 'wmv', 'flv'];
46
35
  const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
47
-
48
36
  if (audioTypes.includes(ext)) return { voice_clip: "true" };
49
37
  if (videoTypes.includes(ext)) return { video: "true" };
50
38
  if (imageTypes.includes(ext)) return { image: "true" };
@@ -63,7 +51,6 @@ module.exports = (defaultFuncs, api, ctx) => {
63
51
  {},
64
52
  { ...ctx, requestThreadID: threadIDHint }
65
53
  ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
66
-
67
54
  if (oksir.error) throw new Error(JSON.stringify(oksir));
68
55
  return oksir.payload.metadata[0];
69
56
  }
@@ -82,24 +69,122 @@ module.exports = (defaultFuncs, api, ctx) => {
82
69
  return uploads;
83
70
  }
84
71
 
85
- async function getUrl(url) {
86
- const resData = await defaultFuncs.post(
87
- "https://www.facebook.com/message_share_attachment/fromURI/",
88
- ctx.jar,
89
- { image_height: 960, image_width: 960, uri: url }
90
- ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
91
- if (!resData || resData.error || !resData.payload) throw new Error("Invalid url");
92
- return resData.payload.share_data.share_params;
72
+ function getThreadCache() {
73
+ if (!ctx.threadTypeCache) ctx.threadTypeCache = Object.create(null);
74
+ return ctx.threadTypeCache;
93
75
  }
94
76
 
95
- async function sendContent(form, threadID, isSingleUser, messageAndOTID) {
96
- if (utils.getType(threadID) === "Array") {
97
- for (let i = 0; i < threadID.length; i++) {
98
- form["specific_to_list[" + i + "]"] = "fbid:" + threadID[i];
77
+ async function isGroupThread(threadID, explicitIsGroup) {
78
+ if (utils.getType(explicitIsGroup) === "Boolean") return !!explicitIsGroup;
79
+ const tid = threadID.toString();
80
+ const cache = getThreadCache();
81
+ if (Object.prototype.hasOwnProperty.call(cache, tid)) return !!cache[tid];
82
+ try {
83
+ const info = await api.getThreadInfo(tid);
84
+ cache[tid] = !!info.isGroup;
85
+ return !!info.isGroup;
86
+ } catch (_) {
87
+ const fallback = tid.length >= 16;
88
+ cache[tid] = fallback;
89
+ return fallback;
90
+ }
91
+ }
92
+
93
+ async function sendViaHttp(msg, threadID, replyToMessage, isGroup) {
94
+ const isSingleUser = !(await isGroupThread(threadID, isGroup));
95
+ let messageAndOTID = utils.generateOfflineThreadingID();
96
+ let form = {
97
+ client: "mercury",
98
+ action_type: "ma-type:user-generated-message",
99
+ author: "fbid:" + ctx.userID,
100
+ timestamp: Date.now(),
101
+ timestamp_absolute: "Today",
102
+ timestamp_relative: utils.generateTimestampRelative(),
103
+ timestamp_time_passed: "0",
104
+ is_unread: false,
105
+ is_cleared: false,
106
+ is_forward: false,
107
+ is_filtered_content: false,
108
+ is_filtered_content_bh: false,
109
+ is_filtered_content_account: false,
110
+ is_filtered_content_quasar: false,
111
+ is_filtered_content_invalid_app: false,
112
+ is_spoof_warning: false,
113
+ source: "source:chat:web",
114
+ "source_tags[0]": "source:chat",
115
+ ...(msg.body && { body: msg.body }),
116
+ html_body: false,
117
+ ui_push_phase: "V3",
118
+ status: "0",
119
+ offline_threading_id: messageAndOTID,
120
+ message_id: messageAndOTID,
121
+ threading_id: utils.generateThreadingID(ctx.clientID),
122
+ "ephemeral_ttl_mode:": "0",
123
+ manual_retry_cnt: "0",
124
+ has_attachment: !!(msg.attachment || msg.url || msg.sticker),
125
+ signatureID: utils.getSignatureID(),
126
+ ...(replyToMessage && { replied_to_message_id: replyToMessage })
127
+ };
128
+
129
+ if (msg.location) {
130
+ if (!msg.location.latitude || !msg.location.longitude) throw new Error("location property needs both latitude and longitude");
131
+ form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
132
+ form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
133
+ form["location_attachment[is_current_location]"] = !!msg.location.current;
134
+ }
135
+ if (msg.sticker) form["sticker_id"] = msg.sticker;
136
+ if (msg.effect) {
137
+ const effectTag = String(msg.effect).toUpperCase().replace(/[\s\-]+/g, '_');
138
+ form.has_lightweight_action = true;
139
+ form['lightweight_action_attached[message_id]'] = messageAndOTID;
140
+ form['lightweight_action_attached[messaging_tag]'] = 'fb.messaging.effects.' + effectTag;
141
+ }
142
+ if (msg.attachment) {
143
+ form.image_ids = [];
144
+ form.gif_ids = [];
145
+ form.file_ids = [];
146
+ form.video_ids = [];
147
+ form.audio_ids = [];
148
+ if (utils.getType(msg.attachment) !== "Array") msg.attachment = [msg.attachment];
149
+ const files = await uploadAttachment(msg.attachment, threadID);
150
+ files.forEach(file => {
151
+ const type = Object.keys(file)[0];
152
+ form["" + type + "s"].push(file[type]);
153
+ });
154
+ }
155
+ if (msg.url) {
156
+ form["shareable_attachment[share_type]"] = "100";
157
+ const params = await getUrl(msg.url);
158
+ form["shareable_attachment[share_params]"] = params;
159
+ }
160
+ if (msg.emoji) {
161
+ if (!msg.emojiSize) msg.emojiSize = "medium";
162
+ if (msg.emojiSize !== "small" && msg.emojiSize !== "medium" && msg.emojiSize !== "large") throw new Error("emojiSize property is invalid");
163
+ if (form.body && form.body !== "") throw new Error("body is not empty");
164
+ form.body = msg.emoji;
165
+ form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
166
+ }
167
+ if (msg.mentions) {
168
+ for (let i = 0; i < msg.mentions.length; i++) {
169
+ const mention = msg.mentions[i];
170
+ const tag = mention.tag;
171
+ if (typeof tag !== "string") throw new Error("Mention tags must be strings.");
172
+ const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
173
+ if (offset < 0) utils.warn("handleMention", 'Mention for "' + tag + '" not found in message string.');
174
+ const id = mention.id || 0;
175
+ const emptyChar = '\u200E';
176
+ form["body"] = emptyChar + msg.body;
177
+ form["profile_xmd[" + i + "][offset]"] = offset + 1;
178
+ form["profile_xmd[" + i + "][length]"] = tag.length;
179
+ form["profile_xmd[" + i + "][id]"] = id;
180
+ form["profile_xmd[" + i + "][type]"] = "p";
99
181
  }
182
+ }
183
+
184
+ if (utils.getType(threadID) === "Array") {
185
+ for (let i = 0; i < threadID.length; i++) form["specific_to_list[" + i + "]"] = "fbid:" + threadID[i];
100
186
  form["specific_to_list[" + threadID.length + "]"] = "fbid:" + ctx.userID;
101
187
  form["client_thread_id"] = "root:" + messageAndOTID;
102
- utils.log("sendMessage", "Sending message to multiple users: " + threadID);
103
188
  } else {
104
189
  if (isSingleUser) {
105
190
  form["specific_to_list[0]"] = "fbid:" + threadID;
@@ -110,7 +195,6 @@ module.exports = (defaultFuncs, api, ctx) => {
110
195
  form["thread_fbid"] = threadID;
111
196
  }
112
197
  }
113
-
114
198
  if (ctx.globalOptions.pageID) {
115
199
  form["author"] = "fbid:" + ctx.globalOptions.pageID;
116
200
  form["specific_to_list[1]"] = "fbid:" + ctx.globalOptions.pageID;
@@ -131,14 +215,10 @@ module.exports = (defaultFuncs, api, ctx) => {
131
215
 
132
216
  if (!resData) throw new Error("Send message failed.");
133
217
  if (resData.error) {
134
- if (resData.error === 1545012) {
135
- utils.warn("sendMessage", "Got error 1545012. This might mean that you're not part of the conversation " + threadID);
136
- }
137
- // Check for suspension signals in error
218
+ if (resData.error === 1545012) utils.warn("sendMessage", "Got error 1545012. This might mean that you're not part of the conversation " + threadID);
138
219
  globalAntiSuspension.detectSuspensionSignal(String(resData.error) + ' ' + JSON.stringify(resData));
139
220
  throw new Error(JSON.stringify(resData));
140
221
  }
141
-
142
222
  const messageInfo = resData.payload.actions.reduce((p, v) => {
143
223
  return { threadID: v.thread_fbid, messageID: v.message_id, timestamp: v.timestamp } || p;
144
224
  }, null);
@@ -182,11 +262,11 @@ module.exports = (defaultFuncs, api, ctx) => {
182
262
  return callback(new Error("MessageID should be of type string and not " + threadIDType + "."));
183
263
  }
184
264
 
185
- if (!ctx.validator.isValidMessage(msg)) {
265
+ if (ctx.validator && !ctx.validator.isValidMessage(msg)) {
186
266
  return callback(new Error("Invalid message content"));
187
267
  }
188
268
  const threadIDs = Array.isArray(threadID) ? threadID : [threadID];
189
- if (!ctx.validator.validateIDArray(threadIDs, ctx.validator.isValidThreadID)) {
269
+ if (ctx.validator && !ctx.validator.validateIDArray(threadIDs, ctx.validator.isValidThreadID)) {
190
270
  return callback(new Error("Invalid thread ID(s)"));
191
271
  }
192
272
 
@@ -198,122 +278,18 @@ module.exports = (defaultFuncs, api, ctx) => {
198
278
  }
199
279
 
200
280
  try {
201
- let messageAndOTID = utils.generateOfflineThreadingID();
202
- let form = {
203
- client: "mercury",
204
- action_type: "ma-type:user-generated-message",
205
- author: "fbid:" + ctx.userID,
206
- timestamp: Date.now(),
207
- timestamp_absolute: "Today",
208
- timestamp_relative: utils.generateTimestampRelative(),
209
- timestamp_time_passed: "0",
210
- is_unread: false,
211
- is_cleared: false,
212
- is_forward: false,
213
- is_filtered_content: false,
214
- is_filtered_content_bh: false,
215
- is_filtered_content_account: false,
216
- is_filtered_content_quasar: false,
217
- is_filtered_content_invalid_app: false,
218
- is_spoof_warning: false,
219
- source: "source:chat:web",
220
- "source_tags[0]": "source:chat",
221
- ...(msg.body && { body: msg.body }),
222
- html_body: false,
223
- ui_push_phase: "V3",
224
- status: "0",
225
- offline_threading_id: messageAndOTID,
226
- message_id: messageAndOTID,
227
- threading_id: utils.generateThreadingID(ctx.clientID),
228
- "ephemeral_ttl_mode:": "0",
229
- manual_retry_cnt: "0",
230
- has_attachment: !!(msg.attachment || msg.url || msg.sticker),
231
- signatureID: utils.getSignatureID(),
232
- ...(replyToMessage && { replied_to_message_id: replyToMessage })
233
- };
234
-
235
- if (msg.location) {
236
- if (!msg.location.latitude || !msg.location.longitude) {
237
- return callback(new Error("location property needs both latitude and longitude"));
238
- }
239
- form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
240
- form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
241
- form["location_attachment[is_current_location]"] = !!msg.location.current;
242
- }
243
- if (msg.sticker) form["sticker_id"] = msg.sticker;
244
- if (msg.effect) {
245
- const effectTag = String(msg.effect).toUpperCase().replace(/[\s\-]+/g, '_');
246
- const messagingTag = 'fb.messaging.effects.' + effectTag;
247
- form.has_lightweight_action = true;
248
- form['lightweight_action_attached[message_id]'] = messageAndOTID;
249
- form['lightweight_action_attached[messaging_tag]'] = messagingTag;
250
- }
251
- if (msg.attachment) {
252
- form.image_ids = [];
253
- form.gif_ids = [];
254
- form.file_ids = [];
255
- form.video_ids = [];
256
- form.audio_ids = [];
257
- if (utils.getType(msg.attachment) !== "Array") msg.attachment = [msg.attachment];
258
- const files = await uploadAttachment(msg.attachment, threadID);
259
- files.forEach(file => {
260
- const type = Object.keys(file)[0];
261
- form["" + type + "s"].push(file[type]);
262
- });
263
- }
264
- if (msg.url) {
265
- form["shareable_attachment[share_type]"] = "100";
266
- const params = await getUrl(msg.url);
267
- form["shareable_attachment[share_params]"] = params;
268
- }
269
- if (msg.emoji) {
270
- if (!msg.emojiSize) msg.emojiSize = "medium";
271
- if (msg.emojiSize !== "small" && msg.emojiSize !== "medium" && msg.emojiSize !== "large") {
272
- return callback(new Error("emojiSize property is invalid"));
273
- }
274
- if (form.body && form.body !== "") return callback(new Error("body is not empty"));
275
- form.body = msg.emoji;
276
- form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
277
- }
278
- if (msg.mentions) {
279
- for (let i = 0; i < msg.mentions.length; i++) {
280
- const mention = msg.mentions[i];
281
- const tag = mention.tag;
282
- if (typeof tag !== "string") return callback(new Error("Mention tags must be strings."));
283
- const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
284
- if (offset < 0) utils.warn("handleMention", 'Mention for "' + tag + '" not found in message string.');
285
- if (!mention.id) utils.warn("handleMention", "Mention id should be non-null.");
286
- const id = mention.id || 0;
287
- const emptyChar = '\u200E';
288
- form["body"] = emptyChar + msg.body;
289
- form["profile_xmd[" + i + "][offset]"] = offset + 1;
290
- form["profile_xmd[" + i + "][length]"] = tag.length;
291
- form["profile_xmd[" + i + "][id]"] = id;
292
- form["profile_xmd[" + i + "][type]"] = "p";
293
- }
294
- }
295
-
296
- const isSingleUser = !(await isGroupThread(threadID, isGroup));
281
+ await globalAntiSuspension.prepareBeforeMessage(String(Array.isArray(threadID) ? threadID[0] : threadID), msg.body || '');
297
282
 
298
- // ── Optimised anti-suspension send flow ───────────────────────────────
299
- // Step 1: enforce thread throttle (single delay — no stacking).
300
- await globalAntiSuspension.prepareBeforeMessage(threadID, msg.body || '');
301
-
302
- // Step 2: start typing indicator BEFORE the typing delay so the delay
303
- // is "hidden" inside the visible typing indicator — zero extra latency.
304
283
  let typingStarted = false;
305
284
  let typingTimeout;
306
- const shouldSimulateTyping = ctx.globalOptions && ctx.globalOptions.simulateTyping && api.sendTypingIndicator;
285
+ const shouldSimulateTyping = ctx.globalOptions && ctx.globalOptions.simulateTyping && api.sendTypingIndicator && ctx.mqttClient && ctx.mqttClient.connected;
307
286
  if (shouldSimulateTyping) {
308
287
  try {
309
288
  await api.sendTypingIndicator(true, threadID);
310
289
  typingStarted = true;
311
-
312
- // Typing delay runs while the indicator is already showing.
313
290
  const msgLen = (msg.body || '').length;
314
291
  const typingMs = await globalAntiSuspension.simulateTyping(threadID, msgLen);
315
292
  await new Promise(resolve => setTimeout(resolve, typingMs));
316
-
317
293
  typingTimeout = setTimeout(() => {
318
294
  if (typingStarted) {
319
295
  try { api.sendTypingIndicator(false, threadID); } catch (_) {}
@@ -323,21 +299,33 @@ module.exports = (defaultFuncs, api, ctx) => {
323
299
  } catch (_) {}
324
300
  }
325
301
 
326
- // Step 3: send.
327
302
  try {
328
- const result = await sendContent(form, threadID, isSingleUser, messageAndOTID);
303
+ let result;
304
+ const mqttReady = ctx.mqttClient && ctx.mqttClient.connected;
305
+ const isMultiRecipient = Array.isArray(threadID);
306
+
307
+ if (mqttReady && !isMultiRecipient && api.sendMessageMqtt) {
308
+ result = await api.sendMessageMqtt(msg, threadID, replyToMessage);
309
+ } else {
310
+ result = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
311
+ }
329
312
  callback(null, result);
330
- } catch (primaryErr) {
331
- // Fallback to MQTT for group threads or when HTTP send fails.
332
- if (api.sendMessageMqtt) {
313
+ } catch (sendErr) {
314
+ const mqttReady = ctx.mqttClient && ctx.mqttClient.connected;
315
+ if (mqttReady && !Array.isArray(threadID) && api.sendMessageMqtt) {
333
316
  try {
334
317
  const mqttRes = await api.sendMessageMqtt(msg, threadID, replyToMessage);
335
318
  callback(null, mqttRes);
336
- } catch (fallbackErr) {
337
- callback(primaryErr);
319
+ } catch (_mqttErr) {
320
+ callback(sendErr);
338
321
  }
339
322
  } else {
340
- callback(primaryErr);
323
+ try {
324
+ const httpRes = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
325
+ callback(null, httpRes);
326
+ } catch (_httpErr) {
327
+ callback(sendErr);
328
+ }
341
329
  }
342
330
  } finally {
343
331
  if (typingTimeout) clearTimeout(typingTimeout);
@@ -16,240 +16,256 @@ module.exports = (defaultFuncs, api, ctx) => {
16
16
  return { file: "true" };
17
17
  }
18
18
 
19
- async function uploadAttachment(attachments, callback) {
20
- callback = callback || function () {};
21
- var uploads = [];
22
- try {
23
- for (var i = 0; i < attachments.length; i++) {
24
- if (!utils.isReadableStream(attachments[i])) {
25
- throw { error: "Attachment should be a readable stream and not " + utils.getType(attachments[i]) + "." };
26
- }
27
-
28
- if (i > 0) {
29
- await globalAntiSuspension.addSmartDelay();
30
- }
31
-
32
- var form = {
33
- upload_1024: attachments[i],
34
- ...detectAttachmentType(attachments[i]),
35
- };
36
-
37
- const upload = await defaultFuncs
38
- .postFormData("https://upload.facebook.com/ajax/mercury/upload.php", ctx.jar, form, {}, { ...ctx, requestThreadID: String(ctx._lastThreadHint || "") })
39
- .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
40
- .then(resData => {
41
- if (resData.error) throw resData;
42
- return resData.payload.metadata[0];
43
- });
44
-
45
- uploads.push(upload);
19
+ async function uploadAttachments(attachments) {
20
+ const uploads = [];
21
+ for (let i = 0; i < attachments.length; i++) {
22
+ if (!utils.isReadableStream(attachments[i])) {
23
+ throw new Error("Attachment should be a readable stream and not " + utils.getType(attachments[i]) + ".");
46
24
  }
47
- callback(null, uploads);
48
- } catch (err) {
49
- utils.error("uploadAttachment", err);
50
- return callback(err);
25
+ if (i > 0) {
26
+ await globalAntiSuspension.addSmartDelay();
27
+ }
28
+ const form = {
29
+ upload_1024: attachments[i],
30
+ ...detectAttachmentType(attachments[i]),
31
+ };
32
+ const upload = await defaultFuncs
33
+ .postFormData("https://upload.facebook.com/ajax/mercury/upload.php", ctx.jar, form, {}, { ...ctx, requestThreadID: String(ctx._lastThreadHint || "") })
34
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
35
+ .then(resData => {
36
+ if (resData.error) throw resData;
37
+ return resData.payload.metadata[0];
38
+ });
39
+ uploads.push(upload);
51
40
  }
41
+ return uploads;
52
42
  }
53
43
 
54
- function getSendPayload(threadID, msg, otid) {
55
- const isString = typeof msg === 'string';
56
- const body = isString ? msg : msg.body || "";
57
- otid = otid.toString() || utils.generateOfflineThreadingID().toString();
58
- let payload = {
59
- thread_id: threadID.toString(),
60
- otid,
61
- source: 0,
62
- send_type: 1,
63
- sync_group: 1,
64
- text: body,
65
- initiating_source: 1,
66
- skip_url_preview_gen: 0,
67
- };
68
- if (typeof msg === 'object') {
69
- if (msg.sticker) {
70
- payload.send_type = 2;
71
- payload.sticker_id = msg.sticker;
72
- payload.text = null;
73
- }
74
- if (msg.attachment) {
75
- payload.send_type = 3;
76
- payload.attachment_fbids = Array.isArray(msg.attachment) ? msg.attachment : [msg.attachment];
77
- }
78
- if (msg.effect) {
79
- const effectTag = String(msg.effect).toUpperCase().replace(/[\s\-]+/g, '_');
80
- payload.lightweight_action_attached = {
81
- messaging_tag: 'fb.messaging.effects.' + effectTag,
82
- };
44
+ function buildMentionData(msg, baseBody) {
45
+ if (!Array.isArray(msg.mentions) || msg.mentions.length === 0) return null;
46
+ const ids = [], offsets = [], lengths = [], types = [];
47
+ let cursor = 0;
48
+ for (const mention of msg.mentions) {
49
+ const rawTag = String(mention.tag || "");
50
+ const displayName = rawTag.replace(/^@+/, "");
51
+ const start = Number.isInteger(mention.fromIndex) ? mention.fromIndex : cursor;
52
+ let index = baseBody.indexOf(rawTag, start);
53
+ let adjustment = 0;
54
+ if (index === -1) {
55
+ index = baseBody.indexOf(displayName, start);
56
+ } else {
57
+ adjustment = rawTag.length - displayName.length;
83
58
  }
59
+ if (index < 0) { index = 0; adjustment = 0; }
60
+ const offset = index + adjustment;
61
+ ids.push(String(mention.id || 0));
62
+ offsets.push(offset);
63
+ lengths.push(displayName.length);
64
+ types.push("p");
65
+ cursor = offset + displayName.length;
84
66
  }
85
- return payload;
67
+ return {
68
+ mention_ids: ids.join(","),
69
+ mention_offsets: offsets.join(","),
70
+ mention_lengths: lengths.join(","),
71
+ mention_types: types.join(","),
72
+ };
73
+ }
74
+
75
+ function hasLinks(text) {
76
+ return /(https?:\/\/|www\.|t\.me\/|fb\.me\/|youtu\.be\/|facebook\.com\/|youtube\.com\/)/i.test(text);
86
77
  }
87
78
 
88
79
  function extractIdsFromPayload(payload) {
89
80
  let messageID = null;
90
81
  let threadID = null;
91
82
  function walk(n) {
92
- if (Array.isArray(n)) {
93
- if (n[0] === 5 && (n[1] === "replaceOptimsiticMessage" || n[1] === "replaceOptimisticMessage")) {
94
- messageID = String(n[3]);
95
- }
96
- if (n[0] === 5 && n[1] === "writeCTAIdToThreadsTable") {
97
- const a = n[2];
98
- if (Array.isArray(a) && a[0] === 19) threadID = String(a[1]);
99
- }
100
- for (const x of n) walk(x);
83
+ if (!Array.isArray(n)) return;
84
+ if (n[0] === 5 && (n[1] === "replaceOptimsiticMessage" || n[1] === "replaceOptimisticMessage")) {
85
+ messageID = String(n[3]);
86
+ }
87
+ if (n[0] === 5 && n[1] === "writeCTAIdToThreadsTable") {
88
+ const a = n[2];
89
+ if (Array.isArray(a) && a[0] === 19) threadID = String(a[1]);
101
90
  }
91
+ for (const x of n) walk(x);
102
92
  }
103
93
  walk(payload?.step);
104
94
  return { threadID, messageID };
105
95
  }
106
96
 
107
- function publishWithAck(content, reqID, callback) {
97
+ function publishWithAck(content, reqID) {
108
98
  return new Promise((resolve, reject) => {
109
99
  if (!ctx.mqttClient || typeof ctx.mqttClient.on !== "function" || typeof ctx.mqttClient.publish !== "function") {
110
- const err = new Error("MQTT client is not initialized");
111
- utils.error("sendMessageMqtt", err);
112
- callback && callback(err);
113
- return reject(err);
100
+ return reject(new Error("MQTT client is not initialized"));
114
101
  }
115
-
116
102
  if (typeof ctx.mqttClient.setMaxListeners === "function") {
117
103
  ctx.mqttClient.setMaxListeners(0);
118
104
  }
119
-
120
- let done = false;
105
+ let settled = false;
106
+ let timer;
121
107
  const cleanup = () => {
122
- if (done) return;
123
- done = true;
124
- ctx.mqttClient.removeListener("message", handleRes);
108
+ if (settled) return;
109
+ settled = true;
110
+ if (timer) clearTimeout(timer);
111
+ ctx.mqttClient.removeListener("message", onMessage);
125
112
  };
126
- const handleRes = (topic, message) => {
113
+ const onMessage = (topic, message) => {
127
114
  if (topic !== "/ls_resp") return;
128
- let jsonMsg;
115
+ let parsed;
129
116
  try {
130
- jsonMsg = JSON.parse(message.toString());
131
- jsonMsg.payload = JSON.parse(jsonMsg.payload);
132
- } catch {
133
- return;
134
- }
135
- if (jsonMsg.request_id !== reqID) return;
136
- const { threadID, messageID } = extractIdsFromPayload(jsonMsg.payload);
137
- const result = { messageID, threadID };
117
+ parsed = JSON.parse(message.toString());
118
+ if (typeof parsed.payload === "string") parsed.payload = JSON.parse(parsed.payload);
119
+ } catch { return; }
120
+ if (parsed.request_id !== reqID) return;
121
+ const { threadID, messageID } = extractIdsFromPayload(parsed.payload);
138
122
  cleanup();
139
- callback && callback(undefined, result);
140
- resolve(result);
123
+ resolve({ messageID, threadID });
141
124
  };
142
- ctx.mqttClient.on("message", handleRes);
143
- ctx.mqttClient.publish("/ls_req", JSON.stringify(content), { qos: 1, retain: false }, err => {
144
- if (err) {
145
- cleanup();
146
- callback && callback(err);
147
- reject(err);
148
- }
125
+ ctx.mqttClient.on("message", onMessage);
126
+ ctx.mqttClient.publish("/ls_req", JSON.stringify(content), { qos: 1, retain: false }, (err) => {
127
+ if (err) { cleanup(); reject(err); }
149
128
  });
150
- setTimeout(() => {
151
- if (done) return;
129
+ timer = setTimeout(() => {
130
+ if (settled) return;
152
131
  cleanup();
153
- const err = { error: "Timeout waiting for ACK" };
154
- callback && callback(err);
155
- reject(err);
132
+ reject({ error: "Timeout waiting for ACK" });
156
133
  }, 15000);
157
134
  });
158
135
  }
159
136
 
160
137
  return async (msg, threadID, replyToMessage, callback) => {
161
- if (typeof msg !== 'string' && typeof msg !== 'object') {
138
+ if (typeof msg !== "string" && typeof msg !== "object") {
162
139
  throw new Error("Message should be of type string or object, not " + utils.getType(msg) + ".");
163
140
  }
164
-
165
- if (typeof threadID !== 'string' && typeof threadID !== 'number') {
141
+ if (typeof threadID !== "string" && typeof threadID !== "number") {
166
142
  throw new Error("threadID must be a string or number.");
167
143
  }
168
-
169
- if (!callback && typeof threadID === "function") {
170
- throw new Error("Pass a threadID as a second argument.");
171
- }
172
-
173
144
  if (!callback && typeof replyToMessage === "function") {
174
145
  callback = replyToMessage;
175
146
  replyToMessage = null;
176
147
  }
177
148
 
178
- // Apply anti-suspension throttling and volume checks before every MQTT send
179
149
  try {
180
- await globalAntiSuspension.prepareBeforeMessage(String(threadID), typeof msg === 'string' ? msg : (msg.body || ''));
181
- } catch (suspErr) {
182
- utils.warn("sendMessageMqtt", "Anti-suspension check raised:", suspErr && suspErr.message ? suspErr.message : suspErr);
183
- }
150
+ await globalAntiSuspension.prepareBeforeMessage(String(threadID), typeof msg === "string" ? msg : (msg.body || ""));
151
+ } catch (_) {}
184
152
 
185
- const timestamp = Date.now();
153
+ const normalized = typeof msg === "string" ? { body: msg } : msg;
154
+ const baseBody = normalized.body != null ? String(normalized.body) : "";
155
+ const epoch = (BigInt(Date.now()) << 22n).toString();
156
+ const requestId = Math.floor(100 + Math.random() * 900);
186
157
  const otid = utils.generateOfflineThreadingID();
187
- const epoch_id = utils.generateOfflineThreadingID();
188
- const payload = getSendPayload(threadID, msg, otid);
189
-
190
- const tasks = [{
191
- label: "46",
192
- payload,
193
- queue_name: threadID.toString(),
194
- task_id: 0,
195
- failure_count: null,
196
- }, {
197
- label: "21",
198
- payload: {
199
- thread_id: threadID.toString(),
200
- last_read_watermark_ts: timestamp,
201
- sync_group: 1,
202
- },
203
- queue_name: threadID.toString(),
204
- task_id: 1,
205
- failure_count: null,
206
- }];
207
-
208
- if (replyToMessage) {
209
- tasks[0].payload.reply_metadata = {
210
- reply_source_id: replyToMessage,
158
+
159
+ const payload0 = {
160
+ thread_id: String(threadID),
161
+ otid: otid.toString(),
162
+ source: 2097153,
163
+ send_type: 1,
164
+ sync_group: 1,
165
+ mark_thread_read: 1,
166
+ text: baseBody === "" ? null : baseBody,
167
+ initiating_source: 0,
168
+ skip_url_preview_gen: 0,
169
+ text_has_links: hasLinks(baseBody) ? 1 : 0,
170
+ multitab_env: 0,
171
+ metadata_dataclass: JSON.stringify({ media_accessibility_metadata: { alt_text: null } }),
172
+ };
173
+
174
+ if (normalized.mentions && Array.isArray(normalized.mentions) && normalized.mentions.length > 0) {
175
+ const mentionData = buildMentionData(normalized, baseBody);
176
+ if (mentionData) payload0.mention_data = mentionData;
177
+ }
178
+
179
+ if (normalized.sticker) {
180
+ payload0.send_type = 2;
181
+ payload0.sticker_id = normalized.sticker;
182
+ payload0.text = null;
183
+ }
184
+
185
+ if (normalized.emoji) {
186
+ payload0.send_type = 1;
187
+ payload0.text = normalized.emoji;
188
+ const sizeMap = { small: 1, medium: 2, large: 3 };
189
+ const emojiSize = normalized.emojiSize;
190
+ payload0.hot_emoji_size = (typeof emojiSize === "number" ? Math.min(3, Math.max(1, emojiSize)) : sizeMap[emojiSize]) || 1;
191
+ }
192
+
193
+ if (normalized.location && normalized.location.latitude != null && normalized.location.longitude != null) {
194
+ payload0.send_type = 1;
195
+ payload0.location_data = {
196
+ coordinates: { latitude: normalized.location.latitude, longitude: normalized.location.longitude },
197
+ is_current_location: Boolean(normalized.location.current),
198
+ is_live_location: Boolean(normalized.location.live),
199
+ };
200
+ }
201
+
202
+ if (normalized.effect) {
203
+ const effectTag = String(normalized.effect).toUpperCase().replace(/[\s\-]+/g, "_");
204
+ payload0.lightweight_action_attached = {
205
+ message_id: otid.toString(),
206
+ messaging_tag: "fb.messaging.effects." + effectTag,
207
+ };
208
+ }
209
+
210
+ if (replyToMessage || normalized.replyToMessage) {
211
+ const replyId = replyToMessage || normalized.replyToMessage;
212
+ payload0.reply_metadata = {
213
+ reply_source_id: replyId,
211
214
  reply_source_type: 1,
212
215
  reply_type: 0,
213
216
  };
214
217
  }
215
218
 
216
- const request_id = ++ctx.wsReqNumber;
217
- const form = {
219
+ if (normalized.attachment) {
220
+ payload0.send_type = 3;
221
+ if (payload0.text === "") payload0.text = null;
222
+ const list = Array.isArray(normalized.attachment) ? normalized.attachment : [normalized.attachment];
223
+ ctx._lastThreadHint = threadID;
224
+ const files = await uploadAttachments(list.filter(a => utils.isReadableStream(a)));
225
+ payload0.attachment_fbids = files.map(f => String(Object.values(f)[0]));
226
+ }
227
+
228
+ const content = {
218
229
  app_id: "2220391788200892",
219
230
  payload: {
220
- tasks,
221
- epoch_id,
222
- version_id: "6120284488008082",
223
- data_trace_id: null,
231
+ tasks: [
232
+ {
233
+ label: "46",
234
+ payload: payload0,
235
+ queue_name: String(threadID),
236
+ task_id: 400,
237
+ failure_count: null,
238
+ },
239
+ {
240
+ label: "21",
241
+ payload: {
242
+ thread_id: String(threadID),
243
+ last_read_watermark_ts: Date.now(),
244
+ sync_group: 1,
245
+ },
246
+ queue_name: String(threadID),
247
+ task_id: 401,
248
+ failure_count: null,
249
+ },
250
+ ],
251
+ epoch_id: epoch,
252
+ version_id: "24804310205905615",
253
+ data_trace_id: `#${Buffer.from(String(Math.random())).toString("base64").replace(/=+$/g, "")}`,
224
254
  },
225
- request_id,
255
+ request_id: requestId,
226
256
  type: 3,
227
257
  };
228
258
 
229
- if (msg.attachment) {
230
- try {
231
- ctx._lastThreadHint = threadID;
232
- const files = await new Promise((resolve, reject) => {
233
- uploadAttachment(
234
- Array.isArray(msg.attachment) ? msg.attachment : [msg.attachment],
235
- (err, files) => {
236
- if (err) return reject(err);
237
- return resolve(files);
238
- }
239
- );
240
- });
241
- form.payload.tasks[0].payload.attachment_fbids = files.map(file => Object.values(file)[0]);
242
- } catch (err) {
243
- utils.error("Attachment upload failed:", err);
244
- throw err;
245
- }
246
- }
259
+ content.payload.tasks = content.payload.tasks.map(t => ({ ...t, payload: JSON.stringify(t.payload) }));
260
+ content.payload = JSON.stringify(content.payload);
247
261
 
248
- form.payload.tasks.forEach(task => {
249
- task.payload = JSON.stringify(task.payload);
250
- });
251
- form.payload = JSON.stringify(form.payload);
252
-
253
- return publishWithAck(form, request_id, callback);
262
+ try {
263
+ const result = await publishWithAck(content, requestId);
264
+ if (callback) callback(undefined, result);
265
+ return result;
266
+ } catch (err) {
267
+ if (callback) callback(err);
268
+ throw err;
269
+ }
254
270
  };
255
271
  };
@@ -2,39 +2,73 @@
2
2
 
3
3
  const utils = require('../utils');
4
4
 
5
- /**
6
- * @param {Object} defaultFuncs
7
- * @param {Object} api
8
- * @param {Object} ctx
9
- */
10
5
  module.exports = function (defaultFuncs, api, ctx) {
11
- /**
12
- * Sends a typing indicator to a specific thread.
13
- * @param {boolean} sendTyping - True to show typing indicator, false to hide.
14
- * @param {string} threadID - The ID of the thread to send the typing indicator to.
15
- * @param {Function} [callback] - An optional callback function.
16
- * @returns {Promise<void>}
17
- */
18
- return async function sendTypingIndicatorV2(sendTyping, threadID, callback) {
19
- let count_req = 0;
20
- const wsContent = {
21
- app_id: 2220391788200892,
22
- payload: JSON.stringify({
23
- label: 3,
24
- payload: JSON.stringify({
25
- thread_key: threadID.toString(),
26
- is_group_thread: +(threadID.toString().length >= 16),
27
- is_typing: +sendTyping,
28
- attribution: 0
29
- }),
30
- version: 5849951561777440
31
- }),
32
- request_id: ++count_req,
33
- type: 4
34
- };
35
- await new Promise((resolve, reject) => ctx.mqttClient.publish('/ls_req', JSON.stringify(wsContent), {}, (err, _packet) => err ? reject(err) : resolve()));
36
- if (callback) {
37
- callback();
38
- }
39
- };
6
+ return function sendTypingIndicator(sendTyping, threadID, callback) {
7
+ let resolveFunc, rejectFunc;
8
+ const returnPromise = new Promise((resolve, reject) => {
9
+ resolveFunc = resolve;
10
+ rejectFunc = reject;
11
+ });
12
+
13
+ if (!callback) {
14
+ callback = (err) => {
15
+ if (err) return rejectFunc(err);
16
+ resolveFunc(true);
17
+ };
18
+ }
19
+
20
+ if (!ctx.mqttClient || typeof ctx.mqttClient.publish !== "function") {
21
+ const err = new Error("You can only use sendTypingIndicator after you start listening.");
22
+ callback(err);
23
+ return returnPromise;
24
+ }
25
+
26
+ const threadIDs = Array.isArray(threadID) ? threadID : [threadID];
27
+ if (!threadIDs.length) {
28
+ const err = new Error("threadID is required");
29
+ callback(err);
30
+ return returnPromise;
31
+ }
32
+
33
+ if (typeof ctx.wsReqNumber !== "number") ctx.wsReqNumber = 0;
34
+
35
+ function buildPayload(tid) {
36
+ const isGroup = String(tid).length >= 16 ? 1 : 0;
37
+ return {
38
+ app_id: "772021112871879",
39
+ payload: JSON.stringify({
40
+ label: "3",
41
+ payload: JSON.stringify({
42
+ thread_key: Number.parseInt(String(tid), 10),
43
+ is_group_thread: isGroup,
44
+ is_typing: sendTyping ? 1 : 0,
45
+ attribution: 0,
46
+ sync_group: 1,
47
+ thread_type: isGroup ? 2 : 1,
48
+ }),
49
+ version: "8965252033599983",
50
+ }),
51
+ request_id: ++ctx.wsReqNumber,
52
+ type: 4,
53
+ };
54
+ }
55
+
56
+ const publishes = threadIDs.map(tid =>
57
+ new Promise((resolve, reject) => {
58
+ ctx.mqttClient.publish("/ls_req", JSON.stringify(buildPayload(tid)), { qos: 1 }, (err) => {
59
+ if (err) return reject(err);
60
+ resolve();
61
+ });
62
+ })
63
+ );
64
+
65
+ Promise.all(publishes)
66
+ .then(() => callback(null, true))
67
+ .catch(err => {
68
+ utils.error("sendTypingIndicator", err);
69
+ callback(err);
70
+ });
71
+
72
+ return returnPromise;
73
+ };
40
74
  };