diff --git a/package.json b/package.json index f7452eb..b642dd7 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "texas-dps-scheduler", - "version": "4.0.0-beta1", + "version": "4.0.0-beta2", "description": "Texas DPS Automatic Scheduler", "main": "dist/index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "tsc", "start": "ts-node --transpile-only src/index.ts", - "eslint": "eslint src/**/*.ts" + "eslint": "eslint src/**/*.ts", + "dev-token": "cross-env HEADLESS=false NODE_ENV=development ts-node src/Browser/index.ts" }, "repository": { "type": "git", @@ -26,6 +27,7 @@ "@types/prompts": "^2.4.9", "@typescript-eslint/eslint-plugin": "^7.17.0", "@typescript-eslint/parser": "^7.17.0", + "cross-env": "^7.0.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", @@ -40,6 +42,11 @@ "js-yaml": "^4.1.0", "p-queue": "6.6.2", "prompts": "^2.4.2", + "puppeteer": "^22.15.0", + "puppeteer-extra": "^3.3.6", + "puppeteer-extra-plugin-adblocker": "^2.13.6", + "puppeteer-extra-plugin-anonymize-ua": "^2.4.6", + "puppeteer-extra-plugin-stealth": "^2.11.2", "random-useragent": "^0.5.0", "tslib": "^2.6.3", "undici": "^6.19.4", diff --git a/src/Browser/index.ts b/src/Browser/index.ts new file mode 100644 index 0000000..1015cf7 --- /dev/null +++ b/src/Browser/index.ts @@ -0,0 +1,106 @@ +import puppeteer from 'puppeteer-extra'; +import { executablePath } from 'puppeteer'; +import * as log from '../Log'; + +// This plugin prevent bot detection +import StealthPlugin from 'puppeteer-extra-plugin-stealth'; +// This plugin anonymize user agent +import AnonymizeUA from 'puppeteer-extra-plugin-anonymize-ua'; +// This plugin use adblocker (bc imleague was bloated with ads!!!!) +import AdblockerPlugin from 'puppeteer-extra-plugin-adblocker'; + +puppeteer.use(AdblockerPlugin({ blockTrackers: true })); +puppeteer.use(StealthPlugin()); +puppeteer.use(AnonymizeUA()); + +export const getAuthToken = async (): Promise => { + try { + // Launch brower instance + const browser = await puppeteer.launch({ + headless: process.env.HEADLESS?.toLowerCase() == 'false' ? false : 'shell', + slowMo: 10, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + executablePath: executablePath(), + timeout: 0, + }); + + const [page] = await browser.pages(); + await page.goto('https://public.txdpsscheduler.com/'); + + // English button + await page.waitForSelector('.container > button'); + await page.click('.container > button'); + + await page.waitForNetworkIdle(); + + // Personal infomation form + await page.waitForSelector('.v-card__text'); + + await page.evaluate(() => { + const vcardText = document.querySelector('.v-card__text'); + if (vcardText) { + const inputs = document.querySelectorAll('.v-input'); + if (inputs.length >= 4) { + for (let i = 1; i < 5; i++) { + const input = inputs[i].querySelector('input') as HTMLInputElement; + switch (i) { + // First name and last name + case 1: + case 2: + input.value = 'test'; + break; + // Date of birth + case 3: + input.value = '01/01/2001'; + break; + // Last 4 digits of SSN + case 4: + input.value = '1111'; + break; + } + // Dispatch an input event to trigger any listeners + const event = new Event('input', { bubbles: true }); + input.dispatchEvent(event); + } + } + } + }); + + await page.setRequestInterception(true); + log.dev('Request interception enabled'); + + const authTokenPromise = new Promise(async resolve => { + // Listen for network requests + page.on('request', async request => request.continue()); + + // Listen for network responses + page.on('response', async response => { + const url = response.url(); + log.dev(`URL: ${url}`); + if (url === 'https://apptapi.txdpsscheduler.com/api/auth' && response.request().method() == 'POST') { + const token = await response.text(); + resolve(token); + } + }); + + // Click the login button + await page.waitForSelector('.v-card__actions > button'); + await page.click('.v-card__actions > button'); + }); + + // Wait for the auth token + const authToken = (await authTokenPromise) as string; + + // Close the browser + await browser.close(); + + log.dev(`Auth token: ${authToken}`); + return authToken; + } catch (err) { + log.error('Error while getting auth token: ', err as Error); + log.info('Try to get auth token again or manual set it in config.yml'); + process.exit(1); + } +}; + +if (process.env.NODE_ENV === 'development') getAuthToken(); diff --git a/src/Client/index.ts b/src/Client/index.ts index a5cd9c7..5b9c4d4 100644 --- a/src/Client/index.ts +++ b/src/Client/index.ts @@ -5,6 +5,7 @@ import parseConfig from '../Config'; import * as log from '../Log'; import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; +import { getAuthToken } from '../Browser'; dayjs.extend(isBetween); import prompts from 'prompts'; import type { EligibilityPayload } from '../Interfaces/Eligibility'; @@ -54,6 +55,11 @@ class TexasScheduler { } public async run() { + if (!this.config.appSettings.authToken || this.config.appSettings.authToken.length === 0) { + log.info('Auth Token is not set, requesting one...'); + this.config.appSettings.authToken = await getAuthToken(); + log.info(`Auth Token: ${this.config.appSettings.authToken}`); + } this.existBooking = await this.checkExistBooking(); const { exist, response } = this.existBooking; if (exist) { diff --git a/src/Log/index.ts b/src/Log/index.ts index a69bffa..56ea923 100644 --- a/src/Log/index.ts +++ b/src/Log/index.ts @@ -19,6 +19,9 @@ const error = (message = 'Unknown error', err?: Error) => { const info = (message: string) => msg(console.info, message); +// Development mode logging +const dev = (message: string) => (process.env.NODE_ENV === 'development' ? msg(console.info, message) : null); + const warn = (message: string) => msg(console.warn, `${yellow('WARNING ->')} ${message}`); -export { error, info, warn }; +export { error, info, warn, dev };