From 8af6a1f566769c583af7dd9e18d162717835b7cc Mon Sep 17 00:00:00 2001 From: sct Date: Mon, 28 Dec 2020 02:21:45 +0000 Subject: [PATCH] feat(notifications): control notifcation types per agent closes #513 --- server/lib/notifications/agents/discord.ts | 12 +- server/lib/notifications/agents/email.ts | 11 +- server/lib/notifications/agents/slack.ts | 12 +- server/lib/notifications/agents/telegram.ts | 9 +- server/lib/notifications/index.ts | 21 ++++ .../NotificationType/index.tsx | 70 +++++++++++ .../NotificationTypeSelector/index.tsx | 106 +++++++++++++++++ .../Notifications/NotificationsDiscord.tsx | 42 +++++-- .../Notifications/NotificationsEmail.tsx | 29 ++++- .../NotificationsSlack/index.tsx | 69 ++++++++--- .../Notifications/NotificationsTelegram.tsx | 110 +++++++++++------- src/i18n/locale/en.json | 10 ++ 12 files changed, 410 insertions(+), 91 deletions(-) create mode 100644 src/components/NotificationTypeSelector/NotificationType/index.tsx create mode 100644 src/components/NotificationTypeSelector/index.tsx diff --git a/server/lib/notifications/agents/discord.ts b/server/lib/notifications/agents/discord.ts index 954469453..9c1897d16 100644 --- a/server/lib/notifications/agents/discord.ts +++ b/server/lib/notifications/agents/discord.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { Notification } from '..'; +import { hasNotificationType, Notification } from '..'; import logger from '../../../logger'; import { getSettings, NotificationAgentDiscord } from '../../settings'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; @@ -196,10 +196,12 @@ class DiscordAgent }; } - // TODO: Add checking for type here once we add notification type filters for agents - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public shouldSend(_type: Notification): boolean { - if (this.getSettings().enabled && this.getSettings().options.webhookUrl) { + public shouldSend(type: Notification): boolean { + if ( + this.getSettings().enabled && + this.getSettings().options.webhookUrl && + hasNotificationType(type, this.getSettings().types) + ) { return true; } diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 4e2c8ceba..d983a52ed 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -1,5 +1,5 @@ import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; -import { Notification } from '..'; +import { hasNotificationType, Notification } from '..'; import path from 'path'; import { getSettings, NotificationAgentEmail } from '../../settings'; import nodemailer from 'nodemailer'; @@ -22,12 +22,13 @@ class EmailAgent return settings.notifications.agents.email; } - // TODO: Add checking for type here once we add notification type filters for agents - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public shouldSend(_type: Notification): boolean { + public shouldSend(type: Notification): boolean { const settings = this.getSettings(); - if (settings.enabled) { + if ( + settings.enabled && + hasNotificationType(type, this.getSettings().types) + ) { return true; } diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index 221228d85..03f901b2b 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { Notification } from '..'; +import { hasNotificationType, Notification } from '..'; import logger from '../../../logger'; import { getSettings, NotificationAgentSlack } from '../../settings'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; @@ -187,10 +187,12 @@ class SlackAgent }; } - // TODO: Add checking for type here once we add notification type filters for agents - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public shouldSend(_type: Notification): boolean { - if (this.getSettings().enabled && this.getSettings().options.webhookUrl) { + public shouldSend(type: Notification): boolean { + if ( + this.getSettings().enabled && + this.getSettings().options.webhookUrl && + hasNotificationType(type, this.getSettings().types) + ) { return true; } diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index 9d9e12705..2b5a9fdfc 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { Notification } from '..'; +import { hasNotificationType, Notification } from '..'; import logger from '../../../logger'; import { getSettings, NotificationAgentTelegram } from '../../settings'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; @@ -25,13 +25,12 @@ class TelegramAgent return settings.notifications.agents.telegram; } - // TODO: Add checking for type here once we add notification type filters for agents - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public shouldSend(_type: Notification): boolean { + public shouldSend(type: Notification): boolean { if ( this.getSettings().enabled && this.getSettings().options.botAPI && - this.getSettings().options.chatId + this.getSettings().options.chatId && + hasNotificationType(type, this.getSettings().types) ) { return true; } diff --git a/server/lib/notifications/index.ts b/server/lib/notifications/index.ts index 0c711abe4..07127cf2f 100644 --- a/server/lib/notifications/index.ts +++ b/server/lib/notifications/index.ts @@ -9,6 +9,27 @@ export enum Notification { TEST_NOTIFICATION = 32, } +export const hasNotificationType = ( + types: Notification | Notification[], + value: number +): boolean => { + let total = 0; + + // If we are not checking any notifications, bail out and return true + if (types === 0) { + return true; + } + + if (Array.isArray(types)) { + // Combine all notification values into one + total = types.reduce((a, v) => a + v, 0); + } else { + total = types; + } + + return !!(value & total); +}; + class NotificationManager { private activeAgents: NotificationAgent[] = []; diff --git a/src/components/NotificationTypeSelector/NotificationType/index.tsx b/src/components/NotificationTypeSelector/NotificationType/index.tsx new file mode 100644 index 000000000..71b2a8491 --- /dev/null +++ b/src/components/NotificationTypeSelector/NotificationType/index.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { NotificationItem, hasNotificationType } from '..'; + +interface NotificationTypeProps { + option: NotificationItem; + currentTypes: number; + parent?: NotificationItem; + onUpdate: (newTypes: number) => void; +} + +const NotificationType: React.FC = ({ + option, + currentTypes, + onUpdate, + parent, +}) => { + return ( + <> +
+
+ { + onUpdate( + hasNotificationType(option.value, currentTypes) + ? currentTypes - option.value + : currentTypes + option.value + ); + }} + defaultChecked={ + hasNotificationType(option.value, currentTypes) || + (!!parent?.value && + hasNotificationType(parent.value, currentTypes)) + } + /> +
+
+ +

