diff --git a/src/ntfy/provider.ts b/src/ntfy/provider.ts index bf35b9c..153caa7 100644 --- a/src/ntfy/provider.ts +++ b/src/ntfy/provider.ts @@ -107,6 +107,39 @@ export class NtfyProvider implements OnApplicationBootstrap { ) } + async sendPerformingRiskyReservationNotification( + reservationId: string, + startTime: Dayjs, + endTime: Dayjs, + ) { + const url = `${this.configService.get( + 'BASE_URL', + )}/reservations/${reservationId}` + await this.publishQueue.add( + ...NtfyProvider.defaultJob({ + title: 'Handling risky reservation. Waiting for confirmation', + message: `Waiting for ${reservationId} - ${startTime.format()} to ${endTime.format()}`, + actions: [ + { + action: 'http', + label: 'Accept', + url: `${url}/resume`, + method: 'POST', + clear: true, + }, + { + action: 'http', + label: 'Reject', + url, + method: 'DELETE', + clear: true, + }, + ], + tags: [MessageTags.warning, MessageTags.passport_control], + }), + ) + } + async sendErrorPerformingReservationNotification( reservationId: string, startTime: Dayjs, diff --git a/src/ntfy/types.ts b/src/ntfy/types.ts index 6e975ab..137f330 100644 --- a/src/ntfy/types.ts +++ b/src/ntfy/types.ts @@ -49,6 +49,7 @@ export enum MessageTags { clock11 = 'clock11', clock1130 = 'clock1130', badminton = 'badminton', + passport_control = 'passport_control', } export enum MessagePriority { @@ -59,13 +60,60 @@ export enum MessagePriority { max = 5, } +type MessageActionType = 'broadcast' | 'copy' | 'http' | 'view' + +export interface MessageActionConfig { + action: MessageActionType + /** + * Label displayed on the action button + */ + label: string + /** + * Clear notification after action button is tapped + */ + clear?: boolean +} + +export interface BroadcastActionConfig extends MessageActionConfig { + action: 'broadcast' + intent?: string + extras?: Record +} + +export interface CopyActionConfig extends MessageActionConfig { + action: 'copy' + value: string +} + +export interface HttpActionConfig extends MessageActionConfig { + action: 'http' + url: string + /** + * @default 'POST' + */ + method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' // there are more but if I use them I'll have gone insane + headers?: Record + body?: string +} +export interface ViewActionConfig extends MessageActionConfig { + action: 'view' + url: string + clear?: boolean +} + +export type MessageAction = + | BroadcastActionConfig + | CopyActionConfig + | HttpActionConfig + | ViewActionConfig + export interface MessageConfig { topic: string message?: string title?: string tags?: MessageTags[] priority?: MessagePriority - actions?: object[] + actions?: MessageAction[] markdown?: boolean icon?: string } diff --git a/src/reservations/service.ts b/src/reservations/service.ts index 4d4a3e1..2ba074d 100644 --- a/src/reservations/service.ts +++ b/src/reservations/service.ts @@ -8,6 +8,12 @@ import { LoggerService } from '../logger/service.logger' import { BaanReserverenService } from '../runner/baanreserveren/service' import { Reservation, ReservationStatus } from './entity' +export enum ReservationDangerLevel { + Safe = 'safe', + Risky = 'risky', + Lethal = 'lethal', +} + @Injectable() export class ReservationsService { constructor( @@ -93,6 +99,19 @@ export class ReservationsService { return await this.reservationsRepository.save(res) } + getDangerLevel(reservation: Reservation) { + // don't book something within the ~~danger zone~~ + const now = dayjs() + const hourDiff = now.diff(reservation.dateRangeStart, 'hours') + if (hourDiff > 8) { + return ReservationDangerLevel.Safe + } else if (hourDiff >= 5) { + return ReservationDangerLevel.Risky + } else { + return ReservationDangerLevel.Lethal + } + } + async update(reservationId: string, update: Partial) { return await this.reservationsRepository.update(reservationId, update) } diff --git a/src/waitingList/service.ts b/src/waitingList/service.ts index ddda36f..41a0428 100644 --- a/src/waitingList/service.ts +++ b/src/waitingList/service.ts @@ -13,7 +13,10 @@ import { ReservationsQueue, } from '../reservations/config' import { Reservation } from '../reservations/entity' -import { ReservationsService } from '../reservations/service' +import { + ReservationDangerLevel, + ReservationsService, +} from '../reservations/service' import { WaitingListDetails } from './types' const EMAIL_SUBJECT_REGEX = new RegExp( @@ -92,24 +95,44 @@ export class WaitingListService { } // don't book something within the ~~danger zone~~ - const now = dayjs() - const { safe, dangerous } = reservations.reduce<{ + const partitioned = reservations.reduce<{ safe: Reservation[] - dangerous: Reservation[] + risky: Reservation[] + lethal: Reservation[] }>( - (acc, res) => - now.diff(res.dateRangeStart, 'hours') > 5 - ? { safe: [...acc.safe, res], dangerous: acc.dangerous } - : { safe: acc.safe, dangerous: [...acc.dangerous, res] }, - { safe: [], dangerous: [] }, + (acc, res) => { + switch (this.reservationsService.getDangerLevel(res)) { + case ReservationDangerLevel.Safe: + return { + safe: [...acc.safe, res], + risky: acc.risky, + lethal: acc.lethal, + } + case ReservationDangerLevel.Risky: + return { + safe: acc.safe, + risky: [...acc.risky, res], + lethal: acc.lethal, + } + case ReservationDangerLevel.Lethal: + return { + safe: acc.safe, + risky: acc.risky, + lethal: [...acc.lethal, res], + } + } + }, + { safe: [], risky: [], lethal: [] }, ) this.loggerService.debug( - `Found ${safe.length} safe reservations on waiting list (${dangerous.length} dangerous)`, + `Found the reservations in given categories: ${JSON.stringify( + partitioned, + )}`, ) await this.reservationsQueue.addBulk( - safe.map((r) => ({ + partitioned.safe.map((r) => ({ data: { reservation: r, speedyMode: false }, opts: { attempts: 1 }, })),