Adding ability to reserve for others and to filter out shitty courts
This commit is contained in:
parent
54aef1ece8
commit
a7c955a18d
4 changed files with 179 additions and 24 deletions
|
|
@ -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"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue