Formatting fixes

This commit is contained in:
Collin Duncan 2023-06-27 16:06:19 +02:00
parent a3a2f12082
commit 9da2d5e2f2
No known key found for this signature in database
14 changed files with 359 additions and 340 deletions

View file

@ -25,7 +25,7 @@ import { LoggerModule } from './logger/module'
},
defaultJobOptions: {
removeOnComplete: true,
}
},
}),
ScheduleModule.forRoot(),
ConfigModule.forRoot({ isGlobal: true }),

View file

@ -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 { map } from 'rxjs/operators'
export interface CustomResponse<T = unknown> {
data: T
data: T
}
@Injectable()
export class CustomResponseTransformInterceptor<T> implements NestInterceptor<T, CustomResponse<T>> {
intercept(_context: ExecutionContext, next: CallHandler): Observable<CustomResponse<T>> {
return next.handle().pipe(map(data => ({ data })))
}
export class CustomResponseTransformInterceptor<T>
implements NestInterceptor<T, CustomResponse<T>>
{
intercept(
_context: ExecutionContext,
next: CallHandler,
): Observable<CustomResponse<T>> {
return next.handle().pipe(map((data) => ({ data })))
}
}

View file

@ -12,27 +12,27 @@ dayjs.locale('nl')
dayjs.tz.setDefault('Europe/Amsterdam')
export interface DateRange {
start: dayjs.Dayjs
end: dayjs.Dayjs
start: dayjs.Dayjs
end: dayjs.Dayjs
}
export interface SerializedDateRange {
start: string
end: string
start: string
end: string
}
export const convertDateRangeStringToObject = ({
start,
end,
start,
end,
}: {
start: string
end: string
start: string
end: string
}): DateRange => ({ start: dayjs(start), end: dayjs(end) })
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

View file

@ -4,13 +4,13 @@ import { LoggerService } from './service'
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
constructor(
@Inject(LoggerService)
private readonly logger: LoggerService,
) {}
constructor(
@Inject(LoggerService)
private readonly logger: LoggerService,
) {}
use(req: Request, _res: Response, next: NextFunction) {
this.logger.log(`${req.method} ${req.originalUrl}`)
next()
}
use(req: Request, _res: Response, next: NextFunction) {
this.logger.log(`${req.method} ${req.originalUrl}`)
next()
}
}

View file

@ -1,5 +1,5 @@
export const RESERVATIONS_QUEUE_NAME = 'reservations'
export default () => ({
queueName: RESERVATIONS_QUEUE_NAME
queueName: RESERVATIONS_QUEUE_NAME,
})

View file

@ -24,8 +24,12 @@ export class ReservationsCronService {
timeZone: 'Europe/Amsterdam',
})
async handleDailyReservations() {
const reservationsToPerform = await this.reservationService.getByDate()
this.logger.log(`Found ${reservationsToPerform.length} reservations to perform`)
await this.reservationsQueue.addBulk(reservationsToPerform.map((r) => ({ data: r })))
}
const reservationsToPerform = await this.reservationService.getByDate()
this.logger.log(
`Found ${reservationsToPerform.length} reservations to perform`,
)
await this.reservationsQueue.addBulk(
reservationsToPerform.map((r) => ({ data: r })),
)
}
}

View file

@ -39,33 +39,33 @@ export class Reservation {
@Exclude()
public createPossibleDates(): Dayjs[] {
const possibleDates: Dayjs[] = []
const possibleDates: Dayjs[] = []
let possibleDate = dayjs(this.dateRangeStart).second(0).millisecond(0)
while (possibleDate.isSameOrBefore(this.dateRangeEnd)) {
possibleDates.push(possibleDate)
possibleDate = possibleDate.add(15, 'minute')
}
let possibleDate = dayjs(this.dateRangeStart).second(0).millisecond(0)
while (possibleDate.isSameOrBefore(this.dateRangeEnd)) {
possibleDates.push(possibleDate)
possibleDate = possibleDate.add(15, 'minute')
}
return possibleDates
}
return possibleDates
}
/**
* Method to check if a reservation is available for reservation in the system
* @returns is reservation date within 7 days
*/
/**
* Method to check if a reservation is available for reservation in the system
* @returns is reservation date within 7 days
*/
@Exclude()
public isAvailableForReservation(): boolean {
public isAvailableForReservation(): boolean {
return dayjs().diff(this.dateRangeStart, 'day') <= 7
}
}
@Exclude()
public getAllowedReservationDate(): Dayjs {
public getAllowedReservationDate(): Dayjs {
return this.dateRangeStart
.hour(0)
.minute(0)
.second(0)
.millisecond(0)
.subtract(7, 'days')
}
.hour(0)
.minute(0)
.second(0)
.millisecond(0)
.subtract(7, 'days')
}
}

