Completing ntfy module and adding to other modules

This commit is contained in:
Collin Duncan 2023-09-06 11:23:21 +02:00
parent 2f7d5c68ef
commit 62a4b8c9e6
No known key found for this signature in database
12 changed files with 176 additions and 49 deletions

View file

@ -1,9 +1,12 @@
import { BullModule } from '@nestjs/bull'
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
import { NtfyClient } from './client' import { NtfyClient } from './client'
import { NtfyProvider } from './provider' import { NtfyProvider } from './provider'
import { NTFY_PUBLISH_QUEUE_NAME } from './types'
@Module({ @Module({
imports: [BullModule.registerQueue({ name: NTFY_PUBLISH_QUEUE_NAME })],
providers: [NtfyProvider, NtfyClient], providers: [NtfyProvider, NtfyClient],
exports: [NtfyProvider, NtfyClient], exports: [NtfyProvider, NtfyClient],
}) })

View file

@ -1,38 +1,113 @@
import { InjectQueue, Process, Processor } from '@nestjs/bull'
import { Inject, Injectable } from '@nestjs/common' import { Inject, Injectable } from '@nestjs/common'
import { Job, JobOptions, Queue } from 'bull'
import { Dayjs } from 'dayjs'
import { NtfyClient } from './client' import { NtfyClient } from './client'
import { MessagePriority, MessageTags } from './types' import {
MessageConfig,
MessagePriority,
MessageTags,
NTFY_PUBLISH_QUEUE_NAME,
} from './types'
@Processor(NTFY_PUBLISH_QUEUE_NAME)
@Injectable() @Injectable()
export class NtfyProvider { export class NtfyProvider {
constructor( constructor(
@Inject(NtfyClient) @Inject(NtfyClient)
private readonly ntfyClient: NtfyClient, private readonly ntfyClient: NtfyClient,
@InjectQueue(NTFY_PUBLISH_QUEUE_NAME)
private readonly publishQueue: Queue,
) {} ) {}
async sendErrorNotification( @Process()
title: string, async handlePublishJob(job: Job<Omit<MessageConfig, 'topic'>>) {
message: string,
priority = MessagePriority.default,
) {
await this.ntfyClient.publish({ await this.ntfyClient.publish({
title, ...job.data,
message,
tags: [MessageTags.red_x],
priority,
}) })
} }
async sendInfoNotification( private static defaultJob(
title: string, data: Omit<MessageConfig, 'topic'>,
message: string, ): [Omit<MessageConfig, 'topic'>, JobOptions] {
priority = MessagePriority.low, return [
data,
{
attempts: 3,
removeOnComplete: true,
backoff: {
type: 'exponential',
},
},
]
}
async sendCronStartNotification(title: string) {
await this.publishQueue.add(
...NtfyProvider.defaultJob({
title,
tags: [MessageTags.alarm_clock, MessageTags.green_circle],
}),
)
}
async sendCronStopNotification(title: string, message: string) {
await this.publishQueue.add(
...NtfyProvider.defaultJob({
title,
message,
tags: [MessageTags.alarm_clock, MessageTags.red_circle],
}),
)
}
async sendPerformingReservationNotification(
reservationId: string,
startTime: Dayjs,
endTime: Dayjs,
) { ) {
await this.ntfyClient.publish({ await this.publishQueue.add(
title, ...NtfyProvider.defaultJob({
message, title: 'Performing reservation',
tags: [MessageTags.info], message: `${reservationId} - ${startTime.format()} to ${endTime.format()}`,
priority, tags: [MessageTags.badminton],
}) priority: MessagePriority.low,
}),
)
}
async sendErrorPerformingReservationNotification(
reservationId: string,
startTime: Dayjs,
endTime: Dayjs,
error: Error,
) {
await this.publishQueue.add(
...NtfyProvider.defaultJob({
title: 'Error performing reservation',
message: `${reservationId} - ${startTime.format()} to ${endTime.format()} : (${
error.name
}) - ${error.message}`,
tags: [MessageTags.badminton, MessageTags.red_x],
priority: MessagePriority.low,
}),
)
}
async sendReservationWaitlistedNotification(
reservationId: string,
startTime: Dayjs,
endTime: Dayjs,
) {
await this.publishQueue.add(
...NtfyProvider.defaultJob({
title: 'Reservation waitlisted',
message: `${reservationId} - ${startTime.format()} to ${endTime.format()}`,
tags: [MessageTags.badminton, MessageTags.hourglass],
priority: MessagePriority.low,
}),
)
} }
} }

