|
|
"use strict"; |
|
|
|
|
|
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 = []; |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return resData.payload.metadata[0]; |
|
|
}) |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
bluebird |
|
|
.all(uploads) |
|
|
.then(function (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) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
|
|
|
|
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 |
|
|
); |
|
|
} |
|
|
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) { |
|
|
|
|
|
if (utils.getType(err) == "Object" && err.error === "Not logged in.") { |
|
|
ctx.loggedIn = false; |
|
|
} |
|
|
return callback(err); |
|
|
}); |
|
|
} |
|
|
|
|
|
function send(form, threadID, messageAndOTID, callback, isGroup) { |
|
|
|
|
|
|
|
|
|
|
|
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]; |
|
|
} |
|
|
|
|
|
uploadAttachment(msg.attachment, function (err, files) { |
|
|
if (err) { |
|
|
return callback(err); |
|
|
} |
|
|
|
|
|
files.forEach(function (file) { |
|
|
var key = Object.keys(file); |
|
|
var type = key[0]; |
|
|
form["" + type + "s"].push(file[type]); |
|
|
}); |
|
|
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 = undefined; |
|
|
} |
|
|
|
|
|
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 + "." |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
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 |
|
|
}; |
|
|
|
|
|
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; |
|
|
}; |
|
|
}; |