From 85e0f7993eee91f213034d2c82ddccc2a2ed10c7 Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Wed, 9 Oct 2024 10:21:31 -0400 Subject: [PATCH] feat(NotificationsService): use existing notifier script to create notifications when possible --- .../notifications.service.spec.ts | 47 ++++++++++++++++++- .../notifications/notifications.service.ts | 46 ++++++++++++++---- 2 files changed, 83 insertions(+), 10 deletions(-) diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.spec.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.spec.ts index b0c5f567d..d31128c24 100644 --- a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.spec.ts +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.spec.ts @@ -13,6 +13,7 @@ import { } from '@app/graphql/generated/api/types'; import { NotificationSchema } from '@app/graphql/generated/api/operations'; import { mkdir } from 'fs/promises'; +import { type NotificationIni } from '@app/core/types/states/notification'; // defined outside `describe` so it's defined inside the `beforeAll` // needed to mock the dynamix import @@ -183,8 +184,12 @@ describe.sequential('NotificationsService', () => { }); it('generates unique ids', async () => { - const notifications = await Promise.all([...new Array(100)].map(() => createNotification())); - const notificationIds = new Set(notifications.map((notification) => notification.id)); + const notifications = await Promise.all( + // we break the "rules" here to speed up this test by ~450ms + // @ts-expect-error makeNotificationId is private + [...new Array(100)].map(() => service.makeNotificationId('test event')) + ); + const notificationIds = new Set(notifications); expect(notificationIds.size).toEqual(notifications.length); }); @@ -363,3 +368,41 @@ describe.sequential('NotificationsService', () => { expect.soft(overview.archive.total).toEqual(3); }); }); + +describe.concurrent('NotificationsService legacy script compatibility', () => { + let service: NotificationsService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [NotificationsService], + }).compile(); + + service = module.get(NotificationsService); + }); + + it.for([['normal'], ['warning'], ['alert']] as const)( + 'yields correct cli args for %ss', + ([importance], { expect }) => { + const notification: NotificationIni = { + event: 'Test Notification', + subject: 'Test Subject', + description: 'Test Description', + importance, + link: 'https://unraid.net', + timestamp: new Date().toISOString(), + }; + const [, args] = service.getLegacyScriptArgs(notification); + expect(args).toContain('-i'); + expect(args).toContain('-e'); + expect(args).toContain('-s'); + expect(args).toContain('-d'); + expect(args).toContain('-l'); + + expect(args).toContain(notification.event); + expect(args).toContain(notification.subject); + expect(args).toContain(notification.description); + expect(args).toContain(importance); + expect(args).toContain(notification.link); + } + ); +}); diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts index 0f43b4c07..113564561 100644 --- a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts @@ -23,6 +23,7 @@ import { encode as encodeIni } from 'ini'; import { v7 as uuidv7 } from 'uuid'; import { CHOKIDAR_USEPOLLING } from '@app/environment'; import { emptyDir } from 'fs-extra'; +import { execa } from 'execa'; @Injectable() export class NotificationsService { @@ -178,19 +179,48 @@ export class NotificationsService { public async createNotification(data: NotificationData): Promise { const id: string = await this.makeNotificationId(data.title); - const path = join(this.paths().UNREAD, id); - const fileData = this.makeNotificationFileData(data); - this.logger.debug(`[createNotification] FileData: ${JSON.stringify(fileData, null, 4)}`); - const ini = encodeIni(fileData); - // this.logger.debug(`[createNotification] INI: ${ini}`); - await writeFile(path, ini); - // await this.addToOverview(notification); - // make sure both NOTIFICATION_ADDED and NOTIFICATION_OVERVIEW are fired + try { + const [command, args] = this.getLegacyScriptArgs(fileData); + await execa(command, args); + } catch (error) { + // manually write the file if the script fails + this.logger.debug(`[createNotification] legacy notifier failed: ${error}`); + this.logger.verbose(`[createNotification] Writing: ${JSON.stringify(fileData, null, 4)}`); + + const path = join(this.paths().UNREAD, id); + const ini = encodeIni(fileData); + // this.logger.debug(`[createNotification] INI: ${ini}`); + await writeFile(path, ini); + } + return this.notificationFileToGqlNotification({ id, type: NotificationType.UNREAD }, fileData); } + /** + * Given a NotificationIni, returns a tuple containing the command and arguments to be + * passed to the legacy notifier script. + * + * The tuple represents a cli command to create an unraid notification. + * + * @param notification The notification to be converted to command line arguments. + * @returns A 2-element tuple containing the legacy notifier command and arguments. + */ + public getLegacyScriptArgs(notification: NotificationIni): [string, string[]] { + const { event, subject, description, link, importance } = notification; + const args = [ + ['-i', importance], + ['-e', event], + ['-s', subject], + ['-d', description], + ]; + if (link) { + args.push(['-l', link]); + } + return ['/usr/local/emhttp/webGui/scripts/notify', args.flat()]; + } + private async makeNotificationId(eventTitle: string, replacement = '_'): Promise { const { default: filenamify } = await import('filenamify'); const allWhitespace = /\s+/g;