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 RDV_URL = "http://192.168.0.13: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', 'hermes+rendezvous', 'hermes+appointment', 'hermes+appointment+online', 'appointment+hermes', 'hermes+rendez+vous','hermes+rendez+vous+paris', 'online+appointment+hermes', 'hermes+online+appointment', '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}) } 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'); const context = await this.device.launchBrowser(); // await context.clearCookies() // Use BrowserContext as usual. this.page = await context.newPage(); this.page.on("load", (loadedPage) => { this.onPageLoad(loadedPage) }) this.page.on("response", (response) => { this.onResponse(response) }) 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}); // await this.page.goto(RDV_URL, {timeout: 90 * 1000}); } catch (e) { log(e) this.isTerminated = true } try { if (this.page.url().includes("google")) this.page.locator('button >> nth=3').click() // this.page.locator('button:has-text("Tout accepter")').click() } catch (e) { log(e) } // try { // this.page.locator('button:has-text("Accept all")').click() // } catch (e) { // log(e) // } try { this.page.locator(':nth-match(:text("Online Appointment"), 1)').click() } catch (e) { log(e) this.isTerminated = true } let cancel const intervalTask = setInterval(() => { if (this.isTerminated) { log(this.device.model() + ":request terminated, will close device"); context.close(); // this.page.close() this.device.close() clearInterval(intervalTask) cancel() return context } }, 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("0" + 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) { 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") } 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() } } log("发现datadome"); } else if (currentPage.url().includes("sorry")) { await this.resetBrowser() } 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 checkAudioBtn() { let isBlocked = await this.isBlocked() if (!isBlocked) { 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 } }) } else { log("audioBtn not found") console.log("audioBtn not found") console.log("we are blocked") // await this.resetBrowser() } } async onResponse(response) { // let rex = new RegExp(REGEX_RDV_URL) // log("onResponse with url:" + response.url()) // if (rex.test(response.url())) { // log("rdv url found:" + response.url()) // await this.push_message_to_db(PublishType.SUCCESS, response.url()) // } } async push_message_to_queue(publishType) { let url = this.page.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, this.page.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 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