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')
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,

View file

@ -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,

View file

@ -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<string>(
'BAANRESERVEREN_USERNAME',
)
this.password = this.configService.getOrThrow<string>(
'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<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) {
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')