import { ApiCtx , ApiOptions , LoginCredentials , Dfs } from './lib/types' ; import * as utils from './lib/utils' ; import log , { LogLevels } from 'npmlog' ; import Jar from './lib/jar' ; import cheerio from 'cheerio' ; import Api from './lib/api' ; import { Response } from 'got' ; const defaultLogRecordSize = 100 ; /** Logs you into Facebook using given credentials and returns an `Api` instance.
 *
 * Login using email & password:
 * ```typescript
 * import facebookLogin from 'ts-messenger-api';
 * const api = await facebookLogin({ email: 'example@email.com', password: 'your_fb_password' }, {});
 * ```
 * Login using an `AppState` given from last login:
 * ```typescript
 * import facebookLogin from 'ts-messenger-api';
 * import fs from 'fs';
 * const api = await facebookLogin({ appState: JSON.parse(fs.readFileSync('path_to_file')) }, {});
 * ```
 */ export default async function login ( loginData , options = { } ) { const globalOptions = { selfListen : false , listenEvents : false , updatePresence : false , forceLogin : false , autoMarkDelivery : true , autoMarkRead : false , logRecordSize : defaultLogRecordSize , 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 ) ; return await loginHelper ( loginData , globalOptions ) ; } /** Sets `globalOptions` and npmlog based on the `options` attribute */ function setOptions ( globalOptions , options ) { Object . keys ( options ) . map ( function ( key ) { switch ( key ) { case 'logLevel' : log . level = options . logLevel as LogLevels ; globalOptions . logLevel = options . logLevel ; break ; case 'logRecordSize' : log . maxRecordSize = options . logRecordSize as number ; globalOptions . logRecordSize = options . logRecordSize ; break ; case 'selfListen' : globalOptions . selfListen = options . selfListen ; break ; case 'listenEvents' : globalOptions . listenEvents = options . listenEvents ; break ; case 'pageID' : globalOptions . pageID = options . pageID ?. toString ( ) ; break ; case 'updatePresence' : globalOptions . updatePresence = options . updatePresence ; break ; case 'forceLogin' : globalOptions . forceLogin = options . forceLogin ; break ; case 'userAgent' : globalOptions . userAgent = options . userAgent ; break ; case 'autoMarkDelivery' : globalOptions . autoMarkDelivery = options . autoMarkDelivery ; break ; case 'autoMarkRead' : globalOptions . autoMarkRead = options . autoMarkRead ; break ; default : log . warn ( 'setOptions' , 'Unrecognized option given to setOptions: ' + key ) ; break ; } } ) ; } async function loginHelper ( credentials , globalOptions ) { let mainPromise ; const jar = new Jar ( ) ; let ctx ; let defaultFuncs ; let api ; if ( credentials . appState ) { credentials . appState . map ( c => jar . setCookie ( `${ c . key }=${ c . value }; expires=${ c . expires }; domain=${ c . domain }; path=${ c . path };` , 'http://' + c . domain ) ) ; mainPromise = utils . get ( 'https://www.facebook.com/' , jar , null , globalOptions ) . then ( utils . saveCookies ( jar ) ) ; } else if ( credentials . email && credentials . password ) { mainPromise = utils . get ( 'https://m.facebook.com/' , null , null , globalOptions ) . then ( utils . saveCookies ( jar ) ) . then ( makeLogin ( jar , credentials . email , credentials . password , globalOptions ) ) ; } else throw new Error ( 'Argument error: you must specify AppState or email-password credentials' ) ; mainPromise = mainPromise . then ( async ( res ) => { const reg = /<meta http-equiv="refresh" content="0;url=([^"]+)[^>]+>/ ; const redirect = reg . exec ( res . body ) ; if ( redirect && redirect [ 1 ] ) res = await utils . get ( redirect [ 1 ] , jar , null , globalOptions ) . then ( utils . saveCookies ( jar ) ) ; const stuff = buildAPI ( globalOptions , res . body , jar ) ; ctx = stuff . ctx ; defaultFuncs = stuff . defaultFuncs ; api = stuff . api ; const form = { reason : 6 } ; log . info ( 'login' , 'Request to reconnect' ) ; await defaultFuncs . get ( 'https://www.facebook.com/ajax/presence/reconnect.php' , ctx . jar , form ) . then ( utils . saveCookies ( ctx . jar ) ) ; const presence = utils . generatePresence ( ctx . userID ) ; ctx . jar . setCookie ( 'presence=' + presence + '; path=/; domain=.facebook.com; secure' , 'https://www.facebook.com' ) ; ctx . jar . setCookie ( 'presence=' + presence + '; path=/; domain=.messenger.com; secure' , 'https://www.messenger.com' ) ; ctx . jar . setCookie ( 'locale=en_US; path=/; domain=.facebook.com; secure' , 'https://www.facebook.com' ) ; ctx . jar . setCookie ( 'locale=en_US; path=/; domain=.messenger.com; secure' , 'https://www.messenger.com' ) ; ctx . jar . setCookie ( 'a11y=' + utils . generateAccessibilityCookie ( ) + '; path=/; domain=.facebook.com; secure' , 'https://www.facebook.com' ) ; } ) ; if ( globalOptions . pageID ) { mainPromise = mainPromise . then ( async ( ) => { return await utils . get ( 'https://www.facebook.com/' + ctx . globalOptions . pageID + '/messages/?section=messages&subsection=inbox' , ctx . jar , null , globalOptions ) ; } ) . then ( async ( resData ) => { let url = utils . getFrom ( resData . body , 'window.location.replace("https:\\/\\/www.facebook.com\\' , '");' ) . split ( '\\' ) . join ( '' ) ; url = url . substring ( 0 , url . length - 1 ) ; return await utils . get ( 'https://www.facebook.com' + url , ctx . jar , null , globalOptions ) ; } ) ; } await mainPromise ; log . info ( 'login' , 'Done logging in.' ) ; return api ; } function buildAPI ( globalOptions , html , jar ) { const userIdCookies = jar . getCookies ( 'https://www.facebook.com' ) . filter ( cookie => cookie . key === 'c_user' ) ; if ( userIdCookies . length === 0 ) { throw new Error ( 'Error retrieving userID. This can be caused by a lot of things, including having wrong AppState credentials or getting blocked by Facebook for logging in from an unknown location. Try logging in with a browser to verify.' ) ; } const userID = userIdCookies [ 0 ] . value ; log . info ( 'login' , 'Logged in' ) ; const clientID = ( ( Math . random ( ) * 2147483648 ) | 0 ) . toString ( 16 ) ; const $ = cheerio . load ( html ) ; const fb_dtsg = $ ( 'input[name=fb_dtsg]' ) ?. attr ( 'value' ) ; const jazoest = $ ( 'input[name=jazoest]' ) ?. attr ( 'value' ) ; const ctx = { userID : userID , jar : jar , clientID : clientID , globalOptions : globalOptions , loggedIn : true , access_token : 'NONE' , clientMutationId : 0 , mqttClient : undefined , lastSeqId : 0 , syncToken : undefined , fb_dtsg , jazoest } ; const defaultFuncs = utils . makeDefaults ( html , userID , ctx ) ; const api = new Api ( defaultFuncs , ctx ) ; return { ctx , defaultFuncs , api } ; } /** Magic function */ function makeLogin ( jar , email , password , loginOptions ) { return async ( res ) => { const html = res . body ; const $ = cheerio . load ( html ) ; const jazoest = $ ( 'input[name=jazoest]' ) . attr ( 'value' ) ; const lsd = $ ( 'input[name=lsd]' ) . attr ( 'value' ) ; const publicKeyDataString = utils . getFrom ( html , 'pubKeyData:' , '}' ) + '}' ; const publicKeyData = { publicKey : utils . getFrom ( publicKeyDataString , 'publicKey:"' , '"' ) , keyId : utils . getFrom ( publicKeyDataString , 'keyId:' , '}' ) } ; const currentTime = Math . floor ( Date . now ( ) / 1000 ) . toString ( ) ; const form = { jazoest , lsd , email , login_source : 'comet_headerless_login' , next : '' , encpass : `#PWD_BROWSER:5:${ currentTime }:${ await require ( './lib/passwordHasher.js' ) ( publicKeyData , currentTime , password ) }` } ; const loginUrl = `https://www.facebook.com/login/?privacy_mutation_token=${ Buffer . from ( `{"type":0,"creation_time":${ currentTime },"callsite_id":381229079575946}` ) . toString ( 'base64' ) }` ; const willBeCookies = html . split ( '"_js_' ) ; willBeCookies . slice ( 1 ) . map ( function ( val ) { const cookieData = JSON . parse ( '["' + utils . getFrom ( val , '' , ']' ) + ']' ) ; jar . setCookie ( utils . formatCookie ( cookieData , 'facebook' ) , 'https://www.facebook.com' ) ; } ) ; log . info ( 'login' , 'Logging in...' ) ; return await utils . post ( loginUrl , jar , form , loginOptions ) . then ( async ( res ) => { utils . saveCookies ( jar ) ( res ) ; const headers = res . headers ; if ( ! res . body . includes ( 'window.location.replace' ) ) throw { error : 'Wrong username/password.' } ; const redirect = utils . getFrom ( res . body , 'window.location.replace("' , '")' ) ; log . info ( 'login' , `Redirected to ${ redirect }` ) ; if ( headers . location && headers . location . indexOf ( 'https://www.facebook.com/checkpoint/' ) > - 1 ) { log . info ( 'login' , 'You have login approvals turned on.' ) ; const nextURL = 'https://www.facebook.com/checkpoint/?next=https%3A%2F%2Fwww.facebook.com%2Fhome.php' ; return await utils . get ( headers . location , jar , null , loginOptions ) . then ( utils . saveCookies ( jar ) ) . then ( async ( res ) => { const html = res . body ; const $ = cheerio . load ( html ) ; let arr = [ ] ; $ ( 'form input' ) . map ( function ( i , v ) { arr . push ( { val : $ ( v ) . val ( ) , name : $ ( v ) . attr ( 'name' ) } ) ; } ) ; arr = arr . filter ( function ( v ) { return v . val && v . val . length ; } ) ; const form = utils . arrToForm ( arr ) ; if ( html . indexOf ( 'checkpoint/?next' ) > - 1 ) { throw { error : 'login-approval' , continue : async ( code ) => { form . approvals_code = code ; form [ 'submit[Continue]' ] = 'Continue' ; return await utils . post ( nextURL , jar , form , loginOptions ) . then ( utils . saveCookies ( jar ) ) . then ( async ( ) => { form . name_action_selected = 'save_device' ; return await utils . post ( nextURL , jar , form , loginOptions ) . then ( utils . saveCookies ( jar ) ) ; } ) . then ( async ( res ) => { const headers = res . headers ; if ( ! headers . location && res . body . indexOf ( 'Review Recent Login' ) > - 1 ) { throw { error : 'Something went wrong with login approvals.' } ; } const appState = utils . getAppState ( jar ) ; return await loginHelper ( { email , password } , loginOptions ) ; } ) . catch ( ( err ) => { throw err ; } ) ; } } ; } else { if ( ! loginOptions . forceLogin ) { throw { error : "Couldn't login. Facebook might have blocked this account. Please login with a browser or enable the option 'forceLogin' and try again." } ; } if ( html . indexOf ( 'Suspicious Login Attempt' ) > - 1 ) { form [ 'submit[This was me]' ] = 'This was me' ; } else { form [ 'submit[This Is Okay]' ] = 'This Is Okay' ; } return await utils . post ( nextURL , jar , form , loginOptions ) . then ( utils . saveCookies ( jar ) ) . then ( async ( ) => { form . name_action_selected = 'save_device' ; return await utils . post ( nextURL , jar , form , loginOptions ) . then ( utils . saveCookies ( jar ) ) ; } ) . then ( async ( res ) => { const headers = res . headers ; if ( ! headers . location && res . body . indexOf ( 'Review Recent Login' ) > - 1 ) { throw { error : 'Something went wrong with review recent login.' } ; } const appState = utils . getAppState ( jar ) ; return await loginHelper ( { email , password } , loginOptions ) ; } ) . catch ( ( e ) => { throw e ; } ) ; } } ) ; } return utils . get ( 'https://www.facebook.com/' , jar , null , loginOptions ) . then ( utils . saveCookies ( jar ) ) ; } ) ; } ; }