{option.description}

+
+
+ {(option.children ?? []).map((child) => ( +
+ onUpdate(newTypes)} + parent={option} + /> +
+ ))} + + ); +}; + +export default NotificationType; diff --git a/src/components/NotificationTypeSelector/index.tsx b/src/components/NotificationTypeSelector/index.tsx new file mode 100644 index 000000000..7b8b7f3fa --- /dev/null +++ b/src/components/NotificationTypeSelector/index.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import NotificationType from './NotificationType'; + +const messages = defineMessages({ + mediarequested: 'Media Requested', + mediarequestedDescription: + 'Sends a notification when new media is requested. For certain agents, this will only send the notification to admins or users with the "Manage Requests" permission.', + mediaapproved: 'Media Approved', + mediaapprovedDescription: 'Sends a notification when media is approved.', + mediaavailable: 'Media Available', + mediaavailableDescription: + 'Sends a notification when media becomes available.', + mediafailed: 'Media Failed', + mediafailedDescription: + 'Sends a notification when media fails to be added to services (Radarr/Sonarr). For certain agents, this will only send the notification to admins or users with the "Manage Requests" permission.', +}); + +export const hasNotificationType = ( + types: Notification | Notification[], + value: number +): boolean => { + let total = 0; + + if (types === 0) { + return true; + } + + if (Array.isArray(types)) { + total = types.reduce((a, v) => a + v, 0); + } else { + total = types; + } + + return !!(value & total); +}; + +export enum Notification { + MEDIA_PENDING = 2, + MEDIA_APPROVED = 4, + MEDIA_AVAILABLE = 8, + MEDIA_FAILED = 16, + TEST_NOTIFICATION = 32, +} + +export interface NotificationItem { + id: string; + name: string; + description: string; + value: Notification; + children?: NotificationItem[]; +} + +interface NotificationTypeSelectorProps { + currentTypes: number; + onUpdate: (newTypes: number) => void; +} + +const NotificationTypeSelector: React.FC = ({ + currentTypes, + onUpdate, +}) => { + const intl = useIntl(); + + const types: NotificationItem[] = [ + { + id: 'media-requested', + name: intl.formatMessage(messages.mediarequested), + description: intl.formatMessage(messages.mediarequestedDescription), + value: Notification.MEDIA_PENDING, + }, + { + id: 'media-approved', + name: intl.formatMessage(messages.mediaapproved), + description: intl.formatMessage(messages.mediaapprovedDescription), + value: Notification.MEDIA_APPROVED, + }, + { + id: 'media-available', + name: intl.formatMessage(messages.mediaavailable), + description: intl.formatMessage(messages.mediaavailableDescription), + value: Notification.MEDIA_AVAILABLE, + }, + { + id: 'media-failed', + name: intl.formatMessage(messages.mediafailed), + description: intl.formatMessage(messages.mediafailedDescription), + value: Notification.MEDIA_FAILED, + }, + ]; + + return ( + <> + {types.map((type) => ( + + ))} + + ); +}; + +export default NotificationTypeSelector; diff --git a/src/components/Settings/Notifications/NotificationsDiscord.tsx b/src/components/Settings/Notifications/NotificationsDiscord.tsx index 2c7b23f49..7ff3d8241 100644 --- a/src/components/Settings/Notifications/NotificationsDiscord.tsx +++ b/src/components/Settings/Notifications/NotificationsDiscord.tsx @@ -7,6 +7,7 @@ import { defineMessages, useIntl } from 'react-intl'; import axios from 'axios'; import * as Yup from 'yup'; import { useToasts } from 'react-toast-notifications'; +import NotificationTypeSelector from '../../NotificationTypeSelector'; const messages = defineMessages({ save: 'Save Changes', @@ -19,6 +20,7 @@ const messages = defineMessages({ discordsettingsfailed: 'Discord notification settings failed to save.', testsent: 'Test notification sent!', test: 'Test', + notificationtypes: 'Notification Types', }); const NotificationsDiscord: React.FC = () => { @@ -69,7 +71,7 @@ const NotificationsDiscord: React.FC = () => { } }} > - {({ errors, touched, isSubmitting, values, isValid }) => { + {({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => { const testSettings = async () => { await axios.post('/api/v1/settings/notifications/discord/test', { enabled: true, @@ -99,7 +101,7 @@ const NotificationsDiscord: React.FC = () => { type="checkbox" id="enabled" name="enabled" - className="form-checkbox rounded-md h-6 w-6 text-indigo-600 transition duration-150 ease-in-out" + className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox" /> @@ -111,7 +113,7 @@ const NotificationsDiscord: React.FC = () => { {intl.formatMessage(messages.webhookUrl)}
-
+
{ placeholder={intl.formatMessage( messages.webhookUrlPlaceholder )} - className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500" + className="flex-1 block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5" />
{errors.webhookUrl && touched.webhookUrl && ( -
{errors.webhookUrl}
+
{errors.webhookUrl}
)}
-
+
+
+
+
+
+ {intl.formatMessage(messages.notificationtypes)} +
+
+
+
+ + setFieldValue('types', newTypes) + } + /> +
+
+
+
+
+
- + - +
+
+
+
+
+
+ {intl.formatMessage(messages.notificationtypes)} +
+
+
+
+ + setFieldValue('types', newTypes) + } + /> +
+
+
+
+
diff --git a/src/components/Settings/Notifications/NotificationsSlack/index.tsx b/src/components/Settings/Notifications/NotificationsSlack/index.tsx index 9808f2103..e86175afb 100644 --- a/src/components/Settings/Notifications/NotificationsSlack/index.tsx +++ b/src/components/Settings/Notifications/NotificationsSlack/index.tsx @@ -8,6 +8,7 @@ import axios from 'axios'; import * as Yup from 'yup'; import { useToasts } from 'react-toast-notifications'; import Alert from '../../../Common/Alert'; +import NotificationTypeSelector from '../../../NotificationTypeSelector'; const messages = defineMessages({ save: 'Save Changes', @@ -23,6 +24,7 @@ const messages = defineMessages({ settingupslack: 'Setting up Slack Notifications', settingupslackDescription: 'To use Slack notifications, you will need to create an Incoming Webhook integration and use the provided webhook URL below.', + notificationtypes: 'Notification Types', }); const NotificationsSlack: React.FC = () => { @@ -44,24 +46,22 @@ const NotificationsSlack: React.FC = () => { return ( <> -

- - {intl.formatMessage(messages.settingupslackDescription, { - WebhookLink: function WebhookLink(msg) { - return ( - - {msg} - - ); - }, - })} - -

+ + {intl.formatMessage(messages.settingupslackDescription, { + WebhookLink: function WebhookLink(msg) { + return ( + + {msg} + + ); + }, + })} + { } }} > - {({ errors, touched, isSubmitting, values, isValid }) => { + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + }) => { const testSettings = async () => { await axios.post('/api/v1/settings/notifications/slack/test', { enabled: true, @@ -150,6 +157,30 @@ const NotificationsSlack: React.FC = () => { )}
+
+
+
+
+
+ {intl.formatMessage(messages.notificationtypes)} +
+
+
+
+ + setFieldValue('types', newTypes) + } + /> +
+
+
+
+
diff --git a/src/components/Settings/Notifications/NotificationsTelegram.tsx b/src/components/Settings/Notifications/NotificationsTelegram.tsx index bd8a8b227..d8cc7b400 100644 --- a/src/components/Settings/Notifications/NotificationsTelegram.tsx +++ b/src/components/Settings/Notifications/NotificationsTelegram.tsx @@ -8,6 +8,7 @@ import axios from 'axios'; import * as Yup from 'yup'; import { useToasts } from 'react-toast-notifications'; import Alert from '../../Common/Alert'; +import NotificationTypeSelector from '../../NotificationTypeSelector'; const messages = defineMessages({ save: 'Save Changes', @@ -26,6 +27,7 @@ const messages = defineMessages({ 'To setup Telegram you need to create a bot and get the bot API key.\ Additionally, you need the chat id for the chat you want the bot to send notifications to.\ You can do this by adding @get_id_bot to the chat or group chat.', + notificationtypes: 'Notification Types', }); const NotificationsTelegram: React.FC = () => { @@ -81,7 +83,7 @@ const NotificationsTelegram: React.FC = () => { } }} > - {({ errors, touched, isSubmitting, values, isValid }) => { + {({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => { const testSettings = async () => { await axios.post('/api/v1/settings/notifications/telegram/test', { enabled: true, @@ -100,39 +102,37 @@ const NotificationsTelegram: React.FC = () => { return ( <> -

- - {intl.formatMessage(messages.settinguptelegramDescription, { - CreateBotLink: function CreateBotLink(msg) { - return ( - - {msg} - - ); - }, - GetIdBotLink: function GetIdBotLink(msg) { - return ( - - {msg} - - ); - }, - })} - -

+ + {intl.formatMessage(messages.settinguptelegramDescription, { + CreateBotLink: function CreateBotLink(msg) { + return ( + + {msg} + + ); + }, + GetIdBotLink: function GetIdBotLink(msg) { + return ( + + {msg} + + ); + }, + })} +
-
+
{errors.botAPI && touched.botAPI && ( -
{errors.botAPI}
+
{errors.botAPI}
)}
-
+
{errors.chatId && touched.chatId && ( -
{errors.chatId}
+
{errors.chatId}
)}
-
+
+
+
+
+
+ {intl.formatMessage(messages.notificationtypes)} +
+
+
+
+ + setFieldValue('types', newTypes) + } + /> +
+
+
+
+
+
- + - +