stfca 1.0.3 → 1.0.5

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/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
 
4
4
  All notable changes to ST-FCA will be documented in this file.
5
5
 
6
+ ## [1.0.4] - 2025-01-13
7
+
8
+ ### Added
9
+ - 🔄 Automatic update checking on package initialization
10
+ - ⚡ Non-blocking update process - doesn't interrupt user's bot startup
11
+ - 🎯 Update check runs once per session to avoid redundant checks
12
+ - 💡 Silent error handling for update checks
13
+
14
+ ### Changed
15
+ - Update checker now integrated directly into login flow
16
+ - Improved user experience with seamless auto-updates
17
+
6
18
  ## [1.0.3] - 2025-01-13
7
19
 
8
20
  ### Added
package/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  var utils = require("./utils");
4
4
  var cheerio = require("cheerio");
5
5
  var log = require("npmlog");
6
+ var { checkForFCAUpdate } = require("./checkUpdate");
6
7
  /*var { getThemeColors } = require("../../func/utils/log.js");
7
8
  var logger = require("../../func/utils/log.js");
8
9
  var { cra, cv, cb, co } = getThemeColors();*/
@@ -10,6 +11,7 @@ log.maxRecordSize = 100;
10
11
  var checkVerified = null;
11
12
  const Boolean_Option = ['online', 'selfListen', 'listenEvents', 'updatePresence', 'forceLogin', 'autoMarkDelivery', 'autoMarkRead', 'listenTyping', 'autoReconnect', 'emitReady'];
12
13
  global.ditconmemay = false;
14
+ global.stfcaUpdateChecked = false;
13
15
 
