shadowx-fca 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
-
2
+ // shadowx-fca — sendMessage (MQTT Version)
3
+ // Supports: BOTH DM/E2EE Inbox AND Group Threads
4
+ // Supports: Both callback style and Promise/await style
3
5
 
4
6
  const utils = require('../utils');
7
+ const log = require('npmlog');
5
8
 
6
9
  const allowedProperties = {
7
10
  attachment : true,
@@ -14,10 +17,18 @@ const allowedProperties = {
14
17
  location : true,
15
18
  };
16
19
 
17
-
18
-
19
20
  module.exports = (defaultFuncs, api, ctx) => {
20
21
 
22
+ let variance = 0;
23
+ const epoch_id = () => Math.floor(Date.now() * (4194304 + (variance = (variance + 0.1) % 5)));
24
+
25
+ const emojiSizes = {
26
+ small: 1,
27
+ medium: 2,
28
+ large: 3,
29
+ };
30
+
31
+ // Upload attachments
21
32
  async function uploadAttachment(attachments) {
22
33
  const uploads = [];
23
34
  for (const att of attachments) {
@@ -30,45 +41,54 @@ module.exports = (defaultFuncs, api, ctx) => {
30
41
  { upload_1024: att, voice_clip: "true" },
31
42
  {}
32
43
  ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
44
+
33
45
  if (res.error) throw new Error("Upload failed: " + JSON.stringify(res));
34
46
  uploads.push(res.payload.metadata[0]);
35
47
  }
36
48
  return uploads;
37
49
  }
38
50
 
51
+ // Get URL share params
39
52
  async function getUrl(url) {
40
53
  const res = await defaultFuncs.post(
41
54
  "https://www.facebook.com/message_share_attachment/fromURI/",
42
55
  ctx.jar,
43
56
  { image_height: 960, image_width: 960, uri: url }
44
57
  ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
45
- if (!res || res.error || !res.payload) throw new Error("URL attachment failed: " + JSON.stringify(res));
46
- return res.payload;
58
+
59
+ if (!res || res.error || !res.payload) {
60
+ throw new Error("URL attachment failed: " + JSON.stringify(res));
61
+ }
62
+ return res.payload.share_data.share_params;
47
63
  }
48
64
 
49
- async function sendContent(form, threadID, messageAndOTID) {
65
+ // Send via HTTP Mercury (Fallback for Groups only)
66
+ async function sendHttp(form, threadID, messageAndOTID, isSingleUser) {
50
67
  const tid = String(threadID);
51
68
 
52
69
  if (Array.isArray(threadID)) {
53
70
  threadID.forEach((id, idx) => { form[`specific_to_list[${idx}]`] = "fbid:" + id; });
54
71
  form[`specific_to_list[${threadID.length}]`] = "fbid:" + ctx.userID;
55
72
  form["client_thread_id"] = "root:" + messageAndOTID;
56
- utils.log("sendMessage", "Creating new group with users: " + threadID.join(', '));
57
73
  } else {
58
- // Group thread — works for any digit length (15, 16, 17, 18, 19...)
59
- form["thread_fbid"] = tid;
60
- utils.log("sendMessage", "Sending to group thread: " + tid);
74
+ if (isSingleUser) {
75
+ form["specific_to_list[0]"] = "fbid:" + threadID;
76
+ form["specific_to_list[1]"] = "fbid:" + ctx.userID;
77
+ form["other_user_fbid"] = threadID;
78
+ } else {
79
+ form["thread_fbid"] = tid;
80
+ }
61
81
  }
62
82
 
63
83
  if (ctx.globalOptions.pageID) {
64
- form["author"] = "fbid:" + ctx.globalOptions.pageID;
65
- form["specific_to_list[1]"] = "fbid:" + ctx.globalOptions.pageID;
66
- form["creator_info[creatorID]"] = ctx.userID;
67
- form["creator_info[creatorType]"] = "direct_admin";
68
- form["creator_info[labelType]"] = "sent_message";
69
- form["creator_info[pageID]"] = ctx.globalOptions.pageID;
70
- form["request_user_id"] = ctx.globalOptions.pageID;
71
- form["creator_info[profileURI]"] = "https://www.facebook.com/profile.php?id=" + ctx.userID;
84
+ form["author"] = "fbid:" + ctx.globalOptions.pageID;
85
+ form["specific_to_list[1]"] = "fbid:" + ctx.globalOptions.pageID;
86
+ form["creator_info[creatorID]"] = ctx.userID;
87
+ form["creator_info[creatorType]"] = "direct_admin";
88
+ form["creator_info[labelType]"] = "sent_message";
89
+ form["creator_info[pageID]"] = ctx.globalOptions.pageID;
90
+ form["request_user_id"] = ctx.globalOptions.pageID;
91
+ form["creator_info[profileURI]"] = "https://www.facebook.com/profile.php?id=" + ctx.userID;
72
92
  }
73
93
 
74
94
  const resData = await defaultFuncs
@@ -78,49 +98,145 @@ module.exports = (defaultFuncs, api, ctx) => {
78
98
  if (!resData) throw new Error("Send message failed — no response.");
79
99
  if (resData.error) {
80
100
  if (resData.error === 1545012) {
81
- utils.warn("sendMessage", "Error 1545012: You may not be a member of thread " + tid);
101
+ throw new Error(`Cannot send message to thread ${tid}: Bot is not part of this conversation`);
82
102
  }
83
103
  throw new Error("Send message error: " + JSON.stringify(resData));
84
104
  }
85
105
 
86
106
  return resData.payload.actions.reduce((p, v) => ({
87
- threadID : v.thread_fbid,
88
- messageID : v.message_id,
89
- timestamp : v.timestamp,
107
+ threadID: v.thread_fbid,
108
+ messageID: v.message_id,
109
+ timestamp: v.timestamp,
90
110
  }), null);
91
111
  }
92
112
 
93
- /**
94
- * Send a message to a Facebook group thread.
95
- *
96
- * Supports both callback and Promise/await style:
97
- *
98
- * api.sendMessage("Hello!", threadID, callback) // callback style
99
- * await api.sendMessage("Hello!", threadID) // promise style
100
- * api.sendMessage("Reply!", threadID, msgID, callback) // reply + callback
101
- * await api.sendMessage("Reply!", threadID, msgID) // reply + promise
102
- *
103
- * @param {string|object} msg — text string or {body, sticker, attachment, url, emoji, mentions, location}
104
- * @param {string|Array} threadID — group thread ID (any length) or array of userIDs to create new group
105
- * @param {string|Function} [replyOrCallback]— optional: message ID to reply to, OR callback function(err, info)
106
- * @param {Function} [callback] — optional: callback(err, info) when replyToMessage is provided
107
- */
108
- return (msg, threadID, replyOrCallback, callbackArg) => {
109
-
110
- // ── Resolve replyToMessage vs callback ───────────────────────
113
+ // Send via MQTT (Works for BOTH DM and Group!)
114
+ function sendMqtt(payload, threadID, replyToMessage, callback) {
115
+ return new Promise((resolve, reject) => {
116
+ if (!ctx.mqttClient) {
117
+ return reject(new Error('Not connected to MQTT. Call listenMqtt first.'));
118
+ }
119
+
120
+ const timestamp = Date.now();
121
+ const epoch = timestamp << 22;
122
+ const otid = epoch + Math.floor(Math.random() * 4194304);
123
+
124
+ const tasks = [
125
+ {
126
+ label: "46",
127
+ payload: {
128
+ thread_id: String(threadID),
129
+ otid: String(otid),
130
+ source: 0,
131
+ send_type: 1,
132
+ sync_group: 1,
133
+ text: payload.text || "",
134
+ initiating_source: 1,
135
+ skip_url_preview_gen: 0
136
+ },
137
+ queue_name: String(threadID),
138
+ task_id: 0,
139
+ failure_count: null
140
+ },
141
+ {
142
+ label: "21",
143
+ payload: {
144
+ thread_id: String(threadID),
145
+ last_read_watermark_ts: Date.now(),
146
+ sync_group: 1
147
+ },
148
+ queue_name: String(threadID),
149
+ task_id: 1,
150
+ failure_count: null
151
+ }
152
+ ];
153
+
154
+ // Add reply metadata if replying
155
+ if (replyToMessage) {
156
+ tasks[0].payload.reply_metadata = {
157
+ reply_source_id: String(replyToMessage),
158
+ reply_source_type: 1,
159
+ reply_type: 0
160
+ };
161
+ }
162
+
163
+ // Add emoji size if present
164
+ if (payload.hot_emoji_size) {
165
+ tasks[0].payload.hot_emoji_size = payload.hot_emoji_size;
166
+ }
167
+
168
+ // Add sticker if present
169
+ if (payload.sticker_id) {
170
+ tasks[0].payload.send_type = 2;
171
+ tasks[0].payload.sticker_id = payload.sticker_id;
172
+ }
173
+
174
+ // Add attachments if present
175
+ if (payload.attachment_fbids && payload.attachment_fbids.length > 0) {
176
+ tasks[0].payload.send_type = 3;
177
+ tasks[0].payload.attachment_fbids = payload.attachment_fbids;
178
+ }
179
+
180
+ // Add mentions if present
181
+ if (payload.mention_data) {
182
+ tasks[0].payload.mention_data = payload.mention_data;
183
+ }
184
+
185
+ // Add location if present
186
+ if (payload.location_data) {
187
+ tasks[0].payload.location_data = payload.location_data;
188
+ }
189
+
190
+ const mqttForm = {
191
+ app_id: ctx.appID || "2220391788200892",
192
+ payload: {
193
+ tasks: tasks,
194
+ epoch_id: epoch_id(),
195
+ version_id: "6120284488008082",
196
+ data_trace_id: null
197
+ },
198
+ request_id: 1,
199
+ type: 3
200
+ };
201
+
202
+ // Stringify all task payloads
203
+ mqttForm.payload.tasks.forEach(task => {
204
+ task.payload = JSON.stringify(task.payload);
205
+ });
206
+ mqttForm.payload = JSON.stringify(mqttForm.payload);
207
+
208
+ log.info("sendMessage", `Sending via MQTT to ${threadID}`);
209
+
210
+ ctx.mqttClient.publish('/ls_req', JSON.stringify(mqttForm), { qos: 1, retain: false }, (err) => {
211
+ if (err) {
212
+ log.error("sendMessage", `MQTT publish failed: ${err.message || err}`);
213
+ reject(new Error(`MQTT publish failed: ${err.message || err}`));
214
+ } else {
215
+ const result = {
216
+ threadID: String(threadID),
217
+ messageID: String(otid),
218
+ timestamp: timestamp
219
+ };
220
+ if (callback) callback(null, result);
221
+ resolve(result);
222
+ }
223
+ });
224
+ });
225
+ }
226
+
227
+ // Main send function
228
+ return async function sendMessage(msg, threadID, replyOrCallback, callbackArg) {
111
229
  let replyToMessage = null;
112
- let callback = null;
230
+ let callback = null;
113
231
 
232
+ // Parse arguments
114
233
  if (typeof replyOrCallback === 'function') {
115
- // sendMessage(msg, threadID, callback)
116
234
  callback = replyOrCallback;
117
- } else if (typeof replyOrCallback === 'string') {
118
- // sendMessage(msg, threadID, replyMsgID) or sendMessage(msg, threadID, replyMsgID, callback)
119
- replyToMessage = replyOrCallback;
235
+ } else if (typeof replyOrCallback === 'string' || typeof replyOrCallback === 'number') {
236
+ replyToMessage = String(replyOrCallback);
120
237
  if (typeof callbackArg === 'function') callback = callbackArg;
121
238
  }
122
239
 
123
- // ── Core send logic (async) ──────────────────────────────────
124
240
  const doSend = async () => {
125
241
  const msgType = utils.getType(msg);
126
242
  if (msgType !== "String" && msgType !== "Object") {
@@ -133,100 +249,159 @@ module.exports = (defaultFuncs, api, ctx) => {
133
249
  throw new Error("Disallowed message properties: " + disallowed.join(", "));
134
250
  }
135
251
 
136
- const messageAndOTID = utils.generateOfflineThreadingID();
137
- const form = {
138
- client : "mercury",
139
- action_type : "ma-type:user-generated-message",
140
- author : "fbid:" + ctx.userID,
141
- timestamp : Date.now(),
142
- timestamp_absolute : "Today",
143
- timestamp_relative : utils.generateTimestampRelative(),
144
- timestamp_time_passed : "0",
145
- is_unread : false,
146
- is_cleared : false,
147
- is_forward : false,
148
- is_filtered_content : false,
149
- is_filtered_content_bh : false,
150
- is_filtered_content_account : false,
151
- is_filtered_content_quasar : false,
152
- is_filtered_content_invalid_app : false,
153
- is_spoof_warning : false,
154
- source : "source:chat:web",
155
- "source_tags[0]" : "source:chat",
156
- ...(msg.body && { body: msg.body }),
157
- html_body : false,
158
- ui_push_phase : "V3",
159
- status : "0",
160
- offline_threading_id : messageAndOTID,
161
- message_id : messageAndOTID,
162
- threading_id : utils.generateThreadingID(ctx.clientID),
163
- "ephemeral_ttl_mode:" : "0",
164
- manual_retry_cnt : "0",
165
- has_attachment : !!(msg.attachment || msg.url || msg.sticker),
166
- signatureID : utils.getSignatureID(),
167
- ...(replyToMessage && { replied_to_message_id: replyToMessage }),
252
+ // Detect if DM or Group
253
+ const threadIDStr = String(threadID);
254
+ const isSingleUser = !Array.isArray(threadID) &&
255
+ (threadIDStr.length === 15 || !threadIDStr.match(/^\d{16,}$/));
256
+
257
+ // Build MQTT payload
258
+ const mqttPayload = {
259
+ text: msg.body || ""
168
260
  };
169
261
 
262
+ // Handle mentions
263
+ if (msg.mentions && msg.mentions.length > 0) {
264
+ const arrayIds = [];
265
+ const arrayOffsets = [];
266
+ const arrayLengths = [];
267
+ const mention_types = [];
268
+
269
+ for (const mention of msg.mentions) {
270
+ const { tag, id, fromIndex } = mention;
271
+ if (typeof tag !== "string") throw new Error("Mention tags must be strings.");
272
+
273
+ const offset = msg.body.indexOf(tag, fromIndex || 0);
274
+ if (offset < 0) log.warn("sendMessage", `Mention "${tag}" not found in body.`);
275
+
276
+ arrayIds.push(id || 0);
277
+ arrayOffsets.push(offset);
278
+ arrayLengths.push(tag.length);
279
+ mention_types.push("p");
280
+ }
281
+
282
+ mqttPayload.mention_data = {
283
+ mention_ids: arrayIds.join(","),
284
+ mention_offsets: arrayOffsets.join(","),
285
+ mention_lengths: arrayLengths.join(","),
286
+ mention_types: mention_types.join(",")
287
+ };
288
+ }
289
+
290
+ // Handle emoji
291
+ if (msg.emoji) {
292
+ let emojiSize = msg.emojiSize || "medium";
293
+ if (!["small", "medium", "large"].includes(emojiSize)) {
294
+ throw new Error("emojiSize must be small, medium, or large.");
295
+ }
296
+ mqttPayload.text = msg.emoji;
297
+ mqttPayload.hot_emoji_size = emojiSizes[emojiSize];
298
+ }
299
+
300
+ // Handle sticker
301
+ if (msg.sticker) {
302
+ mqttPayload.sticker_id = String(msg.sticker);
303
+ }
304
+
305
+ // Handle location
170
306
  if (msg.location) {
171
307
  if (!msg.location.latitude || !msg.location.longitude) {
172
308
  throw new Error("location needs both latitude and longitude.");
173
309
  }
174
- form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
175
- form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
176
- form["location_attachment[is_current_location]"] = !!msg.location.current;
310
+ mqttPayload.location_data = {
311
+ coordinates: {
312
+ latitude: msg.location.latitude,
313
+ longitude: msg.location.longitude
314
+ },
315
+ is_current_location: !!msg.location.current,
316
+ is_live_location: !!msg.location.live
317
+ };
177
318
  }
178
319
 
179
- if (msg.sticker) form["sticker_id"] = msg.sticker;
180
-
320
+ // Handle attachments
181
321
  if (msg.attachment) {
182
- form.image_ids = []; form.gif_ids = []; form.file_ids = [];
183
- form.video_ids = []; form.audio_ids = [];
184
- if (utils.getType(msg.attachment) !== "Array") msg.attachment = [msg.attachment];
322
+ if (utils.getType(msg.attachment) !== "Array") {
323
+ msg.attachment = [msg.attachment];
324
+ }
185
325
  const files = await uploadAttachment(msg.attachment);
186
- files.forEach(file => {
187
- const type = Object.keys(file)[0];
188
- form[type + "s"].push(file[type]);
326
+ mqttPayload.attachment_fbids = files.map(file => {
327
+ const key = Object.keys(file)[0];
328
+ return file[key];
189
329
  });
190
330
  }
191
331
 
332
+ // Handle URL
192
333
  if (msg.url) {
193
- form["shareable_attachment[share_type]"] = "100";
194
- form["shareable_attachment[share_params]"] = await getUrl(msg.url);
195
- }
196
-
197
- if (msg.emoji) {
198
- if (!msg.emojiSize) msg.emojiSize = "medium";
199
- if (!["small","medium","large"].includes(msg.emojiSize)) {
200
- throw new Error("emojiSize must be small, medium, or large.");
334
+ try {
335
+ const params = await getUrl(msg.url);
336
+ // For MQTT, we just append URL to text
337
+ mqttPayload.text = mqttPayload.text ? mqttPayload.text + " " + msg.url : msg.url;
338
+ } catch (err) {
339
+ log.warn("sendMessage", "URL attachment failed, sending as text link");
340
+ mqttPayload.text = mqttPayload.text ? mqttPayload.text + " " + msg.url : msg.url;
201
341
  }
202
- if (!form.body) throw new Error("body must not be empty when using emoji.");
203
- form.body = msg.emoji;
204
- form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
205
342
  }
206
343
 
207
- if (msg.mentions) {
208
- for (let i = 0; i < msg.mentions.length; i++) {
209
- const { tag, id, fromIndex } = msg.mentions[i];
210
- if (typeof tag !== "string") throw new Error("Mention tags must be strings.");
211
- const offset = msg.body.indexOf(tag, fromIndex || 0);
212
- if (offset < 0) utils.warn("sendMessage", `Mention "${tag}" not found in body.`);
213
- const emptyChar = '\u200E';
214
- form["body"] = emptyChar + msg.body;
215
- form[`profile_xmd[${i}][offset]`] = offset + 1;
216
- form[`profile_xmd[${i}][length]`] = tag.length;
217
- form[`profile_xmd[${i}][id]`] = id || 0;
218
- form[`profile_xmd[${i}][type]`] = "p";
344
+ // Try MQTT first (works for both DM and Group)
345
+ try {
346
+ return await sendMqtt(mqttPayload, threadID, replyToMessage, callback);
347
+ } catch (mqttErr) {
348
+ log.warn("sendMessage", "MQTT send failed: " + mqttErr.message);
349
+
350
+ // Fallback to HTTP for groups only
351
+ if (!isSingleUser && !Array.isArray(threadID)) {
352
+ log.info("sendMessage", "Falling back to HTTP for group message");
353
+
354
+ const messageAndOTID = utils.generateOfflineThreadingID();
355
+ const httpForm = {
356
+ client: "mercury",
357
+ action_type: "ma-type:user-generated-message",
358
+ author: "fbid:" + ctx.userID,
359
+ timestamp: Date.now(),
360
+ timestamp_absolute: "Today",
361
+ timestamp_relative: utils.generateTimestampRelative(),
362
+ timestamp_time_passed: "0",
363
+ is_unread: false,
364
+ is_cleared: false,
365
+ is_forward: false,
366
+ is_filtered_content: false,
367
+ is_filtered_content_bh: false,
368
+ is_filtered_content_account: false,
369
+ is_filtered_content_quasar: false,
370
+ is_filtered_content_invalid_app: false,
371
+ is_spoof_warning: false,
372
+ source: "source:chat:web",
373
+ "source_tags[0]": "source:chat",
374
+ body: msg.body || "",
375
+ html_body: false,
376
+ ui_push_phase: "V3",
377
+ status: "0",
378
+ offline_threading_id: messageAndOTID,
379
+ message_id: messageAndOTID,
380
+ threading_id: utils.generateThreadingID(ctx.clientID),
381
+ "ephemeral_ttl_mode:": "0",
382
+ manual_retry_cnt: "0",
383
+ has_attachment: !!(msg.attachment || msg.url || msg.sticker),
384
+ signatureID: utils.getSignatureID(),
385
+ ...(replyToMessage && { replied_to_message_id: replyToMessage }),
386
+ };
387
+
388
+ if (msg.sticker) httpForm["sticker_id"] = msg.sticker;
389
+
390
+ return await sendHttp(httpForm, threadID, messageAndOTID, isSingleUser);
391
+ } else {
392
+ throw new Error("DM/E2EE messages require MQTT. Make sure listenMqtt is called first.");
219
393
  }
220
394
  }
221
-
222
- return sendContent(form, threadID, messageAndOTID);
223
395
  };
224
396
 
225
- // ── Return Promise OR call callback ──────────────────────────
397
+ // Execute and handle callback/promise
226
398
  if (callback) {
227
- doSend().then(info => callback(null, info)).catch(err => callback(err));
399
+ doSend()
400
+ .then(info => callback(null, info))
401
+ .catch(err => callback(err));
402
+ return undefined;
228
403
  } else {
229
404
  return doSend();
230
405
  }
231
406
  };
232
- };
407
+ };