stfca 1.0.6 → 1.0.7

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/index.js CHANGED
@@ -174,7 +174,8 @@ function buildAPI(globalOptions, html, jar) {
174
174
  callback_Task: {},
175
175
  wsReqNumber: 0,
176
176
  wsTaskNumber: 0,
177
- reqCallbacks: {}
177
+ reqCallbacks: {},
178
+ threadTypes: {} // Store thread type (dm/group) for each thread
178
179
  };
179
180
  var api = {
180
181
  setOptions: setOptions.bind(null, globalOptions),
@@ -222,6 +223,26 @@ function buildAPI(globalOptions, html, jar) {
222
223
  };
223
224
  //if (noMqttData) api.htmlData = noMqttData;
224
225
  require('fs').readdirSync(__dirname + '/src/').filter(v => v.endsWith('.js')).forEach(v => { api[v.replace('.js', '')] = require(`./src/${v}`)(utils.makeDefaults(html, userID, ctx), api, ctx); });
226
+
227
+ // Store original sendMessage as the primary method
228
+ const originalSendMessage = api.sendMessage;
229
+
230
+ // Wrap sendMessage to use OldMessage as fallback on error
231
+ api.sendMessage = async function(msg, threadID, callback, replyToMessage, isSingleUser) {
232
+ try {
233
+ return await originalSendMessage(msg, threadID, callback, replyToMessage, isSingleUser);
234
+ } catch (error) {
235
+ // If modern method fails, fallback to OldMessage
236
+ console.log('sendMessage failed, using OldMessage fallback:', error.message);
237
+ return api.OldMessage(msg, threadID, callback, replyToMessage, isSingleUser);
238
+ }
239
+ };
240
+
241
+ // Provide explicit method for DM sending using OldMessage
242
+ api.sendMessageDM = function(msg, threadID, callback, replyToMessage) {
243
+ return api.OldMessage(msg, threadID, callback, replyToMessage, true);
244
+ };
245
+
225
246
  api.listen = api.listenMqtt;
226
247
  return {
227
248
  ctx,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stfca",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Unofficial Facebook Chat API for Node.js - Enhanced by ST | Sheikh Tamim",
5
5
  "main": "index.js",
6
6
  "files": [
package/src/OldMessage.js CHANGED
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
 
3
- const utils = require('../utils');
4
- // @NethWs3Dev
3
+ var utils = require("../utils");
4
+ var log = require("npmlog");
5
+ var bluebird = require("bluebird");
5
6
 
6
- const allowedProperties = {
7
+ var allowedProperties = {
7
8
  attachment: true,
8
9
  url: true,
9
10
  sticker: true,
@@ -14,63 +15,83 @@ const allowedProperties = {
14
15
  location: true,
15
16
  };
16
17
 
17
- module.exports = (defaultFuncs, api, ctx) => {
18
- async function uploadAttachment(attachments) {
18
+ module.exports = function (defaultFuncs, api, ctx) {
19
+ function uploadAttachment(attachments, callback) {
19
20
  var uploads = [];
21
+
22
+ // create an array of promises
20
23
  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]);
24
+ if (!utils.isReadableStream(attachments[i])) throw { error: "Attachment should be a readable stream and not " + utils.getType(attachments[i]) + "." };
25
+ var form = {
26
+ upload_1024: attachments[i],
27
+ voice_clip: "true"
28
+ };
29
+
30
+ uploads.push(
31
+ defaultFuncs
32
+ .postFormData("https://upload.facebook.com/ajax/mercury/upload.php", ctx.jar, form, {}, {})
33
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
34
+ .then(function (resData) {
35
+ if (resData.error) throw resData;
36
+ // We have to return the data unformatted unless we want to change it
37
+ // back in sendMessage.
38
+ return resData.payload.metadata[0];
39
+ })
40
+ );
32
41
  }
33
- return uploads;
42
+
43
+ // resolve all promises
44
+ bluebird
45
+ .all(uploads)
46
+ .then(resData => callback(null, resData))
47
+ .catch(function (err) {
48
+ log.error("uploadAttachment", err);
49
+ return callback(err);
50
+ });
34
51
  }
35
52
 
36
- async function getUrl(url) {
37
- const resData = await defaultFuncs.post("https://www.facebook.com/message_share_attachment/fromURI/", ctx.jar, {
53
+ function getUrl(url, callback) {
54
+ var form = {
38
55
  image_height: 960,
39
56
  image_width: 960,
40
57
  uri: url
41
- }).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
42
- if (!resData || resData.error || !resData.payload){
43
- throw new Error(resData);
44
- }
58
+ };
59
+
60
+ defaultFuncs
61
+ .post("https://www.facebook.com/message_share_attachment/fromURI/", ctx.jar, form)
62
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
63
+ .then(function (resData) {
64
+ if (resData.error) return callback(resData);
65
+ if (!resData.payload) return callback({ error: "Invalid url" });
66
+ callback(null, resData.payload.share_data.share_params);
67
+ })
68
+ .catch(function (err) {
69
+ log.error("getUrl", err);
70
+ return callback(err);
71
+ });
45
72
  }
46
73
 
47
- async function sendContent(form, threadID, isSingleUser, messageAndOTID, callback) {
74
+ function sendContent(form, threadID, isSingleUser, messageAndOTID, callback) {
48
75
  // There are three cases here:
49
76
  // 1. threadID is of type array, where we're starting a new group chat with users
50
77
  // specified in the array.
51
78
  // 2. User is sending a message to a specific user.
52
79
  // 3. No additional form params and the message goes to an existing group chat.
53
80
  if (utils.getType(threadID) === "Array") {
54
- for (var i = 0; i < threadID.length; i++) {
55
- form["specific_to_list[" + i + "]"] = "fbid:" + threadID[i];
56
- }
81
+ for (var i = 0; i < threadID.length; i++) form["specific_to_list[" + i + "]"] = "fbid:" + threadID[i];
57
82
  form["specific_to_list[" + threadID.length + "]"] = "fbid:" + ctx.userID;
58
83
  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
-
84
+ log.info("sendMessage", "Sending message to multiple users: " + threadID);
85
+ }
86
+ else {
65
87
  // This means that threadID is the id of a user, and the chat
66
88
  // is a single person chat
67
- if (isDM) {
89
+ if (isSingleUser) {
68
90
  form["specific_to_list[0]"] = "fbid:" + threadID;
69
91
  form["specific_to_list[1]"] = "fbid:" + ctx.userID;
70
92
  form["other_user_fbid"] = threadID;
71
- } else {
72
- form["thread_fbid"] = threadID;
73
93
  }
94
+ else form["thread_fbid"] = threadID;
74
95
  }
75
96
 
76
97
  if (ctx.globalOptions.pageID) {
@@ -81,53 +102,182 @@ module.exports = (defaultFuncs, api, ctx) => {
81
102
  form["creator_info[labelType]"] = "sent_message";
82
103
  form["creator_info[pageID]"] = ctx.globalOptions.pageID;
83
104
  form["request_user_id"] = ctx.globalOptions.pageID;
84
- form["creator_info[profileURI]"] =
85
- "https://www.facebook.com/profile.php?id=" + ctx.userID;
105
+ form["creator_info[profileURI]"] = "https://www.facebook.com/profile.php?id=" + ctx.userID;
86
106
  }
87
107
 
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.");
108
+ defaultFuncs
109
+ .post("https://www.facebook.com/messaging/send/", ctx.jar, form)
110
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
111
+ .then(function (resData) {
112
+ if (!resData) return callback({ error: "Send message failed." });
113
+ if (resData.error) {
114
+ if (resData.error === 1545012) {
115
+ log.warn("sendMessage", "Got error 1545012. This might mean that you're not part of the conversation " + threadID);
116
+ }
117
+ else {
118
+ log.error("sendMessage", resData);
119
+ }
120
+ return callback(resData);
121
+ }
122
+
123
+ var messageInfo = resData.payload.actions.reduce(function (p, v) {
124
+ return (
125
+ {
126
+ threadID: v.thread_fbid,
127
+ messageID: v.message_id,
128
+ timestamp: v.timestamp
129
+ } || p
130
+ );
131
+ }, null);
132
+
133
+ return callback(null, messageInfo);
134
+ })
135
+ .catch(function (err) {
136
+ log.error("sendMessage", err);
137
+ if (utils.getType(err) == "Object" && err.error === "Not logged in.") ctx.loggedIn = false;
138
+ return callback(err);
139
+ });
140
+ }
141
+
142
+ function send(form, threadID, messageAndOTID, callback, isGroup) {
143
+ // We're doing a query to this to check if the given id is the id of
144
+ // a user or of a group chat. The form will be different depending
145
+ // on that.
146
+ if (utils.getType(threadID) === "Array") sendContent(form, threadID, false, messageAndOTID, callback);
147
+ else {
148
+ if (utils.getType(isGroup) != "Boolean") sendContent(form, threadID, threadID.length === 15, messageAndOTID, callback);
149
+ else sendContent(form, threadID, !isGroup, messageAndOTID, callback);
150
+ }
151
+ }
152
+
153
+ function handleUrl(msg, form, callback, cb) {
154
+ if (msg.url) {
155
+ form["shareable_attachment[share_type]"] = "100";
156
+ getUrl(msg.url, function (err, params) {
157
+ if (err) return callback(err);
158
+ form["shareable_attachment[share_params]"] = params;
159
+ cb();
160
+ });
91
161
  }
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)`);
162
+ else cb();
163
+ }
164
+
165
+ function handleLocation(msg, form, callback, cb) {
166
+ if (msg.location) {
167
+ if (msg.location.latitude == null || msg.location.longitude == null) return callback({ error: "location property needs both latitude and longitude" });
168
+ form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
169
+ form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
170
+ form["location_attachment[is_current_location]"] = !!msg.location.current;
171
+ }
172
+ cb();
173
+ }
174
+
175
+ function handleSticker(msg, form, callback, cb) {
176
+ if (msg.sticker) form["sticker_id"] = msg.sticker;
177
+ cb();
178
+ }
179
+
180
+ function handleEmoji(msg, form, callback, cb) {
181
+ if (msg.emojiSize != null && msg.emoji == null) return callback({ error: "emoji property is empty" });
182
+ if (msg.emoji) {
183
+ if (msg.emojiSize == null) msg.emojiSize = "medium";
184
+ if (msg.emojiSize != "small" && msg.emojiSize != "medium" && msg.emojiSize != "large") return callback({ error: "emojiSize property is invalid" });
185
+ if (form["body"] != null && form["body"] != "") return callback({ error: "body is not empty" });
186
+ form["body"] = msg.emoji;
187
+ form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
188
+ }
189
+ cb();
190
+ }
191
+
192
+ function handleAttachment(msg, form, callback, cb) {
193
+ if (msg.attachment) {
194
+ form["image_ids"] = [];
195
+ form["gif_ids"] = [];
196
+ form["file_ids"] = [];
197
+ form["video_ids"] = [];
198
+ form["audio_ids"] = [];
199
+
200
+ if (utils.getType(msg.attachment) !== "Array") msg.attachment = [msg.attachment];
201
+ if (msg.attachment.every(e=>/_id$/.test(e[0]))) {
202
+ //console.log(msg.attachment)
203
+ msg.attachment.map(e=>form[`${e[0]}s`].push(e[1]));
204
+ return cb();
205
+ }
206
+ uploadAttachment(msg.attachment, function (err, files) {
207
+ if (err) return callback(err);
208
+ files.forEach(function (file) {
209
+ var key = Object.keys(file);
210
+ var type = key[0]; // image_id, file_id, etc
211
+ form["" + type + "s"].push(file[type]); // push the id
212
+ });
213
+ cb();
214
+ });
215
+ }
216
+ else cb();
217
+ }
218
+
219
+ function handleMention(msg, form, callback, cb) {
220
+ if (msg.mentions) {
221
+ for (let i = 0; i < msg.mentions.length; i++) {
222
+ const mention = msg.mentions[i];
223
+ const tag = mention.tag;
224
+ if (typeof tag !== "string") return callback({ error: "Mention tags must be strings." });
225
+ const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
226
+ if (offset < 0) log.warn("handleMention", 'Mention for "' + tag + '" not found in message string.');
227
+ if (mention.id == null) log.warn("handleMention", "Mention id should be non-null.");
228
+
229
+ const id = mention.id || 0;
230
+ const emptyChar = '\u200E';
231
+ form["body"] = emptyChar + msg.body;
232
+ form["profile_xmd[" + i + "][offset]"] = offset + 1;
233
+ form["profile_xmd[" + i + "][length]"] = tag.length;
234
+ form["profile_xmd[" + i + "][id]"] = id;
235
+ form["profile_xmd[" + i + "][type]"] = "p";
96
236
  }
97
- throw new Error(resData);
98
237
  }
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;
238
+ cb();
103
239
  }
104
240
 
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;
241
+ return function sendMessage(msg, threadID, callback, replyToMessage, isGroup) {
242
+ typeof isGroup == "undefined" ? isGroup = null : "";
243
+ if (!callback && (utils.getType(threadID) === "Function" || utils.getType(threadID) === "AsyncFunction")) return threadID({ error: "Pass a threadID as a second argument." });
244
+ if (!replyToMessage && utils.getType(callback) === "String") {
110
245
  replyToMessage = callback;
111
- callback = function() {};
112
- } else if (typeof callback !== "function") {
113
- callback = function() {};
246
+ callback = function () { };
114
247
  }
115
248
 
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(", ") + "`");
249
+ var resolveFunc = function () { };
250
+ var rejectFunc = function () { };
251
+ var returnPromise = new Promise(function (resolve, reject) {
252
+ resolveFunc = resolve;
253
+ rejectFunc = reject;
254
+ });
255
+
256
+ if (!callback) {
257
+ callback = function (err, data) {
258
+ if (err) return rejectFunc(err);
259
+ resolveFunc(data);
260
+ };
128
261
  }
129
- let messageAndOTID = utils.generateOfflineThreadingID();
130
- let form = {
262
+
263
+ var msgType = utils.getType(msg);
264
+ var threadIDType = utils.getType(threadID);
265
+ var messageIDType = utils.getType(replyToMessage);
266
+
267
+ if (msgType !== "String" && msgType !== "Object") return callback({ error: "Message should be of type string or object and not " + msgType + "." });
268
+
269
+ // Changing this to accomodate an array of users
270
+ if (threadIDType !== "Array" && threadIDType !== "Number" && threadIDType !== "String") return callback({ error: "ThreadID should be of type number, string, or array and not " + threadIDType + "." });
271
+
272
+ if (replyToMessage && messageIDType !== 'String') return callback({ error: "MessageID should be of type string and not " + threadIDType + "." });
273
+
274
+ if (msgType === "String") msg = { body: msg };
275
+ var disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
276
+ if (disallowedProperties.length > 0) return callback({ error: "Dissallowed props: `" + disallowedProperties.join(", ") + "`" });
277
+
278
+ var messageAndOTID = utils.generateOfflineThreadingID();
279
+ // console.log(messageAndOTID)
280
+ var form = {
131
281
  client: "mercury",
132
282
  action_type: "ma-type:user-generated-message",
133
283
  author: "fbid:" + ctx.userID,
@@ -146,9 +296,7 @@ module.exports = (defaultFuncs, api, ctx) => {
146
296
  is_spoof_warning: false,
147
297
  source: "source:chat:web",
148
298
  "source_tags[0]": "source:chat",
149
- ...(msg.body && {
150
- body: msg.body
151
- }),
299
+ body: msg.body ? msg.body.toString() : "",
152
300
  html_body: false,
153
301
  ui_push_phase: "V3",
154
302
  status: "0",
@@ -159,76 +307,23 @@ module.exports = (defaultFuncs, api, ctx) => {
159
307
  manual_retry_cnt: "0",
160
308
  has_attachment: !!(msg.attachment || msg.url || msg.sticker),
161
309
  signatureID: utils.getSignatureID(),
162
- ...(replyToMessage && {
163
- replied_to_message_id: replyToMessage
164
- })
310
+ replied_to_message_id: replyToMessage
165
311
  };
312
+ // console.log(form)
166
313
 
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;
314
+ handleLocation(msg, form, callback, () =>
315
+ handleSticker(msg, form, callback, () =>
316
+ handleAttachment(msg, form, callback, () =>
317
+ handleUrl(msg, form, callback, () =>
318
+ handleEmoji(msg, form, callback, () =>
319
+ handleMention(msg, form, callback, () =>
320
+ send(form, threadID, messageAndOTID, callback, isGroup)
321
+ )
322
+ )
323
+ )
324
+ )
325
+ )
326
+ );
327
+ return returnPromise;
233
328
  };
234
329
  };
package/src/listenMqtt.js CHANGED
@@ -246,11 +246,17 @@ function parseDelta(defaultFuncs, api, ctx, globalCallback, v) {
246
246
  let fmtMsg;
247
247
  try {
248
248
  fmtMsg = utils.formatDeltaMessage(v);
249
- // Detect if it's a DM or group thread
249
+ // Detect if it's a DM or group thread - enhanced detection
250
250
  const otherUserFbId = v.delta.messageMetadata.threadKey.otherUserFbId;
251
251
  const threadFbId = v.delta.messageMetadata.threadKey.threadFbId;
252
- fmtMsg.isSingleUser = !!otherUserFbId;
252
+
253
+ // A thread is a DM if it has otherUserFbId and no threadFbId
254
+ fmtMsg.isSingleUser = !!otherUserFbId && !threadFbId;
253
255
  fmtMsg.isGroup = !!threadFbId;
256
+
257
+ // Store thread type in context for sendMessage to use
258
+ if (!ctx.threadTypes) ctx.threadTypes = {};
259
+ ctx.threadTypes[fmtMsg.threadID] = fmtMsg.isSingleUser ? 'dm' : 'group';
254
260
  } catch (err) {
255
261
  return globalCallback({
256
262
  error: "Problem parsing message object.",
@@ -102,7 +102,7 @@ module.exports = (defaultFuncs, api, ctx) => {
102
102
  return messageInfo;
103
103
  }
104
104
 
105
- return async (msg, threadID, callback, replyToMessage, isSingleUser = false) => {
105
+ return async (msg, threadID, callback, replyToMessage, isSingleUser = null) => {
106
106
  // Handle different parameter patterns for backward compatibility
107
107
  if (typeof callback === "string" || (callback && typeof callback === "object")) {
108
108
  // callback is actually replyToMessage, shift parameters
@@ -122,6 +122,15 @@ module.exports = (defaultFuncs, api, ctx) => {
122
122
  if (msgType === "String") {
123
123
  msg = { body: msg };
124
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
+ }
125
134
  let disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
126
135
  if (disallowedProperties.length > 0) {
127
136
  throw new Error("Dissallowed props: `" + disallowedProperties.join(", ") + "`");
package/utils.js CHANGED
@@ -733,17 +733,23 @@ function formatDeltaMessage(m) {
733
733
  var args = body == "" ? [] : body.trim().split(/\s+/);
734
734
  for (var i = 0; i < m_id.length; i++) mentions[m_id[i]] = m.delta.body.substring(m_offset[i], m_offset[i] + m_length[i]);
735
735
 
736
+ // Determine if this is a DM or group
737
+ const otherUserFbId = md.threadKey.otherUserFbId;
738
+ const threadFbId = md.threadKey.threadFbId;
739
+ const isSingleUser = !!otherUserFbId && !threadFbId;
740
+
736
741
  return {
737
742
  type: "message",
738
743
  senderID: formatID(md.actorFbId.toString()),
739
- threadID: formatID((md.threadKey.threadFbId || md.threadKey.otherUserFbId).toString()),
744
+ threadID: formatID((threadFbId || otherUserFbId).toString()),
740
745
  messageID: md.messageId,
741
746
  args: args,
742
747
  body: body,
743
748
  attachments: (m.delta.attachments || []).map(v => _formatAttachment(v)),
744
749
  mentions: mentions,
745
750
  timestamp: md.timestamp,
746
- isGroup: !!md.threadKey.threadFbId,
751
+ isGroup: !!threadFbId,
752
+ isSingleUser: isSingleUser,
747
753
  participantIDs: m.delta.participants || (md.cid ? md.cid.canonicalParticipantFbids : []) || []
748
754
  };
749
755
  }
@@ -753,6 +759,11 @@ function formatID(id) {
753
759
  else return id;
754
760
  }
755
761
 
762
+ function isDMThread(threadID) {
763
+ // DM threads don't have 't_' prefix and are typically numeric user IDs
764
+ return typeof threadID === 'string' && !threadID.includes('t_');
765
+ }
766
+
756
767
  function formatMessage(m) {
757
768
  var originalMessage = m.message ? m.message : m;
758
769
  var obj = {
@@ -2872,5 +2883,6 @@ module.exports = {
2872
2883
  decodeClientPayload,
2873
2884
  getAppState,
2874
2885
  getAdminTextMessageType,
2875
- setProxy
2886
+ setProxy,
2887
+ isDMThread
2876
2888
  };