14
16
  function setOptions(globalOptions, options) {
15
17
  Object.keys(options).map(function (key) {
@@ -407,6 +409,14 @@ function loginHelper(appState, email, password, globalOptions, callback, prCallb
407
409
 
408
410
 
409
411
  function login(loginData, options, callback) {
412
+ // Check for updates (non-blocking, only once per session)
413
+ if (!global.stfcaUpdateChecked) {
414
+ global.stfcaUpdateChecked = true;
415
+ checkForFCAUpdate().catch(err => {
416
+ // Silently ignore update check errors to not block login
417
+ });
418
+ }
419
+
410
420
  if (utils.getType(options) === 'Function' || utils.getType(options) === 'AsyncFunction') {
411
421
  callback = options;
412
422
  options = {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  {
3
3
  "name": "stfca",
4
- "version": "1.0.3",
4
+ "version": "1.0.5",
5
5
  "description": "Unofficial Facebook Chat API for Node.js - Enhanced by ST | Sheikh Tamim",
6
6
  "main": "index.js",
7
7
  "scripts": {
@@ -0,0 +1,234 @@
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 = false) => {
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
+ let disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
126
+ if (disallowedProperties.length > 0) {
127
+ throw new Error("Dissallowed props: `" + disallowedProperties.join(", ") + "`");
128
+ }
129
+ let messageAndOTID = utils.generateOfflineThreadingID();
130
+ let form = {
131
+ client: "mercury",
132
+ action_type: "ma-type:user-generated-message",
133
+ author: "fbid:" + ctx.userID,
134
+ timestamp: Date.now(),
135
+ timestamp_absolute: "Today",
136
+ timestamp_relative: utils.generateTimestampRelative(),
137
+ timestamp_time_passed: "0",
138
+ is_unread: false,
139
+ is_cleared: false,
140
+ is_forward: false,
141
+ is_filtered_content: false,
142
+ is_filtered_content_bh: false,
143
+ is_filtered_content_account: false,
144
+ is_filtered_content_quasar: false,
145
+ is_filtered_content_invalid_app: false,
146
+ is_spoof_warning: false,
147
+ source: "source:chat:web",
148
+ "source_tags[0]": "source:chat",
149
+ ...(msg.body && {
150
+ body: msg.body
151
+ }),
152
+ html_body: false,
153
+ ui_push_phase: "V3",
154
+ status: "0",
155
+ offline_threading_id: messageAndOTID,
156
+ message_id: messageAndOTID,
157
+ threading_id: utils.generateThreadingID(ctx.clientID),
158
+ "ephemeral_ttl_mode:": "0",
159
+ manual_retry_cnt: "0",
160
+ has_attachment: !!(msg.attachment || msg.url || msg.sticker),
161
+ signatureID: utils.getSignatureID(),
162
+ ...(replyToMessage && {
163
+ replied_to_message_id: replyToMessage
164
+ })
165
+ };
166
+
167
+ if (msg.location) {
168
+ if (!msg.location.latitude || !msg.location.longitude) throw new Error("location property needs both latitude and longitude");
169
+ form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
170
+ form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
171
+ form["location_attachment[is_current_location]"] = !!msg.location.current;
172
+ }
173
+ if (msg.sticker) {
174
+ form["sticker_id"] = msg.sticker;
175
+ }
176
+ if (msg.attachment) {
177
+ form.image_ids = [];
178
+ form.gif_ids = [];
179
+ form.file_ids = [];
180
+ form.video_ids = [];
181
+ form.audio_ids = [];
182
+ if (utils.getType(msg.attachment) !== "Array") {
183
+ msg.attachment = [msg.attachment];
184
+ }
185
+ const files = await uploadAttachment(msg.attachment);
186
+ files.forEach(file => {
187
+ const type = Object.keys(file)[0];
188
+ form["" + type + "s"].push(file[type]);
189
+ });
190
+ }
191
+ if (msg.url) {
192
+ form["shareable_attachment[share_type]"] = "100";
193
+ const params = await getUrl(msg.url);
194
+ form["shareable_attachment[share_params]"] = params;
195
+ }
196
+ if (msg.emoji) {
197
+ if (!msg.emojiSize) {
198
+ msg.emojiSize = "medium";
199
+ }
200
+ if (msg.emojiSize !== "small" && msg.emojiSize !== "medium" && msg.emojiSize !== "large") {
201
+ throw new Error("emojiSize property is invalid");
202
+ }
203
+ if (!form.body) {
204
+ throw new Error("body is not empty");
205
+ }
206
+ form.body = msg.emoji;
207
+ form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
208
+ }
209
+ if (msg.mentions) {
210
+ for (let i = 0; i < msg.mentions.length; i++) {
211
+ const mention = msg.mentions[i];
212
+ const tag = mention.tag;
213
+ if (typeof tag !== "string") {
214
+ throw new Error("Mention tags must be strings.");
215
+ }
216
+ const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
217
+ if (offset < 0) utils.warn("handleMention", 'Mention for "' + tag + '" not found in message string.');
218
+ if (!mention.id) utils.warn("handleMention", "Mention id should be non-null.");
219
+ const id = mention.id || 0;
220
+ const emptyChar = '\u200E';
221
+ form["body"] = emptyChar + msg.body;
222
+ form["profile_xmd[" + i + "][offset]"] = offset + 1;
223
+ form["profile_xmd[" + i + "][length]"] = tag.length;
224
+ form["profile_xmd[" + i + "][id]"] = id;
225
+ form["profile_xmd[" + i + "][type]"] = "p";
226
+ }
227
+ }
228
+ const result = await sendContent(form, threadID, isSingleUser, messageAndOTID);
229
+ if (callback && typeof callback === "function") {
230
+ callback(null, result);
231
+ }
232
+ return result;
233
+ };
234
+ };
package/src/listenMqtt.js CHANGED
@@ -112,11 +112,37 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
112
112
  mqttClient.on('connect', function () {
113
113
  topics.forEach(topicsub => mqttClient.subscribe(topicsub));
114
114
 
115
- // Display connection success message with branding
116
- console.log('\n✅ ST-FCA MQTT Connected');
117
- console.log(`📍 Region: ${ctx.region || 'PNB'}`);
118
- console.log(`🔄 Auto-reconnect: ${ctx.globalOptions.autoReconnect ? 'Enabled' : 'Disabled'}${ctx.globalOptions.autoReconnect ? ' (reconnects every 3s on disconnect)' : ''}`);
119
- console.log('🎨 Maintained & Enhanced by ST | Sheikh Tamim\n');
115
+ // Display connection success message with branding and loading animation
116
+ const messages = [
117
+ '\n✅ ST-FCA MQTT Connected',
118
+ `📍 Region: ${ctx.region || 'PNB'}`,
119
+ `🔄 Auto-reconnect: ${ctx.globalOptions.autoReconnect ? 'Enabled' : 'Disabled'}${ctx.globalOptions.autoReconnect ? ' (reconnects every 3s on disconnect)' : ''}`,
120
+ `⏱️ MQTT Restart Interval: ${ctx.globalOptions.restartListenMqtt?.enable ? `${ctx.globalOptions.restartListenMqtt.timeRestart / 1000}s` : 'Disabled'}`,
121
+ '🎨 Maintained & Enhanced by ST | Sheikh Tamim\n'
122
+ ];
123
+
124
+ let index = 0;
125
+ const displayMessages = () => {
126
+ if (index < messages.length) {
127
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
128
+ let frameIndex = 0;
129
+
130
+ const loadingInterval = setInterval(() => {
131
+ process.stdout.write(`\r${frames[frameIndex]} Loading...`);
132
+ frameIndex = (frameIndex + 1) % frames.length;
133
+ }, 80);
134
+
135
+ setTimeout(() => {
136
+ clearInterval(loadingInterval);
137
+ process.stdout.write('\r' + ' '.repeat(20) + '\r');
138
+ console.log(messages[index]);
139
+ index++;
140
+ displayMessages();
141
+ }, 500);
142
+ }
143
+ };
144
+
145
+ displayMessages();
120
146
 
121
147
  var topic;
122
148
  var queue = {
@@ -220,6 +246,11 @@ function parseDelta(defaultFuncs, api, ctx, globalCallback, v) {
220
246
  let fmtMsg;
221
247
  try {
222
248
  fmtMsg = utils.formatDeltaMessage(v);
249
+ // Detect if it's a DM or group thread
250
+ const otherUserFbId = v.delta.messageMetadata.threadKey.otherUserFbId;
251
+ const threadFbId = v.delta.messageMetadata.threadKey.threadFbId;
252
+ fmtMsg.isSingleUser = !!otherUserFbId;
253
+ fmtMsg.isGroup = !!threadFbId;
223
254
  } catch (err) {
224
255
  return globalCallback({
225
256
  error: "Problem parsing message object.",
@@ -1,7 +1,8 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const { Readable } = require("stream");
4
- const log = require("npmlog");
1
+ "use strict";
2
+
3
+ const utils = require('../utils');
4
+ // @NethWs3Dev
5
+
5
6
  const allowedProperties = {
6
7
  attachment: true,
7
8
  url: true,
@@ -11,147 +12,59 @@ const allowedProperties = {
11
12
  body: true,
12
13
  mentions: true,
13
14
  location: true,
14
- asPage: true
15
15
  };
16
- const { isReadableStream } = require("../utils");
17
- const { parseAndCheckLogin } = require("../utils");
18
- const { getType, generateThreadingID, generateTimestampRelative, generateOfflineThreadingID, getSignatureID } = require("../utils");
19
16
 
20
- module.exports = function (defaultFuncs, api, ctx) {
21
- function toReadable(input) {
22
- if (isReadableStream(input)) return input;
23
- if (Buffer.isBuffer(input)) return Readable.from(input);
24
- if (typeof input === "string" && fs.existsSync(input) && fs.statSync(input).isFile()) return fs.createReadStream(path.resolve(input));
25
- throw { error: "Unsupported attachment input. Use stream/buffer/filepath." };
26
- }
27
-
28
- function uploadAttachment(attachments, callback) {
29
- const uploads = [];
30
- for (let i = 0; i < attachments.length; i++) {
31
- if (!isReadableStream(attachments[i])) throw { error: "Attachment should be a readable stream and not " + getType(attachments[i]) + "." };
32
- const form = { upload_1024: attachments[i], voice_clip: "true" };
33
- uploads.push(
34
- defaultFuncs
35
- .postFormData("https://upload.facebook.com/ajax/mercury/upload.php", ctx.jar, form, {}, {})
36
- .then(parseAndCheckLogin(ctx, defaultFuncs))
37
- .then(resData => {
38
- if (resData.error) throw resData;
39
- return resData.payload.metadata[0];
40
- })
41
- );
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]);
42
32
  }
43
- Promise.all(uploads)
44
- .then(resData => callback(null, resData))
45
- .catch(err => {
46
- log.error("uploadAttachment", err);
47
- callback(err);
48
- });
49
- }
50
-
51
- function getUrl(url, callback) {
52
- const form = { image_height: 960, image_width: 960, uri: url };
53
- defaultFuncs
54
- .post("https://www.facebook.com/message_share_attachment/fromURI/", ctx.jar, form)
55
- .then(parseAndCheckLogin(ctx, defaultFuncs))
56
- .then(resData => {
57
- if (resData.error) return callback(resData);
58
- if (!resData.payload) return callback({ error: "Invalid url" });
59
- callback(null, resData.payload.share_data.share_params);
60
- })
61
- .catch(err => {
62
- log.error("getUrl", err);
63
- callback(err);
64
- });
65
- }
66
-
67
- function sleep(ms) {
68
- return new Promise(r => setTimeout(r, ms));
69
- }
70
-
71
- async function postWithRetry(url, jar, form, tries = 3) {
72
- let lastErr;
73
- for (let i = 0; i < tries; i++) {
74
- try {
75
- const res = await defaultFuncs.post(url, jar, form).then(parseAndCheckLogin(ctx, defaultFuncs));
76
- if (res && !res.error) return res;
77
- lastErr = res;
78
- if (res && (res.error === 1545003 || res.error === 368)) await sleep(500 * (i + 1));
79
- else break;
80
- } catch (e) {
81
- lastErr = e;
82
- await sleep(500 * (i + 1));
83
- }
84
- }
85
- throw lastErr || { error: "Send failed" };
86
- }
87
-
88
- function applyPageAuthor(form, msg) {
89
- const pageID = msg && msg.asPage ? msg.asPage : ctx.globalOptions.pageID;
90
- if (!pageID) return;
91
- form["author"] = "fbid:" + pageID;
92
- form["specific_to_list[1]"] = "fbid:" + pageID;
93
- form["creator_info[creatorID]"] = ctx.userID;
94
- form["creator_info[creatorType]"] = "direct_admin";
95
- form["creator_info[labelType]"] = "sent_message";
96
- form["creator_info[pageID]"] = pageID;
97
- form["request_user_id"] = pageID;
98
- form["creator_info[profileURI]"] = "https://www.facebook.com/profile.php?id=" + ctx.userID;
99
- }
100
-
101
- function applyMentions(msg, form) {
102
- if (!msg.mentions || !msg.mentions.length) return;
103
- let body = typeof msg.body === "string" ? msg.body : "";
104
- const need = [];
105
- for (const m of msg.mentions) {
106
- const tag = String(m.tag || "");
107
- if (tag && !body.includes(tag)) need.push(tag);
108
- }
109
- if (need.length) body = (body ? body + " " : "") + need.join(" ");
110
- const emptyChar = "\u200E";
111
- form["body"] = emptyChar + body;
112
- let searchFrom = 0;
113
- msg.mentions.forEach((m, i) => {
114
- const tag = String(m.tag || "");
115
- const from = typeof m.fromIndex === "number" ? m.fromIndex : searchFrom;
116
- const off = Math.max(0, body.indexOf(tag, from));
117
- form[`profile_xmd[${i}][offset]`] = off + 1;
118
- form[`profile_xmd[${i}][length]`] = tag.length;
119
- form[`profile_xmd[${i}][id]`] = m.id || 0;
120
- form[`profile_xmd[${i}][type]`] = "p";
121
- searchFrom = off + tag.length;
122
- });
123
- }
124
-
125
- function finalizeHasAttachment(form) {
126
- const keys = ["image_ids", "gif_ids", "file_ids", "video_ids", "audio_ids", "sticker_id", "shareable_attachment[share_params]"];
127
- form.has_attachment = keys.some(k => k in form && (Array.isArray(form[k]) ? form[k].length > 0 : !!form[k]));
33
+ return uploads;
128
34
  }
129
35
 
130
- function extractMessageInfo(resData, fallbackThreadID) {
131
- let messageID = null;
132
- let threadFBID = null;
133
- let timestamp = null;
134
- const actions = resData && resData.payload && Array.isArray(resData.payload.actions) ? resData.payload.actions : null;
135
- if (actions && actions.length) {
136
- const v = actions.find(x => x && x.message_id) || actions[0];
137
- messageID = v && v.message_id ? v.message_id : null;
138
- threadFBID = (v && (v.thread_fbid || v.thread_id)) || fallbackThreadID || null;
139
- timestamp = v && v.timestamp ? v.timestamp : null;
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);
140
44
  }
141
- if (!messageID) messageID = (resData && resData.payload && resData.payload.message_id) || resData.message_id || null;
142
- if (!threadFBID) threadFBID = (resData && resData.payload && resData.payload.thread_id) || fallbackThreadID || null;
143
- if (!timestamp) timestamp = (resData && resData.timestamp) || Date.now();
144
- if (!messageID) return null;
145
- return { threadID: threadFBID, messageID, timestamp };
146
45
  }
147
46
 
148
- function sendContent(form, threadID, isSingleUser, messageAndOTID, callback) {
149
- if (getType(threadID) === "Array") {
150
- for (let i = 0; i < threadID.length; i++) form["specific_to_list[" + i + "]"] = "fbid:" + threadID[i];
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
+ }
151
57
  form["specific_to_list[" + threadID.length + "]"] = "fbid:" + ctx.userID;
152
58
  form["client_thread_id"] = "root:" + messageAndOTID;
59
+ utils.log("sendMessage", "Sending message to multiple users: " + threadID);
153
60
  } else {
154
- if (isSingleUser) {
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) {
155
68
  form["specific_to_list[0]"] = "fbid:" + threadID;
156
69
  form["specific_to_list[1]"] = "fbid:" + ctx.userID;
157
70
  form["other_user_fbid"] = threadID;
@@ -159,182 +72,68 @@ module.exports = function (defaultFuncs, api, ctx) {
159
72
  form["thread_fbid"] = threadID;
160
73
  }
161
74
  }
162
- postWithRetry("https://www.facebook.com/messaging/send/", ctx.jar, form)
163
- .then(resData => {
164
- if (!resData) return callback({ error: "Send message failed." });
165
- if (resData.error) {
166
- if (resData.error === 1545012) log.warn("sendMessage", "Got error 1545012. This might mean that you're not part of the conversation " + threadID);
167
- else log.error("sendMessage", resData);
168
- return callback(resData);
169
- }
170
- const info = extractMessageInfo(resData, getType(threadID) === "Array" ? null : String(threadID));
171
- if (!info) return callback({ error: "Cannot parse message info." });
172
- callback(null, info);
173
- })
174
- .catch(err => {
175
- log.error("sendMessage", err);
176
- if (getType(err) === "Object" && err.error === "Not logged in.") ctx.loggedIn = false;
177
- callback(err);
178
- });
179
- }
180
75
 
181
- function sendOnce(baseForm, threadID, isSingleUser) {
182
- const otid = generateOfflineThreadingID();
183
- const form = { ...baseForm, offline_threading_id: otid, message_id: otid };
184
- return new Promise((resolve, reject) => {
185
- sendContent(form, threadID, isSingleUser, otid, (err, info) => (err ? reject(err) : resolve(info)));
186
- });
187
- }
188
-
189
- function send(form, threadID, messageAndOTID, callback, isGroup) {
190
- if (getType(threadID) === "Array") return sendContent(form, threadID, false, messageAndOTID, callback);
191
- if (getType(isGroup) === "Boolean") return sendContent(form, threadID, !isGroup, messageAndOTID, callback);
192
- sendOnce(form, threadID, false)
193
- .then(info => callback(null, info))
194
- .catch(() => {
195
- sendOnce(form, threadID, true)
196
- .then(info => callback(null, info))
197
- .catch(err => callback(err));
198
- });
199
- }
200
-
201
- function handleUrl(msg, form, callback, cb) {
202
- if (msg.url) {
203
- form["shareable_attachment[share_type]"] = "100";
204
- getUrl(msg.url, function (err, params) {
205
- if (err) return callback(err);
206
- form["shareable_attachment[share_params]"] = params;
207
- cb();
208
- });
209
- } else cb();
210
- }
211
-
212
- function handleLocation(msg, form, callback, cb) {
213
- if (msg.location) {
214
- if (msg.location.latitude == null || msg.location.longitude == null) return callback({ error: "location property needs both latitude and longitude" });
215
- form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
216
- form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
217
- form["location_attachment[is_current_location]"] = !!msg.location.current;
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;
218
86
  }
219
- cb();
220
- }
221
-
222
- function handleSticker(msg, form, callback, cb) {
223
- if (msg.sticker) form["sticker_id"] = msg.sticker;
224
- cb();
225
- }
226
87
 
227
- function handleEmoji(msg, form, callback, cb) {
228
- if (msg.emojiSize != null && msg.emoji == null) return callback({ error: "emoji property is empty" });
229
- if (msg.emoji) {
230
- if (msg.emojiSize == null) msg.emojiSize = "medium";
231
- if (msg.emojiSize !== "small" && msg.emojiSize !== "medium" && msg.emojiSize !== "large") return callback({ error: "emojiSize property is invalid" });
232
- if (form["body"] != null && form["body"] !== "") return callback({ error: "body is not empty" });
233
- form["body"] = msg.emoji;
234
- form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
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.");
235
91
  }
236
- cb();
237
- }
238
-
239
- function splitAttachments(list) {
240
- if (!Array.isArray(list)) list = [list];
241
- const ids = [];
242
- const streams = [];
243
- for (const a of list) {
244
- if (Array.isArray(a) && /_id$/.test(a[0])) {
245
- ids.push([a[0], String(a[1])]);
246
- continue;
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)`);
247
96
  }
248
- if (a && typeof a === "object") {
249
- if (a.id && a.type && /_id$/.test(a.type)) {
250
- ids.push([a.type, String(a.id)]);
251
- continue;
252
- }
253
- const k = Object.keys(a || {}).find(x => /_id$/.test(x));
254
- if (k) {
255
- ids.push([k, String(a[k])]);
256
- continue;
257
- }
258
- }
259
- streams.push(toReadable(a));
97
+ throw new Error(resData);
260
98
  }
261
- return { ids, streams };
262
- }
263
-
264
- function handleAttachment(msg, form, callback, cb) {
265
- if (!msg.attachment) return cb();
266
- form["image_ids"] = [];
267
- form["gif_ids"] = [];
268
- form["file_ids"] = [];
269
- form["video_ids"] = [];
270
- form["audio_ids"] = [];
271
- const { ids, streams } = splitAttachments(msg.attachment);
272
- for (const [type, id] of ids) form[`${type}s`].push(id);
273
- if (!streams.length) return cb();
274
- uploadAttachment(streams, function (err, files) {
275
- if (err) return callback(err);
276
- files.forEach(function (file) {
277
- const type = Object.keys(file)[0];
278
- form[type + "s"].push(file[type]);
279
- });
280
- cb();
281
- });
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;
282
103
  }
283
104
 
284
- function handleMention(msg, form, callback, cb) {
285
- try {
286
- applyMentions(msg, form);
287
- cb();
288
- } catch (e) {
289
- callback(e);
290
- }
291
- }
292
-
293
- return function sendMessage(msg, threadID, callback, replyToMessage, isGroup) {
294
- const isFn = v => typeof v === "function";
295
- const isStr = v => typeof v === "string";
296
-
297
- if (typeof isGroup === "undefined") isGroup = null;
298
- if (!callback && (getType(threadID) === "Function" || getType(threadID) === "AsyncFunction")) return threadID({ error: "Pass a threadID as a second argument." });
299
-
300
- if (isStr(callback) && isFn(replyToMessage)) {
301
- const t = callback;
302
- callback = replyToMessage;
303
- replyToMessage = t;
304
- } else if (!replyToMessage && isStr(callback)) {
105
+ return async (msg, threadID, callback, replyToMessage, isSingleUser = false) => {
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;
305
110
  replyToMessage = callback;
306
- callback = null;
111
+ callback = function() {};
112
+ } else if (typeof callback !== "function") {
113
+ callback = function() {};
307
114
  }
308
115
 
309
- let resolveFunc = function () { };
310
- let rejectFunc = function () { };
311
- const returnPromise = new Promise(function (resolve, reject) {
312
- resolveFunc = resolve;
313
- rejectFunc = reject;
314
- });
315
- if (!callback) {
316
- callback = function (err, data) {
317
- if (err) return rejectFunc(err);
318
- resolveFunc(data);
319
- };
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 };
320
124
  }
321
- const msgType = getType(msg);
322
- const threadIDType = getType(threadID);
323
- const messageIDType = getType(replyToMessage);
324
- if (msgType !== "String" && msgType !== "Object") return callback({ error: "Message should be of type string or object and not " + msgType + "." });
325
- if (threadIDType !== "Array" && threadIDType !== "Number" && threadIDType !== "String") return callback({ error: "ThreadID should be of type number, string, or array and not " + threadIDType + "." });
326
- if (replyToMessage && messageIDType !== "String") return callback({ error: "MessageID should be of type string and not " + threadIDType + "." });
327
- if (msgType === "String") msg = { body: msg };
328
- const disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
329
- if (disallowedProperties.length > 0) return callback({ error: "Disallowed props: `" + disallowedProperties.join(", ") + "`" });
330
- const messageAndOTID = generateOfflineThreadingID();
331
- const form = {
125
+ let disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
126
+ if (disallowedProperties.length > 0) {
127
+ throw new Error("Dissallowed props: `" + disallowedProperties.join(", ") + "`");
128
+ }
129
+ let messageAndOTID = utils.generateOfflineThreadingID();
130
+ let form = {
332
131
  client: "mercury",
333
132
  action_type: "ma-type:user-generated-message",
334
133
  author: "fbid:" + ctx.userID,
335
134
  timestamp: Date.now(),
336
135
  timestamp_absolute: "Today",
337
- timestamp_relative: generateTimestampRelative(),
136
+ timestamp_relative: utils.generateTimestampRelative(),
338
137
  timestamp_time_passed: "0",
339
138
  is_unread: false,
340
139
  is_cleared: false,
@@ -347,33 +146,89 @@ module.exports = function (defaultFuncs, api, ctx) {
347
146
  is_spoof_warning: false,
348
147
  source: "source:chat:web",
349
148
  "source_tags[0]": "source:chat",
350
- body: msg.body ? msg.body.toString() : "",
149
+ ...(msg.body && {
150
+ body: msg.body
151
+ }),
351
152
  html_body: false,
352
153
  ui_push_phase: "V3",
353
154
  status: "0",
354
155
  offline_threading_id: messageAndOTID,
355
156
  message_id: messageAndOTID,
356
- threading_id: generateThreadingID(ctx.clientID),
357
- ephemeral_ttl_mode: "0",
157
+ threading_id: utils.generateThreadingID(ctx.clientID),
158
+ "ephemeral_ttl_mode:": "0",
358
159
  manual_retry_cnt: "0",
359
- signatureID: getSignatureID(),
360
- replied_to_message_id: replyToMessage ? replyToMessage.toString() : ""
160
+ has_attachment: !!(msg.attachment || msg.url || msg.sticker),
161
+ signatureID: utils.getSignatureID(),
162
+ ...(replyToMessage && {
163
+ replied_to_message_id: replyToMessage
164
+ })
361
165
  };
362
- applyPageAuthor(form, msg);
363
- handleLocation(msg, form, callback, () =>
364
- handleSticker(msg, form, callback, () =>
365
- handleAttachment(msg, form, callback, () =>
366
- handleUrl(msg, form, callback, () =>
367
- handleEmoji(msg, form, callback, () =>
368
- handleMention(msg, form, callback, () => {
369
- finalizeHasAttachment(form);
370
- send(form, threadID, messageAndOTID, callback, isGroup);
371
- })
372
- )
373
- )
374
- )
375
- )
376
- );
377
- return returnPromise;
166
+
167
+ if (msg.location) {
168
+ if (!msg.location.latitude || !msg.location.longitude) throw new Error("location property needs both latitude and longitude");
169
+ form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
170
+ form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
171
+ form["location_attachment[is_current_location]"] = !!msg.location.current;
172
+ }
173
+ if (msg.sticker) {
174
+ form["sticker_id"] = msg.sticker;
175
+ }
176
+ if (msg.attachment) {
177
+ form.image_ids = [];
178
+ form.gif_ids = [];
179
+ form.file_ids = [];
180
+ form.video_ids = [];
181
+ form.audio_ids = [];
182
+ if (utils.getType(msg.attachment) !== "Array") {
183
+ msg.attachment = [msg.attachment];
184
+ }
185
+ const files = await uploadAttachment(msg.attachment);
186
+ files.forEach(file => {
187
+ const type = Object.keys(file)[0];
188
+ form["" + type + "s"].push(file[type]);
189
+ });
190
+ }
191
+ if (msg.url) {
192
+ form["shareable_attachment[share_type]"] = "100";
193
+ const params = await getUrl(msg.url);
194
+ form["shareable_attachment[share_params]"] = params;
195
+ }
196
+ if (msg.emoji) {
197
+ if (!msg.emojiSize) {
198
+ msg.emojiSize = "medium";
199
+ }
200
+ if (msg.emojiSize !== "small" && msg.emojiSize !== "medium" && msg.emojiSize !== "large") {
201
+ throw new Error("emojiSize property is invalid");
202
+ }
203
+ if (!form.body) {
204
+ throw new Error("body is not empty");
205
+ }
206
+ form.body = msg.emoji;
207
+ form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
208
+ }
209
+ if (msg.mentions) {
210
+ for (let i = 0; i < msg.mentions.length; i++) {
211
+ const mention = msg.mentions[i];
212
+ const tag = mention.tag;
213
+ if (typeof tag !== "string") {
214
+ throw new Error("Mention tags must be strings.");
215
+ }
216
+ const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
217
+ if (offset < 0) utils.warn("handleMention", 'Mention for "' + tag + '" not found in message string.');
218
+ if (!mention.id) utils.warn("handleMention", "Mention id should be non-null.");
219
+ const id = mention.id || 0;
220
+ const emptyChar = '\u200E';
221
+ form["body"] = emptyChar + msg.body;
222
+ form["profile_xmd[" + i + "][offset]"] = offset + 1;
223
+ form["profile_xmd[" + i + "][length]"] = tag.length;
224
+ form["profile_xmd[" + i + "][id]"] = id;
225
+ form["profile_xmd[" + i + "][type]"] = "p";
226
+ }
227
+ }
228
+ const result = await sendContent(form, threadID, isSingleUser, messageAndOTID);
229
+ if (callback && typeof callback === "function") {
230
+ callback(null, result);
231
+ }
232
+ return result;
378
233
  };
379
234
  };