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:
parent
878006271f
commit
3219256961
4 changed files with 128 additions and 35 deletions
|
|
@ -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,24 +14,29 @@ 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) {
|
||||||
|
l.getStore()?.debug('Trying to perform reservation', {
|
||||||
|
reservationToPerform: reservationToPerform.toString(true),
|
||||||
|
})
|
||||||
const runner = getRunner()
|
const runner = getRunner()
|
||||||
try {
|
try {
|
||||||
await runner.run(reservationToPerform)
|
await runner.run(reservationToPerform)
|
||||||
await reservationToPerform.delete()
|
await reservationToPerform.delete()
|
||||||
return true
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
l.getStore()?.error('Failed to perform reservation', {
|
l.getStore()?.error('Failed to perform reservation', {
|
||||||
error: (error as LoggableError).toString(),
|
error: (error as LoggableError).toString(),
|
||||||
|
|
@ -38,3 +44,6 @@ export const reserve = async (reservation?: Reservation): Promise<boolean> => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sessionAction = await this.checkSession(reservation.user.username)
|
||||||
|
switch (sessionAction) {
|
||||||
|
case SessionAction.Login: {
|
||||||
await this.login(reservation.user.username, reservation.user.password)
|
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 {}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -49,19 +59,33 @@ export class ReservationsRouter extends Router {
|
||||||
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(
|
||||||
|
`[${reservations.map((r) => r.toString(true)).join(',')}]`,
|
||||||
|
(err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(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,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 { url = '' } = req
|
||||||
const [, , id] = url.split('/')
|
const [, , id] = url.split('/')
|
||||||
l.getStore()?.debug('Deleting reservation', { id })
|
l.getStore()?.debug('Deleting reservation', { id })
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue