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 { asyncLocalStorage as l, LoggableError } from './logger'
import { Reservation } from './reservation' import { Reservation } from './reservation'
import { Runner } from './runner' import { Runner } from './runner'
@ -13,28 +14,36 @@ const getRunner = () => {
return runner return runner
} }
export const reserve = async (reservation?: Reservation): Promise<boolean> => { export const reserve = async (
let reservationToPerform = reservation reservations?: Reservation[]
if (!reservationToPerform) { ): Promise<boolean> => {
let reservationsToPerform = reservations
if (!reservationsToPerform) {
l.getStore()?.debug('No reservation provided, fetching first in database') 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') l.getStore()?.info('No reservation to perform')
return true return true
} }
l.getStore()?.debug('Trying to perform reservation', { reservationToPerform: reservationToPerform.toString(true) }) for (const reservationToPerform of reservationsToPerform) {
const runner = getRunner() l.getStore()?.debug('Trying to perform reservation', {
try { reservationToPerform: reservationToPerform.toString(true),
await runner.run(reservationToPerform)
await reservationToPerform.delete()
return true
} catch (error) {
l.getStore()?.error('Failed to perform reservation', {
error: (error as LoggableError).toString(),
}) })
return false const runner = getRunner()
try {
await runner.run(reservationToPerform)
await reservationToPerform.delete()
} catch (error) {
l.getStore()?.error('Failed to perform reservation', {
error: (error as LoggableError).toString(),
})
return false
}
} }
return true
} }

View file

@ -11,12 +11,24 @@ import puppeteer, {
import { asyncLocalStorage as l, LoggableError } from './logger' import { asyncLocalStorage as l, LoggableError } from './logger'
import { Opponent, Reservation } from './reservation' import { Opponent, Reservation } from './reservation'
enum SessionAction {
NoAction,
Logout,
Login,
}
interface RunnerSession {
username: string
loggedInAt: Dayjs
}
export class Runner { export class Runner {
private browser: Browser | undefined private browser?: Browser
private page: Page | undefined private page?: Page
private options?: LaunchOptions & private options?: LaunchOptions &
BrowserLaunchArgumentOptions & BrowserLaunchArgumentOptions &
BrowserConnectOptions BrowserConnectOptions
private session?: RunnerSession
constructor( constructor(
options?: LaunchOptions & options?: LaunchOptions &
@ -42,10 +54,34 @@ export class Runner {
throw new PuppeteerNewPageError(error as Error) throw new PuppeteerNewPageError(error as Error)
} }
await this.login(reservation.user.username, reservation.user.password) 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) 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) { private async login(username: string, password: string) {
l.getStore()?.debug('Logging in', { username }) l.getStore()?.debug('Logging in', { username })
await this.page 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) { private async makeReservation(reservation: Reservation) {
await this.navigateToDay(reservation.dateRange.start) await this.navigateToDay(reservation.dateRange.start)
await this.selectAvailableTime(reservation) await this.selectAvailableTime(reservation)
@ -130,7 +175,9 @@ export class Runner {
} }
private async selectAvailableTime(res: Reservation): Promise<void> { 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 freeCourt: ElementHandle | null | undefined
let i = 0 let i = 0
while (i < res.possibleDates.length && !freeCourt) { while (i < res.possibleDates.length && !freeCourt) {
@ -198,6 +245,8 @@ export class PuppeteerError extends RunnerError {}
export class PuppeteerBrowserLaunchError extends PuppeteerError {} export class PuppeteerBrowserLaunchError extends PuppeteerError {}
export class PuppeteerNewPageError extends PuppeteerError {} export class PuppeteerNewPageError extends PuppeteerError {}
export class RunnerLogoutError extends RunnerError {}
export class RunnerLoginNavigationError extends RunnerError {} export class RunnerLoginNavigationError extends RunnerError {}
export class RunnerLoginUsernameInputError extends RunnerError {} export class RunnerLoginUsernameInputError extends RunnerError {}
export class RunnerLoginPasswordInputError 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 { v4 } from 'uuid'
import { asyncLocalStorage, Logger, LogLevel } from '../common/logger' import { asyncLocalStorage, Logger, LogLevel } from '../common/logger'
import { reserve } from '../common/reserver' import { reserve } from '../common/reserver'
@ -26,7 +31,7 @@ export const startTasks = () => {
try { try {
if (tasks.length === 0) { if (tasks.length === 0) {
const task = schedule( const task = schedule(
'* 7 * * *', '0 7 * * *',
async (timestamp) => { async (timestamp) => {
asyncLocalStorage.run( asyncLocalStorage.run(
new Logger('cron', v4(), LogLevel.DEBUG), new Logger('cron', v4(), LogLevel.DEBUG),

View file

@ -4,11 +4,13 @@ import { asyncLocalStorage as l } from '../../../common/logger'
import { Router } from './index' import { Router } from './index'
import { Reservation } from '../../../common/reservation' import { Reservation } from '../../../common/reservation'
import { parse } from 'querystring' import { parse } from 'querystring'
import dayjs from '../../../common/dayjs'
import { Dayjs } from 'dayjs'
export class ReservationsRouter extends Router { export class ReservationsRouter extends Router {
public async handleRequest( public async handleRequest(
req: IncomingMessage, req: IncomingMessage,
res: ServerResponse<IncomingMessage>, res: ServerResponse<IncomingMessage>
) { ) {
const { url = '', method } = req const { url = '', method } = req
const [route] = url.split('?') 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 { url = '' } = req
const [, queryParams] = url.split('?') const [, queryParams] = url.split('?')
let pageNumber = 0 let pageNumber = 0
let pageSize = 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') { if (typeof rawPageNumber === 'string') {
pageNumber = Number.parseInt(rawPageNumber) pageNumber = Number.parseInt(rawPageNumber)
} else { } else {
@ -48,20 +58,34 @@ export class ReservationsRouter extends Router {
} else { } else {
pageSize = 50 pageSize = 50
} }
if (typeof rawDate === 'string') {
date = dayjs(rawDate)
}
l.getStore()?.debug('Fetching reservations', { pageNumber, pageSize }) l.getStore()?.debug('Fetching reservations', { pageNumber, pageSize })
try { 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') 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) => { return new Promise<void>((resolve, reject) => {
res.write(`[${reservations.map((r) => r.toString(true)).join(',')}]`, (err) => { res.write(
if (err) { `[${reservations.map((r) => r.toString(true)).join(',')}]`,
reject(err) (err) => {
if (err) {
reject(err)
}
resolve()
} }
resolve() )
})
}) })
} catch (error) { } catch (error) {
l.getStore()?.error('Failed to get reservations', { 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) const jsonBody = await this.parseJsonContent(req)
try { try {
await schedule(jsonBody) await schedule(jsonBody)
@ -85,9 +112,12 @@ 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 { url = '' } = req
const [,,id] = url.split('/') const [, , id] = url.split('/')
l.getStore()?.debug('Deleting reservation', { id }) l.getStore()?.debug('Deleting reservation', { id })
try { try {
await Reservation.deleteById(id) await Reservation.deleteById(id)