From 32192569612d72b67ae5e923c2e0704c1d0bc35d Mon Sep 17 00:00:00 2001 From: Collin Duncan <3679940+cgduncan7@users.noreply.github.com> Date: Fri, 10 Feb 2023 12:24:27 +0100 Subject: [PATCH] Adding some additional logic to support multiple reservations during one runner request and allowing fetching reservations by date. Also changed cron to only run once at 7.00 and fetch all available reservations at that date --- src/common/reserver.ts | 39 ++++++++++------- src/common/runner.ts | 57 +++++++++++++++++++++++-- src/server/cron.ts | 9 +++- src/server/http/routes/reservations.ts | 58 +++++++++++++++++++------- 4 files changed, 128 insertions(+), 35 deletions(-) diff --git a/src/common/reserver.ts b/src/common/reserver.ts index cc48e3d..a76d4e4 100644 --- a/src/common/reserver.ts +++ b/src/common/reserver.ts @@ -1,3 +1,4 @@ +import dayjs from './dayjs' import { asyncLocalStorage as l, LoggableError } from './logger' import { Reservation } from './reservation' import { Runner } from './runner' @@ -13,28 +14,36 @@ const getRunner = () => { return runner } -export const reserve = async (reservation?: Reservation): Promise => { - let reservationToPerform = reservation - if (!reservationToPerform) { +export const reserve = async ( + reservations?: Reservation[] +): Promise => { + let reservationsToPerform = reservations + if (!reservationsToPerform) { l.getStore()?.debug('No reservation provided, fetching first in database') - reservationToPerform = (await Reservation.fetchFirst()) || undefined + reservationsToPerform = + (await Reservation.fetchByDate(dayjs())) || undefined } - if (!reservationToPerform) { + if (!reservationsToPerform || reservationsToPerform.length === 0) { l.getStore()?.info('No reservation to perform') return true } - l.getStore()?.debug('Trying to perform reservation', { reservationToPerform: reservationToPerform.toString(true) }) - const runner = getRunner() - try { - await runner.run(reservationToPerform) - await reservationToPerform.delete() - return true - } catch (error) { - l.getStore()?.error('Failed to perform reservation', { - error: (error as LoggableError).toString(), + for (const reservationToPerform of reservationsToPerform) { + l.getStore()?.debug('Trying to perform reservation', { + reservationToPerform: reservationToPerform.toString(true), }) - return false + const runner = getRunner() + try { + await runner.run(reservationToPerform) + await reservationToPerform.delete() + } catch (error) { + l.getStore()?.error('Failed to perform reservation', { + error: (error as LoggableError).toString(), + }) + return false + } } + + return true } diff --git a/src/common/runner.ts b/src/common/runner.ts index f3e5a7c..347d0c6 100644 --- a/src/common/runner.ts +++ b/src/common/runner.ts @@ -11,12 +11,24 @@ import puppeteer, { import { asyncLocalStorage as l, LoggableError } from './logger' import { Opponent, Reservation } from './reservation' +enum SessionAction { + NoAction, + Logout, + Login, +} + +interface RunnerSession { + username: string + loggedInAt: Dayjs +} + export class Runner { - private browser: Browser | undefined - private page: Page | undefined + private browser?: Browser + private page?: Page private options?: LaunchOptions & BrowserLaunchArgumentOptions & BrowserConnectOptions + private session?: RunnerSession constructor( options?: LaunchOptions & @@ -42,10 +54,34 @@ export class Runner { throw new PuppeteerNewPageError(error as Error) } - await this.login(reservation.user.username, reservation.user.password) + const sessionAction = await this.checkSession(reservation.user.username) + switch (sessionAction) { + case SessionAction.Login: { + await this.login(reservation.user.username, reservation.user.password) + break + } + case SessionAction.Logout: { + await this.logout() + await this.login(reservation.user.username, reservation.user.password) + break + } + case SessionAction.NoAction: + default: + break + } await this.makeReservation(reservation) } + private async checkSession(username: string): Promise { + if (this.page?.url().startsWith('https://squashcity.baanreserveren.nl/')) { + l.getStore()?.info('Already logged in', { username }) + return this.session?.username !== username + ? SessionAction.Logout + : SessionAction.NoAction + } + return SessionAction.Login + } + private async login(username: string, password: string) { l.getStore()?.debug('Logging in', { username }) await this.page @@ -73,6 +109,15 @@ export class Runner { }) } + private async logout() { + l.getStore()?.debug('Logging out', { username: this.session?.username }) + await this.page + ?.goto('https://squashcity.baanreserveren.nl/auth/logout') + .catch((e: Error) => { + throw new RunnerLoginPasswordInputError(e) + }) + } + private async makeReservation(reservation: Reservation) { await this.navigateToDay(reservation.dateRange.start) await this.selectAvailableTime(reservation) @@ -130,7 +175,9 @@ export class Runner { } private async selectAvailableTime(res: Reservation): Promise { - l.getStore()?.debug('Selecting available time', { reservation: res.toString(true) }) + l.getStore()?.debug('Selecting available time', { + reservation: res.toString(true), + }) let freeCourt: ElementHandle | null | undefined let i = 0 while (i < res.possibleDates.length && !freeCourt) { @@ -198,6 +245,8 @@ export class PuppeteerError extends RunnerError {} export class PuppeteerBrowserLaunchError extends PuppeteerError {} export class PuppeteerNewPageError extends PuppeteerError {} +export class RunnerLogoutError extends RunnerError {} + export class RunnerLoginNavigationError extends RunnerError {} export class RunnerLoginUsernameInputError extends RunnerError {} export class RunnerLoginPasswordInputError extends RunnerError {} diff --git a/src/server/cron.ts b/src/server/cron.ts index a719427..2facefd 100644 --- a/src/server/cron.ts +++ b/src/server/cron.ts @@ -1,4 +1,9 @@ -import { schedule, ScheduledTask, ScheduleOptions, getTasks as getCronTasks } from 'node-cron' +import { + schedule, + ScheduledTask, + ScheduleOptions, + getTasks as getCronTasks, +} from 'node-cron' import { v4 } from 'uuid' import { asyncLocalStorage, Logger, LogLevel } from '../common/logger' import { reserve } from '../common/reserver' @@ -26,7 +31,7 @@ export const startTasks = () => { try { if (tasks.length === 0) { const task = schedule( - '* 7 * * *', + '0 7 * * *', async (timestamp) => { asyncLocalStorage.run( new Logger('cron', v4(), LogLevel.DEBUG), diff --git a/src/server/http/routes/reservations.ts b/src/server/http/routes/reservations.ts index bff5638..f723481 100644 --- a/src/server/http/routes/reservations.ts +++ b/src/server/http/routes/reservations.ts @@ -4,11 +4,13 @@ import { asyncLocalStorage as l } from '../../../common/logger' import { Router } from './index' import { Reservation } from '../../../common/reservation' import { parse } from 'querystring' +import dayjs from '../../../common/dayjs' +import { Dayjs } from 'dayjs' export class ReservationsRouter extends Router { public async handleRequest( req: IncomingMessage, - res: ServerResponse, + res: ServerResponse ) { const { url = '', method } = req const [route] = url.split('?') @@ -31,12 +33,20 @@ export class ReservationsRouter extends Router { } } - private async GET_reservations(req: IncomingMessage, res: ServerResponse) { + private async GET_reservations( + req: IncomingMessage, + res: ServerResponse + ) { const { url = '' } = req const [, queryParams] = url.split('?') let pageNumber = 0 let pageSize = 0 - const { pageNumber: rawPageNumber = '0', pageSize: rawPageSize = '50' } = parse(queryParams) + let date: Dayjs | undefined = undefined + const { + pageNumber: rawPageNumber = '0', + pageSize: rawPageSize = '50', + date: rawDate, + } = parse(queryParams) if (typeof rawPageNumber === 'string') { pageNumber = Number.parseInt(rawPageNumber) } else { @@ -48,20 +58,34 @@ export class ReservationsRouter extends Router { } else { pageSize = 50 } - + + if (typeof rawDate === 'string') { + date = dayjs(rawDate) + } + l.getStore()?.debug('Fetching reservations', { pageNumber, pageSize }) try { - const reservations = await Reservation.fetchByPage(pageNumber, pageSize) + let reservations: Reservation[] + if (date) { + reservations = await Reservation.fetchByDate(date, 50) + } else { + reservations = await Reservation.fetchByPage(pageNumber, pageSize) + } res.setHeader('content-type', 'application/json') - l.getStore()?.debug('Found reservations', { reservations: reservations.map((r) => r.toString(true)) }) + l.getStore()?.debug('Found reservations', { + reservations: reservations.map((r) => r.toString(true)), + }) return new Promise((resolve, reject) => { - res.write(`[${reservations.map((r) => r.toString(true)).join(',')}]`, (err) => { - if (err) { - reject(err) + res.write( + `[${reservations.map((r) => r.toString(true)).join(',')}]`, + (err) => { + if (err) { + reject(err) + } + resolve() } - resolve() - }) + ) }) } catch (error) { l.getStore()?.error('Failed to get reservations', { @@ -72,7 +96,10 @@ export class ReservationsRouter extends Router { } } - private async POST_reservations(req: IncomingMessage, res: ServerResponse) { + private async POST_reservations( + req: IncomingMessage, + res: ServerResponse + ) { const jsonBody = await this.parseJsonContent(req) try { await schedule(jsonBody) @@ -85,9 +112,12 @@ export class ReservationsRouter extends Router { } } - private async DELETE_reservation(req: IncomingMessage, res: ServerResponse) { + private async DELETE_reservation( + req: IncomingMessage, + res: ServerResponse + ) { const { url = '' } = req - const [,,id] = url.split('/') + const [, , id] = url.split('/') l.getStore()?.debug('Deleting reservation', { id }) try { await Reservation.deleteById(id)