Formatting fixes
This commit is contained in:
parent
a3a2f12082
commit
9da2d5e2f2
14 changed files with 359 additions and 340 deletions
|
|
@ -25,7 +25,7 @@ import { LoggerModule } from './logger/module'
|
||||||
},
|
},
|
||||||
defaultJobOptions: {
|
defaultJobOptions: {
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,24 @@
|
||||||
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
CallHandler,
|
||||||
|
} from '@nestjs/common'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { map } from 'rxjs/operators'
|
import { map } from 'rxjs/operators'
|
||||||
|
|
||||||
export interface CustomResponse<T = unknown> {
|
export interface CustomResponse<T = unknown> {
|
||||||
data: T
|
data: T
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CustomResponseTransformInterceptor<T> implements NestInterceptor<T, CustomResponse<T>> {
|
export class CustomResponseTransformInterceptor<T>
|
||||||
intercept(_context: ExecutionContext, next: CallHandler): Observable<CustomResponse<T>> {
|
implements NestInterceptor<T, CustomResponse<T>>
|
||||||
return next.handle().pipe(map(data => ({ data })))
|
{
|
||||||
}
|
intercept(
|
||||||
|
_context: ExecutionContext,
|
||||||
|
next: CallHandler,
|
||||||
|
): Observable<CustomResponse<T>> {
|
||||||
|
return next.handle().pipe(map((data) => ({ data })))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -12,27 +12,27 @@ dayjs.locale('nl')
|
||||||
dayjs.tz.setDefault('Europe/Amsterdam')
|
dayjs.tz.setDefault('Europe/Amsterdam')
|
||||||
|
|
||||||
export interface DateRange {
|
export interface DateRange {
|
||||||
start: dayjs.Dayjs
|
start: dayjs.Dayjs
|
||||||
end: dayjs.Dayjs
|
end: dayjs.Dayjs
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SerializedDateRange {
|
export interface SerializedDateRange {
|
||||||
start: string
|
start: string
|
||||||
end: string
|
end: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const convertDateRangeStringToObject = ({
|
export const convertDateRangeStringToObject = ({
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
}: {
|
}: {
|
||||||
start: string
|
start: string
|
||||||
end: string
|
end: string
|
||||||
}): DateRange => ({ start: dayjs(start), end: dayjs(end) })
|
}): DateRange => ({ start: dayjs(start), end: dayjs(end) })
|
||||||
|
|
||||||
const dayjsTz = (
|
const dayjsTz = (
|
||||||
date?: string | number | Date | dayjs.Dayjs | null | undefined
|
date?: string | number | Date | dayjs.Dayjs | null | undefined,
|
||||||
) => {
|
) => {
|
||||||
return dayjs(date).tz()
|
return dayjs(date).tz()
|
||||||
}
|
}
|
||||||
|
|
||||||
export default dayjsTz
|
export default dayjsTz
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ import { LoggerService } from './service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LoggerMiddleware implements NestMiddleware {
|
export class LoggerMiddleware implements NestMiddleware {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(LoggerService)
|
@Inject(LoggerService)
|
||||||
private readonly logger: LoggerService,
|
private readonly logger: LoggerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
use(req: Request, _res: Response, next: NextFunction) {
|
use(req: Request, _res: Response, next: NextFunction) {
|
||||||
this.logger.log(`${req.method} ${req.originalUrl}`)
|
this.logger.log(`${req.method} ${req.originalUrl}`)
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
export const RESERVATIONS_QUEUE_NAME = 'reservations'
|
export const RESERVATIONS_QUEUE_NAME = 'reservations'
|
||||||
|
|
||||||
export default () => ({
|
export default () => ({
|
||||||
queueName: RESERVATIONS_QUEUE_NAME
|
queueName: RESERVATIONS_QUEUE_NAME,
|
||||||
})
|
})
|
||||||
|
|
@ -24,8 +24,12 @@ export class ReservationsCronService {
|
||||||
timeZone: 'Europe/Amsterdam',
|
timeZone: 'Europe/Amsterdam',
|
||||||
})
|
})
|
||||||
async handleDailyReservations() {
|
async handleDailyReservations() {
|
||||||
const reservationsToPerform = await this.reservationService.getByDate()
|
const reservationsToPerform = await this.reservationService.getByDate()
|
||||||
this.logger.log(`Found ${reservationsToPerform.length} reservations to perform`)
|
this.logger.log(
|
||||||
await this.reservationsQueue.addBulk(reservationsToPerform.map((r) => ({ data: r })))
|
`Found ${reservationsToPerform.length} reservations to perform`,
|
||||||
}
|
)
|
||||||
|
await this.reservationsQueue.addBulk(
|
||||||
|
reservationsToPerform.map((r) => ({ data: r })),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,33 +39,33 @@ export class Reservation {
|
||||||
|
|
||||||
@Exclude()
|
@Exclude()
|
||||||
public createPossibleDates(): Dayjs[] {
|
public createPossibleDates(): Dayjs[] {
|
||||||
const possibleDates: Dayjs[] = []
|
const possibleDates: Dayjs[] = []
|
||||||
|
|
||||||
let possibleDate = dayjs(this.dateRangeStart).second(0).millisecond(0)
|
let possibleDate = dayjs(this.dateRangeStart).second(0).millisecond(0)
|
||||||
while (possibleDate.isSameOrBefore(this.dateRangeEnd)) {
|
while (possibleDate.isSameOrBefore(this.dateRangeEnd)) {
|
||||||
possibleDates.push(possibleDate)
|
possibleDates.push(possibleDate)
|
||||||
possibleDate = possibleDate.add(15, 'minute')
|
possibleDate = possibleDate.add(15, 'minute')
|
||||||
}
|
}
|
||||||
|
|
||||||
return possibleDates
|
return possibleDates
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to check if a reservation is available for reservation in the system
|
* Method to check if a reservation is available for reservation in the system
|
||||||
* @returns is reservation date within 7 days
|
* @returns is reservation date within 7 days
|
||||||
*/
|
*/
|
||||||
@Exclude()
|
@Exclude()
|
||||||
public isAvailableForReservation(): boolean {
|
public isAvailableForReservation(): boolean {
|
||||||
return dayjs().diff(this.dateRangeStart, 'day') <= 7
|
return dayjs().diff(this.dateRangeStart, 'day') <= 7
|
||||||
}
|
}
|
||||||
|
|
||||||
@Exclude()
|
@Exclude()
|
||||||
public getAllowedReservationDate(): Dayjs {
|
public getAllowedReservationDate(): Dayjs {
|
||||||
return this.dateRangeStart
|
return this.dateRangeStart
|
||||||
.hour(0)
|
.hour(0)
|
||||||
.minute(0)
|
.minute(0)
|
||||||
.second(0)
|
.second(0)
|
||||||
.millisecond(0)
|
.millisecond(0)
|
||||||
.subtract(7, 'days')
|
.subtract(7, 'days')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import { ReservationsWorker } from './worker'
|
||||||
import { LoggerModule } from '../logger/module'
|
import { LoggerModule } from '../logger/module'
|
||||||
import { RunnerModule } from '../runner/module'
|
import { RunnerModule } from '../runner/module'
|
||||||
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ export class ReservationsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
getByDate(date = dayjs()) {
|
getByDate(date = dayjs()) {
|
||||||
return this.reservationsRepository.createQueryBuilder()
|
return this.reservationsRepository
|
||||||
|
.createQueryBuilder()
|
||||||
.where(`DATE(dateRangeStart, '-7 day') = DATE(:date)`, { date })
|
.where(`DATE(dateRangeStart, '-7 day') = DATE(:date)`, { date })
|
||||||
.getMany()
|
.getMany()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,26 @@ import { LoggerService } from '../logger/service'
|
||||||
|
|
||||||
@Processor(RESERVATIONS_QUEUE_NAME)
|
@Processor(RESERVATIONS_QUEUE_NAME)
|
||||||
export class ReservationsWorker {
|
export class ReservationsWorker {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(BaanReserverenService)
|
@Inject(BaanReserverenService)
|
||||||
private readonly brService: BaanReserverenService,
|
private readonly brService: BaanReserverenService,
|
||||||
|
|
||||||
@Inject(LoggerService)
|
@Inject(LoggerService)
|
||||||
private readonly logger: LoggerService,
|
private readonly logger: LoggerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process()
|
@Process()
|
||||||
async handleReservationJob(job: Job<Reservation>) {
|
async handleReservationJob(job: Job<Reservation>) {
|
||||||
const reservation = plainToInstance(Reservation, job.data, { groups: ['password'] })
|
const reservation = plainToInstance(Reservation, job.data, {
|
||||||
this.logger.log('Handling reservation', { reservation: instanceToPlain(reservation) })
|
groups: ['password'],
|
||||||
await this.performReservation(reservation)
|
})
|
||||||
}
|
this.logger.log('Handling reservation', {
|
||||||
|
reservation: instanceToPlain(reservation),
|
||||||
|
})
|
||||||
|
await this.performReservation(reservation)
|
||||||
|
}
|
||||||
|
|
||||||
async performReservation(reservation: Reservation) {
|
async performReservation(reservation: Reservation) {
|
||||||
await this.brService.performReservation(reservation)
|
await this.brService.performReservation(reservation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -9,221 +9,221 @@ import { Reservation } from '../../reservations/entity'
|
||||||
const baanReserverenRoot = 'https://squashcity.baanreserveren.nl'
|
const baanReserverenRoot = 'https://squashcity.baanreserveren.nl'
|
||||||
|
|
||||||
export enum BaanReserverenUrls {
|
export enum BaanReserverenUrls {
|
||||||
Reservations = '/reservations',
|
Reservations = '/reservations',
|
||||||
Logout = '/auth/logout',
|
Logout = '/auth/logout',
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SessionAction {
|
enum SessionAction {
|
||||||
NoAction,
|
NoAction,
|
||||||
Logout,
|
Logout,
|
||||||
Login,
|
Login,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaanReserverenSession {
|
interface BaanReserverenSession {
|
||||||
username: string
|
username: string
|
||||||
startedAt: Dayjs
|
startedAt: Dayjs
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BaanReserverenService {
|
export class BaanReserverenService {
|
||||||
private session: BaanReserverenSession | null = null
|
private session: BaanReserverenSession | null = null
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(RunnerService)
|
@Inject(RunnerService)
|
||||||
private readonly runnerService: RunnerService,
|
private readonly runnerService: RunnerService,
|
||||||
|
|
||||||
@Inject(EmptyPage)
|
@Inject(EmptyPage)
|
||||||
private readonly page: Page,
|
private readonly page: Page,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private checkSession(username: string) {
|
private checkSession(username: string) {
|
||||||
if (this.page.url().endsWith(BaanReserverenUrls.Reservations)) {
|
if (this.page.url().endsWith(BaanReserverenUrls.Reservations)) {
|
||||||
return this.session?.username !== username
|
return this.session?.username !== username
|
||||||
? SessionAction.Logout
|
? SessionAction.Logout
|
||||||
: SessionAction.NoAction
|
: SessionAction.NoAction
|
||||||
}
|
}
|
||||||
return SessionAction.Login
|
return SessionAction.Login
|
||||||
}
|
}
|
||||||
|
|
||||||
private startSession(username: string) {
|
private startSession(username: string) {
|
||||||
if (this.session && this.session.username !== username) {
|
if (this.session && this.session.username !== username) {
|
||||||
throw new Error('Session already started')
|
throw new Error('Session already started')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.session?.username === username) {
|
if (this.session?.username === username) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.session = {
|
this.session = {
|
||||||
username,
|
username,
|
||||||
startedAt: dayjs(),
|
startedAt: dayjs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private endSession() {
|
private endSession() {
|
||||||
this.session = null
|
this.session = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private async login(username: string, password: string) {
|
private async login(username: string, password: string) {
|
||||||
await this.page
|
await this.page
|
||||||
.waitForSelector('input[name=username]')
|
.waitForSelector('input[name=username]')
|
||||||
.then((i) => i?.type(username))
|
.then((i) => i?.type(username))
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
// throw new RunnerLoginUsernameInputError(e)
|
// throw new RunnerLoginUsernameInputError(e)
|
||||||
})
|
})
|
||||||
await this.page
|
await this.page
|
||||||
.$('input[name=password]')
|
.$('input[name=password]')
|
||||||
.then((i) => i?.type(password))
|
.then((i) => i?.type(password))
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
// throw new RunnerLoginPasswordInputError(e)
|
// throw new RunnerLoginPasswordInputError(e)
|
||||||
})
|
})
|
||||||
await this.page
|
await this.page
|
||||||
.$('button')
|
.$('button')
|
||||||
.then((b) => b?.click())
|
.then((b) => b?.click())
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
// throw new RunnerLoginSubmitError(e)
|
// throw new RunnerLoginSubmitError(e)
|
||||||
})
|
})
|
||||||
this.startSession(username)
|
this.startSession(username)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async logout() {
|
private async logout() {
|
||||||
await this.page.goto(`${baanReserverenRoot}${BaanReserverenUrls.Logout}`)
|
await this.page.goto(`${baanReserverenRoot}${BaanReserverenUrls.Logout}`)
|
||||||
this.endSession()
|
this.endSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async init(reservation: Reservation) {
|
private async init(reservation: Reservation) {
|
||||||
await this.page.goto(baanReserverenRoot)
|
await this.page.goto(baanReserverenRoot)
|
||||||
const action = await this.checkSession(reservation.username)
|
const action = await this.checkSession(reservation.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(reservation.username, reservation.password)
|
||||||
break
|
break
|
||||||
case SessionAction.Login:
|
case SessionAction.Login:
|
||||||
await this.login(reservation.username, reservation.password)
|
await this.login(reservation.username, reservation.password)
|
||||||
break
|
break
|
||||||
case SessionAction.NoAction:
|
case SessionAction.NoAction:
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getLastVisibleDay(): Dayjs {
|
private getLastVisibleDay(): Dayjs {
|
||||||
const lastDayOfMonth = dayjs().add(1, 'month').set('date', 0)
|
const lastDayOfMonth = dayjs().add(1, 'month').set('date', 0)
|
||||||
let daysToAdd = 0
|
let daysToAdd = 0
|
||||||
switch (lastDayOfMonth.day()) {
|
switch (lastDayOfMonth.day()) {
|
||||||
case 0:
|
case 0:
|
||||||
daysToAdd = 0
|
daysToAdd = 0
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
daysToAdd = 7 - lastDayOfMonth.day()
|
daysToAdd = 7 - lastDayOfMonth.day()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return lastDayOfMonth.add(daysToAdd, 'day')
|
return lastDayOfMonth.add(daysToAdd, 'day')
|
||||||
}
|
}
|
||||||
|
|
||||||
private async navigateToDay(date: Dayjs): Promise<void> {
|
private async navigateToDay(date: Dayjs): Promise<void> {
|
||||||
if (this.getLastVisibleDay().isBefore(date)) {
|
if (this.getLastVisibleDay().isBefore(date)) {
|
||||||
await this.page
|
await this.page
|
||||||
?.waitForSelector('td.month.next')
|
?.waitForSelector('td.month.next')
|
||||||
.then((d) => d?.click())
|
.then((d) => d?.click())
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
throw new RunnerNavigationMonthError(e)
|
throw new RunnerNavigationMonthError(e)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.page
|
await this.page
|
||||||
?.waitForSelector(
|
?.waitForSelector(
|
||||||
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get(
|
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get(
|
||||||
'date'
|
'date',
|
||||||
)}`
|
)}`,
|
||||||
)
|
)
|
||||||
.then((d) => d?.click())
|
.then((d) => d?.click())
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
throw new RunnerNavigationDayError(e)
|
throw new RunnerNavigationDayError(e)
|
||||||
})
|
})
|
||||||
await this.page
|
await this.page
|
||||||
?.waitForSelector(
|
?.waitForSelector(
|
||||||
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get(
|
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get(
|
||||||
'date'
|
'date',
|
||||||
)}.selected`
|
)}.selected`,
|
||||||
)
|
)
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
throw new RunnerNavigationSelectionError(e)
|
throw new RunnerNavigationSelectionError(e)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async selectAvailableTime(reservation: Reservation): Promise<void> {
|
private async selectAvailableTime(reservation: Reservation): Promise<void> {
|
||||||
let freeCourt: ElementHandle | null | undefined
|
let freeCourt: ElementHandle | null | undefined
|
||||||
let i = 0
|
let i = 0
|
||||||
const possibleDates = reservation.createPossibleDates()
|
const possibleDates = reservation.createPossibleDates()
|
||||||
while (i < possibleDates.length && !freeCourt) {
|
while (i < possibleDates.length && !freeCourt) {
|
||||||
const possibleDate = possibleDates[i]
|
const possibleDate = possibleDates[i]
|
||||||
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)
|
freeCourt = await this.page?.$(selector)
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!freeCourt) {
|
if (!freeCourt) {
|
||||||
throw new NoCourtAvailableError()
|
throw new NoCourtAvailableError()
|
||||||
}
|
}
|
||||||
|
|
||||||
await freeCourt.click().catch((e: Error) => {
|
await freeCourt.click().catch((e: Error) => {
|
||||||
throw new RunnerCourtSelectionError(e)
|
throw new RunnerCourtSelectionError(e)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async selectOpponent(id: string, name: string): Promise<void> {
|
private async selectOpponent(id: string, name: string): Promise<void> {
|
||||||
const player2Search = await this.page
|
const player2Search = await this.page
|
||||||
?.waitForSelector('tr.res-make-player-2 > td > input')
|
?.waitForSelector('tr.res-make-player-2 > td > input')
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
throw new RunnerOpponentSearchError(e)
|
throw new RunnerOpponentSearchError(e)
|
||||||
})
|
})
|
||||||
await player2Search?.type(name).catch((e: Error) => {
|
await player2Search?.type(name).catch((e: Error) => {
|
||||||
throw new RunnerOpponentSearchInputError(e)
|
throw new RunnerOpponentSearchInputError(e)
|
||||||
})
|
})
|
||||||
await this.page?.waitForNetworkIdle().catch((e: Error) => {
|
await this.page?.waitForNetworkIdle().catch((e: Error) => {
|
||||||
throw new RunnerOpponentSearchNetworkError(e)
|
throw new RunnerOpponentSearchNetworkError(e)
|
||||||
})
|
})
|
||||||
await this.page
|
await this.page
|
||||||
?.$('select.br-user-select[name="players[2]"]')
|
?.$('select.br-user-select[name="players[2]"]')
|
||||||
.then((d) => d?.select(id))
|
.then((d) => d?.select(id))
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
throw new RunnerOpponentSearchSelectionError(e)
|
throw new RunnerOpponentSearchSelectionError(e)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async confirmReservation(): Promise<void> {
|
private async confirmReservation(): Promise<void> {
|
||||||
await this.page
|
await this.page
|
||||||
?.$('input#__make_submit')
|
?.$('input#__make_submit')
|
||||||
.then((b) => b?.click())
|
.then((b) => b?.click())
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
throw new RunnerReservationConfirmButtonError(e)
|
throw new RunnerReservationConfirmButtonError(e)
|
||||||
})
|
})
|
||||||
await this.page
|
await this.page
|
||||||
?.waitForSelector('input#__make_submit2')
|
?.waitForSelector('input#__make_submit2')
|
||||||
.then((b) => b?.click())
|
.then((b) => b?.click())
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
throw new RunnerReservationConfirmSubmitError(e)
|
throw new RunnerReservationConfirmSubmitError(e)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public async performReservation(reservation: Reservation) {
|
public async performReservation(reservation: Reservation) {
|
||||||
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.selectOpponent(reservation.opponentId, reservation.opponentName)
|
await this.selectOpponent(reservation.opponentId, reservation.opponentName)
|
||||||
// await this.confirmReservation()
|
// await this.confirmReservation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RunnerError extends Error {
|
export class RunnerError extends Error {
|
||||||
constructor(error: Error) {
|
constructor(error: Error) {
|
||||||
super(error.message)
|
super(error.message)
|
||||||
this.stack = error.stack
|
this.stack = error.stack
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class PuppeteerError extends RunnerError {}
|
export class PuppeteerError extends RunnerError {}
|
||||||
export class PuppeteerBrowserLaunchError extends PuppeteerError {}
|
export class PuppeteerBrowserLaunchError extends PuppeteerError {}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,8 @@ import { BaanReserverenService } from './baanreserveren/service'
|
||||||
import { LoggerModule } from '../logger/module'
|
import { LoggerModule } from '../logger/module'
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [RunnerService, BaanReserverenService, EmptyPageFactory],
|
providers: [RunnerService, BaanReserverenService, EmptyPageFactory],
|
||||||
imports: [LoggerModule, BullModule.registerQueue({ name: 'reservations' })],
|
imports: [LoggerModule, BullModule.registerQueue({ name: 'reservations' })],
|
||||||
exports: [EmptyPageFactory, BaanReserverenService],
|
exports: [EmptyPageFactory, BaanReserverenService],
|
||||||
})
|
})
|
||||||
|
|
||||||
export class RunnerModule {}
|
export class RunnerModule {}
|
||||||
|
|
@ -5,13 +5,13 @@ import { Page } from 'puppeteer'
|
||||||
export const EmptyPage = Symbol.for('EmptyPage')
|
export const EmptyPage = Symbol.for('EmptyPage')
|
||||||
|
|
||||||
export const EmptyPageFactory: FactoryProvider<Page> = {
|
export const EmptyPageFactory: FactoryProvider<Page> = {
|
||||||
provide: EmptyPage,
|
provide: EmptyPage,
|
||||||
useFactory: async (runnerService: RunnerService) => {
|
useFactory: async (runnerService: RunnerService) => {
|
||||||
const browser = await runnerService.getBrowser()
|
const browser = await runnerService.getBrowser()
|
||||||
const page = await browser.newPage()
|
const page = await browser.newPage()
|
||||||
|
|
||||||
return page
|
return page
|
||||||
},
|
},
|
||||||
inject: [RunnerService],
|
inject: [RunnerService],
|
||||||
scope: Scope.TRANSIENT,
|
scope: Scope.TRANSIENT,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,92 @@
|
||||||
import { Inject, Injectable, BeforeApplicationShutdown, OnModuleInit, Scope } from '@nestjs/common'
|
import {
|
||||||
import puppeteer, { Browser, BrowserConnectOptions, BrowserLaunchArgumentOptions, LaunchOptions, Page } from 'puppeteer'
|
Injectable,
|
||||||
import { LoggerService } from '../logger/service'
|
BeforeApplicationShutdown,
|
||||||
|
OnModuleInit,
|
||||||
enum SessionAction {
|
} from '@nestjs/common'
|
||||||
NoAction,
|
import puppeteer, {
|
||||||
Logout,
|
Browser,
|
||||||
Login,
|
BrowserConnectOptions,
|
||||||
}
|
BrowserLaunchArgumentOptions,
|
||||||
|
LaunchOptions,
|
||||||
|
} from 'puppeteer'
|
||||||
|
|
||||||
interface RunnerSession {
|
interface RunnerSession {
|
||||||
username: string
|
username: string
|
||||||
loggedInAt: Date
|
loggedInAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RunnerService implements OnModuleInit, BeforeApplicationShutdown {
|
export class RunnerService implements OnModuleInit, BeforeApplicationShutdown {
|
||||||
private browser?: Browser
|
private browser?: Browser
|
||||||
private options: LaunchOptions &
|
private options: LaunchOptions &
|
||||||
BrowserLaunchArgumentOptions &
|
BrowserLaunchArgumentOptions &
|
||||||
BrowserConnectOptions = {
|
BrowserConnectOptions = {
|
||||||
args: ['--disable-setuid-sandbox', '--no-sandbox'],
|
args: ['--disable-setuid-sandbox', '--no-sandbox'],
|
||||||
headless: 'new'
|
headless: 'new',
|
||||||
}
|
}
|
||||||
private session: RunnerSession | null = null
|
private session: RunnerSession | null = null
|
||||||
|
|
||||||
private async init() {
|
private async init() {
|
||||||
try {
|
try {
|
||||||
if (!this.browser) {
|
if (!this.browser) {
|
||||||
this.browser = await puppeteer.launch(this.options)
|
this.browser = await puppeteer.launch(this.options)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new PuppeteerBrowserLaunchError(error)
|
throw new PuppeteerBrowserLaunchError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onModuleInit() {
|
public async onModuleInit() {
|
||||||
await this.init()
|
await this.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
public async beforeApplicationShutdown() {
|
public async beforeApplicationShutdown() {
|
||||||
try {
|
try {
|
||||||
if (this.browser && this.browser.isConnected()) {
|
if (this.browser && this.browser.isConnected()) {
|
||||||
await this.browser.close()
|
await this.browser.close()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error shutting down browser', error)
|
console.error('error shutting down browser', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBrowser(): Promise<Browser> {
|
public async getBrowser(): Promise<Browser> {
|
||||||
await this.init()
|
await this.init()
|
||||||
if (!this.browser) {
|
if (!this.browser) {
|
||||||
throw new Error('Browser not initialized')
|
throw new Error('Browser not initialized')
|
||||||
}
|
}
|
||||||
return this.browser
|
return this.browser
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSession(): Promise<RunnerSession | null> {
|
public async getSession(): Promise<RunnerSession | null> {
|
||||||
return this.session
|
return this.session
|
||||||
}
|
}
|
||||||
|
|
||||||
public startSession(username: string) {
|
public startSession(username: string) {
|
||||||
if (this.session && this.session.username !== username) {
|
if (this.session && this.session.username !== username) {
|
||||||
throw new RunnerNewSessionError(new Error('Session already started'))
|
throw new RunnerNewSessionError(new Error('Session already started'))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.session?.username === username) {
|
if (this.session?.username === username) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.session = {
|
this.session = {
|
||||||
username,
|
username,
|
||||||
loggedInAt: new Date(),
|
loggedInAt: new Date(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public endSession() {
|
public endSession() {
|
||||||
this.session = null
|
this.session = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RunnerError extends Error {
|
export class RunnerError extends Error {
|
||||||
constructor(error: Error) {
|
constructor(error: Error) {
|
||||||
super(error.message)
|
super(error.message)
|
||||||
this.stack = error.stack
|
this.stack = error.stack
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class PuppeteerError extends RunnerError {}
|
export class PuppeteerError extends RunnerError {}
|
||||||
export class PuppeteerBrowserLaunchError extends PuppeteerError {}
|
export class PuppeteerBrowserLaunchError extends PuppeteerError {}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue