Adding ability to reserve for others and to filter out shitty courts

This commit is contained in:
Collin Duncan 2023-10-06 11:56:34 +02:00
parent 54aef1ece8
commit a7c955a18d
No known key found for this signature in database
4 changed files with 179 additions and 24 deletions

View file

@ -0,0 +1,93 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class RemoveUsernameAndPasswordFromReservations1696583071967
implements MigrationInterface
{
name = 'RemoveUsernameAndPasswordFromReservations1696583071967'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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"`)
}
}

View file

@ -20,14 +20,8 @@ export class RecurringReservation {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string id: string
@Column('varchar', { length: 64, nullable: false }) @Column('varchar', { length: 32, nullable: false })
username: string ownerId: string
@Transform(({ value, options: { groups = [] } }) =>
groups.includes('password') ? value : '***',
)
@Column('varchar', { length: 255, nullable: false })
password: string
@Column('int', { nullable: false }) @Column('int', { nullable: false })
@IsEnum(DayOfWeek) @IsEnum(DayOfWeek)
@ -60,8 +54,6 @@ export class RecurringReservation {
.set('minute', Number.parseInt(minuteEnd)) .set('minute', Number.parseInt(minuteEnd))
.add(daysInAdvance, 'days') .add(daysInAdvance, 'days')
const reservation = new Reservation({ const reservation = new Reservation({
username: this.username,
password: this.password,
dateRangeStart: dateRangeStart, dateRangeStart: dateRangeStart,
dateRangeEnd: dateRangeEnd, dateRangeEnd: dateRangeEnd,
opponentId: this.opponentId, opponentId: this.opponentId,

View file

@ -10,14 +10,8 @@ export class Reservation {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string id: string
@Column('varchar', { length: 64, nullable: false }) @Column('varchar', { length: 32, nullable: false })
username: string ownerId: string
@Transform(({ value, options: { groups = [] } }) =>
groups.includes('password') ? value : '***',
)
@Column('varchar', { length: 255, nullable: false })
password: string
@Column('datetime', { @Column('datetime', {
nullable: false, nullable: false,

View file

@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common' import { Inject, Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { instanceToPlain } from 'class-transformer' import { instanceToPlain } from 'class-transformer'
import { Dayjs } from 'dayjs' import { Dayjs } from 'dayjs'
import { ElementHandle, Page } from 'puppeteer' import { ElementHandle, Page } from 'puppeteer'
@ -28,20 +29,49 @@ interface BaanReserverenSession {
startedAt: Dayjs 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 MIN_TYPING_DELAY_MS = 10
const MAX_TYPING_DELAY_MS = 30 const MAX_TYPING_DELAY_MS = 30
@Injectable() @Injectable()
export class BaanReserverenService { export class BaanReserverenService {
private session: BaanReserverenSession | null = null private session: BaanReserverenSession | null = null
private username: string
private password: string
constructor( constructor(
@Inject(LoggerService) @Inject(LoggerService)
private readonly loggerService: LoggerService, private readonly loggerService: LoggerService,
@Inject(ConfigService)
private readonly configService: ConfigService,
@Inject(EmptyPage) @Inject(EmptyPage)
private readonly page: Page, private readonly page: Page,
) {} ) {
this.username = this.configService.getOrThrow<string>(
'BAANRESERVEREN_USERNAME',
)
this.password = this.configService.getOrThrow<string>(
'BAANRESERVEREN_PASSWORD',
)
}
// tryna be sneaky // tryna be sneaky
private getTypingDelay() { private getTypingDelay() {
@ -63,7 +93,7 @@ export class BaanReserverenService {
return SessionAction.Login return SessionAction.Login
} }
return this.session?.username !== username return this.session?.username !== this.username
? SessionAction.Logout ? SessionAction.Logout
: SessionAction.NoAction : SessionAction.NoAction
} }
@ -130,14 +160,14 @@ export class BaanReserverenService {
}) })
await this.page.goto(BAAN_RESERVEREN_ROOT_URL) await this.page.goto(BAAN_RESERVEREN_ROOT_URL)
await this.page.waitForNetworkIdle() await this.page.waitForNetworkIdle()
const action = await this.checkSession(reservation.username) const action = await this.checkSession(this.username)
switch (action) { switch (action) {
case SessionAction.Logout: case SessionAction.Logout:
await this.logout() await this.logout()
await this.login(reservation.username, reservation.password) await this.login(this.username, this.password)
break break
case SessionAction.Login: case SessionAction.Login:
await this.login(reservation.username, reservation.password) await this.login(this.username, this.password)
break break
case SessionAction.NoAction: case SessionAction.NoAction:
default: default:
@ -278,6 +308,22 @@ export class BaanReserverenService {
await this.page.waitForNetworkIdle() await this.page.waitForNetworkIdle()
} }
private async filterShittyCourts(
freeCourts: ElementHandle<Element>[],
): Promise<ElementHandle | null> {
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) { private async selectAvailableTime(reservation: Reservation) {
this.loggerService.debug('Selecting available time', { this.loggerService.debug('Selecting available time', {
reservation: instanceToPlain(reservation), reservation: instanceToPlain(reservation),
@ -291,7 +337,12 @@ export class BaanReserverenService {
const timeString = possibleDate.format('HH:mm') const timeString = possibleDate.format('HH:mm')
const selector = const selector =
`tr[data-time='${timeString}']` + `> td.free[rowspan='3'][type='free']` `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++ 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) { private async selectOpponent(id: string, name: string) {
this.loggerService.debug('Selecting opponent', { id, name }) this.loggerService.debug('Selecting opponent', { id, name })
const player2Search = await this.page const player2Search = await this.page
@ -403,6 +467,7 @@ export class BaanReserverenService {
await this.init(reservation) await this.init(reservation)
await this.navigateToDay(reservation.dateRangeStart) await this.navigateToDay(reservation.dateRangeStart)
await this.selectAvailableTime(reservation) await this.selectAvailableTime(reservation)
await this.selectOwner(reservation.ownerId)
await this.selectOpponent(reservation.opponentId, reservation.opponentName) await this.selectOpponent(reservation.opponentId, reservation.opponentName)
await this.confirmReservation() 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 { export class RunnerOpponentSearchError extends RunnerError {
constructor(error: Error) { constructor(error: Error) {
super(error, 'RunnerOpponentSearchError') super(error, 'RunnerOpponentSearchError')