This commit is contained in:
2020-03-02 14:53:23 +07:00
parent d10a3b793b
commit e81094b0d2
4280 changed files with 484929 additions and 0 deletions
+99
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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(/&amp;/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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
});
};
};
View File
+7
View File
@@ -0,0 +1,7 @@
"use strict";
module.exports = function(defaultFuncs, api, ctx) {
return function getCurrentUserID() {
return ctx.userID;
};
};
+29
View File
@@ -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
View File
@@ -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
View File
@@ -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);
});
};
};
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
});
};
};