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)