diff --git a/src/common/dayjs.ts b/src/common/dayjs.ts index cbfa30a..7e0f70d 100644 --- a/src/common/dayjs.ts +++ b/src/common/dayjs.ts @@ -24,6 +24,8 @@ export interface SerializedDateRange { end: string } +export const setDefaults = () => dayjs.tz.setDefault('Europe/Amsterdam') + export const convertDateRangeStringToObject = ({ start, end, diff --git a/src/main.ts b/src/main.ts index fac4c83..29d0deb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import { NestFactory } from '@nestjs/core' import { AppModule } from './app.module' import { CustomResponseTransformInterceptor } from './common/customResponse' +import { setDefaults } from './common/dayjs' async function bootstrap() { const app = await NestFactory.create(AppModule, { abortOnError: false }) @@ -12,6 +13,7 @@ async function bootstrap() { app.enableShutdownHooks() app.useGlobalPipes(new ValidationPipe({ transform: true })) app.useGlobalInterceptors(new CustomResponseTransformInterceptor()) + setDefaults() await app.listen(port, () => console.log(`Listening on port ${port}`)) } diff --git a/src/repl.ts b/src/repl.ts index 5b219df..f158085 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -1,8 +1,10 @@ import { repl } from '@nestjs/core' import { AppModule } from './app.module' +import { setDefaults } from './common/dayjs' async function bootstrap() { + setDefaults() await repl(AppModule) } bootstrap() diff --git a/src/reservations/cron.ts b/src/reservations/cron.ts index e46ce7e..94cc5d6 100644 --- a/src/reservations/cron.ts +++ b/src/reservations/cron.ts @@ -30,7 +30,7 @@ export class ReservationsCronService { private readonly loggerService: LoggerService, ) {} - @Cron(CronExpression.EVERY_DAY_AT_7AM, { + @Cron('55 06 * * *', { name: 'handleDailyReservations', timeZone: 'Europe/Amsterdam', }) @@ -42,12 +42,36 @@ export class ReservationsCronService { this.loggerService.log( `Found ${reservationsToPerform.length} reservations to perform`, ) - await this.reservationsQueue.addBulk( - reservationsToPerform.map((r) => ({ - data: r, - opts: { attempts: DAILY_RESERVATIONS_ATTEMPTS }, - })), - ) + + // In order to make sure session is fresh and speed up some shit let's warm him up + await this.brService.warmup() + + this.loggerService.log(`Warmed up! Waiting for go-time`) + + let not7AM = true + const waitTime = 10 + const time7AM = dayjs() + .set('hour', 7) + .set('minute', 0) + .set('second', 0) + .set('millisecond', 0) + + while (not7AM) { + not7AM = time7AM.isBefore(dayjs()) && time7AM.diff(dayjs()) >= waitTime // current time is more than 100ms from 7am + if (!not7AM) break + await new Promise((res) => setTimeout(res, waitTime)) // wait for waitTime and then try again + } + + this.loggerService.log(`It's go-time`) + + for (const res of reservationsToPerform) { + await this.brService.performReservation(res).catch( + async () => + await this.reservationsQueue.add(res, { + attempts: Math.max(DAILY_RESERVATIONS_ATTEMPTS - 1, 1), + }), + ) + } } else { this.loggerService.log('Monitoring reservations') await this.brService.monitorCourtReservations(dayjs().add(7, 'day')) diff --git a/src/reservations/service.ts b/src/reservations/service.ts index fef2d9e..9788fa0 100644 --- a/src/reservations/service.ts +++ b/src/reservations/service.ts @@ -51,7 +51,7 @@ export class ReservationsService { }, ) .andWhere(`waitListed = false`) - .orderBy('dateRangeStart', 'DESC') + .orderBy('dateRangeStart', 'ASC') return await query.getMany() } diff --git a/src/runner/baanreserveren/service.ts b/src/runner/baanreserveren/service.ts index 2dd04d2..0cfb587 100644 --- a/src/runner/baanreserveren/service.ts +++ b/src/runner/baanreserveren/service.ts @@ -82,7 +82,7 @@ const CourtRank = { [CourtSlot.Thirteen]: 1, // no one likes upstairs } as const -const TYPING_DELAY_MS = 5 +const TYPING_DELAY_MS = 2 @Injectable() export class BaanReserverenService { @@ -127,23 +127,24 @@ export class BaanReserverenService { ) } + // Check session by going to /reservations to see if we are still logged in via cookies private async checkSession(username: string) { this.loggerService.debug('Checking session', { username, session: this.session, }) - if (this.page.url().includes(BAAN_RESERVEREN_ROOT_URL)) { - // Check session by going to /reservations to see if we are still logged in via cookies - await this.navigateToReservations() - if (this.page.url().includes('?reason=LOGGED_IN')) { - return SessionAction.Login - } - return this.session?.username !== this.username - ? SessionAction.Logout - : SessionAction.NoAction + if (!this.page.url().includes(BAAN_RESERVEREN_ROOT_URL)) { + await this.navigateToReservations() } - return SessionAction.Login + + if (this.page.url().includes('?reason=LOGGED_IN')) { + return SessionAction.Login + } + + return this.session?.username !== this.username + ? SessionAction.Logout + : SessionAction.NoAction } private startSession(username: string) { @@ -202,8 +203,7 @@ export class BaanReserverenService { private async init() { this.loggerService.debug('Initializing') - await this.page.goto(BAAN_RESERVEREN_ROOT_URL) - await this.page.waitForNetworkIdle() + await this.navigateToReservations() const action = await this.checkSession(this.username) switch (action) { case SessionAction.Logout: @@ -533,6 +533,37 @@ export class BaanReserverenService { await this.page.waitForNetworkIdle() } + private async getAllCourtStatuses() { + const courts = await this.page.$$('tr > td.slot') + const courtStatuses: { + courtNumber: string + startTime: string + status: string + duration: string + }[] = [] + for (const court of courts) { + const classListObj = await ( + await court.getProperty('classList') + ).jsonValue() + const classList = Object.values(classListObj) + const rClass = classList.filter((cl) => /r-\d{2}/.test(cl))[0] + const courtNumber = + `${CourtSlotToNumber[rClass.replace(/r-/, '') as CourtSlot]}` ?? + 'unknown court' + const startTime = await court + .$eval('div.slot-period', (e) => e.innerText.trim()) + .catch(() => 'unknown') + const status = classList.includes('free') ? 'available' : 'unavailable' + const courtRowSpan = await ( + await court.getProperty('rowSpan') + ).jsonValue() + const duration = `${Number(courtRowSpan ?? '0') * 15} minutes` + courtStatuses.push({ courtNumber, startTime, status, duration }) //const d = require('dayjs'); await get(BaanReserverenService).monitorCourtReservations(d()); + } + + return courtStatuses + } + public async performReservation(reservation: Reservation) { try { await this.init() @@ -588,37 +619,6 @@ export class BaanReserverenService { } } - private async getAllCourtStatuses() { - const courts = await this.page.$$('tr > td.slot') - const courtStatuses: { - courtNumber: string - startTime: string - status: string - duration: string - }[] = [] - for (const court of courts) { - const classListObj = await ( - await court.getProperty('classList') - ).jsonValue() - const classList = Object.values(classListObj) - const rClass = classList.filter((cl) => /r-\d{2}/.test(cl))[0] - const courtNumber = - `${CourtSlotToNumber[rClass.replace(/r-/, '') as CourtSlot]}` ?? - 'unknown court' - const startTime = await court - .$eval('div.slot-period', (e) => e.innerText.trim()) - .catch(() => 'unknown') - const status = classList.includes('free') ? 'available' : 'unavailable' - const courtRowSpan = await ( - await court.getProperty('rowSpan') - ).jsonValue() - const duration = `${Number(courtRowSpan ?? '0') * 15} minutes` - courtStatuses.push({ courtNumber, startTime, status, duration }) //const d = require('dayjs'); await get(BaanReserverenService).monitorCourtReservations(d()); - } - - return courtStatuses - } - public async monitorCourtReservations(date?: Dayjs, swallowError = true) { try { if (date) { @@ -642,6 +642,10 @@ export class BaanReserverenService { } } } + + public async warmup() { + await this.init() + } } export class RunnerError extends Error {