diff --git a/src/mail_validator.js b/src/mail_validator.js new file mode 100644 index 0000000..b1e554c --- /dev/null +++ b/src/mail_validator.js @@ -0,0 +1,37 @@ +const {MongoManager} = require("./workers/mongo_manager"); +const {_android: android} = require("playwright"); +const alert = require("alert"); +const LinkValidator = require("./workers/LinkValidator"); +const mongoManager = new MongoManager(); + +mongoManager.connect().then(r => { + android.devices().then((devices) => { + if (devices.length === 0) { + alert("未找到连接的设备"); + return + } + mongoManager.getAllLinkToValidate().then(list => { + let segmentNumber = list.length / devices.length; + console.log("connected device number:" + devices.length) + console.log("segmentNumber:" + segmentNumber) + for (let i = 0; i < devices.length; i++) { + startWithList(list.slice(i * segmentNumber, segmentNumber * (i + 1)), devices[i]).then(result => { + console.log("stop") + }) + } + }) + + }) +}); + +async function startWithList(listOfUrls, device) { + await listOfUrls.reduce(async (promise, urlToValidate) => { + // This line will wait for the last async function to finish. + // The first iteration uses an already resolved Promise + // so, it will immediately continue. + await promise; + let commandor = new LinkValidator(device, urlToValidate.url, mongoManager); + await commandor.loadPage() + console.log("next"); + }, Promise.resolve()); +} \ No newline at end of file diff --git a/src/workers/GeoCaptchaSolver.js b/src/workers/GeoCaptchaSolver.js index ee0c445..4bcf1b4 100644 --- a/src/workers/GeoCaptchaSolver.js +++ b/src/workers/GeoCaptchaSolver.js @@ -123,7 +123,6 @@ class GeoCaptchaSolver { 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 } } diff --git a/src/workers/LinkValidator.js b/src/workers/LinkValidator.js new file mode 100644 index 0000000..21d2109 --- /dev/null +++ b/src/workers/LinkValidator.js @@ -0,0 +1,227 @@ +const {SolveCaptcha, TWO_CAPTCHA_CONNECTION_FAILED} = require("./SolveCaptcha"); +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.41:8000/test_appointment.html" +BLANK_URL = "about:blank" +const ERROR_CAPTCHA_UNSOLVABLE = "ERROR_CAPTCHA_UNSOLVABLE"; + +const TIME_OUT = 60 * 1000 * 4//4 mins + +const CONFIRMED_MESSAGE = "Your request for a Leather Goods appointment has been registered" +const CONFIRMED_MESSAGE_FR = "Votre demande de rendez-vous Maroquinerie a bien été enregistrée et nous vous en remercions." +const CONFIRMED_MESSAGE_CN = "您的皮具预约申请已登记" +const SORRY_SENTENCE_FR = "nous sommes sincèrement désolés de n'avoir pu vous satisfaire cette fois-ci" +const DOUBLE_REQUEST_ERROR_MESSAGE_FR = "Une demande avec les données saisies a déjà été validée aujourd’hui." +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 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é" +REGEX_RDV_URL = "https:\/\/rendezvousparis\.hermes\.com\/client\/register\/[A-Z0-9]+" +const PLAY_AUDIO_BTN_ID = ".audio-captcha-play-button" +const homedir = require('os').homedir(); +const CAPTCHA_CONTAINER = "#captcha-container"; +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 LinkValidator { + constructor(device, urlToValidate, mongoManager) { + this.device = device; + this.mongoManager = mongoManager; + this.isTerminated = false; + this.urlToValidate = urlToValidate; + } + + async loadPage() { + // Connect to the device. + log("loadPage() called for url " + this.urlToValidate); + // 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 { + await this.page.goto(this.urlToValidate, {timeout: 90 * 1000}); + } catch (e) { + log(e) + this.isTerminated = true + } + + let cancel + const intervalTask = setInterval(() => { + if (this.isTerminated) { + log("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 onPageLoad(currentPage) { + try { + let content = await currentPage.content(); + let captcha_url = "geo.captcha-delivery.com/captcha"; + if (content.toString().includes(captcha_url)) { + log("发现datadome"); + await this.checkAudioBtn(); + } else { + if (content.includes(CONFIRMED_MESSAGE) || content.includes(CONFIRMED_MESSAGE_FR) || content.includes(CONFIRMED_MESSAGE_CN)) { + 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 audioBtn = await this.page.frameLocator("iframe").locator("#captcha__audio__button"); + let isBlocked = await this.isBlocked() + if (!isBlocked) { + log("audioBtn found") + if (!this.isTerminated) { + try { + audioBtn.click() + let captchaSolver = new GeoCaptchaSolver(this.page, this.device) + await captchaSolver.solve((isSuccessful) => { + if (!isSuccessful) { + this.isTerminated = true + } + }) + } catch (e) { + this.isTerminated = true + log(e) + } + } + } else { + log("audioBtn not found") + console.log("audioBtn not found") + console.log("we are blocked") + // + await this.resetBrowser() + } + } + + 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 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(); + if (url === "https://rendezvousparis.hermes.com/client/welcome") { + return + } + // save to mongoDb + await this.mongoManager.linkValidatedFor(url) + await this.mongoManager.removeFromToValidateList(this.urlToValidate) + // await this.resetBrowser() + this.isTerminated = true + } + + 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"); + } 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 = LinkValidator \ No newline at end of file diff --git a/src/workers/mongo_manager.js b/src/workers/mongo_manager.js index 4035a45..12327f0 100644 --- a/src/workers/mongo_manager.js +++ b/src/workers/mongo_manager.js @@ -4,9 +4,8 @@ const BLACK_LIST = "BLACK_LIST" const DB_NAME = "appointment" const COLLECTION_NAME = "appointment" const ACCEPTED_APPOINTMENT_LIST = "ACCEPTED_APPOINTMENT_LIST" -const EMAIL_LIST = "EMAIL_LIST" const BLACK_LIST_COLLECTION_NAME = "BLACK_LIST" - +const LINKS_TO_VALIDATE = "LINKS_TO_VALIDATE" const {MongoClient} = require('mongodb'); const ReserveResultPojo = require("../models/ReserveResultPojo"); require("../models/ContactPojo"); @@ -59,9 +58,30 @@ class MongoManager { await this.db.collection(BLACK_LIST_COLLECTION_NAME).deleteOne(query); } + async removeFromToValidateList(url) { + const query = {_id: url}; + await this.db.collection(LINKS_TO_VALIDATE).deleteOne(query); + } + async getAllBlackedListItems() { return await this.db.collection(BLACK_LIST_COLLECTION_NAME).find().toArray() } + + async linkValidatedFor(urlValidated) { + let splitedUrl = urlValidated.split("/"); + let id = splitedUrl[splitedUrl.length - 1]; + let collectionName = formatDate(new Date()); + return await this.db.collection(collectionName).updateOne({'_id': id,}, { + "$set": { + "url_validated": true, + "validated_at": new Date().toLocaleString() + } + }, {upsert: false}) + } + + async getAllLinkToValidate() { + return await this.db.collection(LINKS_TO_VALIDATE).find().toArray() + } }