shadowx-fca 2.8.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/package.json +1 -1
- package/src/sendMessage.js +234 -398
- package/src/sendMessage2.js +0 -243
package/package.json
CHANGED
package/src/sendMessage.js
CHANGED
|
@@ -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
|
-
|
|
4
|
+
// @NethWs3Dev
|
|
8
5
|
|
|
9
6
|
const allowedProperties = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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/src/sendMessage2.js
DELETED
|
@@ -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
|
-
};
|