UNPKG

hut-chat-api

Version:

A Facebook chat API that doesn't rely on XMPP and This new Logined and get the seqID ( CC PROJECTS AND CHATBOT COMMUNITY )

311 lines (279 loc) 9.28 kB
"use strict"; const utils = require("./utils"); const log = require("npmlog"); let checkVerified = null; const defaultLogRecordSize = 100; log.maxRecordSize = defaultLogRecordSize; function setOptions(globalOptions, options) { Object.keys(options).map(function (key) { switch (key) { case 'online': globalOptions.online = Boolean(options.online); break; case 'logLevel': log.level = options.logLevel; globalOptions.logLevel = options.logLevel; break; case 'logRecordSize': log.maxRecordSize = options.logRecordSize; globalOptions.logRecordSize = options.logRecordSize; break; case 'selfListen': globalOptions.selfListen = Boolean(options.selfListen); break; case 'selfListenEvent': globalOptions.selfListenEvent = options.selfListenEvent; break; case 'listenEvents': globalOptions.listenEvents = Boolean(options.listenEvents); break; case 'pageID': globalOptions.pageID = options.pageID.toString(); break; case 'updatePresence': globalOptions.updatePresence = Boolean(options.updatePresence); break; case 'forceLogin': globalOptions.forceLogin = Boolean(options.forceLogin); break; case 'userAgent': globalOptions.userAgent = options.userAgent; break; case 'autoMarkDelivery': globalOptions.autoMarkDelivery = Boolean(options.autoMarkDelivery); break; case 'autoMarkRead': globalOptions.autoMarkRead = Boolean(options.autoMarkRead); break; case 'listenTyping': globalOptions.listenTyping = Boolean(options.listenTyping); break; case 'proxy': if (typeof options.proxy != "string") { delete globalOptions.proxy; utils.setProxy(); } else { globalOptions.proxy = options.proxy; utils.setProxy(globalOptions.proxy); } break; case 'autoReconnect': globalOptions.autoReconnect = Boolean(options.autoReconnect); break; case 'emitReady': globalOptions.emitReady = Boolean(options.emitReady); break; default: log.warn("setOptions", "Unrecognized option given to setOptions: " + key); break; } }); } function buildAPI(globalOptions, html, jar) { const maybeCookie = jar.getCookies("https://www.facebook.com").filter(function (val) { return val.cookieString().split("=")[0] === "c_user"; }); const objCookie = jar.getCookies("https://www.facebook.com").reduce(function (obj, val) { obj[val.cookieString().split("=")[0]] = val.cookieString().split("=")[1]; return obj; }, {}); if (maybeCookie.length === 0) { throw { error: "Error retrieving userID. This can be caused by a lot of things, including getting blocked by Facebook for logging in from an unknown location. Try logging in with a browser to verify." }; } if (html.indexOf("/checkpoint/block/?next") > -1) { log.warn("login", "Checkpoint detected. Please log in with a browser to verify."); } const userID = maybeCookie[0].cookieString().split("=")[1].toString(); const i_userID = objCookie.i_user || null; log.info("login", `Logged in as ${userID}`); try { clearInterval(checkVerified); } catch (_) { } const clientID = (Math.random() * 2147483648 | 0).toString(16); const oldFBMQTTMatch = html.match(/irisSeqID:"(.+?)",appID:219994525426954,endpoint:"(.+?)"/); let mqttEndpoint, region, fb_dtsg, irisSeqID; try { const endpointMatch = html.match(/"endpoint":"([^"]+)"/); if (endpointMatch) { mqttEndpoint = endpointMatch[1].replace(/\\\//g, '/'); const url = new URL(mqttEndpoint); region = url.searchParams.get('region')?.toUpperCase() || "PRN"; } log.info('login', `Sever region ${region}`); } catch (e) { log.warning('login', 'Not MQTT endpoint'); } const tokenMatch = html.match(/DTSGInitialData.*?token":"(.*?)"/); if (tokenMatch) { fb_dtsg = tokenMatch[1]; } // All data available to api functions const ctx = { userID: userID, i_userID: i_userID, jar: jar, clientID: clientID, globalOptions: globalOptions, loggedIn: true, access_token: 'NONE', clientMutationId: 0, mqttClient: undefined, lastSeqId: irisSeqID, syncToken: undefined, mqttEndpoint, wsReqNumber: 0, wsTaskNumber: 0, reqCallbacks: {}, region, firstListen: true, fb_dtsg }; const api = { setOptions: setOptions.bind(null, globalOptions), getAppState: function getAppState() { const appState = utils.getAppState(jar); // filter duplicate return appState.filter((item, index, self) => self.findIndex((t) => { return t.key === item.key }) === index); } }; const defaultFuncs = utils.makeDefaults(html, i_userID || userID, ctx); require('fs').readdirSync(__dirname + '/src/').filter((v) => v.endsWith('.js')).map(function (v) { api[v.replace('.js', '')] = require('./src/' + v)(defaultFuncs, api, ctx); }); api.listen = api.listenMqtt; return [ctx, defaultFuncs, api]; } // Helps the login function loginHelper(appState, email, password, globalOptions, callback, prCallback) { let mainPromise = null; const jar = utils.getJar(); // If we're given an appState we loop through it and save each cookie // back into the jar. if (appState) { // check and convert cookie to appState if (utils.getType(appState) === 'Array' && appState.some(c => c.name)) { appState = appState.map(c => { c.key = c.name; delete c.name; return c; }) } else if (utils.getType(appState) === 'String') { const arrayAppState = []; appState.split(';').forEach(c => { const [key, value] = c.split('='); arrayAppState.push({ key: (key || "").trim(), value: (value || "").trim(), domain: ".facebook.com", path: "/", expires: new Date().getTime() + 1000 * 60 * 60 * 24 * 365 }); }); appState = arrayAppState; } appState.map(function (c) { const str = c.key + "=" + c.value + "; expires=" + c.expires + "; domain=" + c.domain + "; path=" + c.path + ";"; jar.setCookie(str, "http://" + c.domain); }); // Load the main page. mainPromise = utils .get('https://www.facebook.com/', jar, null, globalOptions, { noRef: true }) .then(utils.saveCookies(jar)); } else { if (email) { throw { error: "Currently, the login method by email and password is no longer supported, please use the login method by appState" }; } else { throw { error: "No appState given." }; } } let ctx = null; let _defaultFuncs = null; let api = null; mainPromise = mainPromise .then(function (res) { // Hacky check for the redirection that happens on some ISPs, which doesn't return statusCode 3xx const reg = /<meta http-equiv="refresh" content="0;url=([^"]+)[^>]+>/; const redirect = reg.exec(res.body); if (redirect && redirect[1]) { return utils .get(redirect[1], jar, null, globalOptions) .then(utils.saveCookies(jar)); } return res; }) .then(function (res) { const html = res.body; const stuff = buildAPI(globalOptions, html, jar); ctx = stuff[0]; _defaultFuncs = stuff[1]; api = stuff[2]; return res; }); // given a pageID we log in as a page if (globalOptions.pageID) { mainPromise = mainPromise .then(function () { return utils .get('https://www.facebook.com/' + ctx.globalOptions.pageID + '/messages/?section=messages&subsection=inbox', ctx.jar, null, globalOptions); }) .then(function (resData) { let url = utils.getFrom(resData.body, 'window.location.replace("https:\\/\\/www.facebook.com\\', '");').split('\\').join(''); url = url.substring(0, url.length - 1); return utils .get('https://www.facebook.com' + url, ctx.jar, null, globalOptions); }); } // At the end we call the callback or catch an exception mainPromise .then(function () { log.info("login", 'Done logging in.'); return callback(null, api); }) .catch(function (e) { log.error("login", e.error || e); callback(e); }); } function login(loginData, options, callback) { if (utils.getType(options) === 'Function' || utils.getType(options) === 'AsyncFunction') { callback = options; options = {}; } const globalOptions = { selfListen: false, selfListenEvent: false, listenEvents: false, listenTyping: false, updatePresence: false, forceLogin: false, autoMarkDelivery: true, autoMarkRead: false, autoReconnect: true, logRecordSize: defaultLogRecordSize, online: true, emitReady: false, userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.7; rv:132.0) Gecko/20100101 Firefox/132.0"//"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" }; setOptions(globalOptions, options); let prCallback = null; if (utils.getType(callback) !== "Function" && utils.getType(callback) !== "AsyncFunction") { let rejectFunc = null; let resolveFunc = null; var returnPromise = new Promise(function (resolve, reject) { resolveFunc = resolve; rejectFunc = reject; }); prCallback = function (error, api) { if (error) { return rejectFunc(error); } return resolveFunc(api); }; callback = prCallback; } loginHelper(loginData.appState, loginData.email, loginData.password, globalOptions, callback, prCallback); return returnPromise; } module.exports = login;