@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.
@@ -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
  };