UNPKG

nv-fca

Version:

A node.js package for automating Facebook Messenger bot, and is one of the most advanced next-generation Facebook Chat API (FCA) by @NethWs3Dev (Kenneth Aceberos)

364 lines (337 loc) 14.4 kB
"use strict"; const utils = require("./utils"); const fs = require("fs"); const cron = require("node-cron"); let globalOptions = {}; let ctx = null; let defaultFuncs = null; let api = null; let region = null; const fbLink = (ext) => ("https://www.facebook.com" + (ext ? '/' + ext : '')); const ERROR_RETRIEVING = "Error retrieving userID. This can be caused by many factors, including being blocked by Facebook for logging in from an unknown location. Try logging in with a browser to verify."; /** * Sets global options based on provided configuration. * @param {Object} options - Configuration options to set. * @returns {Promise<void>} */ async function setOptions(options = {}) { const optionHandlers = { online: (value) => (globalOptions.online = Boolean(value)), selfListen: (value) => (globalOptions.selfListen = Boolean(value)), selfListenEvent: (value) => (globalOptions.selfListenEvent = value), listenEvents: (value) => (globalOptions.listenEvents = Boolean(value)), updatePresence: (value) => (globalOptions.updatePresence = Boolean(value)), forceLogin: (value) => (globalOptions.forceLogin = Boolean(value)), userAgent: (value) => (globalOptions.userAgent = value), autoMarkDelivery: (value) => (globalOptions.autoMarkDelivery = Boolean(value)), autoMarkRead: (value) => (globalOptions.autoMarkRead = Boolean(value)), listenTyping: (value) => (globalOptions.listenTyping = Boolean(value)), proxy: (value) => { if (typeof value !== "string") { delete globalOptions.proxy; utils.setProxy(); } else { globalOptions.proxy = value; utils.setProxy(value); } }, autoReconnect: (value) => (globalOptions.autoReconnect = Boolean(value)), emitReady: (value) => (globalOptions.emitReady = Boolean(value)), randomUserAgent: (value) => { globalOptions.randomUserAgent = Boolean(value); if (value) { globalOptions.userAgent = utils.randomUserAgent(); utils.warn("Random user agent enabled. This is an experimental feature and may not work with some accounts. Use at your own risk."); utils.warn("randomUserAgent", "UA selected:", globalOptions.userAgent); } }, bypassRegion: (value) => (globalOptions.bypassRegion = value), }; Object.entries(options).forEach(([key, value]) => { if (optionHandlers[key]) optionHandlers[key](value); }); } /** * Checks if the account is suspended. * @param {Object} resp - Response object from the request. * @param {Array} appstate - Application state cookies. * @returns {Promise<Object|undefined>} */ async function checkIfSuspended(resp, appstate) { try { const appstateCUser = appstate.find((i) => i.key === "c_user" || i.key === "i_user"); const UID = appstateCUser?.value; if (resp?.request?.uri?.href?.includes(fbLink("checkpoint")) && resp.request.uri.href.includes("1501092823525282")) { const suspendReasons = {}; const daystoDisable = resp.body?.match(/"log_out_uri":"(.*?)","title":"(.*?)"/); if (daystoDisable?.[2]) { suspendReasons.durationInfo = daystoDisable[2]; utils.error(`Suspension time remaining: ${suspendReasons.durationInfo}`); } const reasonDescription = resp.body?.match(/"reason_section_body":"(.*?)"/); if (reasonDescription && reasonDescription[1]) { suspendReasons.longReason = reasonDescription[1]; suspendReasons.shortReason = suspendReasons.longReason .toLowerCase() .replace("your account, or activity on it, doesn't follow our community standards on ", "") .replace(/^\w/, (c) => c.toUpperCase()); utils.error(`Alert on ${UID}: Account has been suspended!`); utils.error(`Why suspended: ${suspendReasons.longReason}`); utils.error(`Reason for suspension: ${suspendReasons.shortReason}`); } ctx = null; return { suspended: true, suspendReasons }; } } catch (error) { utils.error(`Error checking suspension: ${error.message}`); } } /** * Checks if the account is locked. * @param {Object} resp - Response object from the request. * @param {Array} appstate - Application state cookies. * @returns {Promise<Object|undefined>} */ async function checkIfLocked(resp, appstate) { try { const appstateCUser = appstate.find((i) => i.key === "c_user" || i.key === "i_user"); const UID = appstateCUser?.value; if (resp?.request?.uri?.href?.includes(fbLink("checkpoint")) && resp.request.uri.href.includes("828281030927956")) { const lockedReasons = {}; const lockDesc = resp.body?.match(/"is_unvetted_flow":true,"title":"(.*?)"/); if (lockDesc && lockDesc[1]) { lockedReasons.reason = lockDesc[1]; utils.error(`Alert on ${UID}: ${lockedReasons.reason}`); } ctx = null; return { locked: true, lockedReasons }; } } catch (error) { utils.error(`Error checking lock status: ${error.message}`); } } /** * Builds the API context and default functions. * @param {string} html - HTML response from Facebook. * @param {Object} jar - Cookie jar. * @returns {Array} - [Context, Default Functions] */ async function buildAPI(html, jar) { let userID; const filePath = "fb_dtsg_data.json"; const cookies = jar.getCookies(fbLink()); const primaryProfile = cookies.find((val) => val.cookieString().startsWith("c_user=")); const secondaryProfile = cookies.find((val) => val.cookieString().startsWith("i_user=")); if (!primaryProfile && !secondaryProfile) { throw new Error(ERROR_RETRIEVING); } if (html.includes("/checkpoint/block/?next")) { utils.warn("login", "Checkpoint detected. Please log in with a browser to verify."); throw new Error("Checkpoint detected"); } userID = secondaryProfile?.cookieString().split("=")[1] || primaryProfile.cookieString().split("=")[1]; const refreshFb_dtsg = async () => { const getDtsg = await utils.get(fbLink("ajax/dtsg/?__a=true"), jar, null, globalOptions); const dtsg = JSON.parse(getDtsg.body.replace('for (;;);{', "{")).payload.token; let jazoest = "2"; for (const char of dtsg) { jazoest += char.charCodeAt(0); } const result = { fb_dtsg: dtsg, jazoest }; const existingData = fs.existsSync(filePath) ? JSON.parse(fs.readFileSync(filePath, "utf8")) : {}; existingData[userID] = result; fs.writeFileSync(filePath, JSON.stringify(existingData, null, 4), "utf8"); return result; } const dtsgResult = await refreshFb_dtsg(); utils.log("Logged in!"); utils.log("Choosing the best region..."); const clientID = (Math.random() * 2147483648 | 0).toString(16); const mqttMatches = { oldFBMQTTMatch: html.match(/irisSeqID:"(.+?)",appID:219994525426954,endpoint:"(.+?)"/), newFBMQTTMatch: html.match(/{"app_id":"219994525426954","endpoint":"(.+?)","iris_seq_id":"(.+?)"}/), legacyFBMQTTMatch: html.match(/\["MqttWebConfig",\[\],{"fbid":"(.*?)","appID":219994525426954,"endpoint":"(.*?)","pollingEndpoint":"(.*?)"/), }; let mqttEndpoint, irisSeqID; for (const [key, match] of Object.entries(mqttMatches)) { if (globalOptions.bypassRegion || !match) continue; if (key === "oldFBMQTTMatch") { irisSeqID = match[1]; mqttEndpoint = match[2].replace(/\\\//g, "/"); region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase(); } else if (key === "newFBMQTTMatch") { irisSeqID = match[2]; mqttEndpoint = match[1].replace(/\\\//g, "/"); region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase(); } else if (key === "legacyFBMQTTMatch") { mqttEndpoint = match[2].replace(/\\\//g, "/"); region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase(); } break; } if (globalOptions.bypassRegion) { region = globalOptions.bypassRegion.toUpperCase(); utils.warn("Bypass region is enabled. This is an experimental feature yet, doesn't guarantee the effectiveness.") } if (!region) { const regions = ["prn", "pnb", "vll", "hkg", "sin", "ftw", "ash"]; region = regions[Math.floor(Math.random() * regions.length)].toUpperCase(); utils.warn("No region is specified from this account, now using random region. This doesn't guarantee the effectiveness."); } mqttEndpoint = mqttEndpoint || `wss://edge-chat.facebook.com/chat?region=${region}`; utils.log("Region specified:", region); utils.log("MQTT endpoint:", mqttEndpoint); ctx = { userID, jar, clientID, globalOptions, loggedIn: true, access_token: "NONE", clientMutationId: 0, mqttClient: undefined, lastSeqId: irisSeqID, syncToken: undefined, mqttEndpoint, wsReqNumber: 0, wsTaskNumber: 0, reqCallbacks: {}, region, firstListen: true, ...dtsgResult, }; defaultFuncs = utils.makeDefaults(html, userID, ctx); return [ctx, defaultFuncs, { refreshFb_dtsg }]; } /** * Handles login process using app state or credentials. * @param {Object} appState - Application state cookies. * @param {string} email - User email. * @param {string} password - User password. * @param {Object} apiCustomized - Custom API configurations. * @param {Function} callback - Callback function to handle login result. * @returns {Promise<void>} */ async function loginHelper(appState, apiCustomized, callback) { try { const jar = utils.getJar(); utils.log("Logging in..."); if (appState) { ((Array.isArray(appState) ? appState.map(c => [c.name || c.key, c.value].join('=')) : appState?.split(';')) || '').map(cookieString => { const domain = ".facebook.com"; const expires = new Date().getTime() + 1000 * 60 * 60 * 24 * 365; const str = `${cookieString}; expires=${expires}; domain=${domain}; path=/;`; jar.setCookie(str, `http://${domain}`); }); } else { throw new Error("No cookie found. Enter cookie (whether JSON/header string)"); } api = { setOptions: setOptions.bind(null, globalOptions), getAppState() { const appState = utils.getAppState(jar); if (!Array.isArray(appState)) return []; const uniqueAppState = appState.filter((item, index, self) => self.findIndex((t) => t.key === item.key) === index); return uniqueAppState.length > 0 ? uniqueAppState : appState; }, }; const mergedAppState = api.getAppState(); const resp = await utils.get(fbLink(), jar, null, globalOptions, { noRef: true }).then(utils.saveCookies(jar)); const [newCtx, newDefaultFuncs, apiFuncs] = await buildAPI(resp.body, jar); ctx = newCtx; defaultFuncs = newDefaultFuncs; api.addFunctions = (directory) => { const folder = directory.endsWith("/") ? directory : `${directory}/`; fs.readdirSync(folder).filter((v) => v.endsWith(".js")).forEach((v) => { api[v.replace(".js", "")] = require(`${folder}${v}`)(defaultFuncs, api, ctx); }); }; api.addFunctions(`${__dirname}/src`); api.listen = api.listenMqtt; api.refreshFb_dtsg = apiFuncs.refreshFb_dtsg; api.ws3 = { ...(apiCustomized && { ...apiCustomized }) }; const userID = api.getCurrentUserID(); if (resp?.request?.uri?.href?.includes(fbLink("checkpoint")) && resp.request.uri.href.includes("601051028565049")) { utils.warn(`Automated behavior detected on account ${userID}. This may cause auto-logout; resubmit appstate if needed.`); const bypassAutomation = await defaultFuncs.post(fbLink("api/graphql"), jar, { av: userID, fb_api_caller_class: "RelayModern", fb_api_req_friendly_name: "FBScrapingWarningMutation", variables: '{}', server_timestamps: true, doc_id: 6339492849481770, ...(ctx && { fb_dtsg: ctx.fb_dtsg, jazoest: ctx.jazoest }) }, globalOptions); } utils.log("Connected to specified region."); const detectLocked = await checkIfLocked(resp, mergedAppState); if (detectLocked) throw detectLocked; const detectSuspension = await checkIfSuspended(resp, mergedAppState); if (detectSuspension) throw detectSuspension; utils.log("Successfully logged in."); const botInitialData = await api.getBotInitialData(); if (!botInitialData.error) { utils.log(`Hello, ${botInitialData.name} (${botInitialData.uid})`); ctx.userName = botInitialData.name; } else { utils.warn(botInitialData.error); utils.warn(`WARNING: Failed to fetch account info. Proceeding to log in for user ${userID}`); } utils.log("To check updates: you may check on https://github.com/NethWs3Dev/ws3-fca"); return callback(null, api); } catch (error) { return callback(error); } } /** * Main login function. * @param {String} cookie - Login data containing cookie (JSON/header string). * @param {Object|Function} options - Configuration options or callback function. * @param {Function} [callback] - Callback function to handle login result. * @returns {void} */ async function login(cookie, options, callback) { if (typeof options === "function") { callback = options; options = {}; } const defaultOptions = { selfListen: false, selfListenEvent: false, listenEvents: true, listenTyping: false, updatePresence: false, forceLogin: false, autoMarkDelivery: false, autoMarkRead: true, autoReconnect: true, online: true, emitReady: false, userAgent: utils.defaultUserAgent, randomUserAgent: false, }; Object.assign(globalOptions, defaultOptions, options); const loginWs3 = () => { loginHelper(cookie, { relogin: loginWs3, }, (loginError, loginApi) => { if (loginError) { utils.error("login", loginError); return callback(loginError); } return callback(null, loginApi); } ); }; await setOptions(options); loginWs3(); } module.exports = { login };