"use strict"; const utils = require("./utils"); const log = require("npmlog"); const 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 = null; let region = null; let irisSeqID = null; let noMqttData = null; if (oldFBMQTTMatch) { irisSeqID = oldFBMQTTMatch[1]; mqttEndpoint = oldFBMQTTMatch[2]; region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase(); log.info("login", `Got this account's message region: ${region}`); } else { const newFBMQTTMatch = html.match(/{"app_id":"219994525426954","endpoint":"(.+?)","iris_seq_id":"(.+?)"}/); if (newFBMQTTMatch) { irisSeqID = newFBMQTTMatch[2]; mqttEndpoint = newFBMQTTMatch[1].replace(/\\\//g, "/"); region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase(); log.info("login", `Got this account's message region: ${region}`); } else { const legacyFBMQTTMatch = html.match(/(\["MqttWebConfig",\[\],{fbid:")(.+?)(",appID:219994525426954,endpoint:")(.+?)(",pollingEndpoint:")(.+?)(3790])/); if (legacyFBMQTTMatch) { mqttEndpoint = legacyFBMQTTMatch[4]; region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase(); log.warn("login", `Cannot get sequence ID with new RegExp. Fallback to old RegExp (without seqID)...`); log.info("login", `Got this account's message region: ${region}`); log.info("login", `[Unused] Polling endpoint: ${legacyFBMQTTMatch[6]}`); } else { log.warn("login", "Cannot get MQTT region & sequence ID."); noMqttData = html; } } } // 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, wsReqNumber: 0, wsTaskNumber: 0, mqttEndpoint, region, firstListen: true }; 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); } }; if (noMqttData) { api["htmlData"] = noMqttData; } const apiFuncNames = [ 'addExternalModule', 'addUserToGroup', 'changeAdminStatus', 'changeArchivedStatus', 'changeAvatar', 'changeBio', 'changeBlockedStatus', 'changeGroupImage', 'changeNickname', 'changeThreadColor', 'changeThreadEmoji', 'createNewGroup', 'createPoll', 'deleteMessage', 'deleteThread', 'forwardAttachment', 'getCurrentUserID', 'getEmojiUrl', 'getFriendsList', 'getMessage', 'getThreadHistory', 'getThreadInfo', 'getThreadList', 'getThreadPictures', 'getUserID', 'getUserInfo', 'handleMessageRequest', 'listenMqtt', 'logout', 'markAsDelivered', 'markAsRead', 'markAsReadAll', 'markAsSeen', 'muteThread', 'refreshFb_dtsg', 'removeUserFromGroup', 'resolvePhotoUrl', 'searchForThread', 'sendMessage', 'sendTypingIndicator', 'setMessageReaction', 'setPostReaction', 'setTitle', 'threadColors', 'unsendMessage', 'unfriend', 'editMessage', 'shareContact', 'sendComment', // HTTP 'httpGet', 'httpPost', 'httpPostFormData', 'uploadAttachment' ]; const defaultFuncs = utils.makeDefaults(html, i_userID || userID, ctx); // Load all api functions in a loop apiFuncNames.map(function (v) { api[v] = require('./src/' + v)(defaultFuncs, api, ctx); }); //Removing original `listen` that uses pull. //Map it to listenMqtt instead for backward compatibly. 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 = /]+>/; 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 10_10_2) AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 Safari/600.3.18" }; 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;