diff --git a/server/package.json b/server/package.json index 84e3d2e7..860723d8 100644 --- a/server/package.json +++ b/server/package.json @@ -36,9 +36,12 @@ "axios": "^1.7.9", "chalk": "^4.1.2", "cron-validator": "^1.3.1", + "email-templates": "9.0.0", "lodash": "^4.17.21", "nest-winston": "^1.10.0", "node-cache": "^5.1.2", + "nodemailer": "6.9.1", + "openpgp": "^5.7.0", "plex-api": "^5.3.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -57,6 +60,7 @@ "@nestjs/cli": "^10.4.9", "@nestjs/schematics": "^10.2.3", "@nestjs/testing": "^10.4.15", + "@types/email-templates": "^10.0.4", "@types/jest": "^29.5.14", "@types/lodash": "^4.17.14", "@types/node": "^22", diff --git a/server/src/app/app.module.ts b/server/src/app/app.module.ts index 05878acf..7c9e400c 100644 --- a/server/src/app/app.module.ts +++ b/server/src/app/app.module.ts @@ -16,6 +16,8 @@ import { OverseerrApiService } from '../modules/api/overseerr-api/overseerr-api. import ormConfig from './config/typeOrmConfig'; import { TautulliApiModule } from '../modules/api/tautulli-api/tautulli-api.module'; import { TautulliApiService } from '../modules/api/tautulli-api/tautulli-api.service'; +import { NotificationsModule } from '../modules/notifications/notifications.module'; +import { NotificationService } from '../modules/notifications/notifications.service'; @Module({ imports: [ @@ -29,6 +31,7 @@ import { TautulliApiService } from '../modules/api/tautulli-api/tautulli-api.ser TautulliApiModule, RulesModule, CollectionsModule, + NotificationsModule, ], controllers: [AppController], providers: [AppService], @@ -39,12 +42,16 @@ export class AppModule implements OnModuleInit { private readonly plexApi: PlexApiService, private readonly overseerApi: OverseerrApiService, private readonly tautulliApi: TautulliApiService, + private readonly notificationService: NotificationService, ) {} async onModuleInit() { - // Initialize stuff needing settings here.. Otherwise problems + // Initialize modules requiring settings await this.settings.init(); await this.plexApi.initialize({}); await this.overseerApi.init(); await this.tautulliApi.init(); + + // intialize notification agents + await this.notificationService.registerConfiguredAgents(); } } diff --git a/server/src/database/migrations/1727693832830-Add_Notification_settings.ts b/server/src/database/migrations/1727693832830-Add_Notification_settings.ts new file mode 100644 index 00000000..72d54275 --- /dev/null +++ b/server/src/database/migrations/1727693832830-Add_Notification_settings.ts @@ -0,0 +1,38 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddNotificationSettings1727693832830 + implements MigrationInterface +{ + name = 'AddNotificationSettings1727693832830'; + + public async up(queryRunner: QueryRunner): Promise { + // Create notification table + await queryRunner.query(` + CREATE TABLE "notification" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "name" VARCHAR NOT NULL, + "agent" VARCHAR NOT NULL, + "enabled" BOOLEAN DEFAULT false, + "types" TEXT, + "options" TEXT NOT NULL + ); + `); + + // Create notification_rulegroup table with foreign key constraints directly + await queryRunner.query(` + CREATE TABLE "notification_rulegroup" ( + "notificationId" INTEGER NOT NULL, + "rulegroupId" INTEGER NOT NULL, + PRIMARY KEY ("notificationId", "rulegroupId"), + FOREIGN KEY ("notificationId") REFERENCES "notification"("id") ON DELETE CASCADE, + FOREIGN KEY ("rulegroupId") REFERENCES "rule_group"("id") ON DELETE CASCADE + ); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // Drop the tables in reverse order + await queryRunner.query(`DROP TABLE "notification_rulegroup"`); + await queryRunner.query(`DROP TABLE "notification"`); + } +} diff --git a/server/src/database/migrations/1732008945000-Notification_settings_aboutScale.ts b/server/src/database/migrations/1732008945000-Notification_settings_aboutScale.ts new file mode 100644 index 00000000..7ac51aa2 --- /dev/null +++ b/server/src/database/migrations/1732008945000-Notification_settings_aboutScale.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class NotificationSettingsAboutScale1732008945000 + implements MigrationInterface +{ + name = 'NotificationSettingsAboutScale1732008945000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "notification" ADD COLUMN "aboutScale" INTEGER NOT NULL DEFAULT 3; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "notification" DROP COLUMN "aboutScale"; + `); + } +} diff --git a/server/src/modules/collections/collection-worker.service.ts b/server/src/modules/collections/collection-worker.service.ts index b615d053..6f9d29b7 100644 --- a/server/src/modules/collections/collection-worker.service.ts +++ b/server/src/modules/collections/collection-worker.service.ts @@ -15,6 +15,8 @@ import { PlexMetadata } from '../api/plex-api/interfaces/media.interface'; import { EPlexDataType } from '../api/plex-api/enums/plex-data-type-enum'; import { TmdbIdService } from '../api/tmdb-api/tmdb-id.service'; import { TaskBase } from '../tasks/task.base'; +import { NotificationService } from '../notifications/notifications.service'; +import { NotificationType } from '../notifications/notifications-interfaces'; @Injectable() export class CollectionWorkerService extends TaskBase { @@ -37,6 +39,7 @@ export class CollectionWorkerService extends TaskBase { private readonly settings: SettingsService, private readonly tmdbIdService: TmdbIdService, private readonly tmdbIdHelper: TmdbIdService, + private readonly notificationService: NotificationService, ) { super(taskService); } @@ -46,76 +49,105 @@ export class CollectionWorkerService extends TaskBase { } public async execute() { - // check if another instance of this task is already running - if (await this.isRunning()) { - this.logger.log( - `Another instance of the ${this.name} task is currently running. Skipping this execution`, - ); - return; - } - - await super.execute(); + try { + // check if another instance of this task is already running + if (await this.isRunning()) { + this.logger.log( + `Another instance of the ${this.name} task is currently running. Skipping this execution`, + ); + return; + } - // wait 5 seconds to make sure we're not executing together with the rule handler - await new Promise((resolve) => setTimeout(resolve, 5000)); + await this.notificationService.registerConfiguredAgents(true); // re-register notification agents, to avoid flukes + await super.execute(); - // if we are, then wait.. - await this.taskService.waitUntilTaskIsFinished('Rule Handler', this.name); + // wait 5 seconds to make sure we're not executing together with the rule handler + await new Promise((resolve) => setTimeout(resolve, 5000)); - // Start actual task - const appStatus = await this.settings.testConnections(); + // if we are, then wait.. + await this.taskService.waitUntilTaskIsFinished('Rule Handler', this.name); - this.logger.log('Start handling all collections'); - let handledCollections = 0; - if (appStatus) { - // loop over all active collections - const collections = await this.collectionRepo.find({ - where: { isActive: true }, - }); - for (const collection of collections) { - this.infoLogger(`Handling collection '${collection.title}'`); + // Start actual task + const appStatus = await this.settings.testConnections(); - const collectionMedia = await this.collectionMediaRepo.find({ - where: { - collectionId: collection.id, - }, + this.logger.log('Start handling all collections'); + let handledCollections = 0; + if (appStatus) { + // loop over all active collections + const collections = await this.collectionRepo.find({ + where: { isActive: true }, }); + for (const collection of collections) { + this.infoLogger(`Handling collection '${collection.title}'`); - const dangerDate = new Date( - new Date().getTime() - +collection.deleteAfterDays * 86400000, - ); + const collectionMedia = await this.collectionMediaRepo.find({ + where: { + collectionId: collection.id, + }, + }); - for (const media of collectionMedia) { - // handle media addate <= due date - if (new Date(media.addDate) <= dangerDate) { - await this.handleMedia(collection, media); - handledCollections++; + const dangerDate = new Date( + new Date().getTime() - +collection.deleteAfterDays * 86400000, + ); + + const handledMediaForNotification = []; + for (const media of collectionMedia) { + // handle media addate <= due date + if (new Date(media.addDate) <= dangerDate) { + await this.handleMedia(collection, media); + handledCollections++; + handledMediaForNotification.push({ plexId: media.plexId }); + } } - } - this.infoLogger(`Handling collection '${collection.title}' finished`); - } - if (handledCollections > 0) { - if (this.settings.overseerrConfigured()) { - setTimeout(() => { - this.overseerrApi.api - .post('/settings/jobs/availability-sync/run') - .then(() => { - this.infoLogger( - `All collections handled. Triggered Overseerr's availability-sync because media was altered`, - ); - }); - }, 7000); + // handle notification + if (handledMediaForNotification.length > 0) { + await this.notificationService.handleNotification( + NotificationType.MEDIA_HANDLED, + handledMediaForNotification, + collection.title, + ); + } + + this.infoLogger(`Handling collection '${collection.title}' finished`); + } + if (handledCollections > 0) { + if (this.settings.overseerrConfigured()) { + setTimeout(() => { + this.overseerrApi.api + .post('/settings/jobs/availability-sync/run') + .then(() => { + this.infoLogger( + `All collections handled. Triggered Overseerr's availability-sync because media was altered`, + ); + }); + }, 7000); + } + } else { + this.infoLogger(`All collections handled. No data was altered`); } } else { - this.infoLogger(`All collections handled. No data was altered`); + this.infoLogger( + 'Not all applications are reachable.. Skipping collection handling', + ); + + // notify + await this.notificationService.handleNotification( + NotificationType.COLLECTION_HANDLING_FAILED, + undefined, + ); } - } else { - this.infoLogger( - 'Not all applications are reachable.. Skipping collection handling', + await this.finish(); + } catch (e) { + this.logger.error('An error occurred where handling collections'); + this.logger.debug(e); + + // notify + await this.notificationService.handleNotification( + NotificationType.COLLECTION_HANDLING_FAILED, + undefined, ); } - await this.finish(); } private async handleMedia(collection: Collection, media: CollectionMedia) { diff --git a/server/src/modules/collections/collections.module.ts b/server/src/modules/collections/collections.module.ts index 29018a6e..20066098 100644 --- a/server/src/modules/collections/collections.module.ts +++ b/server/src/modules/collections/collections.module.ts @@ -15,6 +15,8 @@ import { Exclusion } from '../rules/entities/exclusion.entities'; import { CollectionLog } from '../collections/entities/collection_log.entities'; import { CollectionLogCleanerService } from '../collections/tasks/collection-log-cleaner.service'; import { TautulliApiModule } from '../api/tautulli-api/tautulli-api.module'; +import { Notification } from '../notifications/entities/notification.entities'; +import { NotificationService } from '../notifications/notifications.service'; @Module({ imports: [ @@ -25,6 +27,7 @@ import { TautulliApiModule } from '../api/tautulli-api/tautulli-api.module'; CollectionLog, RuleGroup, Exclusion, + Notification, ]), OverseerrApiModule, TautulliApiModule, @@ -36,6 +39,7 @@ import { TautulliApiModule } from '../api/tautulli-api/tautulli-api.module'; CollectionsService, CollectionWorkerService, CollectionLogCleanerService, + NotificationService, ], controllers: [CollectionsController], exports: [CollectionsService], diff --git a/server/src/modules/collections/collections.service.ts b/server/src/modules/collections/collections.service.ts index 46e4f549..20b31ade 100644 --- a/server/src/modules/collections/collections.service.ts +++ b/server/src/modules/collections/collections.service.ts @@ -25,6 +25,7 @@ import { ICollection } from './interfaces/collection.interface'; import { Exclusion } from '../rules/entities/exclusion.entities'; import { CollectionLog } from '../../modules/collections/entities/collection_log.entities'; import { ECollectionLogType } from '../../modules/collections/entities/collection_log.entities'; +import { NotificationService } from '../notifications/notifications.service'; interface addCollectionDbResponse { id: number; @@ -54,6 +55,7 @@ export class CollectionsService { private readonly plexApi: PlexApiService, private readonly tmdbApi: TmdbApiService, private readonly tmdbIdHelper: TmdbIdService, + private readonly notificationService: NotificationService, ) {} async getCollection(id?: number, title?: string) { diff --git a/server/src/modules/notifications/agents/agent.ts b/server/src/modules/notifications/agents/agent.ts new file mode 100644 index 00000000..2582ec38 --- /dev/null +++ b/server/src/modules/notifications/agents/agent.ts @@ -0,0 +1,24 @@ +import { Notification } from '../entities/notification.entities'; +import { + NotificationAgentConfig, + NotificationAgentKey, + NotificationType, +} from '../notifications-interfaces'; + +export interface NotificationPayload { + event?: string; + subject: string; + notifySystem: boolean; + image?: string; + message?: string; + extra?: { name: string; value: string }[]; +} + +export interface NotificationAgent { + notification: Notification; + shouldSend(): boolean; + send(type: NotificationType, payload: NotificationPayload): Promise; + getIdentifier(): NotificationAgentKey; + getSettings(): NotificationAgentConfig; + getNotification(): Notification; +} diff --git a/server/src/modules/notifications/agents/discord.ts b/server/src/modules/notifications/agents/discord.ts new file mode 100644 index 00000000..b22acc11 --- /dev/null +++ b/server/src/modules/notifications/agents/discord.ts @@ -0,0 +1,172 @@ +import axios from 'axios'; +import { hasNotificationType } from '../notifications.service'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { Logger } from '@nestjs/common'; +import { + NotificationAgentDiscord, + NotificationAgentKey, + NotificationType, +} from '../notifications-interfaces'; +import { Notification } from '../entities/notification.entities'; + +enum EmbedColors { + DEFAULT = 0, + AQUA = 1752220, + GREEN = 3066993, + BLUE = 3447003, + PURPLE = 10181046, + GOLD = 15844367, + ORANGE = 15105570, + RED = 15158332, + GREY = 9807270, + DARKER_GREY = 8359053, + NAVY = 3426654, + DARK_AQUA = 1146986, + DARK_GREEN = 2067276, + DARK_BLUE = 2123412, + DARK_PURPLE = 7419530, + DARK_GOLD = 12745742, + DARK_ORANGE = 11027200, + DARK_RED = 10038562, + DARK_GREY = 9936031, + LIGHT_GREY = 12370112, + DARK_NAVY = 2899536, + LUMINOUS_VIVID_PINK = 16580705, + DARK_VIVID_PINK = 12320855, +} + +interface DiscordImageEmbed { + url?: string; + proxy_url?: string; + height?: number; + width?: number; +} + +interface Field { + name: string; + value: string; + inline?: boolean; +} +interface DiscordRichEmbed { + title?: string; + type?: 'rich'; // Always rich for webhooks + description?: string; + url?: string; + timestamp?: string; + color?: number; + footer?: { + text: string; + icon_url?: string; + proxy_icon_url?: string; + }; + image?: DiscordImageEmbed; + thumbnail?: DiscordImageEmbed; + provider?: { + name?: string; + url?: string; + }; + author?: { + name?: string; + url?: string; + icon_url?: string; + proxy_icon_url?: string; + }; + fields?: Field[]; +} + +interface DiscordWebhookPayload { + embeds: DiscordRichEmbed[]; + username?: string; + avatar_url?: string; + tts: boolean; + content?: string; +} + +class DiscordAgent implements NotificationAgent { + constructor( + private readonly settings: NotificationAgentDiscord, + readonly notification: Notification, + ) { + this.notification = notification; + } + + private readonly logger = new Logger(DiscordAgent.name); + + getNotification = () => this.notification; + + getSettings = () => this.settings; + + getIdentifier = () => NotificationAgentKey.DISCORD; + + public buildEmbed( + type: NotificationType, + payload: NotificationPayload, + ): DiscordRichEmbed { + const color = EmbedColors.DARK_PURPLE; + const fields: Field[] = []; + + for (const extra of payload.extra ?? []) { + fields.push({ + name: extra.name, + value: extra.value, + inline: true, + }); + } + return { + title: payload.subject, + description: payload.message, + color, + timestamp: new Date().toISOString(), + author: payload.event + ? { + name: payload.event, + } + : undefined, + fields, + thumbnail: { + url: payload.image, + }, + }; + } + + public shouldSend(): boolean { + if (this.getSettings().enabled && this.getSettings().options.webhookUrl) { + return true; + } + + return false; + } + + public async send( + type: NotificationType, + payload: NotificationPayload, + ): Promise { + if (!hasNotificationType(type, this.getSettings().types ?? [0])) { + return true; + } + + this.logger.log('Sending Discord notification'); + + try { + await axios.post( + this.getSettings().options.webhookUrl as string, + { + username: this.getSettings().options.botUsername + ? this.getSettings().options.botUsername + : 'Maintainerr', + avatar_url: this.getSettings().options.botAvatarUrl, + embeds: [this.buildEmbed(type, payload)], + } as DiscordWebhookPayload, + ); + + return true; + } catch (e) { + this.logger.warn('Error sending Discord notification'); + this.logger.debug(e); + + return false; + } + } +} + +export default DiscordAgent; diff --git a/server/src/modules/notifications/agents/email.ts b/server/src/modules/notifications/agents/email.ts new file mode 100644 index 00000000..3d141b36 --- /dev/null +++ b/server/src/modules/notifications/agents/email.ts @@ -0,0 +1,114 @@ +import type { EmailOptions } from 'email-templates'; +import path from 'path'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { Logger } from '@nestjs/common'; +import PreparedEmail from '../email/preparedEmail'; +import { + NotificationAgentEmail, + NotificationAgentKey, + NotificationType, +} from '../notifications-interfaces'; +import { SettingsService } from '../../settings/settings.service'; +import { Notification } from '../entities/notification.entities'; + +class EmailAgent implements NotificationAgent { + public constructor( + private readonly appSettings: SettingsService, + private readonly settings: NotificationAgentEmail, + readonly notification: Notification, + ) { + this.notification = notification; + } + + private readonly logger = new Logger(EmailAgent.name); + + getNotification = () => this.notification; + + getSettings = () => this.settings; + getIdentifier = () => NotificationAgentKey.EMAIL; + + public shouldSend(): boolean { + const settings = this.getSettings(); + + if ( + settings.enabled && + settings.options.emailFrom && + settings.options.smtpHost && + settings.options.smtpPort + ) { + return true; + } + + return false; + } + + private buildMessage( + type: NotificationType, + payload: NotificationPayload, + recipientEmail: string, + recipientName?: string, + ): EmailOptions | undefined { + if (type === NotificationType.TEST_NOTIFICATION) { + return { + template: path.join(__dirname, '../../../templates/email/test-email'), + message: { + to: recipientEmail, + }, + locals: { + body: payload.message, + applicationTitle: 'Maintainerr', + recipientName, + recipientEmail, + }, + }; + } + + return { + template: path.join(__dirname, '../templates/email-template'), + message: { + to: recipientEmail, + }, + locals: { + event: payload.event, + body: payload.message, + extra: payload.extra ?? [], + imageUrl: payload.image, + timestamp: new Date().toTimeString(), + applicationTitle: 'Maintainerr', + recipientName, + recipientEmail, + }, + }; + } + + public async send( + type: NotificationType, + payload: NotificationPayload, + ): Promise { + this.logger.log('Sending email notification'); + + try { + const email = new PreparedEmail( + this.appSettings, + this.getSettings() as NotificationAgentEmail, + this.getSettings().options.pgpKey as string, + ); + await email.send( + this.buildMessage( + type, + payload, + this.getSettings().options.emailTo as string, + ), + ); + } catch (e) { + this.logger.error('Error sending email notification'); + this.logger.debug(e); + + return false; + } + + return true; + } +} + +export default EmailAgent; diff --git a/server/src/modules/notifications/agents/gotify.ts b/server/src/modules/notifications/agents/gotify.ts new file mode 100644 index 00000000..afcfeea5 --- /dev/null +++ b/server/src/modules/notifications/agents/gotify.ts @@ -0,0 +1,104 @@ +import axios from 'axios'; +import { hasNotificationType } from '../notifications.service'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { Logger } from '@nestjs/common'; +import { SettingsService } from '../../settings/settings.service'; +import { + NotificationAgentGotify, + NotificationAgentKey, + NotificationType, +} from '../notifications-interfaces'; +import { Notification } from '../entities/notification.entities'; + +interface GotifyPayload { + title: string; + message: string; + priority: number; + extras: Record; +} + +class GotifyAgent implements NotificationAgent { + public constructor( + private readonly appSettings: SettingsService, + private readonly settings: NotificationAgentGotify, + readonly notification: Notification, + ) { + this.notification = notification; + } + + private readonly logger = new Logger(GotifyAgent.name); + + getNotification = () => this.notification; + + getSettings = () => this.settings; + + getIdentifier = () => NotificationAgentKey.GOTIFY; + + public shouldSend(): boolean { + const settings = this.getSettings(); + + if (settings.enabled && settings.options.url && settings.options.token) { + return true; + } + + return false; + } + + private getNotificationPayload( + type: NotificationType, + payload: NotificationPayload, + ): GotifyPayload { + const priority = 0; + + const title = payload.event + ? `${payload.event} - ${payload.subject}` + : payload.subject; + let message = payload.message ?? ''; + + for (const extra of payload.extra ?? []) { + message += `\n\n**${extra.name}**\n${extra.value}`; + } + + return { + extras: { + 'client::display': { + contentType: 'text/markdown', + }, + }, + title, + message, + priority, + }; + } + + public async send( + type: NotificationType, + payload: NotificationPayload, + ): Promise { + const settings = this.getSettings(); + + if ( + !payload.notifySystem || + !hasNotificationType(type, settings.types ?? [0]) + ) { + return true; + } + + this.logger.log('Sending Gotify notification'); + try { + const endpoint = `${settings.options.url}/message?token=${settings.options.token}`; + const notificationPayload = this.getNotificationPayload(type, payload); + + await axios.post(endpoint, notificationPayload); + + return true; + } catch (e) { + this.logger.error('Error sending Gotify notification'); + this.logger.debug(e); + + return false; + } + } +} + +export default GotifyAgent; diff --git a/server/src/modules/notifications/agents/lunasea.ts b/server/src/modules/notifications/agents/lunasea.ts new file mode 100644 index 00000000..89621baa --- /dev/null +++ b/server/src/modules/notifications/agents/lunasea.ts @@ -0,0 +1,96 @@ +import axios from 'axios'; +import { hasNotificationType } from '../notifications.service'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { Logger } from '@nestjs/common'; +import { SettingsService } from '../../settings/settings.service'; +import { + NotificationAgentKey, + NotificationAgentLunaSea, + NotificationType, +} from '../notifications-interfaces'; +import { Notification } from '../entities/notification.entities'; + +class LunaSeaAgent implements NotificationAgent { + public constructor( + private readonly appSettings: SettingsService, + private readonly settings: NotificationAgentLunaSea, + readonly notification: Notification, + ) { + this.notification = notification; + } + + private readonly logger = new Logger(LunaSeaAgent.name); + + getNotification = () => this.notification; + + getSettings = () => this.settings; + + getIdentifier = () => NotificationAgentKey.LUNASEA; + + private buildPayload(type: NotificationType, payload: NotificationPayload) { + return { + notification_type: NotificationType[type], + event: payload.event, + subject: payload.subject, + message: payload.message, + image: payload.image ?? null, + email: this.getSettings().options.email, + username: this.getSettings().options.displayName + ? this.getSettings().options.profileName + : this.getSettings().options.displayName, + avatar: this.getSettings().options.avatar, + extra: payload.extra ?? [], + }; + } + + public shouldSend(): boolean { + const settings = this.getSettings(); + + if (settings.enabled && settings.options.webhookUrl) { + return true; + } + + return false; + } + + public async send( + type: NotificationType, + payload: NotificationPayload, + ): Promise { + const settings = this.getSettings(); + + if ( + !payload.notifySystem || + !hasNotificationType(type, settings.types ?? [0]) + ) { + return true; + } + + this.logger.log('Sending LunaSea notification'); + + try { + await axios.post( + this.getSettings().options.webhookUrl as string, + this.buildPayload(type, payload), + settings.options.profileName + ? { + headers: { + Authorization: `Basic ${Buffer.from( + `${settings.options.profileName}:`, + ).toString('base64')}`, + }, + } + : undefined, + ); + + return true; + } catch (e) { + this.logger.error('Error sending LunaSea notification'); + this.logger.debug(e); + + return false; + } + } +} + +export default LunaSeaAgent; diff --git a/server/src/modules/notifications/agents/pushbullet.ts b/server/src/modules/notifications/agents/pushbullet.ts new file mode 100644 index 00000000..d1ab99c0 --- /dev/null +++ b/server/src/modules/notifications/agents/pushbullet.ts @@ -0,0 +1,121 @@ +import axios from 'axios'; +import { hasNotificationType } from '../notifications.service'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { SettingsService } from '../../settings/settings.service'; +import { Logger } from '@nestjs/common'; +import { + NotificationAgentKey, + NotificationAgentPushbullet, + NotificationType, +} from '../notifications-interfaces'; +import { Notification } from '../entities/notification.entities'; + +interface PushbulletPayload { + type: string; + title: string; + body: string; + channel_tag?: string; +} + +class PushbulletAgent implements NotificationAgent { + public constructor( + private readonly appSettings: SettingsService, + private readonly settings: NotificationAgentPushbullet, + readonly notification: Notification, + ) { + this.notification = notification; + } + + private readonly logger = new Logger(PushbulletAgent.name); + + getNotification = () => this.notification; + + getSettings = () => this.settings; + + getIdentifier = () => NotificationAgentKey.PUSHBULLET; + + public shouldSend(): boolean { + return true; + } + + private getNotificationPayload( + type: NotificationType, + payload: NotificationPayload, + ): PushbulletPayload { + const title = payload.event + ? `${payload.event} - ${payload.subject}` + : payload.subject; + let body = payload.message ?? ''; + + for (const extra of payload.extra ?? []) { + body += `\n${extra.name}: ${extra.value}`; + } + + return { + type: 'note', + title, + body, + }; + } + + public async send( + type: NotificationType, + payload: NotificationPayload, + ): Promise { + const settings = this.getSettings(); + const endpoint = 'https://api.pushbullet.com/v2/pushes'; + const notificationPayload = this.getNotificationPayload(type, payload); + + // Send system notification + if ( + payload.notifySystem && + hasNotificationType(type, settings.types ?? [0]) && + settings.enabled && + settings.options.accessToken + ) { + this.logger.debug('Sending Pushbullet notification', { + label: 'Notifications', + type: NotificationType[type], + subject: payload.subject, + }); + + try { + await axios.post( + endpoint, + { ...notificationPayload, channel_tag: settings.options.channelTag }, + { + headers: { + 'Access-Token': settings.options.accessToken as string, + }, + }, + ); + } catch (e) { + this.logger.error('Error sending Pushbullet notification'); + this.logger.debug(e); + + return false; + } + } + + if (settings.options.accessToken) { + this.logger.log('Sending Pushbullet notification'); + + try { + await axios.post(endpoint, notificationPayload, { + headers: { + 'Access-Token': settings.options.accessToken as string, + }, + }); + } catch (e) { + this.logger.error('Error sending Pushbullet notification'); + this.logger.debug(e); + + return false; + } + } + + return true; + } +} + +export default PushbulletAgent; diff --git a/server/src/modules/notifications/agents/pushover.ts b/server/src/modules/notifications/agents/pushover.ts new file mode 100644 index 00000000..627284b1 --- /dev/null +++ b/server/src/modules/notifications/agents/pushover.ts @@ -0,0 +1,153 @@ +import axios from 'axios'; +import { hasNotificationType } from '../notifications.service'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { SettingsService } from '../../settings/settings.service'; +import { Logger } from '@nestjs/common'; +import { + NotificationAgentKey, + NotificationAgentPushover, + NotificationType, +} from '../notifications-interfaces'; +import { Notification } from '../entities/notification.entities'; + +interface PushoverImagePayload { + attachment_base64: string; + attachment_type: string; +} + +interface PushoverPayload extends PushoverImagePayload { + token: string; + user: string; + title: string; + message: string; + url: string; + url_title: string; + priority: number; + html: number; +} + +class PushoverAgent implements NotificationAgent { + public constructor( + private readonly appSettings: SettingsService, + private readonly settings: NotificationAgentPushover, + readonly notification: Notification, + ) { + this.notification = notification; + } + + private readonly logger = new Logger(PushoverAgent.name); + + getNotification = () => this.notification; + + getSettings = () => this.settings; + + getIdentifier = () => NotificationAgentKey.PUSHOVER; + + public shouldSend(): boolean { + if ( + this.settings.enabled && + this.settings.options.accessToken && + this.settings.options.userToken + ) { + return true; + } + return false; + } + + private async getImagePayload( + imageUrl: string, + ): Promise> { + try { + const response = await axios.get(imageUrl, { + responseType: 'arraybuffer', + }); + const base64 = Buffer.from(response.data, 'binary').toString('base64'); + const contentType = ( + response.headers['Content-Type'] || response.headers['content-type'] + )?.toString(); + + return { + attachment_base64: base64, + attachment_type: contentType, + }; + } catch (e) { + this.logger.error('Error getting image payload', { + label: 'Notifications', + errorMessage: e.message, + response: e.response?.data, + }); + return {}; + } + } + + private async getNotificationPayload( + type: NotificationType, + payload: NotificationPayload, + ): Promise> { + const title = payload.event ?? payload.subject; + let message = payload.event ? `${payload.subject}` : ''; + const priority = 0; + + if (payload.message) { + message += `${message ? '\n' : ''}${payload.message}`; + } + + for (const extra of payload.extra ?? []) { + message += `\n${extra.name}: ${extra.value}`; + } + + let attachment_base64; + let attachment_type; + if (payload.image) { + const imagePayload = await this.getImagePayload(payload.image); + if (imagePayload.attachment_base64 && imagePayload.attachment_type) { + attachment_base64 = imagePayload.attachment_base64; + attachment_type = imagePayload.attachment_type; + } + } + + return { + title, + message, + priority, + html: 1, + attachment_base64, + attachment_type, + }; + } + + public async send( + type: NotificationType, + payload: NotificationPayload, + ): Promise { + const settings = this.getSettings(); + const endpoint = 'https://api.pushover.net/1/messages.json'; + const notificationPayload = await this.getNotificationPayload( + type, + payload, + ); + + // Send notification + if (hasNotificationType(type, settings.types ?? [0]) && this.shouldSend()) { + this.logger.log('Sending Pushover notification'); + + try { + await axios.post(endpoint, { + ...notificationPayload, + token: settings.options.accessToken, + user: settings.options.userToken, + sound: settings.options.sound, + } as PushoverPayload); + } catch (e) { + this.logger.error('Error sending Pushover notification'); + this.logger.debug(e); + + return false; + } + } + + return true; + } +} + +export default PushoverAgent; diff --git a/server/src/modules/notifications/agents/slack.ts b/server/src/modules/notifications/agents/slack.ts new file mode 100644 index 00000000..434bc925 --- /dev/null +++ b/server/src/modules/notifications/agents/slack.ts @@ -0,0 +1,182 @@ +import axios from 'axios'; +import { hasNotificationType } from '../notifications.service'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { SettingsService } from '../../settings/settings.service'; +import { Logger } from '@nestjs/common'; +import { + NotificationAgentKey, + NotificationAgentSlack, + NotificationType, +} from '../notifications-interfaces'; +import { Notification } from '../entities/notification.entities'; + +interface EmbedField { + type: 'plain_text' | 'mrkdwn'; + text: string; +} + +interface TextItem { + type: 'plain_text' | 'mrkdwn'; + text: string; + emoji?: boolean; +} + +interface Element { + type: 'button'; + text?: TextItem; + action_id: string; + url?: string; + value?: string; + style?: 'primary' | 'danger'; +} + +interface EmbedBlock { + type: 'header' | 'actions' | 'section' | 'context'; + block_id?: 'section789'; + text?: TextItem; + fields?: EmbedField[]; + accessory?: { + type: 'image'; + image_url: string; + alt_text: string; + }; + elements?: (Element | TextItem)[]; +} + +interface SlackBlockEmbed { + text: string; + blocks: EmbedBlock[]; +} + +class SlackAgent implements NotificationAgent { + public constructor( + private readonly appSettings: SettingsService, + private readonly settings: NotificationAgentSlack, + readonly notification: Notification, + ) { + this.notification = notification; + } + + private readonly logger = new Logger(SlackAgent.name); + + getNotification = () => this.notification; + + getSettings = () => this.settings; + + getIdentifier = () => NotificationAgentKey.SLACK; + + public buildEmbed( + type: NotificationType, + payload: NotificationPayload, + ): SlackBlockEmbed { + const fields: EmbedField[] = []; + + for (const extra of payload.extra ?? []) { + fields.push({ + type: 'mrkdwn', + text: `*${extra.name}*\n${extra.value}`, + }); + } + + const blocks: EmbedBlock[] = []; + + if (payload.event) { + blocks.push({ + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: `*${payload.event}*`, + }, + ], + }); + } + + blocks.push({ + type: 'header', + text: { + type: 'plain_text', + text: payload.subject, + }, + }); + + if (payload.message) { + blocks.push({ + type: 'section', + text: { + type: 'mrkdwn', + text: payload.message, + }, + accessory: payload.image + ? { + type: 'image', + image_url: payload.image, + alt_text: payload.subject, + } + : undefined, + }); + } + + if (fields.length > 0) { + blocks.push({ + type: 'section', + fields, + }); + } + + return { + text: payload.event ?? payload.subject, + blocks, + }; + } + + public shouldSend(): boolean { + const settings = this.getSettings(); + + if (settings.enabled && settings.options.webhookUrl) { + return true; + } + + return false; + } + + public async send( + type: NotificationType, + payload: NotificationPayload, + ): Promise { + const settings = this.getSettings(); + + if ( + !payload.notifySystem || + !hasNotificationType(type, settings.types ?? [0]) + ) { + return true; + } + + this.logger.debug('Sending Slack notification', { + label: 'Notifications', + type: NotificationType[type], + subject: payload.subject, + }); + try { + await axios.post( + settings.options.webhookUrl as string, + this.buildEmbed(type, payload), + ); + + return true; + } catch (e) { + this.logger.error('Error sending Slack notification', { + label: 'Notifications', + type: NotificationType[type], + subject: payload.subject, + errorMessage: e.message, + response: e.response?.data, + }); + + return false; + } + } +} + +export default SlackAgent; diff --git a/server/src/modules/notifications/agents/telegram.ts b/server/src/modules/notifications/agents/telegram.ts new file mode 100644 index 00000000..3e306d22 --- /dev/null +++ b/server/src/modules/notifications/agents/telegram.ts @@ -0,0 +1,122 @@ +import axios from 'axios'; +import { hasNotificationType } from '../notifications.service'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { SettingsService } from '../../settings/settings.service'; +import { Logger } from '@nestjs/common'; +import { + NotificationAgentKey, + NotificationAgentTelegram, + NotificationType, +} from '../notifications-interfaces'; +import { Notification } from '../entities/notification.entities'; + +interface TelegramMessagePayload { + text: string; + parse_mode: string; + chat_id: string; + disable_notification: boolean; +} + +interface TelegramPhotoPayload { + photo: string; + caption: string; + parse_mode: string; + chat_id: string; + disable_notification: boolean; +} + +class TelegramAgent implements NotificationAgent { + private baseUrl = 'https://api.telegram.org/'; + + public constructor( + private readonly appSettings: SettingsService, + private readonly settings: NotificationAgentTelegram, + readonly notification: Notification, + ) { + this.notification = notification; + } + + private readonly logger = new Logger(TelegramAgent.name); + + getNotification = () => this.notification; + + getSettings = () => this.settings; + + getIdentifier = () => NotificationAgentKey.TELEGRAM; + + public shouldSend(): boolean { + const settings = this.getSettings(); + + if (settings.enabled && settings.options.botAPI) { + return true; + } + + return false; + } + + private escapeText(text: string | undefined): string { + return text ? text.replace(/[_*[\]()~>#+=|{}.!-]/gi, (x) => '\\' + x) : ''; + } + + private getNotificationPayload( + type: NotificationType, + payload: NotificationPayload, + ): Partial { + let message = `\*${this.escapeText( + payload.event ? `${payload.event} - ${payload.subject}` : payload.subject, + )}\*`; + if (payload.message) { + message += `\n${this.escapeText(payload.message)}`; + } + + for (const extra of payload.extra ?? []) { + message += `\n\*${extra.name}:\* ${extra.value}`; + } + + return payload.image + ? { + photo: payload.image, + caption: message, + parse_mode: 'MarkdownV2', + } + : { + text: message, + parse_mode: 'MarkdownV2', + }; + } + + public async send( + type: NotificationType, + payload: NotificationPayload, + ): Promise { + const settings = this.getSettings(); + const endpoint = `${this.baseUrl}bot${settings.options.botAPI}/${ + payload.image ? 'sendPhoto' : 'sendMessage' + }`; + const notificationPayload = this.getNotificationPayload(type, payload); + + if ( + hasNotificationType(type, settings.types ?? [0]) && + settings.options.chatId + ) { + this.logger.debug('Sending Telegram notification'); + + try { + await axios.post(endpoint, { + ...notificationPayload, + chat_id: settings.options.chatId, + disable_notification: !!settings.options.sendSilently, + } as TelegramMessagePayload | TelegramPhotoPayload); + } catch (e) { + this.logger.error('Error sending Telegram notification'); + this.logger.debug(e); + + return false; + } + } + + return true; + } +} + +export default TelegramAgent; diff --git a/server/src/modules/notifications/agents/webhook.ts b/server/src/modules/notifications/agents/webhook.ts new file mode 100644 index 00000000..0460b4a8 --- /dev/null +++ b/server/src/modules/notifications/agents/webhook.ts @@ -0,0 +1,130 @@ +import axios from 'axios'; +import { get } from 'lodash'; +import { hasNotificationType } from '../notifications.service'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { SettingsService } from '../../settings/settings.service'; +import { Logger } from '@nestjs/common'; +import { + NotificationAgentKey, + NotificationAgentWebhook, + NotificationType, +} from '../notifications-interfaces'; +import { Notification } from '../entities/notification.entities'; + +type KeyMapFunction = ( + payload: NotificationPayload, + type: NotificationType, +) => string; + +const KeyMap: Record = { + notification_type: (_payload, type) => NotificationType[type], + event: 'event', + subject: 'subject', + message: 'message', + image: 'image', +}; + +class WebhookAgent implements NotificationAgent { + public constructor( + private readonly appSettings: SettingsService, + private readonly settings: NotificationAgentWebhook, + readonly notification: Notification, + ) { + this.notification = notification; + } + + private readonly logger = new Logger(WebhookAgent.name); + + getNotification = () => this.notification; + + getSettings = () => this.settings; + + getIdentifier = () => NotificationAgentKey.WEBHOOK; + + private parseKeys( + finalPayload: Record, + payload: NotificationPayload, + type: NotificationType, + ): Record { + Object.keys(finalPayload).forEach((key) => { + if (key === '{{extra}}') { + finalPayload.extra = payload.extra ?? []; + delete finalPayload[key]; + key = 'extra'; + } + + if (typeof finalPayload[key] === 'string') { + Object.keys(KeyMap).forEach((keymapKey) => { + const keymapValue = KeyMap[keymapKey as keyof typeof KeyMap]; + finalPayload[key] = (finalPayload[key] as string).replace( + `{{${keymapKey}}}`, + typeof keymapValue === 'function' + ? keymapValue(payload, type) + : (get(payload, keymapValue) ?? ''), + ); + }); + } else if (finalPayload[key] && typeof finalPayload[key] === 'object') { + finalPayload[key] = this.parseKeys( + finalPayload[key] as Record, + payload, + type, + ); + } + }); + + return finalPayload; + } + + private buildPayload(type: NotificationType, payload: NotificationPayload) { + const payloadString = this.getSettings().options.jsonPayload as string; + const parsedJSON = JSON.parse(payloadString); + + return this.parseKeys(parsedJSON, payload, type); + } + + public shouldSend(): boolean { + const settings = this.getSettings(); + + if (settings.enabled && settings.options.webhookUrl) { + return true; + } + + return false; + } + + public async send( + type: NotificationType, + payload: NotificationPayload, + ): Promise { + const settings = this.getSettings(); + + if (!hasNotificationType(type, settings.types ?? [0])) { + return true; + } + + this.logger.debug('Sending webhook notification'); + + try { + await axios.post( + settings.options.webhookUrl as string, + this.buildPayload(type, payload), + settings.options.authHeader + ? { + headers: { + Authorization: settings.options.authHeader as string, + }, + } + : undefined, + ); + + return true; + } catch (e) { + this.logger.error('Error sending webhook notification'); + this.logger.debug(e); + + return false; + } + } +} + +export default WebhookAgent; diff --git a/server/src/modules/notifications/email/openPgpEncrypt.ts b/server/src/modules/notifications/email/openPgpEncrypt.ts new file mode 100644 index 00000000..f2f09b9e --- /dev/null +++ b/server/src/modules/notifications/email/openPgpEncrypt.ts @@ -0,0 +1,207 @@ +import { Logger } from '@nestjs/common'; +import { randomBytes } from 'crypto'; +import * as openpgp from 'openpgp'; +import type { TransformCallback } from 'stream'; +import { Transform } from 'stream'; + +interface EncryptorOptions { + signingKey?: string; + password?: string; + encryptionKeys: string[]; +} + +class PGPEncryptor extends Transform { + private readonly logger = new Logger(PGPEncryptor.name); + + private _messageChunks: Uint8Array[] = []; + private _messageLength = 0; + private _signingKey?: string; + private _password?: string; + + private _encryptionKeys: string[]; + + constructor(options: EncryptorOptions) { + super(); + this._signingKey = options.signingKey; + this._password = options.password; + this._encryptionKeys = options.encryptionKeys; + } + + // just save the whole message + _transform = ( + chunk: Uint8Array, + _encoding: BufferEncoding, + callback: TransformCallback, + ): void => { + this._messageChunks.push(chunk); + this._messageLength += chunk.length; + callback(); + }; + + // Actually do stuff + _flush = async (callback: TransformCallback): Promise => { + const message = Buffer.concat(this._messageChunks, this._messageLength); + + try { + // Reconstruct message as buffer + const validPublicKeys = await Promise.all( + this._encryptionKeys.map((armoredKey) => + openpgp.readKey({ armoredKey }), + ), + ); + let privateKey: openpgp.PrivateKey | undefined; + + // Just return the message if there is no one to encrypt for + if (!validPublicKeys.length) { + this.push(message); + return callback(); + } + + // Only sign the message if private key and password exist + if (this._signingKey && this._password) { + privateKey = await openpgp.decryptKey({ + privateKey: await openpgp.readPrivateKey({ + armoredKey: this._signingKey, + }), + passphrase: this._password, + }); + } + + const emailPartDelimiter = '\r\n\r\n'; + const messageParts = message.toString().split(emailPartDelimiter); + + /** + * In this loop original headers are split up into two parts, + * one for the email that is sent + * and one for the encrypted content + */ + const header = messageParts.shift() as string; + const emailHeaders: string[][] = []; + const contentHeaders: string[][] = []; + const linesInHeader = header.split('\r\n'); + let previousHeader: string[] = []; + for (let i = 0; i < linesInHeader.length; i++) { + const line = linesInHeader[i]; + /** + * If it is a multi-line header (current line starts with whitespace) + * or it's the first line in the iteration + * add the current line with previous header and move on + */ + if (/^\s/.test(line) || i === 0) { + previousHeader.push(line); + continue; + } + + /** + * This is done to prevent the last header + * from being missed + */ + if (i === linesInHeader.length - 1) { + previousHeader.push(line); + } + + /** + * We need to seperate the actual content headers + * so that we can add it as a header for the encrypted content + * So that the content will be displayed properly after decryption + */ + if ( + /^(content-type|content-transfer-encoding):/i.test(previousHeader[0]) + ) { + contentHeaders.push(previousHeader); + } else { + emailHeaders.push(previousHeader); + } + previousHeader = [line]; + } + + // Generate a new boundary for the email content + const boundary = 'nm_' + randomBytes(14).toString('hex'); + /** + * Concatenate everything into single strings + * and add pgp headers to the email headers + */ + const emailHeadersRaw = + emailHeaders.map((line) => line.join('\r\n')).join('\r\n') + + '\r\n' + + 'Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";' + + '\r\n' + + ' boundary="' + + boundary + + '"' + + '\r\n' + + 'Content-Description: OpenPGP encrypted message' + + '\r\n' + + 'Content-Transfer-Encoding: 7bit'; + const contentHeadersRaw = contentHeaders + .map((line) => line.join('\r\n')) + .join('\r\n'); + + const encryptedMessage = await openpgp.encrypt({ + message: await openpgp.createMessage({ + text: + contentHeadersRaw + + emailPartDelimiter + + messageParts.join(emailPartDelimiter), + }), + encryptionKeys: validPublicKeys, + signingKeys: privateKey, + }); + + const body = + '--' + + boundary + + '\r\n' + + 'Content-Type: application/pgp-encrypted\r\n' + + 'Content-Transfer-Encoding: 7bit\r\n' + + '\r\n' + + 'Version: 1\r\n' + + '\r\n' + + '--' + + boundary + + '\r\n' + + 'Content-Type: application/octet-stream; name=encrypted.asc\r\n' + + 'Content-Disposition: inline; filename=encrypted.asc\r\n' + + 'Content-Transfer-Encoding: 7bit\r\n' + + '\r\n' + + encryptedMessage + + '\r\n--' + + boundary + + '--\r\n'; + + this.push(Buffer.from(emailHeadersRaw + emailPartDelimiter + body)); + callback(); + } catch (e) { + this.logger.error( + 'Something went wrong while encrypting email message with OpenPGP. Sending email without encryption', + { + label: 'Notifications', + errorMessage: e.message, + }, + ); + + this.push(message); + callback(); + } + }; +} + +export const openpgpEncrypt = (options: EncryptorOptions) => { + // Disabling this line because I don't want to fix it but I am tired + // of seeing the lint warning + + return function (mail: any, callback: () => unknown): void { + if (!options.encryptionKeys.length) { + setImmediate(callback); + } + mail.message.transform( + () => + new PGPEncryptor({ + signingKey: options.signingKey, + password: options.password, + encryptionKeys: options.encryptionKeys, + }), + ); + setImmediate(callback); + }; +}; diff --git a/server/src/modules/notifications/email/preparedEmail.ts b/server/src/modules/notifications/email/preparedEmail.ts new file mode 100644 index 00000000..70376aee --- /dev/null +++ b/server/src/modules/notifications/email/preparedEmail.ts @@ -0,0 +1,61 @@ +import nodemailer from 'nodemailer'; +import Email from 'email-templates'; +import { URL } from 'url'; +import { NotificationAgentEmail } from '../notifications-interfaces'; +import { SettingsService } from '../../settings/settings.service'; +import { openpgpEncrypt } from './openPgpEncrypt'; + +class PreparedEmail extends Email { + public constructor( + applicationSettings: SettingsService, + settings: NotificationAgentEmail, + pgpKey?: string, + ) { + const { applicationUrl } = applicationSettings; + + const transport = nodemailer.createTransport({ + name: applicationUrl ? new URL(applicationUrl).hostname : undefined, + host: settings.options.smtpHost, + port: settings.options.smtpPort, + secure: settings.options.secure, + ignoreTLS: settings.options.ignoreTls, + requireTLS: settings.options.requireTls, + tls: settings.options.allowSelfSigned + ? { + rejectUnauthorized: false, + } + : undefined, + auth: + settings.options.authUser && settings.options.authPass + ? { + user: settings.options.authUser, + pass: settings.options.authPass, + } + : undefined, + }); + + if (pgpKey) { + transport.use( + 'stream', + openpgpEncrypt({ + signingKey: settings.options.pgpPrivateKey, + password: settings.options.pgpPassword, + encryptionKeys: [pgpKey], + }), + ); + } + + super({ + message: { + from: { + name: settings.options.senderName, + address: settings.options.emailFrom, + }, + }, + send: true, + transport: transport, + }); + } +} + +export default PreparedEmail; diff --git a/server/src/modules/notifications/entities/notification.entities.ts b/server/src/modules/notifications/entities/notification.entities.ts new file mode 100644 index 00000000..d91856c6 --- /dev/null +++ b/server/src/modules/notifications/entities/notification.entities.ts @@ -0,0 +1,31 @@ +import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { RuleGroup } from '../../rules/entities/rule-group.entities'; + +@Entity() +export class Notification { + @PrimaryGeneratedColumn() + id: number; + + @Column({ nullable: false }) + name: string; + + @Column({ nullable: false }) + agent: string; + + @Column({ default: false }) + enabled: boolean; + + @Column('text', { nullable: true }) + types: string; + + @Column({ type: 'simple-json', nullable: false }) + options: string; + + @Column({ default: 3, nullable: false }) + aboutScale: number; + + @ManyToMany(() => RuleGroup, (rulegroup) => rulegroup.notifications, { + onDelete: 'CASCADE', + }) + rulegroups: RuleGroup[]; +} diff --git a/server/src/modules/notifications/notifications-interfaces.ts b/server/src/modules/notifications/notifications-interfaces.ts new file mode 100644 index 00000000..450159c1 --- /dev/null +++ b/server/src/modules/notifications/notifications-interfaces.ts @@ -0,0 +1,104 @@ +export interface NotificationAgentConfig { + enabled: boolean; + types?: number[]; + options: Record; +} +export interface NotificationAgentDiscord extends NotificationAgentConfig { + options: NotificationAgentConfig['options'] & { + botUsername?: string; + botAvatarUrl?: string; + webhookUrl: string; + }; +} + +export interface NotificationAgentSlack extends NotificationAgentConfig { + options: NotificationAgentConfig['options'] & { + webhookUrl: string; + }; +} + +export interface NotificationAgentEmail extends NotificationAgentConfig { + options: NotificationAgentConfig['options'] & { + emailFrom: string; + smtpHost: string; + smtpPort: number; + secure: boolean; + ignoreTls: boolean; + requireTls: boolean; + authUser?: string; + authPass?: string; + allowSelfSigned: boolean; + senderName: string; + pgpPrivateKey?: string; + pgpPassword?: string; + }; +} + +export interface NotificationAgentLunaSea extends NotificationAgentConfig { + options: NotificationAgentConfig['options'] & { + webhookUrl: string; + profileName?: string; + }; +} + +export interface NotificationAgentTelegram extends NotificationAgentConfig { + options: NotificationAgentConfig['options'] & { + botUsername?: string; + botAPI: string; + chatId: string; + sendSilently: boolean; + }; +} + +export interface NotificationAgentPushbullet extends NotificationAgentConfig { + options: NotificationAgentConfig['options'] & { + accessToken: string; + channelTag?: string; + }; +} + +export interface NotificationAgentPushover extends NotificationAgentConfig { + options: NotificationAgentConfig['options'] & { + accessToken: string; + userToken: string; + sound: string; + }; +} + +export interface NotificationAgentWebhook extends NotificationAgentConfig { + options: NotificationAgentConfig['options'] & { + webhookUrl: string; + jsonPayload: string; + authHeader?: string; + }; +} + +export interface NotificationAgentGotify extends NotificationAgentConfig { + options: NotificationAgentConfig['options'] & { + url: string; + token: string; + }; +} + +export enum NotificationAgentKey { + DISCORD = 'discord', + EMAIL = 'email', + GOTIFY = 'gotify', + PUSHBULLET = 'pushbullet', + PUSHOVER = 'pushover', + SLACK = 'slack', + TELEGRAM = 'telegram', + WEBHOOK = 'webhook', + LUNASEA = 'lunasea', +} + +export enum NotificationType { + NONE = 0, + MEDIA_ADDED_TO_COLLECTION = 2, + MEDIA_REMOVED_FROM_COLLECTION = 4, + MEDIA_ABOUT_TO_BE_HANDLED = 8, // TODO + MEDIA_HANDLED = 16, + RULE_HANDLING_FAILED = 32, + COLLECTION_HANDLING_FAILED = 64, + TEST_NOTIFICATION = 128, +} diff --git a/server/src/modules/notifications/notifications-timer.service.ts b/server/src/modules/notifications/notifications-timer.service.ts new file mode 100644 index 00000000..d62debb1 --- /dev/null +++ b/server/src/modules/notifications/notifications-timer.service.ts @@ -0,0 +1,102 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { TaskBase } from '../tasks/task.base'; +import { TasksService } from '../tasks/tasks.service'; +import { NotificationService } from './notifications.service'; +import { NotificationType } from './notifications-interfaces'; +import { CollectionsService } from '../collections/collections.service'; + +// This job sends notifications for the "About to Be Removed" notificaton type. The job loops through all configured notification providers and sends one notification per provider. +// Each notification includes all media items from all active child collections that are scheduled for removal within the specified number of days. + +// Each media item will only be notified once per notification provider, on the specified day. If this job runs multiple times a day, multiple notifications for the same media items would be sent out. +@Injectable() +export class NotificationTimerService extends TaskBase { + protected logger = new Logger(NotificationTimerService.name); + + protected name = 'Notification Timer'; + protected cronSchedule = '0 14 * * *'; + protected type = NotificationType.MEDIA_ABOUT_TO_BE_HANDLED; + + constructor( + protected readonly taskService: TasksService, + protected readonly collectionService: CollectionsService, + private readonly notificationService: NotificationService, + ) { + super(taskService); + } + + protected onBootstrapHook(): void {} + + public async execute() { + await this.notificationService.registerConfiguredAgents(true); // re register notification agents + + // helper submethod + const getDayStart = (date) => new Date(date.setHours(0, 0, 0, 0)); + + // check if another instance of this task is already running + if (await this.isRunning()) { + this.logger.log( + `Another instance of the ${this.name} task is currently running. Skipping this execution`, + ); + return; + } + + await super.execute(); + + const activeAgents = this.notificationService.getActiveAgents(); + const allNotificationConfigurations = + await this.notificationService.getNotificationConfigurations(true); + + activeAgents.forEach(async (agent) => { + const notification = allNotificationConfigurations.find( + (n) => n.id === agent.getNotification().id, + ); + + if (notification && notification.enabled) { + const itemsToNotify = ( + await Promise.all( + (notification.rulegroups || []).map(async (group) => { + const notifyDate = new Date( + new Date().getTime() - + group.collection.deleteAfterDays * 86400000 + + notification.aboutScale * 86400000, + ); + + const collectionMedia = + await this.collectionService.getCollectionMedia( + group.collection?.id, + ); + + return ( + collectionMedia?.filter((media) => { + const mediaDate = new Date(media.addDate); + return ( + getDayStart(mediaDate).getTime() === + getDayStart(notifyDate).getTime() + ); + }) || [] + ); + }), + ) + ).flat(); + + const transformedItems = itemsToNotify.map((i) => ({ + plexId: i.plexId, + })); + + // send the notification if required + if (notification.rulegroups && transformedItems.length > 0) { + await this.notificationService.handleNotification( + this.type, + transformedItems, + undefined, + notification.aboutScale, + agent, + ); + } + } + }); + + this.finish(); + } +} diff --git a/server/src/modules/notifications/notifications.controller.ts b/server/src/modules/notifications/notifications.controller.ts new file mode 100644 index 00000000..60bb0fa0 --- /dev/null +++ b/server/src/modules/notifications/notifications.controller.ts @@ -0,0 +1,80 @@ +import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common'; +import { NotificationService } from './notifications.service'; +import { NotificationType } from './notifications-interfaces'; + +@Controller('api/notifications') +export class NotificationsController { + constructor(private readonly notificationService: NotificationService) {} + + @Post('/test') + public async sendTestNotification() { + await this.notificationService.handleNotification( + NotificationType.TEST_NOTIFICATION, + null, + ); + } + + @Get('/agents') + getNotificationAgents() { + return this.notificationService.getAgentSpec(); + } + + @Get('/types') + getNotificationTypes() { + return this.notificationService.getTypes(); + } + + @Post('/configuration/add') + async addNotificationConfiguration( + @Body() + payload: { + id?: number; + agent: string; + name: string; + enabled: boolean; + types: number[]; + aboutScale: number; + options: object; + }, + ) { + return this.notificationService.addNotificationConfiguration(payload); + } + + @Post('/configuration/connect') + async connectNotificationConfiguration( + @Body() + payload: { + rulegroupId: number; + notificationId: number; + }, + ) { + return this.notificationService.connectNotificationConfigurationToRule( + payload, + ); + } + + @Post('/configuration/disconnect') + async disconnectionNotificationConfiguration( + @Body() + payload: { + rulegroupId: number; + notificationId: number; + }, + ) { + return this.notificationService.disconnectNotificationConfigurationFromRule( + payload, + ); + } + + @Get('/configurations') + async getNotificationConfigurations() { + return this.notificationService.getNotificationConfigurations(); + } + + @Delete('/configuration/:id') + async deleteNotificationConfiguration(@Param('id') notificationId: number) { + return this.notificationService.deleteNotificationConfiguration( + notificationId, + ); + } +} diff --git a/server/src/modules/notifications/notifications.module.ts b/server/src/modules/notifications/notifications.module.ts new file mode 100644 index 00000000..941bc2e4 --- /dev/null +++ b/server/src/modules/notifications/notifications.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { NotificationsController } from './notifications.controller'; +import { NotificationService } from './notifications.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Notification } from './entities/notification.entities'; +import { RuleGroup } from '../rules/entities/rule-group.entities'; +import { PlexApiModule } from '../api/plex-api/plex-api.module'; +import { NotificationTimerService } from './notifications-timer.service'; +import { TasksModule } from '../tasks/tasks.module'; +import { CollectionsModule } from '../collections/collections.module'; + +@Module({ + imports: [ + PlexApiModule, + CollectionsModule, + TasksModule, + TypeOrmModule.forFeature([Notification, RuleGroup]), + ], + providers: [NotificationService, NotificationTimerService], + controllers: [NotificationsController], + exports: [NotificationService], +}) +export class NotificationsModule {} diff --git a/server/src/modules/notifications/notifications.service.ts b/server/src/modules/notifications/notifications.service.ts new file mode 100644 index 00000000..dfe4b7e9 --- /dev/null +++ b/server/src/modules/notifications/notifications.service.ts @@ -0,0 +1,641 @@ +import { InjectRepository } from '@nestjs/typeorm'; +import type { NotificationAgent, NotificationPayload } from './agents/agent'; +import { Injectable, Logger } from '@nestjs/common'; +import { Notification } from './entities/notification.entities'; +import { DataSource, Repository } from 'typeorm'; +import { RuleGroup } from '../rules/entities/rule-group.entities'; +import { + NotificationAgentKey, + NotificationType, +} from './notifications-interfaces'; +import DiscordAgent from './agents/discord'; +import PushoverAgent from './agents/pushover'; +import { SettingsService } from '../settings/settings.service'; +import { PlexApiService } from '../api/plex-api/plex-api.service'; +import { PlexMetadata } from '../api/plex-api/interfaces/media.interface'; +import EmailAgent from './agents/email'; +import GotifyAgent from './agents/gotify'; +import LunaSeaAgent from './agents/lunasea'; +import PushbulletAgent from './agents/pushbullet'; +import SlackAgent from './agents/slack'; +import TelegramAgent from './agents/telegram'; +import WebhookAgent from './agents/webhook'; + +export const hasNotificationType = ( + type: NotificationType, + value: NotificationType[], +): boolean => { + // If we are not checking any notifications, bail out and return true + if (type === 0) { + return true; + } + + return value.includes(type); +}; + +@Injectable() +export class NotificationService { + private activeAgents: NotificationAgent[] = []; + private readonly logger = new Logger(NotificationService.name); + + constructor( + @InjectRepository(Notification) + private readonly notificationRepo: Repository, + @InjectRepository(RuleGroup) + private readonly ruleGroupRepo: Repository, + private readonly connection: DataSource, + private readonly settings: SettingsService, + private readonly plexApi: PlexApiService, + ) {} + + public registerAgents = ( + agents: NotificationAgent[], + skiplog = false, + ): void => { + this.activeAgents = [...this.activeAgents, ...agents]; + + if (!skiplog) { + this.logger.log( + `Registered ${agents.length} notification agent${agents.length === 1 ? '' : 's'}`, + ); + } + }; + + public getActiveAgents = () => { + return this.activeAgents; + }; + + public sendNotification( + type: NotificationType, + payload: NotificationPayload, + ): void { + this.activeAgents.forEach((agent) => { + this.sendNotificationToAgent(type, payload, agent); + }); + } + + public sendNotificationToAgent( + type: NotificationType, + payload: NotificationPayload, + agent: NotificationAgent, + ): void { + if (agent.shouldSend()) { + if (agent.getSettings().types?.includes(type)) agent.send(type, payload); + } + } + + async addNotificationConfiguration(payload: { + id?: number; + agent: string; + name: string; + enabled: boolean; + types: number[]; + aboutScale: number; + options: object; + }) { + try { + if (payload.id !== undefined) { + // update + await this.connection + .createQueryBuilder() + .update(Notification) + .set({ + name: payload.name, + agent: payload.agent, + enabled: payload.enabled, + aboutScale: payload.aboutScale, + types: JSON.stringify(payload.types), + options: JSON.stringify(payload.options), + }) + .where('id = :id', { id: payload.id }) + .execute(); + } else { + await this.connection + .createQueryBuilder() + .insert() + .into(Notification) + .values({ + name: payload.name, + agent: payload.agent, + enabled: payload.enabled, + aboutScale: payload.aboutScale, + + types: JSON.stringify(payload.types), + options: JSON.stringify(payload.options), + }) + .execute(); + } + // reset & reload notification agents + this.registerConfiguredAgents(true); + return { code: 1, result: 'success' }; + } catch (err) { + this.logger.warn('Adding a new notification configuration failed'); + this.logger.debug(err); + return { code: 0, result: err }; + } + } + + async connectNotificationConfigurationToRule(payload: { + rulegroupId: number; + notificationId: number; + }) { + try { + if (payload.rulegroupId && payload.notificationId) { + const ruleGroup = await this.ruleGroupRepo.findOne({ + where: { id: payload.rulegroupId }, + }); + + const notificationConfig = await this.notificationRepo.findOne({ + where: { id: payload.notificationId }, + }); + + if (ruleGroup && notificationConfig) { + ruleGroup.notifications.push(notificationConfig); + await this.ruleGroupRepo.save(ruleGroup); + return { code: 1, result: 'success' }; + } + } + this.logger.warn('Connecting the notification configuration failed'); + return { code: 0, result: 'failed' }; + } catch (err) { + this.logger.error('Connecting the notification configuration failed'); + this.logger.debug(err); + return { code: 0, result: err }; + } + } + + async disconnectNotificationConfigurationFromRule(payload: { + rulegroupId: number; + notificationId: number; + }) { + try { + const ruleGroup = await this.ruleGroupRepo.findOne({ + where: { id: payload.rulegroupId }, + }); + + const notificationConfig = await this.notificationRepo.findOne({ + where: { id: payload.notificationId }, + }); + + if (ruleGroup && notificationConfig) { + ruleGroup.notifications = ruleGroup.notifications.filter( + (c) => c.id !== payload.notificationId, + ); + await this.ruleGroupRepo.save(ruleGroup); + return { code: 1, result: 'success' }; + } + + return { code: 0, result: 'failed' }; + } catch (err) { + this.logger.error('Disconnecting the notification configuration failed'); + this.logger.debug(err); + return { code: 0, result: err }; + } + } + + async getNotificationConfigurations(withRelation = false) { + try { + if (withRelation) { + const notifConfigs = await this.notificationRepo.find(); + // hack to get the relationship working. I was tired of the typeORM headache + return await Promise.all( + notifConfigs.map(async (n) => { + n.rulegroups = await this.ruleGroupRepo.find({ + where: { notifications: { id: n.id } }, + }); + return n; + }), + ); + } + + return await this.notificationRepo.find(); + } catch (err) { + this.logger.warn('Fetching Notification configurations failed'); + this.logger.debug(err); + } + } + + public async registerConfiguredAgents(skiplog = false) { + this.activeAgents = []; + const configuredAgents = await this.getNotificationConfigurations(); + + const agents: NotificationAgent[] = configuredAgents?.map( + (notification) => { + switch (notification.agent) { + case NotificationAgentKey.DISCORD: + return new DiscordAgent( + { + enabled: notification.enabled, + types: JSON.parse(notification.types), + options: JSON.parse(notification.options), + }, + notification, + ); + case NotificationAgentKey.PUSHOVER: + return new PushoverAgent( + this.settings, + { + enabled: notification.enabled, + types: JSON.parse(notification.types), + options: JSON.parse(notification.options), + }, + notification, + ); + case NotificationAgentKey.EMAIL: + return new EmailAgent( + this.settings, + { + enabled: notification.enabled, + types: JSON.parse(notification.types), + options: JSON.parse(notification.options), + }, + notification, + ); + case NotificationAgentKey.GOTIFY: + return new GotifyAgent( + this.settings, + { + enabled: notification.enabled, + types: JSON.parse(notification.types), + options: JSON.parse(notification.options), + }, + notification, + ); + case NotificationAgentKey.LUNASEA: + return new LunaSeaAgent( + this.settings, + { + enabled: notification.enabled, + types: JSON.parse(notification.types), + options: JSON.parse(notification.options), + }, + notification, + ); + case NotificationAgentKey.PUSHBULLET: + return new PushbulletAgent( + this.settings, + { + enabled: notification.enabled, + types: JSON.parse(notification.types), + options: JSON.parse(notification.options), + }, + notification, + ); + case NotificationAgentKey.SLACK: + return new SlackAgent( + this.settings, + { + enabled: notification.enabled, + types: JSON.parse(notification.types), + options: JSON.parse(notification.options), + }, + notification, + ); + case NotificationAgentKey.TELEGRAM: + return new TelegramAgent( + this.settings, + { + enabled: notification.enabled, + types: JSON.parse(notification.types), + options: JSON.parse(notification.options), + }, + notification, + ); + case NotificationAgentKey.WEBHOOK: + return new WebhookAgent( + this.settings, + { + enabled: notification.enabled, + types: JSON.parse(notification.types), + options: JSON.parse(notification.options), + }, + notification, + ); + } + }, + ); + + this.registerAgents(agents, skiplog); + } + + async deleteNotificationConfiguration(notificationId: number) { + try { + await this.notificationRepo.delete(notificationId); + + // reset & reload notification agents + this.registerConfiguredAgents(true); + + return { code: 1, result: 'success' }; + } catch (err) { + this.logger.error('Notification configuration removal failed'); + this.logger.debug(err); + return { code: 0, result: err }; + } + } + + public getTypes() { + return Object.keys(NotificationType) + .filter((key) => isNaN(Number(key))) + .map((key) => ({ + title: this.humanizeTitle(key), + id: NotificationType[key], + })); + } + + // Helper function to convert enum keys to human-readable titles + private humanizeTitle(key: string): string { + return key + .replace(/_/g, ' ') + .toLowerCase() + .replace(/\b\w/g, (char) => char.toUpperCase()); + } + + public getAgentSpec() { + return [ + { + name: 'email', + options: [ + { field: 'emailFrom', type: 'text', required: true, extraInfo: '' }, + { field: 'emailTo', type: 'text', required: true, extraInfo: '' }, + { field: 'smtpHost', type: 'text', required: true, extraInfo: '' }, + { field: 'smtpPort', type: 'number', required: true, extraInfo: '' }, + { + field: 'secure', + type: 'checkbox', + required: false, + extraInfo: 'TLS: Use implicit TLS', + }, + { + field: 'ignoreTls', + type: 'checkbox', + required: false, + extraInfo: 'TLS: None', + }, + { + field: 'requireTls', + type: 'checkbox', + required: false, + extraInfo: 'TLS: Always use STARTLS', + }, + { + field: 'allowSelfSigned', + type: 'checkbox', + required: false, + extraInfo: '', + }, + { field: 'senderName', type: 'text', required: false, extraInfo: '' }, + { field: 'pgpKey', type: 'text', required: false, extraInfo: '' }, + { + field: 'pgpPassword', + type: 'password', + required: false, + extraInfo: '', + }, + ], + }, + { + name: 'discord', + options: [ + { field: 'webhookUrl', type: 'text', required: true, extraInfo: '' }, + { + field: 'botUsername', + type: 'text', + required: false, + extraInfo: '', + }, + { + field: 'botAvatarUrl', + type: 'text', + required: false, + extraInfo: '', + }, + ], + }, + { + name: 'lunasea', + options: [ + { field: 'webhookUrl', type: 'text', required: true, extraInfo: '' }, + { + field: 'profileName', + type: 'text', + required: false, + extraInfo: 'Only required if not using the default profile', + }, + ], + }, + { + name: 'slack', + options: [ + { field: 'webhookUrl', type: 'text', required: true, extraInfo: '' }, + ], + }, + { + name: 'telegram', + options: [ + { + field: 'botAuthToken', + type: 'text', + required: true, + extraInfo: '', + }, + { + field: 'botUsername', + type: 'text', + required: false, + extraInfo: + 'Allow users to also start a chat with your bot and configure their own notifications', + }, + { + field: 'chatId', + type: 'text', + required: true, + extraInfo: + 'Start a chat with your bot, add @get_id_bot, and issue the /my_id command', + }, + { + field: 'sendSilently', + type: 'checkbox', + required: false, + extraInfo: 'Send notifications with no sound', + }, + ], + }, + { + name: 'pushbullet', + options: [ + { field: 'accessToken', type: 'text', required: true, extraInfo: '' }, + { field: 'channelTag', type: 'text', required: false, extraInfo: '' }, + ], + }, + { + name: 'pushover', + options: [ + { field: 'accessToken', type: 'text', required: true, extraInfo: '' }, + { + field: 'userToken', + type: 'text', + required: true, + extraInfo: 'Your 30-character user or group identifier', + }, + { field: 'sound', type: 'text', required: false, extraInfo: '' }, + ], + }, + { + name: 'webhook', + options: [ + { field: 'webhookUrl', type: 'text', required: true, extraInfo: '' }, + { field: 'jsonPayload', type: 'text', required: true, extraInfo: '' }, + { field: 'authHeader', type: 'text', required: false, extraInfo: '' }, + ], + }, + { + name: 'gotify', + options: [ + { field: 'url', type: 'text', required: true, extraInfo: '' }, + { field: 'token', type: 'text', required: true, extraInfo: '' }, + ], + }, + ]; + } + + public async handleNotification( + type: NotificationType, + mediaItems: { plexId: number }[], + collectionName?: string, + dayAmount?: number, + agent?: NotificationAgent, + ) { + const payload: NotificationPayload = { + subject: '', + notifySystem: false, + message: '', + }; + + payload.message = await this.transformMessageContent( + this.getMessageContent(type, mediaItems && mediaItems.length > 1), + mediaItems, + collectionName, + dayAmount, + ); + + if (agent) { + this.sendNotificationToAgent(type, payload, agent); + } else { + this.sendNotification(type, payload); + } + } + + private getMessageContent(type: NotificationType, multiple: boolean): string { + let message: string; + + if (!multiple) { + switch (type) { + case NotificationType.TEST_NOTIFICATION: + message = + "\uD83D\uDD0D Test Notification: Just checking if this thing works... if you're seeing this, success! If not, well... we have a problem!"; + break; + case NotificationType.COLLECTION_HANDLING_FAILED: + message = + '⚠️ Collection Handling Failed: Oops! Something went wrong while processing your collections.'; + break; + case NotificationType.RULE_HANDLING_FAILED: + message = + '⚠️ Rule Handling Failed: Oops! Something went wrong while processing your rules.'; + break; + case NotificationType.MEDIA_ABOUT_TO_BE_HANDLED: + message = + "⏰ Reminder: {media_title} will be handled in {days} days. If you want to keep it, make sure to take action before it's gone. Don’t miss out!"; + break; + case NotificationType.MEDIA_ADDED_TO_COLLECTION: + message = + "\uD83D\uDCC2 '{media_title}' has been added to '{collection_name}'. The item will be handled in {days} days"; + break; + case NotificationType.MEDIA_REMOVED_FROM_COLLECTION: + message = + "\uD83D\uDCC2 '{media_title}' has been removed from '{collection_name}'. It won't be handled anymore."; + break; + case NotificationType.MEDIA_HANDLED: + message = + "✅ Media Handled. '{media_title}' has been handled by '{collection_name}'"; + break; + } + } else { + switch (type) { + case NotificationType.MEDIA_ABOUT_TO_BE_HANDLED: + message = + "⏰ Reminder: These media items will be handled in {days} days. If you want to keep them, make sure to take action before they're gone. Don’t miss out! \n \n {media_items}"; + break; + case NotificationType.MEDIA_ADDED_TO_COLLECTION: + message = + "\uD83D\uDCC2 These media items have been added to '{collection_name}'. The items will be handled in {days} days. \n \n {media_items}"; + break; + case NotificationType.MEDIA_REMOVED_FROM_COLLECTION: + message = + "\uD83D\uDCC2 These media items have been removed from '{collection_name}'. The items will not be handled anymore. \n \n {media_items}"; + break; + case NotificationType.MEDIA_HANDLED: + message = + "✅ Media Handled: These media items have been handled by '{collection_name}'. \n \n {media_items}"; + break; + } + } + return message; + } + + private async transformMessageContent( + message: string, + items?: { plexId: number }[], + collectionName?: string, + dayAmount?: number, + ): Promise { + try { + if (items) { + if (items.length > 1) { + // if multiple items + const titles = []; + + for (const i of items) { + const item = await this.plexApi.getMetadata(i.plexId.toString()); + + titles.push(this.getTitle(item)); + } + + const result = titles + .map((name) => `* ${name.charAt(0).toUpperCase() + name.slice(1)}`) + .join(' \n'); + + message = message.replace('{media_items}', result); + } else { + // if 1 item + const item = await this.plexApi.getMetadata( + items[0].plexId.toString(), + ); + + message = message.replace('{media_title}', this.getTitle(item)); + } + } + + message = collectionName + ? message.replace('{collection_name}', collectionName) + : message; + + message = + dayAmount && dayAmount > 0 + ? message.replace('{days}', dayAmount.toString()) + : message; + + return message; + } catch (e) { + this.logger.error("Couldn't transform notification message", e); + this.logger.debug(e); + } + } + + private getTitle(item: PlexMetadata): string { + return item.grandparentRatingKey + ? `${item.grandparentTitle} - season ${item.parentIndex} - episode ${item.index}` + : item.parentRatingKey + ? `${item.parentTitle} - season ${item.index}` + : item.title; + } +} diff --git a/server/src/modules/notifications/templates/email-template/html.pug b/server/src/modules/notifications/templates/email-template/html.pug new file mode 100644 index 00000000..0ad08091 --- /dev/null +++ b/server/src/modules/notifications/templates/email-template/html.pug @@ -0,0 +1,37 @@ +doctype html +head + meta(charset='utf-8') + meta(name='x-apple-disable-message-reformatting') + meta(http-equiv='x-ua-compatible' content='ie=edge') + meta(name='viewport' content='width=device-width, initial-scale=1') + meta(name='format-detection' content='telephone=no, date=no, address=no, email=no') + link(href='https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap' rel='stylesheet' media='screen') + style. + .title:hover * { + text-decoration: underline; + } + @media only screen and (max-width:600px) { + table { + font-size: 20px !important; + width: 100% !important; + } + } +div(style='display: block; background-color: #111827; padding: 2.5rem 0;') + table(style='margin: 0 auto; font-family: Inter, Arial, sans-serif; color: #fff; font-size: 16px; width: 26rem;') + tr + td(style="text-align: center;") + if applicationUrl + a(href=applicationUrl style='margin: 0 1rem;') + img(src=applicationUrl +'/logo_full.png' style='width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') + else + div(style='margin: 0 1rem 2.5rem; font-size: 3em; font-weight: 700;') + | #{applicationTitle} + if recipientName !== recipientEmail + tr + td(style='text-align: center;') + div(style='margin: 1rem 0 0; font-size: 1.25em;') + | Hi, #{recipientName.replace(/\.|@/g, ((x) => x + '\ufeff'))}! + tr + td(style='text-align: center;') + div(style='margin: 1rem 0 0; font-size: 1.25em;') + | #{body} \ No newline at end of file diff --git a/server/src/modules/notifications/templates/email-template/subject.pug b/server/src/modules/notifications/templates/email-template/subject.pug new file mode 100644 index 00000000..da2bbf89 --- /dev/null +++ b/server/src/modules/notifications/templates/email-template/subject.pug @@ -0,0 +1 @@ +!= `[${applicationTitle}] - ${event}` \ No newline at end of file diff --git a/server/src/modules/rules/dtos/rules.dto.ts b/server/src/modules/rules/dtos/rules.dto.ts index aa3bf7f8..e872fd26 100644 --- a/server/src/modules/rules/dtos/rules.dto.ts +++ b/server/src/modules/rules/dtos/rules.dto.ts @@ -2,6 +2,7 @@ import { ICollection } from '../../collections/interfaces/collection.interface'; import { RuleDto } from './rule.dto'; import { RuleDbDto } from './ruleDb.dto'; import { EPlexDataType } from '../../api/plex-api/enums/plex-data-type-enum'; +import { Notification } from '../../notifications/entities/notification.entities'; export class RulesDto { id?: number; @@ -19,6 +20,7 @@ export class RulesDto { manualCollectionName?: string; dataType: EPlexDataType; tautulliWatchedPercentOverride?: number; + notifications?: Notification[]; radarrSettingsId?: number; sonarrSettingsId?: number; } diff --git a/server/src/modules/rules/entities/rule-group.entities.ts b/server/src/modules/rules/entities/rule-group.entities.ts index 855f2c93..7a858c86 100644 --- a/server/src/modules/rules/entities/rule-group.entities.ts +++ b/server/src/modules/rules/entities/rule-group.entities.ts @@ -7,10 +7,13 @@ import { OneToMany, OneToOne, JoinColumn, + ManyToMany, + JoinTable, } from 'typeorm'; import { Rules } from './rules.entities'; +import { Notification } from '../../notifications/entities/notification.entities'; -@Entity() +@Entity('rule_group') export class RuleGroup { @PrimaryGeneratedColumn() id: number; @@ -41,6 +44,17 @@ export class RuleGroup { }) rules: Rules[]; + @ManyToMany(() => Notification, { + eager: true, + onDelete: 'NO ACTION', + }) + @JoinTable({ + name: 'notification_rulegroup', + joinColumn: { name: 'rulegroupId', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'notificationId', referencedColumnName: 'id' }, + }) + notifications: Notification[]; + @OneToOne(() => Collection, (c) => c.ruleGroup, { eager: true, onDelete: 'CASCADE', diff --git a/server/src/modules/rules/rules.module.ts b/server/src/modules/rules/rules.module.ts index 37ca9b7d..9f1db6cc 100644 --- a/server/src/modules/rules/rules.module.ts +++ b/server/src/modules/rules/rules.module.ts @@ -30,6 +30,9 @@ import { TautulliApiModule } from '../api/tautulli-api/tautulli-api.module'; import { TautulliGetterService } from './getter/tautulli-getter.service'; import { RadarrSettings } from '../settings/entities/radarr_settings.entities'; import { SonarrSettings } from '../settings/entities/sonarr_settings.entities'; +import { NotificationService } from '../notifications/notifications.service'; +import { NotificationsModule } from '../notifications/notifications.module'; +import { Notification } from '../notifications/entities/notification.entities'; @Module({ imports: [ @@ -45,12 +48,14 @@ import { SonarrSettings } from '../settings/entities/sonarr_settings.entities'; Settings, RadarrSettings, SonarrSettings, + Notification, ]), OverseerrApiModule, TautulliApiModule, TmdbApiModule, CollectionsModule, TasksModule, + NotificationsModule, ], providers: [ RulesService, @@ -66,6 +71,7 @@ import { SonarrSettings } from '../settings/entities/sonarr_settings.entities'; RuleYamlService, RuleComparatorService, RuleConstanstService, + NotificationService, ], controllers: [RulesController], }) diff --git a/server/src/modules/rules/rules.service.ts b/server/src/modules/rules/rules.service.ts index 6b8dc7bf..2016a564 100644 --- a/server/src/modules/rules/rules.service.ts +++ b/server/src/modules/rules/rules.service.ts @@ -32,6 +32,7 @@ import { ECollectionLogType } from '../collections/entities/collection_log.entit import cacheManager from '../api/lib/cache'; import { SonarrSettings } from '../settings/entities/sonarr_settings.entities'; import { RadarrSettings } from '../settings/entities/radarr_settings.entities'; +import { Notification } from '../notifications/entities/notification.entities'; export interface ReturnStatus { code: 0 | 1; @@ -52,8 +53,6 @@ export class RulesService { private readonly rulesRepository: Repository, @InjectRepository(RuleGroup) private readonly ruleGroupRepository: Repository, - @InjectRepository(Collection) - private readonly collectionRepository: Repository, @InjectRepository(CollectionMedia) private readonly collectionMediaRepository: Repository, @InjectRepository(CommunityRuleKarma) @@ -136,6 +135,7 @@ export class RulesService { .innerJoinAndSelect('rg.rules', 'r') .orderBy('r.id') .innerJoinAndSelect('rg.collection', 'c') + .leftJoinAndSelect('rg.notifications', 'n') .where( activeOnly ? 'rg.isActive = true' : 'rg.isActive in (true, false)', ) @@ -164,6 +164,7 @@ export class RulesService { try { return await this.ruleGroupRepository.findOne({ where: { id: ruleGroupId }, + relations: ['notifications'], }); } catch (e) { this.logger.warn(`Rules - Action failed : ${e.message}`); @@ -176,6 +177,7 @@ export class RulesService { try { return await this.ruleGroupRepository.findOne({ where: { collectionId: id }, + relations: ['notifications'], }); } catch (e) { this.logger.warn(`Rules - Action failed : ${e.message}`); @@ -279,6 +281,8 @@ export class RulesService { }, ]); } + + return state; } else { // empty rule if not using rules await this.rulesRepository.save([ @@ -289,6 +293,7 @@ export class RulesService { }, ]); } + return state; } catch (e) { this.logger.warn(`Rules - Action failed : ${e.message}`); @@ -389,6 +394,7 @@ export class RulesService { params.isActive !== undefined ? params.isActive : true, params.dataType !== undefined ? params.dataType : undefined, group.id, + params.notifications, ); // remove previous rules @@ -418,6 +424,7 @@ export class RulesService { }, ]); } + this.logger.log(`Successfully updated rulegroup '${params.name}'.`); return state; } else { @@ -765,6 +772,7 @@ export class RulesService { isActive = true, dataType = undefined, id?: number, + notifications?: Notification[], ): Promise { try { const values = { @@ -784,15 +792,34 @@ export class RulesService { .into(RuleGroup) .values(values) .execute(); - return groupId.identifiers[0].id; + + id = groupId.identifiers[0].id; } else { await connection .update(RuleGroup) .set(values) .where({ id: id }) .execute(); - return id; } + + // Remove all existing notifications from the RuleGroup + await connection + .relation(RuleGroup, 'notifications') + .of(id) + .remove( + await connection + .relation(RuleGroup, 'notifications') + .of(id) + .loadMany(), + ); + + // Associate new notifications to the RuleGroup + await connection + .relation(RuleGroup, 'notifications') + .of(id) + .add(notifications.map((notification) => notification.id)); + + return id; } catch (e) { this.logger.warn(`Rules - Action failed : ${e.message}`); this.logger.debug(e); diff --git a/server/src/modules/rules/tasks/rule-executor.service.ts b/server/src/modules/rules/tasks/rule-executor.service.ts index 228b3208..e418e000 100644 --- a/server/src/modules/rules/tasks/rule-executor.service.ts +++ b/server/src/modules/rules/tasks/rule-executor.service.ts @@ -15,6 +15,8 @@ import cacheManager from '../../api/lib/cache'; import { RuleComparatorService } from '../helpers/rule.comparator.service'; import { Collection } from '../../collections/entities/collection.entities'; import { TaskBase } from '../../tasks/task.base'; +import { NotificationService } from '../../notifications/notifications.service'; +import { NotificationType } from '../../notifications/notifications-interfaces'; interface PlexData { page: number; @@ -43,6 +45,7 @@ export class RuleExecutorService extends TaskBase { private readonly collectionService: CollectionsService, protected readonly taskService: TasksService, private readonly settings: SettingsService, + private readonly notificationService: NotificationService, private readonly comparator: RuleComparatorService, ) { super(taskService); @@ -63,6 +66,7 @@ export class RuleExecutorService extends TaskBase { return; } + this.notificationService.registerConfiguredAgents(true); // re-register notification agents, to avoid flukes await super.execute(); this.logger.log('Starting Execution of all active rules'); @@ -118,6 +122,10 @@ export class RuleExecutorService extends TaskBase { this.logger.log( 'Not all applications are reachable.. Skipped rule execution.', ); + await this.notificationService.handleNotification( + NotificationType.RULE_HANDLING_FAILED, + undefined, + ); } // clean up await this.finish(); @@ -264,6 +272,12 @@ export class RuleExecutorService extends TaskBase { : collection.title }'.`, ); + + await this.notificationService.handleNotification( + NotificationType.MEDIA_REMOVED_FROM_COLLECTION, + dataToRemove, + collection.title, + ); } if (dataToAdd.length > 0) { @@ -274,6 +288,13 @@ export class RuleExecutorService extends TaskBase { : collection.title }'.`, ); + + await this.notificationService.handleNotification( + NotificationType.MEDIA_ADDED_TO_COLLECTION, + dataToAdd, + collection.title, + collection.deleteAfterDays, + ); } collection = @@ -295,9 +316,17 @@ export class RuleExecutorService extends TaskBase { return collection; } else { this.logInfo(`collection not found with id ${rulegroup.collectionId}`); + await this.notificationService.handleNotification( + NotificationType.RULE_HANDLING_FAILED, + undefined, + ); } } catch (err) { - this.logger.warn(`Execption occurred whild handling rule: `, err); + this.logger.error(`Execption occurred while handling rule: `, err); + await this.notificationService.handleNotification( + NotificationType.RULE_HANDLING_FAILED, + undefined, + ); } } diff --git a/server/src/modules/settings/interfaces/notifications-settings.interface.ts b/server/src/modules/settings/interfaces/notifications-settings.interface.ts new file mode 100644 index 00000000..d0d914f6 --- /dev/null +++ b/server/src/modules/settings/interfaces/notifications-settings.interface.ts @@ -0,0 +1,43 @@ +export interface NotificationSettings { + notifications: { + agents: [ + { + name: string; + agent: string; + enabled: boolean; + types: number[]; + options: { + emailFrom?: string; + emailTo?: string; + smtpHost?: string; + smtpPort?: number; + secure?: boolean; + ignoreTls?: boolean; + requireTls?: boolean; + allowSelfSigned?: boolean; + senderName?: string; + pgpKey?: string; + pgpPassword?: string; + webhookUrl?: string; + botUsername?: string; + botAvatarUrl?: string; + displayName?: string; + profileName?: string; + email?: string; + avatar?: string; + botAPI?: string; + chatId?: string; + sendSilently?: boolean; + accessToken?: string; + channelTag?: string; + userToken?: string; + sound?: string; + jsonPayload?: string; + authHeader?: string; + url?: string; + token?: string; + }; + }, + ]; + }; +} diff --git a/server/src/modules/settings/settings.service.ts b/server/src/modules/settings/settings.service.ts index ca99bf52..58a46d8d 100644 --- a/server/src/modules/settings/settings.service.ts +++ b/server/src/modules/settings/settings.service.ts @@ -23,6 +23,8 @@ import { SonarrSettingRawDto, SonarrSettingResponseDto, } from "./dto's/sonarr-setting.dto"; +import { NotificationSettings } from './interfaces/notifications-settings.interface'; +import { NotificationService } from '../notifications/notifications.service'; @Injectable() export class SettingsService implements SettingDto { @@ -59,6 +61,8 @@ export class SettingsService implements SettingDto { tautulli_api_key: string; + notification_settings: NotificationSettings; + collection_handler_job_cron: string; rules_handler_job_cron: string; diff --git a/ui/src/components/Common/PaginatedList/index.tsx b/ui/src/components/Common/PaginatedList/index.tsx new file mode 100644 index 00000000..1192823d --- /dev/null +++ b/ui/src/components/Common/PaginatedList/index.tsx @@ -0,0 +1,94 @@ +import React, { useState } from 'react' + +interface ListItem { + id: number + title: string +} + +interface PaginatedListProps { + items: ListItem[] + onEdit: (id: number) => void + onAdd: () => void + addName?: string +} + +const PaginatedList: React.FC = ({ + items, + onEdit, + onAdd, + addName, +}) => { + const [currentPage, setCurrentPage] = useState(1) + const itemsPerPage = 5 + + const totalPages = Math.ceil(items.length / itemsPerPage) + + const currentItems = items.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage, + ) + + const handlePrevPage = () => { + if (currentPage > 1) setCurrentPage(currentPage - 1) + } + + const handleNextPage = () => { + if (currentPage < totalPages) setCurrentPage(currentPage + 1) + } + + return ( +
+
    + {currentItems.map((item) => ( +
  • +
    + {item.title} + +
    +
  • + ))} +
