@dongdev/fca-unofficial
Version:
A Facebook chat API without XMPP, will not be deprecated after April 30th, 2015.
302 lines (300 loc) • 10.8 kB
JavaScript
;
const utils = require("../utils");
const log = require("npmlog");
const fs = require("fs");
const path = require("path");
const logger = require("../lib/logger");
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,
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) {
if (!data) return;
if (data?.errors) return data.errors;
const messageThread = data.message_thread;
if (!messageThread) return null;
const threadID = messageThread.thread_key.thread_fbid
? messageThread.thread_key.thread_fbid
: messageThread.thread_key.other_user_id;
const lastM = messageThread.last_message;
const 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;
const snippetText =
lastM && lastM.nodes && lastM.nodes[0] ? lastM.nodes[0].snippet : null;
const lastR = messageThread.last_read_receipt;
const 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.edges.map(
(d) => d.node.messaging_actor.id
),
userInfo: messageThread.all_participants.edges.map((d) => ({
id: d.node.messaging_actor.id,
name: d.node.messaging_actor.name,
firstName: d.node.messaging_actor.short_name,
vanity: d.node.messaging_actor.username,
url: d.node.messaging_actor.url,
thumbSrc: d.node.messaging_actor.big_image_src.uri,
profileUrl: d.node.messaging_actor.big_image_src.uri,
gender: d.node.messaging_actor.gender,
type: d.node.messaging_actor.__typename,
isFriend: d.node.messaging_actor.is_viewer_friend,
isBirthday: !!d.node.messaging_actor.is_birthday,
})),
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,
threadTheme: messageThread.thread_theme,
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,
approvalMode: Boolean(messageThread.approval_mode),
approvalQueue: messageThread.group_approval_queue.nodes.map((a) => ({
inviterID: a.inviter.id,
requesterID: a.requester.id,
timestamp: a.request_timestamp,
request_source: a.request_source,
})),
reactionsMuteMode: messageThread.reactions_mute_mode.toLowerCase(),
mentionsMuteMode: messageThread.mentions_mute_mode.toLowerCase(),
isPinProtected: messageThread.is_pin_protected,
relatedPageThread: messageThread.related_page_thread,
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,
inviteLink: {
enable: messageThread.joinable_mode
? messageThread.joinable_mode.mode == 1
: false,
link: messageThread.joinable_mode
? messageThread.joinable_mode.link
: null,
},
};
}
const queue = [];
let isProcessingQueue = false;
const processingThreads = new Set();
const queuedThreads = new Set();
module.exports = function(defaultFuncs, api, ctx) {
const getMultiInfo = async function(threadIDs) {
let form = {};
let tempThreadInf = [];
threadIDs.forEach((x, y) => {
form["o" + y] = {
doc_id: "3449967031715030",
query_params: {
id: x,
message_limit: 0,
load_messages: false,
load_read_receipts: false,
before: null,
},
};
});
let Submit = {
queries: JSON.stringify(form),
batch_name: "MessengerGraphQLThreadFetcher",
};
try {
const resData = await defaultFuncs
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, Submit)
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
if (resData.error || resData[resData.length - 1].error_results !== 0)
throw new Error(
"Error: getThreadInfoGraphQL - You may be rate limited"
);
resData
.slice(0, -1)
.sort((a, b) => Object.keys(a)[0].localeCompare(Object.keys(b)[0]))
.forEach((x, y) =>
tempThreadInf.push(formatThreadGraphQLResponse(x["o" + y].data))
);
return { Success: true, Data: tempThreadInf };
} catch (err) {
return { Success: false, Data: "" };
}
};
const dbFiles = fs
.readdirSync(path.join(__dirname, "../lib/database"))
.filter((f) => path.extname(f) === ".js")
.reduce((acc, file) => {
acc[path.basename(file, ".js")] = require(path.join(
__dirname,
"../lib/database",
file
))(api);
return acc;
}, {});
const { threadData } = dbFiles;
const { create, get, update, getAll } = threadData;
async function fetchThreadInfo(tID, isNew) {
try {
const response = await getMultiInfo([tID]);
if (!response.Success) throw new Error("Failed to fetch thread info");
const threadInfo = response.Data[0];
if (isNew) {
await create(tID, { data: threadInfo });
logger(`Success create data thread: ${tID}`, 'info');
} else {
await update(tID, { data: threadInfo });
logger(`Success update data thread: ${tID}`, 'info');
}
} catch (err) {
console.error("fetchThreadInfo", err);
} finally {
queuedThreads.delete(tID);
}
}
async function checkAndUpdateThreads() {
try {
const allThreads = await getAll("threadID");
const existingThreadIDs = new Set(allThreads.map((t) => t.threadID));
for (const t of existingThreadIDs) {
const result = await get(t);
if (!result) continue;
const now = Date.now();
const lastUpdated = new Date(result.updatedAt).getTime();
if ((now - lastUpdated) / (1000 * 60) > 10 && !queuedThreads.has(t)) {
queuedThreads.add(t);
logger(`ThreadID ${t} aready update data`, 'info');
queue.push(() => fetchThreadInfo(t, false));
}
}
} catch (err) {
console.error("checkAndUpdateThreads", err);
}
}
async function processQueue() {
if (isProcessingQueue) return;
isProcessingQueue = true;
while (queue.length > 0) {
const task = queue.shift();
try {
await task();
} catch (err) {
console.error("Queue processing error", err);
}
}
isProcessingQueue = false;
}
setInterval(() => {
checkAndUpdateThreads();
processQueue();
}, 10000);
return async function getThreadInfoGraphQL(threadID, callback) {
let resolveFunc = function() {};
let rejectFunc = function() {};
const returnPromise = new Promise(function(resolve, reject) {
resolveFunc = resolve;
rejectFunc = reject;
});
if (
utils.getType(callback) != "Function" &&
utils.getType(callback) != "AsyncFunction"
) {
callback = function(err, data) {
if (err) {
return rejectFunc(err);
}
resolveFunc(data);
};
}
if (utils.getType(threadID) !== "Array") {
threadID = [threadID];
}
let result;
try {
result = await get(threadID[0]);
if (result) {
callback(null, result.data);
} else {
if (!processingThreads.has(threadID[0])) {
processingThreads.add(threadID[0]);
logger(`Created new thread data: ${threadID[0]}`, 'info');
const response = await getMultiInfo(threadID);
if (!response.Success) throw new Error("Failed to get thread info");
const data = response.Data[0];
await create(threadID[0], { data });
callback(null, data);
processingThreads.delete(threadID[0]);
}
}
} catch (err) {
callback(err);
}
return returnPromise;
};
};