const {SolveCaptcha, TWO_CAPTCHA_CONNECTION_FAILED} = require("./SolveCaptcha"); const ReserveResultPojo = require("../models/ReserveResultPojo"); const BlackListContactPojo = require("../models/BlackListContactPojo"); const appointmentLogger = require("../utiles/LoggerUtils") const PublishType = require("../models/PublishType"); const { shell } = require('electron') const GeoCaptchaSolver = require("./GeoCaptchaSolver"); const SlidingCaptchaSolver = require("./SlidingCaptchaSolver"); // const RDV_URL = "http://192.168.0.41:8000/test_appointment.html" const RDV_URL = "https://rendezvousparis.hermes.com/client/register"; const BLANK_URL = "about:blank" const ERROR_CAPTCHA_UNSOLVABLE = "ERROR_CAPTCHA_UNSOLVABLE"; const COUNTRY_ID = "#phone_country" const PHONE_NUMBER = "#phone_number" const EMAIL_ID = "#email" const PREFER_STORE = "#prefer" const LAST_NAME = "#surname" const FIRST_NAME = "#name" const CGU_ID = "#cgu" const PROCESSING_ID = "#processing" const PASSPORT_ID = "#passport_id" const CAPTCHA_CONTAINER = "#captcha-container"; const TIME_OUT = 60 * 1000 * 4//4 mins const EMPTY_RESPONSE_ERROR = "ERR_EMPTY_RESPONSE" const MESSAGE_URL_VALIDATION_FR = "Nous avons envoyé un lien par e-mail." const MESSAGE_URL_VALIDATION_EN = "Please click on the link we sent by email" const DOUBLE_REQUEST_ERROR_MESSAGE = "A request with the same data has already been validated today." const DOUBLE_REQUEST_ERROR_MESSAGE_FR = "Une demande avec les données saisies a déjà été validée aujourd’hui." const TOO_MANY_REQUEST_ERROR_MESSAGE = "Due to a large number of requests" const TOO_MANY_REQUEST_ERROR_MESSAGE_FR = "Suite à un trop grand nombre de demandes" const CAPTCHA_ERROR_MESSAGE = "Error verifying captcha, please try again" const CAPTCHA_ERROR_MESSAGE_FR = "La vérification du captcha a échoué" const REGEX_RDV_URL = "https:\/\/rendezvousparis\.hermes\.com\/client\/register\/[A-Z0-9]+" const DEFAULT_STORE = 'faubourg'; const searchTexts = ['hermes+rdv+online+paris', 'hermes+rdv+enligne+paris', 'hermes+rdv+en+ligne+paris', 'hermes+rendezvous+en+ligne+paris', 'hermes+appointment+online+paris', 'hermes+appointment+online+paris', 'appointment+hermes+paris+on+line', 'hermes+rendez+vous+online+paris', 'hermes+rendez+vous+paris+en+ligne', 'hermes+rendez+vous+paris+enligne', 'hermes+rendez+vous+paris+online', 'online+appointment+hermes+paris', 'hermes+online+appointment+paris', 'paris+hermes+online+appointment'] function delay(delayInMs) { return new Promise(resolve => { setTimeout(() => { resolve(2); }, delayInMs); }); } function getRandom() { return Math.floor(Math.random() * 3); } function getRandomWaitTime() { return getRandom() * 1000 } function log(message) { appointmentLogger.log({level: "info", message: message}) } function logWithDevice(message, device) { appointmentLogger.log({level: "info", device: device.model(), message: message}) } class CommandorPage { constructor(contact, device, mongoManager, selectedStore = DEFAULT_STORE, audioAnalyse = false, alertBeep = false) { this.contact = contact; this.device = device; this.mongoManager = mongoManager; this.selectedStore = selectedStore; this.choosedStore = selectedStore this.isFillingFields = false; this.isTerminated = false; this.audioAnalyse = audioAnalyse; this.alertBeep = alertBeep; this.cguChecked = false; this.isNameInput = false; this.isEmailFilled = false; this.isCountryChoosen = false; this.isPhoneInput = false; } async loadPage() { // Connect to the device. log("loadPage() called"); // await this.device.shell('am force-stop com.android.chrome'); try { this.context = await this.device.launchBrowser(); // await context.clearCookies() // Use BrowserContext as usual. this.page = await this.context.newPage(); this.page.on("load", (loadedPage) => { this.onPageLoad(loadedPage) }) this.page.on("response", (response) => { this.onResponse(response) }) } catch (e) { log(e) this.isTerminated = true } try { const item = searchTexts[Math.floor(Math.random() * searchTexts.length)]; await this.page.goto("https://www.google.com/search?q=" + item + "&lr=lang_en", {timeout: 90 * 1000}); } catch (e) { log(e) this.isTerminated = true } try { if (this.page.url().includes("google")) this.page.locator('button >> nth=3').click() } catch (e) { log(e) } try { this.page.locator(':nth-match(:text("rendezvousparis"), 1)').click() } catch (e) { log(e) if (!this.page.url().includes(RDV_URL)) { this.isTerminated = true; } } let cancel const intervalTask = setInterval(() => { if (this.isTerminated) { log(this.device.model() + ":request terminated, will close device"); if (this.context !== undefined) this.context.close(); // this.page.close() // this.device.close() clearInterval(intervalTask) cancel() return this.context } else { if (this.page.url() === RDV_URL) { if (!this.isFillingFields) this.fillFields(this.page); } } }, 10 * 1000)//interval of 10 seconds await new Promise(function (fulfill, reject) { cancel = function () { fulfill(Promise.resolve()) } setTimeout(fulfill, TIME_OUT, 5) }).then(log) } async chooseCountry(page) { if (!page.isClosed()) { try { if (!this.isCountryChoosen) { await page.locator(COUNTRY_ID).focus(); await delay(getRandomWaitTime()) await page.click(COUNTRY_ID); await delay(getRandomWaitTime()) await page.selectOption(COUNTRY_ID, 'FR'); await delay(getRandomWaitTime()) this.isCountryChoosen = true; } } catch (e) { log(e); this.isTerminated = true; } } } async fillEmail(page) { try { if (!page.isClosed()) { if (!this.isEmailFilled) { await page.locator(EMAIL_ID).focus(); await delay(getRandomWaitTime()) await page.locator(EMAIL_ID).fill(this.contact.mail); this.isEmailFilled = true; } } } catch (e) { log(e) } } async inputPhoneNumber(page) { try { if (!page.isClosed()) { if (!this.isPhoneInput) { await page.locator(PHONE_NUMBER).focus(); await page.locator(PHONE_NUMBER).fill("+330" + this.contact.phoneNumber); this.isPhoneInput = true; } } } catch (e) { log(e); this.isTerminated = true; } } async inputName(page) { try { if (!page.isClosed()) { if (!this.isNameInput) { await page.locator(LAST_NAME).focus() await delay(getRandomWaitTime()) await page.locator(LAST_NAME).fill(this.contact.lastName) await page.locator(FIRST_NAME).focus() await delay(getRandomWaitTime()) await page.locator(FIRST_NAME).fill(this.contact.firstName) this.isNameInput = true } } } catch (e) { log(e); this.isTerminated = true } } async inputPassportId(page) { try { if (!page.isClosed()) { if (!this.isPasspordInput) { await page.locator(PASSPORT_ID).focus(); await delay(getRandomWaitTime()); await page.locator(PASSPORT_ID).fill(this.contact.passportNumber.toString()); this.isPasspordInput = true; } } } catch (e) { log(e); this.isTerminated = true; } } async checkCGU(page) { try { if (!page.isClosed()) { if (!this.cguChecked) { await page.locator(CGU_ID).focus() await page.locator(CGU_ID).click() await delay(getRandomWaitTime()) await page.locator(PROCESSING_ID).focus() await page.locator(PROCESSING_ID).click() this.cguChecked = true; } } } catch (e) { log(e); this.isTerminated = true; } } async chooseStore(page) { try { if (!page.isClosed()) { if (this.selectedStore !== "random") { await page.locator(PREFER_STORE).focus() await delay(1000) await page.click(PREFER_STORE); let stores = this.selectedStore.split(":") this.choosedStore = stores[Math.floor(Math.random() * stores.length)] await page.selectOption(PREFER_STORE, this.choosedStore); } } } catch (e) { log(e); this.isTerminated = true; } } async fillFields(page) { log("fillFields called") if (!this.isFillingFields) { this.isFillingFields = true; await this.chooseStore(page); await this.inputName(page); await this.chooseCountry(page); await this.inputPhoneNumber(page) await this.fillEmail(page) // await this.inputPhoneNumber(page) await this.inputPassportId(page) await this.checkCGU(page) await this.resolveCaptcha(page) await delay(10 * 1000) this.isFillingFields = false } } async clickValid(page) { await delay(getRandomWaitTime()) try { if (!this.page.isClosed()) { this.page.evaluate(() => { let element = document.getElementsByClassName("btn")[0]; if (typeof element !== 'undefined') document.getElementsByClassName("btn")[0].focus(); }) await delay(getRandomWaitTime()) if (!this.page.isClosed()) { try { this.page.evaluate(() => { document.getElementsByClassName("btn")[0].click(); }) } catch (e) { log(e) } } } } catch (e) { log(e) } } async resolveCaptcha(page) { if (RDV_URL.includes("192")) { await this.push_message_to_queue(PublishType.SUCCESS) return } //check whether there is captcha let pageContent = await page.content() let hasCaptcha = pageContent.includes("g-recaptcha-response") if (hasCaptcha) { await this.clickCheckbox() await delay(1000) this.captchaSolver = new SolveCaptcha(page); await this.captchaSolver.start((solution) => { log("solution is: " + solution); if (solution !== ERROR_CAPTCHA_UNSOLVABLE && solution !== TWO_CAPTCHA_CONNECTION_FAILED) { try { if (!page.isClosed()) { page.evaluate((solution) => { let element = document.getElementById("g-recaptcha-response"); if (element != null) document.getElementById("g-recaptcha-response").innerHTML = solution; }, solution) this.clickValid(); } } catch (e) { log(e) this.isTerminated = true; } } else { this.isTerminated = true; } }) } else { await this.clickValid(); } } async isBlocked() { let iframeHandler = await this.page.frameLocator("body > iframe"); let captcha_container = await iframeHandler.locator(CAPTCHA_CONTAINER) let html = await captcha_container.innerHTML() console.log("audio_tag: " + html); return html.includes("You have been blocked") || html.includes("Vous avez été bloqué") } async onPageLoad(currentPage) { try { let content = await currentPage.content(); let captcha_url = "geo.captcha-delivery.com/captcha"; if (content.toString().includes(captcha_url)) { if (this.audioAnalyse) { await this.checkAudioBtn(); } if (this.alertBeep) { for (let i = 0; i < 15; i++) { await delay(1000) shell.beep() } } logWithDevice("发现datadome", this.device); } else if (content.includes("502 Bad Gateway")) { logWithDevice("502 Bad Gateway found", this.device) await this.page.reload() } else if (currentPage.url().includes("sorry")) { await this.resetBrowser() } else if (content.includes("PROXY_CONNECTION_FAILED")) { logWithDevice("PROXY_CONNECTION_FAILED, will reload page", this.device); await delay(2000) await this.page.reload() } else if (content.includes("ERR_NETWORK_CHANGED")) { logWithDevice("ERR_NETWORK_CHANGED, will reload page", this.device); await delay(2000) await this.page.reload() } else if (content.includes("ERR_TIMED_OUT")) { logWithDevice("ERR_TIMED_OUT, will reload page", this.device); await delay(2000) await this.page.reload() } else if (content.includes("408 Request Time-out")) { logWithDevice("Request Time-out, will reload page", this.device); await delay(2000) await this.page.reload() } else { if (currentPage.url() === RDV_URL) { await this.fillFields(this.page); // if (this.isFillingFields) // await this.getErrors() } else { if (content.includes(MESSAGE_URL_VALIDATION_FR) || content.includes(MESSAGE_URL_VALIDATION_EN)) { log("successful"); await this.push_message_to_queue(PublishType.SUCCESS); } else if (content.includes(EMPTY_RESPONSE_ERROR)) { log("EMPTY_RESPONSE_ERROR error received, will quit") this.isTerminated = true } else { // try to get errors await this.getErrors() } } } } catch (e) { log(e) } } async saveCookies() { log("saveCookies() called.") try { let cookies = await this.page.context().cookies(); let cookiesInJson = []; cookies.forEach((cookie) => { cookiesInJson.push(JSON.stringify(cookie)) }) await require("fs").writeFileSync(this.contact.mail + '.txt', cookiesInJson.join('\n')); } catch (e) { console.log(e) } } async checkAudioBtn() { let isBlocked = await this.isBlocked() if (!isBlocked) { //try to sliding capthca at first let slidingCaptchaSolver = new SlidingCaptchaSolver(this.device); await slidingCaptchaSolver.solve(this.page, async (isSuccessful) => { console.log("check isAlwaysBlocked") let isAlwaysBlocked = await this.isBlocked(); if (isAlwaysBlocked) { await this.resolveByAudio(); } }) } else { log("audioBtn not found") console.log("audioBtn not found") console.log("we are blocked") await this.resetBrowser() } } async resolveByAudio() { let audioBtn = await this.page.frameLocator("iframe").locator("#captcha__audio__button"); log("audioBtn found") audioBtn.click() let captchaSolver = new GeoCaptchaSolver(this.page, this.device, this.isTerminated) await captchaSolver.solve((isSuccessful) => { if (!isSuccessful) { this.isTerminated = true } }) } async onResponse(response) { let rex = new RegExp(REGEX_RDV_URL) log("onResponse with url:" + response.url()) // log("onResponse with url:" + response.body()) if (rex.test(response.url())) { log("rdv url found:" + response.url()) // save cookies await this.saveCookies(); await this.push_message_to_db(PublishType.SUCCESS, response.url()) } } async push_message_to_queue(publishType) { let url = this.page.url(); await this.push_message_to_db(publishType, url) } async push_message_to_db(publishType, url) { let splitedUrl = url.split("/"); let id = splitedUrl[splitedUrl.length - 1]; if (url === "https://rendezvousparis.hermes.com/client/welcome") { return } // save to mongoDb let reserve = ReserveResultPojo.create_from_contact(this.contact, id, url, this.choosedStore, publishType); reserve.source_from = this.device.model(); await this.mongoManager.saveReserveToDb(reserve.to_mongo_dict()) await this.deleteFromBlackList() // await this.resetBrowser() this.isTerminated = true } async saveToBlackList() { await this.mongoManager.saveBlackListToDb(new BlackListContactPojo(this.contact)) // await this.resetBrowser() this.isTerminated = true } async deleteFromBlackList() { await this.mongoManager.removeFromBlackList(this.contact) } async getErrors() { if (this.page.url() === BLANK_URL) { this.isTerminated = true; } else { try { let errorItem = this.page.locator("div.alert"); if (errorItem) { let errorContent = await errorItem.innerHTML(); await this.handleError(errorContent); } } catch (e) { log(e); } } } async clickCheckbox() { try { // let errorItem = await this.page.click("#recaptcha-anchor > div.recaptcha-checkbox-border") await this.page.frameLocator('[title="reCAPTCHA"]').getByRole('checkbox', {name: 'I\'m not a robot'}).click(); } catch (e) { log(e); } } async handleError(errorContent) { log("handle error:" + errorContent); if (errorContent.includes(DOUBLE_REQUEST_ERROR_MESSAGE) || errorContent.includes(DOUBLE_REQUEST_ERROR_MESSAGE_FR)) { this.isTerminated = true; } else if (errorContent.includes(TOO_MANY_REQUEST_ERROR_MESSAGE) || errorContent.includes(TOO_MANY_REQUEST_ERROR_MESSAGE_FR)) { //add contact to black list and set terminated the task log("handle error: will save to black list db"); await this.saveToBlackList() } else if (errorContent.includes(CAPTCHA_ERROR_MESSAGE) || errorContent.includes(CAPTCHA_ERROR_MESSAGE_FR)) { this.isTerminated = true; } } async resetBrowser() { console.log("will reset browser") await this.device.shell("pm clear com.android.chrome") await delay(1000) await this.device.shell("am set-debug-app --persistent com.android.chrome") await delay(1000) await this.device.shell("pm am start -n com.android.chrome/com.google.android.apps.chrome.Main") await delay(1000) this.isTerminated = true } } module.exports = CommandorPage