feat(NotificationsService): use existing notifier script to create notifications when possible

This commit is contained in:
Pujit Mehrotra
2024-10-09 10:21:31 -04:00
parent d5a424ebe1
commit 85e0f7993e
2 changed files with 83 additions and 10 deletions

View File

@@ -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>(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);
}
);
});

View File

@@ -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<Notification> {
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<string> {
const { default: filenamify } = await import('filenamify');
const allWhitespace = /\s+/g;