Small bug fix in reservation loop and prettier/eslint

This commit is contained in:
Collin Duncan 2021-11-17 14:17:25 +01:00
parent 8a83417b7d
commit 5055d5cd91
No known key found for this signature in database
7 changed files with 2445 additions and 85 deletions

2314
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,11 +7,13 @@
"node": "14.x" "node": "14.x"
}, },
"scripts": { "scripts": {
"lint": "eslint src/ --ext ts",
"prettier": "prettier src -w",
"build": "tsc", "build": "tsc",
"local": "npx ts-node src/local.ts", "local": "npx ts-node src/local.ts",
"zip": "mkdir deploy && zip deploy/reservation-lambda.zip -r dist" "zip": "mkdir deploy && zip deploy/reservation-lambda.zip -r dist",
}, },
"author": "", "author": "Collin Duncan <cgduncan7@gmail.com>",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"puppeteer": "^11.0.0" "puppeteer": "^11.0.0"
@ -23,6 +25,10 @@
"@rollup/plugin-babel": "^5.3.0", "@rollup/plugin-babel": "^5.3.0",
"@types/aws-lambda": "^8.10.85", "@types/aws-lambda": "^8.10.85",
"@types/puppeteer": "^5.4.4", "@types/puppeteer": "^5.4.4",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"eslint": "^8.2.0",
"prettier": "^2.4.1",
"typescript": "^4.4.4" "typescript": "^4.4.4"
} }
} }

View file

@ -1,5 +1,5 @@
import { SQSEvent, SQSHandler } from "aws-lambda" import { SQSEvent, SQSHandler } from 'aws-lambda'
import { validateRequestEvent } from "./request" import { validateRequestEvent } from './request'
import { Reservation } from './reservation' import { Reservation } from './reservation'
import { Runner } from './runner' import { Runner } from './runner'
@ -15,4 +15,4 @@ export const run: SQSHandler = async (event: SQSEvent): Promise<void> => {
const runner = new Runner(username, password, reservations) const runner = new Runner(username, password, reservations)
await runner.run({ headless: false }) await runner.run({ headless: false })
} }

View file