View file

@ -1,31 +1,29 @@
export const NTFY_PUBLISH_QUEUE_NAME = 'ntfy_publish_queue'
export enum MessageTags { export enum MessageTags {
thumbs_up = '+1', alarm_clock = 'alarm_clock',
thumbs_down = '-1', green_circle = 'green_circle',
yellow_circle = 'yellow_circle',
red_circle = 'red_circle',
info = 'information_source',
hourglass = 'hourglass',
warning = 'warning', warning = 'warning',
rotating_light = 'rotating-light',
skull_and_crossbones = 'skull_and_crossbones',
tada = 'tada',
outbox_tray = 'outbox_tray',
inbox_tray = 'inbox_tray',
date = 'date',
no_entry = 'no_entry',
exclamation = 'exclamation', exclamation = 'exclamation',
question = 'question', question = 'question',
no_entry = 'no_entry',
white_check_mark = 'white_check_mark', white_check_mark = 'white_check_mark',
red_x = 'x', red_x = 'x',
zero = 'zero', zero = 'zero',
one = 'zero', one = 'one',
two = 'zero', two = 'two',
three = 'zero', three = 'three',
four = 'zero', four = 'four',
five = 'zero', five = 'five',
six = 'zero', six = 'six',
seven = 'zero', seven = 'seven',
eight = 'zero', eight = 'eight',
nine = 'zero', nine = 'nine',
new = 'new', new = 'new',
ok = 'ok',
sos = 'sos',
clock12 = 'clock12', clock12 = 'clock12',
clock1230 = 'clock1230', clock1230 = 'clock1230',
clock1 = 'clock1', clock1 = 'clock1',
@ -51,7 +49,6 @@ export enum MessageTags {
clock11 = 'clock11', clock11 = 'clock11',
clock1130 = 'clock1130', clock1130 = 'clock1130',
badminton = 'badminton', badminton = 'badminton',
info = 'information_source',
} }
export enum MessagePriority { export enum MessagePriority {

View file

@ -3,6 +3,7 @@ import { Cron, CronExpression } from '@nestjs/schedule'
import dayjs from '../common/dayjs' import dayjs from '../common/dayjs'
import { LoggerService } from '../logger/service.logger' import { LoggerService } from '../logger/service.logger'
import { NtfyProvider } from '../ntfy/provider'
import { RecurringReservationsService } from './service' import { RecurringReservationsService } from './service'
@Injectable() @Injectable()
@ -13,6 +14,9 @@ export class RecurringReservationsCronService {
@Inject(LoggerService) @Inject(LoggerService)
private readonly loggerService: LoggerService, private readonly loggerService: LoggerService,
@Inject(NtfyProvider)
private readonly ntfyProvider: NtfyProvider,
) {} ) {}
@Cron(CronExpression.EVERY_DAY_AT_4AM, { @Cron(CronExpression.EVERY_DAY_AT_4AM, {
@ -21,6 +25,9 @@ export class RecurringReservationsCronService {
}) })
async handleRecurringReservations() { async handleRecurringReservations() {
this.loggerService.log('handleRecurringReservations beginning') this.loggerService.log('handleRecurringReservations beginning')
await this.ntfyProvider.sendCronStartNotification(
'handleRecurringReservations',
)
const dayOfWeek = dayjs().get('day') const dayOfWeek = dayjs().get('day')
const recurringReservationsToSchedule = const recurringReservationsToSchedule =
await this.recurringReservationsService.getByDayOfWeek(dayOfWeek) await this.recurringReservationsService.getByDayOfWeek(dayOfWeek)
@ -33,5 +40,9 @@ export class RecurringReservationsCronService {
) )
} }
this.loggerService.log('handleRecurringReservations ending') this.loggerService.log('handleRecurringReservations ending')
await this.ntfyProvider.sendCronStopNotification(
'handleRecurringReservations',
`Count: ${recurringReservationsToSchedule.length}`,
)
} }
} }

