Adding some additional logic to support multiple reservations during one runner request and allowing fetching reservations by date. Also changed cron to only run once at 7.00 and fetch all available reservations at that date

This commit is contained in:
Collin Duncan 2023-02-10 12:24:27 +01:00
parent 878006271f
commit 3219256961
No known key found for this signature in database
4 changed files with 128 additions and 35 deletions

View file

@ -1,3 +1,4 @@
import dayjs from './dayjs'
import { asyncLocalStorage as l, LoggableError } from './logger'
import { Reservation } from './reservation'
import { Runner } from './runner'
@ -13,24 +14,29 @@ const getRunner = () => {
return runner
}
export const reserve = async (reservation?: Reservation): Promise<boolean> => {
let reservationToPerform = reservation
if (!reservationToPerform) {
export const reserve = async (
reservations?: Reservation[]
): Promise<boolean> => {
let reservationsToPerform = reservations
if (!reservationsToPerform) {
l.getStore()?.debug('No reservation provided, fetching first in database')
reservationToPerform = (await Reservation.fetchFirst()) || undefined
reservationsToPerform =
(await Reservation.fetchByDate(dayjs())) || undefined
}
if (!reservationToPerform) {
if (!reservationsToPerform || reservationsToPerform.length === 0) {
l.getStore()?.info('No reservation to perform')
return true
}
l.getStore()?.debug('Trying to perform reservation', { reservationToPerform: reservationToPerform.toString(true) })
for (const reservationToPerform of reservationsToPerform) {
l.getStore()?.debug('Trying to perform reservation', {
reservationToPerform: reservationToPerform.toString(true),
})
const runner = getRunner()
try {
await runner.run(reservationToPerform)
await reservationToPerform.delete()
return true
} catch (error) {
l.getStore()?.error('Failed to perform reservation', {
error: (error as LoggableError).toString(),
@ -38,3 +44,6 @@ export const reserve = async (reservation?: Reservation): Promise<boolean> => {
return false
}
}
return true
}

View file

@ -11,12 +11,24 @@ import puppeteer, {
import { asyncLocalStorage as l, LoggableError } from './logger'
import { Opponent, Reservation } from './reservation'
enum SessionAction {
NoAction,
Logout,
Login,
}
interface RunnerSession {
username: string
loggedInAt: Dayjs
}
export class Runner {
private browser: Browser | undefined
private page: Page | undefined
private browser?: Browser
private page?: Page
private options?: LaunchOptions &
BrowserLaunchArgumentOptions &
BrowserConnectOptions
private session?: RunnerSession
constructor(
options?: LaunchOptions &
@ -42,10 +54,34 @@ export class Runner {
throw new PuppeteerNewPageError(error as Error)
}
const sessionAction = await this.checkSession(reservation.user.username)
switch (sessionAction) {
case SessionAction.Login: {
await this.login(reservation.user.username, reservation.user.password)
break
}
case SessionAction.Logout: {
await this.logout()
await this.login(reservation.user.username, reservation.user.password)
break
}
case SessionAction.NoAction:
default:
break
}
await this.makeReservation(reservation)
}
private async checkSession(username: string): Promise<SessionAction> {
if (this.page?.url().startsWith('https://squashcity.baanreserveren.nl/')) {
l.getStore()?.info('Already logged in', { username })
return this.session?.username !== username
? SessionAction.Logout
: SessionAction.NoAction
}
return SessionAction.Login
}
private async login(username: string, password: string) {
l.getStore()?.debug('Logging in', { username })
await this.page
@ -73,6 +109,15 @@ export class Runner {
})
}
private async logout() {
l.getStore()?.debug('Logging out', { username: this.session?.username })
await this.page
?.goto('https://squashcity.baanreserveren.nl/auth/logout')
.catch((e: Error) => {
throw new RunnerLoginPasswordInputError(e)
})
}
private async makeReservation(reservation: Reservation) {
await this.navigateToDay(reservation.dateRange.start)
await this.selectAvailableTime(reservation)
@ -130,7 +175,9 @@ export class Runner {
}
private async selectAvailableTime(res: Reservation): Promise<void> {
l.getStore()?.debug('Selecting available time', { reservation: res.toString(true) })
l.getStore()?.debug('Selecting available time', {
reservation: res.toString(true),
})
let freeCourt: ElementHandle | null | undefined
let i = 0
while (i < res.possibleDates.length && !freeCourt) {
@ -198,6 +245,8 @@ export class PuppeteerError extends RunnerError {}
export class PuppeteerBrowserLaunchError extends PuppeteerError {}
export class PuppeteerNewPageError extends PuppeteerError {}
export class RunnerLogoutError extends RunnerError {}
export class RunnerLoginNavigationError extends RunnerError {}
export class RunnerLoginUsernameInputError extends RunnerError {}
export class RunnerLoginPasswordInputError extends RunnerError {}

View file

@ -1,4 +1,9 @@
import { schedule, ScheduledTask, ScheduleOptions, getTasks as getCronTasks } from 'node-cron'
import {
schedule,
ScheduledTask,
ScheduleOptions,
getTasks as getCronTasks,
} from 'node-cron'
import { v4 } from 'uuid'
import { asyncLocalStorage, Logger, LogLevel } from '../common/logger'
import { reserve } from '../common/reserver'
@ -26,7 +31,7 @@ export const startTasks = () => {
try {
if (tasks.length === 0) {
const task = schedule(
'* 7 * * *',
'0 7 * * *',
async (timestamp) => {
asyncLocalStorage.run(
new Logger('cron', v4(), LogLevel.DEBUG),

View file

@ -4,11 +4,13 @@ import { asyncLocalStorage as l } from '../../../common/logger'
import { Router } from './index'
import { Reservation } from '../../../common/reservation'
import { parse } from 'querystring'
import dayjs from '../../../common/dayjs'
import { Dayjs } from 'dayjs'
export class ReservationsRouter extends Router {
public async handleRequest(
req: IncomingMessage,
res: ServerResponse<IncomingMessage>,
res: ServerResponse<IncomingMessage>
) {
const { url = '', method } = req
const [route] = url.split('?')
@ -31,12 +33,20 @@ export class ReservationsRouter extends Router {
}
}
private async GET_reservations(req: IncomingMessage, res: ServerResponse<IncomingMessage>) {
private async GET_reservations(
req: IncomingMessage,
res: ServerResponse<IncomingMessage>
) {
const { url = '' } = req
const [, queryParams] = url.split('?')
let pageNumber = 0
let pageSize = 0
const { pageNumber: rawPageNumber = '0', pageSize: rawPageSize = '50' } = parse(queryParams)
let date: Dayjs | undefined = undefined
const {
pageNumber: rawPageNumber = '0',
pageSize: rawPageSize = '50',
date: rawDate,
} = parse(queryParams)
if (typeof rawPageNumber === 'string') {
pageNumber = Number.parseInt(rawPageNumber)
} else {
@ -49,19 +59,33 @@ export class ReservationsRouter extends Router {
pageSize = 50
}
if (typeof rawDate === 'string') {
date = dayjs(rawDate)
}
l.getStore()?.debug('Fetching reservations', { pageNumber, pageSize })
try {
const reservations = await Reservation.fetchByPage(pageNumber, pageSize)
let reservations: Reservation[]
if (date) {
reservations = await Reservation.fetchByDate(date, 50)
} else {
reservations = await Reservation.fetchByPage(pageNumber, pageSize)
}
res.setHeader('content-type', 'application/json')
l.getStore()?.debug('Found reservations', { reservations: reservations.map((r) => r.toString(true)) })
l.getStore()?.debug('Found reservations', {
reservations: reservations.map((r) => r.toString(true)),
})
return new Promise<void>((resolve, reject) => {
res.write(`[${reservations.map((r) => r.toString(true)).join(',')}]`, (err) => {
res.write(
`[${reservations.map((r) => r.toString(true)).join(',')}]`,
(err) => {
if (err) {
reject(err)
}
resolve()
})
}
)
})
} catch (error) {
l.getStore()?.error('Failed to get reservations', {
@ -72,7 +96,10 @@ export class ReservationsRouter extends Router {
}
}
private async POST_reservations(req: IncomingMessage, res: ServerResponse<IncomingMessage>) {
private async POST_reservations(
req: IncomingMessage,
res: ServerResponse<IncomingMessage>
) {
const jsonBody = await this.parseJsonContent(req)
try {
await schedule(jsonBody)
@ -85,7 +112,10 @@ export class ReservationsRouter extends Router {
}
}
private async DELETE_reservation(req: IncomingMessage, res: ServerResponse<IncomingMessage>) {
private async DELETE_reservation(
req: IncomingMessage,
res: ServerResponse<IncomingMessage>
) {
const { url = '' } = req
const [, , id] = url.split('/')
l.getStore()?.debug('Deleting reservation', { id })