@ -1,7 +1,6 @@
import { IncomingRequest, validateRequestEvent } from "./request" import { IncomingRequest } from './request'
import { Reservation } from "./reservation" import { Reservation } from './reservation'
import { Runner } from "./runner" import { Runner } from './runner'
const run = async (request: IncomingRequest) => { const run = async (request: IncomingRequest) => {
const { username, password, dateTimes, opponent } = request const { username, password, dateTimes, opponent } = request
@ -14,36 +13,52 @@ const run = async (request: IncomingRequest) => {
// get supplied args // get supplied args
const args = process.argv.filter((_, index) => index >= 2) const args = process.argv.filter((_, index) => index >= 2)
if (args.length !== 7) { if (args.length !== 7) {
console.error('Usage: npm run local <username> <password> <year> <month> <day> <startTime> <endTime> <opponentName> <opponentId>') console.error(
'Usage: npm run local <username> <password> <year> <month> <day> <startTime> <endTime> <opponentName> <opponentId>'
)
process.exit(1) process.exit(1)
} }
const [username, password, year, month, day, startTime, endTime, opponentName, opponentId] = args const [
const [startHour, startMinute] = startTime.split(':').map((t) => Number.parseInt(t)) username,
password,
year,
month,
day,
startTime,
endTime,
opponentName,
opponentId,
] = args
const [startHour, startMinute] = startTime
.split(':')
.map((t) => Number.parseInt(t))
const [endHour, endMinute] = endTime.split(':').map((t) => Number.parseInt(t)) const [endHour, endMinute] = endTime.split(':').map((t) => Number.parseInt(t))
run({ run({
username: username, username: username,
password: password, password: password,
dateTimes: [{ dateTimes: [
year: Number.parseInt(year), {
month: Number.parseInt(month), year: Number.parseInt(year),
day: Number.parseInt(day), month: Number.parseInt(month),
timeRange: { day: Number.parseInt(day),
start: { timeRange: {
hour: startHour, start: {
minute: startMinute, hour: startHour,
}, minute: startMinute,
end: { },
hour: endHour, end: {
minute: endMinute, hour: endHour,
minute: endMinute,
},
}, },
}, },
}], ],
opponent: { opponent: {
id: opponentId, id: opponentId,
name: opponentName, name: opponentName,
}, },
}) })
.then(() => console.log('Success')) .then(() => console.log('Success'))
.catch((e) => console.error(e)) .catch((e) => console.error(e))

View file

@ -1,9 +1,5 @@
import { import { SQSEvent } from 'aws-lambda'
APIGatewayProxyEventV2, SQSEvent, import { DateTime, Opponent } from './reservation'
} from "aws-lambda";
import {
DateTime, Opponent
} from "./reservation";
export interface IncomingRequest { export interface IncomingRequest {
username: string username: string
@ -17,14 +13,16 @@ export interface ValidationError {
code: number code: number
} }
export const validateRequestEvent = (event: SQSEvent): { request?: IncomingRequest, error ?: ValidationError } => { export const validateRequestEvent = (
event: SQSEvent
): { request?: IncomingRequest; error?: ValidationError } => {
try { try {
const request = validateRequestBody(event.Records[0].body) const request = validateRequestBody(event.Records[0].body)
validateRequestDateTimes(request.dateTimes) validateRequestDateTimes(request.dateTimes)
validateRequestOpponent(request.opponent) validateRequestOpponent(request.opponent)
return { request } return { request }
} catch (err: any) { } catch (err: unknown) {
return { error: { message: 'Invalid request', code: err.code ?? 0 } } return { error: { message: 'Invalid request', code: (err as ValidationError).code ?? 0 } }
} }
} }
@ -32,7 +30,7 @@ const validateRequestBody = (body?: string): IncomingRequest => {
if (body === undefined) { if (body === undefined) {
throw { throw {
message: 'Invalid request', message: 'Invalid request',
code: 1 code: 1,
} }
} }
@ -46,12 +44,14 @@ const validateRequestBody = (body?: string): IncomingRequest => {
} }
} }
const { const { username, password, dateTimes } = jsonBody
username, if (
password, !username ||
dateTimes, username.length < 1 ||
} = jsonBody !password ||
if (!username || username.length < 1 || !password || password.length < 1 || !dateTimes) { password.length < 1 ||
!dateTimes
) {
throw { throw {
message: 'Invalid request', message: 'Invalid request',
code: 3, code: 3,
@ -65,23 +65,17 @@ const validateRequestDateTimes = (dateTimes: DateTime[]): void => {
const now = new Date() const now = new Date()
for (let i = 0; i < dateTimes.length; i++) { for (let i = 0; i < dateTimes.length; i++) {
const dateTime = dateTimes[i] const dateTime = dateTimes[i]
const { const { year, month, day, timeRange } = dateTime
year, const { start, end } = timeRange
month,
day,
timeRange
} = dateTime
const {
start,
end
} = timeRange
if ( if (
typeof year !== 'number' || typeof year !== 'number' ||
typeof month !== 'number' || typeof month !== 'number' ||
typeof day !== 'number' || typeof day !== 'number' ||
typeof start.hour !== 'number' || typeof start.minute !== 'number' || typeof start.hour !== 'number' ||
typeof end.hour !== 'number' || typeof end.minute !== 'number' typeof start.minute !== 'number' ||
typeof end.hour !== 'number' ||
typeof end.minute !== 'number'
) { ) {
throw { throw {
message: 'Invalid request', message: 'Invalid request',
@ -117,4 +111,4 @@ const validateRequestOpponent = (opponent?: Opponent): void => {
code: 6, code: 6,
} }
} }
} }

View file

@ -18,15 +18,16 @@ export interface DateTime {
} }
} }
export const timeToString = ({ hour, minute }: Time): string => `${`${hour}`.padStart(2, '0')}:${`${minute}`.padStart(2, '0')}` export const timeToString = ({ hour, minute }: Time): string =>
`${`${hour}`.padStart(2, '0')}:${`${minute}`.padStart(2, '0')}`
export class Reservation { export class Reservation {
public readonly dateTime: DateTime public readonly dateTime: DateTime
public readonly opponent: Opponent public readonly opponent: Opponent
public readonly possibleTimes: Time[] public readonly possibleTimes: Time[]
public booked: boolean = false public booked = false
constructor (dateTime: DateTime, opponent: Opponent) { constructor(dateTime: DateTime, opponent: Opponent) {
this.dateTime = dateTime this.dateTime = dateTime
this.opponent = opponent this.opponent = opponent
this.possibleTimes = this.createPossibleTimes() this.possibleTimes = this.createPossibleTimes()
@ -36,9 +37,9 @@ export class Reservation {
const possibleTimes: Time[] = [] const possibleTimes: Time[] = []
const { start, end } = this.dateTime.timeRange const { start, end } = this.dateTime.timeRange
let { hour, minute } = start let { hour, minute } = start
let { hour: endHour, minute: endMinute } = end const { hour: endHour, minute: endMinute } = end
while (hour <= endHour && minute <= endMinute) { while (hour <= endHour && minute <= endMinute) {
possibleTimes.push({ hour, minute }) possibleTimes.push({ hour, minute })
@ -50,4 +51,4 @@ export class Reservation {
return possibleTimes return possibleTimes
} }
} }

View file

@ -1,5 +1,12 @@
import puppeteer, { Browser, BrowserConnectOptions, BrowserLaunchArgumentOptions, ElementHandle, LaunchOptions, Page } from "puppeteer" import puppeteer, {
import { DateTime, Opponent, Reservation, timeToString } from "./reservation"; Browser,
BrowserConnectOptions,
BrowserLaunchArgumentOptions,
ElementHandle,
LaunchOptions,
Page,
} from 'puppeteer'
import { DateTime, Opponent, Reservation, timeToString } from './reservation'
export class Runner { export class Runner {
private readonly username: string private readonly username: string
@ -9,13 +16,21 @@ export class Runner {
private browser: Browser | undefined private browser: Browser | undefined
private page: Page | undefined private page: Page | undefined
public constructor(username: string, password: string, reservations: Reservation[], options?: LaunchOptions) { public constructor(
username: string,
password: string,
reservations: Reservation[]
) {
this.username = username this.username = username
this.password = password this.password = password
this.reservations = reservations this.reservations = reservations
} }
public async run(options?: LaunchOptions & BrowserLaunchArgumentOptions & BrowserConnectOptions): Promise<Reservation[]> { public async run(
options?: LaunchOptions &
BrowserLaunchArgumentOptions &
BrowserConnectOptions
): Promise<Reservation[]> {
this.browser = await puppeteer.launch(options) this.browser = await puppeteer.launch(options)
this.page = await this.browser.newPage() this.page = await this.browser.newPage()
await this.login() await this.login()
@ -23,10 +38,14 @@ export class Runner {
} }
private async login() { private async login() {
await this.page!.goto('https://squashcity.baanreserveren.nl/') await this.page?.goto('https://squashcity.baanreserveren.nl/')
await this.page!.waitForSelector('input[name=username]').then(i => i!.type(this.username)) await this.page
await this.page!.$('input[name=password]').then(i => i!.type(this.password)) ?.waitForSelector('input[name=username]')
await this.page!.$('button').then((b) => b!.click()) .then((i) => i?.type(this.username))
await this.page
?.$('input[name=password]')
.then((i) => i?.type(this.password))
await this.page?.$('button').then((b) => b?.click())
} }
private async makeReservations(): Promise<Reservation[]> { private async makeReservations(): Promise<Reservation[]> {
@ -50,37 +69,48 @@ export class Runner {
} }
private async navigateToDay(dt: DateTime): Promise<void> { private async navigateToDay(dt: DateTime): Promise<void> {
await this.page!.waitForSelector(`td#cal_${dt.year}_${dt.month}_${dt.day}`).then((d) => d!.click()) await this.page
await this.page!.waitForSelector(`td#cal_${dt.year}_${dt.month}_${dt.day}.selected`) ?.waitForSelector(`td#cal_${dt.year}_${dt.month}_${dt.day}`)
.then((d) => d?.click())
await this.page?.waitForSelector(
`td#cal_${dt.year}_${dt.month}_${dt.day}.selected`
)
} }
private async selectAvailableTime(res: Reservation): Promise<void> { private async selectAvailableTime(res: Reservation): Promise<void> {
let freeCourt: ElementHandle | null = null let freeCourt: ElementHandle | null | undefined
let i = 0 let i = 0
while (i < res.possibleTimes.length && freeCourt !== null) { while (i < res.possibleTimes.length && !freeCourt) {
const possibleTime = res.possibleTimes[i] const possibleTime = res.possibleTimes[i]
const timeString = timeToString(possibleTime) const timeString = timeToString(possibleTime)
const selector = `tr[data-time='${timeString}']` + const selector =
`> 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++
} }
if (freeCourt === null) { if (!freeCourt) {
throw new Error("No free court available") throw new Error('No free court available')
} }
await freeCourt.click() await freeCourt.click()
} }
private async selectOpponent(opponent: Opponent): Promise<void> { private async selectOpponent(opponent: Opponent): Promise<void> {
const player2Search = await this.page!.waitForSelector('tr.res-make-player-2 > td > input') const player2Search = await this.page?.waitForSelector(
await player2Search!.type(opponent.name) 'tr.res-make-player-2 > td > input'
await this.page!.waitForNetworkIdle() )
await this.page!.$('select.br-user-select[name="players[2]"]').then(d => d!.select(opponent.id)) await player2Search?.type(opponent.name)
await this.page?.waitForNetworkIdle()
await this.page
?.$('select.br-user-select[name="players[2]"]')
.then((d) => d?.select(opponent.id))
} }
private async confirmReservation(): Promise<void> { private async confirmReservation(): Promise<void> {
await this.page!.$('input#__make_submit').then(b => b!.click()) await this.page?.$('input#__make_submit').then((b) => b?.click())
await this.page!.waitForSelector("input#__make_submit2").then(b => b!.click()) await this.page
?.waitForSelector('input#__make_submit2')
.then((b) => b?.click())
} }
} }