shadowx-fca 2.7.0 → 2.9.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.
package/checkUpdate.js CHANGED
@@ -15,7 +15,7 @@ async function checkForFCAUpdate() {
15
15
  const latestVersion = npmData.version;
16
16
 
17
17
  // Check current installed version in node_modules
18
- let currentVersion = '2.4.0';
18
+ let currentVersion = '1.0.8';
19
19
  const nodeModulesPackagePath = path.join(process.cwd(), 'node_modules', 'shadowx-fca', 'package.json');
20
20
  if (fs.existsSync(nodeModulesPackagePath)) {
21
21
  const installedPackage = JSON.parse(fs.readFileSync(nodeModulesPackagePath, 'utf-8'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shadowx-fca",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "Unofficial Facebook Chat API for Node.js with Auto-Update System - modify by Mueid Mursalin Rifat",
5
5
  "main": "index.js",
6
6
  "files": [
package/src/listenMqtt.js CHANGED
@@ -10,6 +10,9 @@ const debugSeq = false;
10
10
  var identity = function () { };
11
11
  var form = {};
12
12
  var getSeqID = function () { };
13
+ var seqIdRetryCount = 0;
14
+ var MAX_SEQID_RETRIES = 3;
15
+
13
16
  var topics = [
14
17
  "/legacy_web",
15
18
  "/webrtc",
@@ -105,13 +108,20 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
105
108
  mqttClient.on('error', function (err) {
106
109
  log.error("listenMqtt", err);
107
110
  mqttClient.end();
108
- if (ctx.globalOptions.autoReconnect) getSeqID();
109
- else globalCallback({ type: "stop_listen", error: "Connection refused: Server unavailable" }, null);
111
+ if (ctx.globalOptions.autoReconnect) {
112
+ seqIdRetryCount = 0;
113
+ getSeqID();
114
+ } else {
115
+ globalCallback({ type: "stop_listen", error: "Connection refused: Server unavailable" }, null);
116
+ }
110
117
  });
111
118
 
112
119
  mqttClient.on('connect', function () {
113
120
  topics.forEach(topicsub => mqttClient.subscribe(topicsub));
114
121
 
122
+ // Reset retry count on successful connection
123
+ seqIdRetryCount = 0;
124
+
115
125
  // Display connection success message with branding and loading animation
116
126
  const messages = [
117
127
  '🖤 SHADOWX-FCA MQTT Connected',
@@ -787,6 +797,9 @@ module.exports = function (defaultFuncs, api, ctx) {
787
797
  .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
788
798
  .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
789
799
  .then((resData) => {
800
+ // Reset retry count on success
801
+ seqIdRetryCount = 0;
802
+
790
803
  if (utils.getType(resData) != "Array") throw { error: "Not logged in", res: resData };
791
804
  if (resData && resData[resData.length - 1].error_results > 0) throw resData[0].o0.errors;
792
805
  if (resData[resData.length - 1].successful_results === 0) throw { error: "getSeqId: there was no successful_results", res: resData };
@@ -796,8 +809,37 @@ module.exports = function (defaultFuncs, api, ctx) {
796
809
  } else throw { error: "getSeqId: no sync_sequence_id found.", res: resData };
797
810
  })
798
811
  .catch((err) => {
812
+ // Check for binary response / JSON parse error
813
+ if (err.error && err.error.includes && err.error.includes("JSON.parse error")) {
814
+ seqIdRetryCount++;
815
+
816
+ if (seqIdRetryCount <= MAX_SEQID_RETRIES) {
817
+ log.warn("getSeqId", "Binary/JSON parse error (attempt " + seqIdRetryCount + "/" + MAX_SEQID_RETRIES + "). Retrying in " + (seqIdRetryCount * 3) + "s...");
818
+
819
+ setTimeout(() => {
820
+ api.getFreshDtsg().then(newDtsg => {
821
+ if (newDtsg) ctx.fb_dtsg = newDtsg;
822
+ }).catch(() => {}).finally(() => {
823
+ getSeqID();
824
+ });
825
+ }, seqIdRetryCount * 3000);
826
+ return;
827
+ }
828
+
829
+ log.warn("getSeqId", "MQTT sync failed after " + MAX_SEQID_RETRIES + " retries. Falling back to HTTP mode (bot will still work).");
830
+
831
+ // Emit ready so bot continues working
832
+ ctx.loggedIn = true;
833
+ if (ctx.globalOptions.emitReady) {
834
+ globalCallback({ type: "ready", error: null });
835
+ }
836
+ return;
837
+ }
838
+
799
839
  log.error("getSeqId", err);
800
- if (utils.getType(err) == "Object" && err.error === "Not logged in") ctx.loggedIn = false;
840
+ if (utils.getType(err) == "Object" && err.error === "Not logged in") {
841
+ ctx.loggedIn = false;
842
+ }
801
843
  return globalCallback(err);
802
844
  });
803
845
  };
@@ -1,407 +1,243 @@
1
1
  "use strict";
2
- // shadowx-fca — sendMessage (MQTT Version)
3
- // Supports: BOTH DM/E2EE Inbox AND Group Threads
4
- // Supports: Both callback style and Promise/await style
5
2
 
6
3
  const utils = require('../utils');
7
- const log = require('npmlog');
4
+ // @NethWs3Dev
8
5
 
9
6
  const allowedProperties = {
10
- attachment : true,
11
- url : true,
12
- sticker : true,
13
- emoji : true,
14
- emojiSize : true,
15
- body : true,
16
- mentions : true,
17
- location : true,
7
+ attachment: true,
8
+ url: true,
9
+ sticker: true,
10
+ emoji: true,
11
+ emojiSize: true,
12
+ body: true,
13
+ mentions: true,
14
+ location: true,
18
15
  };
19
16
 
20
17
  module.exports = (defaultFuncs, api, ctx) => {
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
32
- async function uploadAttachment(attachments) {
33
- const uploads = [];
34
- for (const att of attachments) {
35
- if (!utils.isReadableStream(att)) {
36
- throw new Error("Attachment must be a readable stream, got: " + utils.getType(att));
37
- }
38
- const res = await defaultFuncs.postFormData(
39
- "https://upload.facebook.com/ajax/mercury/upload.php",
40
- ctx.jar,
41
- { upload_1024: att, voice_clip: "true" },
42
- {}
43
- ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
44
-
45
- if (res.error) throw new Error("Upload failed: " + JSON.stringify(res));
46
- uploads.push(res.payload.metadata[0]);
47
- }
48
- return uploads;
49
- }
50
-
51
- // Get URL share params
52
- async function getUrl(url) {
53
- const res = await defaultFuncs.post(
54
- "https://www.facebook.com/message_share_attachment/fromURI/",
55
- ctx.jar,
56
- { image_height: 960, image_width: 960, uri: url }
57
- ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
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;
63
- }
64
-
65
- // Send via HTTP Mercury (Fallback for Groups only)
66
- async function sendHttp(form, threadID, messageAndOTID, isSingleUser) {
67
- const tid = String(threadID);
68
-
69
- if (Array.isArray(threadID)) {
70
- threadID.forEach((id, idx) => { form[`specific_to_list[${idx}]`] = "fbid:" + id; });
71
- form[`specific_to_list[${threadID.length}]`] = "fbid:" + ctx.userID;
72
- form["client_thread_id"] = "root:" + messageAndOTID;
73
- } else {
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
- }
81
- }
82
-
83
- if (ctx.globalOptions.pageID) {
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;
92
- }
93
-
94
- const resData = await defaultFuncs
95
- .post("https://www.facebook.com/messaging/send/", ctx.jar, form)
96
- .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
97
-
98
- if (!resData) throw new Error("Send message failed no response.");
99
- if (resData.error) {
100
- if (resData.error === 1545012) {
101
- throw new Error(`Cannot send message to thread ${tid}: Bot is not part of this conversation`);
102
- }
103
- throw new Error("Send message error: " + JSON.stringify(resData));
104
- }
105
-
106
- return resData.payload.actions.reduce((p, v) => ({
107
- threadID: v.thread_fbid,
108
- messageID: v.message_id,
109
- timestamp: v.timestamp,
110
- }), null);
111
- }
112
-
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) {
229
- let replyToMessage = null;
230
- let callback = null;
231
-
232
- // Parse arguments
233
- if (typeof replyOrCallback === 'function') {
234
- callback = replyOrCallback;
235
- } else if (typeof replyOrCallback === 'string' || typeof replyOrCallback === 'number') {
236
- replyToMessage = String(replyOrCallback);
237
- if (typeof callbackArg === 'function') callback = callbackArg;
238
- }
239
-
240
- const doSend = async () => {
241
- const msgType = utils.getType(msg);
242
- if (msgType !== "String" && msgType !== "Object") {
243
- throw new Error("Message must be a string or object, got: " + msgType);
244
- }
245
- if (msgType === "String") msg = { body: msg };
246
-
247
- const disallowed = Object.keys(msg).filter(p => !allowedProperties[p]);
248
- if (disallowed.length > 0) {
249
- throw new Error("Disallowed message properties: " + disallowed.join(", "));
250
- }
251
-
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 || ""
260
- };
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
306
- if (msg.location) {
307
- if (!msg.location.latitude || !msg.location.longitude) {
308
- throw new Error("location needs both latitude and longitude.");
309
- }
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
- };
318
- }
319
-
320
- // Handle attachments
321
- if (msg.attachment) {
322
- if (utils.getType(msg.attachment) !== "Array") {
323
- msg.attachment = [msg.attachment];
324
- }
325
- const files = await uploadAttachment(msg.attachment);
326
- mqttPayload.attachment_fbids = files.map(file => {
327
- const key = Object.keys(file)[0];
328
- return file[key];
329
- });
330
- }
331
-
332
- // Handle URL
333
- if (msg.url) {
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;
341
- }
342
- }
343
-
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.");
393
- }
394
- }
395
- };
396
-
397
- // Execute and handle callback/promise
398
- if (callback) {
399
- doSend()
400
- .then(info => callback(null, info))
401
- .catch(err => callback(err));
402
- return undefined;
403
- } else {
404
- return doSend();
405
- }
406
- };
18
+ async function uploadAttachment(attachments) {
19
+ var uploads = [];
20
+ for (var i = 0; i < attachments.length; i++) {
21
+ if (!utils.isReadableStream(attachments[i])) {
22
+ throw new Error("Attachment should be a readable stream and not " + utils.getType(attachments[i]) + ".");
23
+ }
24
+ const oksir = await defaultFuncs.postFormData("https://upload.facebook.com/ajax/mercury/upload.php", ctx.jar,{
25
+ upload_1024: attachments[i],
26
+ voice_clip: "true"
27
+ }, {}).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
28
+ if (oksir.error) {
29
+ throw new Error(resData);
30
+ }
31
+ uploads.push(oksir.payload.metadata[0]);
32
+ }
33
+ return uploads;
34
+ }
35
+
36
+ async function getUrl(url) {
37
+ const resData = await defaultFuncs.post("https://www.facebook.com/message_share_attachment/fromURI/", ctx.jar, {
38
+ image_height: 960,
39
+ image_width: 960,
40
+ uri: url
41
+ }).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
42
+ if (!resData || resData.error || !resData.payload){
43
+ throw new Error(resData);
44
+ }
45
+ }
46
+
47
+ async function sendContent(form, threadID, isSingleUser, messageAndOTID, callback) {
48
+ // There are three cases here:
49
+ // 1. threadID is of type array, where we're starting a new group chat with users
50
+ // specified in the array.
51
+ // 2. User is sending a message to a specific user.
52
+ // 3. No additional form params and the message goes to an existing group chat.
53
+ if (utils.getType(threadID) === "Array") {
54
+ for (var i = 0; i < threadID.length; i++) {
55
+ form["specific_to_list[" + i + "]"] = "fbid:" + threadID[i];
56
+ }
57
+ form["specific_to_list[" + threadID.length + "]"] = "fbid:" + ctx.userID;
58
+ form["client_thread_id"] = "root:" + messageAndOTID;
59
+ utils.log("sendMessage", "Sending message to multiple users: " + threadID);
60
+ } else {
61
+ const threadIDStr = threadID.toString();
62
+ // Check if it's a DM: doesn't start with numeric group ID pattern or explicitly marked as single user
63
+ const isDM = !threadIDStr.match(/^\d{15,}$/) || isSingleUser === true;
64
+
65
+ // This means that threadID is the id of a user, and the chat
66
+ // is a single person chat
67
+ if (isDM) {
68
+ form["specific_to_list[0]"] = "fbid:" + threadID;
69
+ form["specific_to_list[1]"] = "fbid:" + ctx.userID;
70
+ form["other_user_fbid"] = threadID;
71
+ } else {
72
+ form["thread_fbid"] = threadID;
73
+ }
74
+ }
75
+
76
+ if (ctx.globalOptions.pageID) {
77
+ form["author"] = "fbid:" + ctx.globalOptions.pageID;
78
+ form["specific_to_list[1]"] = "fbid:" + ctx.globalOptions.pageID;
79
+ form["creator_info[creatorID]"] = ctx.userID;
80
+ form["creator_info[creatorType]"] = "direct_admin";
81
+ form["creator_info[labelType]"] = "sent_message";
82
+ form["creator_info[pageID]"] = ctx.globalOptions.pageID;
83
+ form["request_user_id"] = ctx.globalOptions.pageID;
84
+ form["creator_info[profileURI]"] =
85
+ "https://www.facebook.com/profile.php?id=" + ctx.userID;
86
+ }
87
+
88
+ const resData = await defaultFuncs.post("https://www.facebook.com/messaging/send/", ctx.jar, form).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
89
+ if (!resData) {
90
+ throw new Error("Send message failed.");
91
+ }
92
+ if (resData.error) {
93
+ if (resData.error === 1545012) {
94
+ utils.warn("sendMessage", "Got error 1545012. This might mean that you're not part of the conversation " + threadID);
95
+ throw new Error(`Cannot send message to thread ${threadID}: Bot is not part of this conversation (Error 1545012)`);
96
+ }
97
+ throw new Error(resData);
98
+ }
99
+ const messageInfo = resData.payload.actions.reduce((p, v) => {
100
+ return { threadID: v.thread_fbid, messageID: v.message_id, timestamp: v.timestamp } || p;
101
+ }, null);
102
+ return messageInfo;
103
+ }
104
+
105
+ return async (msg, threadID, callback, replyToMessage, isSingleUser = null) => {
106
+ // Handle different parameter patterns for backward compatibility
107
+ if (typeof callback === "string" || (callback && typeof callback === "object")) {
108
+ // callback is actually replyToMessage, shift parameters
109
+ isSingleUser = replyToMessage;
110
+ replyToMessage = callback;
111
+ callback = function() {};
112
+ } else if (typeof callback !== "function") {
113
+ callback = function() {};
114
+ }
115
+
116
+ let msgType = utils.getType(msg);
117
+ let threadIDType = utils.getType(threadID);
118
+ let messageIDType = utils.getType(replyToMessage);
119
+ if (msgType !== "String" && msgType !== "Object") throw new Error("Message should be of type string or object and not " + msgType + ".");
120
+ if (threadIDType !== "Array" && threadIDType !== "Number" && threadIDType !== "String") throw new Error("ThreadID should be of type number, string, or array and not " + threadIDType + ".");
121
+ if (replyToMessage && messageIDType !== 'String' && messageIDType !== 'string') throw new Error("MessageID should be of type string and not " + messageIDType + ".");
122
+ if (msgType === "String") {
123
+ msg = { body: msg };
124
+ }
125
+
126
+ // Auto-detect if this is a DM if not explicitly specified
127
+ if (isSingleUser === null && ctx.threadTypes && ctx.threadTypes[threadID]) {
128
+ isSingleUser = ctx.threadTypes[threadID] === 'dm';
129
+ } else if (isSingleUser === null) {
130
+ // Fallback: check if threadID looks like a user ID (15 digits) vs group ID (longer)
131
+ const threadIDStr = threadID.toString();
132
+ isSingleUser = threadIDStr.length === 15 || !threadIDStr.match(/^\d{16,}$/);
133
+ }
134
+ let disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
135
+ if (disallowedProperties.length > 0) {
136
+ throw new Error("Dissallowed props: `" + disallowedProperties.join(", ") + "`");
137
+ }
138
+ let messageAndOTID = utils.generateOfflineThreadingID();
139
+ let form = {
140
+ client: "mercury",
141
+ action_type: "ma-type:user-generated-message",
142
+ author: "fbid:" + ctx.userID,
143
+ timestamp: Date.now(),
144
+ timestamp_absolute: "Today",
145
+ timestamp_relative: utils.generateTimestampRelative(),
146
+ timestamp_time_passed: "0",
147
+ is_unread: false,
148
+ is_cleared: false,
149
+ is_forward: false,
150
+ is_filtered_content: false,
151
+ is_filtered_content_bh: false,
152
+ is_filtered_content_account: false,
153
+ is_filtered_content_quasar: false,
154
+ is_filtered_content_invalid_app: false,
155
+ is_spoof_warning: false,
156
+ source: "source:chat:web",
157
+ "source_tags[0]": "source:chat",
158
+ ...(msg.body && {
159
+ body: msg.body
160
+ }),
161
+ html_body: false,
162
+ ui_push_phase: "V3",
163
+ status: "0",
164
+ offline_threading_id: messageAndOTID,
165
+ message_id: messageAndOTID,
166
+ threading_id: utils.generateThreadingID(ctx.clientID),
167
+ "ephemeral_ttl_mode:": "0",
168
+ manual_retry_cnt: "0",
169
+ has_attachment: !!(msg.attachment || msg.url || msg.sticker),
170
+ signatureID: utils.getSignatureID(),
171
+ ...(replyToMessage && {
172
+ replied_to_message_id: replyToMessage
173
+ })
174
+ };
175
+
176
+ if (msg.location) {
177
+ if (!msg.location.latitude || !msg.location.longitude) throw new Error("location property needs both latitude and longitude");
178
+ form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
179
+ form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
180
+ form["location_attachment[is_current_location]"] = !!msg.location.current;
181
+ }
182
+ if (msg.sticker) {
183
+ form["sticker_id"] = msg.sticker;
184
+ }
185
+ if (msg.attachment) {
186
+ form.image_ids = [];
187
+ form.gif_ids = [];
188
+ form.file_ids = [];
189
+ form.video_ids = [];
190
+ form.audio_ids = [];
191
+ if (utils.getType(msg.attachment) !== "Array") {
192
+ msg.attachment = [msg.attachment];
193
+ }
194
+ const files = await uploadAttachment(msg.attachment);
195
+ files.forEach(file => {
196
+ const type = Object.keys(file)[0];
197
+ form["" + type + "s"].push(file[type]);
198
+ });
199
+ }
200
+ if (msg.url) {
201
+ form["shareable_attachment[share_type]"] = "100";
202
+ const params = await getUrl(msg.url);
203
+ form["shareable_attachment[share_params]"] = params;
204
+ }
205
+ if (msg.emoji) {
206
+ if (!msg.emojiSize) {
207
+ msg.emojiSize = "medium";
208
+ }
209
+ if (msg.emojiSize !== "small" && msg.emojiSize !== "medium" && msg.emojiSize !== "large") {
210
+ throw new Error("emojiSize property is invalid");
211
+ }
212
+ if (!form.body) {
213
+ throw new Error("body is not empty");
214
+ }
215
+ form.body = msg.emoji;
216
+ form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
217
+ }
218
+ if (msg.mentions) {
219
+ for (let i = 0; i < msg.mentions.length; i++) {
220
+ const mention = msg.mentions[i];
221
+ const tag = mention.tag;
222
+ if (typeof tag !== "string") {
223
+ throw new Error("Mention tags must be strings.");
224
+ }
225
+ const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
226
+ if (offset < 0) utils.warn("handleMention", 'Mention for "' + tag + '" not found in message string.');
227
+ if (!mention.id) utils.warn("handleMention", "Mention id should be non-null.");
228
+ const id = mention.id || 0;
229
+ const emptyChar = '\u200E';
230
+ form["body"] = emptyChar + msg.body;
231
+ form["profile_xmd[" + i + "][offset]"] = offset + 1;
232
+ form["profile_xmd[" + i + "][length]"] = tag.length;
233
+ form["profile_xmd[" + i + "][id]"] = id;
234
+ form["profile_xmd[" + i + "][type]"] = "p";
235
+ }
236
+ }
237
+ const result = await sendContent(form, threadID, isSingleUser, messageAndOTID);
238
+ if (callback && typeof callback === "function") {
239
+ callback(null, result);
240
+ }
241
+ return result;
242
+ };
407
243
  };
package/utils.js CHANGED
@@ -2498,6 +2498,28 @@ function parseAndCheckLogin(ctx, defaultFuncs, retryCount) {
2498
2498
  }
2499
2499
  if (data.statusCode !== 200) throw new Error("parseAndCheckLogin got status code: " + data.statusCode + ". Bailing out of trying to parse response.");
2500
2500
 
2501
+ // ⭐ FIX: Check for binary/gzipped response before parsing ⭐
2502
+ if (typeof data.body === 'string' &&
2503
+ (data.body.includes('\x00') || data.body.includes('\x1f\x8b') || data.body.includes('�') || data.body.includes('\x07'))) {
2504
+ log.warn("parseAndCheckLogin", "Binary/gzip response detected. Throwing recoverable error.");
2505
+ throw {
2506
+ error: "JSON.parse error. Check the `detail` property on this error.",
2507
+ detail: new Error("Binary response received"),
2508
+ res: data.body,
2509
+ isBinaryResponse: true
2510
+ };
2511
+ }
2512
+
2513
+ // Check for HTML login page (session expired)
2514
+ if (typeof data.body === 'string' &&
2515
+ (data.body.includes('<!DOCTYPE') || data.body.includes('<html') || data.body.includes('login_form'))) {
2516
+ log.warn("parseAndCheckLogin", "HTML login page detected. Session may be expired.");
2517
+ throw {
2518
+ error: "Not logged in.",
2519
+ res: data.body.substring(0, 500)
2520
+ };
2521
+ }
2522
+
2501
2523
  var res = null;
2502
2524
  try {
2503
2525
  res = JSON.parse(makeParsable(data.body));
@@ -2531,7 +2553,7 @@ function parseAndCheckLogin(ctx, defaultFuncs, retryCount) {
2531
2553
  }
2532
2554
  }
2533
2555
 
2534
- if (res.error === 1357001) throw { error: "Not logged in." };
2556
+ if (res.error === 1357001 || res.error === 1357004) throw { error: "Not logged in." };
2535
2557
  return res;
2536
2558
  });
2537
2559
  };
@@ -1,243 +0,0 @@
1
- "use strict";
2
-
3
- const utils = require('../utils');
4
- // @NethWs3Dev
5
-
6
- const allowedProperties = {
7
- attachment: true,
8
- url: true,
9
- sticker: true,
10
- emoji: true,
11
- emojiSize: true,
12
- body: true,
13
- mentions: true,
14
- location: true,
15
- };
16
-
17
- module.exports = (defaultFuncs, api, ctx) => {
18
- async function uploadAttachment(attachments) {
19
- var uploads = [];
20
- for (var i = 0; i < attachments.length; i++) {
21
- if (!utils.isReadableStream(attachments[i])) {
22
- throw new Error("Attachment should be a readable stream and not " + utils.getType(attachments[i]) + ".");
23
- }
24
- const oksir = await defaultFuncs.postFormData("https://upload.facebook.com/ajax/mercury/upload.php", ctx.jar,{
25
- upload_1024: attachments[i],
26
- voice_clip: "true"
27
- }, {}).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
28
- if (oksir.error) {
29
- throw new Error(resData);
30
- }
31
- uploads.push(oksir.payload.metadata[0]);
32
- }
33
- return uploads;
34
- }
35
-
36
- async function getUrl(url) {
37
- const resData = await defaultFuncs.post("https://www.facebook.com/message_share_attachment/fromURI/", ctx.jar, {
38
- image_height: 960,
39
- image_width: 960,
40
- uri: url
41
- }).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
42
- if (!resData || resData.error || !resData.payload){
43
- throw new Error(resData);
44
- }
45
- }
46
-
47
- async function sendContent(form, threadID, isSingleUser, messageAndOTID, callback) {
48
- // There are three cases here:
49
- // 1. threadID is of type array, where we're starting a new group chat with users
50
- // specified in the array.
51
- // 2. User is sending a message to a specific user.
52
- // 3. No additional form params and the message goes to an existing group chat.
53
- if (utils.getType(threadID) === "Array") {
54
- for (var i = 0; i < threadID.length; i++) {
55
- form["specific_to_list[" + i + "]"] = "fbid:" + threadID[i];
56
- }
57
- form["specific_to_list[" + threadID.length + "]"] = "fbid:" + ctx.userID;
58
- form["client_thread_id"] = "root:" + messageAndOTID;
59
- utils.log("sendMessage", "Sending message to multiple users: " + threadID);
60
- } else {
61
- const threadIDStr = threadID.toString();
62
- // Check if it's a DM: doesn't start with numeric group ID pattern or explicitly marked as single user
63
- const isDM = !threadIDStr.match(/^\d{15,}$/) || isSingleUser === true;
64
-
65
- // This means that threadID is the id of a user, and the chat
66
- // is a single person chat
67
- if (isDM) {
68
- form["specific_to_list[0]"] = "fbid:" + threadID;
69
- form["specific_to_list[1]"] = "fbid:" + ctx.userID;
70
- form["other_user_fbid"] = threadID;
71
- } else {
72
- form["thread_fbid"] = threadID;
73
- }
74
- }
75
-
76
- if (ctx.globalOptions.pageID) {
77
- form["author"] = "fbid:" + ctx.globalOptions.pageID;
78
- form["specific_to_list[1]"] = "fbid:" + ctx.globalOptions.pageID;
79
- form["creator_info[creatorID]"] = ctx.userID;
80
- form["creator_info[creatorType]"] = "direct_admin";
81
- form["creator_info[labelType]"] = "sent_message";
82
- form["creator_info[pageID]"] = ctx.globalOptions.pageID;
83
- form["request_user_id"] = ctx.globalOptions.pageID;
84
- form["creator_info[profileURI]"] =
85
- "https://www.facebook.com/profile.php?id=" + ctx.userID;
86
- }
87
-
88
- const resData = await defaultFuncs.post("https://www.facebook.com/messaging/send/", ctx.jar, form).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
89
- if (!resData) {
90
- throw new Error("Send message failed.");
91
- }
92
- if (resData.error) {
93
- if (resData.error === 1545012) {
94
- utils.warn("sendMessage", "Got error 1545012. This might mean that you're not part of the conversation " + threadID);
95
- throw new Error(`Cannot send message to thread ${threadID}: Bot is not part of this conversation (Error 1545012)`);
96
- }
97
- throw new Error(resData);
98
- }
99
- const messageInfo = resData.payload.actions.reduce((p, v) => {
100
- return { threadID: v.thread_fbid, messageID: v.message_id, timestamp: v.timestamp } || p;
101
- }, null);
102
- return messageInfo;
103
- }
104
-
105
- return async (msg, threadID, callback, replyToMessage, isSingleUser = null) => {
106
- // Handle different parameter patterns for backward compatibility
107
- if (typeof callback === "string" || (callback && typeof callback === "object")) {
108
- // callback is actually replyToMessage, shift parameters
109
- isSingleUser = replyToMessage;
110
- replyToMessage = callback;
111
- callback = function() {};
112
- } else if (typeof callback !== "function") {
113
- callback = function() {};
114
- }
115
-
116
- let msgType = utils.getType(msg);
117
- let threadIDType = utils.getType(threadID);
118
- let messageIDType = utils.getType(replyToMessage);
119
- if (msgType !== "String" && msgType !== "Object") throw new Error("Message should be of type string or object and not " + msgType + ".");
120
- if (threadIDType !== "Array" && threadIDType !== "Number" && threadIDType !== "String") throw new Error("ThreadID should be of type number, string, or array and not " + threadIDType + ".");
121
- if (replyToMessage && messageIDType !== 'String' && messageIDType !== 'string') throw new Error("MessageID should be of type string and not " + messageIDType + ".");
122
- if (msgType === "String") {
123
- msg = { body: msg };
124
- }
125
-
126
- // Auto-detect if this is a DM if not explicitly specified
127
- if (isSingleUser === null && ctx.threadTypes && ctx.threadTypes[threadID]) {
128
- isSingleUser = ctx.threadTypes[threadID] === 'dm';
129
- } else if (isSingleUser === null) {
130
- // Fallback: check if threadID looks like a user ID (15 digits) vs group ID (longer)
131
- const threadIDStr = threadID.toString();
132
- isSingleUser = threadIDStr.length === 15 || !threadIDStr.match(/^\d{16,}$/);
133
- }
134
- let disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
135
- if (disallowedProperties.length > 0) {
136
- throw new Error("Dissallowed props: `" + disallowedProperties.join(", ") + "`");
137
- }
138
- let messageAndOTID = utils.generateOfflineThreadingID();
139
- let form = {
140
- client: "mercury",
141
- action_type: "ma-type:user-generated-message",
142
- author: "fbid:" + ctx.userID,
143
- timestamp: Date.now(),
144
- timestamp_absolute: "Today",
145
- timestamp_relative: utils.generateTimestampRelative(),
146
- timestamp_time_passed: "0",
147
- is_unread: false,
148
- is_cleared: false,
149
- is_forward: false,
150
- is_filtered_content: false,
151
- is_filtered_content_bh: false,
152
- is_filtered_content_account: false,
153
- is_filtered_content_quasar: false,
154
- is_filtered_content_invalid_app: false,
155
- is_spoof_warning: false,
156
- source: "source:chat:web",
157
- "source_tags[0]": "source:chat",
158
- ...(msg.body && {
159
- body: msg.body
160
- }),
161
- html_body: false,
162
- ui_push_phase: "V3",
163
- status: "0",
164
- offline_threading_id: messageAndOTID,
165
- message_id: messageAndOTID,
166
- threading_id: utils.generateThreadingID(ctx.clientID),
167
- "ephemeral_ttl_mode:": "0",
168
- manual_retry_cnt: "0",
169
- has_attachment: !!(msg.attachment || msg.url || msg.sticker),
170
- signatureID: utils.getSignatureID(),
171
- ...(replyToMessage && {
172
- replied_to_message_id: replyToMessage
173
- })
174
- };
175
-
176
- if (msg.location) {
177
- if (!msg.location.latitude || !msg.location.longitude) throw new Error("location property needs both latitude and longitude");
178
- form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
179
- form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
180
- form["location_attachment[is_current_location]"] = !!msg.location.current;
181
- }
182
- if (msg.sticker) {
183
- form["sticker_id"] = msg.sticker;
184
- }
185
- if (msg.attachment) {
186
- form.image_ids = [];
187
- form.gif_ids = [];
188
- form.file_ids = [];
189
- form.video_ids = [];
190
- form.audio_ids = [];
191
- if (utils.getType(msg.attachment) !== "Array") {
192
- msg.attachment = [msg.attachment];
193
- }
194
- const files = await uploadAttachment(msg.attachment);
195
- files.forEach(file => {
196
- const type = Object.keys(file)[0];
197
- form["" + type + "s"].push(file[type]);
198
- });
199
- }
200
- if (msg.url) {
201
- form["shareable_attachment[share_type]"] = "100";
202
- const params = await getUrl(msg.url);
203
- form["shareable_attachment[share_params]"] = params;
204
- }
205
- if (msg.emoji) {
206
- if (!msg.emojiSize) {
207
- msg.emojiSize = "medium";
208
- }
209
- if (msg.emojiSize !== "small" && msg.emojiSize !== "medium" && msg.emojiSize !== "large") {
210
- throw new Error("emojiSize property is invalid");
211
- }
212
- if (!form.body) {
213
- throw new Error("body is not empty");
214
- }
215
- form.body = msg.emoji;
216
- form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
217
- }
218
- if (msg.mentions) {
219
- for (let i = 0; i < msg.mentions.length; i++) {
220
- const mention = msg.mentions[i];
221
- const tag = mention.tag;
222
- if (typeof tag !== "string") {
223
- throw new Error("Mention tags must be strings.");
224
- }
225
- const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
226
- if (offset < 0) utils.warn("handleMention", 'Mention for "' + tag + '" not found in message string.');
227
- if (!mention.id) utils.warn("handleMention", "Mention id should be non-null.");
228
- const id = mention.id || 0;
229
- const emptyChar = '\u200E';
230
- form["body"] = emptyChar + msg.body;
231
- form["profile_xmd[" + i + "][offset]"] = offset + 1;
232
- form["profile_xmd[" + i + "][length]"] = tag.length;
233
- form["profile_xmd[" + i + "][id]"] = id;
234
- form["profile_xmd[" + i + "][type]"] = "p";
235
- }
236
- }
237
- const result = await sendContent(form, threadID, isSingleUser, messageAndOTID);
238
- if (callback && typeof callback === "function") {
239
- callback(null, result);
240
- }
241
- return result;
242
- };
243
- };