diff --git a/overseerr-api.yml b/overseerr-api.yml index ddd284bb..463d8ad1 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -1213,8 +1213,6 @@ components: type: string userToken: type: string - priority: - type: number LunaSeaSettings: type: object properties: @@ -1599,6 +1597,9 @@ components: nullable: true discordEnabled: type: boolean + discordEnabledTypes: + type: number + nullable: true discordId: type: string nullable: true diff --git a/server/entity/UserSettings.ts b/server/entity/UserSettings.ts index 96227bf0..02f39111 100644 --- a/server/entity/UserSettings.ts +++ b/server/entity/UserSettings.ts @@ -108,7 +108,10 @@ export class UserSettings { }) public notificationTypes: Partial; - public hasNotificationType(key: NotificationAgentKey, type: Notification) { + public hasNotificationType( + key: NotificationAgentKey, + type: Notification + ): boolean { return hasNotificationType(type, this.notificationTypes[key] ?? 0); } } diff --git a/server/interfaces/api/userSettingsInterfaces.ts b/server/interfaces/api/userSettingsInterfaces.ts index 8fb6ae87..18e3c7ab 100644 --- a/server/interfaces/api/userSettingsInterfaces.ts +++ b/server/interfaces/api/userSettingsInterfaces.ts @@ -20,6 +20,7 @@ export interface UserSettingsNotificationsResponse { emailEnabled?: boolean; pgpKey?: string; discordEnabled?: boolean; + discordEnabledTypes?: number; discordId?: string; telegramEnabled?: boolean; telegramBotUsername?: string; diff --git a/server/lib/notifications/agents/agent.ts b/server/lib/notifications/agents/agent.ts index 132683e5..66c52a16 100644 --- a/server/lib/notifications/agents/agent.ts +++ b/server/lib/notifications/agents/agent.ts @@ -24,6 +24,6 @@ export abstract class BaseAgent { } export interface NotificationAgent { - shouldSend(type: Notification): boolean; + shouldSend(): boolean; send(type: Notification, payload: NotificationPayload): Promise; } diff --git a/server/lib/notifications/agents/discord.ts b/server/lib/notifications/agents/discord.ts index c3089cdc..97be2cba 100644 --- a/server/lib/notifications/agents/discord.ts +++ b/server/lib/notifications/agents/discord.ts @@ -193,12 +193,10 @@ class DiscordAgent }; } - public shouldSend(type: Notification): boolean { - if ( - this.getSettings().enabled && - this.getSettings().options.webhookUrl && - hasNotificationType(type, this.getSettings().types) - ) { + public shouldSend(): boolean { + const settings = this.getSettings(); + + if (settings.enabled && settings.options.webhookUrl) { return true; } @@ -209,6 +207,12 @@ class DiscordAgent type: Notification, payload: NotificationPayload ): Promise { + const settings = this.getSettings(); + + if (!hasNotificationType(type, settings.types ?? 0)) { + return true; + } + logger.debug('Sending Discord notification', { label: 'Notifications', type: Notification[type], @@ -218,13 +222,6 @@ class DiscordAgent let content = undefined; try { - const { botUsername, botAvatarUrl, webhookUrl } = - this.getSettings().options; - - if (!webhookUrl) { - return false; - } - if (payload.notifyUser) { // Mention user who submitted the request if ( @@ -258,9 +255,9 @@ class DiscordAgent .join(' '); } - await axios.post(webhookUrl, { - username: botUsername, - avatar_url: botAvatarUrl, + await axios.post(settings.options.webhookUrl, { + username: settings.options.botUsername, + avatar_url: settings.options.botAvatarUrl, embeds: [this.buildEmbed(type, payload)], content, } as DiscordWebhookPayload); diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 593543a5..6a06d718 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -1,7 +1,7 @@ import { EmailOptions } from 'email-templates'; import path from 'path'; import { getRepository } from 'typeorm'; -import { hasNotificationType, Notification } from '..'; +import { Notification } from '..'; import { MediaType } from '../../../constants/media'; import { User } from '../../../entity/User'; import logger from '../../../logger'; @@ -28,12 +28,14 @@ class EmailAgent return settings.notifications.agents.email; } - public shouldSend(type: Notification): boolean { + public shouldSend(): boolean { const settings = this.getSettings(); if ( settings.enabled && - hasNotificationType(type, this.getSettings().types) + settings.options.emailFrom && + settings.options.smtpHost && + settings.options.smtpPort ) { return true; } diff --git a/server/lib/notifications/agents/lunasea.ts b/server/lib/notifications/agents/lunasea.ts index 24fa5f44..5d5c5216 100644 --- a/server/lib/notifications/agents/lunasea.ts +++ b/server/lib/notifications/agents/lunasea.ts @@ -50,12 +50,10 @@ class LunaSeaAgent }; } - public shouldSend(type: Notification): boolean { - if ( - this.getSettings().enabled && - this.getSettings().options.webhookUrl && - hasNotificationType(type, this.getSettings().types) - ) { + public shouldSend(): boolean { + const settings = this.getSettings(); + + if (settings.enabled && settings.options.webhookUrl) { return true; } @@ -66,6 +64,12 @@ class LunaSeaAgent type: Notification, payload: NotificationPayload ): Promise { + const settings = this.getSettings(); + + if (!hasNotificationType(type, settings.types ?? 0)) { + return true; + } + logger.debug('Sending LunaSea notification', { label: 'Notifications', type: Notification[type], @@ -73,19 +77,19 @@ class LunaSeaAgent }); try { - const { webhookUrl, profileName } = this.getSettings().options; - - if (!webhookUrl) { - return false; - } - - await axios.post(webhookUrl, this.buildPayload(type, payload), { - headers: { - Authorization: `Basic ${Buffer.from(`${profileName}:`).toString( - 'base64' - )}`, - }, - }); + await axios.post( + settings.options.webhookUrl, + this.buildPayload(type, payload), + settings.options.profileName + ? { + headers: { + Authorization: `Basic ${Buffer.from( + `${settings.options.profileName}:` + ).toString('base64')}`, + }, + } + : undefined + ); return true; } catch (e) { @@ -94,7 +98,7 @@ class LunaSeaAgent type: Notification[type], subject: payload.subject, errorMessage: e.message, - response: e.response.data, + response: e.response?.data, }); return false; diff --git a/server/lib/notifications/agents/pushbullet.ts b/server/lib/notifications/agents/pushbullet.ts index 02431fcd..160eed87 100644 --- a/server/lib/notifications/agents/pushbullet.ts +++ b/server/lib/notifications/agents/pushbullet.ts @@ -24,12 +24,10 @@ class PushbulletAgent return settings.notifications.agents.pushbullet; } - public shouldSend(type: Notification): boolean { - if ( - this.getSettings().enabled && - this.getSettings().options.accessToken && - hasNotificationType(type, this.getSettings().types) - ) { + public shouldSend(): boolean { + const settings = this.getSettings(); + + if (settings.enabled && settings.options.accessToken) { return true; } @@ -137,6 +135,12 @@ class PushbulletAgent type: Notification, payload: NotificationPayload ): Promise { + const settings = this.getSettings(); + + if (!hasNotificationType(type, settings.types ?? 0)) { + return true; + } + logger.debug('Sending Pushbullet notification', { label: 'Notifications', type: Notification[type], @@ -144,14 +148,10 @@ class PushbulletAgent }); try { - const endpoint = 'https://api.pushbullet.com/v2/pushes'; - - const { accessToken } = this.getSettings().options; - const { title, body } = this.constructMessageDetails(type, payload); await axios.post( - endpoint, + 'https://api.pushbullet.com/v2/pushes', { type: 'note', title: title, @@ -159,7 +159,7 @@ class PushbulletAgent } as PushbulletPayload, { headers: { - 'Access-Token': accessToken, + 'Access-Token': settings.options.accessToken, }, } ); diff --git a/server/lib/notifications/agents/pushover.ts b/server/lib/notifications/agents/pushover.ts index 0c38b1bd..b37b5446 100644 --- a/server/lib/notifications/agents/pushover.ts +++ b/server/lib/notifications/agents/pushover.ts @@ -30,12 +30,13 @@ class PushoverAgent return settings.notifications.agents.pushover; } - public shouldSend(type: Notification): boolean { + public shouldSend(): boolean { + const settings = this.getSettings(); + if ( - this.getSettings().enabled && - this.getSettings().options.accessToken && - this.getSettings().options.userToken && - hasNotificationType(type, this.getSettings().types) + settings.enabled && + settings.options.accessToken && + settings.options.userToken ) { return true; } @@ -161,6 +162,12 @@ class PushoverAgent type: Notification, payload: NotificationPayload ): Promise { + const settings = this.getSettings(); + + if (!hasNotificationType(type, settings.types ?? 0)) { + return true; + } + logger.debug('Sending Pushover notification', { label: 'Notifications', type: Notification[type], @@ -169,14 +176,12 @@ class PushoverAgent try { const endpoint = 'https://api.pushover.net/1/messages.json'; - const { accessToken, userToken } = this.getSettings().options; - const { title, message, url, url_title, priority } = this.constructMessageDetails(type, payload); await axios.post(endpoint, { - token: accessToken, - user: userToken, + token: settings.options.accessToken, + user: settings.options.userToken, title: title, message: message, url: url, diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index 40ace37e..8065f9a6 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -218,12 +218,10 @@ class SlackAgent }; } - public shouldSend(type: Notification): boolean { - if ( - this.getSettings().enabled && - this.getSettings().options.webhookUrl && - hasNotificationType(type, this.getSettings().types) - ) { + public shouldSend(): boolean { + const settings = this.getSettings(); + + if (settings.enabled && settings.options.webhookUrl) { return true; } @@ -234,19 +232,22 @@ class SlackAgent type: Notification, payload: NotificationPayload ): Promise { + const settings = this.getSettings(); + + if (!hasNotificationType(type, settings.types ?? 0)) { + return true; + } + logger.debug('Sending Slack notification', { label: 'Notifications', type: Notification[type], subject: payload.subject, }); try { - const webhookUrl = this.getSettings().options.webhookUrl; - - if (!webhookUrl) { - return false; - } - - await axios.post(webhookUrl, this.buildEmbed(type, payload)); + await axios.post( + settings.options.webhookUrl, + this.buildEmbed(type, payload) + ); return true; } catch (e) { diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index c62e7027..b63fbd62 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -1,7 +1,10 @@ import axios from 'axios'; +import { getRepository } from 'typeorm'; import { hasNotificationType, Notification } from '..'; import { MediaType } from '../../../constants/media'; +import { User } from '../../../entity/User'; import logger from '../../../logger'; +import { Permission } from '../../permissions'; import { getSettings, NotificationAgentKey, @@ -40,12 +43,13 @@ class TelegramAgent return settings.notifications.agents.telegram; } - public shouldSend(type: Notification): boolean { + public shouldSend(): boolean { + const settings = this.getSettings(); + if ( - this.getSettings().enabled && - this.getSettings().options.botAPI && - this.getSettings().options.chatId && - hasNotificationType(type, this.getSettings().types) + settings.enabled && + settings.options.botAPI && + settings.options.chatId ) { return true; } @@ -59,8 +63,10 @@ class TelegramAgent private buildMessage( type: Notification, - payload: NotificationPayload - ): string { + payload: NotificationPayload, + chatId: string, + sendSilently: boolean + ): TelegramMessagePayload | TelegramPhotoPayload { const settings = getSettings(); let message = ''; @@ -153,67 +159,36 @@ class TelegramAgent } /* eslint-enable */ - return message; + return payload.image + ? ({ + photo: payload.image, + caption: message, + parse_mode: 'MarkdownV2', + chat_id: chatId, + disable_notification: !!sendSilently, + } as TelegramPhotoPayload) + : ({ + text: message, + parse_mode: 'MarkdownV2', + chat_id: chatId, + disable_notification: !!sendSilently, + } as TelegramMessagePayload); } public async send( type: Notification, payload: NotificationPayload ): Promise { - const endpoint = `${this.baseUrl}bot${this.getSettings().options.botAPI}/${ + const settings = this.getSettings(); + + const endpoint = `${this.baseUrl}bot${settings.options.botAPI}/${ payload.image ? 'sendPhoto' : 'sendMessage' }`; // Send system notification - try { - logger.debug('Sending Telegram notification', { - label: 'Notifications', - type: Notification[type], - subject: payload.subject, - }); - - await axios.post( - endpoint, - payload.image - ? ({ - photo: payload.image, - caption: this.buildMessage(type, payload), - parse_mode: 'MarkdownV2', - chat_id: this.getSettings().options.chatId, - disable_notification: this.getSettings().options.sendSilently, - } as TelegramPhotoPayload) - : ({ - text: this.buildMessage(type, payload), - parse_mode: 'MarkdownV2', - chat_id: `${this.getSettings().options.chatId}`, - disable_notification: this.getSettings().options.sendSilently, - } as TelegramMessagePayload) - ); - } catch (e) { - logger.error('Error sending Telegram notification', { - label: 'Notifications', - type: Notification[type], - subject: payload.subject, - errorMessage: e.message, - response: e.response.data, - }); - return false; - } - - if ( - payload.notifyUser && - payload.notifyUser.settings?.hasNotificationType( - NotificationAgentKey.TELEGRAM, - type - ) && - payload.notifyUser.settings?.telegramChatId && - payload.notifyUser.settings?.telegramChatId !== - this.getSettings().options.chatId - ) { - // Send notification to the user who submitted the request + if (hasNotificationType(type, settings.types ?? 0)) { logger.debug('Sending Telegram notification', { label: 'Notifications', - recipient: payload.notifyUser.displayName, type: Notification[type], subject: payload.subject, }); @@ -221,27 +196,16 @@ class TelegramAgent try { await axios.post( endpoint, - payload.image - ? ({ - photo: payload.image, - caption: this.buildMessage(type, payload), - parse_mode: 'MarkdownV2', - chat_id: payload.notifyUser.settings.telegramChatId, - disable_notification: - payload.notifyUser.settings.telegramSendSilently, - } as TelegramPhotoPayload) - : ({ - text: this.buildMessage(type, payload), - parse_mode: 'MarkdownV2', - chat_id: payload.notifyUser.settings.telegramChatId, - disable_notification: - payload.notifyUser.settings.telegramSendSilently, - } as TelegramMessagePayload) + this.buildMessage( + type, + payload, + settings.options.chatId, + settings.options.sendSilently + ) ); } catch (e) { logger.error('Error sending Telegram notification', { label: 'Notifications', - recipient: payload.notifyUser.displayName, type: Notification[type], subject: payload.subject, errorMessage: e.message, @@ -252,6 +216,103 @@ class TelegramAgent } } + if (payload.notifyUser) { + // Send notification to the user who submitted the request + if ( + payload.notifyUser.settings?.hasNotificationType( + NotificationAgentKey.TELEGRAM, + type + ) && + payload.notifyUser.settings?.telegramChatId && + payload.notifyUser.settings?.telegramChatId !== settings.options.chatId + ) { + logger.debug('Sending Telegram notification', { + label: 'Notifications', + recipient: payload.notifyUser.displayName, + type: Notification[type], + subject: payload.subject, + }); + + try { + await axios.post( + endpoint, + this.buildMessage( + type, + payload, + payload.notifyUser.settings.telegramChatId, + !!payload.notifyUser.settings.telegramSendSilently + ) + ); + } catch (e) { + logger.error('Error sending Telegram notification', { + label: 'Notifications', + recipient: payload.notifyUser.displayName, + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + response: e.response?.data, + }); + + return false; + } + } + } else { + // Send notifications to all users with the Manage Requests permission + const userRepository = getRepository(User); + const users = await userRepository.find(); + + await Promise.all( + users + .filter( + (user) => + user.hasPermission(Permission.MANAGE_REQUESTS) && + user.settings?.hasNotificationType( + NotificationAgentKey.TELEGRAM, + type + ) && + // Check if it's the user's own auto-approved request + (type !== Notification.MEDIA_AUTO_APPROVED || + user.id !== payload.request?.requestedBy.id) + ) + .map(async (user) => { + if ( + user.settings?.telegramChatId && + user.settings.telegramChatId !== settings.options.chatId + ) { + logger.debug('Sending Telegram notification', { + label: 'Notifications', + recipient: user.displayName, + type: Notification[type], + subject: payload.subject, + }); + + try { + await axios.post( + endpoint, + this.buildMessage( + type, + payload, + user.settings.telegramChatId, + !!user.settings?.telegramSendSilently + ) + ); + } catch (e) { + logger.error('Error sending Telegram notification', { + label: 'Notifications', + recipient: user.displayName, + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + response: e.response?.data, + }); + + return false; + } + } + }) + ); + } + return true; } } diff --git a/server/lib/notifications/agents/webhook.ts b/server/lib/notifications/agents/webhook.ts index 946bd3f7..2959f81c 100644 --- a/server/lib/notifications/agents/webhook.ts +++ b/server/lib/notifications/agents/webhook.ts @@ -113,12 +113,10 @@ class WebhookAgent return this.parseKeys(parsedJSON, payload, type); } - public shouldSend(type: Notification): boolean { - if ( - this.getSettings().enabled && - this.getSettings().options.webhookUrl && - hasNotificationType(type, this.getSettings().types) - ) { + public shouldSend(): boolean { + const settings = this.getSettings(); + + if (settings.enabled && settings.options.webhookUrl) { return true; } @@ -129,6 +127,12 @@ class WebhookAgent type: Notification, payload: NotificationPayload ): Promise { + const settings = this.getSettings(); + + if (!hasNotificationType(type, settings.types ?? 0)) { + return true; + } + logger.debug('Sending webhook notification', { label: 'Notifications', type: Notification[type], @@ -136,17 +140,17 @@ class WebhookAgent }); try { - const { webhookUrl, authHeader } = this.getSettings().options; - - if (!webhookUrl) { - return false; - } - - await axios.post(webhookUrl, this.buildPayload(type, payload), { - headers: { - Authorization: authHeader, - }, - }); + await axios.post( + settings.options.webhookUrl, + this.buildPayload(type, payload), + settings.options.authHeader + ? { + headers: { + Authorization: settings.options.authHeader, + }, + } + : undefined + ); return true; } catch (e) { diff --git a/server/lib/notifications/agents/webpush.ts b/server/lib/notifications/agents/webpush.ts index 57dbb4f3..968c1435 100644 --- a/server/lib/notifications/agents/webpush.ts +++ b/server/lib/notifications/agents/webpush.ts @@ -1,6 +1,6 @@ import { getRepository } from 'typeorm'; import webpush from 'web-push'; -import { hasNotificationType, Notification } from '..'; +import { Notification } from '..'; import { MediaType } from '../../../constants/media'; import { User } from '../../../entity/User'; import { UserPushSubscription } from '../../../entity/UserPushSubscription'; @@ -135,11 +135,8 @@ class WebPushAgent } } - public shouldSend(type: Notification): boolean { - if ( - this.getSettings().enabled && - hasNotificationType(type, this.getSettings().types) - ) { + public shouldSend(): boolean { + if (this.getSettings().enabled) { return true; } @@ -150,11 +147,6 @@ class WebPushAgent type: Notification, payload: NotificationPayload ): Promise { - logger.debug('Sending web push notification', { - label: 'Notifications', - type: Notification[type], - subject: payload.subject, - }); const userRepository = getRepository(User); const userPushSubRepository = getRepository(UserPushSubscription); const settings = getSettings(); @@ -213,8 +205,15 @@ class WebPushAgent settings.vapidPrivate ); - Promise.all( + await Promise.all( pushSubs.map(async (sub) => { + logger.debug('Sending web push notification', { + label: 'Notifications', + recipient: sub.user.displayName, + type: Notification[type], + subject: payload.subject, + }); + try { await webpush.sendNotification( { @@ -230,12 +229,24 @@ class WebPushAgent ) ); } catch (e) { + logger.error( + 'Error sending web push notification; removing subscription', + { + label: 'Notifications', + recipient: sub.user.displayName, + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + } + ); + // Failed to send notification so we need to remove the subscription userPushSubRepository.remove(sub); } }) ); } + return true; } } diff --git a/server/lib/notifications/index.ts b/server/lib/notifications/index.ts index ad2aab8d..a2eb0141 100644 --- a/server/lib/notifications/index.ts +++ b/server/lib/notifications/index.ts @@ -30,6 +30,11 @@ export const hasNotificationType = ( total = types; } + // Test notifications don't need to be enabled + if (!(value & Notification.TEST_NOTIFICATION)) { + value += Notification.TEST_NOTIFICATION; + } + return !!(value & total); }; @@ -51,7 +56,7 @@ class NotificationManager { }); this.activeAgents.forEach((agent) => { - if (agent.shouldSend(type)) { + if (agent.shouldSend()) { agent.send(type, payload); } }); diff --git a/server/lib/settings.ts b/server/lib/settings.ts index edc4026f..6c91dabc 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -113,7 +113,7 @@ interface FullPublicSettings extends PublicSettings { export interface NotificationAgentConfig { enabled: boolean; - types: number; + types?: number; options: Record; } export interface NotificationAgentDiscord extends NotificationAgentConfig { @@ -150,7 +150,7 @@ export interface NotificationAgentEmail extends NotificationAgentConfig { export interface NotificationAgentLunaSea extends NotificationAgentConfig { options: { webhookUrl: string; - profileName: string; + profileName?: string; }; } @@ -173,7 +173,6 @@ export interface NotificationAgentPushover extends NotificationAgentConfig { options: { accessToken: string; userToken: string; - priority: number; }; } @@ -181,7 +180,7 @@ export interface NotificationAgentWebhook extends NotificationAgentConfig { options: { webhookUrl: string; jsonPayload: string; - authHeader: string; + authHeader?: string; }; } @@ -272,7 +271,6 @@ class Settings { agents: { email: { enabled: false, - types: 0, options: { emailFrom: '', smtpHost: '', @@ -288,8 +286,6 @@ class Settings { enabled: false, types: 0, options: { - botUsername: '', - botAvatarUrl: '', webhookUrl: '', }, }, @@ -298,7 +294,6 @@ class Settings { types: 0, options: { webhookUrl: '', - profileName: '', }, }, slack: { @@ -312,7 +307,6 @@ class Settings { enabled: false, types: 0, options: { - botUsername: '', botAPI: '', chatId: '', sendSilently: false, @@ -331,7 +325,6 @@ class Settings { options: { accessToken: '', userToken: '', - priority: 0, }, }, webhook: { @@ -339,14 +332,12 @@ class Settings { types: 0, options: { webhookUrl: '', - authHeader: '', jsonPayload: 'IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJzdWJqZWN0XCI6IFwie3tzdWJqZWN0fX1cIixcbiAgICBcIm1lc3NhZ2VcIjogXCJ7e21lc3NhZ2V9fVwiLFxuICAgIFwiaW1hZ2VcIjogXCJ7e2ltYWdlfX1cIixcbiAgICBcImVtYWlsXCI6IFwie3tub3RpZnl1c2VyX2VtYWlsfX1cIixcbiAgICBcInVzZXJuYW1lXCI6IFwie3tub3RpZnl1c2VyX3VzZXJuYW1lfX1cIixcbiAgICBcImF2YXRhclwiOiBcInt7bm90aWZ5dXNlcl9hdmF0YXJ9fVwiLFxuICAgIFwie3ttZWRpYX19XCI6IHtcbiAgICAgICAgXCJtZWRpYV90eXBlXCI6IFwie3ttZWRpYV90eXBlfX1cIixcbiAgICAgICAgXCJ0bWRiSWRcIjogXCJ7e21lZGlhX3RtZGJpZH19XCIsXG4gICAgICAgIFwiaW1kYklkXCI6IFwie3ttZWRpYV9pbWRiaWR9fVwiLFxuICAgICAgICBcInR2ZGJJZFwiOiBcInt7bWVkaWFfdHZkYmlkfX1cIixcbiAgICAgICAgXCJzdGF0dXNcIjogXCJ7e21lZGlhX3N0YXR1c319XCIsXG4gICAgICAgIFwic3RhdHVzNGtcIjogXCJ7e21lZGlhX3N0YXR1czRrfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW10sXG4gICAgXCJ7e3JlcXVlc3R9fVwiOiB7XG4gICAgICAgIFwicmVxdWVzdF9pZFwiOiBcInt7cmVxdWVzdF9pZH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfZW1haWxcIjogXCJ7e3JlcXVlc3RlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV91c2VybmFtZVwiOiBcInt7cmVxdWVzdGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2F2YXRhclwiOiBcInt7cmVxdWVzdGVkQnlfYXZhdGFyfX1cIlxuICAgIH1cbn0i', }, }, webpush: { enabled: false, - types: 0, options: {}, }, }, diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index f37b8c86..226dcae0 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -238,7 +238,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>( isOwnProfileOrAdmin(), async (req, res, next) => { const userRepository = getRepository(User); - const settings = getSettings(); + const settings = getSettings()?.notifications.agents; try { const user = await userRepository.findOne({ @@ -250,16 +250,18 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>( } return res.status(200).json({ - emailEnabled: settings?.notifications.agents.email.enabled, + emailEnabled: settings?.email.enabled, pgpKey: user.settings?.pgpKey, - discordEnabled: settings?.notifications.agents.discord.enabled, + discordEnabled: settings?.discord.enabled, + discordEnabledTypes: settings?.discord.enabled + ? settings?.discord.types + : 0, discordId: user.settings?.discordId, - telegramEnabled: settings?.notifications.agents.telegram.enabled, - telegramBotUsername: - settings?.notifications.agents.telegram.options.botUsername, + telegramEnabled: settings?.telegram.enabled, + telegramBotUsername: settings?.telegram.options.botUsername, telegramChatId: user.settings?.telegramChatId, telegramSendSilently: user?.settings?.telegramSendSilently, - webPushEnabled: settings?.notifications.agents.webpush.enabled, + webPushEnabled: settings?.webpush.enabled, notificationTypes: user.settings?.notificationTypes ?? {}, }); } catch (e) { diff --git a/src/assets/extlogos/discord.svg b/src/assets/extlogos/discord.svg index 736d9ddd..64aef202 100644 --- a/src/assets/extlogos/discord.svg +++ b/src/assets/extlogos/discord.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/NotificationTypeSelector/NotificationType/index.tsx b/src/components/NotificationTypeSelector/NotificationType/index.tsx index 4085b2a6..360c89c7 100644 --- a/src/components/NotificationTypeSelector/NotificationType/index.tsx +++ b/src/components/NotificationTypeSelector/NotificationType/index.tsx @@ -38,7 +38,7 @@ const NotificationType: React.FC = ({ : currentTypes + option.value ); }} - defaultChecked={ + checked={ hasNotificationType(option.value, currentTypes) || (!!parent?.value && hasNotificationType(parent.value, currentTypes)) @@ -46,10 +46,12 @@ const NotificationType: React.FC = ({ />
-
{(option.children ?? []).map((child) => ( diff --git a/src/components/NotificationTypeSelector/index.tsx b/src/components/NotificationTypeSelector/index.tsx index aa418e64..0b71f670 100644 --- a/src/components/NotificationTypeSelector/index.tsx +++ b/src/components/NotificationTypeSelector/index.tsx @@ -1,27 +1,42 @@ -import React from 'react'; +import { sortBy } from 'lodash'; +import React, { useMemo, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; +import useSettings from '../../hooks/useSettings'; +import { Permission, User, useUser } from '../../hooks/useUser'; import NotificationType from './NotificationType'; const messages = defineMessages({ notificationTypes: 'Notification Types', mediarequested: 'Media Requested', mediarequestedDescription: - 'Sends a notification when media is requested and requires approval.', + 'Send notifications when users submit new media requests which require approval.', + usermediarequestedDescription: + 'Get notified when other users submit new media requests which require approval.', mediaapproved: 'Media Approved', mediaapprovedDescription: - 'Sends a notification when requested media is manually approved.', + 'Send notifications when media requests are manually approved.', + usermediaapprovedDescription: + 'Get notified when your media requests are approved.', mediaAutoApproved: 'Media Automatically Approved', mediaAutoApprovedDescription: - 'Sends a notification when requested media is automatically approved.', + 'Send notifications when users submit new media requests which are automatically approved.', + usermediaAutoApprovedDescription: + 'Get notified when other users submit new media requests which are automatically approved.', mediaavailable: 'Media Available', mediaavailableDescription: - 'Sends a notification when requested media becomes available.', + 'Send notifications when media requests become available.', + usermediaavailableDescription: + 'Get notified when your media requests become available.', mediafailed: 'Media Failed', mediafailedDescription: - 'Sends a notification when requested media fails to be added to Radarr or Sonarr.', + 'Send notifications when media requests fail to be added to Radarr or Sonarr.', + usermediafailedDescription: + 'Get notified when media requests fail to be added to Radarr or Sonarr.', mediadeclined: 'Media Declined', mediadeclinedDescription: - 'Sends a notification when a media request is declined.', + 'Send notifications when media requests are declined.', + usermediadeclinedDescription: + 'Get notified when your media requests are declined.', }); export const hasNotificationType = ( @@ -30,16 +45,23 @@ export const hasNotificationType = ( ): 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; } + // Test notifications don't need to be enabled + if (!(value & Notification.TEST_NOTIFICATION)) { + value += Notification.TEST_NOTIFICATION; + } + return !!(value & total); }; @@ -63,69 +85,183 @@ export interface NotificationItem { name: string; description: string; value: Notification; + hasNotifyUser?: boolean; children?: NotificationItem[]; + hidden?: boolean; } interface NotificationTypeSelectorProps { + user?: User; + enabledTypes?: number; currentTypes: number; onUpdate: (newTypes: number) => void; + error?: string; } const NotificationTypeSelector: React.FC = ({ + user, + enabledTypes = ALL_NOTIFICATIONS, currentTypes, onUpdate, + error, }) => { const intl = useIntl(); + const settings = useSettings(); + const { hasPermission } = useUser({ id: user?.id }); + const [allowedTypes, setAllowedTypes] = useState(enabledTypes); + + const availableTypes = useMemo(() => { + const allRequestsAutoApproved = + user && + // Has Manage Requests perm, which grants all Auto-Approve perms + (hasPermission(Permission.MANAGE_REQUESTS) || + // Cannot submit requests of any type + !hasPermission( + [ + Permission.REQUEST, + Permission.REQUEST_MOVIE, + Permission.REQUEST_TV, + Permission.REQUEST_4K, + Permission.REQUEST_4K_MOVIE, + Permission.REQUEST_4K_TV, + ], + { type: 'or' } + ) || + // Cannot submit non-4K movie requests OR has Auto-Approve perms for non-4K movies + ((!hasPermission([Permission.REQUEST, Permission.REQUEST_MOVIE], { + type: 'or', + }) || + hasPermission( + [Permission.AUTO_APPROVE, Permission.AUTO_APPROVE_MOVIE], + { type: 'or' } + )) && + // Cannot submit non-4K series requests OR has Auto-Approve perms for non-4K series + (!hasPermission([Permission.REQUEST, Permission.REQUEST_TV], { + type: 'or', + }) || + hasPermission( + [Permission.AUTO_APPROVE, Permission.AUTO_APPROVE_TV], + { type: 'or' } + )) && + // Cannot submit 4K movie requests OR has Auto-Approve perms for 4K movies + (!settings.currentSettings.movie4kEnabled || + !hasPermission( + [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE], + { type: 'or' } + ) || + hasPermission( + [Permission.AUTO_APPROVE_4K, Permission.AUTO_APPROVE_4K_MOVIE], + { type: 'or' } + )) && + // Cannot submit 4K series requests OR has Auto-Approve perms for 4K series + (!settings.currentSettings.series4kEnabled || + !hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_TV], { + type: 'or', + }) || + hasPermission( + [Permission.AUTO_APPROVE_4K, Permission.AUTO_APPROVE_4K_TV], + { type: 'or' } + )))); + + const types: NotificationItem[] = [ + { + id: 'media-requested', + name: intl.formatMessage(messages.mediarequested), + description: intl.formatMessage( + user + ? messages.usermediarequestedDescription + : messages.mediarequestedDescription + ), + value: Notification.MEDIA_PENDING, + hidden: user && !hasPermission(Permission.MANAGE_REQUESTS), + }, + { + id: 'media-auto-approved', + name: intl.formatMessage(messages.mediaAutoApproved), + description: intl.formatMessage( + user + ? messages.usermediaAutoApprovedDescription + : messages.mediaAutoApprovedDescription + ), + value: Notification.MEDIA_AUTO_APPROVED, + hidden: user && !hasPermission(Permission.MANAGE_REQUESTS), + }, + { + id: 'media-approved', + name: intl.formatMessage(messages.mediaapproved), + description: intl.formatMessage( + user + ? messages.usermediaapprovedDescription + : messages.mediaapprovedDescription + ), + value: Notification.MEDIA_APPROVED, + hasNotifyUser: true, + hidden: allRequestsAutoApproved, + }, + { + id: 'media-declined', + name: intl.formatMessage(messages.mediadeclined), + description: intl.formatMessage( + user + ? messages.usermediadeclinedDescription + : messages.mediadeclinedDescription + ), + value: Notification.MEDIA_DECLINED, + hasNotifyUser: true, + hidden: allRequestsAutoApproved, + }, + { + id: 'media-available', + name: intl.formatMessage(messages.mediaavailable), + description: intl.formatMessage( + user + ? messages.usermediaavailableDescription + : messages.mediaavailableDescription + ), + value: Notification.MEDIA_AVAILABLE, + hasNotifyUser: true, + }, + { + id: 'media-failed', + name: intl.formatMessage(messages.mediafailed), + description: intl.formatMessage( + user + ? messages.usermediafailedDescription + : messages.mediafailedDescription + ), + value: Notification.MEDIA_FAILED, + hidden: user && !hasPermission(Permission.MANAGE_REQUESTS), + }, + ]; - const types: NotificationItem[] = [ - { - id: 'media-requested', - name: intl.formatMessage(messages.mediarequested), - description: intl.formatMessage(messages.mediarequestedDescription), - value: Notification.MEDIA_PENDING, - }, - { - id: 'media-auto-approved', - name: intl.formatMessage(messages.mediaAutoApproved), - description: intl.formatMessage(messages.mediaAutoApprovedDescription), - value: Notification.MEDIA_AUTO_APPROVED, - }, - { - id: 'media-approved', - name: intl.formatMessage(messages.mediaapproved), - description: intl.formatMessage(messages.mediaapprovedDescription), - value: Notification.MEDIA_APPROVED, - }, - { - id: 'media-declined', - name: intl.formatMessage(messages.mediadeclined), - description: intl.formatMessage(messages.mediadeclinedDescription), - value: Notification.MEDIA_DECLINED, - }, - { - 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, - }, - ]; + const filteredTypes = types.filter( + (type) => !type.hidden && hasNotificationType(type.value, enabledTypes) + ); + + const newAllowedTypes = filteredTypes.reduce((a, v) => a + v.value, 0); + if (newAllowedTypes !== allowedTypes) { + setAllowedTypes(newAllowedTypes); + } + + return user + ? sortBy(filteredTypes, 'hasNotifyUser', 'DESC') + : filteredTypes; + }, [user, hasPermission, settings, intl, allowedTypes, enabledTypes]); + + if (!availableTypes.length) { + return null; + } return (
{intl.formatMessage(messages.notificationTypes)} - * + {!user && *}
- {types.map((type) => ( + {availableTypes.map((type) => ( = ({ /> ))}
+ {error &&
{error}
}
diff --git a/src/components/Settings/Notifications/NotificationsDiscord.tsx b/src/components/Settings/Notifications/NotificationsDiscord.tsx index 70427f69..4c64c9c9 100644 --- a/src/components/Settings/Notifications/NotificationsDiscord.tsx +++ b/src/components/Settings/Notifications/NotificationsDiscord.tsx @@ -23,6 +23,7 @@ const messages = defineMessages({ toastDiscordTestSuccess: 'Discord test notification sent!', toastDiscordTestFailed: 'Discord test notification failed to send.', validationUrl: 'You must provide a valid URL', + validationTypes: 'You must select at least one notification type', }); const NotificationsDiscord: React.FC = () => { @@ -46,6 +47,13 @@ const NotificationsDiscord: React.FC = () => { otherwise: Yup.string().nullable(), }) .url(intl.formatMessage(messages.validationUrl)), + types: Yup.number().when('enabled', { + is: true, + then: Yup.number() + .nullable() + .moreThan(0, intl.formatMessage(messages.validationTypes)), + otherwise: Yup.number().nullable(), + }), }); if (!data && !error) { @@ -88,7 +96,15 @@ const NotificationsDiscord: React.FC = () => { } }} > - {({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => { + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + setFieldTouched, + }) => { const testSettings = async () => { setIsTesting(true); let toastId: string | undefined; @@ -211,8 +227,20 @@ const NotificationsDiscord: React.FC = () => { setFieldValue('types', newTypes)} + currentTypes={values.enabled ? values.types : 0} + onUpdate={(newTypes) => { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + errors.types && touched.types + ? (errors.types as string) + : undefined + } />
diff --git a/src/components/Settings/Notifications/NotificationsEmail.tsx b/src/components/Settings/Notifications/NotificationsEmail.tsx index 71a8e230..d170716a 100644 --- a/src/components/Settings/Notifications/NotificationsEmail.tsx +++ b/src/components/Settings/Notifications/NotificationsEmail.tsx @@ -6,12 +6,10 @@ import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; import * as Yup from 'yup'; import globalMessages from '../../../i18n/globalMessages'; -import Alert from '../../Common/Alert'; import Badge from '../../Common/Badge'; import Button from '../../Common/Button'; import LoadingSpinner from '../../Common/LoadingSpinner'; import SensitiveInput from '../../Common/SensitiveInput'; -import NotificationTypeSelector from '../../NotificationTypeSelector'; const messages = defineMessages({ validationSmtpHostRequired: 'You must provide a valid hostname or IP address', @@ -37,20 +35,14 @@ const messages = defineMessages({ allowselfsigned: 'Allow Self-Signed Certificates', senderName: 'Sender Name', validationEmail: 'You must provide a valid email address', - emailNotificationTypesAlertDescription: - 'Media Requested, Media Automatically Approved, and Media Failed email notifications are sent to all users with the Manage Requests permission.', - emailNotificationTypesAlertDescriptionPt2: - 'Media Approved, Media Declined, and Media Available email notifications are sent to the user who submitted the request.', pgpPrivateKey: 'PGP Private Key', pgpPrivateKeyTip: 'Sign encrypted email messages using OpenPGP', - validationPgpPrivateKey: - 'You must provide a valid PGP private key if a PGP password is entered', + validationPgpPrivateKey: 'You must provide a valid PGP private key', pgpPassword: 'PGP Password', pgpPasswordTip: 'Sign encrypted email messages using OpenPGP', - validationPgpPassword: - 'You must provide a PGP password if a PGP private key is entered', + validationPgpPassword: 'You must provide a PGP password', }); export function OpenPgpLink(msg: string): JSX.Element { @@ -130,7 +122,6 @@ const NotificationsEmail: React.FC = () => { { try { await axios.post('/api/v1/settings/notifications/email', { enabled: values.enabled, - types: values.types, options: { emailFrom: values.emailFrom, smtpHost: values.smtpHost, @@ -184,7 +174,7 @@ const NotificationsEmail: React.FC = () => { } }} > - {({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => { + {({ errors, touched, isSubmitting, values, isValid }) => { const testSettings = async () => { setIsTesting(true); let toastId: string | undefined; @@ -201,7 +191,6 @@ const NotificationsEmail: React.FC = () => { ); await axios.post('/api/v1/settings/notifications/email/test', { enabled: true, - types: values.types, options: { emailFrom: values.emailFrom, smtpHost: values.smtpHost, @@ -238,274 +227,234 @@ const NotificationsEmail: React.FC = () => { }; return ( - <> - -

- {intl.formatMessage( - messages.emailNotificationTypesAlertDescription, - { - strong: function strong(msg) { - return ( - - {msg} - - ); - }, - } - )} -

-

- {intl.formatMessage( - messages.emailNotificationTypesAlertDescriptionPt2, - { - strong: function strong(msg) { - return ( - - {msg} - - ); - }, - } - )} -

- - } - type="info" - /> -
-
- -
- -
-
-
- -
-
- -
-
+ +
+ +
+
-
- -
-
- -
- {errors.emailFrom && touched.emailFrom && ( -
{errors.emailFrom}
- )} +
+
+ +
+
+
-
- -
-
- -
- {errors.smtpHost && touched.smtpHost && ( -
{errors.smtpHost}
- )} -
-
-
- -
+
+
+ +
+
- {errors.smtpPort && touched.smtpPort && ( -
{errors.smtpPort}
- )} -
-
-
- -
-
- - - - - - -
+ {errors.emailFrom && touched.emailFrom && ( +
{errors.emailFrom}
+ )}
-
- -
+
+
+ +
+
+ {errors.smtpHost && touched.smtpHost && ( +
{errors.smtpHost}
+ )} +
+
+
+ +
+ + {errors.smtpPort && touched.smtpPort && ( +
{errors.smtpPort}
+ )}
-
- -
-
- -
+
+
+ +
+
+ + + + + +
-
- -
-
- -
+
+
+ +
+ +
+
+
+ +
+
+
-
- -
-
- -
- {errors.pgpPrivateKey && touched.pgpPrivateKey && ( -
{errors.pgpPrivateKey}
- )} +
+
+ +
+
+
-
- -
-
- -
- {errors.pgpPassword && touched.pgpPassword && ( -
{errors.pgpPassword}
- )} +
+
+ +
+
+
+ {errors.pgpPrivateKey && touched.pgpPrivateKey && ( +
{errors.pgpPrivateKey}
+ )}
- setFieldValue('types', newTypes)} - /> -
-
- - - - - - +
+
+ +
+
+
+ {errors.pgpPassword && touched.pgpPassword && ( +
{errors.pgpPassword}
+ )} +
+
+
+
+ + + + + +
- - +
+ ); }} diff --git a/src/components/Settings/Notifications/NotificationsLunaSea/index.tsx b/src/components/Settings/Notifications/NotificationsLunaSea/index.tsx index 35ff51c4..2141a33a 100644 --- a/src/components/Settings/Notifications/NotificationsLunaSea/index.tsx +++ b/src/components/Settings/Notifications/NotificationsLunaSea/index.tsx @@ -23,6 +23,7 @@ const messages = defineMessages({ toastLunaSeaTestSending: 'Sending LunaSea test notification…', toastLunaSeaTestSuccess: 'LunaSea test notification sent!', toastLunaSeaTestFailed: 'LunaSea test notification failed to send.', + validationTypes: 'You must select at least one notification type', }); const NotificationsLunaSea: React.FC = () => { @@ -43,6 +44,13 @@ const NotificationsLunaSea: React.FC = () => { otherwise: Yup.string().nullable(), }) .url(intl.formatMessage(messages.validationWebhookUrl)), + types: Yup.number().when('enabled', { + is: true, + then: Yup.number() + .nullable() + .moreThan(0, intl.formatMessage(messages.validationTypes)), + otherwise: Yup.number().nullable(), + }), }); if (!data && !error) { @@ -82,7 +90,15 @@ const NotificationsLunaSea: React.FC = () => { } }} > - {({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => { + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + setFieldTouched, + }) => { const testSettings = async () => { setIsTesting(true); let toastId: string | undefined; @@ -190,8 +206,20 @@ const NotificationsLunaSea: React.FC = () => {
setFieldValue('types', newTypes)} + currentTypes={values.enabled ? values.types : 0} + onUpdate={(newTypes) => { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + errors.types && touched.types + ? (errors.types as string) + : undefined + } />
diff --git a/src/components/Settings/Notifications/NotificationsPushbullet/index.tsx b/src/components/Settings/Notifications/NotificationsPushbullet/index.tsx index e6d877ba..81185ec7 100644 --- a/src/components/Settings/Notifications/NotificationsPushbullet/index.tsx +++ b/src/components/Settings/Notifications/NotificationsPushbullet/index.tsx @@ -23,6 +23,7 @@ const messages = defineMessages({ toastPushbulletTestSending: 'Sending Pushbullet test notification…', toastPushbulletTestSuccess: 'Pushbullet test notification sent!', toastPushbulletTestFailed: 'Pushbullet test notification failed to send.', + validationTypes: 'You must select at least one notification type', }); const NotificationsPushbullet: React.FC = () => { @@ -41,6 +42,13 @@ const NotificationsPushbullet: React.FC = () => { .required(intl.formatMessage(messages.validationAccessTokenRequired)), otherwise: Yup.string().nullable(), }), + types: Yup.number().when('enabled', { + is: true, + then: Yup.number() + .nullable() + .moreThan(0, intl.formatMessage(messages.validationTypes)), + otherwise: Yup.number().nullable(), + }), }); if (!data && !error) { @@ -78,7 +86,15 @@ const NotificationsPushbullet: React.FC = () => { } }} > - {({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => { + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + setFieldTouched, + }) => { const testSettings = async () => { setIsTesting(true); let toastId: string | undefined; @@ -170,8 +186,20 @@ const NotificationsPushbullet: React.FC = () => {
setFieldValue('types', newTypes)} + currentTypes={values.enabled ? values.types : 0} + onUpdate={(newTypes) => { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + errors.types && touched.types + ? (errors.types as string) + : undefined + } />
diff --git a/src/components/Settings/Notifications/NotificationsPushover/index.tsx b/src/components/Settings/Notifications/NotificationsPushover/index.tsx index 922cf64d..11f25608 100644 --- a/src/components/Settings/Notifications/NotificationsPushover/index.tsx +++ b/src/components/Settings/Notifications/NotificationsPushover/index.tsx @@ -19,12 +19,13 @@ const messages = defineMessages({ userTokenTip: 'Your 30-character user or group identifier', validationAccessTokenRequired: 'You must provide a valid application token', - validationUserTokenRequired: 'You must provide a valid user key', + validationUserTokenRequired: 'You must provide a valid user or group key', pushoversettingssaved: 'Pushover notification settings saved successfully!', pushoversettingsfailed: 'Pushover notification settings failed to save.', toastPushoverTestSending: 'Sending Pushover test notification…', toastPushoverTestSuccess: 'Pushover test notification sent!', toastPushoverTestFailed: 'Pushover test notification failed to send.', + validationTypes: 'You must select at least one notification type', }); const NotificationsPushover: React.FC = () => { @@ -60,6 +61,13 @@ const NotificationsPushover: React.FC = () => { /^[a-z\d]{30}$/i, intl.formatMessage(messages.validationUserTokenRequired) ), + types: Yup.number().when('enabled', { + is: true, + then: Yup.number() + .nullable() + .moreThan(0, intl.formatMessage(messages.validationTypes)), + otherwise: Yup.number().nullable(), + }), }); if (!data && !error) { @@ -99,7 +107,15 @@ const NotificationsPushover: React.FC = () => { } }} > - {({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => { + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + setFieldTouched, + }) => { const testSettings = async () => { setIsTesting(true); let toastId: string | undefined; @@ -216,8 +232,20 @@ const NotificationsPushover: React.FC = () => {
setFieldValue('types', newTypes)} + currentTypes={values.enabled ? values.types : 0} + onUpdate={(newTypes) => { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + errors.types && touched.types + ? (errors.types as string) + : undefined + } />
diff --git a/src/components/Settings/Notifications/NotificationsSlack/index.tsx b/src/components/Settings/Notifications/NotificationsSlack/index.tsx index 57a7361d..f5a9e85f 100644 --- a/src/components/Settings/Notifications/NotificationsSlack/index.tsx +++ b/src/components/Settings/Notifications/NotificationsSlack/index.tsx @@ -21,6 +21,7 @@ const messages = defineMessages({ toastSlackTestSuccess: 'Slack test notification sent!', toastSlackTestFailed: 'Slack test notification failed to send.', validationWebhookUrl: 'You must provide a valid URL', + validationTypes: 'You must select at least one notification type', }); const NotificationsSlack: React.FC = () => { @@ -41,6 +42,13 @@ const NotificationsSlack: React.FC = () => { otherwise: Yup.string().nullable(), }) .url(intl.formatMessage(messages.validationWebhookUrl)), + types: Yup.number().when('enabled', { + is: true, + then: Yup.number() + .nullable() + .moreThan(0, intl.formatMessage(messages.validationTypes)), + otherwise: Yup.number().nullable(), + }), }); if (!data && !error) { @@ -78,7 +86,15 @@ const NotificationsSlack: React.FC = () => { } }} > - {({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => { + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + setFieldTouched, + }) => { const testSettings = async () => { setIsTesting(true); let toastId: string | undefined; @@ -168,8 +184,20 @@ const NotificationsSlack: React.FC = () => {
setFieldValue('types', newTypes)} + currentTypes={values.enabled ? values.types : 0} + onUpdate={(newTypes) => { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + errors.types && touched.types + ? (errors.types as string) + : undefined + } />
diff --git a/src/components/Settings/Notifications/NotificationsTelegram.tsx b/src/components/Settings/Notifications/NotificationsTelegram.tsx index 30e8f416..70261da5 100644 --- a/src/components/Settings/Notifications/NotificationsTelegram.tsx +++ b/src/components/Settings/Notifications/NotificationsTelegram.tsx @@ -105,7 +105,15 @@ const NotificationsTelegram: React.FC = () => { } }} > - {({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => { + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + setFieldTouched, + }) => { const testSettings = async () => { setIsTesting(true); let toastId: string | undefined; @@ -232,6 +240,24 @@ const NotificationsTelegram: React.FC = () => {
@@ -254,8 +280,20 @@ const NotificationsTelegram: React.FC = () => {
setFieldValue('types', newTypes)} + currentTypes={values.enabled ? values.types : 0} + onUpdate={(newTypes) => { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + errors.types && touched.types + ? (errors.types as string) + : undefined + } />
diff --git a/src/components/Settings/Notifications/NotificationsWebPush/index.tsx b/src/components/Settings/Notifications/NotificationsWebPush/index.tsx index f3e0c6d7..1fc54b8d 100644 --- a/src/components/Settings/Notifications/NotificationsWebPush/index.tsx +++ b/src/components/Settings/Notifications/NotificationsWebPush/index.tsx @@ -8,7 +8,6 @@ import globalMessages from '../../../../i18n/globalMessages'; import Alert from '../../../Common/Alert'; import Button from '../../../Common/Button'; import LoadingSpinner from '../../../Common/LoadingSpinner'; -import NotificationTypeSelector from '../../../NotificationTypeSelector'; const messages = defineMessages({ agentenabled: 'Enable Agent', @@ -49,13 +48,11 @@ const NotificationsWebPush: React.FC = () => { { try { await axios.post('/api/v1/settings/notifications/webpush', { enabled: values.enabled, - types: values.types, options: {}, }); mutate('/api/v1/settings/public'); @@ -73,7 +70,7 @@ const NotificationsWebPush: React.FC = () => { } }} > - {({ isSubmitting, values, isValid, setFieldValue }) => { + {({ isSubmitting }) => { const testSettings = async () => { setIsTesting(true); let toastId: string | undefined; @@ -90,7 +87,6 @@ const NotificationsWebPush: React.FC = () => { ); await axios.post('/api/v1/settings/notifications/webpush/test', { enabled: true, - types: values.types, options: {}, }); @@ -125,16 +121,12 @@ const NotificationsWebPush: React.FC = () => {
- setFieldValue('types', newTypes)} - />
setFieldValue('types', newTypes)} + currentTypes={values.enabled ? values.types : 0} + onUpdate={(newTypes) => { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + errors.types && touched.types + ? (errors.types as string) + : undefined + } />
diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx index d5db14ad..e07a8ef6 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx @@ -11,12 +11,11 @@ import { useUser } from '../../../../hooks/useUser'; import globalMessages from '../../../../i18n/globalMessages'; import Button from '../../../Common/Button'; import LoadingSpinner from '../../../Common/LoadingSpinner'; -import { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector'; +import NotificationTypeSelector from '../../../NotificationTypeSelector'; const messages = defineMessages({ discordsettingssaved: 'Discord notification settings saved successfully!', discordsettingsfailed: 'Discord notification settings failed to save.', - enableDiscord: 'Enable Mentions', discordId: 'User ID', discordIdTip: 'The ID number for your user account', @@ -34,8 +33,8 @@ const UserNotificationsDiscord: React.FC = () => { const UserNotificationsDiscordSchema = Yup.object().shape({ discordId: Yup.string() - .when('enableDiscord', { - is: true, + .when('types', { + is: (value: unknown) => !!value, then: Yup.string() .nullable() .required(intl.formatMessage(messages.validationDiscordId)), @@ -51,8 +50,10 @@ const UserNotificationsDiscord: React.FC = () => { return ( { telegramChatId: data?.telegramChatId, telegramSendSilently: data?.telegramSendSilently, notificationTypes: { - discord: values.enableDiscord ? ALL_NOTIFICATIONS : 0, + discord: values.types, }, }); addToast(intl.formatMessage(messages.discordsettingssaved), { @@ -81,27 +82,23 @@ const UserNotificationsDiscord: React.FC = () => { } }} > - {({ errors, touched, isSubmitting, isValid }) => { + {({ + errors, + touched, + isSubmitting, + isValid, + values, + setFieldValue, + setFieldTouched, + }) => { return (
- {data?.discordEnabled && ( -
- -
- -
-
- )}
+ { + setFieldValue('types', newTypes); + setFieldTouched('types'); + }} + error={ + errors.types && touched.types + ? (errors.types as string) + : undefined + } + />
diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx index bac478e2..921558c9 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx @@ -1,5 +1,5 @@ import axios from 'axios'; -import { Field, Form, Formik } from 'formik'; +import { Form, Formik } from 'formik'; import { useRouter } from 'next/router'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -12,13 +12,15 @@ import globalMessages from '../../../../i18n/globalMessages'; import Badge from '../../../Common/Badge'; import Button from '../../../Common/Button'; import LoadingSpinner from '../../../Common/LoadingSpinner'; -import { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector'; +import SensitiveInput from '../../../Common/SensitiveInput'; +import NotificationTypeSelector, { + ALL_NOTIFICATIONS, +} from '../../../NotificationTypeSelector'; import { OpenPgpLink } from '../../../Settings/Notifications/NotificationsEmail'; const messages = defineMessages({ emailsettingssaved: 'Email notification settings saved successfully!', emailsettingsfailed: 'Email notification settings failed to save.', - enableEmail: 'Enable Notifications', pgpPublicKey: 'PGP Public Key', pgpPublicKeyTip: 'Encrypt email messages using OpenPGP', @@ -50,8 +52,8 @@ const UserEmailSettings: React.FC = () => { return ( { telegramChatId: data?.telegramChatId, telegramSendSilently: data?.telegramSendSilently, notificationTypes: { - email: values.enableEmail ? ALL_NOTIFICATIONS : 0, + email: values.types, }, }); addToast(intl.formatMessage(messages.emailsettingssaved), { @@ -80,17 +82,17 @@ const UserEmailSettings: React.FC = () => { } }} > - {({ errors, touched, isSubmitting, isValid }) => { + {({ + errors, + touched, + isSubmitting, + isValid, + values, + setFieldValue, + setFieldTouched, + }) => { return ( -
- -
- -
-
- { )}
+ { + setFieldValue('types', newTypes); + setFieldTouched('types'); + }} + error={ + errors.types && touched.types + ? (errors.types as string) + : undefined + } + />
diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx index 26ad4253..dcbe614a 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx @@ -11,12 +11,11 @@ import { useUser } from '../../../../hooks/useUser'; import globalMessages from '../../../../i18n/globalMessages'; import Button from '../../../Common/Button'; import LoadingSpinner from '../../../Common/LoadingSpinner'; -import { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector'; +import NotificationTypeSelector from '../../../NotificationTypeSelector'; const messages = defineMessages({ telegramsettingssaved: 'Telegram notification settings saved successfully!', telegramsettingsfailed: 'Telegram notification settings failed to save.', - enableTelegram: 'Enable Notifications', telegramChatId: 'Chat ID', telegramChatIdTipLong: 'Start a chat, add @get_id_bot, and issue the /my_id command', @@ -36,8 +35,8 @@ const UserTelegramSettings: React.FC = () => { const UserNotificationsTelegramSchema = Yup.object().shape({ telegramChatId: Yup.string() - .when('enableTelegram', { - is: true, + .when('types', { + is: (value: unknown) => !!value, then: Yup.string() .nullable() .required(intl.formatMessage(messages.validationTelegramChatId)), @@ -56,9 +55,9 @@ const UserTelegramSettings: React.FC = () => { return ( { telegramChatId: values.telegramChatId, telegramSendSilently: values.telegramSendSilently, notificationTypes: { - telegram: values.enableTelegram ? ALL_NOTIFICATIONS : 0, + telegram: values.types, }, }); addToast(intl.formatMessage(messages.telegramsettingssaved), { @@ -87,21 +86,17 @@ const UserTelegramSettings: React.FC = () => { } }} > - {({ errors, touched, isSubmitting, isValid }) => { + {({ + errors, + touched, + isSubmitting, + isValid, + values, + setFieldValue, + setFieldTouched, + }) => { return ( -
- -
- -
-
+ { + setFieldValue('types', newTypes); + setFieldTouched('types'); + }} + error={ + errors.types && touched.types + ? (errors.types as string) + : undefined + } + />
diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush.tsx index 6ae064ef..f2ff47c6 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush.tsx @@ -1,5 +1,5 @@ import axios from 'axios'; -import { Field, Form, Formik } from 'formik'; +import { Form, Formik } from 'formik'; import { useRouter } from 'next/router'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -10,12 +10,13 @@ import { useUser } from '../../../../hooks/useUser'; import globalMessages from '../../../../i18n/globalMessages'; import Button from '../../../Common/Button'; import LoadingSpinner from '../../../Common/LoadingSpinner'; -import { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector'; +import NotificationTypeSelector, { + ALL_NOTIFICATIONS, +} from '../../../NotificationTypeSelector'; const messages = defineMessages({ webpushsettingssaved: 'Web push notification settings saved successfully!', webpushsettingsfailed: 'Web push notification settings failed to save.', - enableWebPush: 'Enable Notifications', }); const UserWebPushSettings: React.FC = () => { @@ -34,18 +35,18 @@ const UserWebPushSettings: React.FC = () => { return ( { try { await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { + pgpKey: data?.pgpKey, discordId: data?.discordId, telegramChatId: data?.telegramChatId, telegramSendSilently: data?.telegramSendSilently, notificationTypes: { - webpush: values.enableWebPush ? ALL_NOTIFICATIONS : 0, + webpush: values.types, }, }); mutate('/api/v1/settings/public'); @@ -63,21 +64,30 @@ const UserWebPushSettings: React.FC = () => { } }} > - {({ isSubmitting, isValid }) => { + {({ + errors, + touched, + isSubmitting, + isValid, + values, + setFieldValue, + setFieldTouched, + }) => { return ( -
- -
- -
-
+ { + setFieldValue('types', newTypes); + setFieldTouched('types'); + }} + error={ + errors.types && touched.types + ? (errors.types as string) + : undefined + } + />
diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx index 62f6edc9..5d2953a1 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx @@ -42,6 +42,18 @@ const UserNotificationSettings: React.FC = ({ children }) => { regex: /\/settings\/notifications\/email/, hidden: !data?.emailEnabled, }, + { + text: intl.formatMessage(messages.webpush), + content: ( + + + {intl.formatMessage(messages.webpush)} + + ), + route: '/settings/notifications/webpush', + regex: /\/settings\/notifications\/webpush/, + hidden: !data?.webPushEnabled, + }, { text: 'Discord', content: ( @@ -65,18 +77,6 @@ const UserNotificationSettings: React.FC = ({ children }) => { regex: /\/settings\/notifications\/telegram/, hidden: !data?.telegramEnabled || !data?.telegramBotUsername, }, - { - text: intl.formatMessage(messages.webpush), - content: ( - - - {intl.formatMessage(messages.webpush)} - - ), - route: '/settings/notifications/webpush', - regex: /\/settings\/notifications\/webpush/, - hidden: !data?.webPushEnabled, - }, ]; settingsRoutes.forEach((settingsRoute) => { diff --git a/src/components/UserProfile/UserSettings/index.tsx b/src/components/UserProfile/UserSettings/index.tsx index 74dffbd7..ee4b74e5 100644 --- a/src/components/UserProfile/UserSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/index.tsx @@ -65,6 +65,8 @@ const UserSettings: React.FC = ({ children }) => { text: intl.formatMessage(messages.menuNotifications), route: data?.emailEnabled ? '/settings/notifications/email' + : data?.webPushEnabled + ? '/settings/notifications/webpush' : '/settings/notifications/discord', regex: /\/settings\/notifications/, }, diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 9f3a8849..0f9acdd1 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -89,18 +89,24 @@ "components.MovieDetails.viewfullcrew": "View Full Crew", "components.MovieDetails.watchtrailer": "Watch Trailer", "components.NotificationTypeSelector.mediaAutoApproved": "Media Automatically Approved", - "components.NotificationTypeSelector.mediaAutoApprovedDescription": "Sends a notification when requested media is automatically approved.", + "components.NotificationTypeSelector.mediaAutoApprovedDescription": "Send notifications when users submit new media requests which are automatically approved.", "components.NotificationTypeSelector.mediaapproved": "Media Approved", - "components.NotificationTypeSelector.mediaapprovedDescription": "Sends a notification when requested media is manually approved.", + "components.NotificationTypeSelector.mediaapprovedDescription": "Send notifications when media requests are manually approved.", "components.NotificationTypeSelector.mediaavailable": "Media Available", - "components.NotificationTypeSelector.mediaavailableDescription": "Sends a notification when requested media becomes available.", + "components.NotificationTypeSelector.mediaavailableDescription": "Send notifications when media requests become available.", "components.NotificationTypeSelector.mediadeclined": "Media Declined", - "components.NotificationTypeSelector.mediadeclinedDescription": "Sends a notification when a media request is declined.", + "components.NotificationTypeSelector.mediadeclinedDescription": "Send notifications when media requests are declined.", "components.NotificationTypeSelector.mediafailed": "Media Failed", - "components.NotificationTypeSelector.mediafailedDescription": "Sends a notification when requested media fails to be added to Radarr or Sonarr.", + "components.NotificationTypeSelector.mediafailedDescription": "Send notifications when media requests fail to be added to Radarr or Sonarr.", "components.NotificationTypeSelector.mediarequested": "Media Requested", - "components.NotificationTypeSelector.mediarequestedDescription": "Sends a notification when media is requested and requires approval.", + "components.NotificationTypeSelector.mediarequestedDescription": "Send notifications when users submit new media requests which require approval.", "components.NotificationTypeSelector.notificationTypes": "Notification Types", + "components.NotificationTypeSelector.usermediaAutoApprovedDescription": "Get notified when other users submit new media requests which are automatically approved.", + "components.NotificationTypeSelector.usermediaapprovedDescription": "Get notified when your media requests are approved.", + "components.NotificationTypeSelector.usermediaavailableDescription": "Get notified when your media requests become available.", + "components.NotificationTypeSelector.usermediadeclinedDescription": "Get notified when your media requests are declined.", + "components.NotificationTypeSelector.usermediafailedDescription": "Get notified when media requests fail to be added to Radarr or Sonarr.", + "components.NotificationTypeSelector.usermediarequestedDescription": "Get notified when other users submit new media requests which require approval.", "components.PermissionEdit.admin": "Admin", "components.PermissionEdit.adminDescription": "Full administrator access. Bypasses all other permission checks.", "components.PermissionEdit.advancedrequest": "Advanced Requests", @@ -261,6 +267,7 @@ "components.Settings.Notifications.NotificationsLunaSea.toastLunaSeaTestFailed": "LunaSea test notification failed to send.", "components.Settings.Notifications.NotificationsLunaSea.toastLunaSeaTestSending": "Sending LunaSea test notification…", "components.Settings.Notifications.NotificationsLunaSea.toastLunaSeaTestSuccess": "LunaSea test notification sent!", + "components.Settings.Notifications.NotificationsLunaSea.validationTypes": "You must select at least one notification type", "components.Settings.Notifications.NotificationsLunaSea.validationWebhookUrl": "You must provide a valid URL", "components.Settings.Notifications.NotificationsLunaSea.webhookUrl": "Webhook URL", "components.Settings.Notifications.NotificationsLunaSea.webhookUrlTip": "Your user- or device-based notification webhook URL", @@ -273,6 +280,7 @@ "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "Sending Pushbullet test notification…", "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "Pushbullet test notification sent!", "components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "You must provide an access token", + "components.Settings.Notifications.NotificationsPushbullet.validationTypes": "You must select at least one notification type", "components.Settings.Notifications.NotificationsPushover.accessToken": "Application API Token", "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "Register an application for use with Overseerr", "components.Settings.Notifications.NotificationsPushover.agentenabled": "Enable Agent", @@ -284,13 +292,15 @@ "components.Settings.Notifications.NotificationsPushover.userToken": "User or Group Key", "components.Settings.Notifications.NotificationsPushover.userTokenTip": "Your 30-character user or group identifier", "components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "You must provide a valid application token", - "components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "You must provide a valid user key", + "components.Settings.Notifications.NotificationsPushover.validationTypes": "You must select at least one notification type", + "components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "You must provide a valid user or group key", "components.Settings.Notifications.NotificationsSlack.agentenabled": "Enable Agent", "components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Slack notification settings failed to save.", "components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Slack notification settings saved successfully!", "components.Settings.Notifications.NotificationsSlack.toastSlackTestFailed": "Slack test notification failed to send.", "components.Settings.Notifications.NotificationsSlack.toastSlackTestSending": "Sending Slack test notification…", "components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "Slack test notification sent!", + "components.Settings.Notifications.NotificationsSlack.validationTypes": "You must select at least one notification type", "components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "You must provide a valid URL", "components.Settings.Notifications.NotificationsSlack.webhookUrl": "Webhook URL", "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Create an Incoming Webhook integration", @@ -311,6 +321,7 @@ "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "Sending webhook test notification…", "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "Webhook test notification sent!", "components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "You must provide a valid JSON payload", + "components.Settings.Notifications.NotificationsWebhook.validationTypes": "You must select at least one notification type", "components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "You must provide a valid URL", "components.Settings.Notifications.NotificationsWebhook.webhookUrl": "Webhook URL", "components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "Webhook notification settings failed to save.", @@ -328,8 +339,6 @@ "components.Settings.Notifications.chatIdTip": "Start a chat with your bot, add @get_id_bot, and issue the /my_id command", "components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.", "components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved successfully!", - "components.Settings.Notifications.emailNotificationTypesAlertDescription": "Media Requested, Media Automatically Approved, and Media Failed email notifications are sent to all users with the Manage Requests permission.", - "components.Settings.Notifications.emailNotificationTypesAlertDescriptionPt2": "Media Approved, Media Declined, and Media Available email notifications are sent to the user who submitted the request.", "components.Settings.Notifications.emailsender": "Sender Address", "components.Settings.Notifications.emailsettingsfailed": "Email notification settings failed to save.", "components.Settings.Notifications.emailsettingssaved": "Email notification settings saved successfully!", @@ -362,10 +371,11 @@ "components.Settings.Notifications.validationBotAPIRequired": "You must provide a bot authorization token", "components.Settings.Notifications.validationChatIdRequired": "You must provide a valid chat ID", "components.Settings.Notifications.validationEmail": "You must provide a valid email address", - "components.Settings.Notifications.validationPgpPassword": "You must provide a PGP password if a PGP private key is entered", - "components.Settings.Notifications.validationPgpPrivateKey": "You must provide a valid PGP private key if a PGP password is entered", + "components.Settings.Notifications.validationPgpPassword": "You must provide a PGP password", + "components.Settings.Notifications.validationPgpPrivateKey": "You must provide a valid PGP private key", "components.Settings.Notifications.validationSmtpHostRequired": "You must provide a valid hostname or IP address", "components.Settings.Notifications.validationSmtpPortRequired": "You must provide a valid port number", + "components.Settings.Notifications.validationTypes": "You must select at least one notification type", "components.Settings.Notifications.validationUrl": "You must provide a valid URL", "components.Settings.Notifications.webhookUrl": "Webhook URL", "components.Settings.Notifications.webhookUrlTip": "Create a webhook integration in your server", @@ -765,10 +775,6 @@ "components.UserProfile.UserSettings.UserNotificationSettings.email": "Email", "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "Email notification settings failed to save.", "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "Email notification settings saved successfully!", - "components.UserProfile.UserSettings.UserNotificationSettings.enableDiscord": "Enable Mentions", - "components.UserProfile.UserSettings.UserNotificationSettings.enableEmail": "Enable Notifications", - "components.UserProfile.UserSettings.UserNotificationSettings.enableTelegram": "Enable Notifications", - "components.UserProfile.UserSettings.UserNotificationSettings.enableWebPush": "Enable Notifications", "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "Notifications", "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Notification Settings", "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "PGP Public Key",