import puppeteer , { ElementHandle } from 'puppeteer' ; import fs from 'fs' ; import selectors from '../utils/selectors' ; import Options from './options' ; import InitialisationError from '../errors/initialisationError' ; import { autoScroll , generateFacebookGroupURLById , getOldPublications , } from '../utils/fbHelpers' ; import GroupPost from './groupPost' ; import TwoFARequiredError from '../errors/twoFARequiredError' ; declare global { interface Window { posts : HTMLElement [ ] ; } } export default class Facebook { private url = 'https://facebook.com' ; private altUrl = 'https://www.facebook.com' ; private config : Options | undefined ; private browser : puppeteer . Browser | undefined ; private page : puppeteer . Page | undefined ; private cookiesFilePath : string ; public constructor ( config , browser , page , cookiesFileName ) { this . config = config ; this . browser = browser ; this . page = page ; this . cookiesFilePath = cookiesFileName ; } param options param cookiesFilePath public static async init ( options , cookiesFilePath = 'fbjs_cookies.json' ) { const browserOptions = { headless : options . headless , args : [ '--no-sandbox' , '--disable-setuid-sendbox' , '--disable-dev-shm-usage' , '--disable-accelerated-2d-canvas' , '--disable-gpu' , ] , } ; if ( process . arch === 'arm' || process . arch === 'arm64' ) { browserOptions . executablePath = 'chromium-browser' ; } const browser = await puppeteer . launch ( browserOptions ) ; /**
         * We need an incognito browser to avoid notification
         * and location permissions of Facebook
         *
         */ const incognitoContext = await browser . createIncognitoBrowserContext ( ) ; const page = await incognitoContext . newPage ( ) ; await page . setUserAgent ( 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3419.0 Safari/537.36' ) ; if ( options . useCookies && fs . existsSync ( cookiesFilePath ) ) { const cookiesString = fs . readFileSync ( cookiesFilePath ) ; const cookies = JSON . parse ( cookiesString . toString ( ) ) ; await page . setCookie ( ... cookies ) ; } return new Facebook ( options , browser , page , cookiesFilePath ) ; } private private async disableAssets ( ) { if ( this . page === undefined ) { throw new InitialisationError ( ) ; } await this . page . setRequestInterception ( true ) ; const blockResources = [ 'image' , 'media' , 'font' , 'textrack' , 'object' , 'beacon' , 'csp_report' , 'imageset' , ] ; this . page . on ( 'request' , ( request ) => { const rt = request . resourceType ( ) ; if ( blockResources . indexOf ( rt ) > 0 || request . url ( ) . match ( /\.((jpe?g)|png|gif)/ ) != null ) { request . abort ( ) ; } else { request . continue ( ) ; } } ) ; } param { string } authCode public async enterAuthCode ( authCode ) { const authCodeInputSelector = '//input[contains(concat(" ", normalize-space(@name), " "), " approvals_code")]' ; const authCodeContinueButtonSelector = '//button[contains(concat(" ", normalize-space(@id), " "), " checkpointSubmitButton")]' ; if ( this . page === undefined || this . config === undefined ) { throw new InitialisationError ( ) ; } await this . page . waitForXPath ( authCodeInputSelector ) ; await ( await this . page . $x ( authCodeInputSelector ) ) [ 0 ] . focus ( ) ; await this . page . keyboard . type ( authCode ) ; await this . page . waitForXPath ( authCodeContinueButtonSelector ) ; await ( await this . page . $x ( authCodeContinueButtonSelector ) ) [ 0 ] . click ( ) ; await this . page . waitForXPath ( authCodeContinueButtonSelector ) ; await ( await this . page . $x ( authCodeContinueButtonSelector ) ) [ 0 ] . click ( ) ; if ( this . config . useCookies ) { const cookies = await this . page . cookies ( ) ; fs . writeFileSync ( this . cookiesFilePath , JSON . stringify ( cookies , null , 2 ) ) ; } do { await this . page . waitForNavigation ( { timeout : 10000000 } ) ; const u = new URL ( this . page . url ( ) ) ; if ( u . pathname === '/' ) { break ; } await this . page . waitForXPath ( authCodeContinueButtonSelector ) ; await ( await this . page . $x ( authCodeContinueButtonSelector ) ) [ 0 ] . click ( ) ; } while ( this . page . url ( ) !== this . url && this . page . url ( ) !== this . altUrl ) ; if ( this . config . disableAssets ) { await this . disableAssets ( ) ; } if ( this . config . useCookies ) { const cookies = await this . page . cookies ( ) ; if ( this . cookiesFilePath === undefined ) { this . cookiesFilePath = 'fbjs_cookies' ; } fs . writeFileSync ( `./${ this . cookiesFilePath . replace ( /\.json$/g , '' ) }.json` , JSON . stringify ( cookies , null , 2 ) ) ; } } /**
     * Function closes everything
     */ public async close ( ) { this . page ?. close ( ) ; this . browser ?. close ( ) ; } param username param password public async login ( username , password ) { if ( this . page === undefined || this . config === undefined ) { throw new InitialisationError ( ) ; } await this . page . goto ( this . url ) ; try { await this . page . waitForXPath ( '//button[@data-cookiebanner="accept_button"]' ) ; const acceptCookiesButton = ( await this . page . $x ( '//button[@data-cookiebanner="accept_button"]' ) ) [ 0 ] ; await this . page . evaluate ( ( el ) => { el . focus ( ) ; el . click ( ) ; } , acceptCookiesButton ) ; } catch { ( ( ) => { } ) ( ) ; } /**
         * Waiting for login form JQuery selector to avoid
         * that forms elements to be not found
         * */ await this . page . waitForSelector ( selectors . login_form . parent ) ; await this . page . focus ( selectors . login_form . email ) ; await this . page . focus ( selectors . login_form . email ) ; await this . page . keyboard . type ( username ) ; await this . page . focus ( selectors . login_form . password ) ; await this . page . keyboard . type ( password ) ; await this . page . waitForXPath ( '//button[@data-testid="royal_login_button"]' ) ; const [ loginButton ] = await this . page . $x ( '//button[@data-testid="royal_login_button"]' ) ; await this . page . evaluate ( ( el ) => { el . click ( ) ; } , loginButton ) ; try { await this . page . waitForXPath ( '//form[contains(concat(" ", normalize-space(@class), " "), " checkpoint")]' ) ; } catch ( e ) { await this . page . waitForXPath ( '//div[@data-pagelet="Stories"]' ) ; if ( this . config . disableAssets ) { await this . disableAssets ( ) ; } if ( this . config . useCookies ) { const cookies = await this . page . cookies ( ) ; if ( this . cookiesFilePath === undefined ) { this . cookiesFilePath = 'fbjs_cookies' ; } fs . writeFileSync ( `./${ this . cookiesFilePath . replace ( /\.json$/g , '' ) }.json` , JSON . stringify ( cookies , null , 2 ) ) ; } return ; } throw new TwoFARequiredError ( ) ; } param groupId param outputFileName public async getGroupPosts ( groupId , outputFileName , callback ? , save = true ) { if ( this . page === undefined || this . config === undefined ) { throw new InitialisationError ( ) ; } const groupUrl = generateFacebookGroupURLById ( groupId ) ; await this . page . goto ( groupUrl , { timeout : 600000 , waitUntil : 'domcontentloaded' , } ) ; if ( outputFileName === undefined ) { outputFileName = `${ this . config . output + groupId }.json` ; } param postData const savePost = ( postData ) : void => { const allPublicationsList = getOldPublications ( outputFileName ! ) ; allPublicationsList . push ( postData ) ; if ( save ) { fs . writeFileSync ( outputFileName ! , JSON . stringify ( allPublicationsList , undefined , 4 ) , { encoding : 'utf8' } ) ; } } ; this . page . evaluate ( autoScroll ) ; /**
         * Waiting for the group feed container to continue
         * and to avoid the selector not found error.
         * Note that we ignore any posts outside this container
         * specifically announcements, because they don't follow
         * the same sorting method as the others.
         * */ await this . page . waitForSelector ( selectors . facebook_group . group_feed_container ) ; /**
         * Handle new added posts
         */ const handlePosts = async ( ) => { const post = await this . page ?. evaluateHandle ( ( ) => window . posts . shift ( ) ) ; const postData = await this . parsePost ( < ElementHandle > post ) ; if ( callback !== undefined && callback !== null ) { callback ( postData ) ; } savePost ( postData ) ; } ; this . page . exposeFunction ( 'handlePosts' , handlePosts ) ; this . page . evaluate ( ( cssSelectors ) => { window . posts = [ ] ; const target = < HTMLElement > document . querySelector ( cssSelectors . facebook_group . group_feed_container ) ; const observer = new MutationObserver ( ( mutations ) => { for ( let i = 0 ; i < mutations . length ; i += 1 ) { for ( let j = 0 ; j < mutations [ i ] . addedNodes . length ; j += 1 ) { const addedNode = < HTMLElement > mutations [ i ] . addedNodes [ j ] ; const postElm = < HTMLElement > addedNode . querySelector ( cssSelectors . facebook_post . post_element ) ; if ( postElm ) { window . posts . push ( postElm ) ; handlePosts ( ) ; } } } } ) ; observer . observe ( target , { childList : true } ) ; } , selectors ) ; } param post public async parsePost ( post ) { if ( this . page === undefined || this . config === undefined ) { throw new InitialisationError ( ) ; } const { authorName , content } = await this . page . evaluate ( async ( postElm , cssSelectors ) : Promise < any > => { let postAuthorElm ; postAuthorElm = < HTMLElement > postElm . querySelector ( cssSelectors . facebook_post . post_author ) ; let postAuthorName ; if ( postAuthorElm ) { postAuthorName = postAuthorElm . innerText ; } else { postAuthorElm = < HTMLElement > postElm . querySelector ( cssSelectors . facebook_post . post_author2 ) ; postAuthorName = postAuthorElm . innerText ; } const postContentElm = < HTMLElement > postElm . querySelector ( cssSelectors . facebook_post . post_content ) ; let postContent ; if ( postContentElm ) { const expandButton = < HTMLElement > postContentElm . querySelector ( cssSelectors . facebook_post . post_content_expand_button ) ; if ( expandButton ) { postContent = await new Promise ( ( res ) => { const observer = new MutationObserver ( ( ) => { observer . disconnect ( ) ; res ( postContentElm . innerText ) ; } ) ; observer . observe ( postContentElm , { childList : true , subtree : true } ) ; expandButton . click ( ) ; } ) ; } else { postContent = postContentElm . innerText ; } } else { postContent = '' ; } return { authorName : postAuthorName , content : postContent , } ; } , post , selectors ) ; const submission = { author : authorName , post : content , } ; return submission ; } }