stfca 1.0.3 → 1.0.4
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/OldMessage.js +234 -0
- package/src/listenMqtt.js +36 -5
- package/src/sendMessage.js +169 -314
package/package.json
CHANGED
|
@@ -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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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.",
|
package/src/sendMessage.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
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 =
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
228
|
-
if (
|
|
229
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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 =
|
|
111
|
+
callback = function() {};
|
|
112
|
+
} else if (typeof callback !== "function") {
|
|
113
|
+
callback = function() {};
|
|
307
114
|
}
|
|
308
115
|
|
|
309
|
-
let
|
|
310
|
-
let
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
};
|