View file

@ -10,7 +10,6 @@ import { ReservationsWorker } from './worker'
import { LoggerModule } from '../logger/module'
import { RunnerModule } from '../runner/module'
@Module({
imports: [
LoggerModule,

View file

@ -20,7 +20,8 @@ export class ReservationsService {
}
getByDate(date = dayjs()) {
return this.reservationsRepository.createQueryBuilder()
return this.reservationsRepository
.createQueryBuilder()
.where(`DATE(dateRangeStart, '-7 day') = DATE(:date)`, { date })
.getMany()
}

View file

@ -9,22 +9,26 @@ import { LoggerService } from '../logger/service'
@Processor(RESERVATIONS_QUEUE_NAME)
export class ReservationsWorker {
constructor(
@Inject(BaanReserverenService)
private readonly brService: BaanReserverenService,
constructor(
@Inject(BaanReserverenService)
private readonly brService: BaanReserverenService,
@Inject(LoggerService)
private readonly logger: LoggerService,
) {}
@Inject(LoggerService)
private readonly logger: LoggerService,
) {}
@Process()
async handleReservationJob(job: Job<Reservation>) {
const reservation = plainToInstance(Reservation, job.data, { groups: ['password'] })
this.logger.log('Handling reservation', { reservation: instanceToPlain(reservation) })
await this.performReservation(reservation)
}
@Process()
async handleReservationJob(job: Job<Reservation>) {
const reservation = plainToInstance(Reservation, job.data, {
groups: ['password'],
})
this.logger.log('Handling reservation', {
reservation: instanceToPlain(reservation),
})
await this.performReservation(reservation)
}
async performReservation(reservation: Reservation) {
await this.brService.performReservation(reservation)
}
async performReservation(reservation: Reservation) {
await this.brService.performReservation(reservation)
}
}

View file

@ -9,221 +9,221 @@ import { Reservation } from '../../reservations/entity'
const baanReserverenRoot = 'https://squashcity.baanreserveren.nl'
export enum BaanReserverenUrls {
Reservations = '/reservations',
Logout = '/auth/logout',
Reservations = '/reservations',
Logout = '/auth/logout',
}
enum SessionAction {
NoAction,
Logout,
Login,
NoAction,
Logout,
Login,
}
interface BaanReserverenSession {
username: string
startedAt: Dayjs
username: string
startedAt: Dayjs
}
@Injectable()
export class BaanReserverenService {
private session: BaanReserverenSession | null = null
private session: BaanReserverenSession | null = null
constructor(
@Inject(RunnerService)
private readonly runnerService: RunnerService,
constructor(
@Inject(RunnerService)
private readonly runnerService: RunnerService,
@Inject(EmptyPage)
private readonly page: Page,
) {}
@Inject(EmptyPage)
private readonly page: Page,
) {}
private checkSession(username: string) {
if (this.page.url().endsWith(BaanReserverenUrls.Reservations)) {
return this.session?.username !== username
? SessionAction.Logout
: SessionAction.NoAction
}
return SessionAction.Login
}
private checkSession(username: string) {
if (this.page.url().endsWith(BaanReserverenUrls.Reservations)) {
return this.session?.username !== username
? SessionAction.Logout
: SessionAction.NoAction
}
return SessionAction.Login
}
private startSession(username: string) {
if (this.session && this.session.username !== username) {
throw new Error('Session already started')
}
private startSession(username: string) {
if (this.session && this.session.username !== username) {
throw new Error('Session already started')
}
if (this.session?.username === username) {
return
}
if (this.session?.username === username) {
return
}
this.session = {
username,
startedAt: dayjs(),
}
}
this.session = {
username,
startedAt: dayjs(),
}
}
private endSession() {
this.session = null
}
private endSession() {
this.session = null
}
private async login(username: string, password: string) {
await this.page
.waitForSelector('input[name=username]')
.then((i) => i?.type(username))
.catch((e: Error) => {
// throw new RunnerLoginUsernameInputError(e)
})
await this.page
.$('input[name=password]')
.then((i) => i?.type(password))
.catch((e: Error) => {
// throw new RunnerLoginPasswordInputError(e)
})
await this.page
.$('button')
.then((b) => b?.click())
.catch((e: Error) => {
// throw new RunnerLoginSubmitError(e)
})
this.startSession(username)
}
private async login(username: string, password: string) {
await this.page
.waitForSelector('input[name=username]')
.then((i) => i?.type(username))
.catch((e: Error) => {
// throw new RunnerLoginUsernameInputError(e)
})
await this.page
.$('input[name=password]')
.then((i) => i?.type(password))
.catch((e: Error) => {
// throw new RunnerLoginPasswordInputError(e)
})
await this.page
.$('button')
.then((b) => b?.click())
.catch((e: Error) => {
// throw new RunnerLoginSubmitError(e)
})
this.startSession(username)
}
private async logout() {
await this.page.goto(`${baanReserverenRoot}${BaanReserverenUrls.Logout}`)
this.endSession()
}
private async logout() {
await this.page.goto(`${baanReserverenRoot}${BaanReserverenUrls.Logout}`)
this.endSession()
}
private async init(reservation: Reservation) {
await this.page.goto(baanReserverenRoot)
const action = await this.checkSession(reservation.username)
switch (action) {
case SessionAction.Logout:
await this.logout()
await this.login(reservation.username, reservation.password)
break
case SessionAction.Login:
await this.login(reservation.username, reservation.password)
break
case SessionAction.NoAction:
default:
break
}
}
private async init(reservation: Reservation) {
await this.page.goto(baanReserverenRoot)
const action = await this.checkSession(reservation.username)
switch (action) {
case SessionAction.Logout:
await this.logout()
await this.login(reservation.username, reservation.password)
break
case SessionAction.Login:
await this.login(reservation.username, reservation.password)
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')
}
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')
}
private async navigateToDay(date: Dayjs): Promise<void> {
if (this.getLastVisibleDay().isBefore(date)) {
await this.page
?.waitForSelector('td.month.next')
.then((d) => d?.click())
.catch((e: Error) => {
throw new RunnerNavigationMonthError(e)
})
}
private async navigateToDay(date: Dayjs): Promise<void> {
if (this.getLastVisibleDay().isBefore(date)) {
await this.page
?.waitForSelector('td.month.next')
.then((d) => d?.click())
.catch((e: Error) => {
throw new RunnerNavigationMonthError(e)
})
}
await this.page
?.waitForSelector(
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get(
'date'
)}`
)
.then((d) => d?.click())
.catch((e: Error) => {
throw new RunnerNavigationDayError(e)
})
await this.page
?.waitForSelector(
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get(
'date'
)}.selected`
)
.catch((e: Error) => {
throw new RunnerNavigationSelectionError(e)
})
}
await this.page
?.waitForSelector(
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get(
'date',
)}`,
)
.then((d) => d?.click())
.catch((e: Error) => {
throw new RunnerNavigationDayError(e)
})
await this.page
?.waitForSelector(
`td#cal_${date.get('year')}_${date.get('month') + 1}_${date.get(
'date',
)}.selected`,
)
.catch((e: Error) => {
throw new RunnerNavigationSelectionError(e)
})
}
private async selectAvailableTime(reservation: Reservation): Promise<void> {
let freeCourt: ElementHandle | null | undefined
let i = 0
const possibleDates = reservation.createPossibleDates()
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']`
freeCourt = await this.page?.$(selector)
i++
}
private async selectAvailableTime(reservation: Reservation): Promise<void> {
let freeCourt: ElementHandle | null | undefined
let i = 0
const possibleDates = reservation.createPossibleDates()
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']`
freeCourt = await this.page?.$(selector)
i++
}
if (!freeCourt) {
throw new NoCourtAvailableError()
}
if (!freeCourt) {
throw new NoCourtAvailableError()
}
await freeCourt.click().catch((e: Error) => {
throw new RunnerCourtSelectionError(e)
})
}
await freeCourt.click().catch((e: Error) => {
throw new RunnerCourtSelectionError(e)
})
}
private async selectOpponent(id: string, name: string): Promise<void> {
const player2Search = await this.page
?.waitForSelector('tr.res-make-player-2 > td > input')
.catch((e: Error) => {
throw new RunnerOpponentSearchError(e)
})
await player2Search?.type(name).catch((e: Error) => {
throw new RunnerOpponentSearchInputError(e)
})
await this.page?.waitForNetworkIdle().catch((e: Error) => {
throw new RunnerOpponentSearchNetworkError(e)
})
await this.page
?.$('select.br-user-select[name="players[2]"]')
.then((d) => d?.select(id))
.catch((e: Error) => {
throw new RunnerOpponentSearchSelectionError(e)
})
}
private async selectOpponent(id: string, name: string): Promise<void> {
const player2Search = await this.page
?.waitForSelector('tr.res-make-player-2 > td > input')
.catch((e: Error) => {
throw new RunnerOpponentSearchError(e)
})
await player2Search?.type(name).catch((e: Error) => {
throw new RunnerOpponentSearchInputError(e)
})
await this.page?.waitForNetworkIdle().catch((e: Error) => {
throw new RunnerOpponentSearchNetworkError(e)
})
await this.page
?.$('select.br-user-select[name="players[2]"]')
.then((d) => d?.select(id))
.catch((e: Error) => {
throw new RunnerOpponentSearchSelectionError(e)
})
}
private async confirmReservation(): Promise<void> {
await this.page
?.$('input#__make_submit')
.then((b) => b?.click())
.catch((e: Error) => {
throw new RunnerReservationConfirmButtonError(e)
})
await this.page
?.waitForSelector('input#__make_submit2')
.then((b) => b?.click())
.catch((e: Error) => {
throw new RunnerReservationConfirmSubmitError(e)
})
}
private async confirmReservation(): Promise<void> {
await this.page
?.$('input#__make_submit')
.then((b) => b?.click())
.catch((e: Error) => {
throw new RunnerReservationConfirmButtonError(e)
})
await this.page
?.waitForSelector('input#__make_submit2')
.then((b) => b?.click())
.catch((e: Error) => {
throw new RunnerReservationConfirmSubmitError(e)
})
}
public async performReservation(reservation: Reservation) {
await this.init(reservation)
await this.navigateToDay(reservation.dateRangeStart)
await this.selectAvailableTime(reservation)
await this.selectOpponent(reservation.opponentId, reservation.opponentName)
// await this.confirmReservation()
}
public async performReservation(reservation: Reservation) {
await this.init(reservation)
await this.navigateToDay(reservation.dateRangeStart)
await this.selectAvailableTime(reservation)
await this.selectOpponent(reservation.opponentId, reservation.opponentName)
// await this.confirmReservation()
}
}
export class RunnerError extends Error {
constructor(error: Error) {
super(error.message)
this.stack = error.stack
}
constructor(error: Error) {
super(error.message)
this.stack = error.stack
}
}
export class PuppeteerError extends RunnerError {}
export class PuppeteerBrowserLaunchError extends PuppeteerError {}

