From a7c955a18d1f65872b1dd8b06b735e617f842ba2 Mon Sep 17 00:00:00 2001 From: Collin Duncan <3679940+cgduncan7@users.noreply.github.com> Date: Fri, 6 Oct 2023 11:56:34 +0200 Subject: [PATCH] Adding ability to reserve for others and to filter out shitty courts --- ...moveUsernameAndPasswordFromReservations.ts | 93 +++++++++++++++++++ src/recurringReservations/entity.ts | 12 +-- src/reservations/entity.ts | 10 +- src/runner/baanreserveren/service.ts | 88 ++++++++++++++++-- 4 files changed, 179 insertions(+), 24 deletions(-) create mode 100644 database/migrations/1696583071967-RemoveUsernameAndPasswordFromReservations.ts diff --git a/database/migrations/1696583071967-RemoveUsernameAndPasswordFromReservations.ts b/database/migrations/1696583071967-RemoveUsernameAndPasswordFromReservations.ts new file mode 100644 index 0000000..1483318 --- /dev/null +++ b/database/migrations/1696583071967-RemoveUsernameAndPasswordFromReservations.ts @@ -0,0 +1,93 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class RemoveUsernameAndPasswordFromReservations1696583071967 + implements MigrationInterface +{ + name = 'RemoveUsernameAndPasswordFromReservations1696583071967' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temporary_reservations" ("id" varchar PRIMARY KEY NOT NULL, "dateRangeStart" datetime NOT NULL, "dateRangeEnd" datetime NOT NULL, "opponentId" varchar(32) NOT NULL, "opponentName" varchar(255) NOT NULL, "waitListed" boolean NOT NULL DEFAULT (0), "waitingListId" integer)`, + ) + await queryRunner.query( + `INSERT INTO "temporary_reservations"("id", "dateRangeStart", "dateRangeEnd", "opponentId", "opponentName", "waitListed", "waitingListId") SELECT "id", "dateRangeStart", "dateRangeEnd", "opponentId", "opponentName", "waitListed", "waitingListId" FROM "reservations"`, + ) + await queryRunner.query(`DROP TABLE "reservations"`) + await queryRunner.query( + `ALTER TABLE "temporary_reservations" RENAME TO "reservations"`, + ) + await queryRunner.query( + `CREATE TABLE "temporary_recurring_reservations" ("id" varchar PRIMARY KEY NOT NULL, "dayOfWeek" integer NOT NULL, "timeStart" varchar(6) NOT NULL, "timeEnd" varchar(6) NOT NULL, "opponentId" varchar(32) NOT NULL, "opponentName" varchar(255) NOT NULL)`, + ) + await queryRunner.query( + `INSERT INTO "temporary_recurring_reservations"("id", "dayOfWeek", "timeStart", "timeEnd", "opponentId", "opponentName") SELECT "id", "dayOfWeek", "timeStart", "timeEnd", "opponentId", "opponentName" FROM "recurring_reservations"`, + ) + await queryRunner.query(`DROP TABLE "recurring_reservations"`) + await queryRunner.query( + `ALTER TABLE "temporary_recurring_reservations" RENAME TO "recurring_reservations"`, + ) + await queryRunner.query( + `CREATE TABLE "temporary_reservations" ("id" varchar PRIMARY KEY NOT NULL, "dateRangeStart" datetime NOT NULL, "dateRangeEnd" datetime NOT NULL, "opponentId" varchar(32) NOT NULL, "opponentName" varchar(255) NOT NULL, "waitListed" boolean NOT NULL DEFAULT (0), "waitingListId" integer, "ownerId" varchar(32) NOT NULL)`, + ) + await queryRunner.query( + `INSERT INTO "temporary_reservations"("id", "dateRangeStart", "dateRangeEnd", "opponentId", "opponentName", "waitListed", "waitingListId", "ownerId") SELECT "id", "dateRangeStart", "dateRangeEnd", "opponentId", "opponentName", "waitListed", "waitingListId", "unknown" FROM "reservations"`, + ) + await queryRunner.query(`DROP TABLE "reservations"`) + await queryRunner.query( + `ALTER TABLE "temporary_reservations" RENAME TO "reservations"`, + ) + await queryRunner.query( + `CREATE TABLE "temporary_recurring_reservations" ("id" varchar PRIMARY KEY NOT NULL, "dayOfWeek" integer NOT NULL, "timeStart" varchar(6) NOT NULL, "timeEnd" varchar(6) NOT NULL, "opponentId" varchar(32) NOT NULL, "opponentName" varchar(255) NOT NULL, "ownerId" varchar(32) NOT NULL)`, + ) + await queryRunner.query( + `INSERT INTO "temporary_recurring_reservations"("id", "dayOfWeek", "timeStart", "timeEnd", "opponentId", "opponentName", "ownerId") SELECT "id", "dayOfWeek", "timeStart", "timeEnd", "opponentId", "opponentName", "unknown" FROM "recurring_reservations"`, + ) + await queryRunner.query(`DROP TABLE "recurring_reservations"`) + await queryRunner.query( + `ALTER TABLE "temporary_recurring_reservations" RENAME TO "recurring_reservations"`, + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "recurring_reservations" RENAME TO "temporary_recurring_reservations"`, + ) + await queryRunner.query( + `CREATE TABLE "recurring_reservations" ("id" varchar PRIMARY KEY NOT NULL, "dayOfWeek" integer NOT NULL, "timeStart" varchar(6) NOT NULL, "timeEnd" varchar(6) NOT NULL, "opponentId" varchar(32) NOT NULL, "opponentName" varchar(255) NOT NULL)`, + ) + await queryRunner.query( + `INSERT INTO "recurring_reservations"("id", "dayOfWeek", "timeStart", "timeEnd", "opponentId", "opponentName") SELECT "id", "dayOfWeek", "timeStart", "timeEnd", "opponentId", "opponentName" FROM "temporary_recurring_reservations"`, + ) + await queryRunner.query(`DROP TABLE "temporary_recurring_reservations"`) + await queryRunner.query( + `ALTER TABLE "reservations" RENAME TO "temporary_reservations"`, + ) + await queryRunner.query( + `CREATE TABLE "reservations" ("id" varchar PRIMARY KEY NOT NULL, "dateRangeStart" datetime NOT NULL, "dateRangeEnd" datetime NOT NULL, "opponentId" varchar(32) NOT NULL, "opponentName" varchar(255) NOT NULL, "waitListed" boolean NOT NULL DEFAULT (0), "waitingListId" integer)`, + ) + await queryRunner.query( + `INSERT INTO "reservations"("id", "dateRangeStart", "dateRangeEnd", "opponentId", "opponentName", "waitListed", "waitingListId") SELECT "id", "dateRangeStart", "dateRangeEnd", "opponentId", "opponentName", "waitListed", "waitingListId" FROM "temporary_reservations"`, + ) + await queryRunner.query(`DROP TABLE "temporary_reservations"`) + await queryRunner.query( + `ALTER TABLE "recurring_reservations" RENAME TO "temporary_recurring_reservations"`, + ) + await queryRunner.query( + `CREATE TABLE "recurring_reservations" ("id" varchar PRIMARY KEY NOT NULL, "username" varchar(64) NOT NULL, "password" varchar(255) NOT NULL, "dayOfWeek" integer NOT NULL, "timeStart" varchar(6) NOT NULL, "timeEnd" varchar(6) NOT NULL, "opponentId" varchar(32) NOT NULL, "opponentName" varchar(255) NOT NULL)`, + ) + await queryRunner.query( + `INSERT INTO "recurring_reservations"("id", "dayOfWeek", "timeStart", "timeEnd", "opponentId", "opponentName") SELECT "id", "dayOfWeek", "timeStart", "timeEnd", "opponentId", "opponentName" FROM "temporary_recurring_reservations"`, + ) + await queryRunner.query(`DROP TABLE "temporary_recurring_reservations"`) + await queryRunner.query( + `ALTER TABLE "reservations" RENAME TO "temporary_reservations"`, + ) + await queryRunner.query( + `CREATE TABLE "reservations" ("id" varchar PRIMARY KEY NOT NULL, "username" varchar(64) NOT NULL, "password" varchar(255) NOT NULL, "dateRangeStart" datetime NOT NULL, "dateRangeEnd" datetime NOT NULL, "opponentId" varchar(32) NOT NULL, "opponentName" varchar(255) NOT NULL, "waitListed" boolean NOT NULL DEFAULT (0), "waitingListId" integer)`, + ) + await queryRunner.query( + `INSERT INTO "reservations"("id", "dateRangeStart", "dateRangeEnd", "opponentId", "opponentName", "waitListed", "waitingListId") SELECT "id", "dateRangeStart", "dateRangeEnd", "opponentId", "opponentName", "waitListed", "waitingListId" FROM "temporary_reservations"`, + ) + await queryRunner.query(`DROP TABLE "temporary_reservations"`) + } +} diff --git a/src/recurringReservations/entity.ts b/src/recurringReservations/entity.ts index 978ba89..47ff678 100644 --- a/src/recurringReservations/entity.ts +++ b/src/recurringReservations/entity.ts @@ -20,14 +20,8 @@ export class RecurringReservation { @PrimaryGeneratedColumn('uuid') id: string - @Column('varchar', { length: 64, nullable: false }) - username: string - - @Transform(({ value, options: { groups = [] } }) => - groups.includes('password') ? value : '***', - ) - @Column('varchar', { length: 255, nullable: false }) - password: string + @Column('varchar', { length: 32, nullable: false }) + ownerId: string @Column('int', { nullable: false }) @IsEnum(DayOfWeek) @@ -60,8 +54,6 @@ export class RecurringReservation { .set('minute', Number.parseInt(minuteEnd)) .add(daysInAdvance, 'days') const reservation = new Reservation({ - username: this.username, - password: this.password, dateRangeStart: dateRangeStart, dateRangeEnd: dateRangeEnd, opponentId: this.opponentId, diff --git a/src/reservations/entity.ts b/src/reservations/entity.ts index afc071c..9bc290a 100644 --- a/src/reservations/entity.ts +++ b/src/reservations/entity.ts @@ -10,14 +10,8 @@ export class Reservation { @PrimaryGeneratedColumn('uuid') id: string - @Column('varchar', { length: 64, nullable: false }) - username: string - - @Transform(({ value, options: { groups = [] } }) => - groups.includes('password') ? value : '***', - ) - @Column('varchar', { length: 255, nullable: false }) - password: string + @Column('varchar', { length: 32, nullable: false }) + ownerId: string @Column('datetime', { nullable: false, diff --git a/src/runner/baanreserveren/service.ts b/src/runner/baanreserveren/service.ts index 82dcbd0..d700fff 100644 --- a/src/runner/baanreserveren/service.ts +++ b/src/runner/baanreserveren/service.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' import { instanceToPlain } from 'class-transformer' import { Dayjs } from 'dayjs' import { ElementHandle, Page } from 'puppeteer' @@ -28,20 +29,49 @@ interface BaanReserverenSession { startedAt: Dayjs } +// TODO: Add to DB to make configurable +enum Court { + One = '51', + Two = '52', + Three = '53', + Four = '54', + Five = '55', + Six = '56', + Seven = '57', + Eight = '58', + Nine = '59', + Ten = '60', + Eleven = '61', + Twelve = '62', + Thirteen = '63', +} + const MIN_TYPING_DELAY_MS = 10 const MAX_TYPING_DELAY_MS = 30 @Injectable() export class BaanReserverenService { private session: BaanReserverenSession | null = null + private username: string + private password: string constructor( @Inject(LoggerService) private readonly loggerService: LoggerService, + @Inject(ConfigService) + private readonly configService: ConfigService, + @Inject(EmptyPage) private readonly page: Page, - ) {} + ) { + this.username = this.configService.getOrThrow( + 'BAANRESERVEREN_USERNAME', + ) + this.password = this.configService.getOrThrow( + 'BAANRESERVEREN_PASSWORD', + ) + } // tryna be sneaky private getTypingDelay() { @@ -63,7 +93,7 @@ export class BaanReserverenService { return SessionAction.Login } - return this.session?.username !== username + return this.session?.username !== this.username ? SessionAction.Logout : SessionAction.NoAction } @@ -130,14 +160,14 @@ export class BaanReserverenService { }) await this.page.goto(BAAN_RESERVEREN_ROOT_URL) await this.page.waitForNetworkIdle() - const action = await this.checkSession(reservation.username) + const action = await this.checkSession(this.username) switch (action) { case SessionAction.Logout: await this.logout() - await this.login(reservation.username, reservation.password) + await this.login(this.username, this.password) break case SessionAction.Login: - await this.login(reservation.username, reservation.password) + await this.login(this.username, this.password) break case SessionAction.NoAction: default: @@ -278,6 +308,22 @@ export class BaanReserverenService { await this.page.waitForNetworkIdle() } + private async filterShittyCourts( + freeCourts: ElementHandle[], + ): Promise { + for (const fc of freeCourts) { + const fcValue = await fc.jsonValue() + switch (fcValue.slot) { + case Court.Five: + case Court.Six: + continue + default: + return fc + } + } + return null + } + private async selectAvailableTime(reservation: Reservation) { this.loggerService.debug('Selecting available time', { reservation: instanceToPlain(reservation), @@ -291,7 +337,12 @@ export class BaanReserverenService { const timeString = possibleDate.format('HH:mm') const selector = `tr[data-time='${timeString}']` + `> td.free[rowspan='3'][type='free']` - freeCourt = await this.page.$(selector) + const freeCourts = await this.page.$$(selector) + freeCourt = await this.filterShittyCourts(freeCourts) + if (freeCourts.length > 0 && freeCourt == null) { + // If there is only a shitty court, sucks + freeCourt = freeCourts[0] + } i++ } @@ -306,6 +357,19 @@ export class BaanReserverenService { }) } + private async selectOwner(id: string) { + this.loggerService.debug('Selecting owner', { id }) + await this.page.waitForNetworkIdle().catch((e: Error) => { + throw new RunnerOwnerSearchNetworkError(e) + }) + await this.page + .$('select.br-user-select[name="players[1]"]') + .then((d) => d?.select(id)) + .catch((e: Error) => { + throw new RunnerOwnerSearchSelectionError(e) + }) + } + private async selectOpponent(id: string, name: string) { this.loggerService.debug('Selecting opponent', { id, name }) const player2Search = await this.page @@ -403,6 +467,7 @@ export class BaanReserverenService { await this.init(reservation) await this.navigateToDay(reservation.dateRangeStart) await this.selectAvailableTime(reservation) + await this.selectOwner(reservation.ownerId) await this.selectOpponent(reservation.opponentId, reservation.opponentName) await this.confirmReservation() } @@ -552,6 +617,17 @@ export class NoCourtAvailableError extends Error { } } +export class RunnerOwnerSearchNetworkError extends RunnerError { + constructor(error: Error) { + super(error, 'RunnerOwnerSearchNetworkError') + } +} +export class RunnerOwnerSearchSelectionError extends RunnerError { + constructor(error: Error) { + super(error, 'RunnerOwnerSearchSelectionError') + } +} + export class RunnerOpponentSearchError extends RunnerError { constructor(error: Error) { super(error, 'RunnerOpponentSearchError')