cyber-fca
Version:
A Facebook chat api for messenger bots.
330 lines (297 loc) • 11.5 kB
JavaScript
var utils = require("../utils");
var log = require("npmlog");
var bluebird = require("bluebird");
var allowedProperties = {
attachment: true,
url: true,
sticker: true,
emoji: true,
emojiSize: true,
body: true,
mentions: true,
location: true,
};
module.exports = function (defaultFuncs, api, ctx) {
function uploadAttachment(attachments, callback) {
var uploads = [];
// create an array of promises
for (var i = 0; i < attachments.length; i++) {
if (!utils.isReadableStream(attachments[i])) throw { error: "Attachment should be a readable stream and not " + utils.getType(attachments[i]) + "." };
var form = {
upload_1024: attachments[i],
voice_clip: "true"
};
uploads.push(
defaultFuncs
.postFormData("https://upload.facebook.com/ajax/mercury/upload.php", ctx.jar, form, {}, {})
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
.then(function (resData) {
if (resData.error) throw resData;
// We have to return the data unformatted unless we want to change it
// back in sendMessage.
return resData.payload.metadata[0];
})
);
}
// resolve all promises
bluebird
.all(uploads)
.then(resData => callback(null, resData))
.catch(function (err) {
log.error("uploadAttachment", err);
return callback(err);
});
}
function getUrl(url, callback) {
var form = {
image_height: 960,
image_width: 960,
uri: url
};
defaultFuncs
.post("https://www.facebook.com/message_share_attachment/fromURI/", ctx.jar, form)
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
.then(function (resData) {
if (resData.error) return callback(resData);
if (!resData.payload) return callback({ error: "Invalid url" });
callback(null, resData.payload.share_data.share_params);
})
.catch(function (err) {
log.error("getUrl", err);
return callback(err);
});
}
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;
log.info("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;
}
defaultFuncs
.post("https://www.facebook.com/messaging/send/", ctx.jar, form)
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
.then(function (resData) {
if (!resData) return callback({ error: "Send message failed." });
if (resData.error) {
if (resData.error === 1545012) {
log.warn("sendMessage", "Got error 1545012. This might mean that you're not part of the conversation " + threadID);
}
else {
log.error("sendMessage", resData);
}
return callback(resData);
}
var messageInfo = resData.payload.actions.reduce(function (p, v) {
return (
{
threadID: v.thread_fbid,
messageID: v.message_id,
timestamp: v.timestamp
} || p
);
}, null);
return callback(null, messageInfo);
})
.catch(function (err) {
log.error("sendMessage", err);
if (utils.getType(err) == "Object" && err.error === "Not logged in.") ctx.loggedIn = false;
return callback(err);
});
}
function send(form, threadID, messageAndOTID, callback, isGroup) {
// We're doing a query to this to check if the given id is the id of
// a user or of a group chat. The form will be different depending
// on that.
if (utils.getType(threadID) === "Array") sendContent(form, threadID, false, messageAndOTID, callback);
else {
if (utils.getType(isGroup) != "Boolean") sendContent(form, threadID, threadID.length === 15, messageAndOTID, callback);
else sendContent(form, threadID, !isGroup, messageAndOTID, callback);
}
}
function handleUrl(msg, form, callback, cb) {
if (msg.url) {
form["shareable_attachment[share_type]"] = "100";
getUrl(msg.url, function (err, params) {
if (err) return callback(err);
form["shareable_attachment[share_params]"] = params;
cb();
});
}
else cb();
}
function handleLocation(msg, form, callback, cb) {
if (msg.location) {
if (msg.location.latitude == null || msg.location.longitude == null) return callback({ 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;
}
cb();
}
function handleSticker(msg, form, callback, cb) {
if (msg.sticker) form["sticker_id"] = msg.sticker;
cb();
}
function handleEmoji(msg, form, callback, cb) {
if (msg.emojiSize != null && msg.emoji == null) return callback({ error: "emoji property is empty" });
if (msg.emoji) {
if (msg.emojiSize == null) msg.emojiSize = "medium";
if (msg.emojiSize != "small" && msg.emojiSize != "medium" && msg.emojiSize != "large") return callback({ error: "emojiSize property is invalid" });
if (form["body"] != null && form["body"] != "") return callback({ error: "body is not empty" });
form["body"] = msg.emoji;
form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
}
cb();
}
function handleAttachment(msg, form, callback, cb) {
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];
if (msg.attachment.every(e=>/_id$/.test(e[0]))) {
//console.log(msg.attachment)
msg.attachment.map(e=>form[`${e[0]}s`].push(e[1]));
return cb();
}
uploadAttachment(msg.attachment, function (err, files) {
if (err) return callback(err);
files.forEach(function (file) {
var key = Object.keys(file);
var type = key[0]; // image_id, file_id, etc
form["" + type + "s"].push(file[type]); // push the id
});
cb();
});
}
else cb();
}
function handleMention(msg, form, callback, cb) {
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") return callback({ error: "Mention tags must be strings." });
const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
if (offset < 0) log.warn("handleMention", 'Mention for "' + tag + '" not found in message string.');
if (mention.id == null) log.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";
}
}
cb();
}
return function sendMessage(msg, threadID, callback, replyToMessage, isGroup) {
typeof isGroup == "undefined" ? isGroup = null : "";
if (!callback && (utils.getType(threadID) === "Function" || utils.getType(threadID) === "AsyncFunction")) return threadID({ error: "Pass a threadID as a second argument." });
if (!replyToMessage && utils.getType(callback) === "String") {
replyToMessage = callback;
callback = function () { };
}
var resolveFunc = function () { };
var rejectFunc = function () { };
var returnPromise = new Promise(function (resolve, reject) {
resolveFunc = resolve;
rejectFunc = reject;
});
if (!callback) {
callback = function (err, data) {
if (err) return rejectFunc(err);
resolveFunc(data);
};
}
var msgType = utils.getType(msg);
var threadIDType = utils.getType(threadID);
var messageIDType = utils.getType(replyToMessage);
if (msgType !== "String" && msgType !== "Object") return callback({ error: "Message should be of type string or object and not " + msgType + "." });
// Changing this to accomodate an array of users
if (threadIDType !== "Array" && threadIDType !== "Number" && threadIDType !== "String") return callback({ error: "ThreadID should be of type number, string, or array and not " + threadIDType + "." });
if (replyToMessage && messageIDType !== 'String') return callback({ error: "MessageID should be of type string and not " + threadIDType + "." });
if (msgType === "String") msg = { body: msg };
var disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
if (disallowedProperties.length > 0) return callback({ error: "Dissallowed props: `" + disallowedProperties.join(", ") + "`" });
var messageAndOTID = utils.generateOfflineThreadingID();
// console.log(messageAndOTID)
var 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",
body: msg.body ? msg.body.toString() : "",
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(),
replied_to_message_id: replyToMessage
};
// console.log(form)
handleLocation(msg, form, callback, () =>
handleSticker(msg, form, callback, () =>
handleAttachment(msg, form, callback, () =>
handleUrl(msg, form, callback, () =>
handleEmoji(msg, form, callback, () =>
handleMention(msg, form, callback, () =>
send(form, threadID, messageAndOTID, callback, isGroup)
)
)
)
)
)
);
return returnPromise;
};
};
;