View file

@ -6,9 +6,8 @@ import { BaanReserverenService } from './baanreserveren/service'
import { LoggerModule } from '../logger/module'
@Module({
providers: [RunnerService, BaanReserverenService, EmptyPageFactory],
imports: [LoggerModule, BullModule.registerQueue({ name: 'reservations' })],
exports: [EmptyPageFactory, BaanReserverenService],
providers: [RunnerService, BaanReserverenService, EmptyPageFactory],
imports: [LoggerModule, BullModule.registerQueue({ name: 'reservations' })],
exports: [EmptyPageFactory, BaanReserverenService],
})
export class RunnerModule {}

View file

@ -5,13 +5,13 @@ import { Page } from 'puppeteer'
export const EmptyPage = Symbol.for('EmptyPage')
export const EmptyPageFactory: FactoryProvider<Page> = {
provide: EmptyPage,
useFactory: async (runnerService: RunnerService) => {
const browser = await runnerService.getBrowser()
const page = await browser.newPage()
provide: EmptyPage,
useFactory: async (runnerService: RunnerService) => {
const browser = await runnerService.getBrowser()
const page = await browser.newPage()
return page
},
inject: [RunnerService],
scope: Scope.TRANSIENT,
return page
},
inject: [RunnerService],
scope: Scope.TRANSIENT,
}