View file

@ -1,8 +1,9 @@
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm' import { TypeOrmModule } from '@nestjs/typeorm'
import { ReservationsModule } from 'src/reservations/module'
import { LoggerModule } from '../logger/module' import { LoggerModule } from '../logger/module'
import { NtfyModule } from '../ntfy/module'
import { ReservationsModule } from '../reservations/module'
import { RunnerModule } from '../runner/module' import { RunnerModule } from '../runner/module'
import { RecurringReservationsController } from './controller' import { RecurringReservationsController } from './controller'
import { RecurringReservationsCronService } from './cron' import { RecurringReservationsCronService } from './cron'
@ -15,6 +16,7 @@ import { RecurringReservationsService } from './service'
TypeOrmModule.forFeature([RecurringReservation]), TypeOrmModule.forFeature([RecurringReservation]),
RunnerModule, RunnerModule,
ReservationsModule, ReservationsModule,
NtfyModule,
], ],
controllers: [RecurringReservationsController], controllers: [RecurringReservationsController],
providers: [RecurringReservationsService, RecurringReservationsCronService], providers: [RecurringReservationsService, RecurringReservationsCronService],

View file

@ -10,6 +10,7 @@ export class RecurringReservationsService {
constructor( constructor(
@InjectRepository(RecurringReservation) @InjectRepository(RecurringReservation)
private recurringReservationsRepository: Repository<RecurringReservation>, private recurringReservationsRepository: Repository<RecurringReservation>,
@Inject(ReservationsService) @Inject(ReservationsService)
private reservationsService: ReservationsService, private reservationsService: ReservationsService,
) {} ) {}

View file

@ -4,6 +4,7 @@ import { Cron, CronExpression } from '@nestjs/schedule'
import { Queue } from 'bull' import { Queue } from 'bull'
import { LoggerService } from '../logger/service.logger' import { LoggerService } from '../logger/service.logger'
import { NtfyProvider } from '../ntfy/provider'
import { RESERVATIONS_QUEUE_NAME } from './config' import { RESERVATIONS_QUEUE_NAME } from './config'
import { ReservationsService } from './service' import { ReservationsService } from './service'
@ -16,6 +17,9 @@ export class ReservationsCronService {
@InjectQueue(RESERVATIONS_QUEUE_NAME) @InjectQueue(RESERVATIONS_QUEUE_NAME)
private readonly reservationsQueue: Queue, private readonly reservationsQueue: Queue,
@Inject(NtfyProvider)
private readonly ntfyProvider: NtfyProvider,
@Inject(LoggerService) @Inject(LoggerService)
private readonly loggerService: LoggerService, private readonly loggerService: LoggerService,
) {} ) {}
@ -26,6 +30,7 @@ export class ReservationsCronService {
}) })
async handleDailyReservations() { async handleDailyReservations() {
this.loggerService.log('handleDailyReservations beginning') this.loggerService.log('handleDailyReservations beginning')
await this.ntfyProvider.sendCronStartNotification('handleDailyReservations')
const reservationsToPerform = await this.reservationService.getSchedulable() const reservationsToPerform = await this.reservationService.getSchedulable()
this.loggerService.log( this.loggerService.log(
`Found ${reservationsToPerform.length} reservations to perform`, `Found ${reservationsToPerform.length} reservations to perform`,
@ -34,6 +39,10 @@ export class ReservationsCronService {
reservationsToPerform.map((r) => ({ data: r, opts: { attempts: 1 } })), reservationsToPerform.map((r) => ({ data: r, opts: { attempts: 1 } })),
) )
this.loggerService.log('handleDailyReservations ending') this.loggerService.log('handleDailyReservations ending')
await this.ntfyProvider.sendCronStopNotification(
'handleDailyReservations',
`Count: ${reservationsToPerform.length}`,
)
} }
@Cron(CronExpression.EVERY_DAY_AT_11PM, { @Cron(CronExpression.EVERY_DAY_AT_11PM, {
@ -42,6 +51,9 @@ export class ReservationsCronService {
}) })
async cleanUpExpiredReservations() { async cleanUpExpiredReservations() {
this.loggerService.log('cleanUpExpiredReservations beginning') this.loggerService.log('cleanUpExpiredReservations beginning')
await this.ntfyProvider.sendCronStartNotification(
'cleanUpExpiredReservations',
)
const reservations = await this.reservationService.getByDate() const reservations = await this.reservationService.getByDate()
this.loggerService.log( this.loggerService.log(
`Found ${reservations.length} reservations to delete`, `Found ${reservations.length} reservations to delete`,
@ -50,5 +62,9 @@ export class ReservationsCronService {
await this.reservationService.deleteById(reservation.id) await this.reservationService.deleteById(reservation.id)
} }
this.loggerService.log('cleanUpExpiredReservations ending') this.loggerService.log('cleanUpExpiredReservations ending')
await this.ntfyProvider.sendCronStopNotification(
'cleanUpExpiredReservations',
`Count: ${reservations.length}`,
)
} }
} }

View file

@ -3,6 +3,7 @@ import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm' import { TypeOrmModule } from '@nestjs/typeorm'
import { LoggerModule } from '../logger/module' import { LoggerModule } from '../logger/module'
import { NtfyModule } from '../ntfy/module'
import { RunnerModule } from '../runner/module' import { RunnerModule } from '../runner/module'
import { RESERVATIONS_QUEUE_NAME } from './config' import { RESERVATIONS_QUEUE_NAME } from './config'
import { ReservationsController } from './controller' import { ReservationsController } from './controller'
@ -17,6 +18,7 @@ import { ReservationsWorker } from './worker'
TypeOrmModule.forFeature([Reservation]), TypeOrmModule.forFeature([Reservation]),
BullModule.registerQueue({ name: RESERVATIONS_QUEUE_NAME }), BullModule.registerQueue({ name: RESERVATIONS_QUEUE_NAME }),
RunnerModule, RunnerModule,
NtfyModule,
], ],
exports: [ReservationsService], exports: [ReservationsService],
controllers: [ReservationsController], controllers: [ReservationsController],

