2024-03-14 08:06:51 +01:00
|
|
|
import { InjectQueue } from '@nestjs/bull'
|
2023-05-26 15:43:14 -05:00
|
|
|
import { Inject, Injectable } from '@nestjs/common'
|
2023-10-06 11:56:34 +02:00
|
|
|
import { ConfigService } from '@nestjs/config'
|
2023-06-29 10:32:09 +02:00
|
|
|
import { instanceToPlain } from 'class-transformer'
|
2023-01-20 15:31:04 +01:00
|
|
|
import { Dayjs } from 'dayjs'
|
2023-05-26 15:43:14 -05:00
|
|
|
import { ElementHandle, Page } from 'puppeteer'
|
2023-06-29 10:32:09 +02:00
|
|
|
|
2023-05-26 15:43:14 -05:00
|
|
|
import dayjs from '../../common/dayjs'
|
2023-09-06 11:23:21 +02:00
|
|
|
import { LoggerService } from '../../logger/service.logger'
|
2024-03-26 14:05:33 +01:00
|
|
|
import { MONITORING_QUEUE_NAME, MonitoringQueue } from '../../monitoring/config'
|
2024-03-14 08:06:51 +01:00
|
|
|
import { MonitorType } from '../../monitoring/entity'
|
2023-05-26 15:43:14 -05:00
|
|
|
import { Reservation } from '../../reservations/entity'
|
2023-06-29 10:32:09 +02:00
|
|
|
import { EmptyPage } from '../pages/empty'
|
2023-05-26 15:43:14 -05:00
|
|
|
|
2023-07-31 15:18:08 +02:00
|
|
|
const BAAN_RESERVEREN_ROOT_URL = 'https://squashcity.baanreserveren.nl'
|
2023-05-26 15:43:14 -05:00
|
|
|
|
|
|
|
|
export enum BaanReserverenUrls {
|
2023-06-27 16:06:19 +02:00
|
|
|
Reservations = '/reservations',
|
|
|
|
|
Logout = '/auth/logout',
|
2023-07-28 19:50:04 +02:00
|
|
|
WaitingList = '/waitinglist',
|
|
|
|
|
WaitingListAdd = '/waitinglist/add',
|
2023-05-26 15:43:14 -05:00
|
|
|
}
|
2021-11-15 11:28:39 +01:00
|
|
|
|
2023-02-10 12:24:27 +01:00
|
|
|
enum SessionAction {
|
2023-06-27 16:06:19 +02:00
|
|
|
NoAction,
|
|
|
|
|
Logout,
|
|
|
|
|
Login,
|
2023-02-10 12:24:27 +01:00
|
|
|
}
|
|
|
|
|
|
2023-05-26 15:43:14 -05:00
|
|
|
interface BaanReserverenSession {
|
2023-06-27 16:06:19 +02:00
|
|
|
username: string
|
|
|
|
|
startedAt: Dayjs
|
2023-02-10 12:24:27 +01:00
|
|
|
}
|
|
|
|
|
|
2023-10-06 11:56:34 +02:00
|
|
|
// TODO: Add to DB to make configurable
|
2024-01-30 10:36:59 +01:00
|
|
|
enum CourtSlot {
|
2023-10-06 11:56:34 +02:00
|
|
|
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',
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-30 10:36:59 +01:00
|
|
|
const CourtSlotToNumber = {
|
|
|
|
|
[CourtSlot.One]: 1,
|
|
|
|
|
[CourtSlot.Two]: 2,
|
|
|
|
|
[CourtSlot.Three]: 3,
|
|
|
|
|
[CourtSlot.Four]: 4,
|
|
|
|
|
[CourtSlot.Five]: 5,
|
|
|
|
|
[CourtSlot.Six]: 6,
|
|
|
|
|
[CourtSlot.Seven]: 7,
|
|
|
|
|
[CourtSlot.Eight]: 8,
|
|
|
|
|
[CourtSlot.Nine]: 9,
|
|
|
|
|
[CourtSlot.Ten]: 10,
|
|
|
|
|
[CourtSlot.Eleven]: 11,
|
|
|
|
|
[CourtSlot.Twelve]: 12,
|
|
|
|
|
[CourtSlot.Thirteen]: 13,
|
|
|
|
|
} as const
|
|
|
|
|
|
|
|
|
|
// Lower is better
|
|
|
|
|
const CourtRank = {
|
|
|
|
|
[CourtSlot.One]: 0,
|
|
|
|
|
[CourtSlot.Two]: 0,
|
|
|
|
|
[CourtSlot.Three]: 0,
|
|
|
|
|
[CourtSlot.Four]: 0,
|
|
|
|
|
[CourtSlot.Five]: 99, // shitty
|
|
|
|
|
[CourtSlot.Six]: 99, // shitty
|
|
|
|
|
[CourtSlot.Seven]: 0,
|
|
|
|
|
[CourtSlot.Eight]: 0,
|
|
|
|
|
[CourtSlot.Nine]: 0,
|
|
|
|
|
[CourtSlot.Ten]: 0,
|
|
|
|
|
[CourtSlot.Eleven]: 1, // no one likes upstairs
|
|
|
|
|
[CourtSlot.Twelve]: 1, // no one likes upstairs
|
|
|
|
|
[CourtSlot.Thirteen]: 1, // no one likes upstairs
|
|
|
|
|
} as const
|
|
|
|
|
|
2024-03-27 14:10:28 +01:00
|
|
|
const TYPING_DELAY_MS = 5
|
2023-07-28 19:50:04 +02:00
|
|
|
|
2023-05-26 15:43:14 -05:00
|
|
|
@Injectable()
|
|
|
|
|
export class BaanReserverenService {
|
2023-06-27 16:06:19 +02:00
|
|
|
private session: BaanReserverenSession | null = null
|
2023-10-06 11:56:34 +02:00
|
|
|
private username: string
|
|
|
|
|
private password: string
|
2023-06-27 16:06:19 +02:00
|
|
|
|
|
|
|
|
constructor(
|
2024-03-14 08:06:51 +01:00
|
|
|
@InjectQueue(MONITORING_QUEUE_NAME)
|
2024-03-26 14:05:33 +01:00
|
|
|
private readonly monitoringQueue: MonitoringQueue,
|
2024-03-14 08:06:51 +01:00
|
|
|
|
2023-06-28 09:28:24 +02:00
|
|
|
@Inject(LoggerService)
|
|
|
|
|
private readonly loggerService: LoggerService,
|
2023-06-27 16:06:19 +02:00
|
|
|
|
2023-10-06 11:56:34 +02:00
|
|
|
@Inject(ConfigService)
|
|
|
|
|
private readonly configService: ConfigService,
|
|
|
|
|
|
2023-06-27 16:06:19 +02:00
|
|
|
@Inject(EmptyPage)
|
|
|
|
|
private readonly page: Page,
|
2023-10-06 11:56:34 +02:00
|
|
|
) {
|
|
|
|
|
this.username = this.configService.getOrThrow<string>(
|
|
|
|
|
'BAANRESERVEREN_USERNAME',
|
|
|
|
|
)
|
|
|
|
|
this.password = this.configService.getOrThrow<string>(
|
|
|
|
|
'BAANRESERVEREN_PASSWORD',
|
|
|
|
|
)
|
|
|
|
|
}
|
2023-06-27 16:06:19 +02:00
|
|
|
|
2023-07-28 19:50:04 +02:00
|
|
|
// tryna be sneaky
|
|
|
|
|
private getTypingDelay() {
|
2024-03-27 14:10:28 +01:00
|
|
|
return TYPING_DELAY_MS
|
2023-07-28 19:50:04 +02:00
|
|
|
}
|
|
|
|
|
|
2024-02-23 07:26:29 -06:00
|
|
|
private async handleError() {
|
2024-02-23 07:35:35 -06:00
|
|
|
await this.page
|
|
|
|
|
.screenshot({
|
|
|
|
|
type: 'png',
|
2024-03-19 10:07:07 +01:00
|
|
|
path: `./${Date.now()}_error-screenshot.png`,
|
2024-02-23 07:35:35 -06:00
|
|
|
})
|
|
|
|
|
.catch((reason: any) =>
|
|
|
|
|
this.loggerService.warn('Failed to take screenshot', { reason }),
|
|
|
|
|
)
|
2024-02-23 07:26:29 -06:00
|
|
|
}
|
|
|
|
|
|
2023-07-31 15:18:08 +02:00
|
|
|
private async checkSession(username: string) {
|
2023-06-28 09:28:24 +02:00
|
|
|
this.loggerService.debug('Checking session', {
|
|
|
|
|
username,
|
|
|
|
|
session: this.session,
|
|
|
|
|
})
|
2023-07-31 15:18:08 +02:00
|
|
|
if (this.page.url().includes(BAAN_RESERVEREN_ROOT_URL)) {
|
2023-08-28 14:23:08 +02:00
|
|
|
// Check session by going to /reservations to see if we are still logged in via cookies
|
|
|
|
|
await this.navigateToReservations()
|
2023-07-31 15:18:08 +02:00
|
|
|
if (this.page.url().includes('?reason=LOGGED_IN')) {
|
|
|
|
|
return SessionAction.Login
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-06 11:56:34 +02:00
|
|
|
return this.session?.username !== this.username
|
2023-06-27 16:06:19 +02:00
|
|
|
? SessionAction.Logout
|
|
|
|
|
: SessionAction.NoAction
|
|
|
|
|
}
|
|
|
|
|
return SessionAction.Login
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private startSession(username: string) {
|
2023-06-28 09:28:24 +02:00
|
|
|
this.loggerService.debug('Starting session', { username })
|
2023-06-27 16:06:19 +02:00
|
|
|
if (this.session && this.session.username !== username) {
|
2023-08-31 11:30:15 +02:00
|
|
|
throw new SessionStartError('Session already started')
|
2023-06-27 16:06:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.session?.username === username) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.session = {
|
|
|
|
|
username,
|
|
|
|
|
startedAt: dayjs(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private endSession() {
|
2023-06-28 09:28:24 +02:00
|
|
|
this.loggerService.debug('Ending session', { session: this.session })
|
2023-06-27 16:06:19 +02:00
|
|
|
this.session = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async login(username: string, password: string) {
|
2023-06-28 09:28:24 +02:00
|
|
|
this.loggerService.debug('Logging in', { username })
|
2023-06-27 16:06:19 +02:00
|
|
|
await this.page
|
|
|
|
|
.waitForSelector('input[name=username]')
|
2023-07-28 19:50:04 +02:00
|
|
|
.then((i) => i?.type(username, { delay: this.getTypingDelay() }))
|
2023-06-27 16:06:19 +02:00
|
|
|
.catch((e: Error) => {
|
2023-06-28 16:53:28 +02:00
|
|
|
throw new RunnerLoginUsernameInputError(e)
|
2023-06-27 16:06:19 +02:00
|
|
|
})
|
|
|
|
|
await this.page
|
|
|
|
|
.$('input[name=password]')
|
2023-07-28 19:50:04 +02:00
|
|
|
.then((i) => i?.type(password, { delay: this.getTypingDelay() }))
|
2023-06-27 16:06:19 +02:00
|
|
|
.catch((e: Error) => {
|
2023-06-28 16:53:28 +02:00
|
|
|
throw new RunnerLoginPasswordInputError(e)
|
2023-06-27 16:06:19 +02:00
|
|
|
})
|
|
|
|
|
await this.page
|
|
|
|
|
.$('button')
|
|
|
|
|
.then((b) => b?.click())
|
|
|
|
|
.catch((e: Error) => {
|
2023-06-28 16:53:28 +02:00
|
|
|
throw new RunnerLoginSubmitError(e)
|
2023-06-27 16:06:19 +02:00
|
|
|
})
|
2023-08-31 17:39:03 +02:00
|
|
|
await this.page.waitForNetworkIdle()
|
2023-06-27 16:06:19 +02:00
|
|
|
this.startSession(username)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async logout() {
|
2023-06-28 09:28:24 +02:00
|
|
|
this.loggerService.debug('Logging out')
|
2023-07-31 15:18:08 +02:00
|
|
|
await this.page.goto(
|
2023-08-10 13:35:32 +02:00
|
|
|
`${BAAN_RESERVEREN_ROOT_URL}/${BaanReserverenUrls.Logout}`,
|
2023-07-31 15:18:08 +02:00
|
|
|
)
|
2023-08-31 17:39:03 +02:00
|
|
|
await this.page.waitForNetworkIdle()
|
2023-06-27 16:06:19 +02:00
|
|
|
this.endSession()
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-30 10:36:59 +01:00
|
|
|
private async init() {
|
|
|
|
|
this.loggerService.debug('Initializing')
|
2023-07-31 15:18:08 +02:00
|
|
|
await this.page.goto(BAAN_RESERVEREN_ROOT_URL)
|
2023-09-21 09:28:22 +02:00
|
|
|
await this.page.waitForNetworkIdle()
|
2023-10-06 11:56:34 +02:00
|
|
|
const action = await this.checkSession(this.username)
|
2023-06-27 16:06:19 +02:00
|
|
|
switch (action) {
|
|
|
|
|
case SessionAction.Logout:
|
|
|
|
|
await this.logout()
|
2023-10-06 11:56:34 +02:00
|
|
|
await this.login(this.username, this.password)
|
2023-06-27 16:06:19 +02:00
|
|
|
break
|
|
|
|
|
case SessionAction.Login:
|
2023-10-06 11:56:34 +02:00
|
|
|
await this.login(this.username, this.password)
|
2023-06-27 16:06:19 +02:00
|
|
|
break
|
|
|
|
|
case SessionAction.NoAction:
|
|
|
|
|
default:
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getLastVisibleDay(): Dayjs {
|
|
|
|
|
const lastDayOfMonth = dayjs().add(1, 'month').set('date', 0)
|
|
|
|
|
let daysToAdd = 0
|
|
|
|
|
switch (lastDayOfMonth.day()) {
|
|
|
|
|
case 0:
|
|
|
|
|
daysToAdd = 0
|
|
|
|
|
break
|
|
|
|
|
default:
|
|
|
|
|
daysToAdd = 7 - lastDayOfMonth.day()
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
return lastDayOfMonth.add(daysToAdd, 'day')
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-28 19:50:04 +02:00
|
|
|
private async navigateToDay(date: Dayjs) {
|
2023-06-28 09:28:24 +02:00
|
|
|
this.loggerService.debug('Navigating to day', { date })
|
2023-06-27 16:06:19 +02:00
|
|
|
if (this.getLastVisibleDay().isBefore(date)) {
|
|
|
|
|
await this.page
|
2023-08-31 11:30:15 +02:00
|
|
|
.waitForSelector('td.month.next')
|
2023-06-27 16:06:19 +02:00
|
|
|
.then((d) => d?.click())
|
|
|
|
|
.catch((e: Error) => {
|
2024-03-19 10:07:07 +01:00
|
|
|
this.loggerService.error('Failed to switch months', {
|
|
|
|
|
error: e.message,
|
|
|
|
|
})
|
2023-06-27 16:06:19 +02:00
|
|
|
throw new RunnerNavigationMonthError(e)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
await this.page
|
2023-08-31 11:30:15 +02:00
|
|
|
.waitForSelector(
|
2023-06-27 16:06:19 +02:00
|
|
|
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get(
|
|
|
|
|
'date',
|
|
|
|
|
)}`,
|
|
|
|
|
)
|
|
|
|
|
.then((d) => d?.click())
|
|
|
|
|
.catch((e: Error) => {
|
2024-03-19 10:07:07 +01:00
|
|
|
this.loggerService.error('Failed to select day', {
|
|
|
|
|
error: e.message,
|
|
|
|
|
})
|
2023-06-27 16:06:19 +02:00
|
|
|
throw new RunnerNavigationDayError(e)
|
|
|
|
|
})
|
|
|
|
|
await this.page
|
2023-08-31 11:30:15 +02:00
|
|
|
.waitForSelector(
|
2023-06-27 16:06:19 +02:00
|
|
|
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get(
|
|
|
|
|
'date',
|
|
|
|
|
)}.selected`,
|
|
|
|
|
)
|
|
|
|
|
.catch((e: Error) => {
|
2023-06-28 16:25:40 +02:00
|
|
|
this.loggerService.error('Failed to wait for selected day', {
|
2024-03-19 10:07:07 +01:00
|
|
|
error: e.message,
|
2023-06-28 16:25:40 +02:00
|
|
|
})
|
2023-06-27 16:06:19 +02:00
|
|
|
throw new RunnerNavigationSelectionError(e)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-28 19:50:04 +02:00
|
|
|
private async navigateToWaitingList() {
|
2023-07-31 15:18:08 +02:00
|
|
|
this.loggerService.debug('Navigating to waiting list')
|
|
|
|
|
await this.page
|
2023-08-10 13:35:32 +02:00
|
|
|
.goto(`${BAAN_RESERVEREN_ROOT_URL}/${BaanReserverenUrls.WaitingList}`)
|
2023-07-31 15:18:08 +02:00
|
|
|
.catch((e) => {
|
|
|
|
|
throw new RunnerWaitingListNavigationError(e)
|
|
|
|
|
})
|
2023-09-20 11:43:52 +02:00
|
|
|
await this.page.waitForNetworkIdle()
|
2023-07-28 19:50:04 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-28 14:23:08 +02:00
|
|
|
private async navigateToReservations() {
|
2023-08-31 17:39:03 +02:00
|
|
|
this.loggerService.debug('Navigating to reservations')
|
2023-08-28 14:23:08 +02:00
|
|
|
await this.page
|
|
|
|
|
.goto(`${BAAN_RESERVEREN_ROOT_URL}/${BaanReserverenUrls.Reservations}`)
|
|
|
|
|
.catch((e) => {
|
|
|
|
|
throw new RunnerWaitingListNavigationError(e)
|
|
|
|
|
})
|
2023-09-20 11:43:52 +02:00
|
|
|
await this.page.waitForNetworkIdle()
|
2023-08-28 14:23:08 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-31 11:30:15 +02:00
|
|
|
private async recordWaitingListEntries(): Promise<number[]> {
|
|
|
|
|
const waitingListEntriesElements = await this.page.$$(
|
|
|
|
|
'#content tbody tr td:nth-child(1)',
|
|
|
|
|
)
|
|
|
|
|
if (!waitingListEntriesElements) return []
|
|
|
|
|
|
|
|
|
|
const waitingListIds = (
|
|
|
|
|
await Promise.all(
|
|
|
|
|
waitingListEntriesElements.map(async (e) => {
|
|
|
|
|
const elementTextContent = await (
|
|
|
|
|
await e.getProperty('textContent')
|
|
|
|
|
).jsonValue()
|
|
|
|
|
|
|
|
|
|
if (elementTextContent) {
|
|
|
|
|
return Number.parseInt(elementTextContent)
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
).filter((id): id is number => id != null && typeof id === 'number')
|
|
|
|
|
|
|
|
|
|
return waitingListIds
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private findNewWaitingListEntryId(
|
|
|
|
|
previous: number[],
|
|
|
|
|
current: number[],
|
|
|
|
|
): number | null {
|
|
|
|
|
const previousSet = new Set(previous)
|
|
|
|
|
for (const c of current) {
|
|
|
|
|
if (!previousSet.has(c)) return c
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async deleteWaitingListEntryRowById(id: number) {
|
|
|
|
|
const rows = await this.page.$x(`//td[text()="${id}"]/parent::tr`)
|
|
|
|
|
if (rows.length === 0) {
|
2023-09-14 09:56:51 +02:00
|
|
|
this.loggerService.error('Cannot find waiting list entry to delete')
|
|
|
|
|
return
|
2023-08-31 11:30:15 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-31 17:39:03 +02:00
|
|
|
const acceptedDialogPromise = new Promise<void>((res, rej) => {
|
2024-04-02 14:15:01 +02:00
|
|
|
this.page.once('dialog', async (dialog) => {
|
2023-08-31 17:39:03 +02:00
|
|
|
await dialog.accept().catch(rej)
|
|
|
|
|
res()
|
|
|
|
|
})
|
|
|
|
|
setTimeout(rej, 10000)
|
|
|
|
|
})
|
|
|
|
|
|
2023-08-31 11:30:15 +02:00
|
|
|
const deleteButton = await rows[0].$('a.wl-delete')
|
|
|
|
|
await deleteButton?.click()
|
2023-08-31 17:39:03 +02:00
|
|
|
await acceptedDialogPromise
|
2023-08-31 11:30:15 +02:00
|
|
|
}
|
|
|
|
|
|
2023-07-28 19:50:04 +02:00
|
|
|
private async openWaitingListDialog() {
|
2023-07-31 15:18:08 +02:00
|
|
|
this.loggerService.debug('Opening waiting list dialog')
|
2023-08-10 13:35:32 +02:00
|
|
|
await this.page.goto(
|
|
|
|
|
`${BAAN_RESERVEREN_ROOT_URL}/${BaanReserverenUrls.WaitingListAdd}`,
|
|
|
|
|
)
|
2023-09-20 11:43:52 +02:00
|
|
|
await this.page.waitForNetworkIdle()
|
2023-07-28 19:50:04 +02:00
|
|
|
}
|
|
|
|
|
|
2023-10-06 12:02:05 +02:00
|
|
|
// As a wise man once said, «Из говна и палок»
|
2024-01-30 10:36:59 +01:00
|
|
|
private async sortCourtsByRank(
|
2023-10-06 11:56:34 +02:00
|
|
|
freeCourts: ElementHandle<Element>[],
|
|
|
|
|
): Promise<ElementHandle | null> {
|
2024-01-30 10:36:59 +01:00
|
|
|
if (freeCourts.length === 0) return null
|
|
|
|
|
const freeCourtsWithJson = await Promise.all(
|
|
|
|
|
freeCourts.map(async (fc) => ({
|
|
|
|
|
elementHAndle: fc,
|
|
|
|
|
jsonValue: await fc.jsonValue(),
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
freeCourtsWithJson.sort(
|
|
|
|
|
(a, b) =>
|
|
|
|
|
(CourtRank[a.jsonValue.slot as CourtSlot] ?? 99) -
|
|
|
|
|
(CourtRank[b.jsonValue.slot as CourtSlot] ?? 99),
|
|
|
|
|
)
|
|
|
|
|
return freeCourtsWithJson[0].elementHAndle
|
2023-10-06 11:56:34 +02:00
|
|
|
}
|
|
|
|
|
|
2023-07-28 19:50:04 +02:00
|
|
|
private async selectAvailableTime(reservation: Reservation) {
|
2023-06-28 21:40:29 +02:00
|
|
|
this.loggerService.debug('Selecting available time', {
|
|
|
|
|
reservation: instanceToPlain(reservation),
|
|
|
|
|
})
|
2023-06-27 16:06:19 +02:00
|
|
|
let freeCourt: ElementHandle | null | undefined
|
|
|
|
|
let i = 0
|
|
|
|
|
const possibleDates = reservation.createPossibleDates()
|
2023-06-28 09:28:24 +02:00
|
|
|
this.loggerService.debug('Possible dates', { possibleDates })
|
2023-06-27 16:06:19 +02:00
|
|
|
while (i < possibleDates.length && !freeCourt) {
|
|
|
|
|
const possibleDate = possibleDates[i]
|
|
|
|
|
const timeString = possibleDate.format('HH:mm')
|
|
|
|
|
const selector =
|
|
|
|
|
`tr[data-time='${timeString}']` + `> td.free[rowspan='3'][type='free']`
|
2023-10-06 11:56:34 +02:00
|
|
|
const freeCourts = await this.page.$$(selector)
|
2024-01-30 10:36:59 +01:00
|
|
|
freeCourt = await this.sortCourtsByRank(freeCourts)
|
2023-06-27 16:06:19 +02:00
|
|
|
i++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!freeCourt) {
|
2023-08-10 13:35:32 +02:00
|
|
|
throw new NoCourtAvailableError('No court available for reservation')
|
2023-06-27 16:06:19 +02:00
|
|
|
}
|
|
|
|
|
|
2023-06-28 21:40:29 +02:00
|
|
|
this.loggerService.debug('Free court found')
|
2023-06-28 09:28:24 +02:00
|
|
|
|
2023-06-27 16:06:19 +02:00
|
|
|
await freeCourt.click().catch((e: Error) => {
|
|
|
|
|
throw new RunnerCourtSelectionError(e)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-06 11:56:34 +02:00
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-28 19:50:04 +02:00
|
|
|
private async selectOpponent(id: string, name: string) {
|
2023-06-28 09:28:24 +02:00
|
|
|
this.loggerService.debug('Selecting opponent', { id, name })
|
2023-06-27 16:06:19 +02:00
|
|
|
const player2Search = await this.page
|
2023-08-31 11:30:15 +02:00
|
|
|
.waitForSelector('input:has(~ select[name="players[2]"])')
|
2023-06-27 16:06:19 +02:00
|
|
|
.catch((e: Error) => {
|
|
|
|
|
throw new RunnerOpponentSearchError(e)
|
|
|
|
|
})
|
2023-07-28 19:50:04 +02:00
|
|
|
await player2Search
|
|
|
|
|
?.type(name, { delay: this.getTypingDelay() })
|
|
|
|
|
.catch((e: Error) => {
|
|
|
|
|
throw new RunnerOpponentSearchInputError(e)
|
|
|
|
|
})
|
2023-08-31 11:30:15 +02:00
|
|
|
await this.page.waitForNetworkIdle().catch((e: Error) => {
|
2023-06-27 16:06:19 +02:00
|
|
|
throw new RunnerOpponentSearchNetworkError(e)
|
|
|
|
|
})
|
|
|
|
|
await this.page
|
2023-08-31 11:30:15 +02:00
|
|
|
.$('select.br-user-select[name="players[2]"]')
|
2023-06-27 16:06:19 +02:00
|
|
|
.then((d) => d?.select(id))
|
|
|
|
|
.catch((e: Error) => {
|
|
|
|
|
throw new RunnerOpponentSearchSelectionError(e)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-28 19:50:04 +02:00
|
|
|
private async confirmReservation() {
|
2023-06-28 09:28:24 +02:00
|
|
|
this.loggerService.debug('Confirming reservation')
|
2023-06-27 16:06:19 +02:00
|
|
|
await this.page
|
2023-08-31 11:30:15 +02:00
|
|
|
.$('input#__make_submit')
|
2023-06-27 16:06:19 +02:00
|
|
|
.then((b) => b?.click())
|
|
|
|
|
.catch((e: Error) => {
|
|
|
|
|
throw new RunnerReservationConfirmButtonError(e)
|
|
|
|
|
})
|
|
|
|
|
await this.page
|
2023-08-31 11:30:15 +02:00
|
|
|
.waitForSelector('input#__make_submit2')
|
2023-06-27 16:06:19 +02:00
|
|
|
.then((b) => b?.click())
|
|
|
|
|
.catch((e: Error) => {
|
|
|
|
|
throw new RunnerReservationConfirmSubmitError(e)
|
|
|
|
|
})
|
2023-09-21 09:28:22 +02:00
|
|
|
await this.page.waitForNetworkIdle()
|
2023-06-27 16:06:19 +02:00
|
|
|
}
|
|
|
|
|
|
2023-07-28 19:50:04 +02:00
|
|
|
private async inputWaitingListDetails(reservation: Reservation) {
|
2023-07-31 15:18:08 +02:00
|
|
|
this.loggerService.debug('Inputting waiting list details')
|
2023-08-31 11:30:15 +02:00
|
|
|
const startDateInput = await this.page.$('input[name="start_date"]')
|
2023-07-31 15:18:08 +02:00
|
|
|
// Click 3 times to select all existing text
|
|
|
|
|
await startDateInput?.click({ count: 3, delay: 10 }).catch((e) => {
|
|
|
|
|
throw new RunnerWaitingListInputError(e)
|
|
|
|
|
})
|
|
|
|
|
await startDateInput
|
|
|
|
|
?.type(reservation.dateRangeStart.format('DD-MM-YYYY'), {
|
|
|
|
|
delay: this.getTypingDelay(),
|
|
|
|
|
})
|
|
|
|
|
.catch((e) => {
|
|
|
|
|
throw new RunnerWaitingListInputError(e)
|
|
|
|
|
})
|
2023-07-28 19:50:04 +02:00
|
|
|
|
2023-08-31 11:30:15 +02:00
|
|
|
const endDateInput = await this.page.$('input[name="end_date"]')
|
2023-07-31 15:18:08 +02:00
|
|
|
await endDateInput
|
|
|
|
|
?.type(reservation.dateRangeEnd.format('DD-MM-YYYY'), {
|
|
|
|
|
delay: this.getTypingDelay(),
|
|
|
|
|
})
|
|
|
|
|
.catch((e) => {
|
|
|
|
|
throw new RunnerWaitingListInputError(e)
|
|
|
|
|
})
|
2023-07-28 19:50:04 +02:00
|
|
|
|
2023-08-31 11:30:15 +02:00
|
|
|
const startTimeInput = await this.page.$('input[name="start_time"]')
|
2023-08-10 13:35:32 +02:00
|
|
|
await startTimeInput
|
2023-07-31 15:18:08 +02:00
|
|
|
?.type(reservation.dateRangeStart.format('HH:mm'), {
|
|
|
|
|
delay: this.getTypingDelay(),
|
|
|
|
|
})
|
|
|
|
|
.catch((e) => {
|
|
|
|
|
throw new RunnerWaitingListInputError(e)
|
|
|
|
|
})
|
2023-07-28 19:50:04 +02:00
|
|
|
|
2023-09-20 12:52:17 +02:00
|
|
|
// Use one reservation (45m) as end time because it seems to work better
|
2023-08-31 11:30:15 +02:00
|
|
|
const endTimeInput = await this.page.$('input[name="end_time"]')
|
2023-08-10 13:35:32 +02:00
|
|
|
await endTimeInput
|
2023-09-20 12:52:17 +02:00
|
|
|
?.type(reservation.dateRangeStart.add(45, 'minutes').format('HH:mm'), {
|
2023-07-31 15:18:08 +02:00
|
|
|
delay: this.getTypingDelay(),
|
|
|
|
|
})
|
|
|
|
|
.catch((e) => {
|
|
|
|
|
throw new RunnerWaitingListInputError(e)
|
|
|
|
|
})
|
2023-07-28 19:50:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async confirmWaitingListDetails() {
|
2023-07-31 15:18:08 +02:00
|
|
|
this.loggerService.debug('Confirming waiting list details')
|
2023-08-31 17:39:03 +02:00
|
|
|
const saveButton = await this.page.$('input[type="submit"]')
|
2023-07-31 15:18:08 +02:00
|
|
|
await saveButton?.click().catch((e) => {
|
|
|
|
|
throw new RunnerWaitingListConfirmError(e)
|
|
|
|
|
})
|
2023-09-20 11:43:52 +02:00
|
|
|
await this.page.waitForNetworkIdle()
|
2023-07-28 19:50:04 +02:00
|
|
|
}
|
|
|
|
|
|
2023-06-27 16:06:19 +02:00
|
|
|
public async performReservation(reservation: Reservation) {
|
2024-02-23 07:26:29 -06:00
|
|
|
try {
|
2024-01-30 10:36:59 +01:00
|
|
|
await this.init()
|
2024-02-23 07:26:29 -06:00
|
|
|
await this.navigateToDay(reservation.dateRangeStart)
|
2024-03-08 16:33:13 +01:00
|
|
|
await this.monitorCourtReservations()
|
2024-02-23 07:26:29 -06:00
|
|
|
await this.selectAvailableTime(reservation)
|
|
|
|
|
await this.selectOwner(reservation.ownerId)
|
|
|
|
|
await this.selectOpponent(
|
|
|
|
|
reservation.opponentId,
|
|
|
|
|
reservation.opponentName,
|
|
|
|
|
)
|
|
|
|
|
await this.confirmReservation()
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
await this.handleError()
|
|
|
|
|
throw error
|
|
|
|
|
}
|
2023-06-27 16:06:19 +02:00
|
|
|
}
|
2023-07-28 19:50:04 +02:00
|
|
|
|
|
|
|
|
public async addReservationToWaitList(reservation: Reservation) {
|
2024-02-23 07:26:29 -06:00
|
|
|
try {
|
2024-01-30 10:36:59 +01:00
|
|
|
await this.init()
|
2024-02-23 07:26:29 -06:00
|
|
|
await this.navigateToWaitingList()
|
|
|
|
|
const previousWaitingListIds = await this.recordWaitingListEntries()
|
|
|
|
|
await this.openWaitingListDialog()
|
|
|
|
|
await this.inputWaitingListDetails(reservation)
|
|
|
|
|
await this.confirmWaitingListDetails()
|
|
|
|
|
await this.navigateToWaitingList()
|
|
|
|
|
const currentWaitingListIds = await this.recordWaitingListEntries()
|
|
|
|
|
const waitingListId = this.findNewWaitingListEntryId(
|
|
|
|
|
previousWaitingListIds,
|
|
|
|
|
currentWaitingListIds,
|
2023-08-31 11:30:15 +02:00
|
|
|
)
|
|
|
|
|
|
2024-02-23 07:26:29 -06:00
|
|
|
if (waitingListId == null) {
|
|
|
|
|
throw new WaitingListSubmissionError(
|
|
|
|
|
'Failed to find new waiting list entry',
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return waitingListId
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
await this.handleError()
|
|
|
|
|
throw error
|
|
|
|
|
}
|
2023-08-31 11:30:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async removeReservationFromWaitList(reservation: Reservation) {
|
2024-02-23 07:26:29 -06:00
|
|
|
try {
|
|
|
|
|
if (!reservation.waitListed || !reservation.waitingListId) return
|
2024-01-30 10:36:59 +01:00
|
|
|
await this.init()
|
2024-02-23 07:26:29 -06:00
|
|
|
await this.navigateToWaitingList()
|
|
|
|
|
await this.deleteWaitingListEntryRowById(reservation.waitingListId)
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
await this.handleError()
|
|
|
|
|
throw error
|
|
|
|
|
}
|
2023-07-28 19:50:04 +02:00
|
|
|
}
|
2024-01-30 10:36:59 +01:00
|
|
|
|
|
|
|
|
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) {
|
2024-03-18 17:16:50 +01:00
|
|
|
const classListObj = await (
|
|
|
|
|
await court.getProperty('classList')
|
|
|
|
|
).jsonValue()
|
|
|
|
|
const classList = Object.values(classListObj)
|
|
|
|
|
const rClass = classList.filter((cl) => /r-\d{2}/.test(cl))[0]
|
2024-01-30 10:36:59 +01:00
|
|
|
const courtNumber =
|
2024-03-18 17:16:50 +01:00
|
|
|
`${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());
|
2024-01-30 10:36:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return courtStatuses
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-08 16:33:13 +01:00
|
|
|
public async monitorCourtReservations(date?: Dayjs, swallowError = true) {
|
|
|
|
|
try {
|
|
|
|
|
if (date) {
|
|
|
|
|
await this.init()
|
|
|
|
|
await this.navigateToDay(date)
|
|
|
|
|
}
|
2024-03-14 08:06:51 +01:00
|
|
|
const statuses = await this.getAllCourtStatuses()
|
|
|
|
|
await this.monitoringQueue.add({
|
|
|
|
|
type: MonitorType.CourtReservations,
|
|
|
|
|
data: statuses,
|
|
|
|
|
})
|
2024-03-08 16:33:13 +01:00
|
|
|
} catch (error: unknown) {
|
2024-03-18 17:16:50 +01:00
|
|
|
this.loggerService.error(
|
|
|
|
|
`Failed to monitor court reservations: ${(error as Error).message}`,
|
|
|
|
|
)
|
2024-03-08 16:33:13 +01:00
|
|
|
if (!swallowError) {
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-30 10:36:59 +01:00
|
|
|
}
|
2021-11-17 14:17:25 +01:00
|
|
|
}
|
2023-01-30 12:38:42 +01:00
|
|
|
|
2023-05-26 15:43:14 -05:00
|
|
|
export class RunnerError extends Error {
|
2023-08-10 13:35:32 +02:00
|
|
|
constructor(error: Error, name?: string) {
|
2023-06-27 16:06:19 +02:00
|
|
|
super(error.message)
|
|
|
|
|
this.stack = error.stack
|
2023-08-10 13:35:32 +02:00
|
|
|
this.name = name ?? 'RunnerError'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class PuppeteerError extends RunnerError {
|
|
|
|
|
constructor(error: Error, name?: string) {
|
|
|
|
|
super(error, name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class PuppeteerBrowserLaunchError extends PuppeteerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'PuppeteerBrowserLaunchError')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class PuppeteerNewPageError extends PuppeteerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'PuppeteerNewPageError')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class RunnerNewSessionError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerNewSessionError')
|
2023-06-27 16:06:19 +02:00
|
|
|
}
|
2023-01-29 14:34:39 +01:00
|
|
|
}
|
|
|
|
|
|
2023-08-10 13:35:32 +02:00
|
|
|
export class RunnerLogoutError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerLogoutError')
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-02-20 11:08:56 +01:00
|
|
|
|
2023-08-10 13:35:32 +02:00
|
|
|
export class RunnerLoginNavigationError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerLoginNavigationError')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class RunnerLoginUsernameInputError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerLoginUsernameInputError')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class RunnerLoginPasswordInputError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerLoginPasswordInputError')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class RunnerLoginSubmitError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerLoginSubmitError')
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-02-10 12:24:27 +01:00
|
|
|
|
2023-08-10 13:35:32 +02:00
|
|
|
export class RunnerNavigationMonthError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerNavigationMonthError')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class RunnerNavigationDayError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerNavigationDayError')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class RunnerNavigationSelectionError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerNavigationSelectionError')
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-29 14:34:39 +01:00
|
|
|
|
2023-08-10 13:35:32 +02:00
|
|
|
export class RunnerWaitingListNavigationError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerWaitingListNavigationError')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class RunnerWaitingListNavigationMenuError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerWaitingListNavigationMenuError')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class RunnerWaitingListNavigationAddError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerWaitingListNavigationAddError')
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-29 14:34:39 +01:00
|
|
|
|
2023-08-10 13:35:32 +02:00
|
|
|
export class RunnerWaitingListInputError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerWaitingListInputError')
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-31 15:18:08 +02:00
|
|
|
|
2023-08-10 13:35:32 +02:00
|
|
|
export class RunnerWaitingListConfirmError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerWaitingListConfirmError')
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-31 15:18:08 +02:00
|
|
|
|
2023-08-10 13:35:32 +02:00
|
|
|
export class RunnerCourtSelectionError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerCourtSelectionError')
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-31 15:18:08 +02:00
|
|
|
|
2023-08-10 13:35:32 +02:00
|
|
|
export class NoCourtAvailableError extends Error {
|
|
|
|
|
constructor(message: string) {
|
|
|
|
|
super(message)
|
|
|
|
|
this.name = 'NoCourtAvailableError'
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-29 14:34:39 +01:00
|
|
|
|
2023-10-06 11:56:34 +02:00
|
|
|
export class RunnerOwnerSearchNetworkError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerOwnerSearchNetworkError')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class RunnerOwnerSearchSelectionError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerOwnerSearchSelectionError')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-10 13:35:32 +02:00
|
|
|
export class RunnerOpponentSearchError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerOpponentSearchError')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class RunnerOpponentSearchInputError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerOpponentSearchInputError')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class RunnerOpponentSearchNetworkError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerOpponentSearchNetworkError')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class RunnerOpponentSearchSelectionError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerOpponentSearchSelectionError')
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-29 14:34:39 +01:00
|
|
|
|
2023-08-10 13:35:32 +02:00
|
|
|
export class RunnerReservationConfirmButtonError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerReservationConfirmButtonError')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class RunnerReservationConfirmSubmitError extends RunnerError {
|
|
|
|
|
constructor(error: Error) {
|
|
|
|
|
super(error, 'RunnerReservationConfirmSubmitError')
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-31 11:30:15 +02:00
|
|
|
|
|
|
|
|
export class SessionStartError extends Error {
|
|
|
|
|
constructor(message: string) {
|
|
|
|
|
super(message)
|
|
|
|
|
this.name = 'SessionStartError'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class WaitingListSubmissionError extends Error {
|
|
|
|
|
constructor(message: string) {
|
|
|
|
|
super(message)
|
|
|
|
|
this.name = 'WaitingListSubmissionError'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class WaitingListEntryDeletionError extends Error {
|
|
|
|
|
constructor(message: string) {
|
|
|
|
|
super(message)
|
|
|
|
|
this.name = 'WaitingListEntryDeletionError'
|
|
|
|
|
}
|
|
|
|
|
}
|