@dongdev/fca-unofficial
Version:
A Facebook chat API without XMPP, will not be deprecated after April 30th, 2015.
1,039 lines (1,037 loc) • 37.1 kB
JavaScript
;
const utils = require("../utils");
const log = require("npmlog");
const mqtt = require("mqtt");
const WebSocket = require("ws");
const HttpsProxyAgent = require("https-proxy-agent");
const EventEmitter = require("events");
const Duplexify = require("duplexify");
const { Transform } = require("stream");
var identity = function () {};
var form = {};
var getSeqID = function () {};
const logger = require("../lib/logger.js");
const topics = [
"/ls_req",
"/ls_resp",
"/legacy_web",
"/webrtc",
"/rtc_multi",
"/onevc",
"/br_sr",
"/sr_res",
"/t_ms",
"/thread_typing",
"/orca_typing_notifications",
"/notify_disconnect",
"/orca_presence",
"/inbox",
"/mercury",
"/messaging_events",
"/orca_message_notifications",
"/pp",
"/webrtc_response",
];
let WebSocket_Global;
function buildProxy() {
const Proxy = new Transform({
objectMode: false,
transform(chunk, enc, next) {
if (WebSocket_Global.readyState !== WebSocket.OPEN) {
return next();
}
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf8");
try {
WebSocket_Global.send(data);
next();
} catch (err) {
console.error("WebSocket send error:", err);
next(err);
}
},
flush(done) {
if (WebSocket_Global.readyState === WebSocket.OPEN) {
WebSocket_Global.close();
}
done();
},
writev(chunks, cb) {
try {
for (const { chunk } of chunks) {
this.push(
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf8")
);
}
cb();
} catch (err) {
console.error("Writev error:", err);
cb(err);
}
},
});
return Proxy;
}
function buildStream(options, WebSocket, Proxy) {
const Stream = Duplexify(undefined, undefined, options);
Stream.socket = WebSocket;
let pingInterval;
let reconnectTimeout;
const clearTimers = () => {
clearInterval(pingInterval);
clearTimeout(reconnectTimeout);
};
WebSocket.onclose = () => {
clearTimers();
Stream.end();
Stream.destroy();
};
WebSocket.onerror = (err) => {
clearTimers();
Stream.destroy(err);
};
WebSocket.onmessage = (event) => {
clearTimeout(reconnectTimeout);
const data =
event.data instanceof ArrayBuffer
? Buffer.from(event.data)
: Buffer.from(event.data, "utf8");
Stream.push(data);
};
WebSocket.onopen = () => {
Stream.setReadable(Proxy);
Stream.setWritable(Proxy);
Stream.emit("connect");
pingInterval = setInterval(() => {
if (WebSocket.readyState === WebSocket.OPEN) {
WebSocket.ping();
}
}, 30000);
reconnectTimeout = setTimeout(() => {
if (WebSocket.readyState === WebSocket.OPEN) {
WebSocket.close();
Stream.end();
Stream.destroy();
}
}, 60000);
};
WebSocket_Global = WebSocket;
Proxy.on("close", () => {
clearTimers();
WebSocket.close();
});
return Stream;
}
function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
const chatOn = ctx.globalOptions.online;
const foreground = false;
const sessionID = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + 1;
const GUID = utils.getGUID();
const username = {
u: ctx.userID,
s: sessionID,
chat_on: chatOn,
fg: foreground,
d: GUID,
ct: "websocket",
aid: 219994525426954,
aids: null,
mqtt_sid: "",
cp: 3,
ecp: 10,
st: [],
pm: [],
dc: "",
no_auto_fg: true,
gas: null,
pack: [],
p: null,
php_override: ""
};
const cookies = ctx.jar.getCookies("https://www.facebook.com").join("; ");
let host;
if (ctx.mqttEndpoint) {
host = `${ctx.mqttEndpoint}&sid=${sessionID}&cid=${GUID}`;
} else if (ctx.region) {
host = `wss://edge-chat.facebook.com/chat?region=${ctx.region.toLowerCase()}&sid=${sessionID}&cid=${GUID}`;
} else {
host = `wss://edge-chat.facebook.com/chat?sid=${sessionID}&cid=${GUID}`;
}
const 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 ||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
Referer: "https://www.facebook.com/",
Host: "edge-chat.facebook.com",
Connection: "Upgrade",
Pragma: "no-cache",
"Cache-Control": "no-cache",
Upgrade: "websocket",
"Sec-WebSocket-Version": "13",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "vi,en;q=0.9",
"Sec-WebSocket-Extensions":
"permessage-deflate; client_max_window_bits",
},
origin: "https://www.facebook.com",
protocolVersion: 13,
binaryType: "arraybuffer",
},
keepalive: 30,
reschedulePings: true,
reconnectPeriod: 1000,
connectTimeout: 5000,
};
if (ctx.globalOptions.proxy !== undefined) {
const agent = new HttpsProxyAgent(ctx.globalOptions.proxy);
options.wsOptions.agent = agent;
}
ctx.mqttClient = new mqtt.Client(
() =>
buildStream(
options,
new WebSocket(host, options.wsOptions),
buildProxy()
),
options
);
const mqttClient = ctx.mqttClient;
global.mqttClient = mqttClient;
mqttClient.on('error', function (err) {
log.error("listenMqtt", err);
mqttClient.end();
if (ctx.globalOptions.autoReconnect) {
listenMqtt(defaultFuncs, api, ctx, globalCallback);
} else {
utils.checkLiveCookie(ctx, defaultFuncs)
.then(res => {
globalCallback({
type: "stop_listen",
error: "Connection refused: Server unavailable"
}, null);
})
.catch(err => {
globalCallback({
type: "account_inactive",
error: "Maybe your account is blocked by facebook, please login and check at https://facebook.com"
}, null);
});
}
});
mqttClient.on("connect", function () {
if (process.env.OnStatus === undefined) {
logger("fca-unoffcial premium", "info");
process.env.OnStatus = true;
}
topics.forEach((topicsub) => mqttClient.subscribe(topicsub));
var topic;
const queue = {
sync_api_version: 11,
max_deltas_able_to_process: 100,
delta_batch_size: 500,
encoding: "JSON",
entity_fbid: ctx.userID,
initial_titan_sequence_id: ctx.lastSeqId,
device_params: null,
};
if (ctx.syncToken) {
topic = "/messenger_sync_get_diffs";
queue.last_seq_id = ctx.lastSeqId;
queue.sync_token = ctx.syncToken;
} else {
topic = "/messenger_sync_create_queue";
queue.initial_titan_sequence_id = ctx.lastSeqId;
queue.device_params = null;
}
mqttClient.publish(topic, JSON.stringify(queue), { qos: 1, retain: false });
mqttClient.publish(
"/foreground_state",
JSON.stringify({ foreground: chatOn }),
{ qos: 1 }
);
mqttClient.publish(
"/set_client_settings",
JSON.stringify({ make_user_available_when_in_foreground: true }),
{ qos: 1 }
);
const rTimeout = setTimeout(function () {
mqttClient.end();
listenMqtt(defaultFuncs, api, ctx, globalCallback);
}, 5000);
ctx.tmsWait = function () {
clearTimeout(rTimeout);
ctx.globalOptions.emitReady
? globalCallback({
type: "ready",
error: null,
})
: "";
delete ctx.tmsWait;
};
});
mqttClient.on("message", function (topic, message, _packet) {
try {
let jsonMessage = Buffer.isBuffer(message)
? Buffer.from(message).toString()
: message;
try {
jsonMessage = JSON.parse(jsonMessage);
} catch (e) {
jsonMessage = {};
}
if (jsonMessage.type === "jewel_requests_add") {
globalCallback(null, {
type: "friend_request_received",
actorFbId: jsonMessage.from.toString(),
timestamp: Date.now().toString(),
});
} else if (jsonMessage.type === "jewel_requests_remove_old") {
globalCallback(null, {
type: "friend_request_cancel",
actorFbId: jsonMessage.from.toString(),
timestamp: Date.now().toString(),
});
} else if (topic === "/t_ms") {
if (ctx.tmsWait && typeof ctx.tmsWait == "function") {
ctx.tmsWait();
}
if (jsonMessage.firstDeltaSeqId && jsonMessage.syncToken) {
ctx.lastSeqId = jsonMessage.firstDeltaSeqId;
ctx.syncToken = jsonMessage.syncToken;
}
if (jsonMessage.lastIssuedSeqId) {
ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId);
}
for (const i in jsonMessage.deltas) {
const delta = jsonMessage.deltas[i];
parseDelta(defaultFuncs, api, ctx, globalCallback, {
delta: delta,
});
}
} else if (
topic === "/thread_typing" ||
topic === "/orca_typing_notifications"
) {
const 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 (const i in jsonMessage.list) {
const data = jsonMessage.list[i];
const userID = data["u"];
const presence = {
type: "presence",
userID: userID.toString(),
timestamp: data["l"] * 1000,
statuses: data["p"],
};
(function () {
globalCallback(null, presence);
})();
}
}
} else if (topic == "/ls_resp") {
const parsedPayload = JSON.parse(jsonMessage.payload);
const reqID = jsonMessage.request_id;
if (ctx["tasks"].has(reqID)) {
const taskData = ctx["tasks"].get(reqID);
const { type: taskType, callback: taskCallback } = taskData;
const taskRespData = getTaskResponseData(taskType, parsedPayload);
if (taskRespData == null) {
taskCallback("error", null);
} else {
taskCallback(null, {
type: taskType,
reqID: reqID,
...taskRespData,
});
}
}
}
} catch (ex) {
console.error("Message parsing error:", ex);
if (ex.stack) console.error(ex.stack);
return;
}
});
mqttClient.on("close", function () {});
mqttClient.on("disconnect", () => {});
}
function getTaskResponseData(taskType, payload) {
try {
switch (taskType) {
case "send_message_mqtt": {
return {
type: taskType,
threadID: payload.step[1][2][2][1][2],
messageID: payload.step[1][2][2][1][3],
payload: payload.step[1][2],
};
}
case "set_message_reaction": {
return {
mid: payload.step[1][2][2][1][4],
};
}
case "edit_message": {
return {
mid: payload.step[1][2][2][1][2],
};
}
}
} catch (error) {
return null;
}
}
function parseDelta(defaultFuncs, api, ctx, globalCallback, { delta }) {
if (delta.class === "NewMessage") {
if (ctx.globalOptions.pageID && ctx.globalOptions.pageID !== delta.queue)
return;
const resolveAttachmentUrl = (i) => {
if (
!delta.attachments ||
i === delta.attachments.length ||
utils.getType(delta.attachments) !== "Array"
) {
let fmtMsg;
try {
fmtMsg = utils.formatDeltaMessage(delta);
} catch (err) {
return log.error("Lỗi Nhẹ", err);
}
if (fmtMsg) {
if (ctx.globalOptions.autoMarkDelivery) {
markDelivery(ctx, api, fmtMsg.threadID, fmtMsg.messageID);
}
if (!ctx.globalOptions.selfListen && fmtMsg.senderID === ctx.userID)
return;
globalCallback(null, fmtMsg);
}
} else {
const attachment = delta.attachments[i];
if (attachment.mercury.attach_type === "photo") {
api.resolvePhotoUrl(attachment.fbid, (err, url) => {
if (!err) attachment.mercury.metadata.url = url;
resolveAttachmentUrl(i + 1);
});
} else {
resolveAttachmentUrl(i + 1);
}
}
};
resolveAttachmentUrl(0);
} else if (delta.class === "ClientPayload") {
const clientPayload = utils.decodeClientPayload(delta.payload);
if (clientPayload && clientPayload.deltas) {
for (const delta of clientPayload.deltas) {
if (delta.deltaMessageReaction && !!ctx.globalOptions.listenEvents) {
const messageReaction = {
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(),
};
globalCallback(null, messageReaction);
} else if (
delta.deltaRecallMessageData &&
!!ctx.globalOptions.listenEvents
) {
const messageUnsend = {
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,
};
globalCallback(null, messageUnsend);
} else if (delta.deltaMessageReply) {
const mdata =
delta.deltaMessageReply.message === undefined
? []
: delta.deltaMessageReply.message.data === undefined
? []
: delta.deltaMessageReply.message.data.prng === undefined
? []
: JSON.parse(delta.deltaMessageReply.message.data.prng);
const m_id = mdata.map((u) => u.i);
const m_offset = mdata.map((u) => u.o);
const m_length = mdata.map((u) => u.l);
const mentions = {};
for (let 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]);
}
const 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((att) => {
const mercury = JSON.parse(att.mercuryJSON);
Object.assign(att, mercury);
return att;
})
.map((att) => {
let x;
try {
x = utils._formatAttachment(att);
} catch (ex) {
x = att;
x.error = ex;
x.type = "unknown";
}
return x;
}),
args: (delta.deltaMessageReply.message.body || "")
.trim()
.split(/\s+/),
body: delta.deltaMessageReply.message.body || "",
isGroup:
!!delta.deltaMessageReply.message.messageMetadata.threadKey
.threadFbId,
mentions,
timestamp: parseInt(
delta.deltaMessageReply.message.messageMetadata.timestamp
),
participantIDs: (
delta.deltaMessageReply.message.participants || []
).map((e) => e.toString()),
};
if (delta.deltaMessageReply.repliedToMessage) {
const mdata =
delta.deltaMessageReply.repliedToMessage === undefined
? []
: delta.deltaMessageReply.repliedToMessage.data === undefined
? []
: delta.deltaMessageReply.repliedToMessage.data.prng ===
undefined
? []
: JSON.parse(
delta.deltaMessageReply.repliedToMessage.data.prng
);
const m_id = mdata.map((u) => u.i);
const m_offset = mdata.map((u) => u.o);
const m_length = mdata.map((u) => u.l);
const rmentions = {};
for (let 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]);
}
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((att) => {
let mercury;
try {
mercury = JSON.parse(att.mercuryJSON);
Object.assign(att, mercury);
} catch (ex) {
mercury = {};
}
return att;
})
.map((att) => {
let x;
try {
x = utils._formatAttachment(att);
} catch (ex) {
x = att;
x.error = ex;
x.type = "unknown";
}
return x;
}),
args: (delta.deltaMessageReply.repliedToMessage.body || "")
.trim()
.split(/\s+/),
body: delta.deltaMessageReply.repliedToMessage.body || "",
isGroup:
!!delta.deltaMessageReply.repliedToMessage.messageMetadata
.threadKey.threadFbId,
mentions: rmentions,
timestamp: parseInt(
delta.deltaMessageReply.repliedToMessage.messageMetadata
.timestamp
),
participantIDs: (
delta.deltaMessageReply.repliedToMessage.participants || []
).map((e) => e.toString()),
};
} else if (delta.deltaMessageReply.replyToMessageId) {
return defaultFuncs
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, {
av: ctx.globalOptions.pageID,
queries: JSON.stringify({
o0: {
doc_id: "2848441488556444",
query_params: {
thread_and_message_id: {
thread_id: callbackToReturn.threadID,
message_id: delta.deltaMessageReply.replyToMessageId.id,
},
},
},
}),
})
.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,
};
const fetchData = resData[0].o0.data.message;
const mobj = {};
for (const n in fetchData.message.ranges) {
mobj[fetchData.message.ranges[n].entity.id] = (
fetchData.message.text || ""
).substr(
fetchData.message.ranges[n].offset,
fetchData.message.ranges[n].length
);
}
callbackToReturn.messageReply = {
type: "Message",
threadID: callbackToReturn.threadID,
messageID: fetchData.message_id,
senderID: fetchData.message_sender.id.toString(),
attachments: fetchData.message.blob_attachment.map((att) =>
utils._formatAttachment({
blob_attachment: att,
})
),
args:
(fetchData.message.text || "").trim().split(/\s+/) || [],
body: fetchData.message.text || "",
isGroup: callbackToReturn.isGroup,
mentions: mobj,
timestamp: parseInt(fetchData.timestamp_precise),
};
})
.catch((err) => log.error("forcedFetch", err))
.finally(() => {
if (ctx.globalOptions.autoMarkDelivery) {
markDelivery(
ctx,
api,
callbackToReturn.threadID,
callbackToReturn.messageID
);
}
if (
!ctx.globalOptions.selfListen &&
callbackToReturn.senderID === ctx.userID
)
return;
globalCallback(null, callbackToReturn);
});
} else {
callbackToReturn.delta = delta;
}
if (ctx.globalOptions.autoMarkDelivery) {
markDelivery(
ctx,
api,
callbackToReturn.threadID,
callbackToReturn.messageID
);
}
if (
!ctx.globalOptions.selfListen &&
callbackToReturn.senderID === ctx.userID
)
return;
globalCallback(null, callbackToReturn);
}
}
return;
}
}
switch (delta.class) {
case "ReadReceipt": {
let fmtMsg;
try {
fmtMsg = utils.formatDeltaReadReceipt(delta);
} catch (err) {
return log.error("Lỗi Nhẹ", err);
}
globalCallback(null, fmtMsg);
break;
}
case "AdminTextMessage": {
switch (delta.type) {
case "instant_game_dynamic_custom_update":
case "accept_pending_thread":
case "confirm_friend_request":
case "shared_album_delete":
case "shared_album_addition":
case "pin_messages_v2":
case "unpin_messages_v2":
case "change_thread_theme":
case "change_thread_nickname":
case "change_thread_icon":
case "change_thread_quick_reaction":
case "change_thread_admins":
case "group_poll":
case "joinable_group_link_mode_change":
case "magic_words":
case "change_thread_approval_mode":
case "messenger_call_log":
case "participant_joined_group_call":
case "rtc_call_log":
case "update_vote": {
let fmtMsg;
try {
fmtMsg = utils.formatDeltaEvent(delta);
} catch (err) {
console.log(delta);
return log.error("Lỗi Nhẹ", err);
}
globalCallback(null, fmtMsg);
break;
}
}
break;
}
case "ForcedFetch": {
if (!delta.threadKey) return;
const mid = delta.messageId;
const tid = delta.threadKey.threadFbId;
if (mid && tid) {
const form = {
av: ctx.globalOptions.pageID,
queries: JSON.stringify({
o0: {
doc_id: "2848441488556444",
query_params: {
thread_and_message_id: {
thread_id: tid.toString(),
message_id: mid,
},
},
},
}),
};
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,
};
const fetchData = resData[0].o0.data.message;
if (utils.getType(fetchData) === "Object") {
log.info("forcedFetch", fetchData);
switch (fetchData.__typename) {
case "ThreadImageMessage":
(!ctx.globalOptions.selfListen &&
fetchData.message_sender.id.toString() === ctx.userID) ||
!ctx.loggedIn
? undefined
: (function () {
globalCallback(null, {
type: "event",
threadID: utils.formatID(tid.toString()),
logMessageType: "log:thread-image",
logMessageData: {
image: {
attachmentID:
fetchData.image_with_metadata &&
fetchData.image_with_metadata
.legacy_attachment_id,
width:
fetchData.image_with_metadata &&
fetchData.image_with_metadata
.original_dimensions.x,
height:
fetchData.image_with_metadata &&
fetchData.image_with_metadata
.original_dimensions.y,
url:
fetchData.image_with_metadata &&
fetchData.image_with_metadata.preview.uri,
},
},
logMessageBody: fetchData.snippet,
timestamp: fetchData.timestamp_precise,
author: fetchData.message_sender.id,
});
})();
break;
case "UserMessage": {
const event = {
type: "message",
senderID: utils.formatID(fetchData.message_sender.id),
body: fetchData.message.text || "",
threadID: utils.formatID(tid.toString()),
messageID: fetchData.message_id,
attachments: [
{
type: "share",
ID: fetchData.extensible_attachment
.legacy_attachment_id,
url: fetchData.extensible_attachment.story_attachment
.url,
title:
fetchData.extensible_attachment.story_attachment
.title_with_entities.text,
description:
fetchData.extensible_attachment.story_attachment
.description.text,
source:
fetchData.extensible_attachment.story_attachment
.source,
image: (
(
fetchData.extensible_attachment.story_attachment
.media || {}
).image || {}
).uri,
width: (
(
fetchData.extensible_attachment.story_attachment
.media || {}
).image || {}
).width,
height: (
(
fetchData.extensible_attachment.story_attachment
.media || {}
).image || {}
).height,
playable:
(
fetchData.extensible_attachment.story_attachment
.media || {}
).is_playable || false,
duration:
(
fetchData.extensible_attachment.story_attachment
.media || {}
).playable_duration_in_ms || 0,
subattachments:
fetchData.extensible_attachment.subattachments,
properties:
fetchData.extensible_attachment.story_attachment
.properties,
},
],
mentions: {},
timestamp: parseInt(fetchData.timestamp_precise),
isGroup: fetchData.message_sender.id !== tid.toString(),
};
log.info("ff-Return", event);
globalCallback(null, event);
break;
}
default:
log.error("forcedFetch", fetchData);
}
} else {
log.error("forcedFetch", fetchData);
}
})
.catch((err) => log.error("forcedFetch", err));
}
break;
}
case "ThreadName":
case "ParticipantsAddedToGroupThread":
case "ParticipantLeftGroupThread": {
let formattedEvent;
try {
formattedEvent = utils.formatDeltaEvent(delta);
} catch (err) {
console.log(err);
return log.error("Lỗi Nhẹ", err);
}
if (
!ctx.globalOptions.selfListen &&
formattedEvent.author.toString() === ctx.userID
)
return;
if (!ctx.loggedIn) return;
globalCallback(null, formattedEvent);
break;
}
case "NewMessage": {
const hasLiveLocation = (delta) => {
const attachment =
delta.attachments?.[0]?.mercury?.extensible_attachment;
const storyAttachment = attachment?.story_attachment;
return storyAttachment?.style_list?.includes("message_live_location");
};
if (delta.attachments?.length === 1 && hasLiveLocation(delta)) {
delta.class = "UserLocation";
try {
const fmtMsg = utils.formatDeltaEvent(delta);
globalCallback(null, fmtMsg);
} catch (err) {
console.log(delta);
log.error("Lỗi Nhẹ", err);
}
}
break;
}
}
}
function markDelivery(ctx, api, threadID, messageID) {
if (threadID && messageID) {
api.markAsDelivered(threadID, messageID, (err) => {
if (err) log.error("markAsDelivered", err);
else {
if (ctx.globalOptions.autoMarkRead) {
api.markAsRead(threadID, (err) => {
if (err) log.error("markAsDelivered", err);
});
}
}
});
}
}
module.exports = function (defaultFuncs, api, ctx) {
let globalCallback = identity;
getSeqID = function getSeqID() {
ctx.t_mqttCalled = false;
defaultFuncs
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
.then((resData) => {
if (utils.getType(resData) !== "Array") throw { error: "Not logged in", res: resData };
if (resData && 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) {
ctx.lastSeqId =
resData[0].o0.data.viewer.message_threads.sync_sequence_id;
listenMqtt(defaultFuncs, api, ctx, globalCallback);
} else
throw {
error: "getSeqId: no sync_sequence_id found.",
res: resData,
};
})
.catch((err) => {
log.error("getSeqId", err);
if (utils.getType(err) === "Object" && err.error === "Not logged in")
ctx.loggedIn = false;
return globalCallback(err);
});
};
return function (callback) {
class MessageEmitter extends EventEmitter {
stopListening(callback) {
callback = callback || (() => {});
globalCallback = identity;
if (ctx.mqttClient) {
ctx.mqttClient.unsubscribe("/webrtc");
ctx.mqttClient.unsubscribe("/rtc_multi");
ctx.mqttClient.unsubscribe("/onevc");
ctx.mqttClient.publish("/browser_close", "{}");
ctx.mqttClient.end(false, function (...data) {
callback(data);
ctx.mqttClient = undefined;
});
}
}
async stopListeningAsync() {
return new Promise((resolve) => {
this.stopListening(resolve);
});
}
}
const msgEmitter = new MessageEmitter();
globalCallback =
callback ||
function (error, message) {
if (error) {
return msgEmitter.emit("error", error);
}
msgEmitter.emit("message", message);
};
if (!ctx.firstListen) ctx.lastSeqId = null;
ctx.syncToken = undefined;
ctx.t_mqttCalled = false;
form = {
av: ctx.globalOptions.pageID,
queries: JSON.stringify({
o0: {
doc_id: "3336396659757871",
query_params: {
limit: 1,
before: null,
tags: ["INBOX"],
includeDeliveryReceipts: false,
includeSeqID: true,
},
},
}),
};
if (!ctx.firstListen || !ctx.lastSeqId) {
getSeqID(defaultFuncs, api, ctx, globalCallback);
} else {
listenMqtt(defaultFuncs, api, ctx, globalCallback);
}
api.stopListening = msgEmitter.stopListening;
api.stopListeningAsync = msgEmitter.stopListeningAsync;
return msgEmitter;
};
};