View file

@ -4,6 +4,7 @@ import { Job } from 'bull'
import { instanceToPlain, plainToInstance } from 'class-transformer' import { instanceToPlain, plainToInstance } from 'class-transformer'
import { LoggerService } from '../logger/service.logger' import { LoggerService } from '../logger/service.logger'
import { NtfyProvider } from '../ntfy/provider'
import { import {
BaanReserverenService, BaanReserverenService,
NoCourtAvailableError, NoCourtAvailableError,
@ -23,6 +24,9 @@ export class ReservationsWorker {
@Inject(LoggerService) @Inject(LoggerService)
private readonly loggerService: LoggerService, private readonly loggerService: LoggerService,
@Inject(NtfyProvider)
private readonly ntfyProvider: NtfyProvider,
) {} ) {}
@Process() @Process()
@ -33,11 +37,16 @@ export class ReservationsWorker {
this.loggerService.log('Handling reservation', { this.loggerService.log('Handling reservation', {
reservation: instanceToPlain(reservation), reservation: instanceToPlain(reservation),
}) })
await this.ntfyProvider.sendPerformingReservationNotification(
reservation.id,
reservation.dateRangeStart,
reservation.dateRangeEnd,
)
await this.performReservation(reservation) await this.performReservation(reservation)
} }
private async handleReservationErrors( private async handleReservationErrors(
error: unknown, error: Error,
reservation: Reservation, reservation: Reservation,
) { ) {
switch (true) { switch (true) {
@ -45,12 +54,23 @@ export class ReservationsWorker {
this.loggerService.warn('No court available') this.loggerService.warn('No court available')
if (!reservation.waitListed) { if (!reservation.waitListed) {
this.loggerService.log('Adding reservation to waiting list') this.loggerService.log('Adding reservation to waiting list')
await this.ntfyProvider.sendReservationWaitlistedNotification(
reservation.id,
reservation.dateRangeStart,
reservation.dateRangeEnd,
)
await this.addReservationToWaitList(reservation) await this.addReservationToWaitList(reservation)
} }
return return
} }
default: default:
this.loggerService.error('Error while performing reservation', error) this.loggerService.error('Error while performing reservation', error)
this.ntfyProvider.sendErrorPerformingReservationNotification(
reservation.id,
reservation.dateRangeStart,
reservation.dateRangeEnd,
error,
)
throw error throw error
} }
} }
@ -60,7 +80,7 @@ export class ReservationsWorker {
await this.brService.performReservation(reservation) await this.brService.performReservation(reservation)
await this.reservationsService.deleteById(reservation.id) await this.reservationsService.deleteById(reservation.id)
} catch (error: unknown) { } catch (error: unknown) {
await this.handleReservationErrors(error, reservation) await this.handleReservationErrors(error as Error, reservation)
} }
} }