View file

@ -1,90 +1,92 @@
import { Inject, Injectable, BeforeApplicationShutdown, OnModuleInit, Scope } from '@nestjs/common'
import puppeteer, { Browser, BrowserConnectOptions, BrowserLaunchArgumentOptions, LaunchOptions, Page } from 'puppeteer'
import { LoggerService } from '../logger/service'
enum SessionAction {
NoAction,
Logout,
Login,
}
import {
Injectable,
BeforeApplicationShutdown,
OnModuleInit,
} from '@nestjs/common'
import puppeteer, {
Browser,
BrowserConnectOptions,
BrowserLaunchArgumentOptions,
LaunchOptions,
} from 'puppeteer'
interface RunnerSession {
username: string
loggedInAt: Date
username: string
loggedInAt: Date
}
@Injectable()
export class RunnerService implements OnModuleInit, BeforeApplicationShutdown {
private browser?: Browser
private options: LaunchOptions &
BrowserLaunchArgumentOptions &
BrowserConnectOptions = {
args: ['--disable-setuid-sandbox', '--no-sandbox'],
headless: 'new'
}
private session: RunnerSession | null = null
private browser?: Browser
private options: LaunchOptions &
BrowserLaunchArgumentOptions &
BrowserConnectOptions = {
args: ['--disable-setuid-sandbox', '--no-sandbox'],
headless: 'new',
}
private session: RunnerSession | null = null
private async init() {
try {
if (!this.browser) {
this.browser = await puppeteer.launch(this.options)
}
} catch (error) {
throw new PuppeteerBrowserLaunchError(error)
}
}
private async init() {
try {
if (!this.browser) {
this.browser = await puppeteer.launch(this.options)
}
} catch (error) {
throw new PuppeteerBrowserLaunchError(error)
}
}
public async onModuleInit() {
await this.init()
}
public async onModuleInit() {
await this.init()
}
public async beforeApplicationShutdown() {
try {
if (this.browser && this.browser.isConnected()) {
await this.browser.close()
}
} catch (error) {
console.error('error shutting down browser', error)
}
}
public async beforeApplicationShutdown() {
try {
if (this.browser && this.browser.isConnected()) {
await this.browser.close()
}
} catch (error) {
console.error('error shutting down browser', error)
}
}
public async getBrowser(): Promise<Browser> {
await this.init()
if (!this.browser) {
throw new Error('Browser not initialized')
}
return this.browser
}
public async getBrowser(): Promise<Browser> {
await this.init()
if (!this.browser) {
throw new Error('Browser not initialized')
}
return this.browser
}
public async getSession(): Promise<RunnerSession | null> {
return this.session
}
public async getSession(): Promise<RunnerSession | null> {
return this.session
}
public startSession(username: string) {
if (this.session && this.session.username !== username) {
throw new RunnerNewSessionError(new Error('Session already started'))
}
public startSession(username: string) {
if (this.session && this.session.username !== username) {
throw new RunnerNewSessionError(new Error('Session already started'))
}
if (this.session?.username === username) {
return
}
if (this.session?.username === username) {
return
}
this.session = {
username,
loggedInAt: new Date(),
}
}
this.session = {
username,
loggedInAt: new Date(),
}
}
public endSession() {
this.session = null
}
public endSession() {
this.session = null
}
}
export class RunnerError extends Error {
constructor(error: Error) {
super(error.message)
this.stack = error.stack
}
constructor(error: Error) {
super(error.message)
this.stack = error.stack
}
}
export class PuppeteerError extends RunnerError {}
export class PuppeteerBrowserLaunchError extends PuppeteerError {}