+ +
+ + + + Page {currentPage} of {totalPages} + + + +
+ +
+ +
+
+ ) +} + +export default PaginatedList diff --git a/ui/src/components/Common/ToggleButton/index.tsx b/ui/src/components/Common/ToggleButton/index.tsx new file mode 100644 index 00000000..3c95fe16 --- /dev/null +++ b/ui/src/components/Common/ToggleButton/index.tsx @@ -0,0 +1,45 @@ +// components/ToggleItem.tsx +import React, { useEffect, useState } from 'react' + +interface ToggleItemProps { + label: string + toggled?: boolean + onStateChange: (state: boolean) => void +} + +const ToggleItem: React.FC = ({ + label, + toggled, + onStateChange, +}) => { + const [isToggled, setIsToggled] = useState(false) + + useEffect(() => { + if (toggled !== undefined) { + setIsToggled(toggled) + } + }, []) + + const handleToggle = () => { + onStateChange(!isToggled) + setIsToggled(!isToggled) + } + + return ( +
+
+ + + {label} +
+ {/* Edit */} +
+ ) +} + +export default ToggleItem diff --git a/ui/src/components/Rules/RuleGroup/AddModal/ConfigureNotificationModal/index.tsx b/ui/src/components/Rules/RuleGroup/AddModal/ConfigureNotificationModal/index.tsx new file mode 100644 index 00000000..81be5f65 --- /dev/null +++ b/ui/src/components/Rules/RuleGroup/AddModal/ConfigureNotificationModal/index.tsx @@ -0,0 +1,85 @@ +import { useEffect, useState } from 'react' +import GetApiHandler from '../../../../../utils/ApiHandler' +import Modal from '../../../../Common/Modal' +import { AgentConfiguration } from '../../../../Settings/Notifications/CreateNotificationModal' +import ToggleItem from '../../../../Common/ToggleButton' + +interface ConfigureNotificationModal { + onCancel: () => void + onSuccess: (selectedConfigurations: AgentConfiguration[]) => void + selectedAgents?: AgentConfiguration[] +} +const ConfigureNotificationModal = (props: ConfigureNotificationModal) => { + const [notifications, setNotifications] = useState() + const [activatedNotifications, setActivatedNotifications] = useState< + AgentConfiguration[] + >([]) + const [isLoading, setIsloading] = useState(true) + + useEffect(() => { + GetApiHandler('/notifications/configurations').then( + (notificationConfigs) => { + setNotifications(notificationConfigs) + if (props.selectedAgents) { + setActivatedNotifications(props.selectedAgents) + } + setIsloading(false) + }, + ) + }, []) + + return ( + props.onCancel()} + okDisabled={false} + onOk={() => props.onSuccess(activatedNotifications)} + okText={'OK'} + okButtonType={'primary'} + title={'Notification Agents'} + iconSvg={''} + > +
+
+ {/* Config Name */} +
+ +
+
+ {!isLoading + ? notifications!.map((n) => ( + an.id === n.id) + ? true + : false + } + onStateChange={(state) => { + state + ? setActivatedNotifications([ + ...activatedNotifications, + n, + ]) + : setActivatedNotifications([ + ...activatedNotifications.filter( + (el) => el.id !== n.id, + ), + ]) + }} + /> + )) + : undefined} +
+
+
+
+
+
+ ) +} +export default ConfigureNotificationModal diff --git a/ui/src/components/Rules/RuleGroup/AddModal/index.tsx b/ui/src/components/Rules/RuleGroup/AddModal/index.tsx index a1ad7439..91b86a55 100644 --- a/ui/src/components/Rules/RuleGroup/AddModal/index.tsx +++ b/ui/src/components/Rules/RuleGroup/AddModal/index.tsx @@ -30,6 +30,9 @@ import CachedImage from '../../../Common/CachedImage' import YamlImporterModal from '../../../Common/YamlImporterModal' import { CloudDownloadIcon } from '@heroicons/react/outline' import { useToasts } from 'react-toast-notifications' +import ConfigureNotificationModal from './ConfigureNotificationModal' +import { AgentConfiguration } from '../../../Settings/Notifications/CreateNotificationModal' +import Modal from '../../../Common/Modal' interface AddModal { editData?: IRuleGroup @@ -59,6 +62,7 @@ interface ICreateApiObject { } rules: IRule[] dataType: EPlexDataType + notifications: AgentConfiguration[] } const AddModal = (props: AddModal) => { @@ -73,6 +77,8 @@ const AddModal = (props: AddModal) => { const [isLoading, setIsLoading] = useState(true) const [CommunityModal, setCommunityModal] = useState(false) const [yamlImporterModal, setYamlImporterModal] = useState(false) + const [configureNotificionModal, setConfigureNotificationModal] = + useState(false) const yaml = useRef() const nameRef = useRef() @@ -89,6 +95,12 @@ const AddModal = (props: AddModal) => { const [forceOverseerr, setForceOverseerr] = useState(false) const [manualCollection, setManualCollection] = useState(false) const ConstantsCtx = useContext(ConstantsContext) + const [ + configuredNotificationConfigurations, + setConfiguredNotificationConfigurations, + ] = useState( + props.editData?.notifications ? props.editData?.notifications : [], + ) const { addToast } = useToasts() @@ -351,6 +363,7 @@ const AddModal = (props: AddModal) => { keepLogsForMonths: +keepLogsForMonthsRef.current.value, }, rules: useRules ? rules : [], + notifications: configuredNotificationConfigurations, } if (!props.editData) { @@ -871,6 +884,29 @@ const AddModal = (props: AddModal) => { +
+ +
+
+ +
+
+
+
@@ -941,6 +977,20 @@ const AddModal = (props: AddModal) => { }} /> ) : undefined} + + {configureNotificionModal ? ( + { + setConfiguredNotificationConfigurations(selection) + setConfigureNotificationModal(false) + }} + onCancel={() => { + setConfigureNotificationModal(false) + }} + selectedAgents={configuredNotificationConfigurations} + /> + ) : undefined} + void + onTest: () => void + onCancel: () => void +} + +const CreateNotificationModal = (props: CreateNotificationModal) => { + const [availableAgents, setAvailableAgents] = useState() + const [availableTypes, setAvailableTypes] = useState() + const nameRef = useRef('') + const aboutScaleRef = useRef(3) + const enabledRef = useRef(false) + const [formValues, setFormValues] = useState() + + const [targetAgent, setTargetAgent] = useState() + const [targetTypes, setTargetTypes] = useState([]) + + const handleSubmit = () => { + const types = targetTypes ? targetTypes.map((t) => t.id) : [] + + if (targetAgent && nameRef.current !== null) { + const payload: AgentConfiguration = { + id: props.selected?.id, + name: nameRef.current, + agent: targetAgent.name, + enabled: enabledRef.current, + types: types, + aboutScale: aboutScaleRef.current, + options: formValues, + } + postNotificationConfig(payload) + } else { + props.onSave(false) + } + } + + useEffect(() => { + GetApiHandler('/notifications/agents').then((agents) => { + setAvailableAgents([{ name: 'none', options: [] }, ...agents]) + + // load selected agents if editing + if (props.selected && props.selected.agent) { + setTargetAgent( + agents.find( + (agent: agentSpec) => props.selected!.agent === agent.name, + ), + ) + } + }) + + GetApiHandler('/notifications/types').then((types: typeSpec[]) => { + setAvailableTypes(types) + + // load selected types if editing + if (props.selected && props.selected.types) { + setTargetTypes( + types.filter((type) => props.selected!.types.includes(type.id)), + ) + } + }) + + // load rest of data if editing + if (props.selected) { + nameRef.current = props.selected.name + enabledRef.current = props.selected.enabled + setFormValues(JSON.parse(props.selected.options as string)) + } + }, []) + + const postNotificationConfig = (payload: AgentConfiguration) => { + PostApiHandler('/notifications/configuration/add', payload).then( + (status) => { + props.onSave(status) + }, + ) + } + + const handleInputChange = (fieldName: string, value: any) => { + setFormValues((prevValues: any) => ({ + ...prevValues, + [fieldName]: value, + })) + } + + if (!availableAgents || !availableTypes) { + return ( + + + + ) + } else { + return ( + props.onCancel()} + okDisabled={false} + okText="Save" + okButtonType={'primary'} + title={'New Notification'} + iconSvg={''} + onOk={handleSubmit} + > +
+
+ {/* Config Name */} +
+ +
+
+ ) => + (nameRef.current = event.target.value) + } + > +
+
+
+ {/* Enabled */} +
+ +
+
+ ) => + event.target.value === 'on' + ? (enabledRef.current = true) + : (enabledRef.current = false) + } + > +
+
+
+ {/* Select agent */} +
+ +
+
+ +
+
+
+ +
+ +
+
+ ) => + (aboutScaleRef.current = +event.target.value) + } + > +
+
+
+ +
+ {/* Load fields */} + {targetAgent?.options.map((option) => { + return ( +
+ +
+
+ + handleInputChange( + option.field, + e.target.value || e.target.checked, + ) + } + > +
+
+
+ ) + })} + + {/* Select types */} +
+ +
+ {availableTypes.map((n) => ( + { + state + ? setTargetTypes([...targetTypes, n]) + : setTargetTypes([ + ...targetTypes.filter((el) => el.id !== n.id), + ]) + }} + /> + ))} +
+
+
+
+
+
+ ) + } +} +export default CreateNotificationModal diff --git a/ui/src/components/Settings/Notifications/index.tsx b/ui/src/components/Settings/Notifications/index.tsx new file mode 100644 index 00000000..eb18f041 --- /dev/null +++ b/ui/src/components/Settings/Notifications/index.tsx @@ -0,0 +1,199 @@ +import { useContext, useEffect, useState } from 'react' +import SettingsContext from '../../../contexts/settings-context' +import React from 'react' +import CreateNotificationModal, { + AgentConfiguration, +} from './CreateNotificationModal' +import GetApiHandler, { + DeleteApiHandler, + PostApiHandler, +} from '../../../utils/ApiHandler' +import Button from '../../Common/Button' +import { + DocumentAddIcon, + PlusCircleIcon, + TrashIcon, +} from '@heroicons/react/solid' +import ExecuteButton from '../../Common/ExecuteButton' +import { debounce } from 'lodash' +import { useToasts } from 'react-toast-notifications' + +const NotificationSettings = () => { + const settingsCtx = useContext(SettingsContext) + const [addModalActive, setAddModalActive] = useState(false) + const [configurations, setConfigurations] = useState() + const [editConfig, setEditConfig] = useState() + const { addToast } = useToasts() + + useEffect(() => { + document.title = 'Maintainerr - Settings - Notifications' + GetApiHandler('/notifications/configurations').then((configs) => + setConfigurations(configs), + ) + }, []) + + useEffect(() => { + GetApiHandler('/notifications/configurations').then((configs) => + setConfigurations(configs), + ) + }, [addModalActive]) + + const doEdit = (id: number) => { + const config = configurations?.find((c) => c.id === id) + + setEditConfig(config) + setAddModalActive(!addModalActive) + } + + function confirmedDelete(id: any) { + DeleteApiHandler(`/notifications/configuration/${id}`).then(() => { + setConfigurations(configurations?.filter((c) => c.id !== id)) + }) + } + + const doTest = () => { + PostApiHandler(`/notifications/test`, {}).then(() => { + addToast( + "Test notification deployed to all agents with the 'Test' type", + { + autoDismiss: true, + appearance: 'success', + }, + ) + }) + } + + return ( +
+
+

Notification Settings

+

Notification configuration

+
+
+
+ +
+
+ +
+
    + {configurations?.map((config) => ( +
  • +
    +
    + {config.name} +
    + {!config.enabled && ( +
    + Disabled +
    + )} +
    + +

    + Agent + + {config.agent} + +

    +
    + + confirmedDelete(config.id)} + /> +
    +
  • + ))} + +
  • + +
  • +
+
+ + {addModalActive ? ( + { + setAddModalActive(!addModalActive) + setEditConfig(undefined) + }} + onSave={() => { + setAddModalActive(!addModalActive) + setEditConfig(undefined) + }} + onTest={() => {}} + {...(editConfig + ? { + selected: { + id: editConfig.id!, + name: editConfig.name!, + enabled: editConfig.enabled!, + agent: editConfig.agent!, + types: editConfig.types!, + options: editConfig.options!, + aboutScale: editConfig.aboutScale!, + }, + } + : {})} + /> + ) : null} +
+ ) +} + +const DeleteButton = ({ + onDeleteRequested, +}: { + onDeleteRequested: () => void +}) => { + const [showSureDelete, setShowSureDelete] = useState(false) + + return ( + + ) +} + +export default NotificationSettings diff --git a/ui/src/components/Settings/index.tsx b/ui/src/components/Settings/index.tsx index db8a2361..2c6bed6c 100644 --- a/ui/src/components/Settings/index.tsx +++ b/ui/src/components/Settings/index.tsx @@ -41,6 +41,11 @@ const SettingsWrapper: React.FC<{ children?: ReactNode }> = (props: { route: '/settings/tautulli', regex: /^\/settings(\/tautulli)?$/, }, + { + text: 'Notifications', + route: '/settings/notifications', + regex: /^\/settings(\/notifications)?$/, + }, { text: 'Jobs', route: '/settings/jobs', diff --git a/ui/src/pages/settings/notifications/index.tsx b/ui/src/pages/settings/notifications/index.tsx new file mode 100644 index 00000000..8d8c7843 --- /dev/null +++ b/ui/src/pages/settings/notifications/index.tsx @@ -0,0 +1,13 @@ +import { NextPage } from 'next' +import SettingsWrapper from '../../../components/Settings' +import NotificationSettings from '../../../components/Settings/Notifications' + +const SettingsOverseerr: NextPage = () => { + return ( + + + + ) +} + +export default SettingsOverseerr diff --git a/ui/src/utils/SettingsUtils.ts b/ui/src/utils/SettingsUtils.ts index 1fb42e97..75cfbcc3 100644 --- a/ui/src/utils/SettingsUtils.ts +++ b/ui/src/utils/SettingsUtils.ts @@ -64,6 +64,13 @@ export function getBaseUrl(url: string): string | undefined { } } +export function camelCaseToPrettyText(camelCaseStr: string): string { + return camelCaseStr + .replace(/([a-z])([A-Z])/g, '$1 $2') + .replace(/^./, (char) => char.toUpperCase()) + .trim() +} + export const handleSettingsInputChange = ( event: React.ChangeEvent, ref: RefObject, diff --git a/ui/styles/globals.css b/ui/styles/globals.css index 8a41d7e8..2adc7fa4 100644 --- a/ui/styles/globals.css +++ b/ui/styles/globals.css @@ -337,6 +337,9 @@ @apply mb-10 mt-6 text-white; } + .section-settings { + @apply mb-5 mt-6 text-white; + } .form-row { @apply mt-6 max-w-6xl sm:mt-5 sm:grid sm:grid-cols-3 sm:items-start sm:gap-4; } diff --git a/ui/tailwind.config.js b/ui/tailwind.config.js index 46ed576b..9e55067e 100644 --- a/ui/tailwind.config.js +++ b/ui/tailwind.config.js @@ -16,6 +16,9 @@ module.exports = { screens: { xs: '440px', }, + colors: { + 'bg-zinc-910': '#18181b', + }, typography: (theme) => ({ DEFAULT: { css: { diff --git a/yarn.lock b/yarn.lock index 597c60ff..93dd4726 100644 --- a/yarn.lock +++ b/yarn.lock @@ -274,6 +274,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-string-parser@npm:7.25.9" + checksum: 10c0/7244b45d8e65f6b4338a6a68a8556f2cb161b782343e97281a5f2b9b93e420cad0d9f5773a59d79f61d0c448913d06f6a2358a87f2e203cf112e3c5b53522ee6 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-validator-identifier@npm:7.22.20" @@ -281,6 +288,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-validator-identifier@npm:7.25.9" + checksum: 10c0/4fc6f830177b7b7e887ad3277ddb3b91d81e6c4a24151540d9d1023e8dc6b1c0505f0f0628ae653601eb4388a8db45c1c14b2c07a9173837aef7e4116456259d + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.23.5": version: 7.23.5 resolution: "@babel/helper-validator-option@npm:7.23.5" @@ -319,6 +333,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.6.0, @babel/parser@npm:^7.9.6": + version: 7.26.5 + resolution: "@babel/parser@npm:7.26.5" + dependencies: + "@babel/types": "npm:^7.26.5" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/2e77dd99ee028ee3c10fa03517ae1169f2432751adf71315e4dc0d90b61639d51760d622f418f6ac665ae4ea65f8485232a112ea0e76f18e5900225d3d19a61e + languageName: node + linkType: hard + "@babel/plugin-syntax-async-generators@npm:^7.8.4": version: 7.8.4 resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" @@ -522,6 +547,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.26.5, @babel/types@npm:^7.6.1, @babel/types@npm:^7.9.6": + version: 7.26.5 + resolution: "@babel/types@npm:7.26.5" + dependencies: + "@babel/helper-string-parser": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + checksum: 10c0/0278053b69d7c2b8573aa36dc5242cad95f0d965e1c0ed21ccacac6330092e59ba5949753448f6d6eccf6ad59baaef270295cc05218352e060ea8c68388638c4 + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -902,6 +937,22 @@ __metadata: languageName: node linkType: hard +"@hapi/boom@npm:^9.1.4": + version: 9.1.4 + resolution: "@hapi/boom@npm:9.1.4" + dependencies: + "@hapi/hoek": "npm:9.x.x" + checksum: 10c0/49bb99443e7bdbbdc87ee8de97cd64351e173b57d7c59061c69972d2de77fb98f2f440c1be42b37e1ac04f57c60fbb79f8fd3e9e5695360add560c37e2d584db + languageName: node + linkType: hard + +"@hapi/hoek@npm:9.x.x": + version: 9.3.0 + resolution: "@hapi/hoek@npm:9.3.0" + checksum: 10c0/a096063805051fb8bba4c947e293c664b05a32b47e13bc654c0dd43813a1cec993bdd8f29ceb838020299e1d0f89f68dc0d62a603c13c9cc8541963f0beca055 + languageName: node + linkType: hard + "@heroicons/react@npm:^1.0.6": version: 1.0.6 resolution: "@heroicons/react@npm:1.0.6" @@ -1457,6 +1508,36 @@ __metadata: languageName: node linkType: hard +"@ladjs/country-language@npm:^0.2.1": + version: 0.2.1 + resolution: "@ladjs/country-language@npm:0.2.1" + dependencies: + underscore: "npm:~1.13.1" + underscore.deep: "npm:~0.5.1" + checksum: 10c0/fc7532573d543d232ae1ca1f2fad39dccc985a49a276825a3af62206368c4990651d5f032cbcc8365e21104ce3d4e984c1badb43a514628bd4a89c4f5c07dc2a + languageName: node + linkType: hard + +"@ladjs/i18n@npm:^7.2.6": + version: 7.2.6 + resolution: "@ladjs/i18n@npm:7.2.6" + dependencies: + "@hapi/boom": "npm:^9.1.4" + "@ladjs/country-language": "npm:^0.2.1" + boolean: "npm:3.2.0" + debug: "npm:^4.3.3" + i18n: "npm:^0.14.1" + i18n-locales: "npm:^0.0.5" + lodash: "npm:^4.17.21" + multimatch: "npm:5" + punycode: "npm:^2.1.1" + qs: "npm:^6.10.3" + titleize: "npm:2" + tlds: "npm:^1.230.0" + checksum: 10c0/71c95a06ef7f32d6a9dea17df9a2714f9b30ecf1fc26545e754f7b121bc99670ea9af1913cd7066066edfbaf89bb4e5bb06371c9cd21611228adbcf52e47531f + languageName: node + linkType: hard + "@ljharb/through@npm:^2.3.12": version: 2.3.13 resolution: "@ljharb/through@npm:2.3.13" @@ -1489,6 +1570,7 @@ __metadata: "@nestjs/schematics": "npm:^10.2.3" "@nestjs/testing": "npm:^10.4.15" "@nestjs/typeorm": "npm:^11.0.0" + "@types/email-templates": "npm:^10.0.4" "@types/jest": "npm:^29.5.14" "@types/lodash": "npm:^4.17.14" "@types/node": "npm:^22" @@ -1498,12 +1580,15 @@ __metadata: axios: "npm:^1.7.9" chalk: "npm:^4.1.2" cron-validator: "npm:^1.3.1" + email-templates: "npm:9.0.0" eslint: "npm:^9.18.0" eslint-config-prettier: "npm:^10.0.1" jest: "npm:^29.7.0" lodash: "npm:^4.17.21" nest-winston: "npm:^1.10.0" node-cache: "npm:^5.1.2" + nodemailer: "npm:6.9.1" + openpgp: "npm:^5.7.0" plex-api: "npm:^5.3.2" prettier: "npm:^3.4.2" reflect-metadata: "npm:^0.2.2" @@ -1565,6 +1650,52 @@ __metadata: languageName: unknown linkType: soft +"@messageformat/core@npm:^3.0.0": + version: 3.4.0 + resolution: "@messageformat/core@npm:3.4.0" + dependencies: + "@messageformat/date-skeleton": "npm:^1.0.0" + "@messageformat/number-skeleton": "npm:^1.0.0" + "@messageformat/parser": "npm:^5.1.0" + "@messageformat/runtime": "npm:^3.0.1" + make-plural: "npm:^7.0.0" + safe-identifier: "npm:^0.4.1" + checksum: 10c0/8f5591c3e72b7771b036452465ee60c912c1cea6adb3d0f55e7a4be8737c02e1d1cb8620bb894dbc9dfcf28f77faff378c3c159e177a0cbe500b1ee74c41f70f + languageName: node + linkType: hard + +"@messageformat/date-skeleton@npm:^1.0.0": + version: 1.1.0 + resolution: "@messageformat/date-skeleton@npm:1.1.0" + checksum: 10c0/5dffb194a3f067be427123de0a567433e1e9219afe2e8c20e78afaf61cb24de479b8e52c5dfb94200d9757c3504e4d52c1cb3e13ff3c837fa7c71d0cc01980c3 + languageName: node + linkType: hard + +"@messageformat/number-skeleton@npm:^1.0.0": + version: 1.2.0 + resolution: "@messageformat/number-skeleton@npm:1.2.0" + checksum: 10c0/a1ad6b6eed4508996121b7f0283bca3e18e755240eefb30c0dfa58f70de0494d390397ffa105689fbb314e06af6365b4a9e9ab1e8902d11fefe673a4e024642e + languageName: node + linkType: hard + +"@messageformat/parser@npm:^5.1.0": + version: 5.1.1 + resolution: "@messageformat/parser@npm:5.1.1" + dependencies: + moo: "npm:^0.5.1" + checksum: 10c0/fc24596d85a20fa9959a0a1c34f9bfd341e4f6eb235d0648e54296e0bed2ebe91d067c7b91036aea8cc2e4af4ddb0722257b0c04f3c610cd9c7ef4f7659d8668 + languageName: node + linkType: hard + +"@messageformat/runtime@npm:^3.0.1": + version: 3.0.1 + resolution: "@messageformat/runtime@npm:3.0.1" + dependencies: + make-plural: "npm:^7.0.0" + checksum: 10c0/a236279da66a138f7860d4a190070b00d752cc5b5aed877ec3b0f6dce708e7e3bd5b290e4f4ec30b97a4213f069fb70dbda9af49f849a106da227d41a9e19581 + languageName: node + linkType: hard + "@monaco-editor/loader@npm:^1.4.0": version: 1.4.0 resolution: "@monaco-editor/loader@npm:1.4.0" @@ -2296,6 +2427,26 @@ __metadata: languageName: node linkType: hard +"@selderee/plugin-htmlparser2@npm:^0.11.0": + version: 0.11.0 + resolution: "@selderee/plugin-htmlparser2@npm:0.11.0" + dependencies: + domhandler: "npm:^5.0.3" + selderee: "npm:^0.11.0" + checksum: 10c0/e938ba9aeb31a9cf30dcb2977ef41685c598bf744bedc88c57aa9e8b7e71b51781695cf99c08aac50773fd7714eba670bd2a079e46db0788abe40c6d220084eb + languageName: node + linkType: hard + +"@selderee/plugin-htmlparser2@npm:^0.6.0": + version: 0.6.0 + resolution: "@selderee/plugin-htmlparser2@npm:0.6.0" + dependencies: + domhandler: "npm:^4.2.0" + selderee: "npm:^0.6.0" + checksum: 10c0/aba39ff7f574eb2097b340597d6bfd7473b43f84d56a78fcabc6f414c8d2aa9e356a306d863b8f3e3d5577512374735c9c5e3535c195764b07bfc0b13bf5e1a4 + languageName: node + linkType: hard + "@semantic-release/changelog@npm:^6.0.3": version: 6.0.3 resolution: "@semantic-release/changelog@npm:6.0.3" @@ -2744,6 +2895,17 @@ __metadata: languageName: node linkType: hard +"@types/email-templates@npm:^10.0.4": + version: 10.0.4 + resolution: "@types/email-templates@npm:10.0.4" + dependencies: + "@types/html-to-text": "npm:*" + "@types/nodemailer": "npm:*" + juice: "npm:^8.0.0" + checksum: 10c0/2efdec62fd4349b714010c2856f7d9e10977fb13e2fae27a3a802bc321152833bf060759aafb7c17f0ff366f063e49ff24d2cf28dd5b7ae5f8116e4da6a482bc + languageName: node + linkType: hard + "@types/eslint-scope@npm:^3.7.7": version: 3.7.7 resolution: "@types/eslint-scope@npm:3.7.7" @@ -2798,6 +2960,13 @@ __metadata: languageName: node linkType: hard +"@types/html-to-text@npm:*": + version: 9.0.4 + resolution: "@types/html-to-text@npm:9.0.4" + checksum: 10c0/8e0e548f280d1c1107f70db01ff6331a295f3097f535f4d07c30ced271818fc3803a8f03bb2117c1207c46a3d40da256a03a17bd05fc9f5f1b4821c5bcbf98d8 + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" @@ -2870,6 +3039,13 @@ __metadata: languageName: node linkType: hard +"@types/minimatch@npm:^3.0.3": + version: 3.0.5 + resolution: "@types/minimatch@npm:3.0.5" + checksum: 10c0/a1a19ba342d6f39b569510f621ae4bbe972dc9378d15e9a5e47904c440ee60744f5b09225bc73be1c6490e3a9c938eee69eb53debf55ce1f15761201aa965f97 + languageName: node + linkType: hard + "@types/ms@npm:*": version: 0.7.34 resolution: "@types/ms@npm:0.7.34" @@ -2878,11 +3054,11 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 22.9.4 - resolution: "@types/node@npm:22.9.4" + version: 20.11.5 + resolution: "@types/node@npm:20.11.5" dependencies: - undici-types: "npm:~6.19.8" - checksum: 10c0/85521424033d32c2cb2279f1a2dfe9a3630f253a4c877352607eece2b5fe45ddd80acc608dfcef9ae9c2b385203332e53cc1b6cb15c958504b26011ddcf65d4f + undici-types: "npm:~5.26.4" + checksum: 10c0/6d18cec852f5cfbed3ec42b5c01c026e7a3f9da540d6e3d6738d4cee9979fb308cf27b6df7ba40a6553e7bc82e678f0ef53ba6e6ad52e5b86bd97b7783c2a42c languageName: node linkType: hard @@ -2904,6 +3080,15 @@ __metadata: languageName: node linkType: hard +"@types/nodemailer@npm:*": + version: 6.4.17 + resolution: "@types/nodemailer@npm:6.4.17" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/689abb3005cf36cf89c2abe56f0aa4469a37e0814633a73fbeb35732e856f4b0d7ab32b6d91585038b6941f5b70db58ec2bd147ebe9f73e528eb6c99604f4e82 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.3": version: 2.4.4 resolution: "@types/normalize-package-data@npm:2.4.4" @@ -2935,11 +3120,11 @@ __metadata: linkType: hard "@types/react-transition-group@npm:^4.4.0": - version: 4.4.11 - resolution: "@types/react-transition-group@npm:4.4.11" + version: 4.4.10 + resolution: "@types/react-transition-group@npm:4.4.10" dependencies: "@types/react": "npm:*" - checksum: 10c0/8fbf0dcc1b81985cdcebe3c59d769fe2ea3f4525f12c3a10a7429a59f93e303c82b2abb744d21cb762879f4514969d70a7ab11b9bf486f92213e8fe70e04098d + checksum: 10c0/3eb9bca143abc21eb781aa5cb1bded0c9335689d515bf0513fb8e63217b7a8122c6a323ecd5644a06938727e1f467ee061d8df1c93b68825a80ff1b47ab777a2 languageName: node linkType: hard @@ -3023,14 +3208,14 @@ __metadata: linkType: hard "@typescript-eslint/eslint-plugin@npm:^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": - version: 8.16.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.16.0" + version: 8.7.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.7.0" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.16.0" - "@typescript-eslint/type-utils": "npm:8.16.0" - "@typescript-eslint/utils": "npm:8.16.0" - "@typescript-eslint/visitor-keys": "npm:8.16.0" + "@typescript-eslint/scope-manager": "npm:8.7.0" + "@typescript-eslint/type-utils": "npm:8.7.0" + "@typescript-eslint/utils": "npm:8.7.0" + "@typescript-eslint/visitor-keys": "npm:8.7.0" graphemer: "npm:^1.4.0" ignore: "npm:^5.3.1" natural-compare: "npm:^1.4.0" @@ -3041,7 +3226,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/b03612b726ee5aff631cd50e05ceeb06a522e64465e4efdc134e3a27a09406b959ef7a05ec4acef1956b3674dc4fedb6d3a62ce69382f9e30c227bd4093003e5 + checksum: 10c0/f04d6fa6a30e32d51feba0f08789f75ca77b6b67cfe494bdbd9aafa241871edc918fa8b344dc9d13dd59ae055d42c3920f0e542534f929afbfdca653dae598fa languageName: node linkType: hard @@ -3067,20 +3252,20 @@ __metadata: linkType: hard "@typescript-eslint/parser@npm:^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": - version: 8.16.0 - resolution: "@typescript-eslint/parser@npm:8.16.0" + version: 8.7.0 + resolution: "@typescript-eslint/parser@npm:8.7.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.16.0" - "@typescript-eslint/types": "npm:8.16.0" - "@typescript-eslint/typescript-estree": "npm:8.16.0" - "@typescript-eslint/visitor-keys": "npm:8.16.0" + "@typescript-eslint/scope-manager": "npm:8.7.0" + "@typescript-eslint/types": "npm:8.7.0" + "@typescript-eslint/typescript-estree": "npm:8.7.0" + "@typescript-eslint/visitor-keys": "npm:8.7.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.57.0 || ^9.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/e49c6640a7a863a16baecfbc5b99392a4731e9c7e9c9aaae4efbc354e305485fe0f39a28bf0acfae85bc01ce37fe0cc140fd315fdaca8b18f9b5e0addff8ceae + checksum: 10c0/1d5020ff1f5d3eb726bc6034d23f0a71e8fe7a713756479a0a0b639215326f71c0b44e2c25cc290b4e7c144bd3c958f1405199711c41601f0ea9174068714a64 languageName: node linkType: hard @@ -3100,16 +3285,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.16.0": - version: 8.16.0 - resolution: "@typescript-eslint/scope-manager@npm:8.16.0" - dependencies: - "@typescript-eslint/types": "npm:8.16.0" - "@typescript-eslint/visitor-keys": "npm:8.16.0" - checksum: 10c0/23b7c738b83f381c6419a36e6ca951944187e3e00abb8e012bce8041880410fe498303e28bdeb0e619023a69b14cf32a5ec1f9427c5382807788cd8e52a46a6e - languageName: node - linkType: hard - "@typescript-eslint/scope-manager@npm:8.20.0": version: 8.20.0 resolution: "@typescript-eslint/scope-manager@npm:8.20.0" @@ -3120,20 +3295,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.16.0": - version: 8.16.0 - resolution: "@typescript-eslint/type-utils@npm:8.16.0" +"@typescript-eslint/scope-manager@npm:8.7.0": + version: 8.7.0 + resolution: "@typescript-eslint/scope-manager@npm:8.7.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:8.16.0" - "@typescript-eslint/utils": "npm:8.16.0" - debug: "npm:^4.3.4" - ts-api-utils: "npm:^1.3.0" - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 10c0/24c0e815c8bdf99bf488c7528bd6a7c790e8b3b674cb7fb075663afc2ee26b48e6f4cf7c0d14bb21e2376ca62bd8525cbcb5688f36135b00b62b1d353d7235b9 + "@typescript-eslint/types": "npm:8.7.0" + "@typescript-eslint/visitor-keys": "npm:8.7.0" + checksum: 10c0/8b731a0d0bd3e8f6a322b3b25006f56879b5d2aad86625070fa438b803cf938cb8d5c597758bfa0d65d6e142b204dc6f363fa239bc44280a74e25aa427408eda languageName: node linkType: hard @@ -3152,10 +3320,18 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.16.0": - version: 8.16.0 - resolution: "@typescript-eslint/types@npm:8.16.0" - checksum: 10c0/141e257ab4060a9c0e2e14334ca14ab6be713659bfa38acd13be70a699fb5f36932a2584376b063063ab3d723b24bc703dbfb1ce57d61d7cfd7ec5bd8a975129 +"@typescript-eslint/type-utils@npm:8.7.0": + version: 8.7.0 + resolution: "@typescript-eslint/type-utils@npm:8.7.0" + dependencies: + "@typescript-eslint/typescript-estree": "npm:8.7.0" + "@typescript-eslint/utils": "npm:8.7.0" + debug: "npm:^4.3.4" + ts-api-utils: "npm:^1.3.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/2bd9fb93a50ff1c060af41528e39c775ae93b09dd71450defdb42a13c68990dd388460ae4e81fb2f4a49c38dc12152c515d43e845eca6198c44b14aab66733bc languageName: node linkType: hard @@ -3166,22 +3342,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.16.0": - version: 8.16.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.16.0" - dependencies: - "@typescript-eslint/types": "npm:8.16.0" - "@typescript-eslint/visitor-keys": "npm:8.16.0" - debug: "npm:^4.3.4" - fast-glob: "npm:^3.3.2" - is-glob: "npm:^4.0.3" - minimatch: "npm:^9.0.4" - semver: "npm:^7.6.0" - ts-api-utils: "npm:^1.3.0" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10c0/f28fea5af4798a718b6735d1758b791a331af17386b83cb2856d89934a5d1693f7cb805e73c3b33f29140884ac8ead9931b1d7c3de10176fa18ca7a346fe10d0 +"@typescript-eslint/types@npm:8.7.0": + version: 8.7.0 + resolution: "@typescript-eslint/types@npm:8.7.0" + checksum: 10c0/f7529eaea4ecc0f5e2d94ea656db8f930f6d1c1e65a3ffcb2f6bec87361173de2ea981405c2c483a35a927b3bdafb606319a1d0395a6feb1284448c8ba74c31e languageName: node linkType: hard @@ -3203,20 +3367,22 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.16.0": - version: 8.16.0 - resolution: "@typescript-eslint/utils@npm:8.16.0" +"@typescript-eslint/typescript-estree@npm:8.7.0": + version: 8.7.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.7.0" dependencies: - "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:8.16.0" - "@typescript-eslint/types": "npm:8.16.0" - "@typescript-eslint/typescript-estree": "npm:8.16.0" - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + "@typescript-eslint/types": "npm:8.7.0" + "@typescript-eslint/visitor-keys": "npm:8.7.0" + debug: "npm:^4.3.4" + fast-glob: "npm:^3.3.2" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^1.3.0" peerDependenciesMeta: typescript: optional: true - checksum: 10c0/1e61187eef3da1ab1486d2a977d8f3b1cb8ef7fa26338500a17eb875ca42a8942ef3f2241f509eef74cf7b5620c109483afc7d83d5b0ab79b1e15920f5a49818 + checksum: 10c0/d714605b6920a9631ab1511b569c1c158b1681c09005ab240125c442a63e906048064151a61ce5eb5f8fe75cea861ce5ae1d87be9d7296b012e4ab6d88755e8b languageName: node linkType: hard @@ -3235,13 +3401,17 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.16.0": - version: 8.16.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.16.0" +"@typescript-eslint/utils@npm:8.7.0": + version: 8.7.0 + resolution: "@typescript-eslint/utils@npm:8.7.0" dependencies: - "@typescript-eslint/types": "npm:8.16.0" - eslint-visitor-keys: "npm:^4.2.0" - checksum: 10c0/537df37801831aa8d91082b2adbffafd40305ed4518f0e7d3cbb17cc466d8b9ac95ac91fa232e7fe585d7c522d1564489ec80052ebb2a6ab9bbf89ef9dd9b7bc + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:8.7.0" + "@typescript-eslint/types": "npm:8.7.0" + "@typescript-eslint/typescript-estree": "npm:8.7.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + checksum: 10c0/7355b754ce2fc118773ed27a3e02b7dfae270eec73c2d896738835ecf842e8309544dfd22c5105aba6cae2787bfdd84129bbc42f4b514f57909dc7f6890b8eba languageName: node linkType: hard @@ -3255,6 +3425,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.7.0": + version: 8.7.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.7.0" + dependencies: + "@typescript-eslint/types": "npm:8.7.0" + eslint-visitor-keys: "npm:^3.4.3" + checksum: 10c0/1240da13c15f9f875644b933b0ad73713ef12f1db5715236824c1ec359e6ef082ce52dd9b2186d40e28be6a816a208c226e6e9af96e5baeb24b4399fe786ae7c + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.0.0": version: 1.2.1 resolution: "@ungap/structured-clone@npm:1.2.1" @@ -3467,6 +3647,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^7.1.1": + version: 7.4.1 + resolution: "acorn@npm:7.4.1" + bin: + acorn: bin/acorn + checksum: 10c0/bd0b2c2b0f334bbee48828ff897c12bd2eb5898d03bf556dcc8942022cec795ac5bb5b6b585e2de687db6231faf07e096b59a361231dd8c9344d5df5f7f0e526 + languageName: node + linkType: hard + "acorn@npm:^8.14.0, acorn@npm:^8.4.1, acorn@npm:^8.8.2": version: 8.14.0 resolution: "acorn@npm:8.14.0" @@ -3570,7 +3759,17 @@ __metadata: languageName: node linkType: hard -"ansi-colors@npm:4.1.3": +"alce@npm:1.2.0": + version: 1.2.0 + resolution: "alce@npm:1.2.0" + dependencies: + esprima: "npm:^1.2.0" + estraverse: "npm:^1.5.0" + checksum: 10c0/ece85d4cceb25a6c182a6f94c6f67e50205a55f48c30ff9cf3088c35d452dc275f6586d9f8c6f66471ad970700dc08093e25d6fce5578b001444af8014251275 + languageName: node + linkType: hard + +"ansi-colors@npm:4.1.3, ansi-colors@npm:^4.1.1": version: 4.1.3 resolution: "ansi-colors@npm:4.1.3" checksum: 10c0/ec87a2f59902f74e61eada7f6e6fe20094a628dab765cfdbd03c3477599368768cffccdb5d3bb19a1b6c99126783a143b1fee31aab729b31ffe5836c7e5e28b9 @@ -3777,6 +3976,13 @@ __metadata: languageName: node linkType: hard +"array-differ@npm:^3.0.0": + version: 3.0.0 + resolution: "array-differ@npm:3.0.0" + checksum: 10c0/c0d924cc2b7e3f5a0e6ae932e8941c5fddc0412bcecf8d5152641910e60f5e1c1e87da2b32083dec2f92f9a8f78e916ea68c22a0579794ba49886951ae783123 + languageName: node + linkType: hard + "array-flatten@npm:3.0.0": version: 3.0.0 resolution: "array-flatten@npm:3.0.0" @@ -3812,6 +4018,13 @@ __metadata: languageName: node linkType: hard +"array-union@npm:^2.1.0": + version: 2.1.0 + resolution: "array-union@npm:2.1.0" + checksum: 10c0/429897e68110374f39b771ec47a7161fc6a8fc33e196857c0a396dc75df0b5f65e4d046674db764330b6bb66b39ef48dd7c53b6a2ee75cfb0681e0c1a7033962 + languageName: node + linkType: hard + "array.prototype.findlast@npm:^1.2.5": version: 1.2.5 resolution: "array.prototype.findlast@npm:1.2.5" @@ -3935,6 +4148,32 @@ __metadata: languageName: node linkType: hard +"arrify@npm:^2.0.1": + version: 2.0.1 + resolution: "arrify@npm:2.0.1" + checksum: 10c0/3fb30b5e7c37abea1907a60b28a554d2f0fc088757ca9bf5b684786e583fdf14360721eb12575c1ce6f995282eab936712d3c4389122682eafab0e0b57f78dbb + languageName: node + linkType: hard + +"asap@npm:~2.0.3": + version: 2.0.6 + resolution: "asap@npm:2.0.6" + checksum: 10c0/c6d5e39fe1f15e4b87677460bd66b66050cd14c772269cee6688824c1410a08ab20254bb6784f9afb75af9144a9f9a7692d49547f4d19d715aeb7c0318f3136d + languageName: node + linkType: hard + +"asn1.js@npm:^5.0.0": + version: 5.4.1 + resolution: "asn1.js@npm:5.4.1" + dependencies: + bn.js: "npm:^4.0.0" + inherits: "npm:^2.0.1" + minimalistic-assert: "npm:^1.0.0" + safer-buffer: "npm:^2.1.0" + checksum: 10c0/b577232fa6069cc52bb128e564002c62b2b1fe47f7137bdcd709c0b8495aa79cee0f8cc458a831b2d8675900eea0d05781b006be5e1aa4f0ae3577a73ec20324 + languageName: node + linkType: hard + "asn1@npm:~0.2.3": version: 0.2.6 resolution: "asn1@npm:0.2.6" @@ -3944,6 +4183,13 @@ __metadata: languageName: node linkType: hard +"assert-never@npm:^1.2.1": + version: 1.4.0 + resolution: "assert-never@npm:1.4.0" + checksum: 10c0/494db08b89fb43d6231c9b4c48da22824f1912d88992bf0268e43b3dad0f64bd56d380addbb997d2dea7d859421d5e2904e8bd01243794f2bb5bfbc8d32d1fc6 + languageName: node + linkType: hard + "assert-plus@npm:1.0.0, assert-plus@npm:^1.0.0": version: 1.0.0 resolution: "assert-plus@npm:1.0.0" @@ -4168,6 +4414,15 @@ __metadata: languageName: node linkType: hard +"babel-walk@npm:3.0.0-canary-5": + version: 3.0.0-canary-5 + resolution: "babel-walk@npm:3.0.0-canary-5" + dependencies: + "@babel/types": "npm:^7.9.6" + checksum: 10c0/17b689874d15c37714cedf6797dd9321dcb998d8e0dda9a8fe8c8bbbf128bbdeb8935cf56e8630d6b67eae76d2a0bc1e470751e082c3b0e30b80d58beafb5e64 + languageName: node + linkType: hard + "bail@npm:^2.0.0": version: 2.0.2 resolution: "bail@npm:2.0.2" @@ -4244,13 +4499,20 @@ __metadata: languageName: node linkType: hard -"bluebird@npm:^3.3.5, bluebird@npm:^3.5.0": +"bluebird@npm:^3.3.5, bluebird@npm:^3.5.0, bluebird@npm:^3.7.2": version: 3.7.2 resolution: "bluebird@npm:3.7.2" checksum: 10c0/680de03adc54ff925eaa6c7bb9a47a0690e8b5de60f4792604aae8ed618c65e6b63a7893b57ca924beaf53eee69c5af4f8314148c08124c550fe1df1add897d2 languageName: node linkType: hard +"bn.js@npm:^4.0.0": + version: 4.12.1 + resolution: "bn.js@npm:4.12.1" + checksum: 10c0/b7f37a0cd5e4b79142b6f4292d518b416be34ae55d6dd6b0f66f96550c8083a50ffbbf8bda8d0ab471158cb81aa74ea4ee58fe33c7802e4a30b13810e98df116 + languageName: node + linkType: hard + "body-parser@npm:^2.0.1": version: 2.0.2 resolution: "body-parser@npm:2.0.2" @@ -4269,6 +4531,20 @@ __metadata: languageName: node linkType: hard +"boolbase@npm:^1.0.0": + version: 1.0.0 + resolution: "boolbase@npm:1.0.0" + checksum: 10c0/e4b53deb4f2b85c52be0e21a273f2045c7b6a6ea002b0e139c744cb6f95e9ec044439a52883b0d74dedd1ff3da55ed140cfdddfed7fb0cccbed373de5dce1bcf + languageName: node + linkType: hard + +"boolean@npm:3.2.0": + version: 3.2.0 + resolution: "boolean@npm:3.2.0" + checksum: 10c0/6a0dc9668f6f3dda42a53c181fcbdad223169c8d87b6c4011b87a8b14a21770efb2934a778f063d7ece17280f8c06d313c87f7b834bb1dd526a867ffcd00febf + languageName: node + linkType: hard + "bottleneck@npm:^2.15.3": version: 2.19.5 resolution: "bottleneck@npm:2.19.5" @@ -4588,6 +4864,16 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^3.0.0": + version: 3.0.0 + resolution: "chalk@npm:3.0.0" + dependencies: + ansi-styles: "npm:^4.1.0" + supports-color: "npm:^7.1.0" + checksum: 10c0/ee650b0a065b3d7a6fda258e75d3a86fc8e4effa55871da730a9e42ccb035bf5fd203525e5a1ef45ec2582ecc4f65b47eb11357c526b84dd29a14fb162c414d2 + languageName: node + linkType: hard + "chalk@npm:^5.3.0": version: 5.3.0 resolution: "chalk@npm:5.3.0" @@ -4623,6 +4909,15 @@ __metadata: languageName: node linkType: hard +"character-parser@npm:^2.2.0": + version: 2.2.0 + resolution: "character-parser@npm:2.2.0" + dependencies: + is-regex: "npm:^1.0.3" + checksum: 10c0/5a8d3eff2c912a6878c84e2ebf9d42524e858aa7e1a1c7e8bb79ab54da109ad008fe9057a9d2b3230541d7ff858eda98983a2ae15db57ba01af2e989d29e932e + languageName: node + linkType: hard + "character-reference-invalid@npm:^2.0.0": version: 2.0.1 resolution: "character-reference-invalid@npm:2.0.1" @@ -4637,6 +4932,34 @@ __metadata: languageName: node linkType: hard +"cheerio-select@npm:^1.5.0": + version: 1.6.0 + resolution: "cheerio-select@npm:1.6.0" + dependencies: + css-select: "npm:^4.3.0" + css-what: "npm:^6.0.1" + domelementtype: "npm:^2.2.0" + domhandler: "npm:^4.3.1" + domutils: "npm:^2.8.0" + checksum: 10c0/4adfdc79e93cba3c9e6162fa82b016ac945035aae2ae5eace0830f44e183f43353d8df42aa8d0aa7efe0d014b6a5ce703d41a887c2a54ec079edeb81bfea889d + languageName: node + linkType: hard + +"cheerio@npm:1.0.0-rc.10": + version: 1.0.0-rc.10 + resolution: "cheerio@npm:1.0.0-rc.10" + dependencies: + cheerio-select: "npm:^1.5.0" + dom-serializer: "npm:^1.3.2" + domhandler: "npm:^4.2.0" + htmlparser2: "npm:^6.1.0" + parse5: "npm:^6.0.1" + parse5-htmlparser2-tree-adapter: "npm:^6.0.1" + tslib: "npm:^2.2.0" + checksum: 10c0/2bb0fae8b1941949f506ddc4df75e3c2d0e5cc6c05478f918dd64a4d2c5282ec84b243890f6a809052a8eb6214641084922c07f726b5287b5dba114b10e52cb9 + languageName: node + linkType: hard + "chokidar@npm:3.6.0, chokidar@npm:^3.6.0": version: 3.6.0 resolution: "chokidar@npm:3.6.0" @@ -4696,7 +5019,7 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^3.2.0": +"ci-info@npm:^3.2.0, ci-info@npm:^3.8.0": version: 3.9.0 resolution: "ci-info@npm:3.9.0" checksum: 10c0/6f0109e36e111684291d46123d491bc4e7b7a1934c3a20dea28cba89f1d4a03acd892f5f6a81ed3855c38647e285a150e3c9ba062e38943bef57fee6c1554c3a @@ -5002,13 +5325,20 @@ __metadata: languageName: node linkType: hard -"commander@npm:^2.20.0": +"commander@npm:^2.19.0, commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" checksum: 10c0/74c781a5248c2402a0a3e966a0a2bba3c054aad144f5c023364be83265e796b20565aa9feff624132ff629aa64e16999fa40a743c10c12f7c61e96a794b99288 languageName: node linkType: hard +"commander@npm:^6.1.0": + version: 6.2.1 + resolution: "commander@npm:6.2.1" + checksum: 10c0/85748abd9d18c8bc88febed58b98f66b7c591d9b5017cad459565761d7b29ca13b7783ea2ee5ce84bf235897333706c4ce29adf1ce15c8252780e7000e2ce9ea + languageName: node + linkType: hard + "comment-json@npm:4.2.5": version: 4.2.5 resolution: "comment-json@npm:4.2.5" @@ -5082,6 +5412,25 @@ __metadata: languageName: node linkType: hard +"consolidate@npm:^0.16.0": + version: 0.16.0 + resolution: "consolidate@npm:0.16.0" + dependencies: + bluebird: "npm:^3.7.2" + checksum: 10c0/0a574394787bf03f70244cbbb0fbae5e9cbd915d5eeda094245c05eeb4702fc12565544cb56267c953ecff93a8fc2efca7fd70d76b9aab5819042c916d7483fb + languageName: node + linkType: hard + +"constantinople@npm:^4.0.1": + version: 4.0.1 + resolution: "constantinople@npm:4.0.1" + dependencies: + "@babel/parser": "npm:^7.6.0" + "@babel/types": "npm:^7.6.1" + checksum: 10c0/15129adef19b1af2c3ade8bd38f97c34781bf461472a30ab414384b28d072be83070c8d2175787c045ef7c222c415101ae609936e7903427796a0c0eca8449fd + languageName: node + linkType: hard + "content-disposition@npm:^1.0.0": version: 1.0.0 resolution: "content-disposition@npm:1.0.0" @@ -5300,6 +5649,19 @@ __metadata: languageName: node linkType: hard +"cross-spawn@npm:^6.0.0": + version: 6.0.6 + resolution: "cross-spawn@npm:6.0.6" + dependencies: + nice-try: "npm:^1.0.4" + path-key: "npm:^2.0.1" + semver: "npm:^5.5.0" + shebang-command: "npm:^1.2.0" + which: "npm:^1.2.9" + checksum: 10c0/bf61fb890e8635102ea9bce050515cf915ff6a50ccaa0b37a17dc82fded0fb3ed7af5478b9367b86baee19127ad86af4be51d209f64fd6638c0862dca185fe1d + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" @@ -5320,6 +5682,26 @@ __metadata: languageName: node linkType: hard +"css-select@npm:^4.3.0": + version: 4.3.0 + resolution: "css-select@npm:4.3.0" + dependencies: + boolbase: "npm:^1.0.0" + css-what: "npm:^6.0.1" + domhandler: "npm:^4.3.1" + domutils: "npm:^2.8.0" + nth-check: "npm:^2.0.1" + checksum: 10c0/a489d8e5628e61063d5a8fe0fa1cc7ae2478cb334a388a354e91cf2908154be97eac9fa7ed4dffe87a3e06cf6fcaa6016553115335c4fd3377e13dac7bd5a8e1 + languageName: node + linkType: hard + +"css-what@npm:^6.0.1": + version: 6.1.0 + resolution: "css-what@npm:6.1.0" + checksum: 10c0/a09f5a6b14ba8dcf57ae9a59474722e80f20406c53a61e9aedb0eedc693b135113ffe2983f4efc4b5065ae639442e9ae88df24941ef159c218b231011d733746 + languageName: node + linkType: hard + "cssesc@npm:^3.0.0": version: 3.0.0 resolution: "cssesc@npm:3.0.0" @@ -5539,7 +5921,7 @@ __metadata: languageName: node linkType: hard -"deepmerge@npm:^4.2.2": +"deepmerge@npm:^4.2.2, deepmerge@npm:^4.3.1": version: 4.3.1 resolution: "deepmerge@npm:4.3.1" checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044 @@ -5623,6 +6005,13 @@ __metadata: languageName: node linkType: hard +"detect-indent@npm:^6.0.0": + version: 6.1.0 + resolution: "detect-indent@npm:6.1.0" + checksum: 10c0/dd83cdeda9af219cf77f5e9a0dc31d828c045337386cfb55ce04fad94ba872ee7957336834154f7647b89b899c3c7acc977c57a79b7c776b506240993f97acc7 + languageName: node + linkType: hard + "detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.3": version: 2.0.3 resolution: "detect-libc@npm:2.0.3" @@ -5630,7 +6019,7 @@ __metadata: languageName: node linkType: hard -"detect-newline@npm:^3.0.0": +"detect-newline@npm:^3.0.0, detect-newline@npm:^3.1.0": version: 3.1.0 resolution: "detect-newline@npm:3.1.0" checksum: 10c0/c38cfc8eeb9fda09febb44bcd85e467c970d4e3bf526095394e5a4f18bc26dd0cf6b22c69c1fa9969261521c593836db335c2795218f6d781a512aea2fb8209d @@ -5683,6 +6072,23 @@ __metadata: languageName: node linkType: hard +"discontinuous-range@npm:1.0.0": + version: 1.0.0 + resolution: "discontinuous-range@npm:1.0.0" + checksum: 10c0/487b105f83c1cc528e25e65d3c4b73958ec79769b7bd0e264414702a23a7e2b282c72982b4bef4af29fcab53f47816c3f0a5c40d85a99a490f4bc35b83dc00f8 + languageName: node + linkType: hard + +"display-notification@npm:2.0.0": + version: 2.0.0 + resolution: "display-notification@npm:2.0.0" + dependencies: + escape-string-applescript: "npm:^1.0.0" + run-applescript: "npm:^3.0.0" + checksum: 10c0/24a93b8d8f47812f26aa54fe27ffd10b7495ee824425e0a3c45abbd9df243452f28addb9a0d712fa77566f6787b682ac2757071bf6f0a018b49f9d39ca36efed + languageName: node + linkType: hard + "dlv@npm:^1.1.3": version: 1.1.3 resolution: "dlv@npm:1.1.3" @@ -5699,6 +6105,13 @@ __metadata: languageName: node linkType: hard +"doctypes@npm:^1.1.0": + version: 1.1.0 + resolution: "doctypes@npm:1.1.0" + checksum: 10c0/b3f9d597ad8b9ac6aeba9d64df61f0098174f7570e3d34f7ee245ebc736c7bee122d9738a18e22010b98983fd9a340d63043d3841f02d8a7742a2d96d2c72610 + languageName: node + linkType: hard + "dom-helpers@npm:^5.0.1": version: 5.2.1 resolution: "dom-helpers@npm:5.2.1" @@ -5709,18 +6122,96 @@ __metadata: languageName: node linkType: hard -"dot-prop@npm:^5.1.0": - version: 5.3.0 - resolution: "dot-prop@npm:5.3.0" +"dom-serializer@npm:^1.0.1, dom-serializer@npm:^1.3.2": + version: 1.4.1 + resolution: "dom-serializer@npm:1.4.1" dependencies: - is-obj: "npm:^2.0.0" - checksum: 10c0/93f0d343ef87fe8869320e62f2459f7e70f49c6098d948cc47e060f4a3f827d0ad61e83cb82f2bd90cd5b9571b8d334289978a43c0f98fea4f0e99ee8faa0599 + domelementtype: "npm:^2.0.1" + domhandler: "npm:^4.2.0" + entities: "npm:^2.0.0" + checksum: 10c0/67d775fa1ea3de52035c98168ddcd59418356943b5eccb80e3c8b3da53adb8e37edb2cc2f885802b7b1765bf5022aec21dfc32910d7f9e6de4c3148f095ab5e0 languageName: node linkType: hard -"dotenv@npm:^16.0.3": - version: 16.3.1 - resolution: "dotenv@npm:16.3.1" +"dom-serializer@npm:^2.0.0": + version: 2.0.0 + resolution: "dom-serializer@npm:2.0.0" + dependencies: + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.2" + entities: "npm:^4.2.0" + checksum: 10c0/d5ae2b7110ca3746b3643d3ef60ef823f5f078667baf530cec096433f1627ec4b6fa8c072f09d079d7cda915fd2c7bc1b7b935681e9b09e591e1e15f4040b8e2 + languageName: node + linkType: hard + +"domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0, domelementtype@npm:^2.3.0": + version: 2.3.0 + resolution: "domelementtype@npm:2.3.0" + checksum: 10c0/686f5a9ef0fff078c1412c05db73a0dce096190036f33e400a07e2a4518e9f56b1e324f5c576a0a747ef0e75b5d985c040b0d51945ce780c0dd3c625a18cd8c9 + languageName: node + linkType: hard + +"domhandler@npm:^3.3.0": + version: 3.3.0 + resolution: "domhandler@npm:3.3.0" + dependencies: + domelementtype: "npm:^2.0.1" + checksum: 10c0/376e6462a6144121f6ae50c9c1b8e0b22d2e0c68f9fb2ef6e57a5f4f9395854b1258cb638c58b171ee291359a5f41a4a57f403954db976484a59ffcee4c1e405 + languageName: node + linkType: hard + +"domhandler@npm:^4.0.0, domhandler@npm:^4.2.0, domhandler@npm:^4.3.1": + version: 4.3.1 + resolution: "domhandler@npm:4.3.1" + dependencies: + domelementtype: "npm:^2.2.0" + checksum: 10c0/5c199c7468cb052a8b5ab80b13528f0db3d794c64fc050ba793b574e158e67c93f8336e87fd81e9d5ee43b0e04aea4d8b93ed7be4899cb726a1601b3ba18538b + languageName: node + linkType: hard + +"domhandler@npm:^5.0.2, domhandler@npm:^5.0.3": + version: 5.0.3 + resolution: "domhandler@npm:5.0.3" + dependencies: + domelementtype: "npm:^2.3.0" + checksum: 10c0/bba1e5932b3e196ad6862286d76adc89a0dbf0c773e5ced1eb01f9af930c50093a084eff14b8de5ea60b895c56a04d5de8bbc4930c5543d029091916770b2d2a + languageName: node + linkType: hard + +"domutils@npm:^2.4.2, domutils@npm:^2.5.2, domutils@npm:^2.8.0": + version: 2.8.0 + resolution: "domutils@npm:2.8.0" + dependencies: + dom-serializer: "npm:^1.0.1" + domelementtype: "npm:^2.2.0" + domhandler: "npm:^4.2.0" + checksum: 10c0/d58e2ae01922f0dd55894e61d18119924d88091837887bf1438f2327f32c65eb76426bd9384f81e7d6dcfb048e0f83c19b222ad7101176ad68cdc9c695b563db + languageName: node + linkType: hard + +"domutils@npm:^3.0.1": + version: 3.2.2 + resolution: "domutils@npm:3.2.2" + dependencies: + dom-serializer: "npm:^2.0.0" + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.3" + checksum: 10c0/47938f473b987ea71cd59e59626eb8666d3aa8feba5266e45527f3b636c7883cca7e582d901531961f742c519d7514636b7973353b648762b2e3bedbf235fada + languageName: node + linkType: hard + +"dot-prop@npm:^5.1.0": + version: 5.3.0 + resolution: "dot-prop@npm:5.3.0" + dependencies: + is-obj: "npm:^2.0.0" + checksum: 10c0/93f0d343ef87fe8869320e62f2459f7e70f49c6098d948cc47e060f4a3f827d0ad61e83cb82f2bd90cd5b9571b8d334289978a43c0f98fea4f0e99ee8faa0599 + languageName: node + linkType: hard + +"dotenv@npm:^16.0.3": + version: 16.3.1 + resolution: "dotenv@npm:16.3.1" checksum: 10c0/b95ff1bbe624ead85a3cd70dbd827e8e06d5f05f716f2d0cbc476532d54c7c9469c3bc4dd93ea519f6ad711cb522c00ac9a62b6eb340d5affae8008facc3fbd7 languageName: node linkType: hard @@ -5807,6 +6298,23 @@ __metadata: languageName: node linkType: hard +"email-templates@npm:9.0.0": + version: 9.0.0 + resolution: "email-templates@npm:9.0.0" + dependencies: + "@ladjs/i18n": "npm:^7.2.6" + consolidate: "npm:^0.16.0" + debug: "npm:^4.3.4" + get-paths: "npm:^0.0.7" + html-to-text: "npm:^8.1.0" + juice: "npm:^8.0.0" + lodash: "npm:^4.17.21" + nodemailer: "npm:^6.7.2" + preview-email: "npm:^3.0.5" + checksum: 10c0/f97c7d3d7ad17cfee4dbe6c5f6bffb0fea3e1cfc3f97f6dc6b6b808916bdc981962ccb79c87a85419f8b857e9041bf105e6a450265cd2a21d6d408d4b3c9077d + languageName: node + linkType: hard + "emittery@npm:^0.13.1": version: 0.13.1 resolution: "emittery@npm:0.13.1" @@ -5856,6 +6364,13 @@ __metadata: languageName: node linkType: hard +"encoding-japanese@npm:2.2.0": + version: 2.2.0 + resolution: "encoding-japanese@npm:2.2.0" + checksum: 10c0/9d1f10dde16f59da8a8a1a04499dffa3e9926b0dbd7dfab8054570527b7e6de30c47e828851f42d2727af31586ec8049a84eeae593ad8b22eea10921fd269798 + languageName: node + linkType: hard + "encoding@npm:^0.1.12, encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -5904,6 +6419,20 @@ __metadata: languageName: node linkType: hard +"entities@npm:^2.0.0": + version: 2.2.0 + resolution: "entities@npm:2.2.0" + checksum: 10c0/7fba6af1f116300d2ba1c5673fc218af1961b20908638391b4e1e6d5850314ee2ac3ec22d741b3a8060479911c99305164aed19b6254bde75e7e6b1b2c3f3aa3 + languageName: node + linkType: hard + +"entities@npm:^4.2.0, entities@npm:^4.4.0": + version: 4.5.0 + resolution: "entities@npm:4.5.0" + checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250 + languageName: node + linkType: hard + "env-ci@npm:^11.0.0": version: 11.0.0 resolution: "env-ci@npm:11.0.0" @@ -6240,6 +6769,13 @@ __metadata: languageName: node linkType: hard +"escape-goat@npm:^3.0.0": + version: 3.0.0 + resolution: "escape-goat@npm:3.0.0" + checksum: 10c0/a2b470bbdb95ccbcd19390576993a2b75735457b1979275f4f0d6da86d2e932a2a12edd9270208e3090299a26df857da1f80555c37bb1bac6fa9135322253ca4 + languageName: node + linkType: hard + "escape-html@npm:^1.0.3, escape-html@npm:~1.0.3": version: 1.0.3 resolution: "escape-html@npm:1.0.3" @@ -6247,6 +6783,13 @@ __metadata: languageName: node linkType: hard +"escape-string-applescript@npm:^1.0.0": + version: 1.0.0 + resolution: "escape-string-applescript@npm:1.0.0" + checksum: 10c0/25d89b0014a2bb22b3714ba20f1255ecaac5004d3a4dc54e739bae3fa38ff4f9fdc1679acabf117e4b1e3bf321b3a14367fbbd4baf216804958422ec6355dfa6 + languageName: node + linkType: hard + "escape-string-regexp@npm:5.0.0": version: 5.0.0 resolution: "escape-string-regexp@npm:5.0.0" @@ -6475,7 +7018,7 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.3.0": +"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.3": version: 3.4.3 resolution: "eslint-visitor-keys@npm:3.4.3" checksum: 10c0/92708e882c0a5ffd88c23c0b404ac1628cf20104a108c745f240a13c332a11aac54f49a22d5762efbffc18ecbc9a580d1b7ad034bf5f3cc3307e5cbff2ec9820 @@ -6549,6 +7092,16 @@ __metadata: languageName: node linkType: hard +"esprima@npm:^1.2.0": + version: 1.2.5 + resolution: "esprima@npm:1.2.5" + bin: + esparse: ./bin/esparse.js + esvalidate: ./bin/esvalidate.js + checksum: 10c0/634f272901b48174b84fd59ae6d9fbe03af38cfaa60501d8c0c7227d96ac53aef232e6fafda7c9760e50997f675e1c828e0b29aa39b092982960acf5e937db8f + languageName: node + linkType: hard + "esprima@npm:^4.0.0, esprima@npm:^4.0.1": version: 4.0.1 resolution: "esprima@npm:4.0.1" @@ -6577,6 +7130,13 @@ __metadata: languageName: node linkType: hard +"estraverse@npm:^1.5.0": + version: 1.9.3 + resolution: "estraverse@npm:1.9.3" + checksum: 10c0/2477bab0c5cdc7534162fbb16b25282c49f434875227937726692ed105762403e9830324cc97c3ea8bf332fe91122ea321f4d4292aaf50db7a90d857e169719e + languageName: node + linkType: hard + "estraverse@npm:^4.1.1": version: 4.3.0 resolution: "estraverse@npm:4.3.0" @@ -6619,6 +7179,21 @@ __metadata: languageName: node linkType: hard +"execa@npm:^0.10.0": + version: 0.10.0 + resolution: "execa@npm:0.10.0" + dependencies: + cross-spawn: "npm:^6.0.0" + get-stream: "npm:^3.0.0" + is-stream: "npm:^1.1.0" + npm-run-path: "npm:^2.0.0" + p-finally: "npm:^1.0.0" + signal-exit: "npm:^3.0.0" + strip-eof: "npm:^1.0.0" + checksum: 10c0/3e8562b78f1552ff629770aa81cdb98dac927b6b7f7adc10815ecf13b6917bbabc0468d7d81494f0bb05c59b83a22639e879ef15a17c0dc3434cc32e90e85ab3 + languageName: node + linkType: hard + "execa@npm:^5.0.0": version: 5.1.1 resolution: "execa@npm:5.1.1" @@ -6747,6 +7322,13 @@ __metadata: languageName: node linkType: hard +"extend-object@npm:^1.0.0": + version: 1.0.0 + resolution: "extend-object@npm:1.0.0" + checksum: 10c0/7644bfe3d2bac31fa3ab0d1aedef2a5a4c27975690f92191ec8a72d6440605f7ad920d60ac356374177ec71b1e3369e7134997b31692e2f7a55a4cb7fbb7ba57 + languageName: node + linkType: hard + "extend@npm:^3.0.0, extend@npm:~3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2" @@ -6839,6 +7421,13 @@ __metadata: languageName: node linkType: hard +"fast-printf@npm:^1.6.9": + version: 1.6.10 + resolution: "fast-printf@npm:1.6.10" + checksum: 10c0/630cccbef8349a6eeada9958c9391e9d14b5bd9527dd28d9e7e9f295b006c640ddca8fc2470cff55ea5c6c92ca530488bbadbf7c24f4852ad8c34d9155da9d75 + languageName: node + linkType: hard + "fast-safe-stringify@npm:2.1.1, fast-safe-stringify@npm:^2.1.1": version: 2.1.1 resolution: "fast-safe-stringify@npm:2.1.1" @@ -7025,6 +7614,22 @@ __metadata: languageName: node linkType: hard +"fixpack@npm:^4.0.0": + version: 4.0.0 + resolution: "fixpack@npm:4.0.0" + dependencies: + alce: "npm:1.2.0" + chalk: "npm:^3.0.0" + detect-indent: "npm:^6.0.0" + detect-newline: "npm:^3.1.0" + extend-object: "npm:^1.0.0" + rc: "npm:^1.2.8" + bin: + fixpack: bin/fixpack + checksum: 10c0/f88d9cb469a2eb55eeff02024011853726a5a1c04dcd4d609f0f7824333e8226e0411079675a68280dba07e11103a8bd8bd039b1fa87504d7d5cf7cc0d05e4c7 + languageName: node + linkType: hard + "flat-cache@npm:^4.0.0": version: 4.0.1 resolution: "flat-cache@npm:4.0.1" @@ -7391,6 +7996,29 @@ __metadata: languageName: node linkType: hard +"get-paths@npm:^0.0.7": + version: 0.0.7 + resolution: "get-paths@npm:0.0.7" + dependencies: + pify: "npm:^4.0.1" + checksum: 10c0/ca4bbd2e80760141bc2304bc9eb2bc442b848090bc8118727251f54ca37b212879026fa8013a9b23d2df0360940eb0b6a540769c1ae1542c100d0ab949964698 + languageName: node + linkType: hard + +"get-port@npm:5.1.1": + version: 5.1.1 + resolution: "get-port@npm:5.1.1" + checksum: 10c0/2873877a469b24e6d5e0be490724a17edb39fafc795d1d662e7bea951ca649713b4a50117a473f9d162312cb0e946597bd0e049ed2f866e79e576e8e213d3d1c + languageName: node + linkType: hard + +"get-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "get-stream@npm:3.0.0" + checksum: 10c0/003f5f3b8870da59c6aafdf6ed7e7b07b48c2f8629cd461bd3900726548b6b8cfa2e14d6b7814fbb08f07a42f4f738407fa70b989928b2783a76b278505bba22 + languageName: node + linkType: hard + "get-stream@npm:^6.0.0": version: 6.0.1 resolution: "get-stream@npm:6.0.1" @@ -7806,6 +8434,15 @@ __metadata: languageName: node linkType: hard +"he@npm:1.2.0, he@npm:^1.2.0": + version: 1.2.0 + resolution: "he@npm:1.2.0" + bin: + he: bin/he + checksum: 10c0/a27d478befe3c8192f006cdd0639a66798979dfa6e2125c6ac582a19a5ebfec62ad83e8382e6036170d873f46e4536a7e795bf8b95bf7c247f4cc0825ccc8c17 + languageName: node + linkType: hard + "highlight.js@npm:^10.7.1": version: 10.7.3 resolution: "highlight.js@npm:10.7.3" @@ -7854,6 +8491,35 @@ __metadata: languageName: node linkType: hard +"html-to-text@npm:9.0.5": + version: 9.0.5 + resolution: "html-to-text@npm:9.0.5" + dependencies: + "@selderee/plugin-htmlparser2": "npm:^0.11.0" + deepmerge: "npm:^4.3.1" + dom-serializer: "npm:^2.0.0" + htmlparser2: "npm:^8.0.2" + selderee: "npm:^0.11.0" + checksum: 10c0/5d2c77b798cf88a81b1da2fc1ea1a3b3e2ff49fe5a3d812392f802fff18ec315cf0969bd7846ef2eb7df8c37f463bc63e8cbdcf84e42696c6f3e15dfa61cdf4f + languageName: node + linkType: hard + +"html-to-text@npm:^8.1.0": + version: 8.2.1 + resolution: "html-to-text@npm:8.2.1" + dependencies: + "@selderee/plugin-htmlparser2": "npm:^0.6.0" + deepmerge: "npm:^4.2.2" + he: "npm:^1.2.0" + htmlparser2: "npm:^6.1.0" + minimist: "npm:^1.2.6" + selderee: "npm:^0.6.0" + bin: + html-to-text: bin/cli.js + checksum: 10c0/2020c0bf9c91113662c69023e7d8a4fac59027fa7513cf85d2882cbaef7d6b9d0902f4f089fdf70f70f064fa60f1f6d8ac5e269c4004dd5bf7fc6b3f1deb2b93 + languageName: node + linkType: hard + "html-url-attributes@npm:^3.0.0": version: 3.0.1 resolution: "html-url-attributes@npm:3.0.1" @@ -7861,6 +8527,42 @@ __metadata: languageName: node linkType: hard +"htmlparser2@npm:^5.0.0": + version: 5.0.1 + resolution: "htmlparser2@npm:5.0.1" + dependencies: + domelementtype: "npm:^2.0.1" + domhandler: "npm:^3.3.0" + domutils: "npm:^2.4.2" + entities: "npm:^2.0.0" + checksum: 10c0/3f276f7ac518930f5330cfe5129dd5764a63e9bae6f57350e90b26affc94b11b2fb6750f056fed245b726d500e78197b4a09c7108c71964fe91303e6e2a29107 + languageName: node + linkType: hard + +"htmlparser2@npm:^6.1.0": + version: 6.1.0 + resolution: "htmlparser2@npm:6.1.0" + dependencies: + domelementtype: "npm:^2.0.1" + domhandler: "npm:^4.0.0" + domutils: "npm:^2.5.2" + entities: "npm:^2.0.0" + checksum: 10c0/3058499c95634f04dc66be8c2e0927cd86799413b2d6989d8ae542ca4dbf5fa948695d02c27d573acf44843af977aec6d9a7bdd0f6faa6b2d99e2a729b2a31b6 + languageName: node + linkType: hard + +"htmlparser2@npm:^8.0.2": + version: 8.0.2 + resolution: "htmlparser2@npm:8.0.2" + dependencies: + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.3" + domutils: "npm:^3.0.1" + entities: "npm:^4.4.0" + checksum: 10c0/609cca85886d0bf2c9a5db8c6926a89f3764596877492e2caa7a25a789af4065bc6ee2cdc81807fe6b1d03a87bf8a373b5a754528a4cc05146b713c20575aab4 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -7963,6 +8665,29 @@ __metadata: languageName: node linkType: hard +"i18n-locales@npm:^0.0.5": + version: 0.0.5 + resolution: "i18n-locales@npm:0.0.5" + dependencies: + "@ladjs/country-language": "npm:^0.2.1" + checksum: 10c0/4e33ef664a82215eae62413f99114940b995e337aad153fbf5294a9995507f3225c4233250719a222bc93eb528d95d0e3d18a7b518009e6b3e94260d87b3cf3f + languageName: node + linkType: hard + +"i18n@npm:^0.14.1": + version: 0.14.2 + resolution: "i18n@npm:0.14.2" + dependencies: + "@messageformat/core": "npm:^3.0.0" + debug: "npm:^4.3.3" + fast-printf: "npm:^1.6.9" + make-plural: "npm:^7.0.0" + math-interval-parser: "npm:^2.0.1" + mustache: "npm:^4.2.0" + checksum: 10c0/fb3b3cf34dc3f8e979f8a809d8f16a48f432995ff63387ce62cb5bc2cf78fe25f0a3067dca5619fb4168d74572b80f3a20efef97b730e51b9dfacb1b4af84c76 + languageName: node + linkType: hard + "iconv-lite@npm:0.5.2": version: 0.5.2 resolution: "iconv-lite@npm:0.5.2" @@ -8427,6 +9152,15 @@ __metadata: languageName: node linkType: hard +"is-core-module@npm:^2.16.0": + version: 2.16.1 + resolution: "is-core-module@npm:2.16.1" + dependencies: + hasown: "npm:^2.0.2" + checksum: 10c0/898443c14780a577e807618aaae2b6f745c8538eca5c7bc11388a3f2dc6de82b9902bcc7eb74f07be672b11bbe82dd6a6edded44a00cb3d8f933d0459905eedd + languageName: node + linkType: hard + "is-data-view@npm:^1.0.1": version: 1.0.1 resolution: "is-data-view@npm:1.0.1" @@ -8473,6 +9207,25 @@ __metadata: languageName: node linkType: hard +"is-docker@npm:^2.0.0": + version: 2.2.1 + resolution: "is-docker@npm:2.2.1" + bin: + is-docker: cli.js + checksum: 10c0/e828365958d155f90c409cdbe958f64051d99e8aedc2c8c4cd7c89dcf35329daed42f7b99346f7828df013e27deb8f721cf9408ba878c76eb9e8290235fbcdcc + languageName: node + linkType: hard + +"is-expression@npm:^4.0.0": + version: 4.0.0 + resolution: "is-expression@npm:4.0.0" + dependencies: + acorn: "npm:^7.1.1" + object-assign: "npm:^4.1.1" + checksum: 10c0/541831d39d3e7bfc8cecd966d6b0f3c0e6d9055342f17b634fb23e74f51ce90f1bfc3cf231c722fe003a61e8d4f0b9e07244fdaba57f4fc70a163c74006fd5a0 + languageName: node + linkType: hard + "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -8626,17 +9379,14 @@ __metadata: languageName: node linkType: hard -"is-regex@npm:^1.1.4": - version: 1.1.4 - resolution: "is-regex@npm:1.1.4" - dependencies: - call-bind: "npm:^1.0.2" - has-tostringtag: "npm:^1.0.0" - checksum: 10c0/bb72aae604a69eafd4a82a93002058c416ace8cde95873589a97fc5dac96a6c6c78a9977d487b7b95426a8f5073969124dd228f043f9f604f041f32fcc465fc1 +"is-promise@npm:^2.0.0": + version: 2.2.2 + resolution: "is-promise@npm:2.2.2" + checksum: 10c0/2dba959812380e45b3df0fb12e7cb4d4528c989c7abb03ececb1d1fd6ab1cbfee956ca9daa587b9db1d8ac3c1e5738cf217bdb3dfd99df8c691be4c00ae09069 languageName: node linkType: hard -"is-regex@npm:^1.2.1": +"is-regex@npm:^1.0.3, is-regex@npm:^1.2.1": version: 1.2.1 resolution: "is-regex@npm:1.2.1" dependencies: @@ -8648,6 +9398,16 @@ __metadata: languageName: node linkType: hard +"is-regex@npm:^1.1.4": + version: 1.1.4 + resolution: "is-regex@npm:1.1.4" + dependencies: + call-bind: "npm:^1.0.2" + has-tostringtag: "npm:^1.0.0" + checksum: 10c0/bb72aae604a69eafd4a82a93002058c416ace8cde95873589a97fc5dac96a6c6c78a9977d487b7b95426a8f5073969124dd228f043f9f604f041f32fcc465fc1 + languageName: node + linkType: hard + "is-set@npm:^2.0.1": version: 2.0.2 resolution: "is-set@npm:2.0.2" @@ -8680,6 +9440,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^1.1.0": + version: 1.1.0 + resolution: "is-stream@npm:1.1.0" + checksum: 10c0/b8ae7971e78d2e8488d15f804229c6eed7ed36a28f8807a1815938771f4adff0e705218b7dab968270433f67103e4fef98062a0beea55d64835f705ee72c7002 + languageName: node + linkType: hard + "is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -8840,6 +9607,15 @@ __metadata: languageName: node linkType: hard +"is-wsl@npm:^2.1.1": + version: 2.2.0 + resolution: "is-wsl@npm:2.2.0" + dependencies: + is-docker: "npm:^2.0.0" + checksum: 10c0/a6fa2d370d21be487c0165c7a440d567274fbba1a817f2f0bfa41cc5e3af25041d84267baa22df66696956038a43973e72fca117918c91431920bdef490fa25e + languageName: node + linkType: hard + "isarray@npm:^2.0.5": version: 2.0.5 resolution: "isarray@npm:2.0.5" @@ -9502,6 +10278,13 @@ __metadata: languageName: node linkType: hard +"js-stringify@npm:^1.0.2": + version: 1.0.2 + resolution: "js-stringify@npm:1.0.2" + checksum: 10c0/a450c04fde3a7e1c27f1c3c4300433f8d79322f9e3c2e76266843cef8c0b5a69b5f11b5f173212b2f15f2df09e068ef7ddf46ef775e2486f3006a6f4e912578d + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -9684,6 +10467,16 @@ __metadata: languageName: node linkType: hard +"jstransformer@npm:1.0.0": + version: 1.0.0 + resolution: "jstransformer@npm:1.0.0" + dependencies: + is-promise: "npm:^2.0.0" + promise: "npm:^7.0.1" + checksum: 10c0/11f9b4f368a55878dd7973154cd83b0adca27f974d21217728652530775b2bec281e92109de66f0c9e37c76af796d5b76b33f3e38363214a83d102d523a7285b + languageName: node + linkType: hard + "jsx-ast-utils@npm:^2.4.1 || ^3.0.0, jsx-ast-utils@npm:^3.3.5": version: 3.3.5 resolution: "jsx-ast-utils@npm:3.3.5" @@ -9696,6 +10489,21 @@ __metadata: languageName: node linkType: hard +"juice@npm:^8.0.0": + version: 8.1.0 + resolution: "juice@npm:8.1.0" + dependencies: + cheerio: "npm:1.0.0-rc.10" + commander: "npm:^6.1.0" + mensch: "npm:^0.3.4" + slick: "npm:^1.12.2" + web-resource-inliner: "npm:^6.0.1" + bin: + juice: bin/juice + checksum: 10c0/c67d328d5865ad32e938708bf38608bd0c856d9c9e226e43f293b049a3ae37340323c6e746288166a108e5ee7c0d00c2480d769c455094d9a7f0205534e44246 + languageName: node + linkType: hard + "just-diff-apply@npm:^5.2.0": version: 5.5.0 resolution: "just-diff-apply@npm:5.5.0" @@ -9779,6 +10587,13 @@ __metadata: languageName: node linkType: hard +"leac@npm:^0.6.0": + version: 0.6.0 + resolution: "leac@npm:0.6.0" + checksum: 10c0/5257781e10791ef8462eb1cbe5e48e3cda7692486f2a775265d6f5216cc088960c62f138163b8df0dcf2119d18673bfe7b050d6b41543d92a7b7ac90e4eb1e8b + languageName: node + linkType: hard + "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -9796,6 +10611,25 @@ __metadata: languageName: node linkType: hard +"libbase64@npm:1.3.0": + version: 1.3.0 + resolution: "libbase64@npm:1.3.0" + checksum: 10c0/4ece76ce09fa389d0c578c83a121c16452916521b177f50c3e8637dd9919170c96c12e1c7de63b1c88da8e5aa7e7ab574c26d18f1f64666f46b358b4b5873c8b + languageName: node + linkType: hard + +"libmime@npm:5.3.6": + version: 5.3.6 + resolution: "libmime@npm:5.3.6" + dependencies: + encoding-japanese: "npm:2.2.0" + iconv-lite: "npm:0.6.3" + libbase64: "npm:1.3.0" + libqp: "npm:2.1.1" + checksum: 10c0/54afa19f3500fe14b7562fd055518f1d82bf0f1076690a9b097d1a126322de7425d6d29aa9db9b51bc41b2ed16eacff65d7f30cd3622f7010f60993dfc79041e + languageName: node + linkType: hard + "libnpmaccess@npm:^8.0.1": version: 8.0.2 resolution: "libnpmaccess@npm:8.0.2" @@ -9931,6 +10765,13 @@ __metadata: languageName: node linkType: hard +"libqp@npm:2.1.1": + version: 2.1.1 + resolution: "libqp@npm:2.1.1" + checksum: 10c0/6e78f0676cd2424b3ddbf3273ab8539871299310dba433b7e2ec10a41830acecb4d074ea8b78b706dea349996f011ce519d92f81ede712c4824a2dd402aa376c + languageName: node + linkType: hard + "lilconfig@npm:^3.0.0": version: 3.0.0 resolution: "lilconfig@npm:3.0.0" @@ -9952,6 +10793,15 @@ __metadata: languageName: node linkType: hard +"linkify-it@npm:5.0.0": + version: 5.0.0 + resolution: "linkify-it@npm:5.0.0" + dependencies: + uc.micro: "npm:^2.0.0" + checksum: 10c0/ff4abbcdfa2003472fc3eb4b8e60905ec97718e11e33cca52059919a4c80cc0e0c2a14d23e23d8c00e5402bc5a885cdba8ca053a11483ab3cc8b3c7a52f88e2d + languageName: node + linkType: hard + "load-json-file@npm:^4.0.0": version: 4.0.0 resolution: "load-json-file@npm:4.0.0" @@ -10087,8 +10937,8 @@ __metadata: linkType: hard "logform@npm:^2.3.2": - version: 2.6.1 - resolution: "logform@npm:2.6.1" + version: 2.6.0 + resolution: "logform@npm:2.6.0" dependencies: "@colors/colors": "npm:1.6.0" "@types/triple-beam": "npm:^1.3.2" @@ -10096,7 +10946,7 @@ __metadata: ms: "npm:^2.1.1" safe-stable-stringify: "npm:^2.3.1" triple-beam: "npm:^1.3.0" - checksum: 10c0/c20019336b1da8c08adea67dd7de2b0effdc6e35289c0156722924b571df94ba9f900ef55620c56bceb07cae7cc46057c9859accdee37a131251ba34d6789bce + checksum: 10c0/6e02f8617a03155b2fce451bacf777a2c01da16d32c4c745b3ec85be6c3f2602f2a4953a8bd096441cb4c42c447b52318541d6b6bc335dce903cb9ad77a1749f languageName: node linkType: hard @@ -10180,6 +11030,35 @@ __metadata: languageName: node linkType: hard +"mailparser@npm:^3.7.1": + version: 3.7.2 + resolution: "mailparser@npm:3.7.2" + dependencies: + encoding-japanese: "npm:2.2.0" + he: "npm:1.2.0" + html-to-text: "npm:9.0.5" + iconv-lite: "npm:0.6.3" + libmime: "npm:5.3.6" + linkify-it: "npm:5.0.0" + mailsplit: "npm:5.4.2" + nodemailer: "npm:6.9.16" + punycode.js: "npm:2.3.1" + tlds: "npm:1.255.0" + checksum: 10c0/dc37e0e2d6885f777cc4cf40d1fc60ca247632428a55986a6f665705a8fca762de5bf8b3b14fd4eac97d6d8a866827f4c99400d25c7eb9879d7906871a3381c0 + languageName: node + linkType: hard + +"mailsplit@npm:5.4.2": + version: 5.4.2 + resolution: "mailsplit@npm:5.4.2" + dependencies: + libbase64: "npm:1.3.0" + libmime: "npm:5.3.6" + libqp: "npm:2.1.1" + checksum: 10c0/14fb8722b7416193bc7af918d8529ad2c2edb471b78c7f26116c95a8a325c33355504ba83c95dbac4ce69a3a2557bb9e0a9b2a0c8585ab5df892b51159a99e52 + languageName: node + linkType: hard + "maintainerr@workspace:.": version: 0.0.0-use.local resolution: "maintainerr@workspace:." @@ -10255,6 +11134,13 @@ __metadata: languageName: node linkType: hard +"make-plural@npm:^7.0.0": + version: 7.4.0 + resolution: "make-plural@npm:7.4.0" + checksum: 10c0/10f24e932865c88d4cdb14d4bb809ce8e615cae7d083c63ad008b3bcaf0d22941ca0bd76acd9cdd7ae4047563501104fff3c37ce8ad2bc5671facde1818fa57f + languageName: node + linkType: hard + "makeerror@npm:1.0.12": version: 1.0.12 resolution: "makeerror@npm:1.0.12" @@ -10289,6 +11175,13 @@ __metadata: languageName: node linkType: hard +"math-interval-parser@npm:^2.0.1": + version: 2.0.1 + resolution: "math-interval-parser@npm:2.0.1" + checksum: 10c0/7e58389f603a4c3303d691e4feacea981c83c7bfc332741c8e5abc49a3e9b7364d8a141dc5ca822287c99d8cb9b76b6d5d1511662dae02faf126e2521da8348e + languageName: node + linkType: hard + "math-intrinsics@npm:^1.0.0, math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" @@ -10447,6 +11340,13 @@ __metadata: languageName: node linkType: hard +"mensch@npm:^0.3.4": + version: 0.3.4 + resolution: "mensch@npm:0.3.4" + checksum: 10c0/177f9c1cb1acd93da98a971288a5da99f819ac06de19ca450040b18ddf8728c7ae0ce22309fadbbfd4ceb773bc5c03bf1cb93ceb91441da9e76e010d314da2ea + languageName: node + linkType: hard + "meow@npm:^13.0.0": version: 13.2.0 resolution: "meow@npm:13.2.0" @@ -10760,6 +11660,15 @@ __metadata: languageName: node linkType: hard +"mime@npm:^2.4.6": + version: 2.6.0 + resolution: "mime@npm:2.6.0" + bin: + mime: cli.js + checksum: 10c0/a7f2589900d9c16e3bdf7672d16a6274df903da958c1643c9c45771f0478f3846dcb1097f31eb9178452570271361e2149310931ec705c037210fc69639c8e6c + languageName: node + linkType: hard + "mime@npm:^4.0.0": version: 4.0.1 resolution: "mime@npm:4.0.1" @@ -10799,6 +11708,13 @@ __metadata: languageName: node linkType: hard +"minimalistic-assert@npm:^1.0.0": + version: 1.0.1 + resolution: "minimalistic-assert@npm:1.0.1" + checksum: 10c0/96730e5601cd31457f81a296f521eb56036e6f69133c0b18c13fe941109d53ad23a4204d946a0d638d7f3099482a0cec8c9bb6d642604612ce43ee536be3dddd + languageName: node + linkType: hard + "minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -11010,6 +11926,13 @@ __metadata: languageName: node linkType: hard +"moo@npm:^0.5.0, moo@npm:^0.5.1": + version: 0.5.2 + resolution: "moo@npm:0.5.2" + checksum: 10c0/a9d9ad8198a51fe35d297f6e9fdd718298ca0b39a412e868a0ebd92286379ab4533cfc1f1f34516177f5129988ab25fe598f78e77c84e3bfe0d4a877b56525a8 + languageName: node + linkType: hard + "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -11046,6 +11969,28 @@ __metadata: languageName: node linkType: hard +"multimatch@npm:5": + version: 5.0.0 + resolution: "multimatch@npm:5.0.0" + dependencies: + "@types/minimatch": "npm:^3.0.3" + array-differ: "npm:^3.0.0" + array-union: "npm:^2.1.0" + arrify: "npm:^2.0.1" + minimatch: "npm:^3.0.4" + checksum: 10c0/252ffae6d19491c169c22fc30cf8a99f6031f94a3495f187d3430b06200e9f05a7efae90ab9d834f090834e0d9c979ab55e7ad21f61a37995d807b4b0ccdcbd1 + languageName: node + linkType: hard + +"mustache@npm:^4.2.0": + version: 4.2.0 + resolution: "mustache@npm:4.2.0" + bin: + mustache: bin/mustache + checksum: 10c0/1f8197e8a19e63645a786581d58c41df7853da26702dbc005193e2437c98ca49b255345c173d50c08fe4b4dbb363e53cb655ecc570791f8deb09887248dd34a2 + languageName: node + linkType: hard + "mute-stream@npm:0.0.8": version: 0.0.8 resolution: "mute-stream@npm:0.0.8" @@ -11094,6 +12039,23 @@ __metadata: languageName: node linkType: hard +"nearley@npm:^2.20.1": + version: 2.20.1 + resolution: "nearley@npm:2.20.1" + dependencies: + commander: "npm:^2.19.0" + moo: "npm:^0.5.0" + railroad-diagrams: "npm:^1.0.0" + randexp: "npm:0.4.6" + bin: + nearley-railroad: bin/nearley-railroad.js + nearley-test: bin/nearley-test.js + nearley-unparse: bin/nearley-unparse.js + nearleyc: bin/nearleyc.js + checksum: 10c0/d25e1fd40b19c53a0ada6a688670f4a39063fd9553ab62885e81a82927d51572ce47193b946afa3d85efa608ba2c68f433c421f69b854bfb7f599eacb5fae37e + languageName: node + linkType: hard + "negotiator@npm:^0.6.2, negotiator@npm:^0.6.3": version: 0.6.4 resolution: "negotiator@npm:0.6.4" @@ -11195,6 +12157,13 @@ __metadata: languageName: node linkType: hard +"nice-try@npm:^1.0.4": + version: 1.0.5 + resolution: "nice-try@npm:1.0.5" + checksum: 10c0/95568c1b73e1d0d4069a3e3061a2102d854513d37bcfda73300015b7ba4868d3b27c198d1dbbd8ebdef4112fc2ed9e895d4a0f2e1cce0bd334f2a1346dc9205f + languageName: node + linkType: hard + "node-abi@npm:^3.3.0": version: 3.54.0 resolution: "node-abi@npm:3.54.0" @@ -11250,6 +12219,20 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^2.6.0": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8 + languageName: node + linkType: hard + "node-gyp@npm:8.x": version: 8.4.1 resolution: "node-gyp@npm:8.4.1" @@ -11304,6 +12287,27 @@ __metadata: languageName: node linkType: hard +"nodemailer@npm:6.9.1": + version: 6.9.1 + resolution: "nodemailer@npm:6.9.1" + checksum: 10c0/adeaef441e31025776e8b1fa818e4b260850fbb60150f6ff7210e4de443bb32f0881a8a4967c42bb108d620f68f18369a42db29fe7cdec5e4ec09b0ddd51b19c + languageName: node + linkType: hard + +"nodemailer@npm:6.9.16": + version: 6.9.16 + resolution: "nodemailer@npm:6.9.16" + checksum: 10c0/9fd73ab4ab5b81544c3c9820afbe386369aba442f997b2f58d171222a898a7aed580fc100bfe6eebc194f18ba6e169d67ee40ca64d32d69022d89e575cef97a4 + languageName: node + linkType: hard + +"nodemailer@npm:^6.7.2, nodemailer@npm:^6.9.13": + version: 6.10.0 + resolution: "nodemailer@npm:6.10.0" + checksum: 10c0/39fd35d65b021b94c968eeac82a66dd843021b6ba53c659d01b1dd4cda73b6a2f96e20facfe500efa4b8d3f1cb23df10245c6c86b2bde5f806691ed17ce87826 + languageName: node + linkType: hard + "nopt@npm:^5.0.0": version: 5.0.0 resolution: "nopt@npm:5.0.0" @@ -11449,6 +12453,15 @@ __metadata: languageName: node linkType: hard +"npm-run-path@npm:^2.0.0": + version: 2.0.2 + resolution: "npm-run-path@npm:2.0.2" + dependencies: + path-key: "npm:^2.0.0" + checksum: 10c0/95549a477886f48346568c97b08c4fda9cdbf7ce8a4fbc2213f36896d0d19249e32d68d7451bdcbca8041b5fba04a6b2c4a618beaf19849505c05b700740f1de + languageName: node + linkType: hard + "npm-run-path@npm:^4.0.1": version: 4.0.1 resolution: "npm-run-path@npm:4.0.1" @@ -11579,6 +12592,15 @@ __metadata: languageName: node linkType: hard +"nth-check@npm:^2.0.1": + version: 2.1.1 + resolution: "nth-check@npm:2.1.1" + dependencies: + boolbase: "npm:^1.0.0" + checksum: 10c0/5fee7ff309727763689cfad844d979aedd2204a817fbaaf0e1603794a7c20db28548d7b024692f953557df6ce4a0ee4ae46cd8ebd9b36cfb300b9226b567c479 + languageName: node + linkType: hard + "oauth-sign@npm:~0.9.0": version: 0.9.0 resolution: "oauth-sign@npm:0.9.0" @@ -11749,6 +12771,25 @@ __metadata: languageName: node linkType: hard +"open@npm:7": + version: 7.4.2 + resolution: "open@npm:7.4.2" + dependencies: + is-docker: "npm:^2.0.0" + is-wsl: "npm:^2.1.1" + checksum: 10c0/77573a6a68f7364f3a19a4c80492712720746b63680ee304555112605ead196afe91052bd3c3d165efdf4e9d04d255e87de0d0a77acec11ef47fd5261251813f + languageName: node + linkType: hard + +"openpgp@npm:^5.7.0": + version: 5.11.2 + resolution: "openpgp@npm:5.11.2" + dependencies: + asn1.js: "npm:^5.0.0" + checksum: 10c0/e16141ef507789b080e2b8c94a3081773ca27cf4782a23d4dbcf80cc6394c8ff1d4d64e072c82fd59c81eedc20c57fdbaca109974733a6e0316d869aa8d7fd27 + languageName: node + linkType: hard + "optionator@npm:^0.9.3": version: 0.9.3 resolution: "optionator@npm:0.9.3" @@ -11805,6 +12846,15 @@ __metadata: languageName: node linkType: hard +"p-event@npm:4.2.0": + version: 4.2.0 + resolution: "p-event@npm:4.2.0" + dependencies: + p-timeout: "npm:^3.1.0" + checksum: 10c0/f1b6a2fb13d47f2a8afc00150da5ece0d28940ce3d8fa562873e091d3337d298e78fee9cb18b768598ff1d11df608b2ae23868309ff6405b864a2451ccd6d25a + languageName: node + linkType: hard + "p-filter@npm:^4.0.0": version: 4.1.0 resolution: "p-filter@npm:4.1.0" @@ -11814,6 +12864,13 @@ __metadata: languageName: node linkType: hard +"p-finally@npm:^1.0.0": + version: 1.0.0 + resolution: "p-finally@npm:1.0.0" + checksum: 10c0/6b8552339a71fe7bd424d01d8451eea92d379a711fc62f6b2fe64cad8a472c7259a236c9a22b4733abca0b5666ad503cb497792a0478c5af31ded793d00937e7 + languageName: node + linkType: hard + "p-is-promise@npm:^3.0.0": version: 3.0.0 resolution: "p-is-promise@npm:3.0.0" @@ -11905,6 +12962,15 @@ __metadata: languageName: node linkType: hard +"p-timeout@npm:^3.0.0, p-timeout@npm:^3.1.0": + version: 3.2.0 + resolution: "p-timeout@npm:3.2.0" + dependencies: + p-finally: "npm:^1.0.0" + checksum: 10c0/524b393711a6ba8e1d48137c5924749f29c93d70b671e6db761afa784726572ca06149c715632da8f70c090073afb2af1c05730303f915604fd38ee207b70a61 + languageName: node + linkType: hard + "p-try@npm:^1.0.0": version: 1.0.0 resolution: "p-try@npm:1.0.0" @@ -11919,6 +12985,15 @@ __metadata: languageName: node linkType: hard +"p-wait-for@npm:3.2.0": + version: 3.2.0 + resolution: "p-wait-for@npm:3.2.0" + dependencies: + p-timeout: "npm:^3.0.0" + checksum: 10c0/38261cfac4af3ca25b0b5b9d44ef88366cef2fdd16c7a72c27c97fcab5dc8948593b6983f839dee5342316b7d24b523d4499ed834214c66619a717442e1b08f0 + languageName: node + linkType: hard + "package-json-from-dist@npm:^1.0.0": version: 1.0.0 resolution: "package-json-from-dist@npm:1.0.0" @@ -12029,7 +13104,7 @@ __metadata: languageName: node linkType: hard -"parse5-htmlparser2-tree-adapter@npm:^6.0.0": +"parse5-htmlparser2-tree-adapter@npm:^6.0.0, parse5-htmlparser2-tree-adapter@npm:^6.0.1": version: 6.0.1 resolution: "parse5-htmlparser2-tree-adapter@npm:6.0.1" dependencies: @@ -12052,6 +13127,26 @@ __metadata: languageName: node linkType: hard +"parseley@npm:^0.12.0": + version: 0.12.1 + resolution: "parseley@npm:0.12.1" + dependencies: + leac: "npm:^0.6.0" + peberminta: "npm:^0.9.0" + checksum: 10c0/df3de74172b72305b867298a71e5882c413df75d30f2bafb5fb70779dfd349c5e4db03441fbf8ca83da8e4aa72bd0ef2b5c73086c4825d27d1c649d61bc0bcc0 + languageName: node + linkType: hard + +"parseley@npm:^0.7.0": + version: 0.7.0 + resolution: "parseley@npm:0.7.0" + dependencies: + moo: "npm:^0.5.1" + nearley: "npm:^2.20.1" + checksum: 10c0/70205aa7ca5378bb14c2b4d59a981144f026687aaaccffdbd28ce00e02d6041fbb8674a37e886dd2f64c14b2daf028ee8a416ef45d9fbac9f1adfb830be9c77f + languageName: node + linkType: hard + "parseurl@npm:^1.3.3, parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" @@ -12080,6 +13175,13 @@ __metadata: languageName: node linkType: hard +"path-key@npm:^2.0.0, path-key@npm:^2.0.1": + version: 2.0.1 + resolution: "path-key@npm:2.0.1" + checksum: 10c0/dd2044f029a8e58ac31d2bf34c34b93c3095c1481942960e84dd2faa95bbb71b9b762a106aead0646695330936414b31ca0bd862bf488a937ad17c8c5d73b32b + languageName: node + linkType: hard + "path-key@npm:^3.0.0, path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" @@ -12142,6 +13244,13 @@ __metadata: languageName: node linkType: hard +"peberminta@npm:^0.9.0": + version: 0.9.0 + resolution: "peberminta@npm:0.9.0" + checksum: 10c0/59c2c39269d9f7f559cf44582f1c0503524c6a9bc3478e0309adba2b41c71ab98745a239a4e6f98f46105291256e6d8f12ae9860d9f016b1c9a6f52c0b63bfe7 + languageName: node + linkType: hard + "performance-now@npm:^2.1.0": version: 2.1.0 resolution: "performance-now@npm:2.1.0" @@ -12205,6 +13314,13 @@ __metadata: languageName: node linkType: hard +"pify@npm:^4.0.1": + version: 4.0.1 + resolution: "pify@npm:4.0.1" + checksum: 10c0/6f9d404b0d47a965437403c9b90eca8bb2536407f03de165940e62e72c8c8b75adda5516c6b9b23675a5877cc0bcac6bdfb0ef0e39414cd2476d5495da40e7cf + languageName: node + linkType: hard + "pirates@npm:^4.0.1, pirates@npm:^4.0.4": version: 4.0.6 resolution: "pirates@npm:4.0.6" @@ -12369,13 +13485,13 @@ __metadata: linkType: hard "postcss@npm:^8.4.47": - version: 8.4.49 - resolution: "postcss@npm:8.4.49" + version: 8.4.47 + resolution: "postcss@npm:8.4.47" dependencies: nanoid: "npm:^3.3.7" - picocolors: "npm:^1.1.1" + picocolors: "npm:^1.1.0" source-map-js: "npm:^1.2.1" - checksum: 10c0/f1b3f17aaf36d136f59ec373459f18129908235e65dbdc3aee5eef8eba0756106f52de5ec4682e29a2eab53eb25170e7e871b3e4b52a8f1de3d344a514306be3 + checksum: 10c0/929f68b5081b7202709456532cee2a145c1843d391508c5a09de2517e8c4791638f71dd63b1898dba6712f8839d7a6da046c72a5e44c162e908f5911f57b5f44 languageName: node linkType: hard @@ -12506,6 +13622,25 @@ __metadata: languageName: node linkType: hard +"preview-email@npm:^3.0.5": + version: 3.1.0 + resolution: "preview-email@npm:3.1.0" + dependencies: + ci-info: "npm:^3.8.0" + display-notification: "npm:2.0.0" + fixpack: "npm:^4.0.0" + get-port: "npm:5.1.1" + mailparser: "npm:^3.7.1" + nodemailer: "npm:^6.9.13" + open: "npm:7" + p-event: "npm:4.2.0" + p-wait-for: "npm:3.2.0" + pug: "npm:^3.0.3" + uuid: "npm:^9.0.1" + checksum: 10c0/09bdcdfa357615e4d03c9b9cb5bcf65069b8457c2148b8c727b5208bfae622cdad1f69cbba491c9bfbe4fe1987a08138b15344b8ff15e8a199260a69534f4d08 + languageName: node + linkType: hard + "proc-log@npm:^3.0.0": version: 3.0.0 resolution: "proc-log@npm:3.0.0" @@ -12551,6 +13686,15 @@ __metadata: languageName: node linkType: hard +"promise@npm:^7.0.1": + version: 7.3.1 + resolution: "promise@npm:7.3.1" + dependencies: + asap: "npm:~2.0.3" + checksum: 10c0/742e5c0cc646af1f0746963b8776299701ad561ce2c70b49365d62c8db8ea3681b0a1bf0d4e2fe07910bf72f02d39e51e8e73dc8d7503c3501206ac908be107f + languageName: node + linkType: hard + "prompts@npm:^2.0.1": version: 2.4.2 resolution: "prompts@npm:2.4.2" @@ -12619,6 +13763,133 @@ __metadata: languageName: node linkType: hard +"pug-attrs@npm:^3.0.0": + version: 3.0.0 + resolution: "pug-attrs@npm:3.0.0" + dependencies: + constantinople: "npm:^4.0.1" + js-stringify: "npm:^1.0.2" + pug-runtime: "npm:^3.0.0" + checksum: 10c0/28178e91c05e8eb9130861c78dccc61eae3e1610931346065bd32ad0b08b023a8dcf2470c3b2409ba45a5098d6d7ed15687717e91cf77770c6381a18626e5194 + languageName: node + linkType: hard + +"pug-code-gen@npm:^3.0.3": + version: 3.0.3 + resolution: "pug-code-gen@npm:3.0.3" + dependencies: + constantinople: "npm:^4.0.1" + doctypes: "npm:^1.1.0" + js-stringify: "npm:^1.0.2" + pug-attrs: "npm:^3.0.0" + pug-error: "npm:^2.1.0" + pug-runtime: "npm:^3.0.1" + void-elements: "npm:^3.1.0" + with: "npm:^7.0.0" + checksum: 10c0/517a93930dbc80bc7fa5f60ff324229a07cc5ab70ed9d344ce105e2fe24de68db5121c8457a9ba99cdc8d48dd18779dd34956ebfcab009b3c1c6843a3cade109 + languageName: node + linkType: hard + +"pug-error@npm:^2.0.0, pug-error@npm:^2.1.0": + version: 2.1.0 + resolution: "pug-error@npm:2.1.0" + checksum: 10c0/bbce339b17fab9890de84975c0cd8723a847bf65f35653d3ebcf77018e8ad91529d56e978ab80f4c64c9f4f07ef9e56e7a9fda3be44249c344a93ba11fccff79 + languageName: node + linkType: hard + +"pug-filters@npm:^4.0.0": + version: 4.0.0 + resolution: "pug-filters@npm:4.0.0" + dependencies: + constantinople: "npm:^4.0.1" + jstransformer: "npm:1.0.0" + pug-error: "npm:^2.0.0" + pug-walk: "npm:^2.0.0" + resolve: "npm:^1.15.1" + checksum: 10c0/7ddd62f5eb97f5242858bd56d93ffed387fef3742210a53770c980020cf91a34384b84b7fc8f0de185b43dfa77de2c4d0f63f575a4c5b3887fdef4e64b8d559d + languageName: node + linkType: hard + +"pug-lexer@npm:^5.0.1": + version: 5.0.1 + resolution: "pug-lexer@npm:5.0.1" + dependencies: + character-parser: "npm:^2.2.0" + is-expression: "npm:^4.0.0" + pug-error: "npm:^2.0.0" + checksum: 10c0/24195a5681953ab91c6a3ccd80a643f760dddb65e2f266bf8ccba145018ba0271536efe1572de2c2224163eb00873c2f1df0ad7ea7aa8bcbf79a66b586ca8435 + languageName: node + linkType: hard + +"pug-linker@npm:^4.0.0": + version: 4.0.0 + resolution: "pug-linker@npm:4.0.0" + dependencies: + pug-error: "npm:^2.0.0" + pug-walk: "npm:^2.0.0" + checksum: 10c0/db754ff34cdd4ba9d9e2d9535cce2a74178f2172e848a5fa6381907cb5bfaa0d39d4cc3eb29893d35fc1c417e83ae3cfd434640ba7d3b635c63199104fae976c + languageName: node + linkType: hard + +"pug-load@npm:^3.0.0": + version: 3.0.0 + resolution: "pug-load@npm:3.0.0" + dependencies: + object-assign: "npm:^4.1.1" + pug-walk: "npm:^2.0.0" + checksum: 10c0/2a7659dfaf9872dd25d851f85e4c27fa447d907b1db3540030cd844614159ff181e067d8f2bedf90eb6b5b1ff03747253859ecbbb822e40f4834b15591d4e108 + languageName: node + linkType: hard + +"pug-parser@npm:^6.0.0": + version: 6.0.0 + resolution: "pug-parser@npm:6.0.0" + dependencies: + pug-error: "npm:^2.0.0" + token-stream: "npm:1.0.0" + checksum: 10c0/faa6cec43afdeb2705eb8c68dfdb2e65836238df8043ae55295ffb72450b8c7a990ea1be60adbde19f58988b9e1d18a84ea42453e2c4f104d0031f78fda737b2 + languageName: node + linkType: hard + +"pug-runtime@npm:^3.0.0, pug-runtime@npm:^3.0.1": + version: 3.0.1 + resolution: "pug-runtime@npm:3.0.1" + checksum: 10c0/0db8166d2e17695a6941d1de81dcb21c8a52921299b1e03bf6a0a3d2b0036b51cf98101b3937b731c745e8d3e0268cb0b728c02f61a80a25fcfaa15c594fb1be + languageName: node + linkType: hard + +"pug-strip-comments@npm:^2.0.0": + version: 2.0.0 + resolution: "pug-strip-comments@npm:2.0.0" + dependencies: + pug-error: "npm:^2.0.0" + checksum: 10c0/ca498adedaeba51dd836b20129bbd161e2d5a397a2baaa553b1e74e888caa2258dcd7326396fc6f8fed8c7b7f906cfebc4c386ccbee8888a27b2ca0d4d86d206 + languageName: node + linkType: hard + +"pug-walk@npm:^2.0.0": + version: 2.0.0 + resolution: "pug-walk@npm:2.0.0" + checksum: 10c0/005d63177bcf057f5a618b182f6d4600afb039200b07a381a0d89288a2b3126e763a0a6c40b758eab0731c8e63cad1bbcb46d96803b9ae9cfc879f6ef5a0f8f4 + languageName: node + linkType: hard + +"pug@npm:^3.0.3": + version: 3.0.3 + resolution: "pug@npm:3.0.3" + dependencies: + pug-code-gen: "npm:^3.0.3" + pug-filters: "npm:^4.0.0" + pug-lexer: "npm:^5.0.1" + pug-linker: "npm:^4.0.0" + pug-load: "npm:^3.0.0" + pug-parser: "npm:^6.0.0" + pug-runtime: "npm:^3.0.1" + pug-strip-comments: "npm:^2.0.0" + checksum: 10c0/bda53d3a6deea1d348cd5ab17427c77f3d74165510ad16f4fd182cc63618ad09388ecda317d17122ee890c8a68f9a54b96221fce7f44a332e463fdbb10a9d1e2 + languageName: node + linkType: hard + "pump@npm:^3.0.0": version: 3.0.0 resolution: "pump@npm:3.0.0" @@ -12629,6 +13900,13 @@ __metadata: languageName: node linkType: hard +"punycode.js@npm:2.3.1": + version: 2.3.1 + resolution: "punycode.js@npm:2.3.1" + checksum: 10c0/1d12c1c0e06127fa5db56bd7fdf698daf9a78104456a6b67326877afc21feaa821257b171539caedd2f0524027fa38e67b13dd094159c8d70b6d26d2bea4dfdb + languageName: node + linkType: hard + "punycode@npm:^2.1.0, punycode@npm:^2.1.1": version: 2.3.1 resolution: "punycode@npm:2.3.1" @@ -12661,6 +13939,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.10.3": + version: 6.14.0 + resolution: "qs@npm:6.14.0" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10c0/8ea5d91bf34f440598ee389d4a7d95820e3b837d3fd9f433871f7924801becaa0cd3b3b4628d49a7784d06a8aea9bc4554d2b6d8d584e2d221dc06238a42909c + languageName: node + linkType: hard + "qs@npm:~6.5.2": version: 6.5.3 resolution: "qs@npm:6.5.3" @@ -12675,6 +13962,23 @@ __metadata: languageName: node linkType: hard +"railroad-diagrams@npm:^1.0.0": + version: 1.0.0 + resolution: "railroad-diagrams@npm:1.0.0" + checksum: 10c0/81bf8f86870a69fb9ed243102db9ad6416d09c4cb83964490d44717690e07dd982f671503236a1f8af28f4cb79d5d7a87613930f10ac08defa845ceb6764e364 + languageName: node + linkType: hard + +"randexp@npm:0.4.6": + version: 0.4.6 + resolution: "randexp@npm:0.4.6" + dependencies: + discontinuous-range: "npm:1.0.0" + ret: "npm:~0.1.10" + checksum: 10c0/14ee14b6d7f5ce69609b51cc914fb7a7c82ad337820a141c5f762c5ad1fe868f5191ea6e82359aee019b625ee1359486628fa833909d12c3b5dd9571908c3345 + languageName: node + linkType: hard + "randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0" @@ -13170,6 +14474,19 @@ __metadata: languageName: node linkType: hard +"resolve@npm:^1.15.1": + version: 1.22.10 + resolution: "resolve@npm:1.22.10" + dependencies: + is-core-module: "npm:^2.16.0" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10c0/8967e1f4e2cc40f79b7e080b4582b9a8c5ee36ffb46041dccb20e6461161adf69f843b43067b4a375de926a2cd669157e29a29578191def399dd5ef89a1b5203 + languageName: node + linkType: hard + "resolve@npm:^2.0.0-next.5": version: 2.0.0-next.5 resolution: "resolve@npm:2.0.0-next.5" @@ -13196,6 +14513,19 @@ __metadata: languageName: node linkType: hard +"resolve@patch:resolve@npm%3A^1.15.1#optional!builtin": + version: 1.22.10 + resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin::version=1.22.10&hash=c3c19d" + dependencies: + is-core-module: "npm:^2.16.0" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10c0/52a4e505bbfc7925ac8f4cd91fd8c4e096b6a89728b9f46861d3b405ac9a1ccf4dcbf8befb4e89a2e11370dacd0160918163885cbc669369590f2f31f4c58939 + languageName: node + linkType: hard + "resolve@patch:resolve@npm%3A^2.0.0-next.5#optional!builtin": version: 2.0.0-next.5 resolution: "resolve@patch:resolve@npm%3A2.0.0-next.5#optional!builtin::version=2.0.0-next.5&hash=c3c19d" @@ -13219,6 +14549,13 @@ __metadata: languageName: node linkType: hard +"ret@npm:~0.1.10": + version: 0.1.15 + resolution: "ret@npm:0.1.15" + checksum: 10c0/01f77cad0f7ea4f955852c03d66982609893edc1240c0c964b4c9251d0f9fb6705150634060d169939b096d3b77f4c84d6b6098a5b5d340160898c8581f1f63f + languageName: node + linkType: hard + "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -13259,6 +14596,15 @@ __metadata: languageName: node linkType: hard +"run-applescript@npm:^3.0.0": + version: 3.2.0 + resolution: "run-applescript@npm:3.2.0" + dependencies: + execa: "npm:^0.10.0" + checksum: 10c0/5f5a75de0bed917d7673125fb8631ff1d50c898d4f0b8fff57e6508dafe7a2529e1cfd7a3c3e4f981aec1294c95b4e37124fd33cd8cd54fbca1e2b9c39d77473 + languageName: node + linkType: hard + "run-async@npm:^2.4.0": version: 2.4.1 resolution: "run-async@npm:2.4.1" @@ -13342,6 +14688,13 @@ __metadata: languageName: node linkType: hard +"safe-identifier@npm:^0.4.1": + version: 0.4.2 + resolution: "safe-identifier@npm:0.4.2" + checksum: 10c0/a6b0cdb5347e48c5ea4ddf4cdca5359b12529a11a7368225c39f882fcc0e679c81e82e3b13e36bd27ba7bdec9286f4cc062e3e527464d93ba61290b6e0bc6747 + languageName: node + linkType: hard + "safe-push-apply@npm:^1.0.0": version: 1.0.0 resolution: "safe-push-apply@npm:1.0.0" @@ -13426,6 +14779,24 @@ __metadata: languageName: node linkType: hard +"selderee@npm:^0.11.0": + version: 0.11.0 + resolution: "selderee@npm:0.11.0" + dependencies: + parseley: "npm:^0.12.0" + checksum: 10c0/c2ad8313a0dbf3c0b74752a8d03cfbc0931ae77a36679cdb64733eb732c1762f95a5174249bf7e8b8103874cb0e013a030f9c8b72f5d41e62f1d847d4a845d39 + languageName: node + linkType: hard + +"selderee@npm:^0.6.0": + version: 0.6.0 + resolution: "selderee@npm:0.6.0" + dependencies: + parseley: "npm:^0.7.0" + checksum: 10c0/bd1f305e38aeca3ffe7f0f785a850b0426d8d56641abd7ed24855b7776730a512ec4114468b43621adcb6d71666b5a088c723bc7edc033d06c0a518cdd866809 + languageName: node + linkType: hard + "semantic-release@npm:^24.2.1": version: 24.2.1 resolution: "semantic-release@npm:24.2.1" @@ -13481,6 +14852,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^5.5.0": + version: 5.7.2 + resolution: "semver@npm:5.7.2" + bin: + semver: bin/semver + checksum: 10c0/e4cf10f86f168db772ae95d86ba65b3fd6c5967c94d97c708ccb463b778c2ee53b914cd7167620950fc07faf5a564e6efe903836639e512a1aa15fbc9667fa25 + languageName: node + linkType: hard + "semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" @@ -13696,6 +15076,15 @@ __metadata: languageName: node linkType: hard +"shebang-command@npm:^1.2.0": + version: 1.2.0 + resolution: "shebang-command@npm:1.2.0" + dependencies: + shebang-regex: "npm:^1.0.0" + checksum: 10c0/7b20dbf04112c456b7fc258622dafd566553184ac9b6938dd30b943b065b21dabd3776460df534cc02480db5e1b6aec44700d985153a3da46e7db7f9bd21326d + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -13705,6 +15094,13 @@ __metadata: languageName: node linkType: hard +"shebang-regex@npm:^1.0.0": + version: 1.0.0 + resolution: "shebang-regex@npm:1.0.0" + checksum: 10c0/9abc45dee35f554ae9453098a13fdc2f1730e525a5eb33c51f096cc31f6f10a4b38074c1ebf354ae7bffa7229506083844008dfc3bb7818228568c0b2dc1fff2 + languageName: node + linkType: hard + "shebang-regex@npm:^3.0.0": version: 3.0.0 resolution: "shebang-regex@npm:3.0.0" @@ -13772,7 +15168,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 @@ -13868,6 +15264,13 @@ __metadata: languageName: node linkType: hard +"slick@npm:^1.12.2": + version: 1.12.2 + resolution: "slick@npm:1.12.2" + checksum: 10c0/fea97c36b2bdcd1b80caea150cd8135dc9d3ffe659bbe04fa6f4b4dff373f5d5aef09a8ef384b331c3fdd9567faf447b75b850ab35d2c69ff8a8a92def3d49e1 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -14368,6 +15771,13 @@ __metadata: languageName: node linkType: hard +"strip-eof@npm:^1.0.0": + version: 1.0.0 + resolution: "strip-eof@npm:1.0.0" + checksum: 10c0/f336beed8622f7c1dd02f2cbd8422da9208fae81daf184f73656332899978919d5c0ca84dc6cfc49ad1fc4dd7badcde5412a063cf4e0d7f8ed95a13a63f68f45 + languageName: node + linkType: hard + "strip-final-newline@npm:^2.0.0": version: 2.0.0 resolution: "strip-final-newline@npm:2.0.0" @@ -14745,6 +16155,22 @@ __metadata: languageName: node linkType: hard +"titleize@npm:2": + version: 2.1.0 + resolution: "titleize@npm:2.1.0" + checksum: 10c0/5f7786ec5fa7813a7ecff9d2134e218bdee6b046b159471f647e8c1b904ec0fc8f6cdd42ca2af34f589572c7f05ca40a353c233b0d97a13e433068606059f00f + languageName: node + linkType: hard + +"tlds@npm:1.255.0, tlds@npm:^1.230.0": + version: 1.255.0 + resolution: "tlds@npm:1.255.0" + bin: + tlds: bin.js + checksum: 10c0/e7e0434142a2ee80e48c383db53cc94757a9fefd545230fad908a31c235f9f9b9cd1d8232d9bc2bd018050f0e8a912c141d0c79289dda852169199892ba847d0 + languageName: node + linkType: hard + "tmp@npm:^0.0.33": version: 0.0.33 resolution: "tmp@npm:0.0.33" @@ -14784,6 +16210,13 @@ __metadata: languageName: node linkType: hard +"token-stream@npm:1.0.0": + version: 1.0.0 + resolution: "token-stream@npm:1.0.0" + checksum: 10c0/c1924a89686fc035d579cbe856da12306571d5fe7408eeeebe80df7c25c5cc644b8ae102d5cbc0f085d0e105f391d1a48dc0e568520434c5b444ea6c7de2b822 + languageName: node + linkType: hard + "tough-cookie@npm:^2.3.3, tough-cookie@npm:~2.5.0": version: 2.5.0 resolution: "tough-cookie@npm:2.5.0" @@ -14794,6 +16227,13 @@ __metadata: languageName: node linkType: hard +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11 + languageName: node + linkType: hard + "traverse@npm:~0.6.6": version: 0.6.8 resolution: "traverse@npm:0.6.8" @@ -14973,7 +16413,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.8.1, tslib@npm:^2.8.0": +"tslib@npm:2.8.1, tslib@npm:^2.2.0, tslib@npm:^2.8.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 @@ -15436,6 +16876,13 @@ __metadata: languageName: node linkType: hard +"uc.micro@npm:^2.0.0": + version: 2.1.0 + resolution: "uc.micro@npm:2.1.0" + checksum: 10c0/8862eddb412dda76f15db8ad1c640ccc2f47cdf8252a4a30be908d535602c8d33f9855dfcccb8b8837855c1ce1eaa563f7fa7ebe3c98fd0794351aab9b9c55fa + languageName: node + linkType: hard + "uglify-js@npm:^3.1.4": version: 3.17.4 resolution: "uglify-js@npm:3.17.4" @@ -15478,10 +16925,26 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.19.8": - version: 6.19.8 - resolution: "undici-types@npm:6.19.8" - checksum: 10c0/078afa5990fba110f6824823ace86073b4638f1d5112ee26e790155f481f2a868cc3e0615505b6f4282bdf74a3d8caad715fd809e870c2bb0704e3ea6082f344 +"underscore.deep@npm:~0.5.1": + version: 0.5.3 + resolution: "underscore.deep@npm:0.5.3" + peerDependencies: + underscore: 1.x + checksum: 10c0/e11e05a456118d9d78fcaa1f892f29fdd74051c249f351052ed75a3124df3ecd0fd3bbb4f56b01c63f251514873ea862d54db131034a3abdcd815cb50861416a + languageName: node + linkType: hard + +"underscore@npm:~1.13.1": + version: 1.13.7 + resolution: "underscore@npm:1.13.7" + checksum: 10c0/fad2b4aac48847674aaf3c30558f383399d4fdafad6dd02dd60e4e1b8103b52c5a9e5937e0cc05dacfd26d6a0132ed0410ab4258241240757e4a4424507471cd + languageName: node + linkType: hard + +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 10c0/bb673d7876c2d411b6eb6c560e0c571eef4a01c1c19925175d16e3a30c4c428181fb8d7ae802a261f283e4166a0ac435e2f505743aa9e45d893f9a3df017b501 languageName: node linkType: hard @@ -15700,7 +17163,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.0": +"uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: @@ -15727,6 +17190,13 @@ __metadata: languageName: node linkType: hard +"valid-data-url@npm:^3.0.0": + version: 3.0.1 + resolution: "valid-data-url@npm:3.0.1" + checksum: 10c0/ffc7cac681976ca2db01003dc14286f75241309e90d96e505580469125c83c2de6b5203f0222226cb08f6daf0aff7de9855655c28a64e8590e7b58c01694a896 + languageName: node + linkType: hard + "validate-npm-package-license@npm:^3.0.4": version: 3.0.4 resolution: "validate-npm-package-license@npm:3.0.4" @@ -15784,6 +17254,13 @@ __metadata: languageName: node linkType: hard +"void-elements@npm:^3.1.0": + version: 3.1.0 + resolution: "void-elements@npm:3.1.0" + checksum: 10c0/0b8686f9f9aa44012e9bd5eabf287ae0cde409b9a2854c5a2335cb83920c957668ac5876e3f0d158dd424744ac411a7270e64128556b451ed3bec875ef18534d + languageName: node + linkType: hard + "walk-up-path@npm:^3.0.1": version: 3.0.1 resolution: "walk-up-path@npm:3.0.1" @@ -15819,6 +17296,27 @@ __metadata: languageName: node linkType: hard +"web-resource-inliner@npm:^6.0.1": + version: 6.0.1 + resolution: "web-resource-inliner@npm:6.0.1" + dependencies: + ansi-colors: "npm:^4.1.1" + escape-goat: "npm:^3.0.0" + htmlparser2: "npm:^5.0.0" + mime: "npm:^2.4.6" + node-fetch: "npm:^2.6.0" + valid-data-url: "npm:^3.0.0" + checksum: 10c0/b4b457de2448255100797b1eaefa0f62a8846b2452de8495b9ec17d3e223ebb4848a31b11a645e3541a5b114eb9f201219cda2f99d1b513631777f8c89d1c8a6 + languageName: node + linkType: hard + +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db + languageName: node + linkType: hard + "webpack-node-externals@npm:3.0.0": version: 3.0.0 resolution: "webpack-node-externals@npm:3.0.0" @@ -15869,6 +17367,16 @@ __metadata: languageName: node linkType: hard +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: "npm:~0.0.3" + webidl-conversions: "npm:^3.0.0" + checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5 + languageName: node + linkType: hard + "which-boxed-primitive@npm:^1.0.2": version: 1.0.2 resolution: "which-boxed-primitive@npm:1.0.2" @@ -16000,6 +17508,17 @@ __metadata: languageName: node linkType: hard +"which@npm:^1.2.9": + version: 1.3.1 + resolution: "which@npm:1.3.1" + dependencies: + isexe: "npm:^2.0.0" + bin: + which: ./bin/which + checksum: 10c0/e945a8b6bbf6821aaaef7f6e0c309d4b615ef35699576d5489b4261da9539f70393c6b2ce700ee4321c18f914ebe5644bc4631b15466ffbaad37d83151f6af59 + languageName: node + linkType: hard + "which@npm:^2.0.1, which@npm:^2.0.2": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -16086,6 +17605,18 @@ __metadata: languageName: node linkType: hard +"with@npm:^7.0.0": + version: 7.0.2 + resolution: "with@npm:7.0.2" + dependencies: + "@babel/parser": "npm:^7.9.6" + "@babel/types": "npm:^7.9.6" + assert-never: "npm:^1.2.1" + babel-walk: "npm:3.0.0-canary-5" + checksum: 10c0/99289e49afc4b1776afae0ef85e84cfa775e8e07464d2b9853a31b0822347031d1cf77f287d25adc8c3f81e4fa68f4ee31526a9c95d4981ba08a1fe24dee111a + languageName: node + linkType: hard + "wordwrap@npm:^1.0.0": version: 1.0.0 resolution: "wordwrap@npm:1.0.0" @@ -16242,11 +17773,9 @@ __metadata: linkType: hard "yaml@npm:^2.3.4": - version: 2.6.0 - resolution: "yaml@npm:2.6.0" - bin: - yaml: bin.mjs - checksum: 10c0/9e74cdb91cc35512a1c41f5ce509b0e93cc1d00eff0901e4ba831ee75a71ddf0845702adcd6f4ee6c811319eb9b59653248462ab94fa021ab855543a75396ceb + version: 2.3.4 + resolution: "yaml@npm:2.3.4" + checksum: 10c0/cf03b68f8fef5e8516b0f0b54edaf2459f1648317fc6210391cf606d247e678b449382f4bd01f77392538429e306c7cba8ff46ff6b37cac4de9a76aff33bd9e1 languageName: node linkType: hard