diff --git a/src/common/reserver.ts b/src/common/reserver.ts index 685457c..81f1a74 100644 --- a/src/common/reserver.ts +++ b/src/common/reserver.ts @@ -1,5 +1,6 @@ +import { asyncLocalStorage as l } from './logger' import { Reservation } from './reservation' -import { Runner } from './runner' +import { LoggableError, Runner } from './runner' let runner: Runner | undefined const getRunner = () => { @@ -12,16 +13,25 @@ const getRunner = () => { return runner } -export const reserve = async (reservation?: Reservation) => { +export const reserve = async (reservation?: Reservation): Promise => { let reservationToPerform = reservation if (!reservationToPerform) { + l.getStore()?.debug('No reservation provided, fetching first in database') reservationToPerform = (await Reservation.fetchFirst()) || undefined } if (!reservationToPerform) { - return + l.getStore()?.info('No reservation to perform') + return true } - + + l.getStore()?.debug('Trying to perform reservation', { reservationToPerform }) const runner = getRunner() - await runner.run(reservationToPerform) + try { + await runner.run(reservationToPerform) + return true + } catch (error) { + l.getStore()?.error('Failed to perform reservation', { error: (error as LoggableError).toString() }) + return false + } } diff --git a/src/common/runner.ts b/src/common/runner.ts index a614f2d..5e2ec4d 100644 --- a/src/common/runner.ts +++ b/src/common/runner.ts @@ -8,7 +8,7 @@ import puppeteer, { LaunchOptions, Page, } from 'puppeteer' -import { asyncLocalStorage } from './logger' +import { asyncLocalStorage as l } from './logger' import { Opponent, Reservation } from './reservation' export class Runner { @@ -26,41 +26,59 @@ export class Runner { this.options = options } - public async run(reservation: Reservation): Promise { - asyncLocalStorage.getStore()?.debug('Launching browser') - if (!this.browser) { - this.browser = await puppeteer.launch(this.options) + public async run(reservation: Reservation) { + l.getStore()?.debug('Launching browser') + try { + if (!this.browser) { + this.browser = await puppeteer.launch(this.options) + } + } catch (error) { + throw new PuppeteerBrowserLaunchError(error as Error) } - this.page = await this.browser?.newPage() + + try { + this.page = await this.browser?.newPage() + } catch (error) { + throw new PuppeteerNewPageError(error as Error) + } + await this.login(reservation.user.username, reservation.user.password) - return this.makeReservation(reservation) + await this.makeReservation(reservation) } private async login(username: string, password: string) { - asyncLocalStorage.getStore()?.debug('Logging in', { username }) - await this.page?.goto('https://squashcity.baanreserveren.nl/') + l.getStore()?.debug('Logging in', { username }) + await this.page + ?.goto('https://squashcity.baanreserveren.nl/') + .catch((e: Error) => { + throw new RunnerLoginNavigationError(e) + }) await this.page ?.waitForSelector('input[name=username]') .then((i) => i?.type(username)) - await this.page?.$('input[name=password]').then((i) => i?.type(password)) - await this.page?.$('button').then((b) => b?.click()) + .catch((e: Error) => { + throw new RunnerLoginUsernameInputError(e) + }) + await this.page + ?.$('input[name=password]') + .then((i) => i?.type(password)) + .catch((e: Error) => { + throw new RunnerLoginPasswordInputError(e) + }) + await this.page + ?.$('button') + .then((b) => b?.click()) + .catch((e: Error) => { + throw new RunnerLoginSubmitError(e) + }) } - private async makeReservation(reservation: Reservation): Promise { - try { - await this.navigateToDay(reservation.dateRange.start) - await this.selectAvailableTime(reservation) - await this.selectOpponent(reservation.opponent) - await this.confirmReservation() - reservation.booked = true - return true - } catch (err: unknown) { - asyncLocalStorage.getStore()?.error('Error making reservation', { - reservation: reservation.format(), - error: err, - }) - return false - } + private async makeReservation(reservation: Reservation) { + await this.navigateToDay(reservation.dateRange.start) + await this.selectAvailableTime(reservation) + await this.selectOpponent(reservation.opponent) + await this.confirmReservation() + reservation.booked = true } private getLastVisibleDay(): Dayjs { @@ -78,17 +96,15 @@ export class Runner { } private async navigateToDay(date: Dayjs): Promise { - asyncLocalStorage.getStore()?.debug(`Navigating to ${date.format()}`) + l.getStore()?.debug(`Navigating to ${date.format()}`) if (this.getLastVisibleDay().isBefore(date)) { - asyncLocalStorage - .getStore() - ?.debug('Date is on different page, increase month') + l.getStore()?.debug('Date is on different page, increase month') await this.page ?.waitForSelector('td.month.next') .then((d) => d?.click()) - .catch(() => { - throw new Error('Could not click correct month') + .catch((e: Error) => { + throw new RunnerNavigationMonthError(e) }) } @@ -99,8 +115,8 @@ export class Runner { )}` ) .then((d) => d?.click()) - .catch(() => { - throw new Error('Could not click correct day') + .catch((e: Error) => { + throw new RunnerNavigationDayError(e) }) await this.page ?.waitForSelector( @@ -108,15 +124,13 @@ export class Runner { 'date' )}.selected` ) - .catch(() => { - throw new Error("Selected day didn't change") + .catch((e: Error) => { + throw new RunnerNavigationSelectionError(e) }) } private async selectAvailableTime(res: Reservation): Promise { - asyncLocalStorage - .getStore() - ?.debug('Selecting available time', res.format()) + l.getStore()?.debug('Selecting available time', res.format()) let freeCourt: ElementHandle | null | undefined let i = 0 while (i < res.possibleDates.length && !freeCourt) { @@ -129,28 +143,76 @@ export class Runner { } if (!freeCourt) { - throw new Error('No free court available') + throw new NoCourtAvailableError() } - await freeCourt.click() + await freeCourt.click().catch((e: Error) => { + throw new RunnerCourtSelectionError(e) + }) } private async selectOpponent(opponent: Opponent): Promise { - asyncLocalStorage.getStore()?.debug('Selecting opponent', opponent) - const player2Search = await this.page?.waitForSelector( - 'tr.res-make-player-2 > td > input' - ) - await player2Search?.type(opponent.name) - await this.page?.waitForNetworkIdle() + l.getStore()?.debug('Selecting opponent', opponent) + const player2Search = await this.page + ?.waitForSelector('tr.res-make-player-2 > td > input') + .catch((e: Error) => { + throw new RunnerOpponentSearchError(e) + }) + await player2Search?.type(opponent.name).catch((e: Error) => { + throw new RunnerOpponentSearchInputError(e) + }) + await this.page?.waitForNetworkIdle().catch((e: Error) => { + throw new RunnerOpponentSearchNetworkError(e) + }) await this.page ?.$('select.br-user-select[name="players[2]"]') .then((d) => d?.select(opponent.id)) + .catch((e: Error) => { + throw new RunnerOpponentSearchSelectionError(e) + }) } private async confirmReservation(): Promise { await this.page?.$('input#__make_submit').then((b) => b?.click()) + .catch((e: Error) => { throw new RunnerReservationConfirmButtonError(e) }) await this.page ?.waitForSelector('input#__make_submit2') .then((b) => b?.click()) + .catch((e: Error) => { throw new RunnerReservationConfirmSubmitError(e) }) + } +} + +export class LoggableError extends Error { + toString() { + return `${this.name} - ${this.message}\n${this.stack}` } } +export class RunnerError extends LoggableError { + constructor(error: Error) { + super(error.message) + this.stack = error.stack + } +} +export class PuppeteerError extends RunnerError {} +export class PuppeteerBrowserLaunchError extends PuppeteerError {} +export class PuppeteerNewPageError extends PuppeteerError {} + +export class RunnerLoginNavigationError extends RunnerError {} +export class RunnerLoginUsernameInputError extends RunnerError {} +export class RunnerLoginPasswordInputError extends RunnerError {} +export class RunnerLoginSubmitError extends RunnerError {} + +export class RunnerNavigationMonthError extends RunnerError {} +export class RunnerNavigationDayError extends RunnerError {} +export class RunnerNavigationSelectionError extends RunnerError {} + +export class RunnerCourtSelectionError extends RunnerError {} +export class NoCourtAvailableError extends LoggableError {} + +export class RunnerOpponentSearchError extends RunnerError {} +export class RunnerOpponentSearchInputError extends RunnerError {} +export class RunnerOpponentSearchNetworkError extends RunnerError {} +export class RunnerOpponentSearchSelectionError extends RunnerError {} + +export class RunnerReservationConfirmButtonError extends RunnerError {} +export class RunnerReservationConfirmSubmitError extends RunnerError {} \ No newline at end of file diff --git a/src/server/cron.ts b/src/server/cron.ts index d9e548c..2c938cd 100644 --- a/src/server/cron.ts +++ b/src/server/cron.ts @@ -12,6 +12,7 @@ const getTaskConfig = (name: string): ScheduleOptions => ({ }) const logger = new Logger('cron', 'default', LogLevel.DEBUG) +let shouldContinue = true export const startTasks = () => { try { @@ -21,15 +22,18 @@ export const startTasks = () => { asyncLocalStorage.run( new Logger('cron', v4(), LogLevel.DEBUG), async () => { - const childLogger = asyncLocalStorage.getStore() - childLogger?.info('Running cron job', { timestamp }) - try { - await reserve() - childLogger?.info('Completed running cron job') - } catch (error: any) { - childLogger?.error('Error running cron job', { - error: error.message, - }) + if (shouldContinue) { + const childLogger = asyncLocalStorage.getStore() + childLogger?.info('Running cron job', { timestamp }) + try { + shouldContinue = await reserve() + childLogger?.info('Completed running cron job') + } catch (error) { + shouldContinue = false + childLogger?.error('Error running cron job', { + error: (error as Error).message, + }) + } } } )