ws3-fca
Version:
A node.js package for automating Facebook Messenger bot, and is one of the most advanced next-generation Facebook Chat API (FCA) by @NethWs3Dev & @ExocoreCommunity
216 lines (207 loc) • 8.52 kB
JavaScript
;
const utils = require('../../../utils');
// @NethWs3Dev
const allowedProperties = {
attachment: true,
url: true,
sticker: true,
emoji: true,
emojiSize: true,
body: true,
mentions: true,
location: true,
};
module.exports = (defaultFuncs, api, ctx) => {
async function uploadAttachment(attachments) {
var uploads = [];
for (var i = 0; i < attachments.length; i++) {
if (!utils.isReadableStream(attachments[i])) {
throw new Error("Attachment should be a readable stream and not " + utils.getType(attachments[i]) + ".");
}
const oksir = await defaultFuncs.postFormData("https://upload.facebook.com/ajax/mercury/upload.php", ctx.jar,{
upload_1024: attachments[i],
voice_clip: "true"
}, {}).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
if (oksir.error) {
throw new Error(resData);
}
uploads.push(oksir.payload.metadata[0]);
}
return uploads;
}
async function getUrl(url) {
const resData = await defaultFuncs.post("https://www.facebook.com/message_share_attachment/fromURI/", ctx.jar, {
image_height: 960,
image_width: 960,
uri: url
}).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
if (!resData || resData.error || !resData.payload){
throw new Error(resData);
}
}
async function sendContent(form, threadID, isSingleUser, messageAndOTID, callback) {
// There are three cases here:
// 1. threadID is of type array, where we're starting a new group chat with users
// specified in the array.
// 2. User is sending a message to a specific user.
// 3. No additional form params and the message goes to an existing group chat.
if (utils.getType(threadID) === "Array") {
for (var i = 0; i < threadID.length; i++) {
form["specific_to_list[" + i + "]"] = "fbid:" + threadID[i];
}
form["specific_to_list[" + threadID.length + "]"] = "fbid:" + ctx.userID;
form["client_thread_id"] = "root:" + messageAndOTID;
utils.log("sendMessage", "Sending message to multiple users: " + threadID);
} else {
// This means that threadID is the id of a user, and the chat
// is a single person chat
if (isSingleUser) {
form["specific_to_list[0]"] = "fbid:" + threadID;
form["specific_to_list[1]"] = "fbid:" + ctx.userID;
form["other_user_fbid"] = threadID;
} else {
form["thread_fbid"] = threadID;
}
}
if (ctx.globalOptions.pageID) {
form["author"] = "fbid:" + ctx.globalOptions.pageID;
form["specific_to_list[1]"] = "fbid:" + ctx.globalOptions.pageID;
form["creator_info[creatorID]"] = ctx.userID;
form["creator_info[creatorType]"] = "direct_admin";
form["creator_info[labelType]"] = "sent_message";
form["creator_info[pageID]"] = ctx.globalOptions.pageID;
form["request_user_id"] = ctx.globalOptions.pageID;
form["creator_info[profileURI]"] =
"https://www.facebook.com/profile.php?id=" + ctx.userID;
}
const resData = await defaultFuncs.post("https://www.facebook.com/messaging/send/", ctx.jar, form).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
if (!resData) {
throw new Error("Send message failed.");
}
if (resData.error) {
if (resData.error === 1545012) {
utils.warn("sendMessage", "Got error 1545012. This might mean that you're not part of the conversation " + threadID);
}
throw new Error(resData);
}
const messageInfo = resData.payload.actions.reduce((p, v) => {
return { threadID: v.thread_fbid, messageID: v.message_id, timestamp: v.timestamp } || p;
}, null);
return messageInfo;
}
return async (msg, threadID, replyToMessage, isSingleUser = false) => {
let msgType = utils.getType(msg);
let threadIDType = utils.getType(threadID);
let messageIDType = utils.getType(replyToMessage);
if (msgType !== "String" && msgType !== "Object") throw new Error("Message should be of type string or object and not " + msgType + ".");
if (threadIDType !== "Array" && threadIDType !== "Number" && threadIDType !== "String") throw new Error("ThreadID should be of type number, string, or array and not " + threadIDType + ".");
if (replyToMessage && messageIDType !== 'String') throw new Error("MessageID should be of type string and not " + threadIDType + ".");
if (msgType === "String") {
msg = { body: msg };
}
let disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
if (disallowedProperties.length > 0) {
throw new Error("Dissallowed props: `" + disallowedProperties.join(", ") + "`");
}
let messageAndOTID = utils.generateOfflineThreadingID();
let form = {
client: "mercury",
action_type: "ma-type:user-generated-message",
author: "fbid:" + ctx.userID,
timestamp: Date.now(),
timestamp_absolute: "Today",
timestamp_relative: utils.generateTimestampRelative(),
timestamp_time_passed: "0",
is_unread: false,
is_cleared: false,
is_forward: false,
is_filtered_content: false,
is_filtered_content_bh: false,
is_filtered_content_account: false,
is_filtered_content_quasar: false,
is_filtered_content_invalid_app: false,
is_spoof_warning: false,
source: "source:chat:web",
"source_tags[0]": "source:chat",
...(msg.body && {
body: msg.body
}),
html_body: false,
ui_push_phase: "V3",
status: "0",
offline_threading_id: messageAndOTID,
message_id: messageAndOTID,
threading_id: utils.generateThreadingID(ctx.clientID),
"ephemeral_ttl_mode:": "0",
manual_retry_cnt: "0",
has_attachment: !!(msg.attachment || msg.url || msg.sticker),
signatureID: utils.getSignatureID(),
...(replyToMessage && {
replied_to_message_id: replyToMessage
})
};
if (msg.location) {
if (!msg.location.latitude || !msg.location.longitude) throw new Error("location property needs both latitude and longitude");
form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
form["location_attachment[is_current_location]"] = !!msg.location.current;
}
if (msg.sticker) {
form["sticker_id"] = msg.sticker;
}
if (msg.attachment) {
form.image_ids = [];
form.gif_ids = [];
form.file_ids = [];
form.video_ids = [];
form.audio_ids = [];
if (utils.getType(msg.attachment) !== "Array") {
msg.attachment = [msg.attachment];
}
const files = await uploadAttachment(msg.attachment);
files.forEach(file => {
const type = Object.keys(file)[0];
form["" + type + "s"].push(file[type]);
});
}
if (msg.url) {
form["shareable_attachment[share_type]"] = "100";
const params = await getUrl(msg.url);
form["shareable_attachment[share_params]"] = params;
}
if (msg.emoji) {
if (!msg.emojiSize) {
msg.emojiSize = "medium";
}
if (msg.emojiSize !== "small" && msg.emojiSize !== "medium" && msg.emojiSize !== "large") {
throw new Error("emojiSize property is invalid");
}
if (!form.body) {
throw new Error("body is not empty");
}
form.body = msg.emoji;
form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
}
if (msg.mentions) {
for (let i = 0; i < msg.mentions.length; i++) {
const mention = msg.mentions[i];
const tag = mention.tag;
if (typeof tag !== "string") {
throw new Error("Mention tags must be strings.");
}
const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
if (offset < 0) utils.warn("handleMention", 'Mention for "' + tag + '" not found in message string.');
if (!mention.id) utils.warn("handleMention", "Mention id should be non-null.");
const id = mention.id || 0;
const emptyChar = '\u200E';
form["body"] = emptyChar + msg.body;
form["profile_xmd[" + i + "][offset]"] = offset + 1;
form["profile_xmd[" + i + "][length]"] = tag.length;
form["profile_xmd[" + i + "][id]"] = id;
form["profile_xmd[" + i + "][type]"] = "p";
}
}
const result = await sendContent(form, threadID, isSingleUser, messageAndOTID);
return result;
};
};