mirror of
https://github.com/tiennm99/FBcount.git
synced 2026-08-05 05:52:17 +00:00
Init
This commit is contained in:
+99
@@ -0,0 +1,99 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function addUserToGroup(userID, threadID, callback) {
|
||||
if (
|
||||
!callback &&
|
||||
(utils.getType(threadID) === "Function" ||
|
||||
utils.getType(threadID) === "AsyncFunction")
|
||||
) {
|
||||
throw { error: "please pass a threadID as a second argument." };
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
if (
|
||||
utils.getType(threadID) !== "Number" &&
|
||||
utils.getType(threadID) !== "String"
|
||||
) {
|
||||
throw {
|
||||
error:
|
||||
"ThreadID should be of type Number or String and not " +
|
||||
utils.getType(threadID) +
|
||||
"."
|
||||
};
|
||||
}
|
||||
|
||||
if (utils.getType(userID) !== "Array") {
|
||||
userID = [userID];
|
||||
}
|
||||
|
||||
var messageAndOTID = utils.generateOfflineThreadingID();
|
||||
var form = {
|
||||
client: "mercury",
|
||||
action_type: "ma-type:log-message",
|
||||
author: "fbid:" + ctx.userID,
|
||||
thread_id: "",
|
||||
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_spoof_warning: false,
|
||||
source: "source:chat:web",
|
||||
"source_tags[0]": "source:chat",
|
||||
log_message_type: "log:subscribe",
|
||||
status: "0",
|
||||
offline_threading_id: messageAndOTID,
|
||||
message_id: messageAndOTID,
|
||||
threading_id: utils.generateThreadingID(ctx.clientID),
|
||||
manual_retry_cnt: "0",
|
||||
thread_fbid: threadID
|
||||
};
|
||||
|
||||
for (var i = 0; i < userID.length; i++) {
|
||||
if (
|
||||
utils.getType(userID[i]) !== "Number" &&
|
||||
utils.getType(userID[i]) !== "String"
|
||||
) {
|
||||
throw {
|
||||
error:
|
||||
"Elements of userID should be of type Number or String and not " +
|
||||
utils.getType(userID[i]) +
|
||||
"."
|
||||
};
|
||||
}
|
||||
|
||||
form["log_message_data[added_participants][" + i + "]"] =
|
||||
"fbid:" + userID[i];
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/messaging/send/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (!resData) {
|
||||
throw { error: "Add to group failed." };
|
||||
}
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("addUserToGroup", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
|
||||
const utils = require("../utils");
|
||||
const log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function changeAdminStatus(threadID, adminIDs, adminStatus, callback) {
|
||||
if (utils.getType(threadID) !== "String") {
|
||||
throw {error: "changeAdminStatus: threadID must be a string"};
|
||||
}
|
||||
|
||||
if (utils.getType(adminIDs) === "String") {
|
||||
adminIDs = [adminIDs];
|
||||
}
|
||||
|
||||
if (utils.getType(adminIDs) !== "Array") {
|
||||
throw {error: "changeAdminStatus: adminIDs must be an array or string"};
|
||||
}
|
||||
|
||||
if (utils.getType(adminStatus) !== "Boolean") {
|
||||
throw {error: "changeAdminStatus: adminStatus must be a string"};
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = () => {};
|
||||
}
|
||||
|
||||
if (utils.getType(callback) !== "Function" && utils.getType(callback) !== "AsyncFunction") {
|
||||
throw {error: "changeAdminStatus: callback is not a function"};
|
||||
}
|
||||
|
||||
let form = {
|
||||
"thread_fbid": threadID,
|
||||
};
|
||||
|
||||
let i = 0;
|
||||
for (let u of adminIDs) {
|
||||
form[`admin_ids[${i++}]`] = u
|
||||
}
|
||||
form["add"] = adminStatus;
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/messaging/save_admins/?dpr=1", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
switch (resData.error) {
|
||||
case 1976004:
|
||||
throw { error: "Cannot alter admin status: you are not an admin.", rawResponse: resData };
|
||||
case 1357031:
|
||||
throw { error: "Cannot alter admin status: this thread is not a group chat.", rawResponse: resData };
|
||||
default:
|
||||
throw { error: "Cannot alter admin status: unknown error.", rawResponse: resData };
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeAdminStatus", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function changeArchivedStatus(threadOrThreads, archive, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {};
|
||||
|
||||
if (utils.getType(threadOrThreads) === "Array") {
|
||||
for (var i = 0; i < threadOrThreads.length; i++) {
|
||||
form["ids[" + threadOrThreads[i] + "]"] = archive;
|
||||
}
|
||||
} else {
|
||||
form["ids[" + threadOrThreads + "]"] = archive;
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/change_archived_status.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeArchivedStatus", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function changeBlockedStatus(userID, block, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
if (block) {
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/nfx/block_messages/?thread_fbid=" +
|
||||
userID +
|
||||
"&location=www_chat_head",
|
||||
ctx.jar,
|
||||
{}
|
||||
)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com" +
|
||||
/action="(.+?)"+?/
|
||||
.exec(resData.jsmods.markup[0][1].__html)[1]
|
||||
.replace(/&/g, "&"),
|
||||
ctx.jar,
|
||||
{}
|
||||
)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(_resData) {
|
||||
if (_resData.error) {
|
||||
throw _resData;
|
||||
}
|
||||
return callback();
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeBlockedStatus", err);
|
||||
return callback(err);
|
||||
});
|
||||
} else {
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/nfx/messenger_undo_block.php?story_location=messenger&context=%7B%22reportable_ent_token%22%3A%22" +
|
||||
userID +
|
||||
"%22%2C%22initial_action_name%22%3A%22BLOCK_MESSAGES%22%7D&",
|
||||
ctx.jar,
|
||||
{}
|
||||
)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeBlockedStatus", err);
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
var bluebird = require("bluebird");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
function handleUpload(image, callback) {
|
||||
var uploads = [];
|
||||
|
||||
var form = {
|
||||
images_only: "true",
|
||||
"attachment[]": image
|
||||
};
|
||||
|
||||
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];
|
||||
})
|
||||
);
|
||||
|
||||
// resolve all promises
|
||||
bluebird
|
||||
.all(uploads)
|
||||
.then(function(resData) {
|
||||
callback(null, resData);
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("handleUpload", err);
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
return function changeGroupImage(image, threadID, callback) {
|
||||
if (
|
||||
!callback &&
|
||||
(utils.getType(threadID) === "Function" ||
|
||||
utils.getType(threadID) === "AsyncFunction")
|
||||
) {
|
||||
throw { error: "please pass a threadID as a second argument." };
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var messageAndOTID = utils.generateOfflineThreadingID();
|
||||
var form = {
|
||||
client: "mercury",
|
||||
action_type: "ma-type:log-message",
|
||||
author: "fbid:" + ctx.userID,
|
||||
author_email: "",
|
||||
ephemeral_ttl_mode: "0",
|
||||
is_filtered_content: false,
|
||||
is_filtered_content_account: false,
|
||||
is_filtered_content_bh: false,
|
||||
is_filtered_content_invalid_app: false,
|
||||
is_filtered_content_quasar: false,
|
||||
is_forward: false,
|
||||
is_spoof_warning: false,
|
||||
is_unread: false,
|
||||
log_message_type: "log:thread-image",
|
||||
manual_retry_cnt: "0",
|
||||
message_id: messageAndOTID,
|
||||
offline_threading_id: messageAndOTID,
|
||||
source: "source:chat:web",
|
||||
"source_tags[0]": "source:chat",
|
||||
status: "0",
|
||||
thread_fbid: threadID,
|
||||
thread_id: "",
|
||||
timestamp: Date.now(),
|
||||
timestamp_absolute: "Today",
|
||||
timestamp_relative: utils.generateTimestampRelative(),
|
||||
timestamp_time_passed: "0"
|
||||
};
|
||||
|
||||
handleUpload(image, function(err, payload) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
form["thread_image_id"] = payload[0]["image_id"];
|
||||
form["thread_id"] = threadID;
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/messaging/set_thread_image/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
// check for errors here
|
||||
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeGroupImage", err);
|
||||
return callback(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function changeNickname(nickname, threadID, participantID, callback) {
|
||||
callback = callback || function() {};
|
||||
|
||||
var form = {
|
||||
nickname: nickname,
|
||||
participant_id: participantID,
|
||||
thread_or_other_fbid: threadID
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/messaging/save_thread_nickname/?source=thread_settings&dpr=1",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error === 1545014) {
|
||||
throw { error: "Trying to change nickname of user isn't in thread" };
|
||||
}
|
||||
if (resData.error === 1357031) {
|
||||
throw {
|
||||
error:
|
||||
"Trying to change user nickname of a thread that doesn't exist. Have at least one message in the thread before trying to change the user nickname."
|
||||
};
|
||||
}
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeNickname", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function changeThreadColor(color, threadID, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var validatedColor = color !== null ? color.toLowerCase() : color; // API only accepts lowercase letters in hex string
|
||||
var colorList = Object.keys(api.threadColors).map(function(name) {
|
||||
return api.threadColors[name];
|
||||
});
|
||||
if (!colorList.includes(validatedColor)) {
|
||||
throw {
|
||||
error:
|
||||
"The color you are trying to use is not a valid thread color. Use api.threadColors to find acceptable values."
|
||||
};
|
||||
}
|
||||
|
||||
var form = {
|
||||
color_choice: validatedColor,
|
||||
thread_or_other_fbid: threadID
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/messaging/save_thread_color/?source=thread_settings&dpr=1",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error === 1357031) {
|
||||
throw {
|
||||
error:
|
||||
"Trying to change colors of a chat that doesn't exist. Have at least one message in the thread before trying to change the colors."
|
||||
};
|
||||
}
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeThreadColor", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function changeThreadEmoji(emoji, threadID, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
var form = {
|
||||
emoji_choice: emoji,
|
||||
thread_or_other_fbid: threadID
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/messaging/save_thread_emoji/?source=thread_settings&__pc=EXP1%3Amessengerdotcom_pkg",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error === 1357031) {
|
||||
throw {
|
||||
error:
|
||||
"Trying to change emoji of a chat that doesn't exist. Have at least one message in the thread before trying to change the emoji."
|
||||
};
|
||||
}
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeThreadEmoji", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function createPoll(title, threadID, options, callback) {
|
||||
if (!callback) {
|
||||
if (utils.getType(options) == "Function") {
|
||||
callback = options;
|
||||
} else {
|
||||
callback = function() {};
|
||||
}
|
||||
}
|
||||
if (!options) {
|
||||
options = {}; // Initial poll options are optional
|
||||
}
|
||||
|
||||
var form = {
|
||||
target_id: threadID,
|
||||
question_text: title
|
||||
};
|
||||
|
||||
// Set fields for options (and whether they are selected initially by the posting user)
|
||||
var ind = 0;
|
||||
for (var opt in options) {
|
||||
if (options.hasOwnProperty(opt)) {
|
||||
form["option_text_array[" + ind + "]"] = opt;
|
||||
form["option_is_selected_array[" + ind + "]"] = options[opt]
|
||||
? "1"
|
||||
: "0";
|
||||
ind++;
|
||||
}
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/messaging/group_polling/create_poll/?dpr=1",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.payload.status != "success") {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("createPoll", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function deleteMessage(messageOrMessages, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
client: "mercury"
|
||||
};
|
||||
|
||||
if (utils.getType(messageOrMessages) !== "Array") {
|
||||
messageOrMessages = [messageOrMessages];
|
||||
}
|
||||
|
||||
for (var i = 0; i < messageOrMessages.length; i++) {
|
||||
form["message_ids[" + i + "]"] = messageOrMessages[i];
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/delete_messages.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("deleteMessage", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function deleteThread(threadOrThreads, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
client: "mercury"
|
||||
};
|
||||
|
||||
if (utils.getType(threadOrThreads) !== "Array") {
|
||||
threadOrThreads = [threadOrThreads];
|
||||
}
|
||||
|
||||
for (var i = 0; i < threadOrThreads.length; i++) {
|
||||
form["ids[" + i + "]"] = threadOrThreads[i];
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/delete_thread.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("deleteThread", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function forwardAttachment(attachmentID, userOrUsers, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
attachment_id: attachmentID
|
||||
};
|
||||
|
||||
if (utils.getType(userOrUsers) !== "Array") {
|
||||
userOrUsers = [userOrUsers];
|
||||
}
|
||||
|
||||
var timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
for (var i = 0; i < userOrUsers.length; i++) {
|
||||
//That's good, the key of the array is really timestmap in seconds + index
|
||||
//Probably time when the attachment will be sent?
|
||||
form["recipient_map[" + (timestamp + i) + "]"] = userOrUsers[i];
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/mercury/attachments/forward/",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx.jar, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("forwardAttachment", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getCurrentUserID() {
|
||||
return ctx.userID;
|
||||
};
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
"use strict";
|
||||
|
||||
const util = require("util");
|
||||
|
||||
module.exports = function() {
|
||||
return function getEmojiUrl(c, size, pixelRatio) {
|
||||
/*
|
||||
Resolves Facebook Messenger emoji image asset URL for an emoji character.
|
||||
Supported sizes are 32, 64, and 128.
|
||||
Supported pixel ratios are '1.0' and '1.5' (possibly more; haven't tested)
|
||||
*/
|
||||
const baseUrl = "https://static.xx.fbcdn.net/images/emoji.php/v8/z%s/%s";
|
||||
pixelRatio = pixelRatio || "1.0";
|
||||
|
||||
let ending = util.format(
|
||||
"%s/%s/%s.png",
|
||||
pixelRatio,
|
||||
size,
|
||||
c.codePointAt(0).toString(16)
|
||||
);
|
||||
let base = 317426846;
|
||||
for (let i = 0; i < ending.length; i++) {
|
||||
base = (base << 5) - base + ending.charCodeAt(i);
|
||||
}
|
||||
|
||||
let hashed = (base & 255).toString(16);
|
||||
return util.format(baseUrl, hashed, ending);
|
||||
};
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
"use strict";
|
||||
|
||||
var cheerio = require("cheerio");
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
// [almost] copy pasted from one of FB's minified file (GenderConst)
|
||||
var GENDERS = {
|
||||
0: "unknown",
|
||||
1: "female_singular",
|
||||
2: "male_singular",
|
||||
3: "female_singular_guess",
|
||||
4: "male_singular_guess",
|
||||
5: "mixed",
|
||||
6: "neuter_singular",
|
||||
7: "unknown_singular",
|
||||
8: "female_plural",
|
||||
9: "male_plural",
|
||||
10: "neuter_plural",
|
||||
11: "unknown_plural"
|
||||
};
|
||||
|
||||
function formatData(obj) {
|
||||
return Object.keys(obj).map(function(key) {
|
||||
var user = obj[key];
|
||||
return {
|
||||
alternateName: user.alternateName,
|
||||
firstName: user.firstName,
|
||||
gender: GENDERS[user.gender],
|
||||
userID: utils.formatID(user.id.toString()),
|
||||
isFriend: user.is_friend != null && user.is_friend ? true : false,
|
||||
fullName: user.name,
|
||||
profilePicture: user.thumbSrc,
|
||||
type: user.type,
|
||||
profileUrl: user.uri,
|
||||
vanity: user.vanity,
|
||||
isBirthday: !!user.is_birthday
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getFriendsList(callback) {
|
||||
if (!callback) {
|
||||
throw { error: "getFriendsList: need callback" };
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.postFormData(
|
||||
"https://www.facebook.com/chat/user_info_all",
|
||||
ctx.jar,
|
||||
{},
|
||||
{ viewer: ctx.userID }
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (!resData) {
|
||||
throw { error: "getFriendsList returned empty object." };
|
||||
}
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
callback(null, formatData(resData.payload));
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getFriendsList", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+632
@@ -0,0 +1,632 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
function formatAttachmentsGraphQLResponse(attachment) {
|
||||
switch (attachment.__typename) {
|
||||
case "MessageImage":
|
||||
return {
|
||||
type: "photo",
|
||||
ID: attachment.legacy_attachment_id,
|
||||
filename: attachment.filename,
|
||||
thumbnailUrl: attachment.thumbnail.uri,
|
||||
|
||||
previewUrl: attachment.preview.uri,
|
||||
previewWidth: attachment.preview.width,
|
||||
previewHeight: attachment.preview.height,
|
||||
|
||||
largePreviewUrl: attachment.large_preview.uri,
|
||||
largePreviewHeight: attachment.large_preview.height,
|
||||
largePreviewWidth: attachment.large_preview.width,
|
||||
|
||||
// You have to query for the real image. See below.
|
||||
url: attachment.large_preview.uri, // @Legacy
|
||||
width: attachment.large_preview.width, // @Legacy
|
||||
height: attachment.large_preview.height, // @Legacy
|
||||
name: attachment.filename, // @Legacy
|
||||
|
||||
// @Undocumented
|
||||
attributionApp: attachment.attribution_app
|
||||
? {
|
||||
attributionAppID: attachment.attribution_app.id,
|
||||
name: attachment.attribution_app.name,
|
||||
logo: attachment.attribution_app.square_logo
|
||||
}
|
||||
: null
|
||||
|
||||
// @TODO No idea what this is, should we expose it?
|
||||
// Ben - July 15th 2017
|
||||
// renderAsSticker: attachment.render_as_sticker,
|
||||
|
||||
// This is _not_ the real URI, this is still just a large preview.
|
||||
// To get the URL we'll need to support a POST query to
|
||||
//
|
||||
// https://www.facebook.com/webgraphql/query/
|
||||
//
|
||||
// With the following query params:
|
||||
//
|
||||
// query_id:728987990612546
|
||||
// variables:{"id":"100009069356507","photoID":"10213724771692996"}
|
||||
// dpr:1
|
||||
//
|
||||
// No special form though.
|
||||
};
|
||||
case "MessageAnimatedImage":
|
||||
return {
|
||||
type: "animated_image",
|
||||
ID: attachment.legacy_attachment_id,
|
||||
filename: attachment.filename,
|
||||
|
||||
previewUrl: attachment.preview_image.uri,
|
||||
previewWidth: attachment.preview_image.width,
|
||||
previewHeight: attachment.preview_image.height,
|
||||
|
||||
url: attachment.animated_image.uri,
|
||||
width: attachment.animated_image.width,
|
||||
height: attachment.animated_image.height,
|
||||
|
||||
thumbnailUrl: attachment.preview_image.uri, // @Legacy
|
||||
name: attachment.filename, // @Legacy
|
||||
facebookUrl: attachment.animated_image.uri, // @Legacy
|
||||
rawGifImage: attachment.animated_image.uri, // @Legacy
|
||||
animatedGifUrl: attachment.animated_image.uri, // @Legacy
|
||||
animatedGifPreviewUrl: attachment.preview_image.uri, // @Legacy
|
||||
animatedWebpUrl: attachment.animated_image.uri, // @Legacy
|
||||
animatedWebpPreviewUrl: attachment.preview_image.uri, // @Legacy
|
||||
|
||||
// @Undocumented
|
||||
attributionApp: attachment.attribution_app
|
||||
? {
|
||||
attributionAppID: attachment.attribution_app.id,
|
||||
name: attachment.attribution_app.name,
|
||||
logo: attachment.attribution_app.square_logo
|
||||
}
|
||||
: null
|
||||
};
|
||||
case "MessageVideo":
|
||||
return {
|
||||
type: "video",
|
||||
filename: attachment.filename,
|
||||
ID: attachment.legacy_attachment_id,
|
||||
|
||||
thumbnailUrl: attachment.large_image.uri, // @Legacy
|
||||
|
||||
previewUrl: attachment.large_image.uri,
|
||||
previewWidth: attachment.large_image.width,
|
||||
previewHeight: attachment.large_image.height,
|
||||
|
||||
url: attachment.playable_url,
|
||||
width: attachment.original_dimensions.x,
|
||||
height: attachment.original_dimensions.y,
|
||||
|
||||
duration: attachment.playable_duration_in_ms,
|
||||
videoType: attachment.video_type.toLowerCase()
|
||||
};
|
||||
break;
|
||||
case "MessageFile":
|
||||
return {
|
||||
type: "file",
|
||||
filename: attachment.filename,
|
||||
ID: attachment.message_file_fbid,
|
||||
|
||||
url: attachment.url,
|
||||
isMalicious: attachment.is_malicious,
|
||||
contentType: attachment.content_type,
|
||||
|
||||
name: attachment.filename, // @Legacy
|
||||
mimeType: "", // @Legacy
|
||||
fileSize: -1 // @Legacy
|
||||
};
|
||||
case "MessageAudio":
|
||||
return {
|
||||
type: "audio",
|
||||
filename: attachment.filename,
|
||||
ID: attachment.url_shimhash, // Not fowardable
|
||||
|
||||
audioType: attachment.audio_type,
|
||||
duration: attachment.playable_duration_in_ms,
|
||||
url: attachment.playable_url,
|
||||
|
||||
isVoiceMail: attachment.is_voicemail
|
||||
};
|
||||
default:
|
||||
return {
|
||||
error: "Don't know about attachment type " + attachment.__typename
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatExtensibleAttachment(attachment) {
|
||||
if (attachment.story_attachment) {
|
||||
return {
|
||||
type: "share",
|
||||
ID: attachment.legacy_attachment_id,
|
||||
url: attachment.story_attachment.url,
|
||||
|
||||
title: attachment.story_attachment.title_with_entities.text,
|
||||
description:
|
||||
attachment.story_attachment.description &&
|
||||
attachment.story_attachment.description.text,
|
||||
source:
|
||||
attachment.story_attachment.source == null
|
||||
? null
|
||||
: attachment.story_attachment.source.text,
|
||||
|
||||
image:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.animated_image == null &&
|
||||
attachment.story_attachment.media.image == null
|
||||
? null
|
||||
: (
|
||||
attachment.story_attachment.media.animated_image ||
|
||||
attachment.story_attachment.media.image
|
||||
).uri,
|
||||
width:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.animated_image == null &&
|
||||
attachment.story_attachment.media.image == null
|
||||
? null
|
||||
: (
|
||||
attachment.story_attachment.media.animated_image ||
|
||||
attachment.story_attachment.media.image
|
||||
).width,
|
||||
height:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.animated_image == null &&
|
||||
attachment.story_attachment.media.image == null
|
||||
? null
|
||||
: (
|
||||
attachment.story_attachment.media.animated_image ||
|
||||
attachment.story_attachment.media.image
|
||||
).height,
|
||||
playable:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.is_playable,
|
||||
duration:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.playable_duration_in_ms,
|
||||
playableUrl:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.playable_url,
|
||||
|
||||
subattachments: attachment.story_attachment.subattachments,
|
||||
|
||||
// Format example:
|
||||
//
|
||||
// [{
|
||||
// key: "width",
|
||||
// value: { text: "1280" }
|
||||
// }]
|
||||
//
|
||||
// That we turn into:
|
||||
//
|
||||
// {
|
||||
// width: "1280"
|
||||
// }
|
||||
//
|
||||
properties: attachment.story_attachment.properties.reduce(function(
|
||||
obj,
|
||||
cur
|
||||
) {
|
||||
obj[cur.key] = cur.value.text;
|
||||
return obj;
|
||||
},
|
||||
{}),
|
||||
|
||||
// Deprecated fields
|
||||
animatedImageSize: "", // @Legacy
|
||||
facebookUrl: "", // @Legacy
|
||||
styleList: "", // @Legacy
|
||||
target: "", // @Legacy
|
||||
thumbnailUrl:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.animated_image == null &&
|
||||
attachment.story_attachment.media.image == null
|
||||
? null
|
||||
: (
|
||||
attachment.story_attachment.media.animated_image ||
|
||||
attachment.story_attachment.media.image
|
||||
).uri, // @Legacy
|
||||
thumbnailWidth:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.animated_image == null &&
|
||||
attachment.story_attachment.media.image == null
|
||||
? null
|
||||
: (
|
||||
attachment.story_attachment.media.animated_image ||
|
||||
attachment.story_attachment.media.image
|
||||
).width, // @Legacy
|
||||
thumbnailHeight:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.animated_image == null &&
|
||||
attachment.story_attachment.media.image == null
|
||||
? null
|
||||
: (
|
||||
attachment.story_attachment.media.animated_image ||
|
||||
attachment.story_attachment.media.image
|
||||
).height // @Legacy
|
||||
};
|
||||
} else {
|
||||
return { error: "Don't know what to do with extensible_attachment." };
|
||||
}
|
||||
}
|
||||
|
||||
function formatReactionsGraphQL(reaction) {
|
||||
return {
|
||||
reaction: reaction.reaction,
|
||||
userID: reaction.user.id
|
||||
};
|
||||
}
|
||||
|
||||
function formatEventData(event) {
|
||||
if (event == null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
switch (event.__typename) {
|
||||
case "ThemeColorExtensibleMessageAdminText":
|
||||
return {
|
||||
color: event.theme_color
|
||||
};
|
||||
case "ThreadNicknameExtensibleMessageAdminText":
|
||||
return {
|
||||
nickname: event.nickname,
|
||||
participantID: event.participant_id
|
||||
};
|
||||
case "ThreadIconExtensibleMessageAdminText":
|
||||
return {
|
||||
threadIcon: event.thread_icon
|
||||
};
|
||||
case "InstantGameUpdateExtensibleMessageAdminText":
|
||||
return {
|
||||
gameID: (event.game == null ? null : event.game.id),
|
||||
update_type: event.update_type,
|
||||
collapsed_text: event.collapsed_text,
|
||||
expanded_text: event.expanded_text,
|
||||
instant_game_update_data: event.instant_game_update_data
|
||||
};
|
||||
case "GameScoreExtensibleMessageAdminText":
|
||||
return {
|
||||
game_type: event.game_type
|
||||
};
|
||||
case "RtcCallLogExtensibleMessageAdminText":
|
||||
return {
|
||||
event: event.event,
|
||||
is_video_call: event.is_video_call,
|
||||
server_info_data: event.server_info_data
|
||||
};
|
||||
case "GroupPollExtensibleMessageAdminText":
|
||||
return {
|
||||
event_type: event.event_type,
|
||||
total_count: event.total_count,
|
||||
question: event.question
|
||||
};
|
||||
case "AcceptPendingThreadExtensibleMessageAdminText":
|
||||
return {
|
||||
accepter_id: event.accepter_id,
|
||||
requester_id: event.requester_id
|
||||
};
|
||||
case "ConfirmFriendRequestExtensibleMessageAdminText":
|
||||
return {
|
||||
friend_request_recipient: event.friend_request_recipient,
|
||||
friend_request_sender: event.friend_request_sender
|
||||
};
|
||||
case "AddContactExtensibleMessageAdminText":
|
||||
return {
|
||||
contact_added_id: event.contact_added_id,
|
||||
contact_adder_id: event.contact_adder_id
|
||||
};
|
||||
case "AdExtensibleMessageAdminText":
|
||||
return {
|
||||
ad_client_token: event.ad_client_token,
|
||||
ad_id: event.ad_id,
|
||||
ad_preferences_link: event.ad_preferences_link,
|
||||
ad_properties: event.ad_properties
|
||||
};
|
||||
// never data
|
||||
case "ParticipantJoinedGroupCallExtensibleMessageAdminText":
|
||||
case "ThreadEphemeralTtlModeExtensibleMessageAdminText":
|
||||
case "StartedSharingVideoExtensibleMessageAdminText":
|
||||
case "LightweightEventCreateExtensibleMessageAdminText":
|
||||
case "LightweightEventNotifyExtensibleMessageAdminText":
|
||||
case "LightweightEventNotifyBeforeEventExtensibleMessageAdminText":
|
||||
case "LightweightEventUpdateTitleExtensibleMessageAdminText":
|
||||
case "LightweightEventUpdateTimeExtensibleMessageAdminText":
|
||||
case "LightweightEventUpdateLocationExtensibleMessageAdminText":
|
||||
case "LightweightEventDeleteExtensibleMessageAdminText":
|
||||
return {};
|
||||
default:
|
||||
return {
|
||||
error: "Don't know what to with event data type " + event.__typename
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatMessagesGraphQLResponse(data) {
|
||||
var messageThread = data.o0.data.message_thread;
|
||||
var threadID = messageThread.thread_key.thread_fbid
|
||||
? messageThread.thread_key.thread_fbid
|
||||
: messageThread.thread_key.other_user_id;
|
||||
|
||||
var messages = messageThread.messages.nodes.map(function(d) {
|
||||
switch (d.__typename) {
|
||||
case "UserMessage":
|
||||
// Give priority to stickers. They're seen as normal messages but we've
|
||||
// been considering them as attachments.
|
||||
var maybeStickerAttachment;
|
||||
if (d.sticker) {
|
||||
maybeStickerAttachment = [
|
||||
{
|
||||
type: "sticker",
|
||||
ID: d.sticker.id,
|
||||
url: d.sticker.url,
|
||||
|
||||
packID: d.sticker.pack.id,
|
||||
spriteUrl: d.sticker.sprite_image,
|
||||
spriteUrl2x: d.sticker.sprite_image_2x,
|
||||
width: d.sticker.width,
|
||||
height: d.sticker.height,
|
||||
|
||||
caption: d.snippet, // Not sure what the heck caption was.
|
||||
description: d.sticker.label, // Not sure about this one either.
|
||||
|
||||
frameCount: d.sticker.frame_count,
|
||||
frameRate: d.sticker.frame_rate,
|
||||
framesPerRow: d.sticker.frames_per_row,
|
||||
framesPerCol: d.sticker.frames_per_col,
|
||||
|
||||
stickerID: d.sticker.id, // @Legacy
|
||||
spriteURI: d.sticker.sprite_image, // @Legacy
|
||||
spriteURI2x: d.sticker.sprite_image_2x // @Legacy
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
var mentionsObj = {};
|
||||
if (d.message !== null) {
|
||||
d.message.ranges.forEach(e => {
|
||||
mentionsObj[e.entity.id] = d.message.text.substr(e.offset, e.length);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: "message",
|
||||
attachments: maybeStickerAttachment
|
||||
? maybeStickerAttachment
|
||||
: d.blob_attachments && d.blob_attachments.length > 0
|
||||
? d.blob_attachments.map(formatAttachmentsGraphQLResponse)
|
||||
: d.extensible_attachment
|
||||
? [formatExtensibleAttachment(d.extensible_attachment)]
|
||||
: [],
|
||||
body: d.message !== null ? d.message.text : '',
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
messageID: d.message_id,
|
||||
senderID: d.message_sender.id,
|
||||
threadID: threadID,
|
||||
timestamp: d.timestamp_precise,
|
||||
|
||||
mentions: mentionsObj,
|
||||
isUnread: d.unread,
|
||||
|
||||
// New
|
||||
messageReactions: d.message_reactions
|
||||
? d.message_reactions.map(formatReactionsGraphQL)
|
||||
: null,
|
||||
isSponsored: d.is_sponsored,
|
||||
snippet: d.snippet
|
||||
};
|
||||
case "ThreadNameMessage":
|
||||
return {
|
||||
type: "event",
|
||||
messageID: d.message_id,
|
||||
threadID: threadID,
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
senderID: d.message_sender.id,
|
||||
timestamp: d.timestamp_precise,
|
||||
eventType: "change_thread_name",
|
||||
snippet: d.snippet,
|
||||
eventData: {
|
||||
threadName: d.thread_name
|
||||
},
|
||||
|
||||
// @Legacy
|
||||
author: d.message_sender.id,
|
||||
logMessageType: "log:thread-name",
|
||||
logMessageData: { name: d.thread_name }
|
||||
};
|
||||
case "ThreadImageMessage":
|
||||
return {
|
||||
type: "event",
|
||||
messageID: d.message_id,
|
||||
threadID: threadID,
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
senderID: d.message_sender.id,
|
||||
timestamp: d.timestamp_precise,
|
||||
eventType: "change_thread_image",
|
||||
snippet: d.snippet,
|
||||
eventData:
|
||||
d.image_with_metadata == null
|
||||
? {} /* removed image */
|
||||
: {
|
||||
/* image added */
|
||||
threadImage: {
|
||||
attachmentID: d.image_with_metadata.legacy_attachment_id,
|
||||
width: d.image_with_metadata.original_dimensions.x,
|
||||
height: d.image_with_metadata.original_dimensions.y,
|
||||
url: d.image_with_metadata.preview.uri
|
||||
}
|
||||
},
|
||||
|
||||
// @Legacy
|
||||
logMessageType: "log:thread-icon",
|
||||
logMessageData: {
|
||||
thread_icon: d.image_with_metadata
|
||||
? d.image_with_metadata.preview.uri
|
||||
: null
|
||||
}
|
||||
};
|
||||
case "ParticipantLeftMessage":
|
||||
return {
|
||||
type: "event",
|
||||
messageID: d.message_id,
|
||||
threadID: threadID,
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
senderID: d.message_sender.id,
|
||||
timestamp: d.timestamp_precise,
|
||||
eventType: "remove_participants",
|
||||
snippet: d.snippet,
|
||||
eventData: {
|
||||
// Array of IDs.
|
||||
participantsRemoved: d.participants_removed.map(function(p) {
|
||||
return p.id;
|
||||
})
|
||||
},
|
||||
|
||||
// @Legacy
|
||||
logMessageType: "log:unsubscribe",
|
||||
logMessageData: {
|
||||
leftParticipantFbId: d.participants_removed.map(function(p) {
|
||||
return p.id;
|
||||
})
|
||||
}
|
||||
};
|
||||
case "ParticipantsAddedMessage":
|
||||
return {
|
||||
type: "event",
|
||||
messageID: d.message_id,
|
||||
threadID: threadID,
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
senderID: d.message_sender.id,
|
||||
timestamp: d.timestamp_precise,
|
||||
eventType: "add_participants",
|
||||
snippet: d.snippet,
|
||||
eventData: {
|
||||
// Array of IDs.
|
||||
participantsAdded: d.participants_added.map(function(p) {
|
||||
return p.id;
|
||||
})
|
||||
},
|
||||
|
||||
// @Legacy
|
||||
logMessageType: "log:subscribe",
|
||||
logMessageData: {
|
||||
addedParticipants: d.participants_added.map(function(p) {
|
||||
return p.id;
|
||||
})
|
||||
}
|
||||
};
|
||||
case "VideoCallMessage":
|
||||
return {
|
||||
type: "event",
|
||||
messageID: d.message_id,
|
||||
threadID: threadID,
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
senderID: d.message_sender.id,
|
||||
timestamp: d.timestamp_precise,
|
||||
eventType: "video_call",
|
||||
snippet: d.snippet,
|
||||
|
||||
// @Legacy
|
||||
logMessageType: "other"
|
||||
};
|
||||
case "VoiceCallMessage":
|
||||
return {
|
||||
type: "event",
|
||||
messageID: d.message_id,
|
||||
threadID: threadID,
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
senderID: d.message_sender.id,
|
||||
timestamp: d.timestamp_precise,
|
||||
eventType: "voice_call",
|
||||
snippet: d.snippet,
|
||||
|
||||
// @Legacy
|
||||
logMessageType: "other"
|
||||
};
|
||||
case "GenericAdminTextMessage":
|
||||
return {
|
||||
type: "event",
|
||||
messageID: d.message_id,
|
||||
threadID: threadID,
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
senderID: d.message_sender.id,
|
||||
timestamp: d.timestamp_precise,
|
||||
snippet: d.snippet,
|
||||
eventType: d.extensible_message_admin_text_type.toLowerCase(),
|
||||
eventData: formatEventData(d.extensible_message_admin_text),
|
||||
|
||||
// @Legacy
|
||||
logMessageType: utils.getAdminTextMessageType(
|
||||
d.extensible_message_admin_text_type
|
||||
),
|
||||
logMessageData: d.extensible_message_admin_text // Maybe different?
|
||||
};
|
||||
default:
|
||||
return { error: "Don't know about message type " + d.__typename };
|
||||
}
|
||||
});
|
||||
return messages;
|
||||
}
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getThreadHistoryGraphQL(
|
||||
threadID,
|
||||
amount,
|
||||
timestamp,
|
||||
callback
|
||||
) {
|
||||
if (!callback) {
|
||||
throw { error: "getThreadHistoryGraphQL: need callback" };
|
||||
}
|
||||
|
||||
// `queries` has to be a string. I couldn't tell from the dev console. This
|
||||
// took me a really long time to figure out. I deserve a cookie for this.
|
||||
var form = {
|
||||
"av": ctx.globalOptions.pageID,
|
||||
queries: JSON.stringify({
|
||||
o0: {
|
||||
// This doc_id was valid on February 2nd 2017.
|
||||
doc_id: "1498317363570230",
|
||||
query_params: {
|
||||
id: threadID,
|
||||
message_limit: amount,
|
||||
load_messages: 1,
|
||||
load_read_receipts: false,
|
||||
before: timestamp
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
// This returns us an array of things. The last one is the success /
|
||||
// failure one.
|
||||
// @TODO What do we do in this case?
|
||||
if (resData[resData.length - 1].error_results !== 0) {
|
||||
throw new Error("well darn there was an error_result");
|
||||
}
|
||||
|
||||
callback(null, formatMessagesGraphQLResponse(resData[0]));
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getThreadHistoryGraphQL", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getThreadHistory(threadID, amount, timestamp, callback) {
|
||||
if (!callback) {
|
||||
throw { error: "getThreadHistory: need callback" };
|
||||
}
|
||||
|
||||
var form = {
|
||||
client: "mercury"
|
||||
};
|
||||
|
||||
api.getUserInfo(threadID, function(err, res) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var key = Object.keys(res).length > 0 ? "user_ids" : "thread_fbids";
|
||||
form["messages[" + key + "][" + threadID + "][offset]"] = 0;
|
||||
form["messages[" + key + "][" + threadID + "][timestamp]"] = timestamp;
|
||||
form["messages[" + key + "][" + threadID + "][limit]"] = amount;
|
||||
|
||||
if (ctx.globalOptions.pageID)
|
||||
form.request_user_id = ctx.globalOptions.pageID;
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/thread_info.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
} else if (!resData.payload) {
|
||||
throw { error: "Could not retrieve thread history." };
|
||||
}
|
||||
|
||||
// Asking for message history from a thread with no message history
|
||||
// will return undefined for actions here
|
||||
if (!resData.payload.actions) {
|
||||
resData.payload.actions = [];
|
||||
}
|
||||
|
||||
var userIDs = {};
|
||||
resData.payload.actions.forEach(function(v) {
|
||||
userIDs[v.author.split(":").pop()] = "";
|
||||
});
|
||||
|
||||
api.getUserInfo(Object.keys(userIDs), function(err, data) {
|
||||
if (err) return callback(err); //callback({error: "Could not retrieve user information in getThreadHistory."});
|
||||
|
||||
resData.payload.actions.forEach(function(v) {
|
||||
var sender = data[v.author.split(":").pop()];
|
||||
if (sender) v.sender_name = sender.name;
|
||||
else v.sender_name = "Facebook User";
|
||||
v.sender_fbid = v.author;
|
||||
delete v.author;
|
||||
});
|
||||
|
||||
callback(
|
||||
null,
|
||||
resData.payload.actions.map(utils.formatHistoryMessage)
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getThreadHistory", err);
|
||||
return callback(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
function formatEventReminders(reminder) {
|
||||
return {
|
||||
reminderID: reminder.id,
|
||||
eventCreatorID: reminder.lightweight_event_creator.id,
|
||||
time: reminder.time,
|
||||
eventType: reminder.lightweight_event_type.toLowerCase(),
|
||||
locationName: reminder.location_name,
|
||||
// @TODO verify this
|
||||
locationCoordinates: reminder.location_coordinates,
|
||||
locationPage: reminder.location_page,
|
||||
eventStatus: reminder.lightweight_event_status.toLowerCase(),
|
||||
note: reminder.note,
|
||||
repeatMode: reminder.repeat_mode.toLowerCase(),
|
||||
eventTitle: reminder.event_title,
|
||||
triggerMessage: reminder.trigger_message,
|
||||
secondsToNotifyBefore: reminder.seconds_to_notify_before,
|
||||
allowsRsvp: reminder.allows_rsvp,
|
||||
relatedEvent: reminder.related_event,
|
||||
members: reminder.event_reminder_members.edges.map(function(member) {
|
||||
return {
|
||||
memberID: member.node.id,
|
||||
state: member.guest_list_state.toLowerCase()
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function formatThreadGraphQLResponse(data) {
|
||||
var messageThread = data.o0.data.message_thread;
|
||||
var threadID = messageThread.thread_key.thread_fbid
|
||||
? messageThread.thread_key.thread_fbid
|
||||
: messageThread.thread_key.other_user_id;
|
||||
|
||||
// Remove me
|
||||
var lastM = messageThread.last_message;
|
||||
var snippetID =
|
||||
lastM &&
|
||||
lastM.nodes &&
|
||||
lastM.nodes[0] &&
|
||||
lastM.nodes[0].message_sender &&
|
||||
lastM.nodes[0].message_sender.messaging_actor
|
||||
? lastM.nodes[0].message_sender.messaging_actor.id
|
||||
: null;
|
||||
var snippetText =
|
||||
lastM && lastM.nodes && lastM.nodes[0] ? lastM.nodes[0].snippet : null;
|
||||
var lastR = messageThread.last_read_receipt;
|
||||
var lastReadTimestamp =
|
||||
lastR && lastR.nodes && lastR.nodes[0] && lastR.nodes[0].timestamp_precise
|
||||
? lastR.nodes[0].timestamp_precise
|
||||
: null;
|
||||
|
||||
return {
|
||||
threadID: threadID,
|
||||
threadName: messageThread.name,
|
||||
participantIDs: messageThread.all_participants.nodes.map(function(d) {
|
||||
return d.messaging_actor.id;
|
||||
}),
|
||||
unreadCount: messageThread.unread_count,
|
||||
messageCount: messageThread.messages_count,
|
||||
timestamp: messageThread.updated_time_precise,
|
||||
muteUntil: messageThread.mute_until,
|
||||
isGroup: messageThread.thread_type == "GROUP",
|
||||
isSubscribed: messageThread.is_viewer_subscribed,
|
||||
isArchived: messageThread.has_viewer_archived,
|
||||
folder: messageThread.folder,
|
||||
cannotReplyReason: messageThread.cannot_reply_reason,
|
||||
eventReminders: messageThread.event_reminders
|
||||
? messageThread.event_reminders.nodes.map(formatEventReminders)
|
||||
: null,
|
||||
emoji: messageThread.customization_info
|
||||
? messageThread.customization_info.emoji
|
||||
: null,
|
||||
color:
|
||||
messageThread.customization_info &&
|
||||
messageThread.customization_info.outgoing_bubble_color
|
||||
? messageThread.customization_info.outgoing_bubble_color.slice(2)
|
||||
: null,
|
||||
nicknames:
|
||||
messageThread.customization_info &&
|
||||
messageThread.customization_info.participant_customizations
|
||||
? messageThread.customization_info.participant_customizations.reduce(
|
||||
function(res, val) {
|
||||
if (val.nickname) res[val.participant_id] = val.nickname;
|
||||
return res;
|
||||
},
|
||||
{}
|
||||
)
|
||||
: {},
|
||||
adminIDs: messageThread.thread_admins,
|
||||
|
||||
// @Undocumented
|
||||
topEmojis: messageThread.top_emojis,
|
||||
reactionsMuteMode: messageThread.reactions_mute_mode.toLowerCase(),
|
||||
mentionsMuteMode: messageThread.mentions_mute_mode.toLowerCase(),
|
||||
isPinProtected: messageThread.is_pin_protected,
|
||||
relatedPageThread: messageThread.related_page_thread,
|
||||
|
||||
// @Legacy
|
||||
name: messageThread.name,
|
||||
snippet: snippetText,
|
||||
snippetSender: snippetID,
|
||||
snippetAttachments: [],
|
||||
serverTimestamp: messageThread.updated_time_precise,
|
||||
imageSrc: messageThread.image ? messageThread.image.uri : null,
|
||||
isCanonicalUser: messageThread.is_canonical_neo_user,
|
||||
isCanonical: messageThread.thread_type != "GROUP",
|
||||
recipientsLoadable: true,
|
||||
hasEmailParticipant: false,
|
||||
readOnly: false,
|
||||
canReply: messageThread.cannot_reply_reason == null,
|
||||
lastMessageTimestamp: messageThread.last_message
|
||||
? messageThread.last_message.timestamp_precise
|
||||
: null,
|
||||
lastMessageType: "message",
|
||||
lastReadTimestamp: lastReadTimestamp,
|
||||
threadType: messageThread.thread_type == "GROUP" ? 2 : 1
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getThreadInfoGraphQL(threadID, callback) {
|
||||
if (!callback) {
|
||||
throw { error: "getThreadInfoGraphQL: need callback" };
|
||||
}
|
||||
|
||||
// `queries` has to be a string. I couldn't tell from the dev console. This
|
||||
// took me a really long time to figure out. I deserve a cookie for this.
|
||||
var form = {
|
||||
queries: JSON.stringify({
|
||||
o0: {
|
||||
// This doc_id is valid as of February 1st, 2018.
|
||||
doc_id: "1498317363570230",
|
||||
query_params: {
|
||||
id: threadID,
|
||||
message_limit: 0,
|
||||
load_messages: 0,
|
||||
load_read_receipts: false,
|
||||
before: null
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
// This returns us an array of things. The last one is the success /
|
||||
// failure one.
|
||||
// @TODO What do we do in this case?
|
||||
if (resData[resData.length - 1].error_results !== 0) {
|
||||
throw new Error("well darn there was an error_result");
|
||||
}
|
||||
|
||||
callback(null, formatThreadGraphQLResponse(resData[0]));
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getThreadInfoGraphQL", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getThreadInfo(threadID, callback) {
|
||||
if (!callback) callback = function() {};
|
||||
|
||||
var form = {
|
||||
client: "mercury"
|
||||
};
|
||||
|
||||
api.getUserInfo(threadID, function(err, userRes) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var key = Object.keys(userRes).length > 0 ? "user_ids" : "thread_fbids";
|
||||
form["threads[" + key + "][0]"] = threadID;
|
||||
|
||||
if (ctx.globalOptions.pageId)
|
||||
form.request_user_id = ctx.globalOptions.pageId;
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/thread_info.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
} else if (!resData.payload) {
|
||||
throw {
|
||||
error: "Could not retrieve thread Info."
|
||||
};
|
||||
}
|
||||
var threadData = resData.payload.threads[0];
|
||||
var userData = userRes[threadID];
|
||||
|
||||
if (threadData == null) {
|
||||
throw {
|
||||
error: "ThreadData is null"
|
||||
};
|
||||
}
|
||||
|
||||
threadData.name =
|
||||
userData != null && userData.name != null
|
||||
? userData.name
|
||||
: threadData.name;
|
||||
threadData.image_src =
|
||||
userData != null && userData.thumbSrc != null
|
||||
? userData.thumbSrc
|
||||
: threadData.image_src;
|
||||
|
||||
callback(null, utils.formatThread(threadData));
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getThreadInfo", err);
|
||||
return callback(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
"use strict";
|
||||
|
||||
const utils = require("../utils");
|
||||
const log = require("npmlog");
|
||||
|
||||
function createProfileUrl(url, username, id) {
|
||||
if (url) return url;
|
||||
return "https://www.facebook.com/" + (username || utils.formatID(id.toString()));
|
||||
}
|
||||
|
||||
function formatParticipants(participants) {
|
||||
return participants.nodes.map((p)=>{
|
||||
p = p.messaging_actor;
|
||||
switch (p["__typename"]) {
|
||||
case "User":
|
||||
return {
|
||||
accountType: p["__typename"],
|
||||
userID: utils.formatID(p.id.toString()), // do we need .toString()? when it is not a string?
|
||||
name: p.name,
|
||||
shortName: p.short_name,
|
||||
gender: p.gender,
|
||||
url: p.url, // how about making it profileURL
|
||||
profilePicture: p.big_image_src.uri,
|
||||
username: (p.username||null),
|
||||
// TODO: maybe better names for these?
|
||||
isViewerFriend: p.is_viewer_friend, // true/false
|
||||
isMessengerUser: p.is_messenger_user, // true/false
|
||||
isVerified: p.is_verified, // true/false
|
||||
isMessageBlockedByViewer: p.is_message_blocked_by_viewer, // true/false
|
||||
isViewerCoworker: p.is_viewer_coworker, // true/false
|
||||
isEmployee: p.is_employee // null? when it is something other? can someone check?
|
||||
};
|
||||
case "Page":
|
||||
return {
|
||||
accountType: p["__typename"],
|
||||
userID: utils.formatID(p.id.toString()), // or maybe... pageID?
|
||||
name: p.name,
|
||||
url: p.url,
|
||||
profilePicture: p.big_image_src.uri,
|
||||
username: (p.username||null),
|
||||
// uhm... better names maybe?
|
||||
acceptsMessengerUserFeedback: p.accepts_messenger_user_feedback, // true/false
|
||||
isMessengerUser: p.is_messenger_user, // true/false
|
||||
isVerified: p.is_verified, // true/false
|
||||
isMessengerPlatformBot: p.is_messenger_platform_bot, // true/false
|
||||
isMessageBlockedByViewer: p.is_message_blocked_by_viewer, // true/false
|
||||
};
|
||||
case "ReducedMessagingActor":
|
||||
return {
|
||||
accountType: p["__typename"],
|
||||
userID: utils.formatID(p.id.toString()),
|
||||
name: p.name,
|
||||
url: createProfileUrl(p.url, p.username, p.id), // in this case p.url is null all the time
|
||||
profilePicture: p.big_image_src.uri, // in this case it is default facebook photo, we could determine gender using it
|
||||
username: (p.username||null), // maybe we could use it to generate profile URL?
|
||||
isMessageBlockedByViewer: p.is_message_blocked_by_viewer, // true/false
|
||||
};
|
||||
case "UnavailableMessagingActor":
|
||||
return {
|
||||
accountType: p["__typename"],
|
||||
userID: utils.formatID(p.id.toString()),
|
||||
name: p.name, // "Facebook User" in user's language
|
||||
url: createProfileUrl(p.url, p.username, p.id), // in this case p.url is null all the time
|
||||
profilePicture: p.big_image_src.uri, // default male facebook photo
|
||||
username: (p.username||null), // maybe we could use it to generate profile URL?
|
||||
isMessageBlockedByViewer: p.is_message_blocked_by_viewer, // true/false
|
||||
};
|
||||
default:
|
||||
log.warn("getThreadList", "Found participant with unsupported typename. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues\n" + JSON.stringify(p, null, 2));
|
||||
return {
|
||||
accountType: p["__typename"],
|
||||
userID: utils.formatID(p.id.toString()),
|
||||
name: p.name || `[unknown ${p["__typename"]}]`, // probably it will always be something... but fallback to [unknown], just in case
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// "FF8C0077" -> "8C0077"
|
||||
function formatColor(color) {
|
||||
if (color && color.match(/^(?:[0-9a-fA-F]{8})$/g)) {
|
||||
return color.slice(2);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
function getThreadName(t) {
|
||||
if (t.name || t.thread_key.thread_fbid) return t.name;
|
||||
|
||||
for (let p of t.all_participants.nodes) {
|
||||
if (p.messaging_actor.id === t.thread_key.other_user_id) return p.messaging_actor.name;
|
||||
}
|
||||
}
|
||||
|
||||
function mapNicknames(customizationInfo) {
|
||||
return (customizationInfo && customizationInfo.participant_customizations) ? customizationInfo.participant_customizations.map(u => {
|
||||
return {
|
||||
"userID": u.participant_id,
|
||||
"nickname": u.nickname
|
||||
};
|
||||
}):[];
|
||||
}
|
||||
|
||||
function formatThreadList(data) {
|
||||
return data.map(t => {
|
||||
let lastMessageNode = (t.last_message&&t.last_message.nodes&&t.last_message.nodes.length>0)?t.last_message.nodes[0]:null;
|
||||
return {
|
||||
threadID: t.thread_key?utils.formatID(t.thread_key.thread_fbid || t.thread_key.other_user_id):null, // shall never be null
|
||||
name: getThreadName(t),
|
||||
unreadCount: t.unread_count,
|
||||
messageCount: t.messages_count,
|
||||
imageSrc: t.image?t.image.uri:null,
|
||||
emoji: t.customization_info?t.customization_info.emoji:null,
|
||||
color: formatColor(t.customization_info?t.customization_info.outgoing_bubble_color:null),
|
||||
nicknames: mapNicknames(t.customization_info),
|
||||
muteUntil: t.mute_until,
|
||||
participants: formatParticipants(t.all_participants),
|
||||
adminIDs: t.thread_admins.map(a => a.id),
|
||||
folder: t.folder,
|
||||
isGroup: t.thread_type === "GROUP",
|
||||
// rtc_call_data: t.rtc_call_data, // TODO: format and document this
|
||||
// isPinProtected: t.is_pin_protected, // feature from future? always false (2018-04-04)
|
||||
customizationEnabled: t.customization_enabled, // false for ONE_TO_ONE with Page or ReducedMessagingActor
|
||||
participantAddMode: t.participant_add_mode_as_string, // "ADD" if "GROUP" and null if "ONE_TO_ONE"
|
||||
montageThread: t.montage_thread?Buffer.from(t.montage_thread.id,"base64").toString():null, // base64 encoded string "message_thread:0000000000000000"
|
||||
// it is not userID nor any other ID known to me...
|
||||
// can somebody inspect it? where is it used?
|
||||
// probably Messenger Day uses it
|
||||
reactionsMuteMode: t.reactions_mute_mode,
|
||||
mentionsMuteMode: t.mentions_mute_mode,
|
||||
isArchived: t.has_viewer_archived,
|
||||
isSubscribed: t.is_viewer_subscribed,
|
||||
timestamp: t.updated_time_precise, // in miliseconds
|
||||
// isCanonicalUser: t.is_canonical_neo_user, // is it always false?
|
||||
// TODO: how about putting snippet in another object? current implementation does not handle every possibile message type etc.
|
||||
snippet: lastMessageNode?lastMessageNode.snippet:null,
|
||||
snippetAttachments: lastMessageNode?lastMessageNode.extensible_attachment:null, // TODO: not sure if it works
|
||||
snippetSender: lastMessageNode?utils.formatID((lastMessageNode.message_sender.messaging_actor.id || "").toString()):null,
|
||||
lastMessageTimestamp: lastMessageNode?lastMessageNode.timestamp_precise:null, // timestamp in miliseconds
|
||||
lastReadTimestamp: (t.last_read_receipt&&t.last_read_receipt.nodes.length>0)
|
||||
? (t.last_read_receipt.nodes[0]?t.last_read_receipt.nodes[0].timestamp_precise:null)
|
||||
: null, // timestamp in miliseconds
|
||||
cannotReplyReason: t.cannot_reply_reason, // TODO: inspect possible values
|
||||
|
||||
// @Legacy
|
||||
participantIDs: formatParticipants(t.all_participants).map(participant => participant.userID),
|
||||
threadType: t.thread_type === "GROUP" ? 2 : 1 // "GROUP" or "ONE_TO_ONE"
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getThreadList(limit, timestamp, tags, callback) {
|
||||
if (!callback && (utils.getType(tags) === "Function" || utils.getType(tags) === "AsyncFunction")) {
|
||||
callback = tags;
|
||||
tags = [""];
|
||||
}
|
||||
if (utils.getType(limit) !== "Number" || !Number.isInteger(limit) || limit <= 0) {
|
||||
throw {error: "getThreadList: limit must be a positive integer"};
|
||||
}
|
||||
if (utils.getType(timestamp) !== "Null" &&
|
||||
(utils.getType(timestamp) !== "Number" || !Number.isInteger(timestamp))) {
|
||||
throw {error: "getThreadList: timestamp must be an integer or null"};
|
||||
}
|
||||
if (utils.getType(tags) === "String") {
|
||||
tags = [tags];
|
||||
}
|
||||
if (utils.getType(tags) !== "Array") {
|
||||
throw {error: "getThreadList: tags must be an array"};
|
||||
}
|
||||
if (utils.getType(callback) !== "Function" && utils.getType(callback) !== "AsyncFunction") {
|
||||
throw {error: "getThreadList: need callback"};
|
||||
}
|
||||
|
||||
const form = {
|
||||
"av": ctx.globalOptions.pageID,
|
||||
"queries": JSON.stringify({
|
||||
"o0": {
|
||||
// This doc_id was valid on 2018-04-04.
|
||||
"doc_id": "1349387578499440",
|
||||
"query_params": {
|
||||
"limit": limit+(timestamp?1:0),
|
||||
"before": timestamp,
|
||||
"tags": tags,
|
||||
"includeDeliveryReceipts": true,
|
||||
"includeSeqID": false
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then((resData) => {
|
||||
if (resData[resData.length - 1].error_results > 0) {
|
||||
throw resData[0].o0.errors;
|
||||
}
|
||||
|
||||
if (resData[resData.length - 1].successful_results === 0) {
|
||||
throw {error: "getThreadList: there was no successful_results", res: resData};
|
||||
}
|
||||
|
||||
// When we ask for threads using timestamp from the previous request,
|
||||
// we are getting the last thread repeated as the first thread in this response.
|
||||
// .shift() gets rid of it
|
||||
// It is also the reason for increasing limit by 1 when timestamp is set
|
||||
// this way user asks for 10 threads, we are asking for 11,
|
||||
// but after removing the duplicated one, it is again 10
|
||||
if (timestamp) {
|
||||
resData[0].o0.data.viewer.message_threads.nodes.shift();
|
||||
}
|
||||
callback(null, formatThreadList(resData[0].o0.data.viewer.message_threads.nodes));
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("getThreadList", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getThreadList(start, end, type, callback) {
|
||||
if (utils.getType(callback) === "Undefined") {
|
||||
if (utils.getType(end) !== "Number") {
|
||||
throw {
|
||||
error: "Please pass a number as a second argument."
|
||||
};
|
||||
} else if (
|
||||
utils.getType(type) === "Function" ||
|
||||
utils.getType(type) === "AsyncFunction"
|
||||
) {
|
||||
callback = type;
|
||||
type = "inbox"; //default to inbox
|
||||
} else if (utils.getType(type) !== "String") {
|
||||
throw {
|
||||
error:
|
||||
"Please pass a String as a third argument. Your options are: inbox, pending, and archived"
|
||||
};
|
||||
} else {
|
||||
throw {
|
||||
error: "getThreadList: need callback"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "archived") {
|
||||
type = "action:archived";
|
||||
} else if (type !== "inbox" && type !== "pending" && type !== "other") {
|
||||
throw {
|
||||
error:
|
||||
"type can only be one of the following: inbox, pending, archived, other"
|
||||
};
|
||||
}
|
||||
|
||||
if (end <= start) end = start + 20;
|
||||
|
||||
var form = {
|
||||
client: "mercury"
|
||||
};
|
||||
|
||||
form[type + "[offset]"] = start;
|
||||
form[type + "[limit]"] = end - start;
|
||||
|
||||
if (ctx.globalOptions.pageID) {
|
||||
form.request_user_id = ctx.globalOptions.pageID;
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/threadlist_info.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
log.verbose("getThreadList", JSON.stringify(resData.payload.threads));
|
||||
return callback(
|
||||
null,
|
||||
(resData.payload.threads || []).map(utils.formatThread)
|
||||
);
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getThreadList", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getThreadPictures(threadID, offset, limit, callback) {
|
||||
if (!callback) {
|
||||
throw { error: "getThreadPictures: need callback" };
|
||||
}
|
||||
|
||||
var form = {
|
||||
thread_id: threadID,
|
||||
offset: offset,
|
||||
limit: limit
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
return Promise.all(
|
||||
resData.payload.imagesData.map(function(image) {
|
||||
form = {
|
||||
thread_id: threadID,
|
||||
image_id: image.fbid
|
||||
};
|
||||
return defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
// the response is pretty messy
|
||||
var queryThreadID =
|
||||
resData.jsmods.require[0][3][1].query_metadata.query_path[0]
|
||||
.message_thread;
|
||||
var imageData =
|
||||
resData.jsmods.require[0][3][1].query_results[queryThreadID]
|
||||
.message_images.edges[0].node.image2;
|
||||
return imageData;
|
||||
});
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(function(resData) {
|
||||
callback(null, resData);
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("Error in getThreadPictures", err);
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
function formatData(data) {
|
||||
return {
|
||||
userID: utils.formatID(data.uid.toString()),
|
||||
photoUrl: data.photo,
|
||||
indexRank: data.index_rank,
|
||||
name: data.text,
|
||||
isVerified: data.is_verified,
|
||||
profileUrl: data.path,
|
||||
category: data.category,
|
||||
score: data.score,
|
||||
type: data.type
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getUserID(name, callback) {
|
||||
if (!callback) {
|
||||
throw { error: "getUserID: need callback" };
|
||||
}
|
||||
|
||||
var form = {
|
||||
value: name.toLowerCase(),
|
||||
viewer: ctx.userID,
|
||||
rsp: "search",
|
||||
context: "search",
|
||||
path: "/home.php",
|
||||
request_id: utils.getGUID()
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.get("https://www.facebook.com/ajax/typeahead/search.php", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
var data = resData.payload.entries;
|
||||
|
||||
callback(null, data.map(formatData));
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getUserID", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
function formatData(data) {
|
||||
var retObj = {};
|
||||
|
||||
for (var prop in data) {
|
||||
if (data.hasOwnProperty(prop)) {
|
||||
var innerObj = data[prop];
|
||||
retObj[prop] = {
|
||||
name: innerObj.name,
|
||||
firstName: innerObj.firstName,
|
||||
vanity: innerObj.vanity,
|
||||
thumbSrc: innerObj.thumbSrc,
|
||||
profileUrl: innerObj.uri,
|
||||
gender: innerObj.gender,
|
||||
type: innerObj.type,
|
||||
isFriend: innerObj.is_friend,
|
||||
isBirthday: !!innerObj.is_birthday
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return retObj;
|
||||
}
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getUserInfo(id, callback) {
|
||||
if (!callback) {
|
||||
throw { error: "getUserInfo: need callback" };
|
||||
}
|
||||
|
||||
if (utils.getType(id) !== "Array") {
|
||||
id = [id];
|
||||
}
|
||||
|
||||
var form = {};
|
||||
id.map(function(v, i) {
|
||||
form["ids[" + i + "]"] = v;
|
||||
});
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/chat/user_info/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
return callback(null, formatData(resData.payload.profiles));
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getUserInfo", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function handleMessageRequest(threadID, accept, callback) {
|
||||
if (utils.getType(accept) !== "Boolean") {
|
||||
throw {
|
||||
error: "Please pass a boolean as a second argument."
|
||||
};
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
client: "mercury"
|
||||
};
|
||||
|
||||
if (utils.getType(threadID) !== "Array") {
|
||||
threadID = [threadID];
|
||||
}
|
||||
|
||||
var messageBox = accept ? "inbox" : "other";
|
||||
|
||||
for (var i = 0; i < threadID.length; i++) {
|
||||
form[messageBox + "[" + i + "]"] = threadID[i];
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/move_thread.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("handleMessageRequest", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+553
@@ -0,0 +1,553 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
var msgsRecv = 0;
|
||||
var identity = function() {};
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
var currentlyRunning = null;
|
||||
var globalCallback = identity;
|
||||
|
||||
var stopListening = function() {
|
||||
globalCallback = identity;
|
||||
if (currentlyRunning) {
|
||||
clearTimeout(currentlyRunning);
|
||||
currentlyRunning = null;
|
||||
}
|
||||
};
|
||||
|
||||
var prev = Date.now();
|
||||
var tmpPrev = Date.now();
|
||||
var lastSync = Date.now();
|
||||
|
||||
var form = {
|
||||
channel: "p_" + ctx.userID,
|
||||
seq: "0",
|
||||
partition: "-2",
|
||||
clientid: ctx.clientID,
|
||||
viewer_uid: ctx.userID,
|
||||
uid: ctx.userID,
|
||||
state: "active",
|
||||
idle: 0,
|
||||
cap: "8",
|
||||
msgs_recv: msgsRecv,
|
||||
qp: "y",
|
||||
pws: "fresh"
|
||||
};
|
||||
|
||||
if (ctx.globalOptions.pageID) {
|
||||
form.aiq = ctx.globalOptions.pageID + ",0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an object maybe representing an event. Handles events it wants to handle
|
||||
* and returns true if it did handle an event (and called the globalCallback).
|
||||
* Returns false otherwise.
|
||||
*/
|
||||
function handleMessagingEvents(event) {
|
||||
switch (event.event) {
|
||||
// "read_receipt" event triggers when other people read the user's messages.
|
||||
case "read_receipt":
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatReadReceipt(event);
|
||||
} catch (err) {
|
||||
globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: event,
|
||||
type: "parse_error"
|
||||
});
|
||||
return true;
|
||||
}
|
||||
globalCallback(null, fmtMsg);
|
||||
return true;
|
||||
// "read event" triggers when the user read other people's messages.
|
||||
case "read":
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatRead(event);
|
||||
} catch (err) {
|
||||
globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: event,
|
||||
type: "parse_error"
|
||||
});
|
||||
return true;
|
||||
}
|
||||
globalCallback(null, fmtMsg);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var serverNumber = "0";
|
||||
|
||||
function listen(servern) {
|
||||
if (currentlyRunning == null || !ctx.loggedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.idle = ~~(Date.now() / 1000) - prev;
|
||||
prev = ~~(Date.now() / 1000);
|
||||
var presence = utils.generatePresence(ctx.userID);
|
||||
ctx.jar.setCookie(
|
||||
"presence=" + presence + "; path=/; domain=.facebook.com; secure",
|
||||
"https://www.facebook.com"
|
||||
);
|
||||
defaultFuncs
|
||||
.get(
|
||||
"https://" + serverNumber + "-edge-chat.facebook.com/pull",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
var now = Date.now();
|
||||
log.info("listen", "Got answer in " + (now - tmpPrev));
|
||||
tmpPrev = now;
|
||||
if (resData && resData.t === "lb") {
|
||||
form.sticky_token = resData.lb_info.sticky;
|
||||
form.sticky_pool = resData.lb_info.pool;
|
||||
}
|
||||
|
||||
if (resData && resData.t === "fullReload") {
|
||||
form.seq = resData.seq;
|
||||
delete form.sticky_pool;
|
||||
delete form.sticky_token;
|
||||
var form4 = {
|
||||
lastSync: ~~(lastSync / 1000)
|
||||
};
|
||||
defaultFuncs
|
||||
.get("https://www.facebook.com/notifications/sync/", ctx.jar, form4)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(function() {
|
||||
lastSync = Date.now();
|
||||
var form = {
|
||||
client: "mercury",
|
||||
"folders[0]": "inbox",
|
||||
last_action_timestamp: ~~(Date.now() - 60)
|
||||
};
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/thread_sync.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(function() {
|
||||
currentlyRunning = setTimeout(listen, 1000);
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (resData.ms) {
|
||||
msgsRecv += resData.ms.length;
|
||||
var atLeastOne = false;
|
||||
resData.ms
|
||||
.sort(function(a, b) {
|
||||
return a.timestamp - b.timestamp;
|
||||
})
|
||||
.forEach(function parsePackets(v) {
|
||||
switch (v.type) {
|
||||
// TODO: 'ttyp' was used before. It changed to 'typ'. We're keeping
|
||||
// both for now but we should remove 'ttyp' at some point.
|
||||
case "ttyp":
|
||||
case "typ":
|
||||
if (
|
||||
!ctx.globalOptions.listenEvents ||
|
||||
(!ctx.globalOptions.selfListen &&
|
||||
v.from.toString() === ctx.userID)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatTyp(v);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return globalCallback(null, fmtMsg);
|
||||
case "chatproxy-presence":
|
||||
// TODO: what happens when you're logged in as a page?
|
||||
if (!ctx.globalOptions.updatePresence) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.loggedIn) {
|
||||
for (var userID in v.buddyList) {
|
||||
var formattedPresence;
|
||||
try {
|
||||
formattedPresence = utils.formatProxyPresence(
|
||||
v.buddyList[userID],
|
||||
userID
|
||||
);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.buddyList[userID],
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
|
||||
if (formattedPresence != null) {
|
||||
globalCallback(null, formattedPresence);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
case "buddylist_overlay":
|
||||
// TODO: what happens when you're logged in as a page?
|
||||
if (!ctx.globalOptions.updatePresence) {
|
||||
return;
|
||||
}
|
||||
// There should be only one key inside overlay
|
||||
Object.keys(v.overlay).map(function(userID) {
|
||||
var formattedPresence;
|
||||
try {
|
||||
formattedPresence = utils.formatPresence(
|
||||
v.overlay[userID],
|
||||
userID
|
||||
);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.overlay[userID],
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
if (ctx.loggedIn) {
|
||||
return globalCallback(null, formattedPresence);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "delta":
|
||||
if (v.delta.class == "NewMessage") {
|
||||
if (ctx.globalOptions.pageID &&
|
||||
ctx.globalOptions.pageID != v.queue
|
||||
)
|
||||
return;
|
||||
|
||||
(function resolveAttachmentUrl(i) {
|
||||
if (i == v.delta.attachments.length) {
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatDeltaMessage(v);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return !ctx.globalOptions.selfListen &&
|
||||
fmtMsg.senderID === ctx.userID ?
|
||||
undefined :
|
||||
globalCallback(null, fmtMsg);
|
||||
} else {
|
||||
if (
|
||||
v.delta.attachments[i].mercury.attach_type == "photo"
|
||||
) {
|
||||
api.resolvePhotoUrl(
|
||||
v.delta.attachments[i].fbid,
|
||||
(err, url) => {
|
||||
if (!err)
|
||||
v.delta.attachments[
|
||||
i
|
||||
].mercury.metadata.url = url;
|
||||
return resolveAttachmentUrl(i + 1);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return resolveAttachmentUrl(i + 1);
|
||||
}
|
||||
}
|
||||
})(0);
|
||||
break;
|
||||
}
|
||||
|
||||
if (v.delta.class == "ClientPayload") {
|
||||
var clientPayload = utils.decodeClientPayload(
|
||||
v.delta.payload
|
||||
);
|
||||
if (clientPayload && clientPayload.deltas) {
|
||||
for (var i in clientPayload.deltas) {
|
||||
var delta = clientPayload.deltas[i];
|
||||
if (delta.deltaMessageReaction && !!ctx.globalOptions.listenEvents) {
|
||||
globalCallback(null, {
|
||||
type: "message_reaction",
|
||||
threadID: delta.deltaMessageReaction.threadKey
|
||||
.threadFbId ?
|
||||
delta.deltaMessageReaction.threadKey.threadFbId : delta.deltaMessageReaction.threadKey
|
||||
.otherUserFbId,
|
||||
messageID: delta.deltaMessageReaction.messageId,
|
||||
reaction: delta.deltaMessageReaction.reaction,
|
||||
senderID: delta.deltaMessageReaction.senderId,
|
||||
userID: delta.deltaMessageReaction.userId,
|
||||
timestamp: v.ofd_ts
|
||||
});
|
||||
} else if (delta.deltaRecallMessageData && !!ctx.globalOptions.listenEvents) {
|
||||
globalCallback(null, {
|
||||
type: "message_unsend",
|
||||
threadID: delta.deltaRecallMessageData.threadKey.threadFbId ?
|
||||
delta.deltaRecallMessageData.threadKey.threadFbId : delta.deltaRecallMessageData.threadKey
|
||||
.otherUserFbId,
|
||||
messageID: delta.deltaRecallMessageData.messageID,
|
||||
senderID: delta.deltaRecallMessageData.senderID,
|
||||
deletionTimestamp: delta.deltaRecallMessageData.deletionTimestamp,
|
||||
timestamp: v.ofd_ts
|
||||
});
|
||||
} else if (delta.deltaMessageReply) {
|
||||
//Mention block - #1
|
||||
var mdata =
|
||||
delta.deltaMessageReply.message.data === undefined ? [] :
|
||||
delta.deltaMessageReply.message.data.prng === undefined ? [] :
|
||||
JSON.parse(delta.deltaMessageReply.message.data.prng);
|
||||
var m_id = mdata.map(u => u.i);
|
||||
var m_offset = mdata.map(u => u.o);
|
||||
var m_length = mdata.map(u => u.l);
|
||||
|
||||
var mentions = {};
|
||||
|
||||
for (var i = 0; i < m_id.length; i++) {
|
||||
mentions[m_id[i]] = delta.deltaMessageReply.message.body.substring(
|
||||
m_offset[i],
|
||||
m_offset[i] + m_length[i]
|
||||
);
|
||||
}
|
||||
//Mention block - 1#
|
||||
//Mention block - #2
|
||||
var mdata =
|
||||
delta.deltaMessageReply.repliedToMessage.data === undefined ? [] :
|
||||
delta.deltaMessageReply.repliedToMessage.data.prng === undefined ? [] :
|
||||
JSON.parse(delta.deltaMessageReply.repliedToMessage.data.prng);
|
||||
var m_id = mdata.map(u => u.i);
|
||||
var m_offset = mdata.map(u => u.o);
|
||||
var m_length = mdata.map(u => u.l);
|
||||
|
||||
var rmentions = {};
|
||||
|
||||
for (var i = 0; i < m_id.length; i++) {
|
||||
rmentions[m_id[i]] = delta.deltaMessageReply.repliedToMessage.body.substring(
|
||||
m_offset[i],
|
||||
m_offset[i] + m_length[i]
|
||||
);
|
||||
}
|
||||
//Mention block - 2#
|
||||
|
||||
globalCallback(null, {
|
||||
type: "message_reply",
|
||||
threadID: delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId ?
|
||||
delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId : delta.deltaMessageReply.message.messageMetadata.threadKey
|
||||
.otherUserFbId,
|
||||
messageID: delta.deltaMessageReply.message.messageMetadata.messageId,
|
||||
senderID: delta.deltaMessageReply.message.messageMetadata.actorFbId,
|
||||
attachments: delta.deltaMessageReply.message.attachments.map(function(att) {
|
||||
var mercury = JSON.parse(att.mercuryJSON);
|
||||
Object.assign(att, mercury);
|
||||
return att;
|
||||
}).map(att => utils._formatAttachment(att)),
|
||||
body: delta.deltaMessageReply.message.body || "",
|
||||
isGroup: !!delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId,
|
||||
mentions: mentions,
|
||||
timestamp: v.ofd_ts,
|
||||
messageReply: {
|
||||
threadID: delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId ?
|
||||
delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId : delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey
|
||||
.otherUserFbId,
|
||||
messageID: delta.deltaMessageReply.repliedToMessage.messageMetadata.messageId,
|
||||
senderID: delta.deltaMessageReply.repliedToMessage.messageMetadata.actorFbId,
|
||||
attachments: delta.deltaMessageReply.repliedToMessage.attachments.map(function(att) {
|
||||
var mercury = JSON.parse(att.mercuryJSON);
|
||||
Object.assign(att, mercury);
|
||||
return att;
|
||||
}).map(att => utils._formatAttachment(att)),
|
||||
body: delta.deltaMessageReply.repliedToMessage.body || "",
|
||||
isGroup: !!delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId,
|
||||
mentions: rmentions
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (v.delta.class !== "NewMessage" &&
|
||||
!ctx.globalOptions.listenEvents
|
||||
)
|
||||
return;
|
||||
switch (v.delta.class) {
|
||||
case "ReadReceipt":
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatDeltaReadReceipt(v.delta);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.delta,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return globalCallback(null, fmtMsg);
|
||||
case "AdminTextMessage":
|
||||
switch (v.delta.type) {
|
||||
case "change_thread_theme":
|
||||
case "change_thread_nickname":
|
||||
case "change_thread_icon":
|
||||
break;
|
||||
case "group_poll":
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatDeltaEvent(v.delta);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.delta,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return globalCallback(null, fmtMsg);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
case "ThreadName":
|
||||
case "ParticipantsAddedToGroupThread":
|
||||
case "ParticipantLeftGroupThread":
|
||||
var formattedEvent;
|
||||
try {
|
||||
formattedEvent = utils.formatDeltaEvent(v.delta);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.delta,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return (!ctx.globalOptions.selfListen &&
|
||||
formattedEvent.author.toString() === ctx.userID) ||
|
||||
!ctx.loggedIn ?
|
||||
undefined :
|
||||
globalCallback(null, formattedEvent);
|
||||
}
|
||||
|
||||
break;
|
||||
case "messaging":
|
||||
if (handleMessagingEvents(v)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "pages_messaging":
|
||||
if (
|
||||
!ctx.globalOptions.pageID ||
|
||||
v.event !== "deliver" ||
|
||||
(!ctx.globalOptions.selfListen &&
|
||||
(v.message.sender_fbid.toString() === ctx.userID ||
|
||||
v.message.sender_fbid.toString() ===
|
||||
ctx.globalOptions.pageID)) ||
|
||||
v.realtime_viewer_fbid.toString() !==
|
||||
ctx.globalOptions.pageID
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
atLeastOne = true;
|
||||
if (ctx.loggedIn) {
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatMessage(v);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return globalCallback(null, fmtMsg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (atLeastOne) {
|
||||
// Send deliveryReceipt notification to the server
|
||||
var formDeliveryReceipt = {};
|
||||
|
||||
resData.ms
|
||||
.filter(function(v) {
|
||||
return (
|
||||
v.message &&
|
||||
v.message.mid &&
|
||||
v.message.sender_fbid.toString() !== ctx.userID
|
||||
);
|
||||
})
|
||||
.forEach(function(val, i) {
|
||||
formDeliveryReceipt["[" + i + "]"] = val.message.mid;
|
||||
});
|
||||
|
||||
// If there's at least one, we do the post request
|
||||
if (formDeliveryReceipt["[0]"]) {
|
||||
defaultFuncs.post(
|
||||
"https://www.facebook.com/ajax/mercury/delivery_receipts.php",
|
||||
ctx.jar,
|
||||
formDeliveryReceipt
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resData.seq) {
|
||||
form.seq = resData.seq;
|
||||
}
|
||||
if (resData.tr) {
|
||||
form.traceid = resData.tr;
|
||||
}
|
||||
if (currentlyRunning) {
|
||||
currentlyRunning = setTimeout(listen, Math.random() * 200 + 50);
|
||||
}
|
||||
return;
|
||||
})
|
||||
.catch(function(err) {
|
||||
if (err.code === "ETIMEDOUT") {
|
||||
log.info("listen", "Suppressed timeout error.");
|
||||
} else if (err.code === "EAI_AGAIN") {
|
||||
serverNumber = (~~(Math.random() * 6)).toString();
|
||||
} else {
|
||||
log.error("listen", err);
|
||||
globalCallback(err);
|
||||
}
|
||||
if (currentlyRunning) {
|
||||
currentlyRunning = setTimeout(listen, Math.random() * 200 + 50);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return function(callback) {
|
||||
globalCallback = callback;
|
||||
|
||||
if (!currentlyRunning) {
|
||||
currentlyRunning = setTimeout(listen, Math.random() * 200 + 50, callback);
|
||||
}
|
||||
|
||||
return stopListening;
|
||||
};
|
||||
};
|
||||
+551
@@ -0,0 +1,551 @@
|
||||
/* eslint-disable no-redeclare */
|
||||
"use strict";
|
||||
var fbconnect = require("./mqtt/fbconnect");
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
var identity = function () {};
|
||||
var mqttClient = undefined;
|
||||
|
||||
var lastSeqId = 0;
|
||||
var syncToken;
|
||||
|
||||
//Don't really know what this does but I think it's for the active state
|
||||
var chatOn = true;
|
||||
var foreground = false;
|
||||
|
||||
var topics = [
|
||||
"/t_ms",
|
||||
"/thread_typing",
|
||||
"/orca_typing_notifications",
|
||||
"/orca_presence",
|
||||
"/legacy_web",
|
||||
"/br_sr",
|
||||
"/sr_res",
|
||||
"/webrtc",
|
||||
"/onevc",
|
||||
"/notify_disconnect",
|
||||
"/inbox",
|
||||
"/mercury",
|
||||
"/messaging_events",
|
||||
"/orca_message_notifications",
|
||||
"/pp",
|
||||
"/webrtc_response",
|
||||
]
|
||||
|
||||
function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
||||
var sessionID = Math.floor(Math.random() * 9007199254740991) + 1;
|
||||
var username = {
|
||||
u: ctx.userID,
|
||||
s: sessionID,
|
||||
chat_on: chatOn,
|
||||
fg: foreground,
|
||||
d: utils.getGUID(),
|
||||
ct: "websocket",
|
||||
//App id from facebook
|
||||
aid: "219994525426954",
|
||||
mqtt_sid: "",
|
||||
cp: 3,
|
||||
ecp: 10,
|
||||
st: topics,
|
||||
pm: [],
|
||||
dc: "",
|
||||
no_auto_fg: true,
|
||||
gas: null
|
||||
};
|
||||
var cookies = ctx.jar.getCookies("https://www.facebook.com").join("; ");
|
||||
|
||||
//Region could be changed for better ping. (Region atn: Southeast Asia, region ash: West US, prob) (Don't really know if we need it).
|
||||
//// var host = 'wss://edge-chat.facebook.com/chat?region=atn&sid=' + sessionID;
|
||||
var host = 'wss://edge-chat.facebook.com/chat?sid=' + sessionID;
|
||||
|
||||
var options = {
|
||||
clientId: "mqttwsclient",
|
||||
protocolId: 'MQIsdp',
|
||||
protocolVersion: 3,
|
||||
username: JSON.stringify(username),
|
||||
clean: true,
|
||||
wsOptions: {
|
||||
'headers': {
|
||||
'Cookie': cookies,
|
||||
'Origin': 'https://www.facebook.com',
|
||||
'User-Agent': ctx.globalOptions.userAgent,
|
||||
'Referer': 'https://www.facebook.com',
|
||||
'Host': 'edge-chat.facebook.com'
|
||||
},
|
||||
origin: 'https://www.facebook.com',
|
||||
protocolVersion: 13
|
||||
}
|
||||
};
|
||||
|
||||
mqttClient = fbconnect.connect(host, options);
|
||||
|
||||
mqttClient.on('error', function(err) {
|
||||
log.error(err);
|
||||
mqttClient.end();
|
||||
globalCallback("Connection refused: Server unavailable", null);
|
||||
});
|
||||
|
||||
mqttClient.on('connect', function() {
|
||||
var topic;
|
||||
var queue = {
|
||||
sync_api_version: 10,
|
||||
max_deltas_able_to_process: 1000,
|
||||
delta_batch_size: 500,
|
||||
encoding: "JSON",
|
||||
entity_fbid: ctx.userID,
|
||||
};
|
||||
|
||||
if(ctx.globalOptions.pageID) {
|
||||
queue.entity_fbid = ctx.globalOptions.pageID;
|
||||
}
|
||||
|
||||
if(syncToken) {
|
||||
topic = "/messenger_sync_get_diffs";
|
||||
queue.last_seq_id = lastSeqId;
|
||||
queue.sync_token = syncToken;
|
||||
} else {
|
||||
topic = "/messenger_sync_create_queue";
|
||||
queue.initial_titan_sequence_id = lastSeqId;
|
||||
queue.device_params = null;
|
||||
}
|
||||
|
||||
mqttClient.publish(topic, JSON.stringify(queue), {qos: 1, retain: false})
|
||||
});
|
||||
|
||||
mqttClient.on('message', function(topic, message, packet) {
|
||||
var jsonMessage = JSON.parse(message);
|
||||
if(topic === "/t_ms") {
|
||||
if(jsonMessage.firstDeltaSeqId && jsonMessage.syncToken) {
|
||||
lastSeqId = jsonMessage.firstDeltaSeqId;
|
||||
syncToken = jsonMessage.syncToken;
|
||||
}
|
||||
|
||||
if(jsonMessage.lastIssuedSeqId) {
|
||||
lastSeqId = parseInt(jsonMessage.lastIssuedSeqId);
|
||||
}
|
||||
|
||||
if(jsonMessage.queueEntityId && ctx.globalOptions.pageID &&
|
||||
ctx.globalOptions.pageID != jsonMessage.queueEntityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
//If it contains more than 1 delta
|
||||
for (var i in jsonMessage.deltas) {
|
||||
var delta = jsonMessage.deltas[i];
|
||||
parseDelta(defaultFuncs, api, ctx, globalCallback, { "delta": delta });
|
||||
}
|
||||
} else if (topic === "/thread_typing" || topic === "/orca_typing_notifications") {
|
||||
var typ = {
|
||||
type: "typ",
|
||||
isTyping: !!jsonMessage.state,
|
||||
from: jsonMessage.sender_fbid.toString(),
|
||||
threadID: utils.formatID((jsonMessage.thread || jsonMessage.sender_fbid).toString())
|
||||
};
|
||||
(function () { globalCallback(null, typ); })();
|
||||
} else if (topic === "/orca_presence") {
|
||||
if (!ctx.globalOptions.updatePresence) {
|
||||
for (var i in jsonMessage.list) {
|
||||
var data = jsonMessage.list[i];
|
||||
var userID = data["u"];
|
||||
|
||||
var presence = {
|
||||
type: "presence",
|
||||
userID: userID.toString(),
|
||||
//Convert to ms
|
||||
timestamp: data["l"] * 1000,
|
||||
statuses: data["p"]
|
||||
};
|
||||
(function () { globalCallback(null, presence); })();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
mqttClient.on('close', function() {
|
||||
// client.end();
|
||||
});
|
||||
}
|
||||
|
||||
function parseDelta(defaultFuncs, api, ctx, globalCallback, v) {
|
||||
if(v.delta.class == "NewMessage") {
|
||||
(function resolveAttachmentUrl(i) {
|
||||
if (i == v.delta.attachments.length) {
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatDeltaMessage(v);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
if (fmtMsg) {
|
||||
if (ctx.globalOptions.autoMarkDelivery) {
|
||||
markDelivery(ctx, api, fmtMsg.threadID, fmtMsg.messageID);
|
||||
}
|
||||
}
|
||||
return !ctx.globalOptions.selfListen &&
|
||||
fmtMsg.senderID === ctx.userID ?
|
||||
undefined :
|
||||
(function () { globalCallback(null, fmtMsg); })();
|
||||
} else {
|
||||
if (
|
||||
v.delta.attachments[i].mercury.attach_type == "photo"
|
||||
) {
|
||||
api.resolvePhotoUrl(
|
||||
v.delta.attachments[i].fbid,
|
||||
(err, url) => {
|
||||
if (!err)
|
||||
v.delta.attachments[
|
||||
i
|
||||
].mercury.metadata.url = url;
|
||||
return resolveAttachmentUrl(i + 1);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return resolveAttachmentUrl(i + 1);
|
||||
}
|
||||
}
|
||||
})(0);
|
||||
}
|
||||
|
||||
if (v.delta.class == "ClientPayload") {
|
||||
var clientPayload = utils.decodeClientPayload(
|
||||
v.delta.payload
|
||||
);
|
||||
if (clientPayload && clientPayload.deltas) {
|
||||
for (var i in clientPayload.deltas) {
|
||||
var delta = clientPayload.deltas[i];
|
||||
if (delta.deltaMessageReaction && !!ctx.globalOptions.listenEvents) {
|
||||
(function () { globalCallback(null, {
|
||||
type: "message_reaction",
|
||||
threadID: (delta.deltaMessageReaction.threadKey
|
||||
.threadFbId ?
|
||||
delta.deltaMessageReaction.threadKey.threadFbId : delta.deltaMessageReaction.threadKey
|
||||
.otherUserFbId).toString(),
|
||||
messageID: delta.deltaMessageReaction.messageId,
|
||||
reaction: delta.deltaMessageReaction.reaction,
|
||||
senderID: delta.deltaMessageReaction.senderId.toString(),
|
||||
userID: delta.deltaMessageReaction.userId.toString()
|
||||
}); })();
|
||||
} else if (delta.deltaRecallMessageData && !!ctx.globalOptions.listenEvents) {
|
||||
(function () { globalCallback(null, {
|
||||
type: "message_unsend",
|
||||
threadID: (delta.deltaRecallMessageData.threadKey.threadFbId ?
|
||||
delta.deltaRecallMessageData.threadKey.threadFbId : delta.deltaRecallMessageData.threadKey
|
||||
.otherUserFbId).toString(),
|
||||
messageID: delta.deltaRecallMessageData.messageID,
|
||||
senderID: delta.deltaRecallMessageData.senderID.toString(),
|
||||
deletionTimestamp: delta.deltaRecallMessageData.deletionTimestamp,
|
||||
timestamp: delta.deltaRecallMessageData.timestamp
|
||||
}); })();
|
||||
} else if (delta.deltaMessageReply) {
|
||||
//Mention block - #1
|
||||
var mdata =
|
||||
delta.deltaMessageReply.message === undefined ? [] :
|
||||
delta.deltaMessageReply.message.data === undefined ? [] :
|
||||
delta.deltaMessageReply.message.data.prng === undefined ? [] :
|
||||
JSON.parse(delta.deltaMessageReply.message.data.prng);
|
||||
var m_id = mdata.map(u => u.i);
|
||||
var m_offset = mdata.map(u => u.o);
|
||||
var m_length = mdata.map(u => u.l);
|
||||
|
||||
var mentions = {};
|
||||
|
||||
for (var i = 0; i < m_id.length; i++) {
|
||||
mentions[m_id[i]] = (delta.deltaMessageReply.message.body || "").substring(
|
||||
m_offset[i],
|
||||
m_offset[i] + m_length[i]
|
||||
);
|
||||
}
|
||||
//Mention block - 1#
|
||||
var callbackToReturn = {
|
||||
type: "message_reply",
|
||||
threadID: (delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId ?
|
||||
delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId : delta.deltaMessageReply.message.messageMetadata.threadKey
|
||||
.otherUserFbId).toString(),
|
||||
messageID: delta.deltaMessageReply.message.messageMetadata.messageId,
|
||||
senderID: delta.deltaMessageReply.message.messageMetadata.actorFbId.toString(),
|
||||
attachments: delta.deltaMessageReply.message.attachments.map(function (att) {
|
||||
var mercury = JSON.parse(att.mercuryJSON);
|
||||
Object.assign(att, mercury);
|
||||
return att;
|
||||
}).map(att => {
|
||||
var x;
|
||||
try {
|
||||
x = utils._formatAttachment(att);
|
||||
} catch (ex) {
|
||||
x = att;
|
||||
x.error = ex;
|
||||
x.type = "unknown";
|
||||
}
|
||||
return x;
|
||||
}),
|
||||
body: delta.deltaMessageReply.message.body || "",
|
||||
isGroup: !!delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId,
|
||||
mentions: mentions,
|
||||
timestamp: delta.deltaMessageReply.message.messageMetadata.timestamp,
|
||||
};
|
||||
|
||||
if (delta.deltaMessageReply.repliedToMessage) {
|
||||
//Mention block - #2
|
||||
mdata =
|
||||
delta.deltaMessageReply.repliedToMessage === undefined ? [] :
|
||||
delta.deltaMessageReply.repliedToMessage.data === undefined ? [] :
|
||||
delta.deltaMessageReply.repliedToMessage.data.prng === undefined ? [] :
|
||||
JSON.parse(delta.deltaMessageReply.repliedToMessage.data.prng);
|
||||
m_id = mdata.map(u => u.i);
|
||||
m_offset = mdata.map(u => u.o);
|
||||
m_length = mdata.map(u => u.l);
|
||||
|
||||
var rmentions = {};
|
||||
|
||||
for (var i = 0; i < m_id.length; i++) {
|
||||
rmentions[m_id[i]] = (delta.deltaMessageReply.repliedToMessage.body || "").substring(
|
||||
m_offset[i],
|
||||
m_offset[i] + m_length[i]
|
||||
);
|
||||
}
|
||||
//Mention block - 2#
|
||||
callbackToReturn.messageReply = {
|
||||
threadID: (delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId ?
|
||||
delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId : delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey
|
||||
.otherUserFbId).toString(),
|
||||
messageID: delta.deltaMessageReply.repliedToMessage.messageMetadata.messageId,
|
||||
senderID: delta.deltaMessageReply.repliedToMessage.messageMetadata.actorFbId.toString(),
|
||||
attachments: delta.deltaMessageReply.repliedToMessage.attachments.map(function (att) {
|
||||
var mercury = JSON.parse(att.mercuryJSON);
|
||||
Object.assign(att, mercury);
|
||||
return att;
|
||||
}).map(att => {
|
||||
var x;
|
||||
try {
|
||||
x = utils._formatAttachment(att);
|
||||
} catch (ex) {
|
||||
x = att;
|
||||
x.error = ex;
|
||||
x.type = "unknown";
|
||||
}
|
||||
return x;
|
||||
}),
|
||||
body: delta.deltaMessageReply.repliedToMessage.body || "",
|
||||
isGroup: !!delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId,
|
||||
mentions: rmentions,
|
||||
timestamp: delta.deltaMessageReply.repliedToMessage.messageMetadata.timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
if (ctx.globalOptions.autoMarkDelivery) {
|
||||
markDelivery(ctx, api, callbackToReturn.threadID, callbackToReturn.messageID);
|
||||
}
|
||||
|
||||
return !ctx.globalOptions.selfListen &&
|
||||
callbackToReturn.senderID === ctx.userID ?
|
||||
undefined :
|
||||
(function () { globalCallback(null, callbackToReturn); })();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (v.delta.class !== "NewMessage" &&
|
||||
!ctx.globalOptions.listenEvents
|
||||
)
|
||||
return;
|
||||
|
||||
switch (v.delta.class) {
|
||||
case "ReadReceipt":
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatDeltaReadReceipt(v.delta);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.delta,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return (function () { globalCallback(null, fmtMsg); })();
|
||||
case "AdminTextMessage":
|
||||
switch (v.delta.type) {
|
||||
case "change_thread_theme":
|
||||
case "change_thread_nickname":
|
||||
case "change_thread_icon":
|
||||
break;
|
||||
case "group_poll":
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatDeltaEvent(v.delta);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.delta,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return (function () { globalCallback(null, fmtMsg); })();
|
||||
default:
|
||||
return;
|
||||
}
|
||||
break;
|
||||
//For group images
|
||||
case "ForcedFetch":
|
||||
if (!v.delta.threadKey) return;
|
||||
var mid = v.delta.messageId;
|
||||
var tid = v.delta.threadKey.threadFbId;
|
||||
if (mid && tid) {
|
||||
const form = {
|
||||
"av": ctx.globalOptions.pageID,
|
||||
"queries": JSON.stringify({
|
||||
"o0": {
|
||||
//This doc_id is valid as of ? (prob January 18, 2020)
|
||||
"doc_id": "1768656253222505",
|
||||
"query_params": {
|
||||
"thread_and_message_id": {
|
||||
"thread_id": tid.toString(),
|
||||
"message_id": mid.toString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then((resData) => {
|
||||
if (resData[resData.length - 1].error_results > 0) {
|
||||
throw resData[0].o0.errors;
|
||||
}
|
||||
|
||||
if (resData[resData.length - 1].successful_results === 0) {
|
||||
throw { error: "forcedFetch: there was no successful_results", res: resData };
|
||||
}
|
||||
|
||||
var fetchData = resData[0].o0.data.message;
|
||||
|
||||
if (fetchData && fetchData.__typename === "ThreadImageMessage") {
|
||||
(!ctx.globalOptions.selfListen &&
|
||||
fetchData.message_sender.id.toString() === ctx.userID) ||
|
||||
!ctx.loggedIn ?
|
||||
undefined :
|
||||
(function () { globalCallback(null, {
|
||||
type: "change_thread_image",
|
||||
threadID: utils.formatID(tid.toString()),
|
||||
snippet: fetchData.snippet,
|
||||
timestamp: fetchData.timestamp_precise,
|
||||
author: fetchData.message_sender.id,
|
||||
image: {
|
||||
attachmentID: fetchData.image_with_metadata.legacy_attachment_id,
|
||||
width: fetchData.image_with_metadata.original_dimensions.x,
|
||||
height: fetchData.image_with_metadata.original_dimensions.y,
|
||||
url: fetchData.image_with_metadata.preview.uri
|
||||
}
|
||||
}); })();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("forcedFetch", err);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "ThreadName":
|
||||
case "ParticipantsAddedToGroupThread":
|
||||
case "ParticipantLeftGroupThread":
|
||||
var formattedEvent;
|
||||
try {
|
||||
formattedEvent = utils.formatDeltaEvent(v.delta);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.delta,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return (!ctx.globalOptions.selfListen &&
|
||||
formattedEvent.author.toString() === ctx.userID) ||
|
||||
!ctx.loggedIn ?
|
||||
undefined :
|
||||
(function () { globalCallback(null, formattedEvent); })();
|
||||
}
|
||||
}
|
||||
|
||||
function markDelivery(ctx, api, threadID, messageID) {
|
||||
if (threadID && messageID) {
|
||||
api.markAsDelivered(threadID, messageID, (err) => {
|
||||
if (err) {
|
||||
log.error(err);
|
||||
} else {
|
||||
if (ctx.globalOptions.autoMarkRead) {
|
||||
api.markAsRead(threadID, (err) => {
|
||||
if (err) {
|
||||
log.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function (defaultFuncs, api, ctx) {
|
||||
var globalCallback = identity;
|
||||
return function (callback) {
|
||||
globalCallback = callback;
|
||||
|
||||
//Same request as getThreadList
|
||||
const form = {
|
||||
"av": ctx.globalOptions.pageID,
|
||||
"queries": JSON.stringify({
|
||||
"o0": {
|
||||
"doc_id": "1349387578499440",
|
||||
"query_params": {
|
||||
"limit": 1,
|
||||
"before": null,
|
||||
"tags": ["INBOX"],
|
||||
"includeDeliveryReceipts": false,
|
||||
"includeSeqID": true
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then((resData) => {
|
||||
if (resData && resData.length > 0 && resData[resData.length - 1].error_results > 0) {
|
||||
throw resData[0].o0.errors;
|
||||
}
|
||||
|
||||
if (resData[resData.length - 1].successful_results === 0) {
|
||||
throw { error: "getSeqId: there was no successful_results", res: resData };
|
||||
}
|
||||
|
||||
if (resData[0].o0.data.viewer.message_threads.sync_sequence_id) {
|
||||
lastSeqId = resData[0].o0.data.viewer.message_threads.sync_sequence_id;
|
||||
listenMqtt(defaultFuncs, api, ctx, globalCallback);
|
||||
}
|
||||
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("getSeqId", err);
|
||||
return callback(err);
|
||||
});
|
||||
|
||||
var stopListening = function () {
|
||||
globalCallback = identity;
|
||||
mqttClient.end();
|
||||
};
|
||||
|
||||
return stopListening;
|
||||
};
|
||||
};
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function logout(callback) {
|
||||
callback = callback || function() {};
|
||||
|
||||
var form = {
|
||||
pmid: "0"
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/bluebar/modern_settings_menu/?help_type=364455653583099&show_contextual_help=1",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
var elem = resData.jsmods.instances[0][2][0].filter(function(v) {
|
||||
return v.value === "logout";
|
||||
})[0];
|
||||
|
||||
var html = resData.jsmods.markup.filter(function(v) {
|
||||
return v[0] === elem.markup.__m;
|
||||
})[0][1].__html;
|
||||
|
||||
var form = {
|
||||
fb_dtsg: utils.getFrom(html, '"fb_dtsg" value="', '"'),
|
||||
ref: utils.getFrom(html, '"ref" value="', '"'),
|
||||
h: utils.getFrom(html, '"h" value="', '"')
|
||||
};
|
||||
|
||||
return defaultFuncs
|
||||
.post("https://www.facebook.com/logout.php", ctx.jar, form)
|
||||
.then(utils.saveCookies(ctx.jar));
|
||||
})
|
||||
.then(function(res) {
|
||||
if (!res.headers) {
|
||||
throw { error: "An error occurred when logging out." };
|
||||
}
|
||||
|
||||
return defaultFuncs
|
||||
.get(res.headers.location, ctx.jar)
|
||||
.then(utils.saveCookies(ctx.jar));
|
||||
})
|
||||
.then(function() {
|
||||
ctx.loggedIn = false;
|
||||
log.info("logout", "Logged out successfully.");
|
||||
callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("logout", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function (defaultFuncs, api, ctx) {
|
||||
return function markAsDelivered(threadID, messageID, callback) {
|
||||
if (!callback) {
|
||||
callback = function () { };
|
||||
}
|
||||
|
||||
if (!threadID || !messageID) {
|
||||
return callback("Error: messageID or threadID is not defined");
|
||||
}
|
||||
|
||||
var form = {};
|
||||
|
||||
form["message_ids[0]"] = messageID;
|
||||
form["thread_ids[" + threadID + "][0]"] = messageID;
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/delivery_receipts.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function (resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function (err) {
|
||||
log.error("markAsDelivered", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function markAsRead(threadID, read, callback) {
|
||||
if (utils.getType(read) === 'Function' || utils.getType(read) === 'AsyncFunction') {
|
||||
callback = read;
|
||||
read = true;
|
||||
}
|
||||
if (read == undefined) {
|
||||
read = true;
|
||||
}
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {};
|
||||
|
||||
if (typeof ctx.globalOptions.pageID !== 'undefined') {
|
||||
form["source"] = "PagesManagerMessagesInterface";
|
||||
form["request_user_id"] = ctx.globalOptions.pageID;
|
||||
}
|
||||
|
||||
form["ids[" + threadID + "]"] = read;
|
||||
form["watermarkTimestamp"] = new Date().getTime();
|
||||
form["shouldSendReadReceipt"] = true;
|
||||
form["commerce_last_message_type"] = "non_ad";
|
||||
form["titanOriginatedThreadId"] = utils.generateThreadingID(ctx.clientID);
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/change_read_status.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("markAsRead", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function markAsReadAll(callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
folder: 'inbox'
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/mark_folder_as_read.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("markAsReadAll", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
//From MQTT.js (https://github.com/mqttjs/MQTT.js)
|
||||
'use strict';
|
||||
|
||||
var mqtt = require('mqtt');
|
||||
var url = require('url');
|
||||
var xtend = require('xtend');
|
||||
var protocols = {};
|
||||
protocols.ws = require('./fbws');
|
||||
protocols.wss = require('./fbws');
|
||||
|
||||
/**
|
||||
* Parse the auth attribute and merge username and password in the options object.
|
||||
*
|
||||
* @param {Object} [opts] option object
|
||||
*/
|
||||
function parseAuthOptions(opts) {
|
||||
var matches;
|
||||
if(opts.auth) {
|
||||
matches = opts.auth.match(/^(.+):(.+)$/);
|
||||
if(matches) {
|
||||
opts.username = matches[1];
|
||||
opts.password = matches[2];
|
||||
} else {
|
||||
opts.username = opts.auth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* connect - connect to an MQTT broker.
|
||||
*
|
||||
* @param {String} [brokerUrl] - url of the broker, optional
|
||||
* @param {Object} opts - see MqttClient#constructor
|
||||
*/
|
||||
function connect(brokerUrl, opts) {
|
||||
if((typeof brokerUrl === 'object') && !opts) {
|
||||
opts = brokerUrl;
|
||||
brokerUrl = null;
|
||||
}
|
||||
|
||||
opts = opts || {};
|
||||
|
||||
if(brokerUrl) {
|
||||
var parsed = url.parse(brokerUrl, true);
|
||||
if(parsed.port != null) {
|
||||
parsed.port = Number(parsed.port);
|
||||
}
|
||||
|
||||
opts = xtend(parsed, opts);
|
||||
|
||||
if(opts.protocol === null) {
|
||||
throw new Error('Missing protocol');
|
||||
}
|
||||
opts.protocol = opts.protocol.replace(/:$/, '');
|
||||
}
|
||||
|
||||
// merge in the auth options if supplied
|
||||
parseAuthOptions(opts);
|
||||
|
||||
// support clientId passed in the query string of the url
|
||||
if(opts.query && typeof opts.query.clientId === 'string') {
|
||||
opts.clientId = opts.query.clientId;
|
||||
}
|
||||
|
||||
if(opts.cert && opts.key) {
|
||||
if(opts.protocol) {
|
||||
if(['mqtts', 'wss'].indexOf(opts.protocol) === -1) {
|
||||
switch(opts.protocol) {
|
||||
case 'mqtt':
|
||||
opts.protocol = 'mqtts';
|
||||
break;
|
||||
case 'ws':
|
||||
opts.protocol = 'wss';
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown protocol for secure connection: "' + opts.protocol + '"!');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// don't know what protocol he want to use, mqtts or wss
|
||||
throw new Error('Missing secure protocol key');
|
||||
}
|
||||
}
|
||||
|
||||
if(!protocols[opts.protocol]) {
|
||||
var isSecure = ['mqtts', 'wss'].indexOf(opts.protocol) !== -1;
|
||||
opts.protocol = [
|
||||
'mqtt',
|
||||
'mqtts',
|
||||
'ws',
|
||||
'wss'
|
||||
].filter(function(key, index) {
|
||||
if(isSecure && index % 2 === 0) {
|
||||
// Skip insecure protocols when requesting a secure one.
|
||||
return false;
|
||||
}
|
||||
return (typeof protocols[key] === 'function');
|
||||
})[0];
|
||||
}
|
||||
|
||||
if(opts.clean === false && !opts.clientId) {
|
||||
throw new Error('Missing clientId for unclean clients');
|
||||
}
|
||||
|
||||
if(opts.protocol) {
|
||||
opts.defaultProtocol = opts.protocol;
|
||||
}
|
||||
|
||||
function wrapper(client) {
|
||||
if(opts.servers) {
|
||||
if(!client._reconnectCount || client._reconnectCount === opts.servers.length) {
|
||||
client._reconnectCount = 0;
|
||||
}
|
||||
|
||||
opts.host = opts.servers[client._reconnectCount].host;
|
||||
opts.port = opts.servers[client._reconnectCount].port;
|
||||
opts.protocol = (!opts.servers[client._reconnectCount].protocol ? opts.defaultProtocol : opts.servers[client._reconnectCount].protocol);
|
||||
opts.hostname = opts.host;
|
||||
|
||||
client._reconnectCount++;
|
||||
}
|
||||
|
||||
return protocols[opts.protocol](client, opts);
|
||||
}
|
||||
|
||||
return new mqtt.Client(wrapper, opts);
|
||||
}
|
||||
|
||||
module.exports = connect;
|
||||
module.exports.connect = connect;
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
//From MQTT.js (https://github.com/mqttjs/MQTT.js)
|
||||
'use strict';
|
||||
|
||||
var websocket = require('websocket-stream');
|
||||
var urlModule = require('url');
|
||||
var WSS_OPTIONS = [
|
||||
'rejectUnauthorized',
|
||||
'ca',
|
||||
'cert',
|
||||
'key',
|
||||
'pfx',
|
||||
'passphrase'
|
||||
];
|
||||
function buildUrl(opts, client) {
|
||||
var url = opts.protocol + '://' + opts.hostname + ':' + opts.port + opts.path;
|
||||
if (typeof (opts.transformWsUrl) === 'function') {
|
||||
url = opts.transformWsUrl(url, opts, client);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function setDefaultOpts(opts) {
|
||||
if (!opts.hostname) {
|
||||
opts.hostname = 'localhost';
|
||||
}
|
||||
if (!opts.port) {
|
||||
if (opts.protocol === 'wss') {
|
||||
opts.port = 443;
|
||||
} else {
|
||||
opts.port = 80;
|
||||
}
|
||||
}
|
||||
if (!opts.path) {
|
||||
opts.path = '/';
|
||||
}
|
||||
|
||||
if (!opts.wsOptions) {
|
||||
opts.wsOptions = {};
|
||||
}
|
||||
if (opts.protocol === 'wss') {
|
||||
// Add cert/key/ca etc options
|
||||
WSS_OPTIONS.forEach(function (prop) {
|
||||
if (opts.hasOwnProperty(prop) && !opts.wsOptions.hasOwnProperty(prop)) {
|
||||
opts.wsOptions[prop] = opts[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createWebSocket(client, opts) {
|
||||
var websocketSubProtocol =
|
||||
(opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3)
|
||||
? 'mqttv3.1'
|
||||
: 'mqtt';
|
||||
|
||||
setDefaultOpts(opts);
|
||||
var url = buildUrl(opts, client);
|
||||
return websocket(url, undefined, opts.wsOptions);
|
||||
}
|
||||
|
||||
function buildBuilder(client, opts) {
|
||||
return createWebSocket(client, opts);
|
||||
}
|
||||
|
||||
module.exports = buildBuilder;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
// muteSecond: -1=permanent mute, 0=unmute, 60=one minute, 3600=one hour, etc.
|
||||
return function muteThread(threadID, muteSeconds, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
thread_fbid: threadID,
|
||||
mute_settings: muteSeconds
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/change_mute_thread.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("muteThread", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function removeUserFromGroup(userID, threadID, callback) {
|
||||
if (
|
||||
!callback &&
|
||||
(utils.getType(threadID) === "Function" ||
|
||||
utils.getType(threadID) === "AsyncFunction")
|
||||
) {
|
||||
throw { error: "please pass a threadID as a second argument." };
|
||||
}
|
||||
if (
|
||||
utils.getType(threadID) !== "Number" &&
|
||||
utils.getType(threadID) !== "String"
|
||||
) {
|
||||
throw {
|
||||
error:
|
||||
"threadID should be of type Number or String and not " +
|
||||
utils.getType(threadID) +
|
||||
"."
|
||||
};
|
||||
}
|
||||
if (
|
||||
utils.getType(userID) !== "Number" &&
|
||||
utils.getType(userID) !== "String"
|
||||
) {
|
||||
throw {
|
||||
error:
|
||||
"userID should be of type Number or String and not " +
|
||||
utils.getType(userID) +
|
||||
"."
|
||||
};
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
uid: userID,
|
||||
tid: threadID
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/chat/remove_participants", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (!resData) {
|
||||
throw { error: "Remove from group failed." };
|
||||
}
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("removeUserFromGroup", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function resolvePhotoUrl(photoID, callback) {
|
||||
if (!callback) {
|
||||
throw { error: "resolvePhotoUrl: need callback" };
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.get("https://www.facebook.com/mercury/attachments/photo", ctx.jar, {
|
||||
photo_id: photoID
|
||||
})
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(resData => {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
var photoUrl = resData.jsmods.require[0][3][0];
|
||||
|
||||
return callback(null, photoUrl);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error("resolvePhotoUrl", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function searchForThread(name, callback) {
|
||||
if (!callback) {
|
||||
throw { error: "searchForThread: need callback" };
|
||||
}
|
||||
|
||||
var tmpForm = {
|
||||
client: "web_messenger",
|
||||
query: name,
|
||||
offset: 0,
|
||||
limit: 21,
|
||||
index: "fbid"
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/search_threads.php",
|
||||
ctx.jar,
|
||||
tmpForm
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
if (!resData.payload.mercury_payload.threads) {
|
||||
return callback({ error: "Could not find thread `" + name + "`." });
|
||||
}
|
||||
return callback(
|
||||
null,
|
||||
resData.payload.mercury_payload.threads.map(utils.formatThread)
|
||||
);
|
||||
});
|
||||
};
|
||||
};
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
"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
|
||||
};
|
||||
|
||||
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(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) {
|
||||
// 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
|
||||
);
|
||||
}
|
||||
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);
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
function send(form, threadID, messageAndOTID, callback) {
|
||||
// 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 {
|
||||
api.getUserInfo(threadID, function(err, res) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
sendContent(
|
||||
form,
|
||||
threadID,
|
||||
Object.keys(res).length > 0,
|
||||
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 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]; // 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;
|
||||
form["profile_xmd[" + i + "][offset]"] = offset;
|
||||
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) {
|
||||
if (
|
||||
!callback &&
|
||||
(utils.getType(threadID) === "Function" ||
|
||||
utils.getType(threadID) === "AsyncFunction")
|
||||
) {
|
||||
return callback({ error: "Pass a threadID as a second argument." });
|
||||
}
|
||||
if (
|
||||
!replyToMessage &&
|
||||
utils.getType(callback) === "String"
|
||||
) {
|
||||
replyToMessage = callback;
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
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)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
function makeTypingIndicator(typ, threadID, callback) {
|
||||
var form = {
|
||||
typ: +typ,
|
||||
to: "",
|
||||
source: "mercury-chat",
|
||||
thread: threadID
|
||||
};
|
||||
|
||||
// Check if thread is a single person chat or a group chat
|
||||
// More info on this is in api.sendMessage
|
||||
api.getUserInfo(threadID, function(err, res) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// If id is single person chat
|
||||
if (Object.keys(res).length > 0) {
|
||||
form.to = threadID;
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/ajax/messaging/typ.php", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("sendTypingIndicator", err);
|
||||
return callback(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return function sendTypingIndicator(threadID, callback) {
|
||||
if (
|
||||
utils.getType(callback) !== "Function" &&
|
||||
utils.getType(callback) !== "AsyncFunction"
|
||||
) {
|
||||
if (callback) {
|
||||
log.warn(
|
||||
"sendTypingIndicator",
|
||||
"callback is not a function - ignoring."
|
||||
);
|
||||
}
|
||||
callback = () => {};
|
||||
}
|
||||
|
||||
makeTypingIndicator(true, threadID, callback);
|
||||
|
||||
return function end(cb) {
|
||||
if (
|
||||
utils.getType(cb) !== "Function" &&
|
||||
utils.getType(cb) !== "AsyncFunction"
|
||||
) {
|
||||
if (cb) {
|
||||
log.warn(
|
||||
"sendTypingIndicator",
|
||||
"callback is not a function - ignoring."
|
||||
);
|
||||
}
|
||||
cb = () => {};
|
||||
}
|
||||
|
||||
makeTypingIndicator(false, threadID, cb);
|
||||
};
|
||||
};
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function setMessageReaction(reaction, messageID, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
switch (reaction) {
|
||||
case "\uD83D\uDE0D": //:heart_eyes:
|
||||
case "\uD83D\uDE06": //:laughing:
|
||||
case "\uD83D\uDE2E": //:open_mouth:
|
||||
case "\uD83D\uDE22": //:cry:
|
||||
case "\uD83D\uDE20": //:angry:
|
||||
case "\uD83D\uDC4D": //:thumbsup:
|
||||
case "\uD83D\uDC4E": //:thumbsdown:
|
||||
case "":
|
||||
//valid
|
||||
break;
|
||||
case ":heart_eyes:":
|
||||
case ":love:":
|
||||
reaction = "\uD83D\uDE0D";
|
||||
break;
|
||||
case ":laughing:":
|
||||
case ":haha:":
|
||||
reaction = "\uD83D\uDE06";
|
||||
break;
|
||||
case ":open_mouth:":
|
||||
case ":wow:":
|
||||
reaction = "\uD83D\uDE2E";
|
||||
break;
|
||||
case ":cry:":
|
||||
case ":sad:":
|
||||
reaction = "\uD83D\uDE22";
|
||||
break;
|
||||
case ":angry:":
|
||||
reaction = "\uD83D\uDE20";
|
||||
break;
|
||||
case ":thumbsup:":
|
||||
case ":like:":
|
||||
reaction = "\uD83D\uDC4D";
|
||||
break;
|
||||
case ":thumbsdown:":
|
||||
case ":dislike:":
|
||||
reaction = "\uD83D\uDC4E";
|
||||
break;
|
||||
default:
|
||||
return callback({ error: "Reaction is not a valid emoji." });
|
||||
break;
|
||||
}
|
||||
|
||||
var variables = {
|
||||
data: {
|
||||
client_mutation_id: ctx.clientMutationId++,
|
||||
actor_id: ctx.userID,
|
||||
action: reaction == "" ? "REMOVE_REACTION" : "ADD_REACTION",
|
||||
message_id: messageID,
|
||||
reaction: reaction
|
||||
}
|
||||
};
|
||||
|
||||
var qs = {
|
||||
doc_id: "1491398900900362",
|
||||
variables: JSON.stringify(variables),
|
||||
dpr: 1
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.postFormData(
|
||||
"https://www.facebook.com/webgraphql/mutation/",
|
||||
ctx.jar,
|
||||
{},
|
||||
qs
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx.jar, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (!resData) {
|
||||
throw { error: "setReaction returned empty object." };
|
||||
}
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
callback(null);
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("setReaction", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function setTitle(newTitle, threadID, callback) {
|
||||
if (
|
||||
!callback &&
|
||||
(utils.getType(threadID) === "Function" ||
|
||||
utils.getType(threadID) === "AsyncFunction")
|
||||
) {
|
||||
throw { error: "please pass a threadID as a second argument." };
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var messageAndOTID = utils.generateOfflineThreadingID();
|
||||
var form = {
|
||||
client: "mercury",
|
||||
action_type: "ma-type:log-message",
|
||||
author: "fbid:" + ctx.userID,
|
||||
thread_id: "",
|
||||
author_email: "",
|
||||
coordinates: "",
|
||||
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_spoof_warning: false,
|
||||
source: "source:chat:web",
|
||||
"source_tags[0]": "source:chat",
|
||||
status: "0",
|
||||
offline_threading_id: messageAndOTID,
|
||||
message_id: messageAndOTID,
|
||||
threading_id: utils.generateThreadingID(ctx.clientID),
|
||||
manual_retry_cnt: "0",
|
||||
thread_fbid: threadID,
|
||||
thread_name: newTitle,
|
||||
thread_id: threadID,
|
||||
log_message_type: "log:thread-name"
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/messaging/set_thread_name/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error && resData.error === 1545012) {
|
||||
throw { error: "Cannot change chat title: Not member of chat." };
|
||||
}
|
||||
|
||||
if (resData.error && resData.error === 1545003) {
|
||||
throw { error: "Cannot set title of single-user chat." };
|
||||
}
|
||||
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("setTitle", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
// Currently the only colors that can be passed to api.changeThreadColor(); may change if Facebook adds more
|
||||
return {
|
||||
MessengerBlue: null,
|
||||
Viking: "#44bec7",
|
||||
GoldenPoppy: "#ffc300",
|
||||
RadicalRed: "#fa3c4c",
|
||||
Shocking: "#d696bb",
|
||||
PictonBlue: "#6699cc",
|
||||
FreeSpeechGreen: "#13cf13",
|
||||
Pumpkin: "#ff7e29",
|
||||
LightCoral: "#e68585",
|
||||
MediumSlateBlue: "#7646ff",
|
||||
DeepSkyBlue: "#20cef5",
|
||||
Fern: "#67b868",
|
||||
Cameo: "#d4a88c",
|
||||
BrilliantRose: "#ff5ca1",
|
||||
BilobaFlower: "#a695c7"
|
||||
};
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function unsendMessage(messageID, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
message_id: messageID
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/messaging/unsend_message/",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("unsendMessage", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user