View file

@ -2,9 +2,9 @@ import { Inject, Injectable } from '@nestjs/common'
import { instanceToPlain } from 'class-transformer' import { instanceToPlain } from 'class-transformer'
import { Dayjs } from 'dayjs' import { Dayjs } from 'dayjs'
import { ElementHandle, Page } from 'puppeteer' import { ElementHandle, Page } from 'puppeteer'
import { LoggerService } from 'src/logger/service.logger'
import dayjs from '../../common/dayjs' import dayjs from '../../common/dayjs'
import { LoggerService } from '../../logger/service.logger'
import { Reservation } from '../../reservations/entity' import { Reservation } from '../../reservations/entity'
import { EmptyPage } from '../pages/empty' import { EmptyPage } from '../pages/empty'

View file

@ -1,11 +1,11 @@
import { BullModule } from '@nestjs/bull' import { BullModule } from '@nestjs/bull'
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
import { EmailModule } from 'src/email/module'
import { ReservationsModule } from 'src/reservations/module'
import { EMAILS_QUEUE_NAME } from '../email/config' import { EMAILS_QUEUE_NAME } from '../email/config'
import { EmailModule } from '../email/module'
import { LoggerModule } from '../logger/module' import { LoggerModule } from '../logger/module'
import { RESERVATIONS_QUEUE_NAME } from '../reservations/config' import { RESERVATIONS_QUEUE_NAME } from '../reservations/config'
import { ReservationsModule } from '../reservations/module'
import { WaitingListService } from './service' import { WaitingListService } from './service'
@Module({ @Module({

View file

@ -1,14 +1,14 @@
import { InjectQueue, Process, Processor } from '@nestjs/bull' import { InjectQueue, Process, Processor } from '@nestjs/bull'
import { Inject } from '@nestjs/common' import { Inject } from '@nestjs/common'
import { Job, Queue } from 'bull' import { Job, Queue } from 'bull'
import { EmailProvider } from 'src/email/provider'
import { ReservationsService } from 'src/reservations/service'
import dayjs from '../common/dayjs' import dayjs from '../common/dayjs'
import { EMAILS_QUEUE_NAME } from '../email/config' import { EMAILS_QUEUE_NAME } from '../email/config'
import { EmailProvider } from '../email/provider'
import { Email } from '../email/types' import { Email } from '../email/types'
import { LoggerService } from '../logger/service.logger' import { LoggerService } from '../logger/service.logger'
import { RESERVATIONS_QUEUE_NAME } from '../reservations/config' import { RESERVATIONS_QUEUE_NAME } from '../reservations/config'
import { ReservationsService } from '../reservations/service'
import { WaitingListDetails } from './types' import { WaitingListDetails } from './types'
const EMAIL_SUBJECT_REGEX = new RegExp( const EMAIL_SUBJECT_REGEX = new RegExp(