@lazyneoaz/nkxchat 1.0.3 → 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.
@@ -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);