From 42d0637355d58cbe3ccbfa57a5a1580b55b9b7f7 Mon Sep 17 00:00:00 2001 From: Schrottfresser Date: Sun, 20 Apr 2025 12:51:38 +0200 Subject: [PATCH 01/62] feat(notifications): create new notifications page --- .../Settings/SettingsNotifications.tsx | 142 +----------------- src/pages/settings/notifications.tsx | 15 ++ src/pages/settings/notifications/discord.tsx | 19 --- src/pages/settings/notifications/email.tsx | 19 --- src/pages/settings/notifications/gotify.tsx | 19 --- .../settings/notifications/pushbullet.tsx | 19 --- src/pages/settings/notifications/pushover.tsx | 19 --- src/pages/settings/notifications/slack.tsx | 19 --- src/pages/settings/notifications/telegram.tsx | 19 --- src/pages/settings/notifications/webhook.tsx | 19 --- src/pages/settings/notifications/webpush.tsx | 19 --- 11 files changed, 22 insertions(+), 306 deletions(-) create mode 100644 src/pages/settings/notifications.tsx delete mode 100644 src/pages/settings/notifications/discord.tsx delete mode 100644 src/pages/settings/notifications/email.tsx delete mode 100644 src/pages/settings/notifications/gotify.tsx delete mode 100644 src/pages/settings/notifications/pushbullet.tsx delete mode 100644 src/pages/settings/notifications/pushover.tsx delete mode 100644 src/pages/settings/notifications/slack.tsx delete mode 100644 src/pages/settings/notifications/telegram.tsx delete mode 100644 src/pages/settings/notifications/webhook.tsx delete mode 100644 src/pages/settings/notifications/webpush.tsx diff --git a/src/components/Settings/SettingsNotifications.tsx b/src/components/Settings/SettingsNotifications.tsx index 564e4c7343..12c6f60595 100644 --- a/src/components/Settings/SettingsNotifications.tsx +++ b/src/components/Settings/SettingsNotifications.tsx @@ -1,148 +1,21 @@ -import DiscordLogo from '@app/assets/extlogos/discord.svg'; -import GotifyLogo from '@app/assets/extlogos/gotify.svg'; -import NtfyLogo from '@app/assets/extlogos/ntfy.svg'; -import PushbulletLogo from '@app/assets/extlogos/pushbullet.svg'; -import PushoverLogo from '@app/assets/extlogos/pushover.svg'; -import SlackLogo from '@app/assets/extlogos/slack.svg'; -import TelegramLogo from '@app/assets/extlogos/telegram.svg'; import PageTitle from '@app/components/Common/PageTitle'; -import type { SettingsRoute } from '@app/components/Common/SettingsTabs'; -import SettingsTabs from '@app/components/Common/SettingsTabs'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; -import { BoltIcon, CloudIcon, EnvelopeIcon } from '@heroicons/react/24/solid'; import { useIntl } from 'react-intl'; const messages = defineMessages('components.Settings', { notifications: 'Notifications', - notificationsettings: 'Notification Settings', - notificationAgentSettingsDescription: - 'Configure and enable notification agents.', + notificationSettings: 'Notification Settings', + notificationSettingsDescription: + 'Configure and enable global notification agents.', email: 'Email', webhook: 'Webhook', webpush: 'Web Push', }); -type SettingsNotificationsProps = { - children: React.ReactNode; -}; - -const SettingsNotifications = ({ children }: SettingsNotificationsProps) => { +const SettingsNotifications = () => { const intl = useIntl(); - const settingsRoutes: SettingsRoute[] = [ - { - text: intl.formatMessage(messages.email), - content: ( - - - {intl.formatMessage(messages.email)} - - ), - route: '/settings/notifications/email', - regex: /^\/settings\/notifications\/email/, - }, - { - text: intl.formatMessage(messages.webpush), - content: ( - - - {intl.formatMessage(messages.webpush)} - - ), - route: '/settings/notifications/webpush', - regex: /^\/settings\/notifications\/webpush/, - }, - { - text: 'Discord', - content: ( - - - Discord - - ), - route: '/settings/notifications/discord', - regex: /^\/settings\/notifications\/discord/, - }, - { - text: 'Gotify', - content: ( - - - Gotify - - ), - route: '/settings/notifications/gotify', - regex: /^\/settings\/notifications\/gotify/, - }, - { - text: 'ntfy.sh', - content: ( - - - ntfy.sh - - ), - route: '/settings/notifications/ntfy', - regex: /^\/settings\/notifications\/ntfy/, - }, - { - text: 'Pushbullet', - content: ( - - - Pushbullet - - ), - route: '/settings/notifications/pushbullet', - regex: /^\/settings\/notifications\/pushbullet/, - }, - { - text: 'Pushover', - content: ( - - - Pushover - - ), - route: '/settings/notifications/pushover', - regex: /^\/settings\/notifications\/pushover/, - }, - { - text: 'Slack', - content: ( - - - Slack - - ), - route: '/settings/notifications/slack', - regex: /^\/settings\/notifications\/slack/, - }, - { - text: 'Telegram', - content: ( - - - Telegram - - ), - route: '/settings/notifications/telegram', - regex: /^\/settings\/notifications\/telegram/, - }, - { - text: intl.formatMessage(messages.webhook), - content: ( - - - {intl.formatMessage(messages.webhook)} - - ), - route: '/settings/notifications/webhook', - regex: /^\/settings\/notifications\/webhook/, - }, - ]; - return ( <> { intl.formatMessage(globalMessages.settings), ]} /> +

- {intl.formatMessage(messages.notificationsettings)} + {intl.formatMessage(messages.notificationSettings)}

- {intl.formatMessage(messages.notificationAgentSettingsDescription)} + {intl.formatMessage(messages.notificationSettingsDescription)}

- -
{children}
); }; diff --git a/src/pages/settings/notifications.tsx b/src/pages/settings/notifications.tsx new file mode 100644 index 0000000000..f5944fce16 --- /dev/null +++ b/src/pages/settings/notifications.tsx @@ -0,0 +1,15 @@ +import SettingsLayout from '@app/components/Settings/SettingsLayout'; +import SettingsNotifications from '@app/components/Settings/SettingsNotifications'; +import useRouteGuard from '@app/hooks/useRouteGuard'; +import { Permission } from '@server/lib/permissions'; + +const Notifications = () => { + useRouteGuard(Permission.ADMIN); + return ( + + + + ); +}; + +export default Notifications; diff --git a/src/pages/settings/notifications/discord.tsx b/src/pages/settings/notifications/discord.tsx deleted file mode 100644 index 7a583a220b..0000000000 --- a/src/pages/settings/notifications/discord.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import NotificationsDiscord from '@app/components/Settings/Notifications/NotificationsDiscord'; -import SettingsLayout from '@app/components/Settings/SettingsLayout'; -import SettingsNotifications from '@app/components/Settings/SettingsNotifications'; -import useRouteGuard from '@app/hooks/useRouteGuard'; -import { Permission } from '@app/hooks/useUser'; -import type { NextPage } from 'next'; - -const NotificationsPage: NextPage = () => { - useRouteGuard(Permission.ADMIN); - return ( - - - - - - ); -}; - -export default NotificationsPage; diff --git a/src/pages/settings/notifications/email.tsx b/src/pages/settings/notifications/email.tsx deleted file mode 100644 index 2193f17687..0000000000 --- a/src/pages/settings/notifications/email.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import NotificationsEmail from '@app/components/Settings/Notifications/NotificationsEmail'; -import SettingsLayout from '@app/components/Settings/SettingsLayout'; -import SettingsNotifications from '@app/components/Settings/SettingsNotifications'; -import useRouteGuard from '@app/hooks/useRouteGuard'; -import { Permission } from '@app/hooks/useUser'; -import type { NextPage } from 'next'; - -const NotificationsPage: NextPage = () => { - useRouteGuard(Permission.ADMIN); - return ( - - - - - - ); -}; - -export default NotificationsPage; diff --git a/src/pages/settings/notifications/gotify.tsx b/src/pages/settings/notifications/gotify.tsx deleted file mode 100644 index 6ca4bd984f..0000000000 --- a/src/pages/settings/notifications/gotify.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import NotificationsGotify from '@app/components/Settings/Notifications/NotificationsGotify'; -import SettingsLayout from '@app/components/Settings/SettingsLayout'; -import SettingsNotifications from '@app/components/Settings/SettingsNotifications'; -import useRouteGuard from '@app/hooks/useRouteGuard'; -import { Permission } from '@app/hooks/useUser'; -import type { NextPage } from 'next'; - -const NotificationsPage: NextPage = () => { - useRouteGuard(Permission.ADMIN); - return ( - - - - - - ); -}; - -export default NotificationsPage; diff --git a/src/pages/settings/notifications/pushbullet.tsx b/src/pages/settings/notifications/pushbullet.tsx deleted file mode 100644 index 50c1d98bf5..0000000000 --- a/src/pages/settings/notifications/pushbullet.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import NotificationsPushbullet from '@app/components/Settings/Notifications/NotificationsPushbullet'; -import SettingsLayout from '@app/components/Settings/SettingsLayout'; -import SettingsNotifications from '@app/components/Settings/SettingsNotifications'; -import useRouteGuard from '@app/hooks/useRouteGuard'; -import { Permission } from '@app/hooks/useUser'; -import type { NextPage } from 'next'; - -const NotificationsPage: NextPage = () => { - useRouteGuard(Permission.ADMIN); - return ( - - - - - - ); -}; - -export default NotificationsPage; diff --git a/src/pages/settings/notifications/pushover.tsx b/src/pages/settings/notifications/pushover.tsx deleted file mode 100644 index 4211e668ca..0000000000 --- a/src/pages/settings/notifications/pushover.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import NotificationsPushover from '@app/components/Settings/Notifications/NotificationsPushover'; -import SettingsLayout from '@app/components/Settings/SettingsLayout'; -import SettingsNotifications from '@app/components/Settings/SettingsNotifications'; -import useRouteGuard from '@app/hooks/useRouteGuard'; -import { Permission } from '@app/hooks/useUser'; -import type { NextPage } from 'next'; - -const NotificationsPage: NextPage = () => { - useRouteGuard(Permission.ADMIN); - return ( - - - - - - ); -}; - -export default NotificationsPage; diff --git a/src/pages/settings/notifications/slack.tsx b/src/pages/settings/notifications/slack.tsx deleted file mode 100644 index 1ee8a963ea..0000000000 --- a/src/pages/settings/notifications/slack.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import NotificationsSlack from '@app/components/Settings/Notifications/NotificationsSlack'; -import SettingsLayout from '@app/components/Settings/SettingsLayout'; -import SettingsNotifications from '@app/components/Settings/SettingsNotifications'; -import useRouteGuard from '@app/hooks/useRouteGuard'; -import { Permission } from '@app/hooks/useUser'; -import type { NextPage } from 'next'; - -const NotificationsSlackPage: NextPage = () => { - useRouteGuard(Permission.ADMIN); - return ( - - - - - - ); -}; - -export default NotificationsSlackPage; diff --git a/src/pages/settings/notifications/telegram.tsx b/src/pages/settings/notifications/telegram.tsx deleted file mode 100644 index e4d98d5c76..0000000000 --- a/src/pages/settings/notifications/telegram.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import NotificationsTelegram from '@app/components/Settings/Notifications/NotificationsTelegram'; -import SettingsLayout from '@app/components/Settings/SettingsLayout'; -import SettingsNotifications from '@app/components/Settings/SettingsNotifications'; -import useRouteGuard from '@app/hooks/useRouteGuard'; -import { Permission } from '@app/hooks/useUser'; -import type { NextPage } from 'next'; - -const NotificationsPage: NextPage = () => { - useRouteGuard(Permission.ADMIN); - return ( - - - - - - ); -}; - -export default NotificationsPage; diff --git a/src/pages/settings/notifications/webhook.tsx b/src/pages/settings/notifications/webhook.tsx deleted file mode 100644 index 52ce2e0116..0000000000 --- a/src/pages/settings/notifications/webhook.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import NotificationsWebhook from '@app/components/Settings/Notifications/NotificationsWebhook'; -import SettingsLayout from '@app/components/Settings/SettingsLayout'; -import SettingsNotifications from '@app/components/Settings/SettingsNotifications'; -import useRouteGuard from '@app/hooks/useRouteGuard'; -import { Permission } from '@app/hooks/useUser'; -import type { NextPage } from 'next'; - -const NotificationsPage: NextPage = () => { - useRouteGuard(Permission.ADMIN); - return ( - - - - - - ); -}; - -export default NotificationsPage; diff --git a/src/pages/settings/notifications/webpush.tsx b/src/pages/settings/notifications/webpush.tsx deleted file mode 100644 index cf2e295682..0000000000 --- a/src/pages/settings/notifications/webpush.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import NotificationsWebPush from '@app/components/Settings/Notifications/NotificationsWebPush'; -import SettingsLayout from '@app/components/Settings/SettingsLayout'; -import SettingsNotifications from '@app/components/Settings/SettingsNotifications'; -import useRouteGuard from '@app/hooks/useRouteGuard'; -import { Permission } from '@app/hooks/useUser'; -import type { NextPage } from 'next'; - -const NotificationsWebPushPage: NextPage = () => { - useRouteGuard(Permission.ADMIN); - return ( - - - - - - ); -}; - -export default NotificationsWebPushPage; From 3e94fc07c87d750062cbbdbb93071ef18bd14f84 Mon Sep 17 00:00:00 2001 From: Schrottfresser Date: Sun, 20 Apr 2025 17:15:26 +0200 Subject: [PATCH 02/62] feat(notifications): remove special behavior of webhook route --- cypress/config/settings.cypress.json | 2 +- server/lib/notifications/agents/webhook.ts | 6 +----- server/lib/settings/index.ts | 3 +-- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/cypress/config/settings.cypress.json b/cypress/config/settings.cypress.json index e30dde8616..b256a27d1c 100644 --- a/cypress/config/settings.cypress.json +++ b/cypress/config/settings.cypress.json @@ -119,7 +119,7 @@ "types": 0, "options": { "webhookUrl": "", - "jsonPayload": "IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJldmVudFwiOiBcInt7ZXZlbnR9fVwiLFxuICAgIFwic3ViamVjdFwiOiBcInt7c3ViamVjdH19XCIsXG4gICAgXCJtZXNzYWdlXCI6IFwie3ttZXNzYWdlfX1cIixcbiAgICBcImltYWdlXCI6IFwie3tpbWFnZX19XCIsXG4gICAgXCJ7e21lZGlhfX1cIjoge1xuICAgICAgICBcIm1lZGlhX3R5cGVcIjogXCJ7e21lZGlhX3R5cGV9fVwiLFxuICAgICAgICBcInRtZGJJZFwiOiBcInt7bWVkaWFfdG1kYmlkfX1cIixcbiAgICAgICAgXCJ0dmRiSWRcIjogXCJ7e21lZGlhX3R2ZGJpZH19XCIsXG4gICAgICAgIFwic3RhdHVzXCI6IFwie3ttZWRpYV9zdGF0dXN9fVwiLFxuICAgICAgICBcInN0YXR1czRrXCI6IFwie3ttZWRpYV9zdGF0dXM0a319XCJcbiAgICB9LFxuICAgIFwie3tyZXF1ZXN0fX1cIjoge1xuICAgICAgICBcInJlcXVlc3RfaWRcIjogXCJ7e3JlcXVlc3RfaWR9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2VtYWlsXCI6IFwie3tyZXF1ZXN0ZWRCeV9lbWFpbH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfdXNlcm5hbWVcIjogXCJ7e3JlcXVlc3RlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV9hdmF0YXJcIjogXCJ7e3JlcXVlc3RlZEJ5X2F2YXRhcn19XCJcbiAgICB9LFxuICAgIFwie3tpc3N1ZX19XCI6IHtcbiAgICAgICAgXCJpc3N1ZV9pZFwiOiBcInt7aXNzdWVfaWR9fVwiLFxuICAgICAgICBcImlzc3VlX3R5cGVcIjogXCJ7e2lzc3VlX3R5cGV9fVwiLFxuICAgICAgICBcImlzc3VlX3N0YXR1c1wiOiBcInt7aXNzdWVfc3RhdHVzfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2VtYWlsXCI6IFwie3tyZXBvcnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X3VzZXJuYW1lXCI6IFwie3tyZXBvcnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2F2YXRhclwiOiBcInt7cmVwb3J0ZWRCeV9hdmF0YXJ9fVwiXG4gICAgfSxcbiAgICBcInt7Y29tbWVudH19XCI6IHtcbiAgICAgICAgXCJjb21tZW50X21lc3NhZ2VcIjogXCJ7e2NvbW1lbnRfbWVzc2FnZX19XCIsXG4gICAgICAgIFwiY29tbWVudGVkQnlfZW1haWxcIjogXCJ7e2NvbW1lbnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJjb21tZW50ZWRCeV91c2VybmFtZVwiOiBcInt7Y29tbWVudGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcImNvbW1lbnRlZEJ5X2F2YXRhclwiOiBcInt7Y29tbWVudGVkQnlfYXZhdGFyfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW11cbn0i" + "jsonPayload": "" } }, "webpush": { diff --git a/server/lib/notifications/agents/webhook.ts b/server/lib/notifications/agents/webhook.ts index c441cb65bf..1e2f2342fa 100644 --- a/server/lib/notifications/agents/webhook.ts +++ b/server/lib/notifications/agents/webhook.ts @@ -138,11 +138,7 @@ class WebhookAgent } private buildPayload(type: Notification, payload: NotificationPayload) { - const payloadString = Buffer.from( - this.getSettings().options.jsonPayload, - 'base64' - ).toString('utf8'); - + const payloadString = this.getSettings().options.jsonPayload; const parsedJSON = JSON.parse(JSON.parse(payloadString)); return this.parseKeys(parsedJSON, payload, type); diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 60e4769d86..895e680d34 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -493,8 +493,7 @@ class Settings { types: 0, options: { webhookUrl: '', - jsonPayload: - 'IntcbiAgXCJub3RpZmljYXRpb25fdHlwZVwiOiBcInt7bm90aWZpY2F0aW9uX3R5cGV9fVwiLFxuICBcImV2ZW50XCI6IFwie3tldmVudH19XCIsXG4gIFwic3ViamVjdFwiOiBcInt7c3ViamVjdH19XCIsXG4gIFwibWVzc2FnZVwiOiBcInt7bWVzc2FnZX19XCIsXG4gIFwiaW1hZ2VcIjogXCJ7e2ltYWdlfX1cIixcbiAgXCJ7e21lZGlhfX1cIjoge1xuICAgIFwibWVkaWFfdHlwZVwiOiBcInt7bWVkaWFfdHlwZX19XCIsXG4gICAgXCJ0bWRiSWRcIjogXCJ7e21lZGlhX3RtZGJpZH19XCIsXG4gICAgXCJ0dmRiSWRcIjogXCJ7e21lZGlhX3R2ZGJpZH19XCIsXG4gICAgXCJzdGF0dXNcIjogXCJ7e21lZGlhX3N0YXR1c319XCIsXG4gICAgXCJzdGF0dXM0a1wiOiBcInt7bWVkaWFfc3RhdHVzNGt9fVwiXG4gIH0sXG4gIFwie3tyZXF1ZXN0fX1cIjoge1xuICAgIFwicmVxdWVzdF9pZFwiOiBcInt7cmVxdWVzdF9pZH19XCIsXG4gICAgXCJyZXF1ZXN0ZWRCeV9lbWFpbFwiOiBcInt7cmVxdWVzdGVkQnlfZW1haWx9fVwiLFxuICAgIFwicmVxdWVzdGVkQnlfdXNlcm5hbWVcIjogXCJ7e3JlcXVlc3RlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICBcInJlcXVlc3RlZEJ5X2F2YXRhclwiOiBcInt7cmVxdWVzdGVkQnlfYXZhdGFyfX1cIixcbiAgICBcInJlcXVlc3RlZEJ5X3NldHRpbmdzX2Rpc2NvcmRJZFwiOiBcInt7cmVxdWVzdGVkQnlfc2V0dGluZ3NfZGlzY29yZElkfX1cIixcbiAgICBcInJlcXVlc3RlZEJ5X3NldHRpbmdzX3RlbGVncmFtQ2hhdElkXCI6IFwie3tyZXF1ZXN0ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZH19XCJcbiAgfSxcbiAgXCJ7e2lzc3VlfX1cIjoge1xuICAgIFwiaXNzdWVfaWRcIjogXCJ7e2lzc3VlX2lkfX1cIixcbiAgICBcImlzc3VlX3R5cGVcIjogXCJ7e2lzc3VlX3R5cGV9fVwiLFxuICAgIFwiaXNzdWVfc3RhdHVzXCI6IFwie3tpc3N1ZV9zdGF0dXN9fVwiLFxuICAgIFwicmVwb3J0ZWRCeV9lbWFpbFwiOiBcInt7cmVwb3J0ZWRCeV9lbWFpbH19XCIsXG4gICAgXCJyZXBvcnRlZEJ5X3VzZXJuYW1lXCI6IFwie3tyZXBvcnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICBcInJlcG9ydGVkQnlfYXZhdGFyXCI6IFwie3tyZXBvcnRlZEJ5X2F2YXRhcn19XCIsXG4gICAgXCJyZXBvcnRlZEJ5X3NldHRpbmdzX2Rpc2NvcmRJZFwiOiBcInt7cmVwb3J0ZWRCeV9zZXR0aW5nc19kaXNjb3JkSWR9fVwiLFxuICAgIFwicmVwb3J0ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZFwiOiBcInt7cmVwb3J0ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZH19XCJcbiAgfSxcbiAgXCJ7e2NvbW1lbnR9fVwiOiB7XG4gICAgXCJjb21tZW50X21lc3NhZ2VcIjogXCJ7e2NvbW1lbnRfbWVzc2FnZX19XCIsXG4gICAgXCJjb21tZW50ZWRCeV9lbWFpbFwiOiBcInt7Y29tbWVudGVkQnlfZW1haWx9fVwiLFxuICAgIFwiY29tbWVudGVkQnlfdXNlcm5hbWVcIjogXCJ7e2NvbW1lbnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICBcImNvbW1lbnRlZEJ5X2F2YXRhclwiOiBcInt7Y29tbWVudGVkQnlfYXZhdGFyfX1cIixcbiAgICBcImNvbW1lbnRlZEJ5X3NldHRpbmdzX2Rpc2NvcmRJZFwiOiBcInt7Y29tbWVudGVkQnlfc2V0dGluZ3NfZGlzY29yZElkfX1cIixcbiAgICBcImNvbW1lbnRlZEJ5X3NldHRpbmdzX3RlbGVncmFtQ2hhdElkXCI6IFwie3tjb21tZW50ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZH19XCJcbiAgfSxcbiAgXCJ7e2V4dHJhfX1cIjogW11cbn0i', + jsonPayload: '', }, }, webpush: { From 114fda1fe2cbe59909b1e9c13a935011775e644f Mon Sep 17 00:00:00 2001 From: Schrottfresser Date: Sun, 20 Apr 2025 17:24:49 +0200 Subject: [PATCH 03/62] feat(notifications): remove notification agent endpoints --- jellyseerr-api.yml | 459 +----------------- server/lib/settings/index.ts | 39 +- server/routes/settings/notifications.ts | 335 +------------ .../Settings/SettingsNotifications.tsx | 44 ++ 4 files changed, 88 insertions(+), 789 deletions(-) diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index 752b85f862..cd6b9cd2f0 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -3254,233 +3254,22 @@ paths: timestamp: type: string example: '2020-12-15T16:20:00.069Z' - /settings/notifications/email: + /settings/notifications: get: - summary: Get email notification settings - description: Returns current email notification settings in a JSON object. + summary: Get notification settings + description: Returns current notification settings in a JSON object. tags: - settings responses: '200': - description: Returned email settings - content: - application/json: - schema: - $ref: '#/components/schemas/NotificationEmailSettings' - post: - summary: Update email notification settings - description: Updates email notification settings with provided values - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NotificationEmailSettings' - responses: - '200': - description: 'Values were sucessfully updated' - content: - application/json: - schema: - $ref: '#/components/schemas/NotificationEmailSettings' - /settings/notifications/email/test: - post: - summary: Test email settings - description: Sends a test notification to the email agent. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NotificationEmailSettings' - responses: - '204': - description: Test notification attempted - /settings/notifications/discord: - get: - summary: Get Discord notification settings - description: Returns current Discord notification settings in a JSON object. - tags: - - settings - responses: - '200': - description: Returned Discord settings - content: - application/json: - schema: - $ref: '#/components/schemas/DiscordSettings' - post: - summary: Update Discord notification settings - description: Updates Discord notification settings with the provided values. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/DiscordSettings' - responses: - '200': - description: 'Values were sucessfully updated' - content: - application/json: - schema: - $ref: '#/components/schemas/DiscordSettings' - /settings/notifications/discord/test: - post: - summary: Test Discord settings - description: Sends a test notification to the Discord agent. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/DiscordSettings' - responses: - '204': - description: Test notification attempted - /settings/notifications/pushbullet: - get: - summary: Get Pushbullet notification settings - description: Returns current Pushbullet notification settings in a JSON object. - tags: - - settings - responses: - '200': - description: Returned Pushbullet settings - content: - application/json: - schema: - $ref: '#/components/schemas/PushbulletSettings' - post: - summary: Update Pushbullet notification settings - description: Update Pushbullet notification settings with the provided values. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/PushbulletSettings' - responses: - '200': - description: 'Values were sucessfully updated' - content: - application/json: - schema: - $ref: '#/components/schemas/PushbulletSettings' - /settings/notifications/pushbullet/test: - post: - summary: Test Pushbullet settings - description: Sends a test notification to the Pushbullet agent. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/PushbulletSettings' - responses: - '204': - description: Test notification attempted - /settings/notifications/pushover: - get: - summary: Get Pushover notification settings - description: Returns current Pushover notification settings in a JSON object. - tags: - - settings - responses: - '200': - description: Returned Pushover settings - content: - application/json: - schema: - $ref: '#/components/schemas/PushoverSettings' - post: - summary: Update Pushover notification settings - description: Update Pushover notification settings with the provided values. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/PushoverSettings' - responses: - '200': - description: 'Values were sucessfully updated' - content: - application/json: - schema: - $ref: '#/components/schemas/PushoverSettings' - /settings/notifications/pushover/test: - post: - summary: Test Pushover settings - description: Sends a test notification to the Pushover agent. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/PushoverSettings' - responses: - '204': - description: Test notification attempted - /settings/notifications/pushover/sounds: - get: - summary: Get Pushover sounds - description: Returns valid Pushover sound options in a JSON array. - tags: - - settings - parameters: - - in: query - name: token - required: true - schema: - type: string - nullable: false - responses: - '200': - description: Returned Pushover settings - content: - application/json: - schema: - type: array - items: - type: object - properties: - name: - type: string - description: - type: string - /settings/notifications/gotify: - get: - summary: Get Gotify notification settings - description: Returns current Gotify notification settings in a JSON object. - tags: - - settings - responses: - '200': - description: Returned Gotify settings + description: Returned notification settings content: application/json: schema: $ref: '#/components/schemas/GotifySettings' post: - summary: Update Gotify notification settings - description: Update Gotify notification settings with the provided values. + summary: Update notification settings + description: Update notification settings with the provided values. tags: - settings requestBody: @@ -3496,10 +3285,10 @@ paths: application/json: schema: $ref: '#/components/schemas/GotifySettings' - /settings/notifications/gotify/test: + /settings/notifications/test: post: - summary: Test Gotify settings - description: Sends a test notification to the Gotify agent. + summary: Test notification settings + description: Sends a test notification. tags: - settings requestBody: @@ -3511,236 +3300,6 @@ paths: responses: '204': description: Test notification attempted - /settings/notifications/ntfy: - get: - summary: Get ntfy.sh notification settings - description: Returns current ntfy.sh notification settings in a JSON object. - tags: - - settings - responses: - '200': - description: Returned ntfy.sh settings - content: - application/json: - schema: - $ref: '#/components/schemas/NtfySettings' - post: - summary: Update ntfy.sh notification settings - description: Update ntfy.sh notification settings with the provided values. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NtfySettings' - responses: - '200': - description: 'Values were sucessfully updated' - content: - application/json: - schema: - $ref: '#/components/schemas/NtfySettings' - /settings/notifications/ntfy/test: - post: - summary: Test ntfy.sh settings - description: Sends a test notification to the ntfy.sh agent. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NtfySettings' - responses: - '204': - description: Test notification attempted - /settings/notifications/slack: - get: - summary: Get Slack notification settings - description: Returns current Slack notification settings in a JSON object. - tags: - - settings - responses: - '200': - description: Returned slack settings - content: - application/json: - schema: - $ref: '#/components/schemas/SlackSettings' - post: - summary: Update Slack notification settings - description: Updates Slack notification settings with the provided values. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SlackSettings' - responses: - '200': - description: 'Values were sucessfully updated' - content: - application/json: - schema: - $ref: '#/components/schemas/SlackSettings' - /settings/notifications/slack/test: - post: - summary: Test Slack settings - description: Sends a test notification to the Slack agent. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SlackSettings' - responses: - '204': - description: Test notification attempted - /settings/notifications/telegram: - get: - summary: Get Telegram notification settings - description: Returns current Telegram notification settings in a JSON object. - tags: - - settings - responses: - '200': - description: Returned Telegram settings - content: - application/json: - schema: - $ref: '#/components/schemas/TelegramSettings' - post: - summary: Update Telegram notification settings - description: Update Telegram notification settings with the provided values. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/TelegramSettings' - responses: - '200': - description: 'Values were sucessfully updated' - content: - application/json: - schema: - $ref: '#/components/schemas/TelegramSettings' - /settings/notifications/telegram/test: - post: - summary: Test Telegram settings - description: Sends a test notification to the Telegram agent. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/TelegramSettings' - responses: - '204': - description: Test notification attempted - /settings/notifications/webpush: - get: - summary: Get Web Push notification settings - description: Returns current Web Push notification settings in a JSON object. - tags: - - settings - responses: - '200': - description: Returned web push settings - content: - application/json: - schema: - $ref: '#/components/schemas/WebPushSettings' - post: - summary: Update Web Push notification settings - description: Updates Web Push notification settings with the provided values. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/WebPushSettings' - responses: - '200': - description: 'Values were sucessfully updated' - content: - application/json: - schema: - $ref: '#/components/schemas/WebPushSettings' - /settings/notifications/webpush/test: - post: - summary: Test Web Push settings - description: Sends a test notification to the Web Push agent. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/WebPushSettings' - responses: - '204': - description: Test notification attempted - /settings/notifications/webhook: - get: - summary: Get webhook notification settings - description: Returns current webhook notification settings in a JSON object. - tags: - - settings - responses: - '200': - description: Returned webhook settings - content: - application/json: - schema: - $ref: '#/components/schemas/WebhookSettings' - post: - summary: Update webhook notification settings - description: Updates webhook notification settings with the provided values. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/WebhookSettings' - responses: - '200': - description: 'Values were sucessfully updated' - content: - application/json: - schema: - $ref: '#/components/schemas/WebhookSettings' - /settings/notifications/webhook/test: - post: - summary: Test webhook settings - description: Sends a test notification to the webhook agent. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/WebhookSettings' - responses: - '204': - description: Test notification attempted /settings/discover: get: summary: Get all discover sliders diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 895e680d34..a8b1afd27e 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -208,8 +208,12 @@ interface FullPublicSettings extends PublicSettings { export interface NotificationAgentConfig { enabled: boolean; types?: number; + name: string; + id?: number; + type: NotificationAgentKey; options: Record; } + export interface NotificationAgentDiscord extends NotificationAgentConfig { options: { botUsername?: string; @@ -302,6 +306,7 @@ export enum NotificationAgentKey { EMAIL = 'email', GOTIFY = 'gotify', NTFY = 'ntfy', + LUNASEA = 'lunasea', PUSHBULLET = 'pushbullet', PUSHOVER = 'pushover', SLACK = 'slack', @@ -310,7 +315,7 @@ export enum NotificationAgentKey { WEBPUSH = 'webpush', } -interface NotificationAgents { +interface NotificationAgentTemplates { discord: NotificationAgentDiscord; email: NotificationAgentEmail; gotify: NotificationAgentGotify; @@ -324,7 +329,8 @@ interface NotificationAgents { } interface NotificationSettings { - agents: NotificationAgents; + instances: NotificationAgentConfig[]; + agentTemplates: NotificationAgentTemplates; } interface JobSettings { @@ -431,9 +437,12 @@ class Settings { initialized: false, }, notifications: { - agents: { + instances: [], + agentTemplates: { email: { enabled: false, + name: '', + type: NotificationAgentKey.EMAIL, options: { userEmailRequired: false, emailFrom: '', @@ -449,6 +458,8 @@ class Settings { discord: { enabled: false, types: 0, + name: '', + type: NotificationAgentKey.DISCORD, options: { webhookUrl: '', webhookRoleId: '', @@ -458,6 +469,8 @@ class Settings { slack: { enabled: false, types: 0, + name: '', + type: NotificationAgentKey.SLACK, options: { webhookUrl: '', }, @@ -465,6 +478,8 @@ class Settings { telegram: { enabled: false, types: 0, + name: '', + type: NotificationAgentKey.TELEGRAM, options: { botAPI: '', chatId: '', @@ -475,6 +490,8 @@ class Settings { pushbullet: { enabled: false, types: 0, + name: '', + type: NotificationAgentKey.PUSHBULLET, options: { accessToken: '', }, @@ -482,6 +499,8 @@ class Settings { pushover: { enabled: false, types: 0, + name: '', + type: NotificationAgentKey.PUSHOVER, options: { accessToken: '', userToken: '', @@ -491,6 +510,8 @@ class Settings { webhook: { enabled: false, types: 0, + name: '', + type: NotificationAgentKey.WEBHOOK, options: { webhookUrl: '', jsonPayload: '', @@ -498,11 +519,15 @@ class Settings { }, webpush: { enabled: false, + name: '', + type: NotificationAgentKey.WEBPUSH, options: {}, }, gotify: { enabled: false, types: 0, + name: '', + type: NotificationAgentKey.GOTIFY, options: { url: '', token: '', @@ -675,11 +700,11 @@ class Settings { enableSpecialEpisodes: this.data.main.enableSpecialEpisodes, cacheImages: this.data.main.cacheImages, vapidPublic: this.vapidPublic, - enablePushRegistration: this.data.notifications.agents.webpush.enabled, + // TODO no static values here + enablePushRegistration: false, locale: this.data.main.locale, - emailEnabled: this.data.notifications.agents.email.enabled, - userEmailRequired: - this.data.notifications.agents.email.options.userEmailRequired, + emailEnabled: false, + userEmailRequired: false, newPlexLogin: this.data.main.newPlexLogin, youtubeUrl: this.data.main.youtubeUrl, }; diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index cee96b7d7b..0b54d3ae82 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -1,16 +1,7 @@ import type { User } from '@server/entity/User'; import { Notification } from '@server/lib/notifications'; import type { NotificationAgent } from '@server/lib/notifications/agents/agent'; -import DiscordAgent from '@server/lib/notifications/agents/discord'; -import EmailAgent from '@server/lib/notifications/agents/email'; import GotifyAgent from '@server/lib/notifications/agents/gotify'; -import NtfyAgent from '@server/lib/notifications/agents/ntfy'; -import PushbulletAgent from '@server/lib/notifications/agents/pushbullet'; -import PushoverAgent from '@server/lib/notifications/agents/pushover'; -import SlackAgent from '@server/lib/notifications/agents/slack'; -import TelegramAgent from '@server/lib/notifications/agents/telegram'; -import WebhookAgent from '@server/lib/notifications/agents/webhook'; -import WebPushAgent from '@server/lib/notifications/agents/webpush'; import { getSettings } from '@server/lib/settings'; import { Router } from 'express'; @@ -25,333 +16,13 @@ const sendTestNotification = async (agent: NotificationAgent, user: User) => message: 'Check check, 1, 2, 3. Are we coming in clear?', }); -notificationRoutes.get('/discord', (_req, res) => { - const settings = getSettings(); - - res.status(200).json(settings.notifications.agents.discord); -}); - -notificationRoutes.post('/discord', async (req, res) => { - const settings = getSettings(); - - settings.notifications.agents.discord = req.body; - await settings.save(); - - res.status(200).json(settings.notifications.agents.discord); -}); - -notificationRoutes.post('/discord/test', async (req, res, next) => { - if (!req.user) { - return next({ - status: 500, - message: 'User information is missing from the request.', - }); - } - - const discordAgent = new DiscordAgent(req.body); - if (await sendTestNotification(discordAgent, req.user)) { - return res.status(204).send(); - } else { - return next({ - status: 500, - message: 'Failed to send Discord notification.', - }); - } -}); - -notificationRoutes.get('/slack', (_req, res) => { - const settings = getSettings(); - - res.status(200).json(settings.notifications.agents.slack); -}); - -notificationRoutes.post('/slack', async (req, res) => { - const settings = getSettings(); - - settings.notifications.agents.slack = req.body; - await settings.save(); - - res.status(200).json(settings.notifications.agents.slack); -}); - -notificationRoutes.post('/slack/test', async (req, res, next) => { - if (!req.user) { - return next({ - status: 500, - message: 'User information is missing from the request.', - }); - } - - const slackAgent = new SlackAgent(req.body); - if (await sendTestNotification(slackAgent, req.user)) { - return res.status(204).send(); - } else { - return next({ - status: 500, - message: 'Failed to send Slack notification.', - }); - } -}); - -notificationRoutes.get('/telegram', (_req, res) => { - const settings = getSettings(); - - res.status(200).json(settings.notifications.agents.telegram); -}); - -notificationRoutes.post('/telegram', async (req, res) => { - const settings = getSettings(); - - settings.notifications.agents.telegram = req.body; - await settings.save(); - - res.status(200).json(settings.notifications.agents.telegram); -}); - -notificationRoutes.post('/telegram/test', async (req, res, next) => { - if (!req.user) { - return next({ - status: 500, - message: 'User information is missing from the request.', - }); - } - - const telegramAgent = new TelegramAgent(req.body); - if (await sendTestNotification(telegramAgent, req.user)) { - return res.status(204).send(); - } else { - return next({ - status: 500, - message: 'Failed to send Telegram notification.', - }); - } -}); - -notificationRoutes.get('/pushbullet', (_req, res) => { - const settings = getSettings(); - - res.status(200).json(settings.notifications.agents.pushbullet); -}); - -notificationRoutes.post('/pushbullet', async (req, res) => { - const settings = getSettings(); - - settings.notifications.agents.pushbullet = req.body; - await settings.save(); - - res.status(200).json(settings.notifications.agents.pushbullet); -}); - -notificationRoutes.post('/pushbullet/test', async (req, res, next) => { - if (!req.user) { - return next({ - status: 500, - message: 'User information is missing from the request.', - }); - } - - const pushbulletAgent = new PushbulletAgent(req.body); - if (await sendTestNotification(pushbulletAgent, req.user)) { - return res.status(204).send(); - } else { - return next({ - status: 500, - message: 'Failed to send Pushbullet notification.', - }); - } -}); - -notificationRoutes.get('/pushover', (_req, res) => { - const settings = getSettings(); - - res.status(200).json(settings.notifications.agents.pushover); -}); - -notificationRoutes.post('/pushover', async (req, res) => { - const settings = getSettings(); - - settings.notifications.agents.pushover = req.body; - await settings.save(); - - res.status(200).json(settings.notifications.agents.pushover); -}); - -notificationRoutes.post('/pushover/test', async (req, res, next) => { - if (!req.user) { - return next({ - status: 500, - message: 'User information is missing from the request.', - }); - } - - const pushoverAgent = new PushoverAgent(req.body); - if (await sendTestNotification(pushoverAgent, req.user)) { - return res.status(204).send(); - } else { - return next({ - status: 500, - message: 'Failed to send Pushover notification.', - }); - } -}); - -notificationRoutes.get('/email', (_req, res) => { - const settings = getSettings(); - - res.status(200).json(settings.notifications.agents.email); -}); - -notificationRoutes.post('/email', async (req, res) => { - const settings = getSettings(); - - settings.notifications.agents.email = req.body; - await settings.save(); - - res.status(200).json(settings.notifications.agents.email); -}); - -notificationRoutes.post('/email/test', async (req, res, next) => { - if (!req.user) { - return next({ - status: 500, - message: 'User information is missing from the request.', - }); - } - - const emailAgent = new EmailAgent(req.body); - if (await sendTestNotification(emailAgent, req.user)) { - return res.status(204).send(); - } else { - return next({ - status: 500, - message: 'Failed to send email notification.', - }); - } -}); - -notificationRoutes.get('/webpush', (_req, res) => { - const settings = getSettings(); - - res.status(200).json(settings.notifications.agents.webpush); -}); - -notificationRoutes.post('/webpush', async (req, res) => { - const settings = getSettings(); - - settings.notifications.agents.webpush = req.body; - await settings.save(); - - res.status(200).json(settings.notifications.agents.webpush); -}); - -notificationRoutes.post('/webpush/test', async (req, res, next) => { - if (!req.user) { - return next({ - status: 500, - message: 'User information is missing from the request.', - }); - } - - const webpushAgent = new WebPushAgent(req.body); - if (await sendTestNotification(webpushAgent, req.user)) { - return res.status(204).send(); - } else { - return next({ - status: 500, - message: 'Failed to send web push notification.', - }); - } -}); - -notificationRoutes.get('/webhook', (_req, res) => { - const settings = getSettings(); - - const webhookSettings = settings.notifications.agents.webhook; - - const response: typeof webhookSettings = { - enabled: webhookSettings.enabled, - types: webhookSettings.types, - options: { - ...webhookSettings.options, - jsonPayload: JSON.parse( - Buffer.from(webhookSettings.options.jsonPayload, 'base64').toString( - 'utf8' - ) - ), - }, - }; - - res.status(200).json(response); -}); - -notificationRoutes.post('/webhook', async (req, res, next) => { - const settings = getSettings(); - try { - JSON.parse(req.body.options.jsonPayload); - - settings.notifications.agents.webhook = { - enabled: req.body.enabled, - types: req.body.types, - options: { - jsonPayload: Buffer.from(req.body.options.jsonPayload).toString( - 'base64' - ), - webhookUrl: req.body.options.webhookUrl, - authHeader: req.body.options.authHeader, - }, - }; - await settings.save(); - - res.status(200).json(settings.notifications.agents.webhook); - } catch (e) { - next({ status: 500, message: e.message }); - } -}); - -notificationRoutes.post('/webhook/test', async (req, res, next) => { - if (!req.user) { - return next({ - status: 500, - message: 'User information is missing from the request.', - }); - } - - try { - JSON.parse(req.body.options.jsonPayload); - - const testBody = { - enabled: req.body.enabled, - types: req.body.types, - options: { - jsonPayload: Buffer.from(req.body.options.jsonPayload).toString( - 'base64' - ), - webhookUrl: req.body.options.webhookUrl, - authHeader: req.body.options.authHeader, - }, - }; - - const webhookAgent = new WebhookAgent(testBody); - if (await sendTestNotification(webhookAgent, req.user)) { - return res.status(204).send(); - } else { - return next({ - status: 500, - message: 'Failed to send webhook notification.', - }); - } - } catch (e) { - next({ status: 500, message: e.message }); - } -}); - -notificationRoutes.get('/gotify', (_req, res) => { +notificationRoutes.get('/', (_req, res) => { const settings = getSettings(); res.status(200).json(settings.notifications.agents.gotify); }); -notificationRoutes.post('/gotify', async (req, res) => { +notificationRoutes.post('/', async (req, res) => { const settings = getSettings(); settings.notifications.agents.gotify = req.body; @@ -360,7 +31,7 @@ notificationRoutes.post('/gotify', async (req, res) => { res.status(200).json(settings.notifications.agents.gotify); }); -notificationRoutes.post('/gotify/test', async (req, res, next) => { +notificationRoutes.post('/test', async (req, res, next) => { if (!req.user) { return next({ status: 500, diff --git a/src/components/Settings/SettingsNotifications.tsx b/src/components/Settings/SettingsNotifications.tsx index 12c6f60595..9eed585289 100644 --- a/src/components/Settings/SettingsNotifications.tsx +++ b/src/components/Settings/SettingsNotifications.tsx @@ -1,4 +1,5 @@ import PageTitle from '@app/components/Common/PageTitle'; +import Table from '@app/components/Common/Table'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { useIntl } from 'react-intl'; @@ -8,6 +9,9 @@ const messages = defineMessages('components.Settings', { notificationSettings: 'Notification Settings', notificationSettingsDescription: 'Configure and enable global notification agents.', + instanceName: 'Name', + instanceId: 'ID', + notificationAgent: 'Agent', email: 'Email', webhook: 'Webhook', webpush: 'Web Push', @@ -16,6 +20,20 @@ const messages = defineMessages('components.Settings', { const SettingsNotifications = () => { const intl = useIntl(); + /*const { + data, + error, + mutate: revalidate, + } = useSWR( + `/api/v1/settings/notifications?take=${currentPageSize}&skip=${ + pageIndex * currentPageSize + }&sort=${currentSort}` + ); + + if (!data && !error) { + return ; + }*/ + return ( <> { {intl.formatMessage(messages.notificationSettingsDescription)}

+ + + + + + + + {intl.formatMessage(messages.instanceName)} + {intl.formatMessage(messages.instanceId)} + + {intl.formatMessage(messages.notificationAgent)} + + + + + + { + + + + + + + } + +
); }; From 54d74bb31654d323bb1a767717521057766c241a Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Sun, 20 Apr 2025 22:45:40 +0200 Subject: [PATCH 04/62] feat(notifications): move notification agent getSettings to super class --- server/lib/notifications/agents/agent.ts | 24 ++++++++++++++++--- server/lib/notifications/agents/discord.ts | 12 +--------- server/lib/notifications/agents/email.ts | 14 ++--------- server/lib/notifications/agents/gotify.ts | 10 -------- server/lib/notifications/agents/pushbullet.ts | 14 ++--------- server/lib/notifications/agents/pushover.ts | 12 +--------- server/lib/notifications/agents/slack.ts | 12 +--------- server/lib/notifications/agents/telegram.ts | 10 -------- server/lib/notifications/agents/webhook.ts | 15 ++---------- server/lib/notifications/agents/webpush.ts | 10 -------- 10 files changed, 30 insertions(+), 103 deletions(-) diff --git a/server/lib/notifications/agents/agent.ts b/server/lib/notifications/agents/agent.ts index 952e1acf00..8ec7fdb4b6 100644 --- a/server/lib/notifications/agents/agent.ts +++ b/server/lib/notifications/agents/agent.ts @@ -3,7 +3,10 @@ import type IssueComment from '@server/entity/IssueComment'; import type Media from '@server/entity/Media'; import type { MediaRequest } from '@server/entity/MediaRequest'; import type { User } from '@server/entity/User'; -import type { NotificationAgentConfig } from '@server/lib/settings'; +import { + getSettings, + type NotificationAgentConfig, +} from '@server/lib/settings'; import type { Notification } from '..'; export interface NotificationPayload { @@ -25,11 +28,26 @@ export interface NotificationPayload { export abstract class BaseAgent { protected settings?: T; - public constructor(settings?: T) { + protected id: number; + + public constructor(id: number, settings?: T) { this.settings = settings; + this.id = id; } - protected abstract getSettings(): T; + protected getSettings(): NotificationAgentConfig { + if (this.settings) { + return this.settings; + } + + const settings = getSettings(); + + const notificationInstance = settings.notifications.instances.find( + (instance) => instance.id === Number(this.id) + ); + + return notificationInstance as NotificationAgentConfig; + } } export interface NotificationAgent { diff --git a/server/lib/notifications/agents/discord.ts b/server/lib/notifications/agents/discord.ts index cabd332def..d5956c6926 100644 --- a/server/lib/notifications/agents/discord.ts +++ b/server/lib/notifications/agents/discord.ts @@ -95,16 +95,6 @@ class DiscordAgent extends BaseAgent implements NotificationAgent { - protected getSettings(): NotificationAgentDiscord { - if (this.settings) { - return this.settings; - } - - const settings = getSettings(); - - return settings.notifications.agents.discord; - } - public buildEmbed( type: Notification, payload: NotificationPayload @@ -243,7 +233,7 @@ class DiscordAgent type: Notification, payload: NotificationPayload ): Promise { - const settings = this.getSettings(); + const settings = this.getSettings() as NotificationAgentDiscord; if ( !payload.notifySystem || diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 59c5b4aa78..1ef67f349e 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -17,16 +17,6 @@ class EmailAgent extends BaseAgent implements NotificationAgent { - protected getSettings(): NotificationAgentEmail { - if (this.settings) { - return this.settings; - } - - const settings = getSettings(); - - return settings.notifications.agents.email; - } - public shouldSend(): boolean { const settings = this.getSettings(); @@ -216,7 +206,7 @@ class EmailAgent try { const email = new PreparedEmail( - this.getSettings(), + this.getSettings() as NotificationAgentEmail, payload.notifyUser.settings?.pgpKey ); if (EmailValidator.validate(payload.notifyUser.email)) { @@ -278,7 +268,7 @@ class EmailAgent try { const email = new PreparedEmail( - this.getSettings(), + this.getSettings() as NotificationAgentEmail, user.settings?.pgpKey ); if (EmailValidator.validate(user.email)) { diff --git a/server/lib/notifications/agents/gotify.ts b/server/lib/notifications/agents/gotify.ts index 046bb44f65..fc6d8198a3 100644 --- a/server/lib/notifications/agents/gotify.ts +++ b/server/lib/notifications/agents/gotify.ts @@ -18,16 +18,6 @@ class GotifyAgent extends BaseAgent implements NotificationAgent { - protected getSettings(): NotificationAgentGotify { - if (this.settings) { - return this.settings; - } - - const settings = getSettings(); - - return settings.notifications.agents.gotify; - } - public shouldSend(): boolean { const settings = this.getSettings(); diff --git a/server/lib/notifications/agents/pushbullet.ts b/server/lib/notifications/agents/pushbullet.ts index eed4fda917..04e0c2fc64 100644 --- a/server/lib/notifications/agents/pushbullet.ts +++ b/server/lib/notifications/agents/pushbullet.ts @@ -3,7 +3,7 @@ import { MediaStatus } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; import type { NotificationAgentPushbullet } from '@server/lib/settings'; -import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import { NotificationAgentKey } from '@server/lib/settings'; import logger from '@server/logger'; import axios from 'axios'; import { @@ -25,16 +25,6 @@ class PushbulletAgent extends BaseAgent implements NotificationAgent { - protected getSettings(): NotificationAgentPushbullet { - if (this.settings) { - return this.settings; - } - - const settings = getSettings(); - - return settings.notifications.agents.pushbullet; - } - public shouldSend(): boolean { return true; } @@ -105,7 +95,7 @@ class PushbulletAgent type: Notification, payload: NotificationPayload ): Promise { - const settings = this.getSettings(); + const settings = this.getSettings() as NotificationAgentPushbullet; const endpoint = 'https://api.pushbullet.com/v2/pushes'; const notificationPayload = this.getNotificationPayload(type, payload); diff --git a/server/lib/notifications/agents/pushover.ts b/server/lib/notifications/agents/pushover.ts index db5176a765..a0cfef2c07 100644 --- a/server/lib/notifications/agents/pushover.ts +++ b/server/lib/notifications/agents/pushover.ts @@ -34,16 +34,6 @@ class PushoverAgent extends BaseAgent implements NotificationAgent { - protected getSettings(): NotificationAgentPushover { - if (this.settings) { - return this.settings; - } - - const settings = getSettings(); - - return settings.notifications.agents.pushover; - } - public shouldSend(): boolean { return true; } @@ -179,7 +169,7 @@ class PushoverAgent type: Notification, payload: NotificationPayload ): Promise { - const settings = this.getSettings(); + const settings = this.getSettings() as NotificationAgentPushover; const endpoint = 'https://api.pushover.net/1/messages.json'; const notificationPayload = await this.getNotificationPayload( type, diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index 8f1f0c9535..0621f3d454 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -49,16 +49,6 @@ class SlackAgent extends BaseAgent implements NotificationAgent { - protected getSettings(): NotificationAgentSlack { - if (this.settings) { - return this.settings; - } - - const settings = getSettings(); - - return settings.notifications.agents.slack; - } - public buildEmbed( type: Notification, payload: NotificationPayload @@ -223,7 +213,7 @@ class SlackAgent type: Notification, payload: NotificationPayload ): Promise { - const settings = this.getSettings(); + const settings = this.getSettings() as NotificationAgentSlack; if ( !payload.notifySystem || diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index 01d4de4973..860773c638 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -37,16 +37,6 @@ class TelegramAgent { private baseUrl = 'https://api.telegram.org/'; - protected getSettings(): NotificationAgentTelegram { - if (this.settings) { - return this.settings; - } - - const settings = getSettings(); - - return settings.notifications.agents.telegram; - } - public shouldSend(): boolean { const settings = this.getSettings(); diff --git a/server/lib/notifications/agents/webhook.ts b/server/lib/notifications/agents/webhook.ts index 1e2f2342fa..3e97739686 100644 --- a/server/lib/notifications/agents/webhook.ts +++ b/server/lib/notifications/agents/webhook.ts @@ -1,7 +1,6 @@ import { IssueStatus, IssueType } from '@server/constants/issue'; import { MediaStatus } from '@server/constants/media'; import type { NotificationAgentWebhook } from '@server/lib/settings'; -import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import axios from 'axios'; import { get } from 'lodash'; @@ -61,16 +60,6 @@ class WebhookAgent extends BaseAgent implements NotificationAgent { - protected getSettings(): NotificationAgentWebhook { - if (this.settings) { - return this.settings; - } - - const settings = getSettings(); - - return settings.notifications.agents.webhook; - } - private parseKeys( finalPayload: Record, payload: NotificationPayload, @@ -138,7 +127,7 @@ class WebhookAgent } private buildPayload(type: Notification, payload: NotificationPayload) { - const payloadString = this.getSettings().options.jsonPayload; + const payloadString = this.getSettings().options.jsonPayload as string; const parsedJSON = JSON.parse(JSON.parse(payloadString)); return this.parseKeys(parsedJSON, payload, type); @@ -158,7 +147,7 @@ class WebhookAgent type: Notification, payload: NotificationPayload ): Promise { - const settings = this.getSettings(); + const settings = this.getSettings() as NotificationAgentWebhook; if ( !payload.notifySystem || diff --git a/server/lib/notifications/agents/webpush.ts b/server/lib/notifications/agents/webpush.ts index 56472018e8..bd1f7f0532 100644 --- a/server/lib/notifications/agents/webpush.ts +++ b/server/lib/notifications/agents/webpush.ts @@ -28,16 +28,6 @@ class WebPushAgent extends BaseAgent implements NotificationAgent { - protected getSettings(): NotificationAgentConfig { - if (this.settings) { - return this.settings; - } - - const settings = getSettings(); - - return settings.notifications.agents.webpush; - } - private getNotificationPayload( type: Notification, payload: NotificationPayload From 3ed77a78e77aa3abb2e1832fb7ea12f3f6c2b731 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Sun, 20 Apr 2025 22:47:34 +0200 Subject: [PATCH 05/62] feat(notifications): impelement new notifications endpoint --- jellyseerr-api.yml | 235 ++++-------------------- server/routes/settings/notifications.ts | 88 ++++++++- 2 files changed, 113 insertions(+), 210 deletions(-) diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index cd6b9cd2f0..7fe5a6fe2d 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -1388,7 +1388,7 @@ components: results: type: number example: 100 - DiscordSettings: + NotificationSettings: type: object properties: enabled: @@ -1396,202 +1396,14 @@ components: example: false types: type: number - example: 2 - options: - type: object - properties: - botUsername: - type: string - botAvatarUrl: - type: string - webhookUrl: - type: string - webhookRoleId: - type: string - enableMentions: - type: boolean - SlackSettings: - type: object - properties: - enabled: - type: boolean - example: false - types: - type: number - example: 2 - options: - type: object - properties: - webhookUrl: - type: string - WebPushSettings: - type: object - properties: - enabled: - type: boolean - example: false - types: - type: number - example: 2 - WebhookSettings: - type: object - properties: - enabled: - type: boolean - example: false - types: - type: number - example: 2 - options: - type: object - properties: - webhookUrl: - type: string - authHeader: - type: string - jsonPayload: - type: string - TelegramSettings: - type: object - properties: - enabled: - type: boolean - example: false - types: - type: number - example: 2 - options: - type: object - properties: - botUsername: - type: string - botAPI: - type: string - chatId: - type: string - messageThreadId: - type: string - sendSilently: - type: boolean - PushbulletSettings: - type: object - properties: - enabled: - type: boolean - example: false - types: - type: number - example: 2 - options: - type: object - properties: - accessToken: - type: string - channelTag: - type: string - nullable: true - PushoverSettings: - type: object - properties: - enabled: - type: boolean - example: false - types: - type: number - example: 2 - options: - type: object - properties: - accessToken: - type: string - userToken: - type: string - sound: - type: string - GotifySettings: - type: object - properties: - enabled: - type: boolean - example: false - types: - type: number - example: 2 - options: - type: object - properties: - url: - type: string - token: - type: string - NtfySettings: - type: object - properties: - enabled: - type: boolean - example: false - types: - type: number - example: 2 - options: - type: object - properties: - url: - type: string - topic: - type: string - authMethodUsernamePassword: - type: boolean - username: - type: string - password: - type: string - authMethodToken: - type: boolean - token: - type: string - NotificationEmailSettings: - type: object - properties: - enabled: - type: boolean - example: false - types: - type: number - example: 2 + example: 0 + name: + type: string + type: + type: string + example: 'email' options: type: object - properties: - emailFrom: - type: string - example: no-reply@example.com - senderName: - type: string - example: Jellyseerr - smtpHost: - type: string - example: 127.0.0.1 - smtpPort: - type: number - example: 465 - secure: - type: boolean - example: false - ignoreTls: - type: boolean - example: false - requireTls: - type: boolean - example: false - authUser: - type: string - nullable: true - authPass: - type: string - nullable: true - allowSelfSigned: - type: boolean - example: false Job: type: object properties: @@ -3254,49 +3066,70 @@ paths: timestamp: type: string example: '2020-12-15T16:20:00.069Z' - /settings/notifications: + /settings/notifications/{instanceId}: get: summary: Get notification settings description: Returns current notification settings in a JSON object. tags: - settings + parameters: + - in: path + name: instanceId + required: true + schema: + type: number + example: 0 responses: '200': description: Returned notification settings content: application/json: schema: - $ref: '#/components/schemas/GotifySettings' + $ref: '#/components/schemas/NotificationSettings' post: summary: Update notification settings description: Update notification settings with the provided values. tags: - settings + parameters: + - in: path + name: instanceId + required: true + schema: + type: number + example: 0 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/GotifySettings' + $ref: '#/components/schemas/NotificationSettings' responses: '200': description: 'Values were sucessfully updated' content: application/json: schema: - $ref: '#/components/schemas/GotifySettings' - /settings/notifications/test: + $ref: '#/components/schemas/NotificationSettings' + /settings/notifications/{instanceId}/test: post: summary: Test notification settings description: Sends a test notification. tags: - settings + parameters: + - in: path + name: instanceId + required: true + schema: + type: number + example: 0 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/GotifySettings' + $ref: '#/components/schemas/NotificationSettings' responses: '204': description: Test notification attempted diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index 0b54d3ae82..8a3bac5b30 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -1,8 +1,17 @@ import type { User } from '@server/entity/User'; import { Notification } from '@server/lib/notifications'; import type { NotificationAgent } from '@server/lib/notifications/agents/agent'; +import DiscordAgent from '@server/lib/notifications/agents/discord'; +import EmailAgent from '@server/lib/notifications/agents/email'; import GotifyAgent from '@server/lib/notifications/agents/gotify'; -import { getSettings } from '@server/lib/settings'; +import LunaSeaAgent from '@server/lib/notifications/agents/lunasea'; +import PushbulletAgent from '@server/lib/notifications/agents/pushbullet'; +import PushoverAgent from '@server/lib/notifications/agents/pushover'; +import SlackAgent from '@server/lib/notifications/agents/slack'; +import TelegramAgent from '@server/lib/notifications/agents/telegram'; +import WebhookAgent from '@server/lib/notifications/agents/webhook'; +import WebPushAgent from '@server/lib/notifications/agents/webpush'; +import { getSettings, NotificationAgentKey } from '@server/lib/settings'; import { Router } from 'express'; const notificationRoutes = Router(); @@ -16,22 +25,43 @@ const sendTestNotification = async (agent: NotificationAgent, user: User) => message: 'Check check, 1, 2, 3. Are we coming in clear?', }); -notificationRoutes.get('/', (_req, res) => { +notificationRoutes.get<{ id: string }>('/:id', (req, res, next) => { const settings = getSettings(); - res.status(200).json(settings.notifications.agents.gotify); + const notificationInstance = settings.notifications.instances.find( + (instance) => instance.id === Number(req.params.id) + ); + + if (!notificationInstance) { + return next({ status: '404', message: 'Notifications instance not found' }); + } + + res.status(200).json(notificationInstance); }); -notificationRoutes.post('/', async (req, res) => { +notificationRoutes.post<{ id: string }>('/:id', async (req, res) => { const settings = getSettings(); - settings.notifications.agents.gotify = req.body; + let notificationInstanceIndex = settings.notifications.instances.findIndex( + (instance) => instance.id === Number(req.params.id) + ); + + if (notificationInstanceIndex === -1) { + notificationInstanceIndex = settings.notifications.instances.length; + } + + const request = req.body; + request.id = notificationInstanceIndex; + settings.notifications.instances[notificationInstanceIndex] = req.body; + await settings.save(); - res.status(200).json(settings.notifications.agents.gotify); + res + .status(200) + .json(settings.notifications.instances[notificationInstanceIndex]); }); -notificationRoutes.post('/test', async (req, res, next) => { +notificationRoutes.post<{ id: string }>('/:id/test', async (req, res, next) => { if (!req.user) { return next({ status: 500, @@ -39,8 +69,48 @@ notificationRoutes.post('/test', async (req, res, next) => { }); } - const gotifyAgent = new GotifyAgent(req.body); - if (await sendTestNotification(gotifyAgent, req.user)) { + let notificationAgent: NotificationAgent; + const instanceType = req.body.type as NotificationAgentKey; + switch (instanceType) { + case NotificationAgentKey.DISCORD: + notificationAgent = new DiscordAgent(req.body); + break; + case NotificationAgentKey.EMAIL: + notificationAgent = new EmailAgent(req.body); + break; + case NotificationAgentKey.GOTIFY: + notificationAgent = new GotifyAgent(req.body); + break; + case NotificationAgentKey.LUNASEA: + notificationAgent = new LunaSeaAgent(req.body); + break; + case NotificationAgentKey.PUSHBULLET: + notificationAgent = new PushbulletAgent(req.body); + break; + case NotificationAgentKey.PUSHOVER: + notificationAgent = new PushoverAgent(req.body); + break; + case NotificationAgentKey.SLACK: + notificationAgent = new SlackAgent(req.body); + break; + case NotificationAgentKey.TELEGRAM: + notificationAgent = new TelegramAgent(req.body); + break; + case NotificationAgentKey.WEBHOOK: + notificationAgent = new WebhookAgent(req.body); + break; + case NotificationAgentKey.WEBPUSH: + notificationAgent = new WebPushAgent(req.body); + break; + + default: + return next({ + status: 500, + message: 'A valid instance type is missing from the request.', + }); + } + + if (await sendTestNotification(notificationAgent, req.user)) { return res.status(204).send(); } else { return next({ From da30206afc2dc2298a7bd0aec5e7be7b226a8013 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Mon, 21 Apr 2025 00:00:10 +0200 Subject: [PATCH 06/62] feat(notifications): fix notification testing --- server/lib/notifications/agents/agent.ts | 4 ++-- server/lib/notifications/agents/webhook.ts | 2 +- server/routes/settings/notifications.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/lib/notifications/agents/agent.ts b/server/lib/notifications/agents/agent.ts index 8ec7fdb4b6..916a419bb0 100644 --- a/server/lib/notifications/agents/agent.ts +++ b/server/lib/notifications/agents/agent.ts @@ -28,9 +28,9 @@ export interface NotificationPayload { export abstract class BaseAgent { protected settings?: T; - protected id: number; + protected id?: number; - public constructor(id: number, settings?: T) { + public constructor(settings?: T, id?: number) { this.settings = settings; this.id = id; } diff --git a/server/lib/notifications/agents/webhook.ts b/server/lib/notifications/agents/webhook.ts index 3e97739686..58ec1f463c 100644 --- a/server/lib/notifications/agents/webhook.ts +++ b/server/lib/notifications/agents/webhook.ts @@ -128,7 +128,7 @@ class WebhookAgent private buildPayload(type: Notification, payload: NotificationPayload) { const payloadString = this.getSettings().options.jsonPayload as string; - const parsedJSON = JSON.parse(JSON.parse(payloadString)); + const parsedJSON = JSON.parse(payloadString); return this.parseKeys(parsedJSON, payload, type); } diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index 8a3bac5b30..81f6d7e717 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -115,7 +115,7 @@ notificationRoutes.post<{ id: string }>('/:id/test', async (req, res, next) => { } else { return next({ status: 500, - message: 'Failed to send Gotify notification.', + message: `Failed to send ${instanceType} notification.`, }); } }); From a4b78c92a085a92c694143f3e2d3afae3d7f57b4 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Mon, 21 Apr 2025 00:12:19 +0200 Subject: [PATCH 07/62] feat(notifications): add endpoint to get all notification instances --- jellyseerr-api.yml | 25 ++++++++++++++++++++----- server/routes/settings/notifications.ts | 6 ++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index 7fe5a6fe2d..c1f595f671 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -3066,9 +3066,24 @@ paths: timestamp: type: string example: '2020-12-15T16:20:00.069Z' + /settings/notifications: + get: + summary: Get all notification instance settings + description: Returns all current notification instance settings in a JSON object. + tags: + - settings + responses: + '200': + description: Returned all notification settings + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/NotificationSettings' /settings/notifications/{instanceId}: get: - summary: Get notification settings + summary: Get given notification instance settings description: Returns current notification settings in a JSON object. tags: - settings @@ -3087,8 +3102,8 @@ paths: schema: $ref: '#/components/schemas/NotificationSettings' post: - summary: Update notification settings - description: Update notification settings with the provided values. + summary: Add or update given notification instance settings + description: Add or Update given notification instance settings with the provided values. tags: - settings parameters: @@ -3113,8 +3128,8 @@ paths: $ref: '#/components/schemas/NotificationSettings' /settings/notifications/{instanceId}/test: post: - summary: Test notification settings - description: Sends a test notification. + summary: Test given notification instance + description: Sends a test notification from the given instance settings. tags: - settings parameters: diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index 81f6d7e717..ef6862dd48 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -25,6 +25,12 @@ const sendTestNotification = async (agent: NotificationAgent, user: User) => message: 'Check check, 1, 2, 3. Are we coming in clear?', }); +notificationRoutes.get('/', (_req, res) => { + const settings = getSettings(); + + res.status(200).json(settings.notifications.instances); +}); + notificationRoutes.get<{ id: string }>('/:id', (req, res, next) => { const settings = getSettings(); From 69aaf84ed9e6a3533b465a7ee303b4af434d17ba Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Mon, 21 Apr 2025 00:20:36 +0200 Subject: [PATCH 08/62] feat(notifications): rename notification agent config type to agent --- jellyseerr-api.yml | 2 +- server/lib/settings/index.ts | 20 ++++++++++---------- server/routes/settings/notifications.ts | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index c1f595f671..91491742bd 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -1399,7 +1399,7 @@ components: example: 0 name: type: string - type: + agent: type: string example: 'email' options: diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index a8b1afd27e..1f4ab3b8fc 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -210,7 +210,7 @@ export interface NotificationAgentConfig { types?: number; name: string; id?: number; - type: NotificationAgentKey; + agent: NotificationAgentKey; options: Record; } @@ -442,7 +442,7 @@ class Settings { email: { enabled: false, name: '', - type: NotificationAgentKey.EMAIL, + agent: NotificationAgentKey.EMAIL, options: { userEmailRequired: false, emailFrom: '', @@ -459,7 +459,7 @@ class Settings { enabled: false, types: 0, name: '', - type: NotificationAgentKey.DISCORD, + agent: NotificationAgentKey.DISCORD, options: { webhookUrl: '', webhookRoleId: '', @@ -470,7 +470,7 @@ class Settings { enabled: false, types: 0, name: '', - type: NotificationAgentKey.SLACK, + agent: NotificationAgentKey.SLACK, options: { webhookUrl: '', }, @@ -479,7 +479,7 @@ class Settings { enabled: false, types: 0, name: '', - type: NotificationAgentKey.TELEGRAM, + agent: NotificationAgentKey.TELEGRAM, options: { botAPI: '', chatId: '', @@ -491,7 +491,7 @@ class Settings { enabled: false, types: 0, name: '', - type: NotificationAgentKey.PUSHBULLET, + agent: NotificationAgentKey.PUSHBULLET, options: { accessToken: '', }, @@ -500,7 +500,7 @@ class Settings { enabled: false, types: 0, name: '', - type: NotificationAgentKey.PUSHOVER, + agent: NotificationAgentKey.PUSHOVER, options: { accessToken: '', userToken: '', @@ -511,7 +511,7 @@ class Settings { enabled: false, types: 0, name: '', - type: NotificationAgentKey.WEBHOOK, + agent: NotificationAgentKey.WEBHOOK, options: { webhookUrl: '', jsonPayload: '', @@ -520,14 +520,14 @@ class Settings { webpush: { enabled: false, name: '', - type: NotificationAgentKey.WEBPUSH, + agent: NotificationAgentKey.WEBPUSH, options: {}, }, gotify: { enabled: false, types: 0, name: '', - type: NotificationAgentKey.GOTIFY, + agent: NotificationAgentKey.GOTIFY, options: { url: '', token: '', diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index ef6862dd48..3d03fef387 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -76,7 +76,7 @@ notificationRoutes.post<{ id: string }>('/:id/test', async (req, res, next) => { } let notificationAgent: NotificationAgent; - const instanceType = req.body.type as NotificationAgentKey; + const instanceType = req.body.agent as NotificationAgentKey; switch (instanceType) { case NotificationAgentKey.DISCORD: notificationAgent = new DiscordAgent(req.body); From 5b03c62f5f82832707ee8feb937c03345e5661fb Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Mon, 21 Apr 2025 00:40:48 +0200 Subject: [PATCH 09/62] feat(notifications): add notification instance delete endpoint --- jellyseerr-api.yml | 17 ++++++++++++++++- server/routes/settings/notifications.ts | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index 91491742bd..cc64665c4a 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -3121,11 +3121,26 @@ paths: $ref: '#/components/schemas/NotificationSettings' responses: '200': - description: 'Values were sucessfully updated' + description: 'Notification instance was sucessfully added or updated' content: application/json: schema: $ref: '#/components/schemas/NotificationSettings' + delete: + summary: Delete given notification instance + description: Delete given notification instance. + tags: + - settings + parameters: + - in: path + name: instanceId + required: true + schema: + type: number + example: 0 + responses: + '200': + description: 'Notification instance was sucessfully deleted' /settings/notifications/{instanceId}/test: post: summary: Test given notification instance diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index 3d03fef387..8d012bf65b 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -67,6 +67,24 @@ notificationRoutes.post<{ id: string }>('/:id', async (req, res) => { .json(settings.notifications.instances[notificationInstanceIndex]); }); +notificationRoutes.delete<{ id: string }>('/:id', async (req, res, next) => { + const settings = getSettings(); + + const notificationInstanceIndex = settings.notifications.instances.findIndex( + (instance) => instance.id === Number(req.params.id) + ); + + if (notificationInstanceIndex === -1) { + return next({ status: '404', message: 'Notifications instance not found' }); + } + + settings.notifications.instances.splice(notificationInstanceIndex, 1); + + await settings.save(); + + res.status(200).send(); +}); + notificationRoutes.post<{ id: string }>('/:id/test', async (req, res, next) => { if (!req.user) { return next({ From 636783ecbdac803dafe20bd1bbf027439afe1add Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Mon, 21 Apr 2025 01:31:21 +0200 Subject: [PATCH 10/62] feat(notifications): implement notification agent registration and unregistration at endpoints --- server/index.ts | 25 ---- server/lib/notifications/agents/agent.ts | 4 +- server/lib/notifications/index.ts | 21 +++- server/routes/settings/notifications.ts | 144 ++++++++++++++++------- 4 files changed, 122 insertions(+), 72 deletions(-) diff --git a/server/index.ts b/server/index.ts index 24b7f2250a..5c164003fd 100644 --- a/server/index.ts +++ b/server/index.ts @@ -5,17 +5,6 @@ import DiscoverSlider from '@server/entity/DiscoverSlider'; import { Session } from '@server/entity/Session'; import { User } from '@server/entity/User'; import { startJobs } from '@server/job/schedule'; -import notificationManager from '@server/lib/notifications'; -import DiscordAgent from '@server/lib/notifications/agents/discord'; -import EmailAgent from '@server/lib/notifications/agents/email'; -import GotifyAgent from '@server/lib/notifications/agents/gotify'; -import NtfyAgent from '@server/lib/notifications/agents/ntfy'; -import PushbulletAgent from '@server/lib/notifications/agents/pushbullet'; -import PushoverAgent from '@server/lib/notifications/agents/pushover'; -import SlackAgent from '@server/lib/notifications/agents/slack'; -import TelegramAgent from '@server/lib/notifications/agents/telegram'; -import WebhookAgent from '@server/lib/notifications/agents/webhook'; -import WebPushAgent from '@server/lib/notifications/agents/webpush'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import clearCookies from '@server/middleware/clearcookies'; @@ -115,20 +104,6 @@ app } } - // Register Notification Agents - notificationManager.registerAgents([ - new DiscordAgent(), - new EmailAgent(), - new GotifyAgent(), - new NtfyAgent(), - new PushbulletAgent(), - new PushoverAgent(), - new SlackAgent(), - new TelegramAgent(), - new WebhookAgent(), - new WebPushAgent(), - ]); - const userRepository = getRepository(User); const totalUsers = await userRepository.count(); if (totalUsers > 0) { diff --git a/server/lib/notifications/agents/agent.ts b/server/lib/notifications/agents/agent.ts index 916a419bb0..1c6c508a92 100644 --- a/server/lib/notifications/agents/agent.ts +++ b/server/lib/notifications/agents/agent.ts @@ -28,7 +28,7 @@ export interface NotificationPayload { export abstract class BaseAgent { protected settings?: T; - protected id?: number; + public id?: number; public constructor(settings?: T, id?: number) { this.settings = settings; @@ -51,6 +51,8 @@ export abstract class BaseAgent { } export interface NotificationAgent { + id?: number; + shouldSend(): boolean; send(type: Notification, payload: NotificationPayload): Promise; } diff --git a/server/lib/notifications/index.ts b/server/lib/notifications/index.ts index 71aea8fe9f..2b85e2c379 100644 --- a/server/lib/notifications/index.ts +++ b/server/lib/notifications/index.ts @@ -92,9 +92,24 @@ export const shouldSendAdminNotification = ( class NotificationManager { private activeAgents: NotificationAgent[] = []; - public registerAgents = (agents: NotificationAgent[]): void => { - this.activeAgents = [...this.activeAgents, ...agents]; - logger.info('Registered notification agents', { label: 'Notifications' }); + // TODO also register at startup + public registerAgent = (agent: NotificationAgent): void => { + this.activeAgents.push(agent); + logger.info(`Registered notification agent instance ${agent.id}`, { + label: 'Notifications', + }); + }; + + public unregisterAgent = (instanceId: number): void => { + const instanceIndex = this.activeAgents.findIndex( + (instance) => instance.id === instanceId + ); + + this.activeAgents.splice(instanceIndex, 1); + logger.info( + `Unregistered notification agent instance with id ${instanceId}`, + { label: 'Notifications' } + ); }; public sendNotification( diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index 8d012bf65b..7fc6becbac 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -1,5 +1,5 @@ import type { User } from '@server/entity/User'; -import { Notification } from '@server/lib/notifications'; +import notificationManager, { Notification } from '@server/lib/notifications'; import type { NotificationAgent } from '@server/lib/notifications/agents/agent'; import DiscordAgent from '@server/lib/notifications/agents/discord'; import EmailAgent from '@server/lib/notifications/agents/email'; @@ -11,6 +11,18 @@ import SlackAgent from '@server/lib/notifications/agents/slack'; import TelegramAgent from '@server/lib/notifications/agents/telegram'; import WebhookAgent from '@server/lib/notifications/agents/webhook'; import WebPushAgent from '@server/lib/notifications/agents/webpush'; +import type { + NotificationAgentConfig, + NotificationAgentDiscord, + NotificationAgentEmail, + NotificationAgentGotify, + NotificationAgentLunaSea, + NotificationAgentPushbullet, + NotificationAgentPushover, + NotificationAgentSlack, + NotificationAgentTelegram, + NotificationAgentWebhook, +} from '@server/lib/settings'; import { getSettings, NotificationAgentKey } from '@server/lib/settings'; import { Router } from 'express'; @@ -25,6 +37,69 @@ const sendTestNotification = async (agent: NotificationAgent, user: User) => message: 'Check check, 1, 2, 3. Are we coming in clear?', }); +const createNotificationAgent = ( + body: NotificationAgentConfig, + id?: number +) => { + let notificationAgent: NotificationAgent; + + const instanceAgentType = body.agent; + switch (instanceAgentType) { + case NotificationAgentKey.DISCORD: + notificationAgent = new DiscordAgent( + body as NotificationAgentDiscord, + id + ); + break; + case NotificationAgentKey.EMAIL: + notificationAgent = new EmailAgent(body as NotificationAgentEmail, id); + break; + case NotificationAgentKey.GOTIFY: + notificationAgent = new GotifyAgent(body as NotificationAgentGotify, id); + break; + case NotificationAgentKey.LUNASEA: + notificationAgent = new LunaSeaAgent( + body as NotificationAgentLunaSea, + id + ); + break; + case NotificationAgentKey.PUSHBULLET: + notificationAgent = new PushbulletAgent( + body as NotificationAgentPushbullet, + id + ); + break; + case NotificationAgentKey.PUSHOVER: + notificationAgent = new PushoverAgent( + body as NotificationAgentPushover, + id + ); + break; + case NotificationAgentKey.SLACK: + notificationAgent = new SlackAgent(body as NotificationAgentSlack, id); + break; + case NotificationAgentKey.TELEGRAM: + notificationAgent = new TelegramAgent( + body as NotificationAgentTelegram, + id + ); + break; + case NotificationAgentKey.WEBHOOK: + notificationAgent = new WebhookAgent( + body as NotificationAgentWebhook, + id + ); + break; + case NotificationAgentKey.WEBPUSH: + notificationAgent = new WebPushAgent(body, id); + break; + default: + return; + } + + return notificationAgent; +}; + notificationRoutes.get('/', (_req, res) => { const settings = getSettings(); @@ -45,15 +120,30 @@ notificationRoutes.get<{ id: string }>('/:id', (req, res, next) => { res.status(200).json(notificationInstance); }); -notificationRoutes.post<{ id: string }>('/:id', async (req, res) => { +notificationRoutes.post<{ id: string }>('/:id', async (req, res, next) => { const settings = getSettings(); + const notificationInstanceId = Number(req.params.id); let notificationInstanceIndex = settings.notifications.instances.findIndex( - (instance) => instance.id === Number(req.params.id) + (instance) => instance.id === notificationInstanceId ); if (notificationInstanceIndex === -1) { notificationInstanceIndex = settings.notifications.instances.length; + + const notificationAgent = createNotificationAgent( + req.body, + notificationInstanceIndex + ); + + if (!notificationAgent) { + return next({ + status: 500, + message: 'A valid instance type is missing from the request.', + }); + } + + notificationManager.registerAgent(notificationAgent); } const request = req.body; @@ -79,6 +169,7 @@ notificationRoutes.delete<{ id: string }>('/:id', async (req, res, next) => { } settings.notifications.instances.splice(notificationInstanceIndex, 1); + notificationManager.unregisterAgent(Number(req.params.id)); await settings.save(); @@ -93,45 +184,12 @@ notificationRoutes.post<{ id: string }>('/:id/test', async (req, res, next) => { }); } - let notificationAgent: NotificationAgent; - const instanceType = req.body.agent as NotificationAgentKey; - switch (instanceType) { - case NotificationAgentKey.DISCORD: - notificationAgent = new DiscordAgent(req.body); - break; - case NotificationAgentKey.EMAIL: - notificationAgent = new EmailAgent(req.body); - break; - case NotificationAgentKey.GOTIFY: - notificationAgent = new GotifyAgent(req.body); - break; - case NotificationAgentKey.LUNASEA: - notificationAgent = new LunaSeaAgent(req.body); - break; - case NotificationAgentKey.PUSHBULLET: - notificationAgent = new PushbulletAgent(req.body); - break; - case NotificationAgentKey.PUSHOVER: - notificationAgent = new PushoverAgent(req.body); - break; - case NotificationAgentKey.SLACK: - notificationAgent = new SlackAgent(req.body); - break; - case NotificationAgentKey.TELEGRAM: - notificationAgent = new TelegramAgent(req.body); - break; - case NotificationAgentKey.WEBHOOK: - notificationAgent = new WebhookAgent(req.body); - break; - case NotificationAgentKey.WEBPUSH: - notificationAgent = new WebPushAgent(req.body); - break; - - default: - return next({ - status: 500, - message: 'A valid instance type is missing from the request.', - }); + const notificationAgent = createNotificationAgent(req.body); + if (!notificationAgent) { + return next({ + status: 500, + message: 'A valid instance type is missing from the request.', + }); } if (await sendTestNotification(notificationAgent, req.user)) { @@ -139,7 +197,7 @@ notificationRoutes.post<{ id: string }>('/:id/test', async (req, res, next) => { } else { return next({ status: 500, - message: `Failed to send ${instanceType} notification.`, + message: `Failed to send ${req.body.agent} notification.`, }); } }); From efa4ab7af271a7ba375bc96d143b9605569f93e7 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Mon, 21 Apr 2025 01:49:38 +0200 Subject: [PATCH 11/62] feat(notifications): implement registration off all configured agents at startup --- server/index.ts | 4 + server/lib/notifications/index.ts | 105 +++++++++++++++++++++++- server/routes/settings/notifications.ts | 93 ++------------------- 3 files changed, 113 insertions(+), 89 deletions(-) diff --git a/server/index.ts b/server/index.ts index 5c164003fd..e9f6d6e3b2 100644 --- a/server/index.ts +++ b/server/index.ts @@ -5,6 +5,7 @@ import DiscoverSlider from '@server/entity/DiscoverSlider'; import { Session } from '@server/entity/Session'; import { User } from '@server/entity/User'; import { startJobs } from '@server/job/schedule'; +import notificationManager from '@server/lib/notifications'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import clearCookies from '@server/middleware/clearcookies'; @@ -104,6 +105,9 @@ app } } + // Register Notification Agents + notificationManager.registerAllAgents(); + const userRepository = getRepository(User); const totalUsers = await userRepository.count(); if (totalUsers > 0) { diff --git a/server/lib/notifications/index.ts b/server/lib/notifications/index.ts index 2b85e2c379..5634d4749c 100644 --- a/server/lib/notifications/index.ts +++ b/server/lib/notifications/index.ts @@ -1,5 +1,28 @@ import type { User } from '@server/entity/User'; +import DiscordAgent from '@server/lib/notifications/agents/discord'; +import EmailAgent from '@server/lib/notifications/agents/email'; +import GotifyAgent from '@server/lib/notifications/agents/gotify'; +import LunaSeaAgent from '@server/lib/notifications/agents/lunasea'; +import PushbulletAgent from '@server/lib/notifications/agents/pushbullet'; +import PushoverAgent from '@server/lib/notifications/agents/pushover'; +import SlackAgent from '@server/lib/notifications/agents/slack'; +import TelegramAgent from '@server/lib/notifications/agents/telegram'; +import WebhookAgent from '@server/lib/notifications/agents/webhook'; +import WebPushAgent from '@server/lib/notifications/agents/webpush'; import { Permission } from '@server/lib/permissions'; +import type { + NotificationAgentConfig, + NotificationAgentDiscord, + NotificationAgentEmail, + NotificationAgentGotify, + NotificationAgentLunaSea, + NotificationAgentPushbullet, + NotificationAgentPushover, + NotificationAgentSlack, + NotificationAgentTelegram, + NotificationAgentWebhook, +} from '@server/lib/settings'; +import { getSettings, NotificationAgentKey } from '@server/lib/settings'; import logger from '@server/logger'; import type { NotificationAgent, NotificationPayload } from './agents/agent'; @@ -92,7 +115,6 @@ export const shouldSendAdminNotification = ( class NotificationManager { private activeAgents: NotificationAgent[] = []; - // TODO also register at startup public registerAgent = (agent: NotificationAgent): void => { this.activeAgents.push(agent); logger.info(`Registered notification agent instance ${agent.id}`, { @@ -127,6 +149,87 @@ class NotificationManager { } }); } + + public createNotificationAgent = ( + body: NotificationAgentConfig, + id?: number + ) => { + let notificationAgent: NotificationAgent; + + const instanceAgentType = body.agent; + switch (instanceAgentType) { + case NotificationAgentKey.DISCORD: + notificationAgent = new DiscordAgent( + body as NotificationAgentDiscord, + id + ); + break; + case NotificationAgentKey.EMAIL: + notificationAgent = new EmailAgent(body as NotificationAgentEmail, id); + break; + case NotificationAgentKey.GOTIFY: + notificationAgent = new GotifyAgent( + body as NotificationAgentGotify, + id + ); + break; + case NotificationAgentKey.LUNASEA: + notificationAgent = new LunaSeaAgent( + body as NotificationAgentLunaSea, + id + ); + break; + case NotificationAgentKey.PUSHBULLET: + notificationAgent = new PushbulletAgent( + body as NotificationAgentPushbullet, + id + ); + break; + case NotificationAgentKey.PUSHOVER: + notificationAgent = new PushoverAgent( + body as NotificationAgentPushover, + id + ); + break; + case NotificationAgentKey.SLACK: + notificationAgent = new SlackAgent(body as NotificationAgentSlack, id); + break; + case NotificationAgentKey.TELEGRAM: + notificationAgent = new TelegramAgent( + body as NotificationAgentTelegram, + id + ); + break; + case NotificationAgentKey.WEBHOOK: + notificationAgent = new WebhookAgent( + body as NotificationAgentWebhook, + id + ); + break; + case NotificationAgentKey.WEBPUSH: + notificationAgent = new WebPushAgent(body, id); + break; + default: + return; + } + + return notificationAgent; + }; + + public registerAllAgents = () => { + const agentInstances = getSettings().notifications.instances; + + agentInstances.forEach((instance) => { + const notificationAgent = notificationManager.createNotificationAgent( + instance, + instance.id + ); + + if (notificationAgent) { + notificationManager.registerAgent(notificationAgent); + } + }); + }; } const notificationManager = new NotificationManager(); diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index 7fc6becbac..b0e181714d 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -1,29 +1,7 @@ import type { User } from '@server/entity/User'; import notificationManager, { Notification } from '@server/lib/notifications'; import type { NotificationAgent } from '@server/lib/notifications/agents/agent'; -import DiscordAgent from '@server/lib/notifications/agents/discord'; -import EmailAgent from '@server/lib/notifications/agents/email'; -import GotifyAgent from '@server/lib/notifications/agents/gotify'; -import LunaSeaAgent from '@server/lib/notifications/agents/lunasea'; -import PushbulletAgent from '@server/lib/notifications/agents/pushbullet'; -import PushoverAgent from '@server/lib/notifications/agents/pushover'; -import SlackAgent from '@server/lib/notifications/agents/slack'; -import TelegramAgent from '@server/lib/notifications/agents/telegram'; -import WebhookAgent from '@server/lib/notifications/agents/webhook'; -import WebPushAgent from '@server/lib/notifications/agents/webpush'; -import type { - NotificationAgentConfig, - NotificationAgentDiscord, - NotificationAgentEmail, - NotificationAgentGotify, - NotificationAgentLunaSea, - NotificationAgentPushbullet, - NotificationAgentPushover, - NotificationAgentSlack, - NotificationAgentTelegram, - NotificationAgentWebhook, -} from '@server/lib/settings'; -import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; import { Router } from 'express'; const notificationRoutes = Router(); @@ -37,69 +15,6 @@ const sendTestNotification = async (agent: NotificationAgent, user: User) => message: 'Check check, 1, 2, 3. Are we coming in clear?', }); -const createNotificationAgent = ( - body: NotificationAgentConfig, - id?: number -) => { - let notificationAgent: NotificationAgent; - - const instanceAgentType = body.agent; - switch (instanceAgentType) { - case NotificationAgentKey.DISCORD: - notificationAgent = new DiscordAgent( - body as NotificationAgentDiscord, - id - ); - break; - case NotificationAgentKey.EMAIL: - notificationAgent = new EmailAgent(body as NotificationAgentEmail, id); - break; - case NotificationAgentKey.GOTIFY: - notificationAgent = new GotifyAgent(body as NotificationAgentGotify, id); - break; - case NotificationAgentKey.LUNASEA: - notificationAgent = new LunaSeaAgent( - body as NotificationAgentLunaSea, - id - ); - break; - case NotificationAgentKey.PUSHBULLET: - notificationAgent = new PushbulletAgent( - body as NotificationAgentPushbullet, - id - ); - break; - case NotificationAgentKey.PUSHOVER: - notificationAgent = new PushoverAgent( - body as NotificationAgentPushover, - id - ); - break; - case NotificationAgentKey.SLACK: - notificationAgent = new SlackAgent(body as NotificationAgentSlack, id); - break; - case NotificationAgentKey.TELEGRAM: - notificationAgent = new TelegramAgent( - body as NotificationAgentTelegram, - id - ); - break; - case NotificationAgentKey.WEBHOOK: - notificationAgent = new WebhookAgent( - body as NotificationAgentWebhook, - id - ); - break; - case NotificationAgentKey.WEBPUSH: - notificationAgent = new WebPushAgent(body, id); - break; - default: - return; - } - - return notificationAgent; -}; - notificationRoutes.get('/', (_req, res) => { const settings = getSettings(); @@ -131,7 +46,7 @@ notificationRoutes.post<{ id: string }>('/:id', async (req, res, next) => { if (notificationInstanceIndex === -1) { notificationInstanceIndex = settings.notifications.instances.length; - const notificationAgent = createNotificationAgent( + const notificationAgent = notificationManager.createNotificationAgent( req.body, notificationInstanceIndex ); @@ -184,7 +99,9 @@ notificationRoutes.post<{ id: string }>('/:id/test', async (req, res, next) => { }); } - const notificationAgent = createNotificationAgent(req.body); + const notificationAgent = notificationManager.createNotificationAgent( + req.body + ); if (!notificationAgent) { return next({ status: 500, From 53fe1595bc5b706c5ee7e4d13b1ed767e2bfb0c0 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Mon, 21 Apr 2025 20:35:03 +0200 Subject: [PATCH 12/62] feat(notifications): implement notification agent reregister on agent change --- server/lib/notifications/index.ts | 169 ++++++++++++------------ server/routes/settings/notifications.ts | 58 ++++++-- 2 files changed, 133 insertions(+), 94 deletions(-) diff --git a/server/lib/notifications/index.ts b/server/lib/notifications/index.ts index 5634d4749c..71a26d75b0 100644 --- a/server/lib/notifications/index.ts +++ b/server/lib/notifications/index.ts @@ -112,17 +112,80 @@ export const shouldSendAdminNotification = ( ); }; +export const createAccordingNotificationAgent = ( + body: NotificationAgentConfig, + id?: number +) => { + let notificationAgent: NotificationAgent; + + const instanceAgentType = body.agent; + switch (instanceAgentType) { + case NotificationAgentKey.DISCORD: + notificationAgent = new DiscordAgent( + body as NotificationAgentDiscord, + id + ); + break; + case NotificationAgentKey.EMAIL: + notificationAgent = new EmailAgent(body as NotificationAgentEmail, id); + break; + case NotificationAgentKey.GOTIFY: + notificationAgent = new GotifyAgent(body as NotificationAgentGotify, id); + break; + case NotificationAgentKey.LUNASEA: + notificationAgent = new LunaSeaAgent( + body as NotificationAgentLunaSea, + id + ); + break; + case NotificationAgentKey.PUSHBULLET: + notificationAgent = new PushbulletAgent( + body as NotificationAgentPushbullet, + id + ); + break; + case NotificationAgentKey.PUSHOVER: + notificationAgent = new PushoverAgent( + body as NotificationAgentPushover, + id + ); + break; + case NotificationAgentKey.SLACK: + notificationAgent = new SlackAgent(body as NotificationAgentSlack, id); + break; + case NotificationAgentKey.TELEGRAM: + notificationAgent = new TelegramAgent( + body as NotificationAgentTelegram, + id + ); + break; + case NotificationAgentKey.WEBHOOK: + notificationAgent = new WebhookAgent( + body as NotificationAgentWebhook, + id + ); + break; + case NotificationAgentKey.WEBPUSH: + notificationAgent = new WebPushAgent(body, id); + break; + default: + return; + } + + return notificationAgent; +}; + class NotificationManager { private activeAgents: NotificationAgent[] = []; - public registerAgent = (agent: NotificationAgent): void => { + public registerAgent = (agent: NotificationAgent) => { this.activeAgents.push(agent); logger.info(`Registered notification agent instance ${agent.id}`, { label: 'Notifications', }); }; - public unregisterAgent = (instanceId: number): void => { + public unregisterAgent = (instanceId: number) => { const instanceIndex = this.activeAgents.findIndex( (instance) => instance.id === instanceId ); @@ -134,93 +197,24 @@ class NotificationManager { ); }; - public sendNotification( - type: Notification, - payload: NotificationPayload - ): void { - logger.info(`Sending notification(s) for ${Notification[type]}`, { - label: 'Notifications', - subject: payload.subject, - }); + public reregisterAgent = (agent: NotificationAgent, instanceId: number) => { + const instanceIndex = this.activeAgents.findIndex( + (instance) => instance.id === instanceId + ); - this.activeAgents.forEach((agent) => { - if (agent.shouldSend()) { - agent.send(type, payload); - } - }); - } + this.activeAgents[instanceIndex] = agent; - public createNotificationAgent = ( - body: NotificationAgentConfig, - id?: number - ) => { - let notificationAgent: NotificationAgent; - - const instanceAgentType = body.agent; - switch (instanceAgentType) { - case NotificationAgentKey.DISCORD: - notificationAgent = new DiscordAgent( - body as NotificationAgentDiscord, - id - ); - break; - case NotificationAgentKey.EMAIL: - notificationAgent = new EmailAgent(body as NotificationAgentEmail, id); - break; - case NotificationAgentKey.GOTIFY: - notificationAgent = new GotifyAgent( - body as NotificationAgentGotify, - id - ); - break; - case NotificationAgentKey.LUNASEA: - notificationAgent = new LunaSeaAgent( - body as NotificationAgentLunaSea, - id - ); - break; - case NotificationAgentKey.PUSHBULLET: - notificationAgent = new PushbulletAgent( - body as NotificationAgentPushbullet, - id - ); - break; - case NotificationAgentKey.PUSHOVER: - notificationAgent = new PushoverAgent( - body as NotificationAgentPushover, - id - ); - break; - case NotificationAgentKey.SLACK: - notificationAgent = new SlackAgent(body as NotificationAgentSlack, id); - break; - case NotificationAgentKey.TELEGRAM: - notificationAgent = new TelegramAgent( - body as NotificationAgentTelegram, - id - ); - break; - case NotificationAgentKey.WEBHOOK: - notificationAgent = new WebhookAgent( - body as NotificationAgentWebhook, - id - ); - break; - case NotificationAgentKey.WEBPUSH: - notificationAgent = new WebPushAgent(body, id); - break; - default: - return; - } - - return notificationAgent; + logger.info( + `Reregistered notification agent instance with id ${instanceId}`, + { label: 'Notifications' } + ); }; public registerAllAgents = () => { const agentInstances = getSettings().notifications.instances; agentInstances.forEach((instance) => { - const notificationAgent = notificationManager.createNotificationAgent( + const notificationAgent = createAccordingNotificationAgent( instance, instance.id ); @@ -230,6 +224,19 @@ class NotificationManager { } }); }; + + public sendNotification(type: Notification, payload: NotificationPayload) { + logger.info(`Sending notification(s) for ${Notification[type]}`, { + label: 'Notifications', + subject: payload.subject, + }); + + this.activeAgents.forEach((agent) => { + if (agent.shouldSend()) { + agent.send(type, payload); + } + }); + } } const notificationManager = new NotificationManager(); diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index b0e181714d..9eb6489e7e 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -1,5 +1,8 @@ import type { User } from '@server/entity/User'; -import notificationManager, { Notification } from '@server/lib/notifications'; +import notificationManager, { + createAccordingNotificationAgent, + Notification, +} from '@server/lib/notifications'; import type { NotificationAgent } from '@server/lib/notifications/agents/agent'; import { getSettings } from '@server/lib/settings'; import { Router } from 'express'; @@ -43,27 +46,58 @@ notificationRoutes.post<{ id: string }>('/:id', async (req, res, next) => { (instance) => instance.id === notificationInstanceId ); + const request = req.body; + request.id = notificationInstanceId; + + // instance was not found -> register new one with new id if (notificationInstanceIndex === -1) { - notificationInstanceIndex = settings.notifications.instances.length; + settings.notifications.instances.push(request); + notificationInstanceIndex = settings.notifications.instances.findIndex( + (instance) => instance.id === notificationInstanceId + ); - const notificationAgent = notificationManager.createNotificationAgent( - req.body, - notificationInstanceIndex + const notificationAgent = createAccordingNotificationAgent( + request, + notificationInstanceId ); if (!notificationAgent) { return next({ status: 500, - message: 'A valid instance type is missing from the request.', + message: 'A valid agent is missing from the request.', }); } notificationManager.registerAgent(notificationAgent); } + // agent has changed -> reregister + else if ( + settings.notifications.instances[notificationInstanceIndex].agent !== + request.agent + ) { + settings.notifications.instances[notificationInstanceIndex] = request; + + const notificationAgent = createAccordingNotificationAgent( + request, + notificationInstanceId + ); - const request = req.body; - request.id = notificationInstanceIndex; - settings.notifications.instances[notificationInstanceIndex] = req.body; + if (!notificationAgent) { + return next({ + status: 500, + message: 'A valid agent is missing from the request.', + }); + } + + notificationManager.reregisterAgent( + notificationAgent, + notificationInstanceId + ); + } + // only change instance + else { + settings.notifications.instances[notificationInstanceIndex] = request; + } await settings.save(); @@ -99,13 +133,11 @@ notificationRoutes.post<{ id: string }>('/:id/test', async (req, res, next) => { }); } - const notificationAgent = notificationManager.createNotificationAgent( - req.body - ); + const notificationAgent = createAccordingNotificationAgent(req.body); if (!notificationAgent) { return next({ status: 500, - message: 'A valid instance type is missing from the request.', + message: 'A valid agent is missing from the request.', }); } From 758ff493eb7d71197ac9e9ae5d32c76e1872e2ab Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Tue, 22 Apr 2025 23:39:29 +0200 Subject: [PATCH 13/62] feat(notifications): implement default notification instance --- jellyseerr-api.yml | 3 +++ server/entity/User.ts | 16 ++++++++++--- server/lib/notifications/index.ts | 17 ++++++++++++++ server/lib/settings/index.ts | 23 +++++++++++++++---- server/routes/auth.ts | 2 +- server/routes/user/index.ts | 2 +- server/routes/user/usersettings.ts | 36 ++++++++++++++++++++++-------- 7 files changed, 81 insertions(+), 18 deletions(-) diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index cc64665c4a..84bd825257 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -1402,6 +1402,9 @@ components: agent: type: string example: 'email' + default: + type: boolean + example: false options: type: object Job: diff --git a/server/entity/User.ts b/server/entity/User.ts index 8a96f396e7..57f7500289 100644 --- a/server/entity/User.ts +++ b/server/entity/User.ts @@ -4,9 +4,11 @@ import { getRepository } from '@server/datasource'; import { Watchlist } from '@server/entity/Watchlist'; import type { QuotaResponse } from '@server/interfaces/api/userInterfaces'; import PreparedEmail from '@server/lib/email'; +import { retrieveDefaultNotificationInstanceSettings } from '@server/lib/notifications'; import type { PermissionCheckOptions } from '@server/lib/permissions'; import { hasPermission, Permission } from '@server/lib/permissions'; -import { getSettings } from '@server/lib/settings'; +import type { NotificationAgentEmail } from '@server/lib/settings'; +import { getSettings, NotificationAgentKey } from '@server/lib/settings'; import logger from '@server/logger'; import { AfterDate } from '@server/utils/dateHelpers'; import { DbAwareColumn } from '@server/utils/DbColumnHelper'; @@ -196,7 +198,11 @@ export class User { label: 'User Management', }); - const email = new PreparedEmail(getSettings().notifications.agents.email); + const defaultEmailInstance = retrieveDefaultNotificationInstanceSettings( + NotificationAgentKey.EMAIL + ) as NotificationAgentEmail; + const email = new PreparedEmail(defaultEmailInstance); + await email.send({ template: path.join(__dirname, '../templates/email/generatedpassword'), message: { @@ -233,7 +239,11 @@ export class User { logger.info(`Sending reset password email for ${this.email}`, { label: 'User Management', }); - const email = new PreparedEmail(getSettings().notifications.agents.email); + const defaultEmailInstance = retrieveDefaultNotificationInstanceSettings( + NotificationAgentKey.EMAIL + ) as NotificationAgentEmail; + const email = new PreparedEmail(defaultEmailInstance); + await email.send({ template: path.join(__dirname, '../templates/email/resetpassword'), message: { diff --git a/server/lib/notifications/index.ts b/server/lib/notifications/index.ts index 71a26d75b0..0242e30def 100644 --- a/server/lib/notifications/index.ts +++ b/server/lib/notifications/index.ts @@ -175,6 +175,23 @@ export const createAccordingNotificationAgent = ( return notificationAgent; }; +export const retrieveDefaultNotificationInstanceSettings = ( + agentKey: NotificationAgentKey +) => { + const settings = getSettings(); + + const defaults = settings.notifications.instances.filter((instance) => + instance.default && instance.agent ? instance.agent === agentKey : true + ); + + // return agent template if no default is configured + if (!defaults[0]) { + return settings.notifications.agentTemplates[agentKey]; + } + + return defaults[0]; +}; + class NotificationManager { private activeAgents: NotificationAgent[] = []; diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 1f4ab3b8fc..5d4058fab9 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -211,6 +211,7 @@ export interface NotificationAgentConfig { name: string; id?: number; agent: NotificationAgentKey; + default?: boolean; options: Record; } @@ -700,11 +701,25 @@ class Settings { enableSpecialEpisodes: this.data.main.enableSpecialEpisodes, cacheImages: this.data.main.cacheImages, vapidPublic: this.vapidPublic, - // TODO no static values here - enablePushRegistration: false, + enablePushRegistration: this.notifications.instances.some( + (instance) => + instance.default && + instance.agent === NotificationAgentKey.WEBPUSH && + instance.enabled + ), locale: this.data.main.locale, - emailEnabled: false, - userEmailRequired: false, + emailEnabled: this.notifications.instances.some( + (instance) => + instance.default && + instance.agent === NotificationAgentKey.EMAIL && + instance.enabled + ), + userEmailRequired: this.notifications.instances.some( + (instance) => + instance.default && + instance.agent === NotificationAgentKey.EMAIL && + instance.options.userEmailRequired + ), newPlexLogin: this.data.main.newPlexLogin, youtubeUrl: this.data.main.youtubeUrl, }; diff --git a/server/routes/auth.ts b/server/routes/auth.ts index b8e78a2d0e..aa861da581 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -36,7 +36,7 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => { // check if email is required in settings and if user has an valid email const settings = await getSettings(); if ( - settings.notifications.agents.email.options.userEmailRequired && + settings.fullPublicSettings.userEmailRequired && !EmailValidator.validate(user.email) ) { user.warnings.push('userEmailRequired'); diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index f817a9cb33..66675c77c2 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -148,7 +148,7 @@ router.post( if ( !passedExplicitPassword && - !settings.notifications.agents.email.enabled + !settings.fullPublicSettings.emailEnabled ) { throw new Error('Email notifications must be enabled'); } diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index 690cff8309..f706c71bea 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -10,8 +10,14 @@ import type { UserSettingsGeneralResponse, UserSettingsNotificationsResponse, } from '@server/interfaces/api/userSettingsInterfaces'; +import { retrieveDefaultNotificationInstanceSettings } from '@server/lib/notifications'; import { Permission } from '@server/lib/permissions'; -import { getSettings } from '@server/lib/settings'; +import type { + NotificationAgentDiscord, + NotificationAgentEmail, + NotificationAgentTelegram, +} from '@server/lib/settings'; +import { getSettings, NotificationAgentKey } from '@server/lib/settings'; import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; import { ApiError } from '@server/types/error'; @@ -551,7 +557,19 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>( isOwnProfileOrAdmin(), async (req, res, next) => { const userRepository = getRepository(User); - const settings = getSettings()?.notifications.agents; + + const defaultEmail = retrieveDefaultNotificationInstanceSettings( + NotificationAgentKey.EMAIL + ) as NotificationAgentEmail; + const defaultDiscord = retrieveDefaultNotificationInstanceSettings( + NotificationAgentKey.DISCORD + ) as NotificationAgentDiscord; + const defaultTelegram = retrieveDefaultNotificationInstanceSettings( + NotificationAgentKey.TELEGRAM + ) as NotificationAgentTelegram; + const defaultWebPush = retrieveDefaultNotificationInstanceSettings( + NotificationAgentKey.WEBPUSH + ); try { const user = await userRepository.findOne({ @@ -563,25 +581,25 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>( } return res.status(200).json({ - emailEnabled: settings.email.enabled, + emailEnabled: defaultEmail.enabled, pgpKey: user.settings?.pgpKey, discordEnabled: - settings?.discord.enabled && settings.discord.options.enableMentions, + defaultDiscord.enabled && defaultDiscord.options.enableMentions, discordEnabledTypes: - settings?.discord.enabled && settings.discord.options.enableMentions - ? settings.discord.types + defaultDiscord.enabled && defaultDiscord.options.enableMentions + ? defaultDiscord.types : 0, discordId: user.settings?.discordId, pushbulletAccessToken: user.settings?.pushbulletAccessToken, pushoverApplicationToken: user.settings?.pushoverApplicationToken, pushoverUserKey: user.settings?.pushoverUserKey, pushoverSound: user.settings?.pushoverSound, - telegramEnabled: settings.telegram.enabled, - telegramBotUsername: settings.telegram.options.botUsername, + telegramEnabled: defaultTelegram.enabled, + telegramBotUsername: defaultTelegram.options.botUsername, telegramChatId: user.settings?.telegramChatId, telegramMessageThreadId: user.settings?.telegramMessageThreadId, telegramSendSilently: user.settings?.telegramSendSilently, - webPushEnabled: settings.webpush.enabled, + webPushEnabled: defaultWebPush.enabled, notificationTypes: user.settings?.notificationTypes ?? {}, }); } catch (e) { From 76baf0d94f72ff754d49e78f50496253017e1706 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Wed, 23 Apr 2025 21:16:38 +0200 Subject: [PATCH 14/62] fix(notifications): fix missing notification agent type cast --- server/lib/notifications/agents/gotify.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/notifications/agents/gotify.ts b/server/lib/notifications/agents/gotify.ts index fc6d8198a3..c03b0bbd1a 100644 --- a/server/lib/notifications/agents/gotify.ts +++ b/server/lib/notifications/agents/gotify.ts @@ -38,7 +38,7 @@ class GotifyAgent payload: NotificationPayload ): GotifyPayload { const { applicationUrl, applicationTitle } = getSettings().main; - const settings = this.getSettings(); + const settings = this.getSettings() as NotificationAgentGotify; const priority = settings.options.priority ?? 1; const title = payload.event From 5a073991e740ded1c95d8954105888322aa7d43c Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Wed, 23 Apr 2025 22:21:54 +0200 Subject: [PATCH 15/62] feat(notifications): implement notifications endpoint take, skip and sort --- jellyseerr-api.yml | 33 ++++++++++++++++++++++++- server/routes/settings/notifications.ts | 29 ++++++++++++++++++++-- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index 84bd825257..421a91c953 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -1401,7 +1401,19 @@ components: type: string agent: type: string - example: 'email' + enum: + [ + 'discord', + 'email', + 'gotify', + 'lunasea', + 'pushbullet', + 'pushover', + 'slack', + 'telegram', + 'webhook', + 'webpush', + ] default: type: boolean example: false @@ -3075,6 +3087,25 @@ paths: description: Returns all current notification instance settings in a JSON object. tags: - settings + parameters: + - in: query + name: take + schema: + type: number + nullable: true + example: 20 + - in: query + name: skip + schema: + type: number + nullable: true + example: 0 + - in: query + name: sort + schema: + type: string + enum: [id, name, agent] + default: id responses: '200': description: Returned all notification settings diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index 9eb6489e7e..1498013c24 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -4,6 +4,7 @@ import notificationManager, { Notification, } from '@server/lib/notifications'; import type { NotificationAgent } from '@server/lib/notifications/agents/agent'; +import type { NotificationAgentConfig } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import { Router } from 'express'; @@ -18,10 +19,34 @@ const sendTestNotification = async (agent: NotificationAgent, user: User) => message: 'Check check, 1, 2, 3. Are we coming in clear?', }); -notificationRoutes.get('/', (_req, res) => { +notificationRoutes.get('/', (req, res) => { const settings = getSettings(); - res.status(200).json(settings.notifications.instances); + const pageSize = req.query.take ? Number(req.query.take) : 10; + const skip = req.query.skip ? Number(req.query.skip) : 0; + + let sortFunc: ( + a: NotificationAgentConfig, + b: NotificationAgentConfig + ) => number; + switch (req.query.sort) { + case 'name': + sortFunc = (a, b) => a.name.localeCompare(b.name); + break; + + case 'agent': + sortFunc = (a, b) => a.agent.localeCompare(b.agent); + break; + + default: + sortFunc = (a, b) => (a.id || 0) - (b.id || 0); + } + + const instancesResult = settings.notifications.instances + .sort(sortFunc) + .slice(skip, skip + pageSize); + + res.status(200).json(instancesResult); }); notificationRoutes.get<{ id: string }>('/:id', (req, res, next) => { From 54ee340ffab0a5ad216f845e132db9c58f087415 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Thu, 24 Apr 2025 22:12:10 +0200 Subject: [PATCH 16/62] feat(notifications): add post notifications endpoint to add a instance without id --- jellyseerr-api.yml | 18 +++++++ server/routes/settings/notifications.ts | 70 ++++++++++++++++++++----- 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index 421a91c953..08b7a0bf09 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -3115,6 +3115,24 @@ paths: type: array items: $ref: '#/components/schemas/NotificationSettings' + post: + summary: Add a notification instance + description: Add a notification instance with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationSettings' + responses: + '200': + description: 'Notification instance was sucessfully added' + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationSettings' /settings/notifications/{instanceId}: get: summary: Get given notification instance settings diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index 1498013c24..1e0ee2068b 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -19,6 +19,18 @@ const sendTestNotification = async (agent: NotificationAgent, user: User) => message: 'Check check, 1, 2, 3. Are we coming in clear?', }); +const findFirstFreeNotificationInstanceId = () => { + const instances = getSettings().notifications.instances; + + for (let i = 0; i < instances.length; ++i) { + if (!instances.find((instance) => instance.id === i)) { + return i; + } + } + + return instances.length; +}; + notificationRoutes.get('/', (req, res) => { const settings = getSettings(); @@ -63,11 +75,45 @@ notificationRoutes.get<{ id: string }>('/:id', (req, res, next) => { res.status(200).json(notificationInstance); }); +notificationRoutes.post('/', async (req, res, next) => { + const settings = getSettings(); + const instances = settings.notifications.instances; + + const notificationInstanceId = findFirstFreeNotificationInstanceId(); + + const request = req.body; + request.id = notificationInstanceId; + + instances.push(request); + const notificationInstanceIndex = instances.findIndex( + (instance) => instance.id === notificationInstanceId + ); + + const notificationAgent = createAccordingNotificationAgent( + request, + notificationInstanceId + ); + + if (!notificationAgent) { + return next({ + status: 500, + message: 'A valid agent is missing from the request.', + }); + } + + notificationManager.registerAgent(notificationAgent); + + await settings.save(); + + res.status(200).json(instances[notificationInstanceIndex]); +}); + notificationRoutes.post<{ id: string }>('/:id', async (req, res, next) => { const settings = getSettings(); + const instances = settings.notifications.instances; const notificationInstanceId = Number(req.params.id); - let notificationInstanceIndex = settings.notifications.instances.findIndex( + let notificationInstanceIndex = instances.findIndex( (instance) => instance.id === notificationInstanceId ); @@ -76,8 +122,8 @@ notificationRoutes.post<{ id: string }>('/:id', async (req, res, next) => { // instance was not found -> register new one with new id if (notificationInstanceIndex === -1) { - settings.notifications.instances.push(request); - notificationInstanceIndex = settings.notifications.instances.findIndex( + instances.push(request); + notificationInstanceIndex = instances.findIndex( (instance) => instance.id === notificationInstanceId ); @@ -96,11 +142,8 @@ notificationRoutes.post<{ id: string }>('/:id', async (req, res, next) => { notificationManager.registerAgent(notificationAgent); } // agent has changed -> reregister - else if ( - settings.notifications.instances[notificationInstanceIndex].agent !== - request.agent - ) { - settings.notifications.instances[notificationInstanceIndex] = request; + else if (instances[notificationInstanceIndex].agent !== request.agent) { + instances[notificationInstanceIndex] = request; const notificationAgent = createAccordingNotificationAgent( request, @@ -121,20 +164,19 @@ notificationRoutes.post<{ id: string }>('/:id', async (req, res, next) => { } // only change instance else { - settings.notifications.instances[notificationInstanceIndex] = request; + instances[notificationInstanceIndex] = request; } await settings.save(); - res - .status(200) - .json(settings.notifications.instances[notificationInstanceIndex]); + res.status(200).json(instances[notificationInstanceIndex]); }); notificationRoutes.delete<{ id: string }>('/:id', async (req, res, next) => { const settings = getSettings(); + const instances = settings.notifications.instances; - const notificationInstanceIndex = settings.notifications.instances.findIndex( + const notificationInstanceIndex = instances.findIndex( (instance) => instance.id === Number(req.params.id) ); @@ -142,7 +184,7 @@ notificationRoutes.delete<{ id: string }>('/:id', async (req, res, next) => { return next({ status: '404', message: 'Notifications instance not found' }); } - settings.notifications.instances.splice(notificationInstanceIndex, 1); + instances.splice(notificationInstanceIndex, 1); notificationManager.unregisterAgent(Number(req.params.id)); await settings.save(); From 0a729734592e184a4812cce580bb9697fad06929 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Thu, 24 Apr 2025 22:57:20 +0200 Subject: [PATCH 17/62] fix(notifications): make id in notifications agent config mandatory --- jellyseerr-api.yml | 6 +++--- server/lib/settings/index.ts | 11 ++++++++++- server/routes/settings/index.ts | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index 08b7a0bf09..18dd82a898 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -3081,7 +3081,7 @@ paths: timestamp: type: string example: '2020-12-15T16:20:00.069Z' - /settings/notifications: + /settings/notification: get: summary: Get all notification instance settings description: Returns all current notification instance settings in a JSON object. @@ -3133,7 +3133,7 @@ paths: application/json: schema: $ref: '#/components/schemas/NotificationSettings' - /settings/notifications/{instanceId}: + /settings/notification/{instanceId}: get: summary: Get given notification instance settings description: Returns current notification settings in a JSON object. @@ -3193,7 +3193,7 @@ paths: responses: '200': description: 'Notification instance was sucessfully deleted' - /settings/notifications/{instanceId}/test: + /settings/notification/{instanceId}/test: post: summary: Test given notification instance description: Sends a test notification from the given instance settings. diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 5d4058fab9..97723abde0 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -209,7 +209,7 @@ export interface NotificationAgentConfig { enabled: boolean; types?: number; name: string; - id?: number; + id: number; agent: NotificationAgentKey; default?: boolean; options: Record; @@ -443,6 +443,7 @@ class Settings { email: { enabled: false, name: '', + id: 0, agent: NotificationAgentKey.EMAIL, options: { userEmailRequired: false, @@ -460,6 +461,7 @@ class Settings { enabled: false, types: 0, name: '', + id: 0, agent: NotificationAgentKey.DISCORD, options: { webhookUrl: '', @@ -471,6 +473,7 @@ class Settings { enabled: false, types: 0, name: '', + id: 0, agent: NotificationAgentKey.SLACK, options: { webhookUrl: '', @@ -480,6 +483,7 @@ class Settings { enabled: false, types: 0, name: '', + id: 0, agent: NotificationAgentKey.TELEGRAM, options: { botAPI: '', @@ -492,6 +496,7 @@ class Settings { enabled: false, types: 0, name: '', + id: 0, agent: NotificationAgentKey.PUSHBULLET, options: { accessToken: '', @@ -501,6 +506,7 @@ class Settings { enabled: false, types: 0, name: '', + id: 0, agent: NotificationAgentKey.PUSHOVER, options: { accessToken: '', @@ -512,6 +518,7 @@ class Settings { enabled: false, types: 0, name: '', + id: 0, agent: NotificationAgentKey.WEBHOOK, options: { webhookUrl: '', @@ -521,6 +528,7 @@ class Settings { webpush: { enabled: false, name: '', + id: 0, agent: NotificationAgentKey.WEBPUSH, options: {}, }, @@ -528,6 +536,7 @@ class Settings { enabled: false, types: 0, name: '', + id: 0, agent: NotificationAgentKey.GOTIFY, options: { url: '', diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index 683539680b..e0f0af9b71 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -46,7 +46,7 @@ import sonarrRoutes from './sonarr'; const settingsRoutes = Router(); -settingsRoutes.use('/notifications', notificationRoutes); +settingsRoutes.use('/notification', notificationRoutes); settingsRoutes.use('/radarr', radarrRoutes); settingsRoutes.use('/sonarr', sonarrRoutes); settingsRoutes.use('/discover', discoverSettingRoutes); From 490a3e93b47666d603e8d78199ec23ab9c6588d0 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Thu, 24 Apr 2025 23:04:52 +0200 Subject: [PATCH 18/62] feat(notifications): rename notifications endpoint to notification --- server/lib/notifications/agents/agent.ts | 2 +- server/lib/notifications/index.ts | 6 +++--- server/lib/settings/index.ts | 18 +++++++++--------- server/routes/settings/index.ts | 2 +- .../{notifications.ts => notification.ts} | 12 ++++++------ 5 files changed, 20 insertions(+), 20 deletions(-) rename server/routes/settings/{notifications.ts => notification.ts} (94%) diff --git a/server/lib/notifications/agents/agent.ts b/server/lib/notifications/agents/agent.ts index 1c6c508a92..d4181ec157 100644 --- a/server/lib/notifications/agents/agent.ts +++ b/server/lib/notifications/agents/agent.ts @@ -42,7 +42,7 @@ export abstract class BaseAgent { const settings = getSettings(); - const notificationInstance = settings.notifications.instances.find( + const notificationInstance = settings.notification.instances.find( (instance) => instance.id === Number(this.id) ); diff --git a/server/lib/notifications/index.ts b/server/lib/notifications/index.ts index 0242e30def..0765ec0dd4 100644 --- a/server/lib/notifications/index.ts +++ b/server/lib/notifications/index.ts @@ -180,13 +180,13 @@ export const retrieveDefaultNotificationInstanceSettings = ( ) => { const settings = getSettings(); - const defaults = settings.notifications.instances.filter((instance) => + const defaults = settings.notification.instances.filter((instance) => instance.default && instance.agent ? instance.agent === agentKey : true ); // return agent template if no default is configured if (!defaults[0]) { - return settings.notifications.agentTemplates[agentKey]; + return settings.notification.agentTemplates[agentKey]; } return defaults[0]; @@ -228,7 +228,7 @@ class NotificationManager { }; public registerAllAgents = () => { - const agentInstances = getSettings().notifications.instances; + const agentInstances = getSettings().notification.instances; agentInstances.forEach((instance) => { const notificationAgent = createAccordingNotificationAgent( diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 97723abde0..a330f9b6ab 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -364,7 +364,7 @@ export interface AllSettings { radarr: RadarrSettings[]; sonarr: SonarrSettings[]; public: PublicSettings; - notifications: NotificationSettings; + notification: NotificationSettings; jobs: Record; network: NetworkSettings; metadataSettings: MetadataSettings; @@ -437,7 +437,7 @@ class Settings { public: { initialized: false, }, - notifications: { + notification: { instances: [], agentTemplates: { email: { @@ -710,20 +710,20 @@ class Settings { enableSpecialEpisodes: this.data.main.enableSpecialEpisodes, cacheImages: this.data.main.cacheImages, vapidPublic: this.vapidPublic, - enablePushRegistration: this.notifications.instances.some( + enablePushRegistration: this.notification.instances.some( (instance) => instance.default && instance.agent === NotificationAgentKey.WEBPUSH && instance.enabled ), locale: this.data.main.locale, - emailEnabled: this.notifications.instances.some( + emailEnabled: this.notification.instances.some( (instance) => instance.default && instance.agent === NotificationAgentKey.EMAIL && instance.enabled ), - userEmailRequired: this.notifications.instances.some( + userEmailRequired: this.notification.instances.some( (instance) => instance.default && instance.agent === NotificationAgentKey.EMAIL && @@ -734,12 +734,12 @@ class Settings { }; } - get notifications(): NotificationSettings { - return this.data.notifications; + get notification(): NotificationSettings { + return this.data.notification; } - set notifications(data: NotificationSettings) { - this.data.notifications = data; + set notification(data: NotificationSettings) { + this.data.notification = data; } get jobs(): Record { diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index e0f0af9b71..6152c91a62 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -40,7 +40,7 @@ import path from 'path'; import semver from 'semver'; import { URL } from 'url'; import metadataRoutes from './metadata'; -import notificationRoutes from './notifications'; +import notificationRoutes from './notification'; import radarrRoutes from './radarr'; import sonarrRoutes from './sonarr'; diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notification.ts similarity index 94% rename from server/routes/settings/notifications.ts rename to server/routes/settings/notification.ts index 1e0ee2068b..4e7caebdf3 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notification.ts @@ -20,7 +20,7 @@ const sendTestNotification = async (agent: NotificationAgent, user: User) => }); const findFirstFreeNotificationInstanceId = () => { - const instances = getSettings().notifications.instances; + const instances = getSettings().notification.instances; for (let i = 0; i < instances.length; ++i) { if (!instances.find((instance) => instance.id === i)) { @@ -54,7 +54,7 @@ notificationRoutes.get('/', (req, res) => { sortFunc = (a, b) => (a.id || 0) - (b.id || 0); } - const instancesResult = settings.notifications.instances + const instancesResult = settings.notification.instances .sort(sortFunc) .slice(skip, skip + pageSize); @@ -64,7 +64,7 @@ notificationRoutes.get('/', (req, res) => { notificationRoutes.get<{ id: string }>('/:id', (req, res, next) => { const settings = getSettings(); - const notificationInstance = settings.notifications.instances.find( + const notificationInstance = settings.notification.instances.find( (instance) => instance.id === Number(req.params.id) ); @@ -77,7 +77,7 @@ notificationRoutes.get<{ id: string }>('/:id', (req, res, next) => { notificationRoutes.post('/', async (req, res, next) => { const settings = getSettings(); - const instances = settings.notifications.instances; + const instances = settings.notification.instances; const notificationInstanceId = findFirstFreeNotificationInstanceId(); @@ -110,7 +110,7 @@ notificationRoutes.post('/', async (req, res, next) => { notificationRoutes.post<{ id: string }>('/:id', async (req, res, next) => { const settings = getSettings(); - const instances = settings.notifications.instances; + const instances = settings.notification.instances; const notificationInstanceId = Number(req.params.id); let notificationInstanceIndex = instances.findIndex( @@ -174,7 +174,7 @@ notificationRoutes.post<{ id: string }>('/:id', async (req, res, next) => { notificationRoutes.delete<{ id: string }>('/:id', async (req, res, next) => { const settings = getSettings(); - const instances = settings.notifications.instances; + const instances = settings.notification.instances; const notificationInstanceIndex = instances.findIndex( (instance) => instance.id === Number(req.params.id) From 4acc2c5c21aa24e9567a7bf88b58522327c54e3d Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Thu, 24 Apr 2025 23:37:18 +0200 Subject: [PATCH 19/62] feat(notifications): rework get notification endpoint --- jellyseerr-api.yml | 224 +++++++++++++++++++- server/interfaces/api/settingsInterfaces.ts | 9 + server/lib/settings/index.ts | 2 +- server/routes/settings/notification.ts | 16 +- 4 files changed, 238 insertions(+), 13 deletions(-) diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index 18dd82a898..09dd071726 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -1388,7 +1388,7 @@ components: results: type: number example: 100 - NotificationSettings: + NotificationInstance: type: object properties: enabled: @@ -1419,6 +1419,203 @@ components: example: false options: type: object + NotificationAgentTemplates: + type: object + properties: + discord: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + botUsername: + type: string + botAvatarUrl: + type: string + webhookUrl: + type: string + webhookRoleId: + type: string + enableMentions: + type: boolean + slack: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + webhookUrl: + type: string + WebPushSettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + webhook: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + webhookUrl: + type: string + authHeader: + type: string + jsonPayload: + type: string + telegram: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + botUsername: + type: string + botAPI: + type: string + chatId: + type: string + messageThreadId: + type: string + sendSilently: + type: boolean + pushbullet: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + accessToken: + type: string + channelTag: + type: string + nullable: true + pushover: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + accessToken: + type: string + userToken: + type: string + sound: + type: string + gotify: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + url: + type: string + token: + type: string + lunasea: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + webhookUrl: + type: string + profileName: + type: string + email: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + emailFrom: + type: string + example: no-reply@example.com + senderName: + type: string + example: Jellyseerr + smtpHost: + type: string + example: 127.0.0.1 + smtpPort: + type: number + example: 465 + secure: + type: boolean + example: false + ignoreTls: + type: boolean + example: false + requireTls: + type: boolean + example: false + authUser: + type: string + nullable: true + authPass: + type: string + nullable: true + allowSelfSigned: + type: boolean + example: false Job: type: object properties: @@ -3112,9 +3309,16 @@ paths: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/NotificationSettings' + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/NotificationInstance' + agentTemplates: + $ref: '#/components/schemas/NotificationAgentTemplates' + pageInfo: + $ref: '#/components/schemas/PageInfo' post: summary: Add a notification instance description: Add a notification instance with the provided values. @@ -3125,14 +3329,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/NotificationSettings' + $ref: '#/components/schemas/NotificationInstance' responses: '200': description: 'Notification instance was sucessfully added' content: application/json: schema: - $ref: '#/components/schemas/NotificationSettings' + $ref: '#/components/schemas/NotificationInstance' /settings/notification/{instanceId}: get: summary: Get given notification instance settings @@ -3152,7 +3356,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/NotificationSettings' + $ref: '#/components/schemas/NotificationInstance' post: summary: Add or update given notification instance settings description: Add or Update given notification instance settings with the provided values. @@ -3170,14 +3374,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/NotificationSettings' + $ref: '#/components/schemas/NotificationInstance' responses: '200': description: 'Notification instance was sucessfully added or updated' content: application/json: schema: - $ref: '#/components/schemas/NotificationSettings' + $ref: '#/components/schemas/NotificationInstance' delete: summary: Delete given notification instance description: Delete given notification instance. @@ -3211,7 +3415,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/NotificationSettings' + $ref: '#/components/schemas/NotificationInstance' responses: '204': description: Test notification attempted diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index 5e058eccda..2f76776b69 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -1,3 +1,7 @@ +import type { + NotificationAgentConfig, + NotificationAgentTemplates, +} from '@server/lib/settings'; import type { DnsEntries, DnsStats } from 'dns-caching'; import type { PaginatedResponse } from './common'; @@ -78,3 +82,8 @@ export interface StatusResponse { commitsBehind: number; restartRequired: boolean; } + +export interface NotificationSettingsResultResponse extends PaginatedResponse { + results: NotificationAgentConfig[]; + agentTemplates: NotificationAgentTemplates; +} diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index a330f9b6ab..d5b65297bf 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -316,7 +316,7 @@ export enum NotificationAgentKey { WEBPUSH = 'webpush', } -interface NotificationAgentTemplates { +export interface NotificationAgentTemplates { discord: NotificationAgentDiscord; email: NotificationAgentEmail; gotify: NotificationAgentGotify; diff --git a/server/routes/settings/notification.ts b/server/routes/settings/notification.ts index 4e7caebdf3..3eeaacebcf 100644 --- a/server/routes/settings/notification.ts +++ b/server/routes/settings/notification.ts @@ -1,4 +1,5 @@ import type { User } from '@server/entity/User'; +import type { NotificationSettingsResultResponse } from '@server/interfaces/api/settingsInterfaces'; import notificationManager, { createAccordingNotificationAgent, Notification, @@ -54,11 +55,22 @@ notificationRoutes.get('/', (req, res) => { sortFunc = (a, b) => (a.id || 0) - (b.id || 0); } - const instancesResult = settings.notification.instances + const instancesResponse = settings.notification.instances .sort(sortFunc) .slice(skip, skip + pageSize); - res.status(200).json(instancesResult); + const notificationResponse: NotificationSettingsResultResponse = { + results: instancesResponse, + agentTemplates: settings.notification.agentTemplates, + pageInfo: { + pages: Math.ceil(instancesResponse.length / pageSize), + pageSize, + results: instancesResponse.length, + page: Math.ceil(skip / pageSize) + 1, + }, + }; + + res.status(200).json(notificationResponse); }); notificationRoutes.get<{ id: string }>('/:id', (req, res, next) => { From 4dd06a80c7bf22e46c61b68da3134b817ba8eec9 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Fri, 25 Apr 2025 00:24:15 +0200 Subject: [PATCH 20/62] fix(notifications): fix notification endpoint wrong page size return --- server/routes/settings/notification.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/routes/settings/notification.ts b/server/routes/settings/notification.ts index 3eeaacebcf..52ac753e28 100644 --- a/server/routes/settings/notification.ts +++ b/server/routes/settings/notification.ts @@ -34,6 +34,7 @@ const findFirstFreeNotificationInstanceId = () => { notificationRoutes.get('/', (req, res) => { const settings = getSettings(); + const instances = settings.notification.instances; const pageSize = req.query.take ? Number(req.query.take) : 10; const skip = req.query.skip ? Number(req.query.skip) : 0; @@ -55,7 +56,7 @@ notificationRoutes.get('/', (req, res) => { sortFunc = (a, b) => (a.id || 0) - (b.id || 0); } - const instancesResponse = settings.notification.instances + const instancesResponse = instances .sort(sortFunc) .slice(skip, skip + pageSize); @@ -63,9 +64,9 @@ notificationRoutes.get('/', (req, res) => { results: instancesResponse, agentTemplates: settings.notification.agentTemplates, pageInfo: { - pages: Math.ceil(instancesResponse.length / pageSize), + pages: Math.ceil(instances.length / pageSize), pageSize, - results: instancesResponse.length, + results: instances.length, page: Math.ceil(skip / pageSize) + 1, }, }; From 0d6fc395502fb90b134cf35b29ba1b2b7063b9a5 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Fri, 25 Apr 2025 00:30:11 +0200 Subject: [PATCH 21/62] feat(notifications): implement basic settings notifications page --- .../Settings/SettingsNotifications.tsx | 306 ++++++++++++++++-- 1 file changed, 277 insertions(+), 29 deletions(-) diff --git a/src/components/Settings/SettingsNotifications.tsx b/src/components/Settings/SettingsNotifications.tsx index 9eed585289..bf03b30b64 100644 --- a/src/components/Settings/SettingsNotifications.tsx +++ b/src/components/Settings/SettingsNotifications.tsx @@ -1,38 +1,139 @@ +import Button from '@app/components/Common/Button'; +import Header from '@app/components/Common/Header'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import PageTitle from '@app/components/Common/PageTitle'; import Table from '@app/components/Common/Table'; +import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; +import { + BarsArrowDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from '@heroicons/react/24/solid'; +import type { NotificationSettingsResultResponse } from '@server/interfaces/api/settingsInterfaces'; +import axios from 'axios'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; +import useSWR from 'swr'; const messages = defineMessages('components.Settings', { notifications: 'Notifications', - notificationSettings: 'Notification Settings', - notificationSettingsDescription: - 'Configure and enable global notification agents.', + notificationInstanceList: 'Notification Instance List', instanceName: 'Name', instanceId: 'ID', notificationAgent: 'Agent', + instanceDeleted: 'Notification instance deleted successfully!', + instanceDeleteError: + 'Something went wrong while deleting the notification instance.', email: 'Email', webhook: 'Webhook', webpush: 'Web Push', }); +enum Sort { + ID = 'id', + NAME = 'name', + AGENT = 'agent', +} + const SettingsNotifications = () => { const intl = useIntl(); + const router = useRouter(); + const { addToast } = useToasts(); + const [currentSort, setCurrentSort] = useState(Sort.ID); + const [currentPageSize, setCurrentPageSize] = useState(10); + const [, setDeleting] = useState(false); + const [selectedInstances, setSelectedInstances] = useState([]); - /*const { - data, - error, - mutate: revalidate, - } = useSWR( - `/api/v1/settings/notifications?take=${currentPageSize}&skip=${ - pageIndex * currentPageSize - }&sort=${currentSort}` - ); + const page = router.query.page ? Number(router.query.page) : 1; + const pageIndex = page - 1; + const updateQueryParams = useUpdateQueryParams({ page: page.toString() }); + + const { data, mutate: revalidate } = + useSWR( + `/api/v1/settings/notification?take=${currentPageSize}&skip=${ + pageIndex * currentPageSize + }&sort=${currentSort}` + ); + + useEffect(() => { + const filterString = window.localStorage.getItem('nl-filter-settings'); + + if (filterString) { + const filterSettings = JSON.parse(filterString); + + setCurrentSort(filterSettings.currentSort); + setCurrentPageSize(filterSettings.currentPageSize); + } + }, []); + + useEffect(() => { + window.localStorage.setItem( + 'nl-filter-settings', + JSON.stringify({ + currentSort, + currentPageSize, + }) + ); + }, [currentSort, currentPageSize]); + + const isAllInstancesSelected = () => + selectedInstances.length === data?.results.length; + const isInstanceSelected = (instanceId: number) => + selectedInstances.includes(instanceId); - if (!data && !error) { + const toggleAllInstances = () => { + if ( + data && + selectedInstances.length >= 0 && + selectedInstances.length < data.results.length - 1 + ) { + setSelectedInstances(data.results.map((instance) => instance.id)); + } else { + setSelectedInstances([]); + } + }; + + const toggleInstance = (instanceId: number) => { + if (selectedInstances.includes(instanceId)) { + setSelectedInstances((instances) => + instances.filter((u) => u !== instanceId) + ); + } else { + setSelectedInstances((instances) => [...instances, instanceId]); + } + }; + + const deleteInstance = async (instanceId: number) => { + setDeleting(true); + + try { + await axios.delete(`/api/v1/settings/notification/${instanceId}`); + + addToast(intl.formatMessage(messages.instanceDeleted), { + autoDismiss: true, + appearance: 'success', + }); + } catch (e) { + addToast(intl.formatMessage(messages.instanceDeleteError), { + autoDismiss: true, + appearance: 'error', + }); + } finally { + setDeleting(false); + revalidate(); + } + }; + + if (!data) { return ; - }*/ + } + + const hasNextPage = data.pageInfo.pages > pageIndex + 1; + const hasPrevPage = pageIndex > 0; return ( <> @@ -43,38 +144,185 @@ const SettingsNotifications = () => { ]} /> -
-

- {intl.formatMessage(messages.notificationSettings)} -

-

- {intl.formatMessage(messages.notificationSettingsDescription)} -

+
+
{intl.formatMessage(messages.notificationInstanceList)}
+
+
+ {/**/} +
+
+ + + + +
+
- + {(data.results ?? []).length > 1 && ( + { + toggleAllInstances(); + }} + /> + )} {intl.formatMessage(messages.instanceName)}{intl.formatMessage(messages.instanceId)} {intl.formatMessage(messages.notificationAgent)} + - { - - - - - + {data.results.map((instance) => ( + + + { + toggleInstance(instance.id); + }} + /> + + {instance.name} + {instance.id} + {instance.agent} + + + + - } + ))} + + + + +
From 15895555dc84629b4fdcd97ddf54ada7a06475e9 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Mon, 5 May 2025 11:52:28 +0200 Subject: [PATCH 22/62] feat(notifications): adjust settings tabs notifications link --- src/components/Settings/SettingsLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Settings/SettingsLayout.tsx b/src/components/Settings/SettingsLayout.tsx index fc0ee07ce5..da876c8715 100644 --- a/src/components/Settings/SettingsLayout.tsx +++ b/src/components/Settings/SettingsLayout.tsx @@ -67,7 +67,7 @@ const SettingsLayout = ({ children }: SettingsLayoutProps) => { }, { text: intl.formatMessage(messages.menuNotifications), - route: '/settings/notifications/email', + route: '/settings/notifications', regex: /^\/settings\/notifications/, }, { From 9c6229f82c4bb6f0c779c610c03329906116a529 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Tue, 6 May 2025 21:22:15 +0200 Subject: [PATCH 23/62] feat(notifications): move notification forms out of seperate folders --- .../{NotificationsGotify/index.tsx => NotificationsGotify.tsx} | 0 .../index.tsx => NotificationsPushbullet.tsx} | 0 .../index.tsx => NotificationsPushover.tsx} | 0 .../{NotificationsSlack/index.tsx => NotificationsSlack.tsx} | 0 .../{NotificationsWebPush/index.tsx => NotificationsWebPush.tsx} | 0 .../{NotificationsWebhook/index.tsx => NotificationsWebhook.tsx} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename src/components/Settings/Notifications/{NotificationsGotify/index.tsx => NotificationsGotify.tsx} (100%) rename src/components/Settings/Notifications/{NotificationsPushbullet/index.tsx => NotificationsPushbullet.tsx} (100%) rename src/components/Settings/Notifications/{NotificationsPushover/index.tsx => NotificationsPushover.tsx} (100%) rename src/components/Settings/Notifications/{NotificationsSlack/index.tsx => NotificationsSlack.tsx} (100%) rename src/components/Settings/Notifications/{NotificationsWebPush/index.tsx => NotificationsWebPush.tsx} (100%) rename src/components/Settings/Notifications/{NotificationsWebhook/index.tsx => NotificationsWebhook.tsx} (100%) diff --git a/src/components/Settings/Notifications/NotificationsGotify/index.tsx b/src/components/Settings/Notifications/NotificationsGotify.tsx similarity index 100% rename from src/components/Settings/Notifications/NotificationsGotify/index.tsx rename to src/components/Settings/Notifications/NotificationsGotify.tsx diff --git a/src/components/Settings/Notifications/NotificationsPushbullet/index.tsx b/src/components/Settings/Notifications/NotificationsPushbullet.tsx similarity index 100% rename from src/components/Settings/Notifications/NotificationsPushbullet/index.tsx rename to src/components/Settings/Notifications/NotificationsPushbullet.tsx diff --git a/src/components/Settings/Notifications/NotificationsPushover/index.tsx b/src/components/Settings/Notifications/NotificationsPushover.tsx similarity index 100% rename from src/components/Settings/Notifications/NotificationsPushover/index.tsx rename to src/components/Settings/Notifications/NotificationsPushover.tsx diff --git a/src/components/Settings/Notifications/NotificationsSlack/index.tsx b/src/components/Settings/Notifications/NotificationsSlack.tsx similarity index 100% rename from src/components/Settings/Notifications/NotificationsSlack/index.tsx rename to src/components/Settings/Notifications/NotificationsSlack.tsx diff --git a/src/components/Settings/Notifications/NotificationsWebPush/index.tsx b/src/components/Settings/Notifications/NotificationsWebPush.tsx similarity index 100% rename from src/components/Settings/Notifications/NotificationsWebPush/index.tsx rename to src/components/Settings/Notifications/NotificationsWebPush.tsx diff --git a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx b/src/components/Settings/Notifications/NotificationsWebhook.tsx similarity index 100% rename from src/components/Settings/Notifications/NotificationsWebhook/index.tsx rename to src/components/Settings/Notifications/NotificationsWebhook.tsx From 53c6c0a795759c5f1e610110a3dfef9c5c641f20 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Wed, 7 May 2025 16:14:19 +0200 Subject: [PATCH 24/62] fix(notifications): repair ntfy notifications with rework --- jellyseerr-api.yml | 27 ++++++++++++ server/lib/notifications/agents/ntfy.ts | 12 +----- server/lib/notifications/index.ts | 5 +++ server/lib/settings/index.ts | 3 ++ server/routes/settings/notification.ts | 56 +++---------------------- 5 files changed, 42 insertions(+), 61 deletions(-) diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index 09dd071726..e73dc58e68 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -1406,6 +1406,7 @@ components: 'discord', 'email', 'gotify', + 'ntfy', 'lunasea', 'pushbullet', 'pushover', @@ -1558,6 +1559,32 @@ components: type: string token: type: string + ntfy: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + url: + type: string + topic: + type: string + authMethodUsernamePassword: + type: boolean + username: + type: string + password: + type: string + authMethodToken: + type: boolean + token: + type: string lunasea: type: object properties: diff --git a/server/lib/notifications/agents/ntfy.ts b/server/lib/notifications/agents/ntfy.ts index 005e9aa156..c9c5b1db61 100644 --- a/server/lib/notifications/agents/ntfy.ts +++ b/server/lib/notifications/agents/ntfy.ts @@ -11,16 +11,6 @@ class NtfyAgent extends BaseAgent implements NotificationAgent { - protected getSettings(): NotificationAgentNtfy { - if (this.settings) { - return this.settings; - } - - const settings = getSettings(); - - return settings.notifications.agents.ntfy; - } - private buildPayload(type: Notification, payload: NotificationPayload) { const { applicationUrl } = getSettings().main; @@ -103,7 +93,7 @@ class NtfyAgent type: Notification, payload: NotificationPayload ): Promise { - const settings = this.getSettings(); + const settings = this.getSettings() as NotificationAgentNtfy; if ( !payload.notifySystem || diff --git a/server/lib/notifications/index.ts b/server/lib/notifications/index.ts index 0765ec0dd4..43fe2d49e5 100644 --- a/server/lib/notifications/index.ts +++ b/server/lib/notifications/index.ts @@ -3,6 +3,7 @@ import DiscordAgent from '@server/lib/notifications/agents/discord'; import EmailAgent from '@server/lib/notifications/agents/email'; import GotifyAgent from '@server/lib/notifications/agents/gotify'; import LunaSeaAgent from '@server/lib/notifications/agents/lunasea'; +import NtfyAgent from '@server/lib/notifications/agents/ntfy'; import PushbulletAgent from '@server/lib/notifications/agents/pushbullet'; import PushoverAgent from '@server/lib/notifications/agents/pushover'; import SlackAgent from '@server/lib/notifications/agents/slack'; @@ -16,6 +17,7 @@ import type { NotificationAgentEmail, NotificationAgentGotify, NotificationAgentLunaSea, + NotificationAgentNtfy, NotificationAgentPushbullet, NotificationAgentPushover, NotificationAgentSlack, @@ -132,6 +134,9 @@ export const createAccordingNotificationAgent = ( case NotificationAgentKey.GOTIFY: notificationAgent = new GotifyAgent(body as NotificationAgentGotify, id); break; + case NotificationAgentKey.NTFY: + notificationAgent = new NtfyAgent(body as NotificationAgentNtfy, id); + break; case NotificationAgentKey.LUNASEA: notificationAgent = new LunaSeaAgent( body as NotificationAgentLunaSea, diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index d5b65297bf..abf7c59acb 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -547,6 +547,9 @@ class Settings { ntfy: { enabled: false, types: 0, + name: '', + id: 0, + agent: NotificationAgentKey.NTFY, options: { url: '', topic: '', diff --git a/server/routes/settings/notification.ts b/server/routes/settings/notification.ts index 52ac753e28..2eafd61c79 100644 --- a/server/routes/settings/notification.ts +++ b/server/routes/settings/notification.ts @@ -97,11 +97,6 @@ notificationRoutes.post('/', async (req, res, next) => { const request = req.body; request.id = notificationInstanceId; - instances.push(request); - const notificationInstanceIndex = instances.findIndex( - (instance) => instance.id === notificationInstanceId - ); - const notificationAgent = createAccordingNotificationAgent( request, notificationInstanceId @@ -116,6 +111,8 @@ notificationRoutes.post('/', async (req, res, next) => { notificationManager.registerAgent(notificationAgent); + const notificationInstanceIndex = instances.length; + instances[notificationInstanceIndex] = request; await settings.save(); res.status(200).json(instances[notificationInstanceIndex]); @@ -135,11 +132,6 @@ notificationRoutes.post<{ id: string }>('/:id', async (req, res, next) => { // instance was not found -> register new one with new id if (notificationInstanceIndex === -1) { - instances.push(request); - notificationInstanceIndex = instances.findIndex( - (instance) => instance.id === notificationInstanceId - ); - const notificationAgent = createAccordingNotificationAgent( request, notificationInstanceId @@ -153,11 +145,11 @@ notificationRoutes.post<{ id: string }>('/:id', async (req, res, next) => { } notificationManager.registerAgent(notificationAgent); + + notificationInstanceIndex = instances.length; } // agent has changed -> reregister else if (instances[notificationInstanceIndex].agent !== request.agent) { - instances[notificationInstanceIndex] = request; - const notificationAgent = createAccordingNotificationAgent( request, notificationInstanceId @@ -175,10 +167,8 @@ notificationRoutes.post<{ id: string }>('/:id', async (req, res, next) => { notificationInstanceId ); } - // only change instance - else { - instances[notificationInstanceIndex] = request; - } + + instances[notificationInstanceIndex] = request; await settings.save(); @@ -231,38 +221,4 @@ notificationRoutes.post<{ id: string }>('/:id/test', async (req, res, next) => { } }); -notificationRoutes.get('/ntfy', (_req, res) => { - const settings = getSettings(); - - res.status(200).json(settings.notifications.agents.ntfy); -}); - -notificationRoutes.post('/ntfy', async (req, res) => { - const settings = getSettings(); - - settings.notifications.agents.ntfy = req.body; - await settings.save(); - - res.status(200).json(settings.notifications.agents.ntfy); -}); - -notificationRoutes.post('/ntfy/test', async (req, res, next) => { - if (!req.user) { - return next({ - status: 500, - message: 'User information is missing from the request.', - }); - } - - const ntfyAgent = new NtfyAgent(req.body); - if (await sendTestNotification(ntfyAgent, req.user)) { - return res.status(204).send(); - } else { - return next({ - status: 500, - message: 'Failed to send ntfy notification.', - }); - } -}); - export default notificationRoutes; From 8aa7531cde2d23d603fa8cd8920ddaab09251110 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Wed, 7 May 2025 17:02:08 +0200 Subject: [PATCH 25/62] feat(notifications): remove instance id from notification test endpoint --- jellyseerr-api.yml | 37 +++++------- server/routes/settings/notification.ts | 82 +++++++++++++------------- 2 files changed, 56 insertions(+), 63 deletions(-) diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index e73dc58e68..80b3c5476b 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -3364,6 +3364,21 @@ paths: application/json: schema: $ref: '#/components/schemas/NotificationInstance' + /settings/notification/test: + post: + summary: Test given notification instance settings + description: Sends a test notification from the given instance settings. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationInstance' + responses: + '204': + description: Test notification attempted /settings/notification/{instanceId}: get: summary: Get given notification instance settings @@ -3424,28 +3439,6 @@ paths: responses: '200': description: 'Notification instance was sucessfully deleted' - /settings/notification/{instanceId}/test: - post: - summary: Test given notification instance - description: Sends a test notification from the given instance settings. - tags: - - settings - parameters: - - in: path - name: instanceId - required: true - schema: - type: number - example: 0 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NotificationInstance' - responses: - '204': - description: Test notification attempted /settings/discover: get: summary: Get all discover sliders diff --git a/server/routes/settings/notification.ts b/server/routes/settings/notification.ts index 2eafd61c79..cdd6698940 100644 --- a/server/routes/settings/notification.ts +++ b/server/routes/settings/notification.ts @@ -74,20 +74,6 @@ notificationRoutes.get('/', (req, res) => { res.status(200).json(notificationResponse); }); -notificationRoutes.get<{ id: string }>('/:id', (req, res, next) => { - const settings = getSettings(); - - const notificationInstance = settings.notification.instances.find( - (instance) => instance.id === Number(req.params.id) - ); - - if (!notificationInstance) { - return next({ status: '404', message: 'Notifications instance not found' }); - } - - res.status(200).json(notificationInstance); -}); - notificationRoutes.post('/', async (req, res, next) => { const settings = getSettings(); const instances = settings.notification.instances; @@ -118,6 +104,46 @@ notificationRoutes.post('/', async (req, res, next) => { res.status(200).json(instances[notificationInstanceIndex]); }); +notificationRoutes.post('/test', async (req, res, next) => { + if (!req.user) { + return next({ + status: 500, + message: 'User information is missing from the request.', + }); + } + + const notificationAgent = createAccordingNotificationAgent(req.body); + if (!notificationAgent) { + return next({ + status: 500, + message: 'A valid agent is missing from the request.', + }); + } + + if (await sendTestNotification(notificationAgent, req.user)) { + return res.status(204).send(); + } else { + return next({ + status: 500, + message: `Failed to send ${req.body.agent} test notification.`, + }); + } +}); + +notificationRoutes.get<{ id: string }>('/:id', (req, res, next) => { + const settings = getSettings(); + + const notificationInstance = settings.notification.instances.find( + (instance) => instance.id === Number(req.params.id) + ); + + if (!notificationInstance) { + return next({ status: '404', message: 'Notifications instance not found' }); + } + + res.status(200).json(notificationInstance); +}); + notificationRoutes.post<{ id: string }>('/:id', async (req, res, next) => { const settings = getSettings(); const instances = settings.notification.instances; @@ -184,7 +210,7 @@ notificationRoutes.delete<{ id: string }>('/:id', async (req, res, next) => { ); if (notificationInstanceIndex === -1) { - return next({ status: '404', message: 'Notifications instance not found' }); + return next({ status: 404, message: 'Notifications instance not found' }); } instances.splice(notificationInstanceIndex, 1); @@ -195,30 +221,4 @@ notificationRoutes.delete<{ id: string }>('/:id', async (req, res, next) => { res.status(200).send(); }); -notificationRoutes.post<{ id: string }>('/:id/test', async (req, res, next) => { - if (!req.user) { - return next({ - status: 500, - message: 'User information is missing from the request.', - }); - } - - const notificationAgent = createAccordingNotificationAgent(req.body); - if (!notificationAgent) { - return next({ - status: 500, - message: 'A valid agent is missing from the request.', - }); - } - - if (await sendTestNotification(notificationAgent, req.user)) { - return res.status(204).send(); - } else { - return next({ - status: 500, - message: `Failed to send ${req.body.agent} notification.`, - }); - } -}); - export default notificationRoutes; From 499c264c37fe008f613d610a254742b698598dd9 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Wed, 7 May 2025 17:10:40 +0200 Subject: [PATCH 26/62] feat(notifications): restructure ntfy agent according to rework --- .../index.tsx => NotificationsNtfy.tsx} | 0 src/pages/settings/notifications/ntfy.tsx | 19 ------------------- 2 files changed, 19 deletions(-) rename src/components/Settings/Notifications/{NotificationsNtfy/index.tsx => NotificationsNtfy.tsx} (100%) delete mode 100644 src/pages/settings/notifications/ntfy.tsx diff --git a/src/components/Settings/Notifications/NotificationsNtfy/index.tsx b/src/components/Settings/Notifications/NotificationsNtfy.tsx similarity index 100% rename from src/components/Settings/Notifications/NotificationsNtfy/index.tsx rename to src/components/Settings/Notifications/NotificationsNtfy.tsx diff --git a/src/pages/settings/notifications/ntfy.tsx b/src/pages/settings/notifications/ntfy.tsx deleted file mode 100644 index 2cbfad224c..0000000000 --- a/src/pages/settings/notifications/ntfy.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import NotificationsNtfy from '@app/components/Settings/Notifications/NotificationsNtfy'; -import SettingsLayout from '@app/components/Settings/SettingsLayout'; -import SettingsNotifications from '@app/components/Settings/SettingsNotifications'; -import useRouteGuard from '@app/hooks/useRouteGuard'; -import { Permission } from '@app/hooks/useUser'; -import type { NextPage } from 'next'; - -const NotificationsPage: NextPage = () => { - useRouteGuard(Permission.ADMIN); - return ( - - - - - - ); -}; - -export default NotificationsPage; From 85a939152bde83cfbc43817771f4e0a623aa5dfa Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Fri, 9 May 2025 19:15:19 +0200 Subject: [PATCH 27/62] feat(notifications): add notification instance list test function --- .../Settings/SettingsNotifications.tsx | 74 +++++++++++++++---- 1 file changed, 61 insertions(+), 13 deletions(-) diff --git a/src/components/Settings/SettingsNotifications.tsx b/src/components/Settings/SettingsNotifications.tsx index bf03b30b64..bfb2035d5b 100644 --- a/src/components/Settings/SettingsNotifications.tsx +++ b/src/components/Settings/SettingsNotifications.tsx @@ -8,8 +8,11 @@ import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { BarsArrowDownIcon, + BeakerIcon, ChevronLeftIcon, ChevronRightIcon, + PencilIcon, + TrashIcon, } from '@heroicons/react/24/solid'; import type { NotificationSettingsResultResponse } from '@server/interfaces/api/settingsInterfaces'; import axios from 'axios'; @@ -28,9 +31,9 @@ const messages = defineMessages('components.Settings', { instanceDeleted: 'Notification instance deleted successfully!', instanceDeleteError: 'Something went wrong while deleting the notification instance.', - email: 'Email', - webhook: 'Webhook', - webpush: 'Web Push', + toastTestSending: 'Sending test notification…', + toastTestSuccess: 'Test notification sent!', + toastTestFailed: 'Test notification failed to send.', }); enum Sort { @@ -42,10 +45,9 @@ enum Sort { const SettingsNotifications = () => { const intl = useIntl(); const router = useRouter(); - const { addToast } = useToasts(); + const { addToast, removeToast } = useToasts(); const [currentSort, setCurrentSort] = useState(Sort.ID); const [currentPageSize, setCurrentPageSize] = useState(10); - const [, setDeleting] = useState(false); const [selectedInstances, setSelectedInstances] = useState([]); const page = router.query.page ? Number(router.query.page) : 1; @@ -108,8 +110,6 @@ const SettingsNotifications = () => { }; const deleteInstance = async (instanceId: number) => { - setDeleting(true); - try { await axios.delete(`/api/v1/settings/notification/${instanceId}`); @@ -123,11 +123,46 @@ const SettingsNotifications = () => { appearance: 'error', }); } finally { - setDeleting(false); revalidate(); } }; + const testInstance = async (instanceId: number) => { + let toastId: string | undefined; + try { + addToast( + intl.formatMessage(messages.toastTestSending), + { + autoDismiss: false, + appearance: 'info', + }, + (id) => { + toastId = id; + } + ); + await axios.post( + '/api/v1/settings/notification/test', + data?.results[instanceId] + ); + + if (toastId) { + removeToast(toastId); + } + addToast(intl.formatMessage(messages.toastTestSuccess), { + autoDismiss: true, + appearance: 'success', + }); + } catch (e) { + if (toastId) { + removeToast(toastId); + } + addToast(intl.formatMessage(messages.toastTestFailed), { + autoDismiss: true, + appearance: 'error', + }); + } + }; + if (!data) { return ; } @@ -230,7 +265,14 @@ const SettingsNotifications = () => { {instance.name} {instance.id} {instance.agent} - + + From 6b133a28be982daade9f6066ad00bbc4117f3795 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Sat, 10 May 2025 13:08:42 +0200 Subject: [PATCH 28/62] feat(settings): extract settings interfaces from lib to interfaces --- server/api/plexapi.ts | 2 +- server/api/servarr/base.ts | 2 +- server/api/tautulli.ts | 2 +- server/entity/User.ts | 5 +- server/entity/UserSettings.ts | 2 +- server/interfaces/api/plexInterfaces.ts | 2 +- server/interfaces/api/settingsInterfaces.ts | 2 +- .../interfaces/api/userSettingsInterfaces.ts | 2 +- server/interfaces/settings.ts | 350 ++++++++++++++++ server/job/schedule.ts | 2 +- server/lib/availabilitySync.ts | 5 +- server/lib/email/index.ts | 2 +- server/lib/notifications/agents/agent.ts | 6 +- server/lib/notifications/agents/discord.ts | 7 +- server/lib/notifications/agents/email.ts | 7 +- server/lib/notifications/agents/gotify.ts | 2 +- server/lib/notifications/agents/ntfy.ts | 2 +- server/lib/notifications/agents/pushbullet.ts | 4 +- server/lib/notifications/agents/pushover.ts | 7 +- server/lib/notifications/agents/slack.ts | 2 +- server/lib/notifications/agents/telegram.ts | 7 +- server/lib/notifications/agents/webhook.ts | 2 +- server/lib/notifications/agents/webpush.ts | 7 +- server/lib/notifications/index.ts | 35 +- server/lib/scanners/jellyfin/index.ts | 2 +- server/lib/scanners/plex/index.ts | 2 +- server/lib/scanners/radarr/index.ts | 2 +- server/lib/scanners/sonarr/index.ts | 2 +- server/lib/settings/index.ts | 383 +----------------- .../migrations/0001_migrate_hostname.ts | 2 +- .../migrations/0002_migrate_apitokens.ts | 2 +- .../migrations/0003_emby_media_server_type.ts | 2 +- .../migrations/0004_migrate_region_setting.ts | 2 +- .../0005_migrate_network_settings.ts | 2 +- .../migrations/0006_remove_lunasea.ts | 2 +- server/lib/settings/migrator.ts | 2 +- server/routes/settings/index.ts | 2 +- server/routes/settings/notification.ts | 2 +- server/routes/settings/radarr.ts | 2 +- server/routes/settings/sonarr.ts | 2 +- server/routes/user/usersettings.ts | 13 +- server/utils/customProxyAgent.ts | 2 +- server/utils/restartFlag.ts | 2 +- src/components/LanguageSelector/index.tsx | 2 +- src/components/ManageSlideOver/index.tsx | 5 +- src/components/RegionSelector/index.tsx | 2 +- .../Selector/CertificationSelector.tsx | 2 +- .../Notifications/NotificationsNtfy.tsx | 2 +- .../OverrideRule/OverrideRuleModal.tsx | 2 +- .../OverrideRule/OverrideRuleTiles.tsx | 2 +- src/components/Settings/RadarrModal/index.tsx | 2 +- src/components/Settings/SettingsJellyfin.tsx | 2 +- .../Settings/SettingsJobsCache/index.tsx | 2 +- .../Settings/SettingsMain/index.tsx | 2 +- .../Settings/SettingsNetwork/index.tsx | 2 +- src/components/Settings/SettingsPlex.tsx | 5 +- src/components/Settings/SettingsServices.tsx | 5 +- .../Settings/SettingsUsers/index.tsx | 2 +- src/components/Settings/SonarrModal/index.tsx | 2 +- src/components/Setup/index.tsx | 2 +- src/hooks/useUser.ts | 2 +- 61 files changed, 484 insertions(+), 457 deletions(-) create mode 100644 server/interfaces/settings.ts diff --git a/server/api/plexapi.ts b/server/api/plexapi.ts index 5007fe05e0..92a4b3f2dc 100644 --- a/server/api/plexapi.ts +++ b/server/api/plexapi.ts @@ -1,4 +1,4 @@ -import type { Library, PlexSettings } from '@server/lib/settings'; +import type { Library, PlexSettings } from '@server/interfaces/settings'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import NodePlexAPI from 'plex-api'; diff --git a/server/api/servarr/base.ts b/server/api/servarr/base.ts index c49b936106..5657847624 100644 --- a/server/api/servarr/base.ts +++ b/server/api/servarr/base.ts @@ -1,7 +1,7 @@ import ExternalAPI from '@server/api/externalapi'; +import type { DVRSettings } from '@server/interfaces/settings'; import type { AvailableCacheIds } from '@server/lib/cache'; import cacheManager from '@server/lib/cache'; -import type { DVRSettings } from '@server/lib/settings'; export interface SystemStatus { version: string; diff --git a/server/api/tautulli.ts b/server/api/tautulli.ts index 608e233131..1d3471ae2d 100644 --- a/server/api/tautulli.ts +++ b/server/api/tautulli.ts @@ -1,5 +1,5 @@ import type { User } from '@server/entity/User'; -import type { TautulliSettings } from '@server/lib/settings'; +import type { TautulliSettings } from '@server/interfaces/settings'; import logger from '@server/logger'; import { requestInterceptorFunction } from '@server/utils/customProxyAgent'; import type { AxiosInstance } from 'axios'; diff --git a/server/entity/User.ts b/server/entity/User.ts index 57f7500289..8c48abcbd2 100644 --- a/server/entity/User.ts +++ b/server/entity/User.ts @@ -3,12 +3,13 @@ import { UserType } from '@server/constants/user'; import { getRepository } from '@server/datasource'; import { Watchlist } from '@server/entity/Watchlist'; import type { QuotaResponse } from '@server/interfaces/api/userInterfaces'; +import type { NotificationAgentEmail } from '@server/interfaces/settings'; +import { NotificationAgentKey } from '@server/interfaces/settings'; import PreparedEmail from '@server/lib/email'; import { retrieveDefaultNotificationInstanceSettings } from '@server/lib/notifications'; import type { PermissionCheckOptions } from '@server/lib/permissions'; import { hasPermission, Permission } from '@server/lib/permissions'; -import type { NotificationAgentEmail } from '@server/lib/settings'; -import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { AfterDate } from '@server/utils/dateHelpers'; import { DbAwareColumn } from '@server/utils/DbColumnHelper'; diff --git a/server/entity/UserSettings.ts b/server/entity/UserSettings.ts index 82671fe3b3..fa125b4543 100644 --- a/server/entity/UserSettings.ts +++ b/server/entity/UserSettings.ts @@ -1,6 +1,6 @@ import type { NotificationAgentTypes } from '@server/interfaces/api/userSettingsInterfaces'; +import { NotificationAgentKey } from '@server/interfaces/settings'; import { hasNotificationType, Notification } from '@server/lib/notifications'; -import { NotificationAgentKey } from '@server/lib/settings'; import { Column, Entity, diff --git a/server/interfaces/api/plexInterfaces.ts b/server/interfaces/api/plexInterfaces.ts index 32be891e93..4b8aaa094b 100644 --- a/server/interfaces/api/plexInterfaces.ts +++ b/server/interfaces/api/plexInterfaces.ts @@ -1,4 +1,4 @@ -import type { PlexSettings } from '@server/lib/settings'; +import type { PlexSettings } from '@server/interfaces/settings'; export interface PlexStatus { settings: PlexSettings; diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index 2f76776b69..534a06ff27 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -1,7 +1,7 @@ import type { NotificationAgentConfig, NotificationAgentTemplates, -} from '@server/lib/settings'; +} from '@server/interfaces/settings'; import type { DnsEntries, DnsStats } from 'dns-caching'; import type { PaginatedResponse } from './common'; diff --git a/server/interfaces/api/userSettingsInterfaces.ts b/server/interfaces/api/userSettingsInterfaces.ts index 327764618e..32be24aa5c 100644 --- a/server/interfaces/api/userSettingsInterfaces.ts +++ b/server/interfaces/api/userSettingsInterfaces.ts @@ -1,4 +1,4 @@ -import type { NotificationAgentKey } from '@server/lib/settings'; +import type { NotificationAgentKey } from '@server/interfaces/settings'; export interface UserSettingsGeneralResponse { username?: string; diff --git a/server/interfaces/settings.ts b/server/interfaces/settings.ts new file mode 100644 index 0000000000..37b04bcad0 --- /dev/null +++ b/server/interfaces/settings.ts @@ -0,0 +1,350 @@ +export interface Library { + id: string; + name: string; + enabled: boolean; + type: 'show' | 'movie'; + lastScan?: number; +} + +export interface Region { + iso_3166_1: string; + english_name: string; + name?: string; +} + +export interface Language { + iso_639_1: string; + english_name: string; + name: string; +} + +export interface PlexSettings { + name: string; + machineId?: string; + ip: string; + port: number; + useSsl?: boolean; + libraries: Library[]; + webAppUrl?: string; +} + +export interface JellyfinSettings { + name: string; + ip: string; + port: number; + useSsl?: boolean; + urlBase?: string; + externalHostname?: string; + jellyfinForgotPasswordUrl?: string; + libraries: Library[]; + serverId: string; + apiKey: string; +} +export interface TautulliSettings { + hostname?: string; + port?: number; + useSsl?: boolean; + urlBase?: string; + apiKey?: string; + externalUrl?: string; +} + +export interface DVRSettings { + id: number; + name: string; + hostname: string; + port: number; + apiKey: string; + useSsl: boolean; + baseUrl?: string; + activeProfileId: number; + activeProfileName: string; + activeDirectory: string; + tags: number[]; + is4k: boolean; + isDefault: boolean; + externalUrl?: string; + syncEnabled: boolean; + preventSearch: boolean; + tagRequests: boolean; + overrideRule: number[]; +} + +export interface RadarrSettings extends DVRSettings { + minimumAvailability: string; +} + +export interface SonarrSettings extends DVRSettings { + seriesType: 'standard' | 'daily' | 'anime'; + animeSeriesType: 'standard' | 'daily' | 'anime'; + activeAnimeProfileId?: number; + activeAnimeProfileName?: string; + activeAnimeDirectory?: string; + activeAnimeLanguageProfileId?: number; + activeLanguageProfileId?: number; + animeTags?: number[]; + enableSeasonFolders: boolean; +} + +interface Quota { + quotaLimit?: number; + quotaDays?: number; +} + +export interface MainSettings { + apiKey: string; + applicationTitle: string; + applicationUrl: string; + cacheImages: boolean; + defaultPermissions: number; + defaultQuotas: { + movie: Quota; + tv: Quota; + }; + hideAvailable: boolean; + hideBlacklisted: boolean; + localLogin: boolean; + mediaServerLogin: boolean; + newPlexLogin: boolean; + discoverRegion: string; + streamingRegion: string; + originalLanguage: string; + blacklistedTags: string; + blacklistedTagsLimit: number; + mediaServerType: number; + partialRequestsEnabled: boolean; + enableSpecialEpisodes: boolean; + locale: string; + youtubeUrl: string; +} + +export enum MetadataProviderType { + TMDB = 'tmdb', + TVDB = 'tvdb', +} + +export interface MetadataSettings { + tv: MetadataProviderType; + anime: MetadataProviderType; +} + +export interface ProxySettings { + enabled: boolean; + hostname: string; + port: number; + useSsl: boolean; + user: string; + password: string; + bypassFilter: string; + bypassLocalAddresses: boolean; +} + +export interface DnsCacheSettings { + enabled: boolean; + forceMinTtl?: number; + forceMaxTtl?: number; +} + +export interface NetworkSettings { + csrfProtection: boolean; + forceIpv4First: boolean; + trustProxy: boolean; + proxy: ProxySettings; + dnsCache: DnsCacheSettings; +} + +export interface PublicSettings { + initialized: boolean; +} + +export interface FullPublicSettings extends PublicSettings { + applicationTitle: string; + applicationUrl: string; + hideAvailable: boolean; + hideBlacklisted: boolean; + localLogin: boolean; + mediaServerLogin: boolean; + movie4kEnabled: boolean; + series4kEnabled: boolean; + discoverRegion: string; + streamingRegion: string; + originalLanguage: string; + mediaServerType: number; + jellyfinExternalHost?: string; + jellyfinForgotPasswordUrl?: string; + jellyfinServerName?: string; + partialRequestsEnabled: boolean; + enableSpecialEpisodes: boolean; + cacheImages: boolean; + vapidPublic: string; + enablePushRegistration: boolean; + locale: string; + emailEnabled: boolean; + userEmailRequired: boolean; + newPlexLogin: boolean; + youtubeUrl: string; +} + +export interface NotificationAgentConfig { + enabled: boolean; + types?: number; + name: string; + id: number; + agent: NotificationAgentKey; + default?: boolean; + options: Record; +} + +export interface NotificationAgentDiscord extends NotificationAgentConfig { + options: { + botUsername?: string; + botAvatarUrl?: string; + webhookUrl: string; + webhookRoleId?: string; + enableMentions: boolean; + }; +} + +export interface NotificationAgentSlack extends NotificationAgentConfig { + options: { + webhookUrl: string; + }; +} + +export interface NotificationAgentEmail extends NotificationAgentConfig { + options: { + userEmailRequired: boolean; + 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 NotificationAgentTelegram extends NotificationAgentConfig { + options: { + botUsername?: string; + botAPI: string; + chatId: string; + messageThreadId: string; + sendSilently: boolean; + }; +} + +export interface NotificationAgentPushbullet extends NotificationAgentConfig { + options: { + accessToken: string; + channelTag?: string; + }; +} + +export interface NotificationAgentPushover extends NotificationAgentConfig { + options: { + accessToken: string; + userToken: string; + sound: string; + }; +} + +export interface NotificationAgentWebhook extends NotificationAgentConfig { + options: { + webhookUrl: string; + jsonPayload: string; + authHeader?: string; + }; +} + +export interface NotificationAgentGotify extends NotificationAgentConfig { + options: { + url: string; + token: string; + priority: number; + }; +} + +export interface NotificationAgentNtfy extends NotificationAgentConfig { + options: { + url: string; + topic: string; + authMethodUsernamePassword?: boolean; + username?: string; + password?: string; + authMethodToken?: boolean; + token?: string; + }; +} + +export enum NotificationAgentKey { + DISCORD = 'discord', + EMAIL = 'email', + GOTIFY = 'gotify', + NTFY = 'ntfy', + PUSHBULLET = 'pushbullet', + PUSHOVER = 'pushover', + SLACK = 'slack', + TELEGRAM = 'telegram', + WEBHOOK = 'webhook', + WEBPUSH = 'webpush', +} + +export interface NotificationAgentTemplates { + discord: NotificationAgentDiscord; + email: NotificationAgentEmail; + gotify: NotificationAgentGotify; + ntfy: NotificationAgentNtfy; + pushbullet: NotificationAgentPushbullet; + pushover: NotificationAgentPushover; + slack: NotificationAgentSlack; + telegram: NotificationAgentTelegram; + webhook: NotificationAgentWebhook; + webpush: NotificationAgentConfig; +} + +export interface NotificationSettings { + instances: NotificationAgentConfig[]; + agentTemplates: NotificationAgentTemplates; +} + +export interface JobSettings { + schedule: string; +} + +export type JobId = + | 'plex-recently-added-scan' + | 'plex-full-scan' + | 'plex-watchlist-sync' + | 'plex-refresh-token' + | 'radarr-scan' + | 'sonarr-scan' + | 'download-sync' + | 'download-sync-reset' + | 'jellyfin-recently-added-scan' + | 'jellyfin-full-scan' + | 'image-cache-cleanup' + | 'availability-sync' + | 'process-blacklisted-tags'; + +export interface AllSettings { + clientId: string; + vapidPublic: string; + vapidPrivate: string; + main: MainSettings; + plex: PlexSettings; + jellyfin: JellyfinSettings; + tautulli: TautulliSettings; + radarr: RadarrSettings[]; + sonarr: SonarrSettings[]; + public: PublicSettings; + notification: NotificationSettings; + jobs: Record; + network: NetworkSettings; + metadataSettings: MetadataSettings; +} diff --git a/server/job/schedule.ts b/server/job/schedule.ts index c740dbaec5..3f928cd280 100644 --- a/server/job/schedule.ts +++ b/server/job/schedule.ts @@ -1,4 +1,5 @@ import { MediaServerType } from '@server/constants/server'; +import type { JobId } from '@server/interfaces/settings'; import blacklistedTagsProcessor from '@server/job/blacklistedTagsProcessor'; import availabilitySync from '@server/lib/availabilitySync'; import downloadTracker from '@server/lib/downloadtracker'; @@ -11,7 +12,6 @@ import { import { plexFullScanner, plexRecentScanner } from '@server/lib/scanners/plex'; import { radarrScanner } from '@server/lib/scanners/radarr'; import { sonarrScanner } from '@server/lib/scanners/sonarr'; -import type { JobId } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import watchlistSync from '@server/lib/watchlistsync'; import logger from '@server/logger'; diff --git a/server/lib/availabilitySync.ts b/server/lib/availabilitySync.ts index 9bdf51c40c..32c4b38187 100644 --- a/server/lib/availabilitySync.ts +++ b/server/lib/availabilitySync.ts @@ -12,7 +12,10 @@ import Media from '@server/entity/Media'; import MediaRequest from '@server/entity/MediaRequest'; import type Season from '@server/entity/Season'; import { User } from '@server/entity/User'; -import type { RadarrSettings, SonarrSettings } from '@server/lib/settings'; +import type { + RadarrSettings, + SonarrSettings, +} from '@server/interfaces/settings'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { getHostname } from '@server/utils/getHostname'; diff --git a/server/lib/email/index.ts b/server/lib/email/index.ts index 66b3698faf..f48efaec9f 100644 --- a/server/lib/email/index.ts +++ b/server/lib/email/index.ts @@ -1,4 +1,4 @@ -import type { NotificationAgentEmail } from '@server/lib/settings'; +import type { NotificationAgentEmail } from '@server/interfaces/settings'; import { getSettings } from '@server/lib/settings'; import Email from 'email-templates'; import nodemailer from 'nodemailer'; diff --git a/server/lib/notifications/agents/agent.ts b/server/lib/notifications/agents/agent.ts index d4181ec157..08799d411c 100644 --- a/server/lib/notifications/agents/agent.ts +++ b/server/lib/notifications/agents/agent.ts @@ -3,10 +3,8 @@ import type IssueComment from '@server/entity/IssueComment'; import type Media from '@server/entity/Media'; import type { MediaRequest } from '@server/entity/MediaRequest'; import type { User } from '@server/entity/User'; -import { - getSettings, - type NotificationAgentConfig, -} from '@server/lib/settings'; +import type { NotificationAgentConfig } from '@server/interfaces/settings'; +import { getSettings } from '@server/lib/settings'; import type { Notification } from '..'; export interface NotificationPayload { diff --git a/server/lib/notifications/agents/discord.ts b/server/lib/notifications/agents/discord.ts index d5956c6926..ec52b10a9d 100644 --- a/server/lib/notifications/agents/discord.ts +++ b/server/lib/notifications/agents/discord.ts @@ -1,8 +1,11 @@ import { IssueStatus, IssueTypeName } from '@server/constants/issue'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; -import type { NotificationAgentDiscord } from '@server/lib/settings'; -import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import { + NotificationAgentKey, + type NotificationAgentDiscord, +} from '@server/interfaces/settings'; +import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import axios from 'axios'; import { diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 1ef67f349e..15b13b7a2e 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -2,9 +2,12 @@ import { IssueType, IssueTypeName } from '@server/constants/issue'; import { MediaType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; +import { + NotificationAgentKey, + type NotificationAgentEmail, +} from '@server/interfaces/settings'; import PreparedEmail from '@server/lib/email'; -import type { NotificationAgentEmail } from '@server/lib/settings'; -import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import type { EmailOptions } from 'email-templates'; import * as EmailValidator from 'email-validator'; diff --git a/server/lib/notifications/agents/gotify.ts b/server/lib/notifications/agents/gotify.ts index c03b0bbd1a..caded2ce76 100644 --- a/server/lib/notifications/agents/gotify.ts +++ b/server/lib/notifications/agents/gotify.ts @@ -1,5 +1,5 @@ import { IssueStatus, IssueTypeName } from '@server/constants/issue'; -import type { NotificationAgentGotify } from '@server/lib/settings'; +import type { NotificationAgentGotify } from '@server/interfaces/settings'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import axios from 'axios'; diff --git a/server/lib/notifications/agents/ntfy.ts b/server/lib/notifications/agents/ntfy.ts index c9c5b1db61..9736e45aee 100644 --- a/server/lib/notifications/agents/ntfy.ts +++ b/server/lib/notifications/agents/ntfy.ts @@ -1,5 +1,5 @@ import { IssueStatus, IssueTypeName } from '@server/constants/issue'; -import type { NotificationAgentNtfy } from '@server/lib/settings'; +import type { NotificationAgentNtfy } from '@server/interfaces/settings'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import axios from 'axios'; diff --git a/server/lib/notifications/agents/pushbullet.ts b/server/lib/notifications/agents/pushbullet.ts index 04e0c2fc64..9bec6b81ea 100644 --- a/server/lib/notifications/agents/pushbullet.ts +++ b/server/lib/notifications/agents/pushbullet.ts @@ -2,8 +2,8 @@ import { IssueStatus, IssueTypeName } from '@server/constants/issue'; import { MediaStatus } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; -import type { NotificationAgentPushbullet } from '@server/lib/settings'; -import { NotificationAgentKey } from '@server/lib/settings'; +import type { NotificationAgentPushbullet } from '@server/interfaces/settings'; +import { NotificationAgentKey } from '@server/interfaces/settings'; import logger from '@server/logger'; import axios from 'axios'; import { diff --git a/server/lib/notifications/agents/pushover.ts b/server/lib/notifications/agents/pushover.ts index a0cfef2c07..b4e364d960 100644 --- a/server/lib/notifications/agents/pushover.ts +++ b/server/lib/notifications/agents/pushover.ts @@ -2,8 +2,11 @@ import { IssueStatus, IssueTypeName } from '@server/constants/issue'; import { MediaStatus } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; -import type { NotificationAgentPushover } from '@server/lib/settings'; -import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import { + NotificationAgentKey, + type NotificationAgentPushover, +} from '@server/interfaces/settings'; +import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import axios from 'axios'; import { diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index 0621f3d454..6b81710a58 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -1,5 +1,5 @@ import { IssueStatus, IssueTypeName } from '@server/constants/issue'; -import type { NotificationAgentSlack } from '@server/lib/settings'; +import type { NotificationAgentSlack } from '@server/interfaces/settings'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import axios from 'axios'; diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index 860773c638..f48cc21dc0 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -2,8 +2,11 @@ import { IssueStatus, IssueTypeName } from '@server/constants/issue'; import { MediaStatus } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; -import type { NotificationAgentTelegram } from '@server/lib/settings'; -import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import { + NotificationAgentKey, + type NotificationAgentTelegram, +} from '@server/interfaces/settings'; +import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import axios from 'axios'; import { diff --git a/server/lib/notifications/agents/webhook.ts b/server/lib/notifications/agents/webhook.ts index 58ec1f463c..c30aaa6680 100644 --- a/server/lib/notifications/agents/webhook.ts +++ b/server/lib/notifications/agents/webhook.ts @@ -1,6 +1,6 @@ import { IssueStatus, IssueType } from '@server/constants/issue'; import { MediaStatus } from '@server/constants/media'; -import type { NotificationAgentWebhook } from '@server/lib/settings'; +import type { NotificationAgentWebhook } from '@server/interfaces/settings'; import logger from '@server/logger'; import axios from 'axios'; import { get } from 'lodash'; diff --git a/server/lib/notifications/agents/webpush.ts b/server/lib/notifications/agents/webpush.ts index bd1f7f0532..79c9f1ce4e 100644 --- a/server/lib/notifications/agents/webpush.ts +++ b/server/lib/notifications/agents/webpush.ts @@ -4,8 +4,11 @@ import { getRepository } from '@server/datasource'; import MediaRequest from '@server/entity/MediaRequest'; import { User } from '@server/entity/User'; import { UserPushSubscription } from '@server/entity/UserPushSubscription'; -import type { NotificationAgentConfig } from '@server/lib/settings'; -import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import { + NotificationAgentKey, + type NotificationAgentConfig, +} from '@server/interfaces/settings'; +import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import webpush from 'web-push'; import { Notification, shouldSendAdminNotification } from '..'; diff --git a/server/lib/notifications/index.ts b/server/lib/notifications/index.ts index 43fe2d49e5..1c6a366519 100644 --- a/server/lib/notifications/index.ts +++ b/server/lib/notifications/index.ts @@ -1,8 +1,20 @@ import type { User } from '@server/entity/User'; +import { + NotificationAgentKey, + type NotificationAgentConfig, + type NotificationAgentDiscord, + type NotificationAgentEmail, + type NotificationAgentGotify, + type NotificationAgentNtfy, + type NotificationAgentPushbullet, + type NotificationAgentPushover, + type NotificationAgentSlack, + type NotificationAgentTelegram, + type NotificationAgentWebhook, +} from '@server/interfaces/settings'; import DiscordAgent from '@server/lib/notifications/agents/discord'; import EmailAgent from '@server/lib/notifications/agents/email'; import GotifyAgent from '@server/lib/notifications/agents/gotify'; -import LunaSeaAgent from '@server/lib/notifications/agents/lunasea'; import NtfyAgent from '@server/lib/notifications/agents/ntfy'; import PushbulletAgent from '@server/lib/notifications/agents/pushbullet'; import PushoverAgent from '@server/lib/notifications/agents/pushover'; @@ -11,20 +23,7 @@ import TelegramAgent from '@server/lib/notifications/agents/telegram'; import WebhookAgent from '@server/lib/notifications/agents/webhook'; import WebPushAgent from '@server/lib/notifications/agents/webpush'; import { Permission } from '@server/lib/permissions'; -import type { - NotificationAgentConfig, - NotificationAgentDiscord, - NotificationAgentEmail, - NotificationAgentGotify, - NotificationAgentLunaSea, - NotificationAgentNtfy, - NotificationAgentPushbullet, - NotificationAgentPushover, - NotificationAgentSlack, - NotificationAgentTelegram, - NotificationAgentWebhook, -} from '@server/lib/settings'; -import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import type { NotificationAgent, NotificationPayload } from './agents/agent'; @@ -137,12 +136,6 @@ export const createAccordingNotificationAgent = ( case NotificationAgentKey.NTFY: notificationAgent = new NtfyAgent(body as NotificationAgentNtfy, id); break; - case NotificationAgentKey.LUNASEA: - notificationAgent = new LunaSeaAgent( - body as NotificationAgentLunaSea, - id - ); - break; case NotificationAgentKey.PUSHBULLET: notificationAgent = new PushbulletAgent( body as NotificationAgentPushbullet, diff --git a/server/lib/scanners/jellyfin/index.ts b/server/lib/scanners/jellyfin/index.ts index 3283e34206..cfda899fc1 100644 --- a/server/lib/scanners/jellyfin/index.ts +++ b/server/lib/scanners/jellyfin/index.ts @@ -13,7 +13,7 @@ import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; import Season from '@server/entity/Season'; import { User } from '@server/entity/User'; -import type { Library } from '@server/lib/settings'; +import type { Library } from '@server/interfaces/settings'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import AsyncLock from '@server/utils/asyncLock'; diff --git a/server/lib/scanners/plex/index.ts b/server/lib/scanners/plex/index.ts index 24862e5582..c7d17c058f 100644 --- a/server/lib/scanners/plex/index.ts +++ b/server/lib/scanners/plex/index.ts @@ -10,6 +10,7 @@ import type { } from '@server/api/themoviedb/interfaces'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; +import type { Library } from '@server/interfaces/settings'; import cacheManager from '@server/lib/cache'; import type { MediaIds, @@ -18,7 +19,6 @@ import type { StatusBase, } from '@server/lib/scanners/baseScanner'; import BaseScanner from '@server/lib/scanners/baseScanner'; -import type { Library } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import { uniqWith } from 'lodash'; diff --git a/server/lib/scanners/radarr/index.ts b/server/lib/scanners/radarr/index.ts index bc299d7b16..70b73375ef 100644 --- a/server/lib/scanners/radarr/index.ts +++ b/server/lib/scanners/radarr/index.ts @@ -1,11 +1,11 @@ import type { RadarrMovie } from '@server/api/servarr/radarr'; import RadarrAPI from '@server/api/servarr/radarr'; +import type { RadarrSettings } from '@server/interfaces/settings'; import type { RunnableScanner, StatusBase, } from '@server/lib/scanners/baseScanner'; import BaseScanner from '@server/lib/scanners/baseScanner'; -import type { RadarrSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import { uniqWith } from 'lodash'; diff --git a/server/lib/scanners/sonarr/index.ts b/server/lib/scanners/sonarr/index.ts index 7a6e95c0aa..d2844fef7a 100644 --- a/server/lib/scanners/sonarr/index.ts +++ b/server/lib/scanners/sonarr/index.ts @@ -3,13 +3,13 @@ import SonarrAPI from '@server/api/servarr/sonarr'; import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; +import type { SonarrSettings } from '@server/interfaces/settings'; import type { ProcessableSeason, RunnableScanner, StatusBase, } from '@server/lib/scanners/baseScanner'; import BaseScanner from '@server/lib/scanners/baseScanner'; -import type { SonarrSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import { uniqWith } from 'lodash'; diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index abf7c59acb..5382ed8085 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -1,4 +1,24 @@ import { MediaServerType } from '@server/constants/server'; +import type { + FullPublicSettings, + JellyfinSettings, + JobId, + JobSettings, + MainSettings, + MetadataSettings, + NetworkSettings, + NotificationSettings, + PlexSettings, + PublicSettings, + RadarrSettings, + SonarrSettings, + TautulliSettings, +} from '@server/interfaces/settings'; +import { + MetadataProviderType, + NotificationAgentKey, + type AllSettings, +} from '@server/interfaces/settings'; import { Permission } from '@server/lib/permissions'; import { runMigrations } from '@server/lib/settings/migrator'; import { randomUUID } from 'crypto'; @@ -7,369 +27,6 @@ import { merge } from 'lodash'; import path from 'path'; import webpush from 'web-push'; -export interface Library { - id: string; - name: string; - enabled: boolean; - type: 'show' | 'movie'; - lastScan?: number; -} - -export interface Region { - iso_3166_1: string; - english_name: string; - name?: string; -} - -export interface Language { - iso_639_1: string; - english_name: string; - name: string; -} - -export interface PlexSettings { - name: string; - machineId?: string; - ip: string; - port: number; - useSsl?: boolean; - libraries: Library[]; - webAppUrl?: string; -} - -export interface JellyfinSettings { - name: string; - ip: string; - port: number; - useSsl?: boolean; - urlBase?: string; - externalHostname?: string; - jellyfinForgotPasswordUrl?: string; - libraries: Library[]; - serverId: string; - apiKey: string; -} -export interface TautulliSettings { - hostname?: string; - port?: number; - useSsl?: boolean; - urlBase?: string; - apiKey?: string; - externalUrl?: string; -} - -export interface DVRSettings { - id: number; - name: string; - hostname: string; - port: number; - apiKey: string; - useSsl: boolean; - baseUrl?: string; - activeProfileId: number; - activeProfileName: string; - activeDirectory: string; - tags: number[]; - is4k: boolean; - isDefault: boolean; - externalUrl?: string; - syncEnabled: boolean; - preventSearch: boolean; - tagRequests: boolean; - overrideRule: number[]; -} - -export interface RadarrSettings extends DVRSettings { - minimumAvailability: string; -} - -export interface SonarrSettings extends DVRSettings { - seriesType: 'standard' | 'daily' | 'anime'; - animeSeriesType: 'standard' | 'daily' | 'anime'; - activeAnimeProfileId?: number; - activeAnimeProfileName?: string; - activeAnimeDirectory?: string; - activeAnimeLanguageProfileId?: number; - activeLanguageProfileId?: number; - animeTags?: number[]; - enableSeasonFolders: boolean; -} - -interface Quota { - quotaLimit?: number; - quotaDays?: number; -} - -export enum MetadataProviderType { - TMDB = 'tmdb', - TVDB = 'tvdb', -} - -export interface MetadataSettings { - tv: MetadataProviderType; - anime: MetadataProviderType; -} - -export interface ProxySettings { - enabled: boolean; - hostname: string; - port: number; - useSsl: boolean; - user: string; - password: string; - bypassFilter: string; - bypassLocalAddresses: boolean; -} - -export interface MainSettings { - apiKey: string; - applicationTitle: string; - applicationUrl: string; - cacheImages: boolean; - defaultPermissions: number; - defaultQuotas: { - movie: Quota; - tv: Quota; - }; - hideAvailable: boolean; - hideBlacklisted: boolean; - localLogin: boolean; - mediaServerLogin: boolean; - newPlexLogin: boolean; - discoverRegion: string; - streamingRegion: string; - originalLanguage: string; - blacklistedTags: string; - blacklistedTagsLimit: number; - mediaServerType: number; - partialRequestsEnabled: boolean; - enableSpecialEpisodes: boolean; - locale: string; - youtubeUrl: string; -} - -export interface ProxySettings { - enabled: boolean; - hostname: string; - port: number; - useSsl: boolean; - user: string; - password: string; - bypassFilter: string; - bypassLocalAddresses: boolean; -} - -export interface DnsCacheSettings { - enabled: boolean; - forceMinTtl?: number; - forceMaxTtl?: number; -} - -export interface NetworkSettings { - csrfProtection: boolean; - forceIpv4First: boolean; - trustProxy: boolean; - proxy: ProxySettings; - dnsCache: DnsCacheSettings; -} - -interface PublicSettings { - initialized: boolean; -} - -interface FullPublicSettings extends PublicSettings { - applicationTitle: string; - applicationUrl: string; - hideAvailable: boolean; - hideBlacklisted: boolean; - localLogin: boolean; - mediaServerLogin: boolean; - movie4kEnabled: boolean; - series4kEnabled: boolean; - discoverRegion: string; - streamingRegion: string; - originalLanguage: string; - mediaServerType: number; - jellyfinExternalHost?: string; - jellyfinForgotPasswordUrl?: string; - jellyfinServerName?: string; - partialRequestsEnabled: boolean; - enableSpecialEpisodes: boolean; - cacheImages: boolean; - vapidPublic: string; - enablePushRegistration: boolean; - locale: string; - emailEnabled: boolean; - userEmailRequired: boolean; - newPlexLogin: boolean; - youtubeUrl: string; -} - -export interface NotificationAgentConfig { - enabled: boolean; - types?: number; - name: string; - id: number; - agent: NotificationAgentKey; - default?: boolean; - options: Record; -} - -export interface NotificationAgentDiscord extends NotificationAgentConfig { - options: { - botUsername?: string; - botAvatarUrl?: string; - webhookUrl: string; - webhookRoleId?: string; - enableMentions: boolean; - }; -} - -export interface NotificationAgentSlack extends NotificationAgentConfig { - options: { - webhookUrl: string; - }; -} - -export interface NotificationAgentEmail extends NotificationAgentConfig { - options: { - userEmailRequired: boolean; - 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 NotificationAgentTelegram extends NotificationAgentConfig { - options: { - botUsername?: string; - botAPI: string; - chatId: string; - messageThreadId: string; - sendSilently: boolean; - }; -} - -export interface NotificationAgentPushbullet extends NotificationAgentConfig { - options: { - accessToken: string; - channelTag?: string; - }; -} - -export interface NotificationAgentPushover extends NotificationAgentConfig { - options: { - accessToken: string; - userToken: string; - sound: string; - }; -} - -export interface NotificationAgentWebhook extends NotificationAgentConfig { - options: { - webhookUrl: string; - jsonPayload: string; - authHeader?: string; - }; -} - -export interface NotificationAgentGotify extends NotificationAgentConfig { - options: { - url: string; - token: string; - priority: number; - }; -} - -export interface NotificationAgentNtfy extends NotificationAgentConfig { - options: { - url: string; - topic: string; - authMethodUsernamePassword?: boolean; - username?: string; - password?: string; - authMethodToken?: boolean; - token?: string; - }; -} - -export enum NotificationAgentKey { - DISCORD = 'discord', - EMAIL = 'email', - GOTIFY = 'gotify', - NTFY = 'ntfy', - LUNASEA = 'lunasea', - PUSHBULLET = 'pushbullet', - PUSHOVER = 'pushover', - SLACK = 'slack', - TELEGRAM = 'telegram', - WEBHOOK = 'webhook', - WEBPUSH = 'webpush', -} - -export interface NotificationAgentTemplates { - discord: NotificationAgentDiscord; - email: NotificationAgentEmail; - gotify: NotificationAgentGotify; - ntfy: NotificationAgentNtfy; - pushbullet: NotificationAgentPushbullet; - pushover: NotificationAgentPushover; - slack: NotificationAgentSlack; - telegram: NotificationAgentTelegram; - webhook: NotificationAgentWebhook; - webpush: NotificationAgentConfig; -} - -interface NotificationSettings { - instances: NotificationAgentConfig[]; - agentTemplates: NotificationAgentTemplates; -} - -interface JobSettings { - schedule: string; -} - -export type JobId = - | 'plex-recently-added-scan' - | 'plex-full-scan' - | 'plex-watchlist-sync' - | 'plex-refresh-token' - | 'radarr-scan' - | 'sonarr-scan' - | 'download-sync' - | 'download-sync-reset' - | 'jellyfin-recently-added-scan' - | 'jellyfin-full-scan' - | 'image-cache-cleanup' - | 'availability-sync' - | 'process-blacklisted-tags'; - -export interface AllSettings { - clientId: string; - vapidPublic: string; - vapidPrivate: string; - main: MainSettings; - plex: PlexSettings; - jellyfin: JellyfinSettings; - tautulli: TautulliSettings; - radarr: RadarrSettings[]; - sonarr: SonarrSettings[]; - public: PublicSettings; - notification: NotificationSettings; - jobs: Record; - network: NetworkSettings; - metadataSettings: MetadataSettings; -} - const SETTINGS_PATH = process.env.CONFIG_DIRECTORY ? `${process.env.CONFIG_DIRECTORY}/settings.json` : path.join(__dirname, '../../../config/settings.json'); diff --git a/server/lib/settings/migrations/0001_migrate_hostname.ts b/server/lib/settings/migrations/0001_migrate_hostname.ts index ddc8211cf9..6c6fcd801a 100644 --- a/server/lib/settings/migrations/0001_migrate_hostname.ts +++ b/server/lib/settings/migrations/0001_migrate_hostname.ts @@ -1,4 +1,4 @@ -import type { AllSettings } from '@server/lib/settings'; +import type { AllSettings } from '@server/interfaces/settings'; const migrateHostname = (settings: any): AllSettings => { if (settings.jellyfin?.hostname) { diff --git a/server/lib/settings/migrations/0002_migrate_apitokens.ts b/server/lib/settings/migrations/0002_migrate_apitokens.ts index 0149c3e37f..fb71bc0ab8 100644 --- a/server/lib/settings/migrations/0002_migrate_apitokens.ts +++ b/server/lib/settings/migrations/0002_migrate_apitokens.ts @@ -2,7 +2,7 @@ import JellyfinAPI from '@server/api/jellyfin'; import { MediaServerType } from '@server/constants/server'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; -import type { AllSettings } from '@server/lib/settings'; +import type { AllSettings } from '@server/interfaces/settings'; import { getHostname } from '@server/utils/getHostname'; const migrateApiTokens = async (settings: any): Promise => { diff --git a/server/lib/settings/migrations/0003_emby_media_server_type.ts b/server/lib/settings/migrations/0003_emby_media_server_type.ts index 1671e7a98f..1ebc62acd6 100644 --- a/server/lib/settings/migrations/0003_emby_media_server_type.ts +++ b/server/lib/settings/migrations/0003_emby_media_server_type.ts @@ -1,5 +1,5 @@ import { MediaServerType } from '@server/constants/server'; -import type { AllSettings } from '@server/lib/settings'; +import type { AllSettings } from '@server/interfaces/settings'; const migrateHostname = (settings: any): AllSettings => { const oldMediaServerType = settings.main.mediaServerType; diff --git a/server/lib/settings/migrations/0004_migrate_region_setting.ts b/server/lib/settings/migrations/0004_migrate_region_setting.ts index a140e07818..b5ae15999a 100644 --- a/server/lib/settings/migrations/0004_migrate_region_setting.ts +++ b/server/lib/settings/migrations/0004_migrate_region_setting.ts @@ -1,4 +1,4 @@ -import type { AllSettings } from '@server/lib/settings'; +import type { AllSettings } from '@server/interfaces/settings'; const migrateRegionSetting = (settings: any): AllSettings => { if ( diff --git a/server/lib/settings/migrations/0005_migrate_network_settings.ts b/server/lib/settings/migrations/0005_migrate_network_settings.ts index 6d4a826f3e..dd8e6a71d4 100644 --- a/server/lib/settings/migrations/0005_migrate_network_settings.ts +++ b/server/lib/settings/migrations/0005_migrate_network_settings.ts @@ -1,4 +1,4 @@ -import type { AllSettings } from '@server/lib/settings'; +import type { AllSettings } from '@server/interfaces/settings'; const migrateNetworkSettings = (settings: any): AllSettings => { if (settings.network) { diff --git a/server/lib/settings/migrations/0006_remove_lunasea.ts b/server/lib/settings/migrations/0006_remove_lunasea.ts index e5279afa9c..7f4830bc33 100644 --- a/server/lib/settings/migrations/0006_remove_lunasea.ts +++ b/server/lib/settings/migrations/0006_remove_lunasea.ts @@ -1,4 +1,4 @@ -import type { AllSettings } from '@server/lib/settings'; +import type { AllSettings } from '@server/interfaces/settings'; const removeLunaSeaSetting = (settings: any): AllSettings => { if ( diff --git a/server/lib/settings/migrator.ts b/server/lib/settings/migrator.ts index 801140000a..cf3584c3d8 100644 --- a/server/lib/settings/migrator.ts +++ b/server/lib/settings/migrator.ts @@ -1,4 +1,4 @@ -import type { AllSettings } from '@server/lib/settings'; +import type { AllSettings } from '@server/interfaces/settings'; import logger from '@server/logger'; import fs from 'fs/promises'; import path from 'path'; diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index 6152c91a62..fb08eb317b 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -13,6 +13,7 @@ import type { LogsResultsResponse, SettingsAboutResponse, } from '@server/interfaces/api/settingsInterfaces'; +import type { JobId, Library, MainSettings } from '@server/interfaces/settings'; import { scheduledJobs } from '@server/job/schedule'; import type { AvailableCacheIds } from '@server/lib/cache'; import cacheManager from '@server/lib/cache'; @@ -20,7 +21,6 @@ import ImageProxy from '@server/lib/imageproxy'; import { Permission } from '@server/lib/permissions'; import { jellyfinFullScanner } from '@server/lib/scanners/jellyfin'; import { plexFullScanner } from '@server/lib/scanners/plex'; -import type { JobId, Library, MainSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; diff --git a/server/routes/settings/notification.ts b/server/routes/settings/notification.ts index cdd6698940..072eaa38be 100644 --- a/server/routes/settings/notification.ts +++ b/server/routes/settings/notification.ts @@ -1,11 +1,11 @@ import type { User } from '@server/entity/User'; import type { NotificationSettingsResultResponse } from '@server/interfaces/api/settingsInterfaces'; +import type { NotificationAgentConfig } from '@server/interfaces/settings'; import notificationManager, { createAccordingNotificationAgent, Notification, } from '@server/lib/notifications'; import type { NotificationAgent } from '@server/lib/notifications/agents/agent'; -import type { NotificationAgentConfig } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import { Router } from 'express'; diff --git a/server/routes/settings/radarr.ts b/server/routes/settings/radarr.ts index efa5866582..8755330115 100644 --- a/server/routes/settings/radarr.ts +++ b/server/routes/settings/radarr.ts @@ -1,5 +1,5 @@ import RadarrAPI from '@server/api/servarr/radarr'; -import type { RadarrSettings } from '@server/lib/settings'; +import type { RadarrSettings } from '@server/interfaces/settings'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { Router } from 'express'; diff --git a/server/routes/settings/sonarr.ts b/server/routes/settings/sonarr.ts index 84bf4d7932..8ba9d2596d 100644 --- a/server/routes/settings/sonarr.ts +++ b/server/routes/settings/sonarr.ts @@ -1,5 +1,5 @@ import SonarrAPI from '@server/api/servarr/sonarr'; -import type { SonarrSettings } from '@server/lib/settings'; +import type { SonarrSettings } from '@server/interfaces/settings'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { Router } from 'express'; diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index f706c71bea..748d531c9e 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -10,14 +10,15 @@ import type { UserSettingsGeneralResponse, UserSettingsNotificationsResponse, } from '@server/interfaces/api/userSettingsInterfaces'; +import { + NotificationAgentKey, + type NotificationAgentDiscord, + type NotificationAgentEmail, + type NotificationAgentTelegram, +} from '@server/interfaces/settings'; import { retrieveDefaultNotificationInstanceSettings } from '@server/lib/notifications'; import { Permission } from '@server/lib/permissions'; -import type { - NotificationAgentDiscord, - NotificationAgentEmail, - NotificationAgentTelegram, -} from '@server/lib/settings'; -import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; import { ApiError } from '@server/types/error'; diff --git a/server/utils/customProxyAgent.ts b/server/utils/customProxyAgent.ts index 60f8be6e11..916c0710b9 100644 --- a/server/utils/customProxyAgent.ts +++ b/server/utils/customProxyAgent.ts @@ -1,4 +1,4 @@ -import type { ProxySettings } from '@server/lib/settings'; +import type { ProxySettings } from '@server/interfaces/settings'; import logger from '@server/logger'; import axios, { type InternalAxiosRequestConfig } from 'axios'; import { HttpProxyAgent } from 'http-proxy-agent'; diff --git a/server/utils/restartFlag.ts b/server/utils/restartFlag.ts index d0a492ba37..7bcbb512bf 100644 --- a/server/utils/restartFlag.ts +++ b/server/utils/restartFlag.ts @@ -1,4 +1,4 @@ -import type { AllSettings, NetworkSettings } from '@server/lib/settings'; +import type { AllSettings, NetworkSettings } from '@server/interfaces/settings'; import { getSettings } from '@server/lib/settings'; class RestartFlag { diff --git a/src/components/LanguageSelector/index.tsx b/src/components/LanguageSelector/index.tsx index 083ecbc75a..d789207da6 100644 --- a/src/components/LanguageSelector/index.tsx +++ b/src/components/LanguageSelector/index.tsx @@ -1,6 +1,6 @@ import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; -import type { Language } from '@server/lib/settings'; +import type { Language } from '@server/interfaces/settings'; import { sortBy } from 'lodash'; import { useMemo } from 'react'; import { useIntl } from 'react-intl'; diff --git a/src/components/ManageSlideOver/index.tsx b/src/components/ManageSlideOver/index.tsx index c1b115d415..5059c1f6bf 100644 --- a/src/components/ManageSlideOver/index.tsx +++ b/src/components/ManageSlideOver/index.tsx @@ -25,7 +25,10 @@ import { } from '@server/constants/media'; import { MediaServerType } from '@server/constants/server'; import type { MediaWatchDataResponse } from '@server/interfaces/api/mediaInterfaces'; -import type { RadarrSettings, SonarrSettings } from '@server/lib/settings'; +import type { + RadarrSettings, + SonarrSettings, +} from '@server/interfaces/settings'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; import axios from 'axios'; diff --git a/src/components/RegionSelector/index.tsx b/src/components/RegionSelector/index.tsx index 989c62efe0..febae6b3d7 100644 --- a/src/components/RegionSelector/index.tsx +++ b/src/components/RegionSelector/index.tsx @@ -2,7 +2,7 @@ import useSettings from '@app/hooks/useSettings'; import defineMessages from '@app/utils/defineMessages'; import { Listbox, Transition } from '@headlessui/react'; import { CheckIcon, ChevronDownIcon } from '@heroicons/react/24/solid'; -import type { Region } from '@server/lib/settings'; +import type { Region } from '@server/interfaces/settings'; import { countries } from 'country-flag-icons'; import 'country-flag-icons/3x2/flags.css'; import { sortBy } from 'lodash'; diff --git a/src/components/Selector/CertificationSelector.tsx b/src/components/Selector/CertificationSelector.tsx index 671c97e5dc..f1cafcda45 100644 --- a/src/components/Selector/CertificationSelector.tsx +++ b/src/components/Selector/CertificationSelector.tsx @@ -1,6 +1,6 @@ import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner'; import defineMessages from '@app/utils/defineMessages'; -import type { Region } from '@server/lib/settings'; +import type { Region } from '@server/interfaces/settings'; import React, { useCallback, useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; import AsyncSelect from 'react-select/async'; diff --git a/src/components/Settings/Notifications/NotificationsNtfy.tsx b/src/components/Settings/Notifications/NotificationsNtfy.tsx index 85fa943b99..200c79731a 100644 --- a/src/components/Settings/Notifications/NotificationsNtfy.tsx +++ b/src/components/Settings/Notifications/NotificationsNtfy.tsx @@ -6,7 +6,7 @@ import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { isValidURL } from '@app/utils/urlValidationHelper'; import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline'; -import type { NotificationAgentNtfy } from '@server/lib/settings'; +import type { NotificationAgentNtfy } from '@server/interfaces/settings'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; import { useState } from 'react'; diff --git a/src/components/Settings/OverrideRule/OverrideRuleModal.tsx b/src/components/Settings/OverrideRule/OverrideRuleModal.tsx index f5282323eb..702854960d 100644 --- a/src/components/Settings/OverrideRule/OverrideRuleModal.tsx +++ b/src/components/Settings/OverrideRule/OverrideRuleModal.tsx @@ -15,7 +15,7 @@ import type { DVRSettings, RadarrSettings, SonarrSettings, -} from '@server/lib/settings'; +} from '@server/interfaces/settings'; import axios from 'axios'; import { Field, Formik } from 'formik'; import { useCallback, useEffect, useState } from 'react'; diff --git a/src/components/Settings/OverrideRule/OverrideRuleTiles.tsx b/src/components/Settings/OverrideRule/OverrideRuleTiles.tsx index 3b08a1b274..aa4500ad5d 100644 --- a/src/components/Settings/OverrideRule/OverrideRuleTiles.tsx +++ b/src/components/Settings/OverrideRule/OverrideRuleTiles.tsx @@ -10,7 +10,7 @@ import type { Language, RadarrSettings, SonarrSettings, -} from '@server/lib/settings'; +} from '@server/interfaces/settings'; import type { Keyword } from '@server/models/common'; import axios from 'axios'; import { useCallback, useEffect, useState } from 'react'; diff --git a/src/components/Settings/RadarrModal/index.tsx b/src/components/Settings/RadarrModal/index.tsx index 6921d1b5e3..b1f9d6ff40 100644 --- a/src/components/Settings/RadarrModal/index.tsx +++ b/src/components/Settings/RadarrModal/index.tsx @@ -5,7 +5,7 @@ import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { isValidURL } from '@app/utils/urlValidationHelper'; import { Transition } from '@headlessui/react'; -import type { RadarrSettings } from '@server/lib/settings'; +import type { RadarrSettings } from '@server/interfaces/settings'; import axios from 'axios'; import { Field, Formik } from 'formik'; import { useCallback, useEffect, useRef, useState } from 'react'; diff --git a/src/components/Settings/SettingsJellyfin.tsx b/src/components/Settings/SettingsJellyfin.tsx index 324a0dce79..33e9f1e1f8 100644 --- a/src/components/Settings/SettingsJellyfin.tsx +++ b/src/components/Settings/SettingsJellyfin.tsx @@ -10,7 +10,7 @@ import { isValidURL } from '@app/utils/urlValidationHelper'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ApiErrorCode } from '@server/constants/error'; import { MediaServerType } from '@server/constants/server'; -import type { JellyfinSettings } from '@server/lib/settings'; +import type { JellyfinSettings } from '@server/interfaces/settings'; import axios from 'axios'; import { Field, Formik } from 'formik'; import { useState } from 'react'; diff --git a/src/components/Settings/SettingsJobsCache/index.tsx b/src/components/Settings/SettingsJobsCache/index.tsx index 4b4c98f445..e65d731f33 100644 --- a/src/components/Settings/SettingsJobsCache/index.tsx +++ b/src/components/Settings/SettingsJobsCache/index.tsx @@ -19,7 +19,7 @@ import type { CacheItem, CacheResponse, } from '@server/interfaces/api/settingsInterfaces'; -import type { JobId } from '@server/lib/settings'; +import type { JobId } from '@server/interfaces/settings'; import axios from 'axios'; import cronstrue from 'cronstrue/i18n'; import { formatDuration, intervalToDuration } from 'date-fns'; diff --git a/src/components/Settings/SettingsMain/index.tsx b/src/components/Settings/SettingsMain/index.tsx index 1d3dd68bdb..1562392d35 100644 --- a/src/components/Settings/SettingsMain/index.tsx +++ b/src/components/Settings/SettingsMain/index.tsx @@ -17,7 +17,7 @@ import { isValidURL } from '@app/utils/urlValidationHelper'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ArrowPathIcon } from '@heroicons/react/24/solid'; import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces'; -import type { MainSettings } from '@server/lib/settings'; +import type { MainSettings } from '@server/interfaces/settings'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; import { useIntl } from 'react-intl'; diff --git a/src/components/Settings/SettingsNetwork/index.tsx b/src/components/Settings/SettingsNetwork/index.tsx index f0ac1f13a2..7fe278aa1a 100644 --- a/src/components/Settings/SettingsNetwork/index.tsx +++ b/src/components/Settings/SettingsNetwork/index.tsx @@ -6,7 +6,7 @@ import SettingsBadge from '@app/components/Settings/SettingsBadge'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; -import type { NetworkSettings } from '@server/lib/settings'; +import type { NetworkSettings } from '@server/interfaces/settings'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; import { useIntl } from 'react-intl'; diff --git a/src/components/Settings/SettingsPlex.tsx b/src/components/Settings/SettingsPlex.tsx index cac73fb94a..fe4388b7ab 100644 --- a/src/components/Settings/SettingsPlex.tsx +++ b/src/components/Settings/SettingsPlex.tsx @@ -16,7 +16,10 @@ import { XMarkIcon, } from '@heroicons/react/24/solid'; import type { PlexDevice } from '@server/interfaces/api/plexInterfaces'; -import type { PlexSettings, TautulliSettings } from '@server/lib/settings'; +import type { + PlexSettings, + TautulliSettings, +} from '@server/interfaces/settings'; import axios from 'axios'; import { Field, Formik } from 'formik'; import { orderBy } from 'lodash'; diff --git a/src/components/Settings/SettingsServices.tsx b/src/components/Settings/SettingsServices.tsx index be39f744da..e7bc6cfc0a 100644 --- a/src/components/Settings/SettingsServices.tsx +++ b/src/components/Settings/SettingsServices.tsx @@ -16,7 +16,10 @@ import { Transition } from '@headlessui/react'; import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/solid'; import type OverrideRule from '@server/entity/OverrideRule'; import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces'; -import type { RadarrSettings, SonarrSettings } from '@server/lib/settings'; +import type { + RadarrSettings, + SonarrSettings, +} from '@server/interfaces/settings'; import axios from 'axios'; import { Fragment, useState } from 'react'; import { useIntl } from 'react-intl'; diff --git a/src/components/Settings/SettingsUsers/index.tsx b/src/components/Settings/SettingsUsers/index.tsx index 7dad9f940b..0dba9001a2 100644 --- a/src/components/Settings/SettingsUsers/index.tsx +++ b/src/components/Settings/SettingsUsers/index.tsx @@ -9,7 +9,7 @@ import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { MediaServerType } from '@server/constants/server'; -import type { MainSettings } from '@server/lib/settings'; +import type { MainSettings } from '@server/interfaces/settings'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; import { useIntl } from 'react-intl'; diff --git a/src/components/Settings/SonarrModal/index.tsx b/src/components/Settings/SonarrModal/index.tsx index c497bb6a34..346329eb2a 100644 --- a/src/components/Settings/SonarrModal/index.tsx +++ b/src/components/Settings/SonarrModal/index.tsx @@ -5,7 +5,7 @@ import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { isValidURL } from '@app/utils/urlValidationHelper'; import { Transition } from '@headlessui/react'; -import type { SonarrSettings } from '@server/lib/settings'; +import type { SonarrSettings } from '@server/interfaces/settings'; import axios from 'axios'; import { Field, Formik } from 'formik'; import { useCallback, useEffect, useRef, useState } from 'react'; diff --git a/src/components/Setup/index.tsx b/src/components/Setup/index.tsx index 4d6ea4cefd..86b71fb5f2 100644 --- a/src/components/Setup/index.tsx +++ b/src/components/Setup/index.tsx @@ -14,7 +14,7 @@ import useLocale from '@app/hooks/useLocale'; import useSettings from '@app/hooks/useSettings'; import defineMessages from '@app/utils/defineMessages'; import { MediaServerType } from '@server/constants/server'; -import type { Library } from '@server/lib/settings'; +import type { Library } from '@server/interfaces/settings'; import axios from 'axios'; import Image from 'next/image'; import { useRouter } from 'next/router'; diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts index 2a14ad1d56..19fdd0acd5 100644 --- a/src/hooks/useUser.ts +++ b/src/hooks/useUser.ts @@ -1,7 +1,7 @@ import { UserType } from '@server/constants/user'; +import type { NotificationAgentKey } from '@server/interfaces/settings'; import type { PermissionCheckOptions } from '@server/lib/permissions'; import { hasPermission, Permission } from '@server/lib/permissions'; -import type { NotificationAgentKey } from '@server/lib/settings'; import type { MutatorCallback } from 'swr'; import useSWR from 'swr'; From 86fde94c7831b70bcfffb80fbaf6cd10d9994937 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Sun, 11 May 2025 17:02:36 +0200 Subject: [PATCH 29/62] feat(notifications): implement notification instance editing --- .../Notifications/NotificationsDiscord.tsx | 349 ------------ .../Notifications/NotificationsEmail.tsx | 520 ------------------ .../Notifications/NotificationsGotify.tsx | 305 ---------- .../Notifications/NotificationsPushbullet.tsx | 264 --------- .../Notifications/NotificationsPushover.tsx | 331 ----------- .../Notifications/NotificationsSlack.tsx | 249 --------- .../Notifications/NotificationsTelegram.tsx | 399 -------------- .../Notifications/NotificationsWebPush.tsx | 174 ------ .../Notifications/NotificationsWebhook.tsx | 397 ------------- .../NotificationModal/DiscordModal.tsx | 294 ++++++++++ .../NotificationModal/EmailModal.tsx | 476 ++++++++++++++++ .../NotificationModal/GotifyModal.tsx | 246 +++++++++ .../NotificationModal/LunaSeaModal.tsx | 211 +++++++ .../NotificationModal/NtfyModal.tsx | 307 +++++++++++ .../NotificationModal/PushbulletModal.tsx | 203 +++++++ .../NotificationModal/PushoverModal.tsx | 277 ++++++++++ .../NotificationModal/SlackModal.tsx | 188 +++++++ .../NotificationModal/TelegramModal.tsx | 361 ++++++++++++ .../NotificationModal/WebPushModal.tsx | 117 ++++ .../NotificationModal/WebhookModal.tsx | 346 ++++++++++++ .../NotificationModal/index.tsx | 230 ++++++++ .../index.tsx} | 49 +- 22 files changed, 3296 insertions(+), 2997 deletions(-) delete mode 100644 src/components/Settings/Notifications/NotificationsDiscord.tsx delete mode 100644 src/components/Settings/Notifications/NotificationsEmail.tsx delete mode 100644 src/components/Settings/Notifications/NotificationsGotify.tsx delete mode 100644 src/components/Settings/Notifications/NotificationsPushbullet.tsx delete mode 100644 src/components/Settings/Notifications/NotificationsPushover.tsx delete mode 100644 src/components/Settings/Notifications/NotificationsSlack.tsx delete mode 100644 src/components/Settings/Notifications/NotificationsTelegram.tsx delete mode 100644 src/components/Settings/Notifications/NotificationsWebPush.tsx delete mode 100644 src/components/Settings/Notifications/NotificationsWebhook.tsx create mode 100644 src/components/Settings/SettingsNotifications/NotificationModal/DiscordModal.tsx create mode 100644 src/components/Settings/SettingsNotifications/NotificationModal/EmailModal.tsx create mode 100644 src/components/Settings/SettingsNotifications/NotificationModal/GotifyModal.tsx create mode 100644 src/components/Settings/SettingsNotifications/NotificationModal/LunaSeaModal.tsx create mode 100644 src/components/Settings/SettingsNotifications/NotificationModal/NtfyModal.tsx create mode 100644 src/components/Settings/SettingsNotifications/NotificationModal/PushbulletModal.tsx create mode 100644 src/components/Settings/SettingsNotifications/NotificationModal/PushoverModal.tsx create mode 100644 src/components/Settings/SettingsNotifications/NotificationModal/SlackModal.tsx create mode 100644 src/components/Settings/SettingsNotifications/NotificationModal/TelegramModal.tsx create mode 100644 src/components/Settings/SettingsNotifications/NotificationModal/WebPushModal.tsx create mode 100644 src/components/Settings/SettingsNotifications/NotificationModal/WebhookModal.tsx create mode 100644 src/components/Settings/SettingsNotifications/NotificationModal/index.tsx rename src/components/Settings/{SettingsNotifications.tsx => SettingsNotifications/index.tsx} (89%) diff --git a/src/components/Settings/Notifications/NotificationsDiscord.tsx b/src/components/Settings/Notifications/NotificationsDiscord.tsx deleted file mode 100644 index 6abea6a2b8..0000000000 --- a/src/components/Settings/Notifications/NotificationsDiscord.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import Button from '@app/components/Common/Button'; -import LoadingSpinner from '@app/components/Common/LoadingSpinner'; -import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; -import useSettings from '@app/hooks/useSettings'; -import globalMessages from '@app/i18n/globalMessages'; -import defineMessages from '@app/utils/defineMessages'; -import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline'; -import axios from 'axios'; -import { Field, Form, Formik } from 'formik'; -import { useState } from 'react'; -import { useIntl } from 'react-intl'; -import { useToasts } from 'react-toast-notifications'; -import useSWR from 'swr'; -import * as Yup from 'yup'; - -const messages = defineMessages('components.Settings.Notifications', { - agentenabled: 'Enable Agent', - botUsername: 'Bot Username', - botAvatarUrl: 'Bot Avatar URL', - webhookUrl: 'Webhook URL', - webhookUrlTip: - 'Create a webhook integration in your server', - webhookRoleId: 'Notification Role ID', - webhookRoleIdTip: - 'The role ID to mention in the webhook message. Leave empty to disable mentions', - discordsettingssaved: 'Discord notification settings saved successfully!', - discordsettingsfailed: 'Discord notification settings failed to save.', - toastDiscordTestSending: 'Sending Discord test notification…', - toastDiscordTestSuccess: 'Discord test notification sent!', - toastDiscordTestFailed: 'Discord test notification failed to send.', - validationUrl: 'You must provide a valid URL', - validationWebhookRoleId: 'You must provide a valid Discord Role ID', - validationTypes: 'You must select at least one notification type', - enableMentions: 'Enable Mentions', -}); - -const NotificationsDiscord = () => { - const intl = useIntl(); - const settings = useSettings(); - const { addToast, removeToast } = useToasts(); - const [isTesting, setIsTesting] = useState(false); - const { - data, - error, - mutate: revalidate, - } = useSWR('/api/v1/settings/notifications/discord'); - - const NotificationsDiscordSchema = Yup.object().shape({ - botAvatarUrl: Yup.string() - .nullable() - .url(intl.formatMessage(messages.validationUrl)), - webhookUrl: Yup.string() - .when('enabled', { - is: true, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationUrl)), - otherwise: Yup.string().nullable(), - }) - .url(intl.formatMessage(messages.validationUrl)), - webhookRoleId: Yup.string() - .nullable() - .matches( - /^\d{17,19}$/, - intl.formatMessage(messages.validationWebhookRoleId) - ), - }); - - if (!data && !error) { - return ; - } - - return ( - { - try { - await axios.post('/api/v1/settings/notifications/discord', { - enabled: values.enabled, - types: values.types, - options: { - botUsername: values.botUsername, - botAvatarUrl: values.botAvatarUrl, - webhookUrl: values.webhookUrl, - webhookRoleId: values.webhookRoleId, - enableMentions: values.enableMentions, - }, - }); - - addToast(intl.formatMessage(messages.discordsettingssaved), { - appearance: 'success', - autoDismiss: true, - }); - } catch (e) { - addToast(intl.formatMessage(messages.discordsettingsfailed), { - appearance: 'error', - autoDismiss: true, - }); - } finally { - revalidate(); - } - }} - > - {({ - errors, - touched, - isSubmitting, - values, - isValid, - setFieldValue, - setFieldTouched, - }) => { - const testSettings = async () => { - setIsTesting(true); - let toastId: string | undefined; - try { - addToast( - intl.formatMessage(messages.toastDiscordTestSending), - { - autoDismiss: false, - appearance: 'info', - }, - (id) => { - toastId = id; - } - ); - await axios.post('/api/v1/settings/notifications/discord/test', { - enabled: true, - types: values.types, - options: { - botUsername: values.botUsername, - botAvatarUrl: values.botAvatarUrl, - webhookUrl: values.webhookUrl, - webhookRoleId: values.webhookRoleId, - enableMentions: values.enableMentions, - }, - }); - - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastDiscordTestSuccess), { - autoDismiss: true, - appearance: 'success', - }); - } catch (e) { - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastDiscordTestFailed), { - autoDismiss: true, - appearance: 'error', - }); - } finally { - setIsTesting(false); - } - }; - - return ( -
-
- -
- -
-
-
- -
-
- -
- {errors.webhookUrl && - touched.webhookUrl && - typeof errors.webhookUrl === 'string' && ( -
{errors.webhookUrl}
- )} -
-
-
- -
-
- -
- {errors.botUsername && - touched.botUsername && - typeof errors.botUsername === 'string' && ( -
{errors.botUsername}
- )} -
-
-
- -
-
- -
- {errors.botAvatarUrl && - touched.botAvatarUrl && - typeof errors.botAvatarUrl === 'string' && ( -
{errors.botAvatarUrl}
- )} -
-
-
- -
-
- -
- {errors.webhookRoleId && - touched.webhookRoleId && - typeof errors.webhookRoleId === 'string' && ( -
{errors.webhookRoleId}
- )} -
-
-
- -
- -
-
- { - setFieldValue('types', newTypes); - setFieldTouched('types'); - - if (newTypes) { - setFieldValue('enabled', true); - } - }} - error={ - values.enabled && !values.types && touched.types - ? intl.formatMessage(messages.validationTypes) - : undefined - } - /> -
-
- - - - - - -
-
- - ); - }} -
- ); -}; - -export default NotificationsDiscord; diff --git a/src/components/Settings/Notifications/NotificationsEmail.tsx b/src/components/Settings/Notifications/NotificationsEmail.tsx deleted file mode 100644 index 55843fa69a..0000000000 --- a/src/components/Settings/Notifications/NotificationsEmail.tsx +++ /dev/null @@ -1,520 +0,0 @@ -import Button from '@app/components/Common/Button'; -import LoadingSpinner from '@app/components/Common/LoadingSpinner'; -import SensitiveInput from '@app/components/Common/SensitiveInput'; -import SettingsBadge from '@app/components/Settings/SettingsBadge'; -import globalMessages from '@app/i18n/globalMessages'; -import defineMessages from '@app/utils/defineMessages'; -import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline'; -import axios from 'axios'; -import { Field, Form, Formik } from 'formik'; -import { useState } from 'react'; -import { useIntl } from 'react-intl'; -import { useToasts } from 'react-toast-notifications'; -import useSWR, { mutate } from 'swr'; -import * as Yup from 'yup'; - -const messages = defineMessages('components.Settings.Notifications', { - validationSmtpHostRequired: 'You must provide a valid hostname or IP address', - validationSmtpPortRequired: 'You must provide a valid port number', - agentenabled: 'Enable Agent', - userEmailRequired: 'Require user email', - emailsender: 'Sender Address', - smtpHost: 'SMTP Host', - smtpPort: 'SMTP Port', - encryption: 'Encryption Method', - encryptionTip: - 'In most cases, Implicit TLS uses port 465 and STARTTLS uses port 587', - encryptionNone: 'None', - encryptionDefault: 'Use STARTTLS if available', - encryptionOpportunisticTls: 'Always use STARTTLS', - encryptionImplicitTls: 'Use Implicit TLS', - authUser: 'SMTP Username', - authPass: 'SMTP Password', - emailsettingssaved: 'Email notification settings saved successfully!', - emailsettingsfailed: 'Email notification settings failed to save.', - toastEmailTestSending: 'Sending email test notification…', - toastEmailTestSuccess: 'Email test notification sent!', - toastEmailTestFailed: 'Email test notification failed to send.', - allowselfsigned: 'Allow Self-Signed Certificates', - senderName: 'Sender Name', - validationEmail: 'You must provide a valid email address', - pgpPrivateKey: 'PGP Private Key', - pgpPrivateKeyTip: - 'Sign encrypted email messages using OpenPGP', - 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', -}); - -export function OpenPgpLink(msg: React.ReactNode) { - return ( - - {msg} - - ); -} - -const NotificationsEmail = () => { - const intl = useIntl(); - const { addToast, removeToast } = useToasts(); - const [isTesting, setIsTesting] = useState(false); - const { - data, - error, - mutate: revalidate, - } = useSWR('/api/v1/settings/notifications/email'); - - const NotificationsEmailSchema = Yup.object().shape( - { - emailFrom: Yup.string() - .when('enabled', { - is: true, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationEmail)), - otherwise: Yup.string().nullable(), - }) - .email(intl.formatMessage(messages.validationEmail)), - smtpHost: Yup.string().when('enabled', { - is: true, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationSmtpHostRequired)), - otherwise: Yup.string().nullable(), - }), - smtpPort: Yup.number().when('enabled', { - is: true, - then: Yup.number() - .nullable() - .required(intl.formatMessage(messages.validationSmtpPortRequired)), - otherwise: Yup.number().nullable(), - }), - pgpPrivateKey: Yup.string() - .when('pgpPassword', { - is: (value: unknown) => !!value, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationPgpPrivateKey)), - otherwise: Yup.string().nullable(), - }) - .matches( - /-----BEGIN PGP PRIVATE KEY BLOCK-----.+-----END PGP PRIVATE KEY BLOCK-----/, - intl.formatMessage(messages.validationPgpPrivateKey) - ), - pgpPassword: Yup.string().when('pgpPrivateKey', { - is: (value: unknown) => !!value, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationPgpPassword)), - otherwise: Yup.string().nullable(), - }), - }, - [['pgpPrivateKey', 'pgpPassword']] - ); - - if (!data && !error) { - return ; - } - - return ( - { - try { - await axios.post('/api/v1/settings/notifications/email', { - enabled: values.enabled, - options: { - userEmailRequired: values.userEmailRequired, - emailFrom: values.emailFrom, - smtpHost: values.smtpHost, - smtpPort: Number(values.smtpPort), - secure: values.encryption === 'implicit', - ignoreTls: values.encryption === 'none', - requireTls: values.encryption === 'opportunistic', - authUser: values.authUser, - authPass: values.authPass, - allowSelfSigned: values.allowSelfSigned, - senderName: values.senderName, - pgpPrivateKey: values.pgpPrivateKey, - pgpPassword: values.pgpPassword, - }, - }); - mutate('/api/v1/settings/public'); - - addToast(intl.formatMessage(messages.emailsettingssaved), { - appearance: 'success', - autoDismiss: true, - }); - } catch (e) { - addToast(intl.formatMessage(messages.emailsettingsfailed), { - appearance: 'error', - autoDismiss: true, - }); - } finally { - revalidate(); - } - }} - > - {({ errors, touched, isSubmitting, values, isValid }) => { - const testSettings = async () => { - setIsTesting(true); - let toastId: string | undefined; - try { - addToast( - intl.formatMessage(messages.toastEmailTestSending), - { - autoDismiss: false, - appearance: 'info', - }, - (id) => { - toastId = id; - } - ); - await axios.post('/api/v1/settings/notifications/email/test', { - enabled: true, - options: { - emailFrom: values.emailFrom, - smtpHost: values.smtpHost, - smtpPort: Number(values.smtpPort), - secure: values.encryption === 'implicit', - ignoreTls: values.encryption === 'none', - requireTls: values.encryption === 'opportunistic', - authUser: values.authUser, - authPass: values.authPass, - allowSelfSigned: values.allowSelfSigned, - senderName: values.senderName, - pgpPrivateKey: values.pgpPrivateKey, - pgpPassword: values.pgpPassword, - }, - }); - - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastEmailTestSuccess), { - autoDismiss: true, - appearance: 'success', - }); - } catch (e) { - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastEmailTestFailed), { - autoDismiss: true, - appearance: 'error', - }); - } finally { - setIsTesting(false); - } - }; - - return ( -
-
- -
- -
-
-
- -
- -
-
-
- -
-
- -
-
-
-
- -
-
- -
- {errors.emailFrom && - touched.emailFrom && - typeof errors.emailFrom === 'string' && ( -
{errors.emailFrom}
- )} -
-
-
- -
-
- -
- {errors.smtpHost && - touched.smtpHost && - typeof errors.smtpHost === 'string' && ( -
{errors.smtpHost}
- )} -
-
-
- -
- - {errors.smtpPort && - touched.smtpPort && - typeof errors.smtpPort === 'string' && ( -
{errors.smtpPort}
- )} -
-
-
- -
-
- - - - - - -
-
-
-
- -
- -
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- -
-
- -
- {errors.pgpPrivateKey && - touched.pgpPrivateKey && - typeof errors.pgpPrivateKey === 'string' && ( -
{errors.pgpPrivateKey}
- )} -
-
-
- -
-
- -
- {errors.pgpPassword && - touched.pgpPassword && - typeof errors.pgpPassword === 'string' && ( -
{errors.pgpPassword}
- )} -
-
-
-
- - - - - - -
-
-
- ); - }} -
- ); -}; - -export default NotificationsEmail; diff --git a/src/components/Settings/Notifications/NotificationsGotify.tsx b/src/components/Settings/Notifications/NotificationsGotify.tsx deleted file mode 100644 index 461103511c..0000000000 --- a/src/components/Settings/Notifications/NotificationsGotify.tsx +++ /dev/null @@ -1,305 +0,0 @@ -import Button from '@app/components/Common/Button'; -import LoadingSpinner from '@app/components/Common/LoadingSpinner'; -import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; -import globalMessages from '@app/i18n/globalMessages'; -import defineMessages from '@app/utils/defineMessages'; -import { isValidURL } from '@app/utils/urlValidationHelper'; -import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/solid'; -import axios from 'axios'; -import { Field, Form, Formik } from 'formik'; -import { useState } from 'react'; -import { useIntl } from 'react-intl'; -import { useToasts } from 'react-toast-notifications'; -import useSWR from 'swr'; -import * as Yup from 'yup'; - -const messages = defineMessages( - 'components.Settings.Notifications.NotificationsGotify', - { - agentenabled: 'Enable Agent', - url: 'Server URL', - token: 'Application Token', - priority: 'Priority', - validationUrlRequired: 'You must provide a valid URL', - validationUrlTrailingSlash: 'URL must not end in a trailing slash', - validationTokenRequired: 'You must provide an application token', - validationPriorityRequired: 'You must set a priority number', - gotifysettingssaved: 'Gotify notification settings saved successfully!', - gotifysettingsfailed: 'Gotify notification settings failed to save.', - toastGotifyTestSending: 'Sending Gotify test notification…', - toastGotifyTestSuccess: 'Gotify test notification sent!', - toastGotifyTestFailed: 'Gotify test notification failed to send.', - validationTypes: 'You must select at least one notification type', - } -); - -const NotificationsGotify = () => { - const intl = useIntl(); - const { addToast, removeToast } = useToasts(); - const [isTesting, setIsTesting] = useState(false); - const { - data, - error, - mutate: revalidate, - } = useSWR('/api/v1/settings/notifications/gotify'); - - const NotificationsGotifySchema = Yup.object().shape({ - url: Yup.string() - .when('enabled', { - is: true, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationUrlRequired)), - otherwise: Yup.string().nullable(), - }) - .test( - 'valid-url', - intl.formatMessage(messages.validationUrlRequired), - isValidURL - ) - .test( - 'no-trailing-slash', - intl.formatMessage(messages.validationUrlTrailingSlash), - (value) => !value || !value.endsWith('/') - ), - token: Yup.string().when('enabled', { - is: true, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationTokenRequired)), - otherwise: Yup.string().nullable(), - }), - priority: Yup.string().when('enabled', { - is: true, - then: Yup.string() - .nullable() - .min(0) - .max(9) - .required(intl.formatMessage(messages.validationPriorityRequired)), - otherwise: Yup.string().nullable(), - }), - }); - - if (!data && !error) { - return ; - } - - return ( - { - try { - await axios.post('/api/v1/settings/notifications/gotify', { - enabled: values.enabled, - types: values.types, - options: { - url: values.url, - token: values.token, - priority: Number(values.priority), - }, - }); - addToast(intl.formatMessage(messages.gotifysettingssaved), { - appearance: 'success', - autoDismiss: true, - }); - } catch (e) { - addToast(intl.formatMessage(messages.gotifysettingsfailed), { - appearance: 'error', - autoDismiss: true, - }); - } finally { - revalidate(); - } - }} - > - {({ - errors, - touched, - isSubmitting, - values, - isValid, - setFieldValue, - setFieldTouched, - }) => { - const testSettings = async () => { - setIsTesting(true); - let toastId: string | undefined; - try { - addToast( - intl.formatMessage(messages.toastGotifyTestSending), - { - autoDsmiss: false, - appearance: 'info', - }, - (id) => { - toastId = id; - } - ); - await axios.post('/api/v1/settings/notifications/gotify/test', { - enabled: true, - types: values.types, - options: { - url: values.url, - token: values.token, - priority: Number(values.priority), - }, - }); - - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastGotifyTestSuccess), { - autoDismiss: true, - appearance: 'success', - }); - } catch (e) { - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastGotifyTestFailed), { - autoDismiss: true, - appearance: 'error', - }); - } finally { - setIsTesting(false); - } - }; - - return ( -
-
- -
- -
-
-
- -
-
- -
- {errors.url && - touched.url && - typeof errors.url === 'string' && ( -
{errors.url}
- )} -
-
-
- -
-
- -
- {errors.token && - touched.token && - typeof errors.token === 'string' && ( -
{errors.token}
- )} -
-
-
- -
- - {errors.priority && - touched.priority && - typeof errors.priority === 'string' && ( -
{errors.priority}
- )} -
-
- { - setFieldValue('types', newTypes); - setFieldTouched('types'); - - if (newTypes) { - setFieldValue('enabled', true); - } - }} - error={ - values.enabled && !values.types && touched.types - ? intl.formatMessage(messages.validationTypes) - : undefined - } - /> -
-
- - - - - - -
-
- - ); - }} -
- ); -}; - -export default NotificationsGotify; diff --git a/src/components/Settings/Notifications/NotificationsPushbullet.tsx b/src/components/Settings/Notifications/NotificationsPushbullet.tsx deleted file mode 100644 index 696142d2e1..0000000000 --- a/src/components/Settings/Notifications/NotificationsPushbullet.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import Button from '@app/components/Common/Button'; -import LoadingSpinner from '@app/components/Common/LoadingSpinner'; -import SensitiveInput from '@app/components/Common/SensitiveInput'; -import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; -import globalMessages from '@app/i18n/globalMessages'; -import defineMessages from '@app/utils/defineMessages'; -import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline'; -import axios from 'axios'; -import { Field, Form, Formik } from 'formik'; -import { useState } from 'react'; -import { useIntl } from 'react-intl'; -import { useToasts } from 'react-toast-notifications'; -import useSWR from 'swr'; -import * as Yup from 'yup'; - -const messages = defineMessages( - 'components.Settings.Notifications.NotificationsPushbullet', - { - agentEnabled: 'Enable Agent', - accessToken: 'Access Token', - accessTokenTip: - 'Create a token from your Account Settings', - validationAccessTokenRequired: 'You must provide an access token', - channelTag: 'Channel Tag', - pushbulletSettingsSaved: - 'Pushbullet notification settings saved successfully!', - pushbulletSettingsFailed: - 'Pushbullet notification settings failed to save.', - 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 = () => { - const intl = useIntl(); - const { addToast, removeToast } = useToasts(); - const [isTesting, setIsTesting] = useState(false); - const { - data, - error, - mutate: revalidate, - } = useSWR('/api/v1/settings/notifications/pushbullet'); - - const NotificationsPushbulletSchema = Yup.object().shape({ - accessToken: Yup.string().when('enabled', { - is: true, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationAccessTokenRequired)), - otherwise: Yup.string().nullable(), - }), - }); - - if (!data && !error) { - return ; - } - - return ( - { - try { - await axios.post('/api/v1/settings/notifications/pushbullet', { - enabled: values.enabled, - types: values.types, - options: { - accessToken: values.accessToken, - channelTag: values.channelTag, - }, - }); - addToast(intl.formatMessage(messages.pushbulletSettingsSaved), { - appearance: 'success', - autoDismiss: true, - }); - } catch (e) { - addToast(intl.formatMessage(messages.pushbulletSettingsFailed), { - appearance: 'error', - autoDismiss: true, - }); - } finally { - revalidate(); - } - }} - > - {({ - errors, - touched, - isSubmitting, - values, - isValid, - setFieldValue, - setFieldTouched, - }) => { - const testSettings = async () => { - setIsTesting(true); - let toastId: string | undefined; - try { - addToast( - intl.formatMessage(messages.toastPushbulletTestSending), - { - autoDismiss: false, - appearance: 'info', - }, - (id) => { - toastId = id; - } - ); - await axios.post('/api/v1/settings/notifications/pushbullet/test', { - enabled: true, - types: values.types, - options: { - accessToken: values.accessToken, - channelTag: values.channelTag, - }, - }); - - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastPushbulletTestSuccess), { - autoDismiss: true, - appearance: 'success', - }); - } catch (e) { - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastPushbulletTestFailed), { - autoDismiss: true, - appearance: 'error', - }); - } finally { - setIsTesting(false); - } - }; - - return ( -
-
- -
- -
-
-
- -
-
- -
- {errors.accessToken && - touched.accessToken && - typeof errors.accessToken === 'string' && ( -
{errors.accessToken}
- )} -
-
-
- -
-
- -
-
-
- { - setFieldValue('types', newTypes); - setFieldTouched('types'); - - if (newTypes) { - setFieldValue('enabled', true); - } - }} - error={ - values.enabled && !values.types && touched.types - ? intl.formatMessage(messages.validationTypes) - : undefined - } - /> -
-
- - - - - - -
-
- - ); - }} -
- ); -}; - -export default NotificationsPushbullet; diff --git a/src/components/Settings/Notifications/NotificationsPushover.tsx b/src/components/Settings/Notifications/NotificationsPushover.tsx deleted file mode 100644 index fb2f307bfb..0000000000 --- a/src/components/Settings/Notifications/NotificationsPushover.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import Button from '@app/components/Common/Button'; -import LoadingSpinner from '@app/components/Common/LoadingSpinner'; -import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; -import globalMessages from '@app/i18n/globalMessages'; -import defineMessages from '@app/utils/defineMessages'; -import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline'; -import type { PushoverSound } from '@server/api/pushover'; -import axios from 'axios'; -import { Field, Form, Formik } from 'formik'; -import { useState } from 'react'; -import { useIntl } from 'react-intl'; -import { useToasts } from 'react-toast-notifications'; -import useSWR from 'swr'; -import * as Yup from 'yup'; - -const messages = defineMessages( - 'components.Settings.Notifications.NotificationsPushover', - { - agentenabled: 'Enable Agent', - accessToken: 'Application API Token', - accessTokenTip: - 'Register an application for use with Jellyseerr', - userToken: 'User or Group Key', - userTokenTip: - 'Your 30-character user or group identifier', - sound: 'Notification Sound', - deviceDefault: 'Device Default', - validationAccessTokenRequired: 'You must provide a valid application token', - 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 = () => { - const intl = useIntl(); - const { addToast, removeToast } = useToasts(); - const [isTesting, setIsTesting] = useState(false); - const { - data, - error, - mutate: revalidate, - } = useSWR('/api/v1/settings/notifications/pushover'); - const { data: soundsData } = useSWR( - data?.options.accessToken - ? `/api/v1/settings/notifications/pushover/sounds?token=${data.options.accessToken}` - : null - ); - - const NotificationsPushoverSchema = Yup.object().shape({ - accessToken: Yup.string() - .when('enabled', { - is: true, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationAccessTokenRequired)), - otherwise: Yup.string().nullable(), - }) - .matches( - /^[a-z\d]{30}$/i, - intl.formatMessage(messages.validationAccessTokenRequired) - ), - userToken: Yup.string() - .when('enabled', { - is: true, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationUserTokenRequired)), - otherwise: Yup.string().nullable(), - }) - .matches( - /^[a-z\d]{30}$/i, - intl.formatMessage(messages.validationUserTokenRequired) - ), - }); - - if (!data && !error) { - return ; - } - - return ( - { - try { - await axios.post('/api/v1/settings/notifications/pushover', { - enabled: values.enabled, - types: values.types, - options: { - accessToken: values.accessToken, - userToken: values.userToken, - sound: values.sound, - }, - }); - addToast(intl.formatMessage(messages.pushoversettingssaved), { - appearance: 'success', - autoDismiss: true, - }); - } catch (e) { - addToast(intl.formatMessage(messages.pushoversettingsfailed), { - appearance: 'error', - autoDismiss: true, - }); - } finally { - revalidate(); - } - }} - > - {({ - errors, - touched, - isSubmitting, - values, - isValid, - setFieldValue, - setFieldTouched, - }) => { - const testSettings = async () => { - setIsTesting(true); - let toastId: string | undefined; - try { - addToast( - intl.formatMessage(messages.toastPushoverTestSending), - { - autoDismiss: false, - appearance: 'info', - }, - (id) => { - toastId = id; - } - ); - await axios.post('/api/v1/settings/notifications/pushover/test', { - enabled: true, - types: values.types, - options: { - accessToken: values.accessToken, - userToken: values.userToken, - sound: values.sound, - }, - }); - - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastPushoverTestSuccess), { - autoDismiss: true, - appearance: 'success', - }); - } catch (e) { - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastPushoverTestFailed), { - autoDismiss: true, - appearance: 'error', - }); - } finally { - setIsTesting(false); - } - }; - - return ( -
-
- -
- -
-
-
- -
-
- -
- {errors.accessToken && - touched.accessToken && - typeof errors.accessToken === 'string' && ( -
{errors.accessToken}
- )} -
-
-
- -
-
- -
- {errors.userToken && - touched.userToken && - typeof errors.userToken === 'string' && ( -
{errors.userToken}
- )} -
-
-
- -
-
- - - {soundsData?.map((sound, index) => ( - - ))} - -
-
-
- { - setFieldValue('types', newTypes); - setFieldTouched('types'); - - if (newTypes) { - setFieldValue('enabled', true); - } - }} - error={ - values.enabled && !values.types && touched.types - ? intl.formatMessage(messages.validationTypes) - : undefined - } - /> -
-
- - - - - - -
-
- - ); - }} -
- ); -}; - -export default NotificationsPushover; diff --git a/src/components/Settings/Notifications/NotificationsSlack.tsx b/src/components/Settings/Notifications/NotificationsSlack.tsx deleted file mode 100644 index 24ea784794..0000000000 --- a/src/components/Settings/Notifications/NotificationsSlack.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import Button from '@app/components/Common/Button'; -import LoadingSpinner from '@app/components/Common/LoadingSpinner'; -import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; -import globalMessages from '@app/i18n/globalMessages'; -import defineMessages from '@app/utils/defineMessages'; -import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline'; -import axios from 'axios'; -import { Field, Form, Formik } from 'formik'; -import { useState } from 'react'; -import { useIntl } from 'react-intl'; -import { useToasts } from 'react-toast-notifications'; -import useSWR from 'swr'; -import * as Yup from 'yup'; - -const messages = defineMessages( - 'components.Settings.Notifications.NotificationsSlack', - { - agentenabled: 'Enable Agent', - webhookUrl: 'Webhook URL', - webhookUrlTip: - 'Create an Incoming Webhook integration', - slacksettingssaved: 'Slack notification settings saved successfully!', - slacksettingsfailed: 'Slack notification settings failed to save.', - toastSlackTestSending: 'Sending Slack test notification…', - 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 = () => { - const intl = useIntl(); - const { addToast, removeToast } = useToasts(); - const [isTesting, setIsTesting] = useState(false); - const { - data, - error, - mutate: revalidate, - } = useSWR('/api/v1/settings/notifications/slack'); - - const NotificationsSlackSchema = Yup.object().shape({ - webhookUrl: Yup.string() - .when('enabled', { - is: true, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationWebhookUrl)), - otherwise: Yup.string().nullable(), - }) - .url(intl.formatMessage(messages.validationWebhookUrl)), - }); - - if (!data && !error) { - return ; - } - - return ( - { - try { - await axios.post('/api/v1/settings/notifications/slack', { - enabled: values.enabled, - types: values.types, - options: { - webhookUrl: values.webhookUrl, - }, - }); - addToast(intl.formatMessage(messages.slacksettingssaved), { - appearance: 'success', - autoDismiss: true, - }); - } catch (e) { - addToast(intl.formatMessage(messages.slacksettingsfailed), { - appearance: 'error', - autoDismiss: true, - }); - } finally { - revalidate(); - } - }} - > - {({ - errors, - touched, - isSubmitting, - values, - isValid, - setFieldValue, - setFieldTouched, - }) => { - const testSettings = async () => { - setIsTesting(true); - let toastId: string | undefined; - try { - addToast( - intl.formatMessage(messages.toastSlackTestSending), - { - autoDismiss: false, - appearance: 'info', - }, - (id) => { - toastId = id; - } - ); - await axios.post('/api/v1/settings/notifications/slack/test', { - enabled: true, - types: values.types, - options: { - webhookUrl: values.webhookUrl, - }, - }); - - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastSlackTestSuccess), { - autoDismiss: true, - appearance: 'success', - }); - } catch (e) { - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastSlackTestFailed), { - autoDismiss: true, - appearance: 'error', - }); - } finally { - setIsTesting(false); - } - }; - - return ( -
-
- -
- -
-
-
- -
-
- -
- {errors.webhookUrl && - touched.webhookUrl && - typeof errors.webhookUrl === 'string' && ( -
{errors.webhookUrl}
- )} -
-
- { - setFieldValue('types', newTypes); - setFieldTouched('types'); - - if (newTypes) { - setFieldValue('enabled', true); - } - }} - error={ - values.enabled && !values.types && touched.types - ? intl.formatMessage(messages.validationTypes) - : undefined - } - /> -
-
- - - - - - -
-
- - ); - }} -
- ); -}; - -export default NotificationsSlack; diff --git a/src/components/Settings/Notifications/NotificationsTelegram.tsx b/src/components/Settings/Notifications/NotificationsTelegram.tsx deleted file mode 100644 index 7272d7bd23..0000000000 --- a/src/components/Settings/Notifications/NotificationsTelegram.tsx +++ /dev/null @@ -1,399 +0,0 @@ -import Button from '@app/components/Common/Button'; -import LoadingSpinner from '@app/components/Common/LoadingSpinner'; -import SensitiveInput from '@app/components/Common/SensitiveInput'; -import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; -import globalMessages from '@app/i18n/globalMessages'; -import defineMessages from '@app/utils/defineMessages'; -import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline'; -import axios from 'axios'; -import { Field, Form, Formik } from 'formik'; -import { useState } from 'react'; -import { useIntl } from 'react-intl'; -import { useToasts } from 'react-toast-notifications'; -import useSWR from 'swr'; -import * as Yup from 'yup'; - -const messages = defineMessages('components.Settings.Notifications', { - agentenabled: 'Enable Agent', - botUsername: 'Bot Username', - botUsernameTip: - 'Allow users to also start a chat with your bot and configure their own notifications', - botAPI: 'Bot Authorization Token', - botApiTip: - 'Create a bot for use with Jellyseerr', - chatId: 'Chat ID', - chatIdTip: - 'Start a chat with your bot, add @get_id_bot, and issue the /my_id command', - messageThreadId: 'Thread/Topic ID', - messageThreadIdTip: - "If your group-chat has topics enabled, you can specify a thread/topic's ID here", - validationBotAPIRequired: 'You must provide a bot authorization token', - validationChatIdRequired: 'You must provide a valid chat ID', - validationMessageThreadId: - 'The thread/topic ID must be a positive whole number', - telegramsettingssaved: 'Telegram notification settings saved successfully!', - telegramsettingsfailed: 'Telegram notification settings failed to save.', - toastTelegramTestSending: 'Sending Telegram test notification…', - toastTelegramTestSuccess: 'Telegram test notification sent!', - toastTelegramTestFailed: 'Telegram test notification failed to send.', - sendSilently: 'Send Silently', - sendSilentlyTip: 'Send notifications with no sound', -}); - -const NotificationsTelegram = () => { - const intl = useIntl(); - const { addToast, removeToast } = useToasts(); - const [isTesting, setIsTesting] = useState(false); - const { - data, - error, - mutate: revalidate, - } = useSWR('/api/v1/settings/notifications/telegram'); - - const NotificationsTelegramSchema = Yup.object().shape({ - botAPI: Yup.string().when('enabled', { - is: true, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationBotAPIRequired)), - otherwise: Yup.string().nullable(), - }), - chatId: Yup.string() - .when(['enabled', 'types'], { - is: (enabled: boolean, types: number) => enabled && !!types, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationChatIdRequired)), - otherwise: Yup.string().nullable(), - }) - .matches( - /^-?\d+$/, - intl.formatMessage(messages.validationChatIdRequired) - ), - messageThreadId: Yup.string() - .when(['types'], { - is: (enabled: boolean, types: number) => enabled && !!types, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationMessageThreadId)), - otherwise: Yup.string().nullable(), - }) - .matches(/^\d+$/, intl.formatMessage(messages.validationMessageThreadId)), - }); - - if (!data && !error) { - return ; - } - - return ( - { - try { - await axios.post('/api/v1/settings/notifications/telegram', { - enabled: values.enabled, - types: values.types, - options: { - botAPI: values.botAPI, - chatId: values.chatId, - messageThreadId: values.messageThreadId, - sendSilently: values.sendSilently, - botUsername: values.botUsername, - }, - }); - - addToast(intl.formatMessage(messages.telegramsettingssaved), { - appearance: 'success', - autoDismiss: true, - }); - } catch (e) { - addToast(intl.formatMessage(messages.telegramsettingsfailed), { - appearance: 'error', - autoDismiss: true, - }); - } finally { - revalidate(); - } - }} - > - {({ - errors, - touched, - isSubmitting, - values, - isValid, - setFieldValue, - setFieldTouched, - }) => { - const testSettings = async () => { - setIsTesting(true); - let toastId: string | undefined; - try { - addToast( - intl.formatMessage(messages.toastTelegramTestSending), - { - autoDismiss: false, - appearance: 'info', - }, - (id) => { - toastId = id; - } - ); - await axios.post('/api/v1/settings/notifications/telegram/test', { - enabled: true, - types: values.types, - options: { - botAPI: values.botAPI, - chatId: values.chatId, - messageThreadId: values.messageThreadId, - sendSilently: values.sendSilently, - botUsername: values.botUsername, - }, - }); - - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastTelegramTestSuccess), { - autoDismiss: true, - appearance: 'success', - }); - } catch (e) { - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastTelegramTestFailed), { - autoDismiss: true, - appearance: 'error', - }); - } finally { - setIsTesting(false); - } - }; - - return ( -
-
- -
- -
-
-
- -
-
- -
- {errors.botAPI && - touched.botAPI && - typeof errors.botAPI === 'string' && ( -
{errors.botAPI}
- )} -
-
-
- -
-
- -
- {errors.botUsername && - touched.botUsername && - typeof errors.botUsername === 'string' && ( -
{errors.botUsername}
- )} -
-
-
- -
-
- -
- {errors.chatId && - touched.chatId && - typeof errors.chatId === 'string' && ( -
{errors.chatId}
- )} -
-
-
- -
-
- -
- {errors.messageThreadId && - touched.messageThreadId && - typeof errors.messageThreadId === 'string' && ( -
{errors.messageThreadId}
- )} -
-
-
- -
- -
-
- { - setFieldValue('types', newTypes); - setFieldTouched('types'); - - if (newTypes) { - setFieldValue('enabled', true); - } - }} - error={ - errors.types && touched.types - ? (errors.types as string) - : undefined - } - /> -
-
- - - - - - -
-
- - ); - }} -
- ); -}; - -export default NotificationsTelegram; diff --git a/src/components/Settings/Notifications/NotificationsWebPush.tsx b/src/components/Settings/Notifications/NotificationsWebPush.tsx deleted file mode 100644 index de5d23edbd..0000000000 --- a/src/components/Settings/Notifications/NotificationsWebPush.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import Alert from '@app/components/Common/Alert'; -import Button from '@app/components/Common/Button'; -import LoadingSpinner from '@app/components/Common/LoadingSpinner'; -import globalMessages from '@app/i18n/globalMessages'; -import defineMessages from '@app/utils/defineMessages'; -import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline'; -import axios from 'axios'; -import { Field, Form, Formik } from 'formik'; -import { useEffect, useState } from 'react'; -import { useIntl } from 'react-intl'; -import { useToasts } from 'react-toast-notifications'; -import useSWR, { mutate } from 'swr'; - -const messages = defineMessages( - 'components.Settings.Notifications.NotificationsWebPush', - { - agentenabled: 'Enable Agent', - webpushsettingssaved: 'Web push notification settings saved successfully!', - webpushsettingsfailed: 'Web push notification settings failed to save.', - toastWebPushTestSending: 'Sending web push test notification…', - toastWebPushTestSuccess: 'Web push test notification sent!', - toastWebPushTestFailed: 'Web push test notification failed to send.', - httpsRequirement: - 'In order to receive web push notifications, Jellyseerr must be served over HTTPS.', - } -); - -const NotificationsWebPush = () => { - const intl = useIntl(); - const { addToast, removeToast } = useToasts(); - const [isTesting, setIsTesting] = useState(false); - const [isHttps, setIsHttps] = useState(false); - const { - data, - error, - mutate: revalidate, - } = useSWR('/api/v1/settings/notifications/webpush'); - - useEffect(() => { - setIsHttps(window.location.protocol.startsWith('https')); - }, []); - - if (!data && !error) { - return ; - } - - return ( - <> - {!isHttps && ( - - )} - { - try { - await axios.post('/api/v1/settings/notifications/webpush', { - enabled: values.enabled, - options: {}, - }); - mutate('/api/v1/settings/public'); - addToast(intl.formatMessage(messages.webpushsettingssaved), { - appearance: 'success', - autoDismiss: true, - }); - } catch (e) { - addToast(intl.formatMessage(messages.webpushsettingsfailed), { - appearance: 'error', - autoDismiss: true, - }); - } finally { - revalidate(); - } - }} - > - {({ isSubmitting }) => { - const testSettings = async () => { - setIsTesting(true); - let toastId: string | undefined; - try { - addToast( - intl.formatMessage(messages.toastWebPushTestSending), - { - autoDismiss: false, - appearance: 'info', - }, - (id) => { - toastId = id; - } - ); - await axios.post('/api/v1/settings/notifications/webpush/test', { - enabled: true, - options: {}, - }); - - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastWebPushTestSuccess), { - autoDismiss: true, - appearance: 'success', - }); - } catch (e) { - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastWebPushTestFailed), { - autoDismiss: true, - appearance: 'error', - }); - } finally { - setIsTesting(false); - } - }; - - return ( -
-
- -
- -
-
-
-
- - - - - - -
-
-
- ); - }} -
- - ); -}; - -export default NotificationsWebPush; diff --git a/src/components/Settings/Notifications/NotificationsWebhook.tsx b/src/components/Settings/Notifications/NotificationsWebhook.tsx deleted file mode 100644 index 0595090f35..0000000000 --- a/src/components/Settings/Notifications/NotificationsWebhook.tsx +++ /dev/null @@ -1,397 +0,0 @@ -import Button from '@app/components/Common/Button'; -import LoadingSpinner from '@app/components/Common/LoadingSpinner'; -import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; -import globalMessages from '@app/i18n/globalMessages'; -import defineMessages from '@app/utils/defineMessages'; -import { isValidURL } from '@app/utils/urlValidationHelper'; -import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline'; -import { - ArrowPathIcon, - QuestionMarkCircleIcon, -} from '@heroicons/react/24/solid'; -import axios from 'axios'; -import { Field, Form, Formik } from 'formik'; -import dynamic from 'next/dynamic'; -import Link from 'next/link'; -import { useState } from 'react'; -import { useIntl } from 'react-intl'; -import { useToasts } from 'react-toast-notifications'; -import useSWR from 'swr'; -import * as Yup from 'yup'; - -const JSONEditor = dynamic(() => import('@app/components/JSONEditor'), { - ssr: false, -}); - -const defaultPayload = { - notification_type: '{{notification_type}}', - event: '{{event}}', - subject: '{{subject}}', - message: '{{message}}', - image: '{{image}}', - '{{media}}': { - media_type: '{{media_type}}', - tmdbId: '{{media_tmdbid}}', - tvdbId: '{{media_tvdbid}}', - status: '{{media_status}}', - status4k: '{{media_status4k}}', - }, - '{{request}}': { - request_id: '{{request_id}}', - requestedBy_email: '{{requestedBy_email}}', - requestedBy_username: '{{requestedBy_username}}', - requestedBy_avatar: '{{requestedBy_avatar}}', - requestedBy_settings_discordId: '{{requestedBy_settings_discordId}}', - requestedBy_settings_telegramChatId: - '{{requestedBy_settings_telegramChatId}}', - }, - '{{issue}}': { - issue_id: '{{issue_id}}', - issue_type: '{{issue_type}}', - issue_status: '{{issue_status}}', - reportedBy_email: '{{reportedBy_email}}', - reportedBy_username: '{{reportedBy_username}}', - reportedBy_avatar: '{{reportedBy_avatar}}', - reportedBy_settings_discordId: '{{reportedBy_settings_discordId}}', - reportedBy_settings_telegramChatId: - '{{reportedBy_settings_telegramChatId}}', - }, - '{{comment}}': { - comment_message: '{{comment_message}}', - commentedBy_email: '{{commentedBy_email}}', - commentedBy_username: '{{commentedBy_username}}', - commentedBy_avatar: '{{commentedBy_avatar}}', - commentedBy_settings_discordId: '{{commentedBy_settings_discordId}}', - commentedBy_settings_telegramChatId: - '{{commentedBy_settings_telegramChatId}}', - }, - '{{extra}}': [], -}; - -const messages = defineMessages( - 'components.Settings.Notifications.NotificationsWebhook', - { - agentenabled: 'Enable Agent', - webhookUrl: 'Webhook URL', - authheader: 'Authorization Header', - validationJsonPayloadRequired: 'You must provide a valid JSON payload', - webhooksettingssaved: 'Webhook notification settings saved successfully!', - webhooksettingsfailed: 'Webhook notification settings failed to save.', - toastWebhookTestSending: 'Sending webhook test notification…', - toastWebhookTestSuccess: 'Webhook test notification sent!', - toastWebhookTestFailed: 'Webhook test notification failed to send.', - resetPayload: 'Reset to Default', - resetPayloadSuccess: 'JSON payload reset successfully!', - customJson: 'JSON Payload', - templatevariablehelp: 'Template Variable Help', - validationWebhookUrl: 'You must provide a valid URL', - validationTypes: 'You must select at least one notification type', - } -); - -const NotificationsWebhook = () => { - const intl = useIntl(); - const { addToast, removeToast } = useToasts(); - const [isTesting, setIsTesting] = useState(false); - const { - data, - error, - mutate: revalidate, - } = useSWR('/api/v1/settings/notifications/webhook'); - - const NotificationsWebhookSchema = Yup.object().shape({ - webhookUrl: Yup.string() - .when('enabled', { - is: true, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationWebhookUrl)), - otherwise: Yup.string().nullable(), - }) - .test( - 'valid-url', - intl.formatMessage(messages.validationWebhookUrl), - isValidURL - ), - jsonPayload: Yup.string() - .when('enabled', { - is: true, - then: Yup.string() - .nullable() - .required(intl.formatMessage(messages.validationJsonPayloadRequired)), - otherwise: Yup.string().nullable(), - }) - .test( - 'validate-json', - intl.formatMessage(messages.validationJsonPayloadRequired), - (value) => { - try { - JSON.parse(value ?? ''); - return true; - } catch (e) { - return false; - } - } - ), - }); - - if (!data && !error) { - return ; - } - - return ( - { - try { - await axios.post('/api/v1/settings/notifications/webhook', { - enabled: values.enabled, - types: values.types, - options: { - webhookUrl: values.webhookUrl, - jsonPayload: JSON.stringify(values.jsonPayload), - authHeader: values.authHeader, - }, - }); - addToast(intl.formatMessage(messages.webhooksettingssaved), { - appearance: 'success', - autoDismiss: true, - }); - } catch (e) { - addToast(intl.formatMessage(messages.webhooksettingsfailed), { - appearance: 'error', - autoDismiss: true, - }); - } finally { - revalidate(); - } - }} - > - {({ - errors, - touched, - isSubmitting, - values, - isValid, - setFieldValue, - setFieldTouched, - }) => { - const resetPayload = () => { - setFieldValue( - 'jsonPayload', - JSON.stringify(defaultPayload, undefined, ' ') - ); - addToast(intl.formatMessage(messages.resetPayloadSuccess), { - appearance: 'info', - autoDismiss: true, - }); - }; - - const testSettings = async () => { - setIsTesting(true); - let toastId: string | undefined; - try { - addToast( - intl.formatMessage(messages.toastWebhookTestSending), - { - autoDismiss: false, - appearance: 'info', - }, - (id) => { - toastId = id; - } - ); - await axios.post('/api/v1/settings/notifications/webhook/test', { - enabled: true, - types: values.types, - options: { - webhookUrl: values.webhookUrl, - jsonPayload: JSON.stringify(values.jsonPayload), - authHeader: values.authHeader, - }, - }); - - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastWebhookTestSuccess), { - autoDismiss: true, - appearance: 'success', - }); - } catch (e) { - if (toastId) { - removeToast(toastId); - } - addToast(intl.formatMessage(messages.toastWebhookTestFailed), { - autoDismiss: true, - appearance: 'error', - }); - } finally { - setIsTesting(false); - } - }; - - return ( -
-
- -
- -
-
-
- -
-
- -
- {errors.webhookUrl && - touched.webhookUrl && - typeof errors.webhookUrl === 'string' && ( -
{errors.webhookUrl}
- )} -
-
-
- -
-
- -
-
-
-
- -
-
- setFieldValue('jsonPayload', value)} - value={values.jsonPayload} - onBlur={() => setFieldTouched('jsonPayload')} - /> -
- {errors.jsonPayload && - touched.jsonPayload && - typeof errors.jsonPayload === 'string' && ( -
{errors.jsonPayload}
- )} -
- - - - -
-
-
- { - setFieldValue('types', newTypes); - setFieldTouched('types'); - - if (newTypes) { - setFieldValue('enabled', true); - } - }} - error={ - values.enabled && !values.types && touched.types - ? intl.formatMessage(messages.validationTypes) - : undefined - } - /> -
-
- - - - - - -
-
- - ); - }} -
- ); -}; - -export default NotificationsWebhook; diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/DiscordModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/DiscordModal.tsx new file mode 100644 index 0000000000..8cf94b9df1 --- /dev/null +++ b/src/components/Settings/SettingsNotifications/NotificationModal/DiscordModal.tsx @@ -0,0 +1,294 @@ +import Modal from '@app/components/Common/Modal'; +import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import useSettings from '@app/hooks/useSettings'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import type { NotificationAgentDiscord } from '@server/interfaces/settings'; +import { Field, Form, Formik } from 'formik'; +import { useIntl } from 'react-intl'; +import * as Yup from 'yup'; + +const messages = defineMessages( + 'components.Settings.SettingsNotifications.NotificationModal', + { + instanceName: 'Name', + discordBotUsername: 'Bot Username', + discordBotAvatarUrl: 'Bot Avatar URL', + discordWebhookUrl: 'Webhook URL', + discordWebhookUrlTip: + 'Create a webhook integration in your server', + discordWebhookRoleId: 'Notification Role ID', + discordWebhookRoleIdTip: + 'The role ID to mention in the webhook message. Leave empty to disable mentions', + discordValidationUrl: 'You must provide a valid URL', + discordValidationWebhookRoleId: 'You must provide a valid Discord Role ID', + discordValidationTypes: 'You must select at least one notification type', + discordEnableMentions: 'Enable Mentions', + } +); + +interface DiscordModalProps { + title: string; + data: NotificationAgentDiscord; + onClose: () => void; + onTest: (testData: NotificationAgentDiscord) => void; + onSave: (submitData: NotificationAgentDiscord) => void; +} + +const DiscordModal = ({ + title, + data, + onClose, + onTest, + onSave, +}: DiscordModalProps) => { + const intl = useIntl(); + const settings = useSettings(); + + const NotificationsDiscordSchema = Yup.object().shape({ + botAvatarUrl: Yup.string() + .nullable() + .url(intl.formatMessage(messages.discordValidationUrl)), + webhookUrl: Yup.string() + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.discordValidationUrl)), + otherwise: Yup.string().nullable(), + }) + .url(intl.formatMessage(messages.discordValidationUrl)), + webhookRoleId: Yup.string() + .nullable() + .matches( + /^\d{17,19}$/, + intl.formatMessage(messages.discordValidationWebhookRoleId) + ), + }); + + return ( + { + await onSave({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + botUsername: values.botUsername, + botAvatarUrl: values.botAvatarUrl, + webhookUrl: values.webhookUrl, + webhookRoleId: values.webhookRoleId, + enableMentions: values.enableMentions, + }, + }); + }} + > + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + setFieldTouched, + handleSubmit, + }) => { + return ( + onClose()} + secondaryButtonType="warning" + secondaryText={intl.formatMessage(globalMessages.test)} + secondaryDisabled={isSubmitting || !isValid} + onSecondary={() => + onTest({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + botUsername: values.botUsername, + botAvatarUrl: values.botAvatarUrl, + webhookUrl: values.webhookUrl, + webhookRoleId: values.webhookRoleId, + enableMentions: values.enableMentions, + }, + }) + } + okButtonType="primary" + okText={ + isSubmitting + ? intl.formatMessage(globalMessages.saving) + : intl.formatMessage(globalMessages.save) + } + onOk={() => { + handleSubmit(); + }} + okDisabled={isSubmitting || !isValid} + > +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+ {errors.webhookUrl && + touched.webhookUrl && + typeof errors.webhookUrl === 'string' && ( +
{errors.webhookUrl}
+ )} +
+
+
+ +
+
+ +
+ {errors.botUsername && + touched.botUsername && + typeof errors.botUsername === 'string' && ( +
{errors.botUsername}
+ )} +
+
+
+ +
+
+ +
+ {errors.botAvatarUrl && + touched.botAvatarUrl && + typeof errors.botAvatarUrl === 'string' && ( +
{errors.botAvatarUrl}
+ )} +
+
+
+ +
+
+ +
+ {errors.webhookRoleId && + touched.webhookRoleId && + typeof errors.webhookRoleId === 'string' && ( +
{errors.webhookRoleId}
+ )} +
+
+
+ +
+ +
+
+ { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + values.enabled && !values.types && touched.types + ? intl.formatMessage(messages.discordValidationTypes) + : undefined + } + /> + +
+ ); + }} +
+ ); +}; + +export default DiscordModal; diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/EmailModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/EmailModal.tsx new file mode 100644 index 0000000000..5b8b03f92f --- /dev/null +++ b/src/components/Settings/SettingsNotifications/NotificationModal/EmailModal.tsx @@ -0,0 +1,476 @@ +import Modal from '@app/components/Common/Modal'; +import SensitiveInput from '@app/components/Common/SensitiveInput'; +import SettingsBadge from '@app/components/Settings/SettingsBadge'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import type { NotificationAgentEmail } from '@server/interfaces/settings'; +import { Field, Form, Formik } from 'formik'; +import { useIntl } from 'react-intl'; +import * as Yup from 'yup'; + +const messages = defineMessages( + 'components.Settings.SettingsNotifications.NotificationModal', + { + instanceName: 'Name', + emailValidationSmtpHostRequired: + 'You must provide a valid hostname or IP address', + emailValidationSmtpPortRequired: 'You must provide a valid port number', + emailUserRequired: 'Require user email', + emailSender: 'Sender Address', + emailSmtpHost: 'SMTP Host', + emailSmtpPort: 'SMTP Port', + emailEncryption: 'Encryption Method', + emailEncryptionTip: + 'In most cases, Implicit TLS uses port 465 and STARTTLS uses port 587', + emailEncryptionNone: 'None', + emailEncryptionDefault: 'Use STARTTLS if available', + emailEncryptionOpportunisticTls: 'Always use STARTTLS', + emailEncryptionImplicitTls: 'Use Implicit TLS', + emailAuthUser: 'SMTP Username', + emailAuthPass: 'SMTP Password', + emailAllowSelfSigned: 'Allow Self-Signed Certificates', + emailSenderName: 'Sender Name', + emailValidation: 'You must provide a valid email address', + emailPgpPrivateKey: 'PGP Private Key', + emailPgpPrivateKeyTip: + 'Sign encrypted email messages using OpenPGP', + emailValidationPgpPrivateKey: 'You must provide a valid PGP private key', + emailPgpPassword: 'PGP Password', + emailPgpPasswordTip: + 'Sign encrypted email messages using OpenPGP', + emailValidationPgpPassword: 'You must provide a PGP password', + } +); + +export function OpenPgpLink(msg: React.ReactNode) { + return ( + + {msg} + + ); +} + +interface EmailModalProps { + title: string; + data: NotificationAgentEmail; + onClose: () => void; + onTest: (testData: NotificationAgentEmail) => void; + onSave: (submitData: NotificationAgentEmail) => void; +} + +const EmailModal = ({ + title, + data, + onClose, + onTest, + onSave, +}: EmailModalProps) => { + const intl = useIntl(); + + const NotificationsEmailSchema = Yup.object().shape( + { + emailFrom: Yup.string() + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.emailValidation)), + otherwise: Yup.string().nullable(), + }) + .email(intl.formatMessage(messages.emailValidation)), + smtpHost: Yup.string().when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required( + intl.formatMessage(messages.emailValidationSmtpHostRequired) + ), + otherwise: Yup.string().nullable(), + }), + smtpPort: Yup.number().when('enabled', { + is: true, + then: Yup.number() + .nullable() + .required( + intl.formatMessage(messages.emailValidationSmtpPortRequired) + ), + otherwise: Yup.number().nullable(), + }), + pgpPrivateKey: Yup.string() + .when('pgpPassword', { + is: (value: unknown) => !!value, + then: Yup.string() + .nullable() + .required( + intl.formatMessage(messages.emailValidationPgpPrivateKey) + ), + otherwise: Yup.string().nullable(), + }) + .matches( + /-----BEGIN PGP PRIVATE KEY BLOCK-----.+-----END PGP PRIVATE KEY BLOCK-----/, + intl.formatMessage(messages.emailValidationPgpPrivateKey) + ), + pgpPassword: Yup.string().when('pgpPrivateKey', { + is: (value: unknown) => !!value, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.emailValidationPgpPassword)), + otherwise: Yup.string().nullable(), + }), + }, + [['pgpPrivateKey', 'pgpPassword']] + ); + + return ( + { + await onSave({ + enabled: values.enabled, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + userEmailRequired: values.userEmailRequired, + emailFrom: values.emailFrom, + smtpHost: values.smtpHost, + smtpPort: Number(values.smtpPort), + secure: values.encryption === 'implicit', + ignoreTls: values.encryption === 'none', + requireTls: values.encryption === 'opportunistic', + authUser: values.authUser, + authPass: values.authPass, + allowSelfSigned: values.allowSelfSigned, + senderName: values.senderName, + pgpPrivateKey: values.pgpPrivateKey, + pgpPassword: values.pgpPassword, + }, + }); + }} + > + {({ errors, touched, isSubmitting, values, isValid, handleSubmit }) => { + return ( + onClose()} + secondaryButtonType="warning" + secondaryText={intl.formatMessage(globalMessages.test)} + secondaryDisabled={isSubmitting || !isValid} + onSecondary={() => + onTest({ + enabled: values.enabled, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + userEmailRequired: values.userEmailRequired, + emailFrom: values.emailFrom, + smtpHost: values.smtpHost, + smtpPort: Number(values.smtpPort), + secure: values.encryption === 'implicit', + ignoreTls: values.encryption === 'none', + requireTls: values.encryption === 'opportunistic', + authUser: values.authUser, + authPass: values.authPass, + allowSelfSigned: values.allowSelfSigned, + senderName: values.senderName, + pgpPrivateKey: values.pgpPrivateKey, + pgpPassword: values.pgpPassword, + }, + }) + } + okButtonType="primary" + okText={ + isSubmitting + ? intl.formatMessage(globalMessages.saving) + : intl.formatMessage(globalMessages.save) + } + onOk={() => { + handleSubmit(); + }} + okDisabled={isSubmitting || !isValid} + > +
+
+ +
+
+ +
+
+
+
+ +
+ +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+ {errors.emailFrom && + touched.emailFrom && + typeof errors.emailFrom === 'string' && ( +
{errors.emailFrom}
+ )} +
+
+
+ +
+
+ +
+ {errors.smtpHost && + touched.smtpHost && + typeof errors.smtpHost === 'string' && ( +
{errors.smtpHost}
+ )} +
+
+
+ +
+ + {errors.smtpPort && + touched.smtpPort && + typeof errors.smtpPort === 'string' && ( +
{errors.smtpPort}
+ )} +
+
+
+ +
+
+ + + + + + +
+
+
+
+ +
+ +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+ {errors.pgpPrivateKey && + touched.pgpPrivateKey && + typeof errors.pgpPrivateKey === 'string' && ( +
{errors.pgpPrivateKey}
+ )} +
+
+
+ +
+
+ +
+ {errors.pgpPassword && + touched.pgpPassword && + typeof errors.pgpPassword === 'string' && ( +
{errors.pgpPassword}
+ )} +
+
+
+
+ ); + }} +
+ ); +}; + +export default EmailModal; diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/GotifyModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/GotifyModal.tsx new file mode 100644 index 0000000000..6363795771 --- /dev/null +++ b/src/components/Settings/SettingsNotifications/NotificationModal/GotifyModal.tsx @@ -0,0 +1,246 @@ +import Modal from '@app/components/Common/Modal'; +import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import { isValidURL } from '@app/utils/urlValidationHelper'; +import type { NotificationAgentGotify } from '@server/interfaces/settings'; +import { Field, Form, Formik } from 'formik'; +import { useIntl } from 'react-intl'; +import * as Yup from 'yup'; + +const messages = defineMessages( + 'components.Settings.SettingsNotifications.NotificationModal', + { + instanceName: 'Name', + gotifyUrl: 'Server URL', + gotifyToken: 'Application Token', + gotifyPriority: 'Priority', + gotifyValidationUrlRequired: 'You must provide a valid URL', + gotifyValidationUrlTrailingSlash: 'URL must not end in a trailing slash', + gotifyValidationTokenRequired: 'You must provide an application token', + gotifyValidationPriorityRequired: 'You must set a priority number', + validationTypes: 'You must select at least one notification type', + } +); + +interface GotifyModalProps { + title: string; + data: NotificationAgentGotify; + onClose: () => void; + onTest: (testData: NotificationAgentGotify) => void; + onSave: (submitData: NotificationAgentGotify) => void; +} + +const GotifyModal = ({ + title, + data, + onClose, + onTest, + onSave, +}: GotifyModalProps) => { + const intl = useIntl(); + + const NotificationsGotifySchema = Yup.object().shape({ + url: Yup.string() + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.gotifyValidationUrlRequired)), + otherwise: Yup.string().nullable(), + }) + .test( + 'valid-url', + intl.formatMessage(messages.gotifyValidationUrlRequired), + isValidURL + ) + .test( + 'no-trailing-slash', + intl.formatMessage(messages.gotifyValidationUrlTrailingSlash), + (value) => !value || !value.endsWith('/') + ), + token: Yup.string().when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.gotifyValidationTokenRequired)), + otherwise: Yup.string().nullable(), + }), + priority: Yup.string().when('enabled', { + is: true, + then: Yup.string() + .nullable() + .min(0) + .max(9) + .required( + intl.formatMessage(messages.gotifyValidationPriorityRequired) + ), + otherwise: Yup.string().nullable(), + }), + }); + + return ( + { + await onSave({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + url: values.url, + token: values.token, + priority: Number(values.priority), + }, + }); + }} + > + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + setFieldTouched, + handleSubmit, + }) => { + return ( + onClose()} + secondaryButtonType="warning" + secondaryText={intl.formatMessage(globalMessages.test)} + secondaryDisabled={isSubmitting || !isValid} + onSecondary={() => + onTest({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + url: values.url, + token: values.token, + priority: Number(values.priority), + }, + }) + } + okButtonType="primary" + okText={ + isSubmitting + ? intl.formatMessage(globalMessages.saving) + : intl.formatMessage(globalMessages.save) + } + onOk={() => { + handleSubmit(); + }} + okDisabled={isSubmitting || !isValid} + > +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+ {errors.url && + touched.url && + typeof errors.url === 'string' && ( +
{errors.url}
+ )} +
+
+
+ +
+
+ +
+ {errors.token && + touched.token && + typeof errors.token === 'string' && ( +
{errors.token}
+ )} +
+
+
+ +
+ + {errors.priority && + touched.priority && + typeof errors.priority === 'string' && ( +
{errors.priority}
+ )} +
+
+ { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + values.enabled && !values.types && touched.types + ? intl.formatMessage(messages.validationTypes) + : undefined + } + /> + +
+ ); + }} +
+ ); +}; + +export default GotifyModal; diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/LunaSeaModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/LunaSeaModal.tsx new file mode 100644 index 0000000000..266d738f5d --- /dev/null +++ b/src/components/Settings/SettingsNotifications/NotificationModal/LunaSeaModal.tsx @@ -0,0 +1,211 @@ +import Modal from '@app/components/Common/Modal'; +import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import type { NotificationAgentLunaSea } from '@server/interfaces/settings'; +import { Field, Form, Formik } from 'formik'; +import { useIntl } from 'react-intl'; +import * as Yup from 'yup'; + +const messages = defineMessages( + 'components.Settings.SettingsNotifications.NotificationModal', + { + instanceName: 'Name', + lunaSeaWebhookUrl: 'Webhook URL', + lunaSeaWebhookUrlTip: + 'Your user- or device-based notification webhook URL', + lunaSeaValidationWebhookUrl: 'You must provide a valid URL', + lunaSeaProfileName: 'Profile Name', + lunaSeaProfileNameTip: + 'Only required if not using the default profile', + validationTypes: 'You must select at least one notification type', + } +); + +interface LunaSeaModalProps { + title: string; + data: NotificationAgentLunaSea; + onClose: () => void; + onTest: (testData: NotificationAgentLunaSea) => void; + onSave: (submitData: NotificationAgentLunaSea) => void; +} + +const LunaSeaModal = ({ + title, + data, + onClose, + onTest, + onSave, +}: LunaSeaModalProps) => { + const intl = useIntl(); + + const NotificationsLunaSeaSchema = Yup.object().shape({ + webhookUrl: Yup.string() + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.lunaSeaValidationWebhookUrl)), + otherwise: Yup.string().nullable(), + }) + .url(intl.formatMessage(messages.lunaSeaValidationWebhookUrl)), + }); + + return ( + { + await onSave({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + webhookUrl: values.webhookUrl, + profileName: values.profileName, + }, + }); + }} + > + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + setFieldTouched, + handleSubmit, + }) => { + return ( + onClose()} + secondaryButtonType="warning" + secondaryText={intl.formatMessage(globalMessages.test)} + secondaryDisabled={isSubmitting || !isValid} + onSecondary={() => + onTest({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + webhookUrl: values.webhookUrl, + profileName: values.profileName, + }, + }) + } + okButtonType="primary" + okText={ + isSubmitting + ? intl.formatMessage(globalMessages.saving) + : intl.formatMessage(globalMessages.save) + } + onOk={() => { + handleSubmit(); + }} + okDisabled={isSubmitting || !isValid} + > +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+ {errors.webhookUrl && + touched.webhookUrl && + typeof errors.webhookUrl === 'string' && ( +
{errors.webhookUrl}
+ )} +
+
+
+ +
+
+ +
+
+
+ { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + values.enabled && !values.types && touched.types + ? intl.formatMessage(messages.validationTypes) + : undefined + } + /> + +
+ ); + }} +
+ ); +}; + +export default LunaSeaModal; diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/NtfyModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/NtfyModal.tsx new file mode 100644 index 0000000000..1120f452b7 --- /dev/null +++ b/src/components/Settings/SettingsNotifications/NotificationModal/NtfyModal.tsx @@ -0,0 +1,307 @@ +import Modal from '@app/components/Common/Modal'; +import SensitiveInput from '@app/components/Common/SensitiveInput'; +import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import { isValidURL } from '@app/utils/urlValidationHelper'; +import type { NotificationAgentNtfy } from '@server/interfaces/settings'; +import { Field, Form, Formik } from 'formik'; +import { useIntl } from 'react-intl'; +import * as Yup from 'yup'; + +const messages = defineMessages( + 'components.Settings.SettingsNotifications.NotificationModal', + { + instanceName: 'Name', + ntfyUrl: 'Server root URL', + ntfyTopic: 'Topic', + ntfyUsernamePasswordAuth: 'Username + Password authentication', + ntfyUsername: 'Username', + ntfyPassword: 'Password', + ntfyTokenAuth: 'Token authentication', + ntfyToken: 'Token', + ntfyValidationNtfyUrl: 'You must provide a valid URL', + ntfyValidationNtfyTopic: 'You must provide a topic', + validationTypes: 'You must select at least one notification type', + } +); + +interface NtfyModalProps { + title: string; + data: NotificationAgentNtfy; + onClose: () => void; + onTest: (testData: NotificationAgentNtfy) => void; + onSave: (submitData: NotificationAgentNtfy) => void; +} + +const NtfyModal = ({ + title, + data, + onClose, + onTest, + onSave, +}: NtfyModalProps) => { + const intl = useIntl(); + + const NotificationsNtfySchema = Yup.object().shape({ + url: Yup.string() + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.ntfyValidationNtfyUrl)), + otherwise: Yup.string().nullable(), + }) + .test( + 'valid-url', + intl.formatMessage(messages.ntfyValidationNtfyUrl), + isValidURL + ), + topic: Yup.string() + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.ntfyValidationNtfyUrl)), + otherwise: Yup.string().nullable(), + }) + .defined(intl.formatMessage(messages.ntfyValidationNtfyTopic)), + }); + + return ( + { + await onSave({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + url: values.url, + topic: values.topic, + authMethodUsernamePassword: values.authMethodUsernamePassword, + username: values.username, + password: values.password, + authMethodToken: values.authMethodToken, + token: values.token, + }, + }); + }} + > + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + setFieldTouched, + handleSubmit, + }) => { + return ( + onClose()} + secondaryButtonType="warning" + secondaryText={intl.formatMessage(globalMessages.test)} + secondaryDisabled={isSubmitting || !isValid} + onSecondary={() => + onTest({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + url: values.url, + topic: values.topic, + authMethodUsernamePassword: values.authMethodUsernamePassword, + username: values.username, + password: values.password, + authMethodToken: values.authMethodToken, + token: values.token, + }, + }) + } + okButtonType="primary" + okText={ + isSubmitting + ? intl.formatMessage(globalMessages.saving) + : intl.formatMessage(globalMessages.save) + } + onOk={() => { + handleSubmit(); + }} + okDisabled={isSubmitting || !isValid} + > +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+ {errors.url && + touched.url && + typeof errors.url === 'string' && ( +
{errors.url}
+ )} +
+
+
+ +
+
+ +
+ {errors.topic && + touched.topic && + typeof errors.topic === 'string' && ( +
{errors.topic}
+ )} +
+
+
+ +
+ { + setFieldValue( + 'authMethodUsernamePassword', + !values.authMethodUsernamePassword + ); + }} + /> +
+
+ {values.authMethodUsernamePassword && ( +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ )} +
+ +
+ { + setFieldValue('authMethodToken', !values.authMethodToken); + }} + /> +
+
+ {values.authMethodToken && ( +
+ +
+
+ +
+
+
+ )} + { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + values.enabled && !values.types && touched.types + ? intl.formatMessage(messages.validationTypes) + : undefined + } + /> + +
+ ); + }} +
+ ); +}; + +export default NtfyModal; diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/PushbulletModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/PushbulletModal.tsx new file mode 100644 index 0000000000..294c4d943e --- /dev/null +++ b/src/components/Settings/SettingsNotifications/NotificationModal/PushbulletModal.tsx @@ -0,0 +1,203 @@ +import Modal from '@app/components/Common/Modal'; +import SensitiveInput from '@app/components/Common/SensitiveInput'; +import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import type { NotificationAgentPushbullet } from '@server/interfaces/settings'; +import { Field, Form, Formik } from 'formik'; +import { useIntl } from 'react-intl'; +import * as Yup from 'yup'; + +const messages = defineMessages( + 'components.Settings.SettingsNotifications.NotificationModal', + { + instanceName: 'Name', + pushbulletAccessToken: 'Access Token', + pushbulletAccessTokenTip: + 'Create a token from your Account Settings', + pushbulletValidationAccessTokenRequired: 'You must provide an access token', + pushbulletChannelTag: 'Channel Tag', + validationTypes: 'You must select at least one notification type', + } +); + +interface PushbulletModalProps { + title: string; + data: NotificationAgentPushbullet; + onClose: () => void; + onTest: (testData: NotificationAgentPushbullet) => void; + onSave: (submitData: NotificationAgentPushbullet) => void; +} + +const PushbulletModal = ({ + title, + data, + onClose, + onTest, + onSave, +}: PushbulletModalProps) => { + const intl = useIntl(); + + const NotificationsPushbulletSchema = Yup.object().shape({ + accessToken: Yup.string().when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required( + intl.formatMessage(messages.pushbulletValidationAccessTokenRequired) + ), + otherwise: Yup.string().nullable(), + }), + }); + + return ( + { + await onSave({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + accessToken: values.accessToken, + channelTag: values.channelTag, + }, + }); + }} + > + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + setFieldTouched, + handleSubmit, + }) => { + return ( + onClose()} + secondaryButtonType="warning" + secondaryText={intl.formatMessage(globalMessages.test)} + secondaryDisabled={isSubmitting || !isValid} + onSecondary={() => + onTest({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + accessToken: values.accessToken, + channelTag: values.channelTag, + }, + }) + } + okButtonType="primary" + okText={ + isSubmitting + ? intl.formatMessage(globalMessages.saving) + : intl.formatMessage(globalMessages.save) + } + onOk={() => { + handleSubmit(); + }} + okDisabled={isSubmitting || !isValid} + > +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+ {errors.accessToken && + touched.accessToken && + typeof errors.accessToken === 'string' && ( +
{errors.accessToken}
+ )} +
+
+
+ +
+
+ +
+
+
+ { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + values.enabled && !values.types && touched.types + ? intl.formatMessage(messages.validationTypes) + : undefined + } + /> + +
+ ); + }} +
+ ); +}; + +export default PushbulletModal; diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/PushoverModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/PushoverModal.tsx new file mode 100644 index 0000000000..b36079b69a --- /dev/null +++ b/src/components/Settings/SettingsNotifications/NotificationModal/PushoverModal.tsx @@ -0,0 +1,277 @@ +import Modal from '@app/components/Common/Modal'; +import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import type { PushoverSound } from '@server/api/pushover'; +import type { NotificationAgentPushover } from '@server/interfaces/settings'; +import { Field, Form, Formik } from 'formik'; +import { useIntl } from 'react-intl'; +import useSWR from 'swr'; +import * as Yup from 'yup'; + +const messages = defineMessages( + 'components.Settings.SettingsNotifications.NotificationModal', + { + instanceName: 'Name', + pushoverAccessToken: 'Application API Token', + pushoverAccessTokenTip: + 'Register an application for use with Jellyseerr', + pushoverUserToken: 'User or Group Key', + pushoverUserTokenTip: + 'Your 30-character user or group identifier', + pushoverSound: 'Notification Sound', + pushoverDeviceDefault: 'Device Default', + pushoverValidationAccessTokenRequired: + 'You must provide a valid application token', + pushoverValidationUserTokenRequired: + 'You must provide a valid user or group key', + validationTypes: 'You must select at least one notification type', + } +); + +interface PushoverModalProps { + title: string; + data: NotificationAgentPushover; + onClose: () => void; + onTest: (testData: NotificationAgentPushover) => void; + onSave: (submitData: NotificationAgentPushover) => void; +} + +const PushoverModal = ({ + title, + data, + onClose, + onTest, + onSave, +}: PushoverModalProps) => { + const intl = useIntl(); + const { data: soundsData } = useSWR( + data.options.accessToken + ? `/api/v1/settings/notifications/pushover/sounds?token=${data.options.accessToken}` + : null + ); + + const NotificationsPushoverSchema = Yup.object().shape({ + accessToken: Yup.string() + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required( + intl.formatMessage(messages.pushoverValidationAccessTokenRequired) + ), + otherwise: Yup.string().nullable(), + }) + .matches( + /^[a-z\d]{30}$/i, + intl.formatMessage(messages.pushoverValidationAccessTokenRequired) + ), + userToken: Yup.string() + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required( + intl.formatMessage(messages.pushoverValidationUserTokenRequired) + ), + otherwise: Yup.string().nullable(), + }) + .matches( + /^[a-z\d]{30}$/i, + intl.formatMessage(messages.pushoverValidationUserTokenRequired) + ), + }); + + return ( + { + await onSave({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + accessToken: values.accessToken, + userToken: values.userToken, + sound: values.sound, + }, + }); + }} + > + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + setFieldTouched, + handleSubmit, + }) => { + return ( + onClose()} + secondaryButtonType="warning" + secondaryText={intl.formatMessage(globalMessages.test)} + secondaryDisabled={isSubmitting || !isValid} + onSecondary={() => + onTest({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + accessToken: values.accessToken, + userToken: values.userToken, + sound: values.sound, + }, + }) + } + okButtonType="primary" + okText={ + isSubmitting + ? intl.formatMessage(globalMessages.saving) + : intl.formatMessage(globalMessages.save) + } + onOk={() => { + handleSubmit(); + }} + okDisabled={isSubmitting || !isValid} + > +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+ {errors.accessToken && + touched.accessToken && + typeof errors.accessToken === 'string' && ( +
{errors.accessToken}
+ )} +
+
+
+ +
+
+ +
+ {errors.userToken && + touched.userToken && + typeof errors.userToken === 'string' && ( +
{errors.userToken}
+ )} +
+
+
+ +
+
+ + + {soundsData?.map((sound, index) => ( + + ))} + +
+
+
+ { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + values.enabled && !values.types && touched.types + ? intl.formatMessage(messages.validationTypes) + : undefined + } + /> + +
+ ); + }} +
+ ); +}; + +export default PushoverModal; diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/SlackModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/SlackModal.tsx new file mode 100644 index 0000000000..267562943b --- /dev/null +++ b/src/components/Settings/SettingsNotifications/NotificationModal/SlackModal.tsx @@ -0,0 +1,188 @@ +import Modal from '@app/components/Common/Modal'; +import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import type { NotificationAgentSlack } from '@server/interfaces/settings'; +import { Field, Form, Formik } from 'formik'; +import { useIntl } from 'react-intl'; +import * as Yup from 'yup'; + +const messages = defineMessages( + 'components.Settings.SettingsNotifications.NotificationModal', + { + instanceName: 'Name', + slackWebhookUrl: 'Webhook URL', + slackWebhookUrlTip: + 'Create an Incoming Webhook integration', + slackValidationWebhookUrl: 'You must provide a valid URL', + validationTypes: 'You must select at least one notification type', + } +); + +interface SlackModalProps { + title: string; + data: NotificationAgentSlack; + onClose: () => void; + onTest: (testData: NotificationAgentSlack) => void; + onSave: (submitData: NotificationAgentSlack) => void; +} + +const SlackModal = ({ + title, + data, + onClose, + onTest, + onSave, +}: SlackModalProps) => { + const intl = useIntl(); + + const NotificationsSlackSchema = Yup.object().shape({ + webhookUrl: Yup.string() + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.slackValidationWebhookUrl)), + otherwise: Yup.string().nullable(), + }) + .url(intl.formatMessage(messages.slackValidationWebhookUrl)), + }); + + return ( + { + await onSave({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + webhookUrl: values.webhookUrl, + }, + }); + }} + > + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + setFieldTouched, + handleSubmit, + }) => { + return ( + onClose()} + secondaryButtonType="warning" + secondaryText={intl.formatMessage(globalMessages.test)} + secondaryDisabled={isSubmitting || !isValid} + onSecondary={() => + onTest({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + webhookUrl: values.webhookUrl, + }, + }) + } + okButtonType="primary" + okText={ + isSubmitting + ? intl.formatMessage(globalMessages.saving) + : intl.formatMessage(globalMessages.save) + } + onOk={() => { + handleSubmit(); + }} + okDisabled={isSubmitting || !isValid} + > +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+ {errors.webhookUrl && + touched.webhookUrl && + typeof errors.webhookUrl === 'string' && ( +
{errors.webhookUrl}
+ )} +
+
+ { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + values.enabled && !values.types && touched.types + ? intl.formatMessage(messages.validationTypes) + : undefined + } + /> + +
+ ); + }} +
+ ); +}; + +export default SlackModal; diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/TelegramModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/TelegramModal.tsx new file mode 100644 index 0000000000..f3d0e9abfc --- /dev/null +++ b/src/components/Settings/SettingsNotifications/NotificationModal/TelegramModal.tsx @@ -0,0 +1,361 @@ +import Modal from '@app/components/Common/Modal'; +import SensitiveInput from '@app/components/Common/SensitiveInput'; +import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import type { NotificationAgentTelegram } from '@server/interfaces/settings'; +import { Field, Form, Formik } from 'formik'; +import { useIntl } from 'react-intl'; +import * as Yup from 'yup'; + +const messages = defineMessages( + 'components.Settings.SettingsNotifications.NotificationModal', + { + instanceName: 'Name', + telegramBotUsername: 'Bot Username', + telegramBotUsernameTip: + 'Allow users to also start a chat with your bot and configure their own notifications', + telegramBotAPI: 'Bot Authorization Token', + telegramBotApiTip: + 'Create a bot for use with Jellyseerr', + telegramChatId: 'Chat ID', + telegramChatIdTip: + 'Start a chat with your bot, add @get_id_bot, and issue the /my_id command', + telegramMessageThreadId: 'Thread/Topic ID', + telegramMessageThreadIdTip: + "If your group-chat has topics enabled, you can specify a thread/topic's ID here", + telegramValidationBotAPIRequired: + 'You must provide a bot authorization token', + telegramValidationChatIdRequired: 'You must provide a valid chat ID', + telegramValidationMessageThreadId: + 'The thread/topic ID must be a positive whole number', + telegramSendSilently: 'Send Silently', + telegramSendSilentlyTip: 'Send notifications with no sound', + } +); + +interface TelegramModalProps { + title: string; + data: NotificationAgentTelegram; + onClose: () => void; + onTest: (testData: NotificationAgentTelegram) => void; + onSave: (submitData: NotificationAgentTelegram) => void; +} + +const TelegramModal = ({ + title, + data, + onClose, + onTest, + onSave, +}: TelegramModalProps) => { + const intl = useIntl(); + + const NotificationsTelegramSchema = Yup.object().shape({ + botAPI: Yup.string().when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required( + intl.formatMessage(messages.telegramValidationBotAPIRequired) + ), + otherwise: Yup.string().nullable(), + }), + chatId: Yup.string() + .when(['enabled', 'types'], { + is: (enabled: boolean, types: number) => enabled && !!types, + then: Yup.string() + .nullable() + .required( + intl.formatMessage(messages.telegramValidationChatIdRequired) + ), + otherwise: Yup.string().nullable(), + }) + .matches( + /^-?\d+$/, + intl.formatMessage(messages.telegramValidationChatIdRequired) + ), + messageThreadId: Yup.string() + .when(['types'], { + is: (enabled: boolean, types: number) => enabled && !!types, + then: Yup.string() + .nullable() + .required( + intl.formatMessage(messages.telegramValidationMessageThreadId) + ), + otherwise: Yup.string().nullable(), + }) + .matches( + /^\d+$/, + intl.formatMessage(messages.telegramValidationMessageThreadId) + ), + }); + + return ( + { + await onSave({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + botUsername: values.botUsername, + botAPI: values.botAPI, + chatId: values.chatId, + messageThreadId: values.messageThreadId, + sendSilently: values.sendSilently, + }, + }); + }} + > + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + setFieldTouched, + handleSubmit, + }) => { + return ( + onClose()} + secondaryButtonType="warning" + secondaryText={intl.formatMessage(globalMessages.test)} + secondaryDisabled={isSubmitting || !isValid} + onSecondary={() => + onTest({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + botUsername: values.botUsername, + botAPI: values.botAPI, + chatId: values.chatId, + messageThreadId: values.messageThreadId, + sendSilently: values.sendSilently, + }, + }) + } + okButtonType="primary" + okText={ + isSubmitting + ? intl.formatMessage(globalMessages.saving) + : intl.formatMessage(globalMessages.save) + } + onOk={() => { + handleSubmit(); + }} + okDisabled={isSubmitting || !isValid} + > +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+ {errors.botAPI && + touched.botAPI && + typeof errors.botAPI === 'string' && ( +
{errors.botAPI}
+ )} +
+
+
+ +
+
+ +
+ {errors.botUsername && + touched.botUsername && + typeof errors.botUsername === 'string' && ( +
{errors.botUsername}
+ )} +
+
+
+ +
+
+ +
+ {errors.chatId && + touched.chatId && + typeof errors.chatId === 'string' && ( +
{errors.chatId}
+ )} +
+
+
+ +
+
+ +
+ {errors.messageThreadId && + touched.messageThreadId && + typeof errors.messageThreadId === 'string' && ( +
{errors.messageThreadId}
+ )} +
+
+
+ +
+ +
+
+ { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + errors.types && touched.types + ? (errors.types as string) + : undefined + } + /> + +
+ ); + }} +
+ ); +}; + +export default TelegramModal; diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/WebPushModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/WebPushModal.tsx new file mode 100644 index 0000000000..3f8eb233e3 --- /dev/null +++ b/src/components/Settings/SettingsNotifications/NotificationModal/WebPushModal.tsx @@ -0,0 +1,117 @@ +import Alert from '@app/components/Common/Alert'; +import Modal from '@app/components/Common/Modal'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import type { NotificationAgentConfig } from '@server/interfaces/settings'; +import { Field, Form, Formik } from 'formik'; +import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; + +const messages = defineMessages( + 'components.Settings.SettingsNotifications.NotificationModal', + { + instanceName: 'Name', + webPushHttpsRequirement: + 'In order to receive web push notifications, Jellyseerr must be served over HTTPS.', + } +); + +interface WebPushModalProps { + title: string; + data: NotificationAgentConfig; + onClose: () => void; + onTest: (testData: NotificationAgentConfig) => void; + onSave: (submitData: NotificationAgentConfig) => void; +} + +const WebPushModal = ({ + title, + data, + onClose, + onTest, + onSave, +}: WebPushModalProps) => { + const intl = useIntl(); + const [isHttps, setIsHttps] = useState(false); + + useEffect(() => { + setIsHttps(window.location.protocol.startsWith('https')); + }, []); + + return ( + <> + {!isHttps && ( + + )} + { + await onSave({ + enabled: values.enabled, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: {}, + }); + }} + > + {({ values, isSubmitting, handleSubmit }) => { + return ( + onClose()} + secondaryButtonType="warning" + secondaryText={intl.formatMessage(globalMessages.test)} + secondaryDisabled={isSubmitting} + onSecondary={() => + onTest({ + enabled: values.enabled, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: {}, + }) + } + okButtonType="primary" + okText={ + isSubmitting + ? intl.formatMessage(globalMessages.saving) + : intl.formatMessage(globalMessages.save) + } + onOk={() => { + handleSubmit(); + }} + okDisabled={isSubmitting} + > +
+
+ +
+
+ +
+
+
+
+
+ ); + }} +
+ + ); +}; + +export default WebPushModal; diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/WebhookModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/WebhookModal.tsx new file mode 100644 index 0000000000..960abb3aac --- /dev/null +++ b/src/components/Settings/SettingsNotifications/NotificationModal/WebhookModal.tsx @@ -0,0 +1,346 @@ +import Button from '@app/components/Common/Button'; +import Modal from '@app/components/Common/Modal'; +import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import { isValidURL } from '@app/utils/urlValidationHelper'; +import { + ArrowPathIcon, + QuestionMarkCircleIcon, +} from '@heroicons/react/24/solid'; +import type { NotificationAgentWebhook } from '@server/interfaces/settings'; +import { Field, Form, Formik } from 'formik'; +import dynamic from 'next/dynamic'; +import Link from 'next/link'; +import { useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; +import * as Yup from 'yup'; + +const JSONEditor = dynamic(() => import('@app/components/JSONEditor'), { + ssr: false, +}); + +const defaultPayload = { + notification_type: '{{notification_type}}', + event: '{{event}}', + subject: '{{subject}}', + message: '{{message}}', + image: '{{image}}', + '{{media}}': { + media_type: '{{media_type}}', + tmdbId: '{{media_tmdbid}}', + tvdbId: '{{media_tvdbid}}', + status: '{{media_status}}', + status4k: '{{media_status4k}}', + }, + '{{request}}': { + request_id: '{{request_id}}', + requestedBy_email: '{{requestedBy_email}}', + requestedBy_username: '{{requestedBy_username}}', + requestedBy_avatar: '{{requestedBy_avatar}}', + requestedBy_settings_discordId: '{{requestedBy_settings_discordId}}', + requestedBy_settings_telegramChatId: + '{{requestedBy_settings_telegramChatId}}', + }, + '{{issue}}': { + issue_id: '{{issue_id}}', + issue_type: '{{issue_type}}', + issue_status: '{{issue_status}}', + reportedBy_email: '{{reportedBy_email}}', + reportedBy_username: '{{reportedBy_username}}', + reportedBy_avatar: '{{reportedBy_avatar}}', + reportedBy_settings_discordId: '{{reportedBy_settings_discordId}}', + reportedBy_settings_telegramChatId: + '{{reportedBy_settings_telegramChatId}}', + }, + '{{comment}}': { + comment_message: '{{comment_message}}', + commentedBy_email: '{{commentedBy_email}}', + commentedBy_username: '{{commentedBy_username}}', + commentedBy_avatar: '{{commentedBy_avatar}}', + commentedBy_settings_discordId: '{{commentedBy_settings_discordId}}', + commentedBy_settings_telegramChatId: + '{{commentedBy_settings_telegramChatId}}', + }, + '{{extra}}': [], +}; + +const messages = defineMessages( + 'components.Settings.SettingsNotifications.NotificationModal', + { + instanceName: 'Name', + webhookUrl: 'Webhook URL', + webhookAuthheader: 'Authorization Header', + webhookValidationJsonPayloadRequired: + 'You must provide a valid JSON payload', + webhookResetPayload: 'Reset to Default', + webhookResetPayloadSuccess: 'JSON payload reset successfully!', + webhookCustomJson: 'JSON Payload', + webhookTemplateVariableHelp: 'Template Variable Help', + webhookValidationWebhookUrl: 'You must provide a valid URL', + validationTypes: 'You must select at least one notification type', + } +); + +interface WebhookModalProps { + title: string; + data: NotificationAgentWebhook; + onClose: () => void; + onTest: (testData: NotificationAgentWebhook) => void; + onSave: (submitData: NotificationAgentWebhook) => void; +} + +const WebhookModal = ({ + title, + data, + onClose, + onTest, + onSave, +}: WebhookModalProps) => { + const intl = useIntl(); + const { addToast } = useToasts(); + + const NotificationsWebhookSchema = Yup.object().shape({ + webhookUrl: Yup.string() + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.webhookValidationWebhookUrl)), + otherwise: Yup.string().nullable(), + }) + .test( + 'valid-url', + intl.formatMessage(messages.webhookValidationWebhookUrl), + isValidURL + ), + jsonPayload: Yup.string() + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required( + intl.formatMessage(messages.webhookValidationJsonPayloadRequired) + ), + otherwise: Yup.string().nullable(), + }) + .test( + 'validate-json', + intl.formatMessage(messages.webhookValidationJsonPayloadRequired), + (value) => { + try { + JSON.parse(value ?? ''); + return true; + } catch (e) { + return false; + } + } + ), + }); + + return ( + { + await onSave({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + webhookUrl: values.webhookUrl, + jsonPayload: values.jsonPayload, + authHeader: values.authHeader, + }, + }); + }} + > + {({ + errors, + touched, + isSubmitting, + values, + isValid, + setFieldValue, + setFieldTouched, + handleSubmit, + }) => { + const resetPayload = () => { + setFieldValue( + 'jsonPayload', + JSON.stringify(defaultPayload, undefined, ' ') + ); + addToast(intl.formatMessage(messages.webhookResetPayloadSuccess), { + appearance: 'info', + autoDismiss: true, + }); + }; + + return ( + onClose()} + secondaryButtonType="warning" + secondaryText={intl.formatMessage(globalMessages.test)} + secondaryDisabled={isSubmitting || !isValid} + onSecondary={() => + onTest({ + enabled: values.enabled, + types: values.types, + name: values.name, + id: values.id, + agent: values.agent, + default: values.default, + options: { + webhookUrl: values.webhookUrl, + jsonPayload: values.jsonPayload, + authHeader: values.authHeader, + }, + }) + } + okButtonType="primary" + okText={ + isSubmitting + ? intl.formatMessage(globalMessages.saving) + : intl.formatMessage(globalMessages.save) + } + onOk={() => { + handleSubmit(); + }} + okDisabled={isSubmitting || !isValid} + > +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+ {errors.webhookUrl && + touched.webhookUrl && + typeof errors.webhookUrl === 'string' && ( +
{errors.webhookUrl}
+ )} +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ setFieldValue('jsonPayload', value)} + value={values.jsonPayload} + onBlur={() => setFieldTouched('jsonPayload')} + /> +
+ {errors.jsonPayload && + touched.jsonPayload && + typeof errors.jsonPayload === 'string' && ( +
{errors.jsonPayload}
+ )} +
+ + + + +
+
+
+ { + setFieldValue('types', newTypes); + setFieldTouched('types'); + + if (newTypes) { + setFieldValue('enabled', true); + } + }} + error={ + values.enabled && !values.types && touched.types + ? intl.formatMessage(messages.validationTypes) + : undefined + } + /> + +
+ ); + }} +
+ ); +}; + +export default WebhookModal; diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/index.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/index.tsx new file mode 100644 index 0000000000..fb3dcd0d3d --- /dev/null +++ b/src/components/Settings/SettingsNotifications/NotificationModal/index.tsx @@ -0,0 +1,230 @@ +import DiscordModal from '@app/components/Settings/SettingsNotifications/NotificationModal/DiscordModal'; +import EmailModal from '@app/components/Settings/SettingsNotifications/NotificationModal/EmailModal'; +import GotifyModal from '@app/components/Settings/SettingsNotifications/NotificationModal/GotifyModal'; +import LunaSeaModal from '@app/components/Settings/SettingsNotifications/NotificationModal/LunaSeaModal'; +import NtfyModal from '@app/components/Settings/SettingsNotifications/NotificationModal/NtfyModal'; +import PushbulletModal from '@app/components/Settings/SettingsNotifications/NotificationModal/PushbulletModal'; +import PushoverModal from '@app/components/Settings/SettingsNotifications/NotificationModal/PushoverModal'; +import SlackModal from '@app/components/Settings/SettingsNotifications/NotificationModal/SlackModal'; +import TelegramModal from '@app/components/Settings/SettingsNotifications/NotificationModal/TelegramModal'; +import WebhookModal from '@app/components/Settings/SettingsNotifications/NotificationModal/WebhookModal'; +import WebPushModal from '@app/components/Settings/SettingsNotifications/NotificationModal/WebPushModal'; +import defineMessages from '@app/utils/defineMessages'; +import type { + NotificationAgentConfig, + NotificationAgentDiscord, + NotificationAgentEmail, + NotificationAgentGotify, + NotificationAgentLunaSea, + NotificationAgentNtfy, + NotificationAgentPushbullet, + NotificationAgentPushover, + NotificationAgentSlack, + NotificationAgentTelegram, + NotificationAgentWebhook, +} from '@server/interfaces/settings'; +import { NotificationAgentKey } from '@server/interfaces/settings'; +import axios from 'axios'; +import { useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; + +const messages = defineMessages( + 'components.Settings.SettingsNotifications.NotificationModal', + { + editTitle: 'Edit Notification Instance', + createTitle: 'Create Notification Instance', + toastTestSending: 'Sending test notification…', + toastTestSuccess: 'Test notification sent!', + toastTestFailed: 'Test notification failed to send.', + toastSaveSuccess: 'Notification settings saved successfully!', + toastSaveFail: 'Notification settings failed to save.', + } +); + +interface NotificationModalProps { + data: NotificationAgentConfig | null; + afterSave: () => void; + onClose: () => void; +} + +const NotificationModal = ({ + data, + afterSave, + onClose, +}: NotificationModalProps) => { + const intl = useIntl(); + const { addToast, removeToast } = useToasts(); + + const onSave = async (submitData: NotificationAgentConfig) => { + try { + await axios.post( + `/api/v1/settings/notification/${submitData.id}`, + submitData + ); + + addToast(intl.formatMessage(messages.toastSaveSuccess), { + appearance: 'success', + autoDismiss: true, + }); + } catch (e) { + addToast(intl.formatMessage(messages.toastSaveFail), { + appearance: 'error', + autoDismiss: true, + }); + } finally { + afterSave(); + } + }; + + const onTest = async (testData: NotificationAgentConfig) => { + let toastId: string | undefined; + try { + addToast( + intl.formatMessage(messages.toastTestSending), + { + autoDismiss: false, + appearance: 'info', + }, + (id) => { + toastId = id; + } + ); + await axios.post('/api/v1/settings/notification/test', testData); + + if (toastId) { + removeToast(toastId); + } + addToast(intl.formatMessage(messages.toastTestSuccess), { + autoDismiss: true, + appearance: 'success', + }); + } catch (e) { + if (toastId) { + removeToast(toastId); + } + addToast(intl.formatMessage(messages.toastTestFailed), { + autoDismiss: true, + appearance: 'error', + }); + } + }; + + const editTitle = `${intl.formatMessage(messages.editTitle)} #${data?.id}`; + + switch (data?.agent) { + case NotificationAgentKey.DISCORD: + return ( + + ); + case NotificationAgentKey.EMAIL: + return ( + + ); + case NotificationAgentKey.GOTIFY: + return ( + + ); + case NotificationAgentKey.NTFY: + return ( + + ); + case NotificationAgentKey.LUNASEA: + return ( + + ); + case NotificationAgentKey.PUSHBULLET: + return ( + + ); + case NotificationAgentKey.PUSHOVER: + return ( + + ); + case NotificationAgentKey.SLACK: + return ( + + ); + case NotificationAgentKey.TELEGRAM: + return ( + + ); + case NotificationAgentKey.WEBHOOK: + return ( + + ); + case NotificationAgentKey.WEBPUSH: + return ( + + ); + } + + return <>; +}; + +export default NotificationModal; diff --git a/src/components/Settings/SettingsNotifications.tsx b/src/components/Settings/SettingsNotifications/index.tsx similarity index 89% rename from src/components/Settings/SettingsNotifications.tsx rename to src/components/Settings/SettingsNotifications/index.tsx index bfb2035d5b..a9f457be28 100644 --- a/src/components/Settings/SettingsNotifications.tsx +++ b/src/components/Settings/SettingsNotifications/index.tsx @@ -3,9 +3,11 @@ import Header from '@app/components/Common/Header'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import PageTitle from '@app/components/Common/PageTitle'; import Table from '@app/components/Common/Table'; +import NotificationModal from '@app/components/Settings/SettingsNotifications/NotificationModal'; import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; +import { Transition } from '@headlessui/react'; import { BarsArrowDownIcon, BeakerIcon, @@ -15,6 +17,7 @@ import { TrashIcon, } from '@heroicons/react/24/solid'; import type { NotificationSettingsResultResponse } from '@server/interfaces/api/settingsInterfaces'; +import type { NotificationAgentConfig } from '@server/interfaces/settings'; import axios from 'axios'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; @@ -22,7 +25,7 @@ import { useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; -const messages = defineMessages('components.Settings', { +const messages = defineMessages('components.Settings.SettingsNotifications', { notifications: 'Notifications', notificationInstanceList: 'Notification Instance List', instanceName: 'Name', @@ -49,6 +52,13 @@ const SettingsNotifications = () => { const [currentSort, setCurrentSort] = useState(Sort.ID); const [currentPageSize, setCurrentPageSize] = useState(10); const [selectedInstances, setSelectedInstances] = useState([]); + const [notificationModal, setNotificationModal] = useState<{ + open: boolean; + instance: NotificationAgentConfig | null; + }>({ + open: false, + instance: null, + }); const page = router.query.page ? Number(router.query.page) : 1; const pageIndex = page - 1; @@ -127,7 +137,7 @@ const SettingsNotifications = () => { } }; - const testInstance = async (instanceId: number) => { + const testInstance = async (instanceIndex: number) => { let toastId: string | undefined; try { addToast( @@ -142,7 +152,7 @@ const SettingsNotifications = () => { ); await axios.post( '/api/v1/settings/notification/test', - data?.results[instanceId] + data?.results[instanceIndex] ); if (toastId) { @@ -179,6 +189,30 @@ const SettingsNotifications = () => { ]} /> + {notificationModal.open && ( + + { + revalidate(); + setNotificationModal({ open: false, instance: null }); + }} + onClose={() => { + setNotificationModal({ open: false, instance: null }); + }} + /> + + )} +
{intl.formatMessage(messages.notificationInstanceList)}
@@ -246,7 +280,7 @@ const SettingsNotifications = () => { - {data.results.map((instance) => ( + {data.results.map((instance, instanceIndex) => ( { buttonType="warning" className="mr-2" onClick={() => - router.push( - '/users/[userId]/settings', - `/users/${instance.id}/settings` - ) + setNotificationModal({ open: true, instance: instance }) } > @@ -291,7 +322,7 @@ const SettingsNotifications = () => { className="mr-4" onClick={(e) => { e.preventDefault(); - testInstance(instance.id); + testInstance(instanceIndex); }} > From b4269c02ab16dcf61707fd30615949bb77f8f768 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Sun, 11 May 2025 17:03:10 +0200 Subject: [PATCH 30/62] fix(notifications): fix imports --- .../UserNotificationSettings/UserNotificationsEmail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx index c1196d5acf..2f325747d6 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx @@ -4,8 +4,8 @@ import SensitiveInput from '@app/components/Common/SensitiveInput'; import NotificationTypeSelector, { ALL_NOTIFICATIONS, } from '@app/components/NotificationTypeSelector'; -import { OpenPgpLink } from '@app/components/Settings/Notifications/NotificationsEmail'; import SettingsBadge from '@app/components/Settings/SettingsBadge'; +import { OpenPgpLink } from '@app/components/Settings/SettingsNotifications/NotificationModal/EmailModal'; import { useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; From f82b320e24368dfb4b1b34fca6585607888fae88 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Sun, 18 May 2025 12:06:58 +0200 Subject: [PATCH 31/62] feat(notifications): add notification modal type --- src/components/Common/Dropdown/index.tsx | 6 +- .../NotificationModal/DiscordModal.tsx | 17 +- .../NotificationModal/EmailModal.tsx | 17 +- .../NotificationModal/GotifyModal.tsx | 17 +- .../NotificationModal/LunaSeaModal.tsx | 17 +- .../NotificationModal/NtfyModal.tsx | 23 ++- .../NotificationModal/PushbulletModal.tsx | 17 +- .../NotificationModal/PushoverModal.tsx | 17 +- .../NotificationModal/SlackModal.tsx | 17 +- .../NotificationModal/TelegramModal.tsx | 17 +- .../NotificationModal/WebPushModal.tsx | 17 +- .../NotificationModal/WebhookModal.tsx | 17 +- .../NotificationModal/index.tsx | 34 ++-- .../Settings/SettingsNotifications/index.tsx | 191 ++++++++++++++++-- .../UserLinkedAccountsSettings/index.tsx | 6 +- 15 files changed, 359 insertions(+), 71 deletions(-) diff --git a/src/components/Common/Dropdown/index.tsx b/src/components/Common/Dropdown/index.tsx index 74ce79f2e9..f0e54ddaa0 100644 --- a/src/components/Common/Dropdown/index.tsx +++ b/src/components/Common/Dropdown/index.tsx @@ -22,7 +22,7 @@ const DropdownItem = ({ - {text} + {text} {children && (dropdownIcon ? dropdownIcon : )} {children && ( diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/DiscordModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/DiscordModal.tsx index 8cf94b9df1..b3843a164b 100644 --- a/src/components/Settings/SettingsNotifications/NotificationModal/DiscordModal.tsx +++ b/src/components/Settings/SettingsNotifications/NotificationModal/DiscordModal.tsx @@ -1,5 +1,6 @@ import Modal from '@app/components/Common/Modal'; import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import { NotificationModalType } from '@app/components/Settings/SettingsNotifications/NotificationModal'; import useSettings from '@app/hooks/useSettings'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; @@ -11,6 +12,9 @@ import * as Yup from 'yup'; const messages = defineMessages( 'components.Settings.SettingsNotifications.NotificationModal', { + editTitle: 'Edit Notification Instance', + createTitle: 'Create Notification Instance', + createInstance: 'Create Instance', instanceName: 'Name', discordBotUsername: 'Bot Username', discordBotAvatarUrl: 'Bot Avatar URL', @@ -28,7 +32,7 @@ const messages = defineMessages( ); interface DiscordModalProps { - title: string; + type: NotificationModalType; data: NotificationAgentDiscord; onClose: () => void; onTest: (testData: NotificationAgentDiscord) => void; @@ -36,7 +40,7 @@ interface DiscordModalProps { } const DiscordModal = ({ - title, + type, data, onClose, onTest, @@ -110,6 +114,11 @@ const DiscordModal = ({ setFieldTouched, handleSubmit, }) => { + const title = + type === NotificationModalType.EDIT + ? `${intl.formatMessage(messages.editTitle)} #${data?.id}` + : intl.formatMessage(messages.createTitle); + return ( { handleSubmit(); diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/EmailModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/EmailModal.tsx index 5b8b03f92f..3695d70342 100644 --- a/src/components/Settings/SettingsNotifications/NotificationModal/EmailModal.tsx +++ b/src/components/Settings/SettingsNotifications/NotificationModal/EmailModal.tsx @@ -1,6 +1,7 @@ import Modal from '@app/components/Common/Modal'; import SensitiveInput from '@app/components/Common/SensitiveInput'; import SettingsBadge from '@app/components/Settings/SettingsBadge'; +import { NotificationModalType } from '@app/components/Settings/SettingsNotifications/NotificationModal'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import type { NotificationAgentEmail } from '@server/interfaces/settings'; @@ -11,6 +12,9 @@ import * as Yup from 'yup'; const messages = defineMessages( 'components.Settings.SettingsNotifications.NotificationModal', { + editTitle: 'Edit Notification Instance', + createTitle: 'Create Notification Instance', + createInstance: 'Create Instance', instanceName: 'Name', emailValidationSmtpHostRequired: 'You must provide a valid hostname or IP address', @@ -51,7 +55,7 @@ export function OpenPgpLink(msg: React.ReactNode) { } interface EmailModalProps { - title: string; + type: NotificationModalType; data: NotificationAgentEmail; onClose: () => void; onTest: (testData: NotificationAgentEmail) => void; @@ -59,7 +63,7 @@ interface EmailModalProps { } const EmailModal = ({ - title, + type, data, onClose, onTest, @@ -174,6 +178,11 @@ const EmailModal = ({ }} > {({ errors, touched, isSubmitting, values, isValid, handleSubmit }) => { + const title = + type === NotificationModalType.EDIT + ? `${intl.formatMessage(messages.editTitle)} #${data?.id}` + : intl.formatMessage(messages.createTitle); + return ( { handleSubmit(); diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/GotifyModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/GotifyModal.tsx index 6363795771..21f8e5a652 100644 --- a/src/components/Settings/SettingsNotifications/NotificationModal/GotifyModal.tsx +++ b/src/components/Settings/SettingsNotifications/NotificationModal/GotifyModal.tsx @@ -1,5 +1,6 @@ import Modal from '@app/components/Common/Modal'; import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import { NotificationModalType } from '@app/components/Settings/SettingsNotifications/NotificationModal'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { isValidURL } from '@app/utils/urlValidationHelper'; @@ -11,6 +12,9 @@ import * as Yup from 'yup'; const messages = defineMessages( 'components.Settings.SettingsNotifications.NotificationModal', { + editTitle: 'Edit Notification Instance', + createTitle: 'Create Notification Instance', + createInstance: 'Create Instance', instanceName: 'Name', gotifyUrl: 'Server URL', gotifyToken: 'Application Token', @@ -24,7 +28,7 @@ const messages = defineMessages( ); interface GotifyModalProps { - title: string; + type: NotificationModalType; data: NotificationAgentGotify; onClose: () => void; onTest: (testData: NotificationAgentGotify) => void; @@ -32,7 +36,7 @@ interface GotifyModalProps { } const GotifyModal = ({ - title, + type, data, onClose, onTest, @@ -119,6 +123,11 @@ const GotifyModal = ({ setFieldTouched, handleSubmit, }) => { + const title = + type === NotificationModalType.EDIT + ? `${intl.formatMessage(messages.editTitle)} #${data?.id}` + : intl.formatMessage(messages.createTitle); + return ( { handleSubmit(); diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/LunaSeaModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/LunaSeaModal.tsx index 266d738f5d..7a6f2384a9 100644 --- a/src/components/Settings/SettingsNotifications/NotificationModal/LunaSeaModal.tsx +++ b/src/components/Settings/SettingsNotifications/NotificationModal/LunaSeaModal.tsx @@ -1,5 +1,6 @@ import Modal from '@app/components/Common/Modal'; import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import { NotificationModalType } from '@app/components/Settings/SettingsNotifications/NotificationModal'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import type { NotificationAgentLunaSea } from '@server/interfaces/settings'; @@ -10,6 +11,9 @@ import * as Yup from 'yup'; const messages = defineMessages( 'components.Settings.SettingsNotifications.NotificationModal', { + editTitle: 'Edit Notification Instance', + createTitle: 'Create Notification Instance', + createInstance: 'Create Instance', instanceName: 'Name', lunaSeaWebhookUrl: 'Webhook URL', lunaSeaWebhookUrlTip: @@ -23,7 +27,7 @@ const messages = defineMessages( ); interface LunaSeaModalProps { - title: string; + type: NotificationModalType; data: NotificationAgentLunaSea; onClose: () => void; onTest: (testData: NotificationAgentLunaSea) => void; @@ -31,7 +35,7 @@ interface LunaSeaModalProps { } const LunaSeaModal = ({ - title, + type, data, onClose, onTest, @@ -89,6 +93,11 @@ const LunaSeaModal = ({ setFieldTouched, handleSubmit, }) => { + const title = + type === NotificationModalType.EDIT + ? `${intl.formatMessage(messages.editTitle)} #${data?.id}` + : intl.formatMessage(messages.createTitle); + return ( { handleSubmit(); diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/NtfyModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/NtfyModal.tsx index 1120f452b7..d45c94cd08 100644 --- a/src/components/Settings/SettingsNotifications/NotificationModal/NtfyModal.tsx +++ b/src/components/Settings/SettingsNotifications/NotificationModal/NtfyModal.tsx @@ -1,6 +1,7 @@ import Modal from '@app/components/Common/Modal'; import SensitiveInput from '@app/components/Common/SensitiveInput'; import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import { NotificationModalType } from '@app/components/Settings/SettingsNotifications/NotificationModal'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { isValidURL } from '@app/utils/urlValidationHelper'; @@ -12,6 +13,9 @@ import * as Yup from 'yup'; const messages = defineMessages( 'components.Settings.SettingsNotifications.NotificationModal', { + editTitle: 'Edit Notification Instance', + createTitle: 'Create Notification Instance', + createInstance: 'Create Instance', instanceName: 'Name', ntfyUrl: 'Server root URL', ntfyTopic: 'Topic', @@ -27,20 +31,14 @@ const messages = defineMessages( ); interface NtfyModalProps { - title: string; + type: NotificationModalType; data: NotificationAgentNtfy; onClose: () => void; onTest: (testData: NotificationAgentNtfy) => void; onSave: (submitData: NotificationAgentNtfy) => void; } -const NtfyModal = ({ - title, - data, - onClose, - onTest, - onSave, -}: NtfyModalProps) => { +const NtfyModal = ({ type, data, onClose, onTest, onSave }: NtfyModalProps) => { const intl = useIntl(); const NotificationsNtfySchema = Yup.object().shape({ @@ -116,6 +114,11 @@ const NtfyModal = ({ setFieldTouched, handleSubmit, }) => { + const title = + type === NotificationModalType.EDIT + ? `${intl.formatMessage(messages.editTitle)} #${data?.id}` + : intl.formatMessage(messages.createTitle); + return ( { handleSubmit(); diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/PushbulletModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/PushbulletModal.tsx index 294c4d943e..26f8d789c9 100644 --- a/src/components/Settings/SettingsNotifications/NotificationModal/PushbulletModal.tsx +++ b/src/components/Settings/SettingsNotifications/NotificationModal/PushbulletModal.tsx @@ -1,6 +1,7 @@ import Modal from '@app/components/Common/Modal'; import SensitiveInput from '@app/components/Common/SensitiveInput'; import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import { NotificationModalType } from '@app/components/Settings/SettingsNotifications/NotificationModal'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import type { NotificationAgentPushbullet } from '@server/interfaces/settings'; @@ -11,6 +12,9 @@ import * as Yup from 'yup'; const messages = defineMessages( 'components.Settings.SettingsNotifications.NotificationModal', { + editTitle: 'Edit Notification Instance', + createTitle: 'Create Notification Instance', + createInstance: 'Create Instance', instanceName: 'Name', pushbulletAccessToken: 'Access Token', pushbulletAccessTokenTip: @@ -22,7 +26,7 @@ const messages = defineMessages( ); interface PushbulletModalProps { - title: string; + type: NotificationModalType; data: NotificationAgentPushbullet; onClose: () => void; onTest: (testData: NotificationAgentPushbullet) => void; @@ -30,7 +34,7 @@ interface PushbulletModalProps { } const PushbulletModal = ({ - title, + type, data, onClose, onTest, @@ -88,6 +92,11 @@ const PushbulletModal = ({ setFieldTouched, handleSubmit, }) => { + const title = + type === NotificationModalType.EDIT + ? `${intl.formatMessage(messages.editTitle)} #${data?.id}` + : intl.formatMessage(messages.createTitle); + return ( { handleSubmit(); diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/PushoverModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/PushoverModal.tsx index b36079b69a..a20069fe27 100644 --- a/src/components/Settings/SettingsNotifications/NotificationModal/PushoverModal.tsx +++ b/src/components/Settings/SettingsNotifications/NotificationModal/PushoverModal.tsx @@ -1,5 +1,6 @@ import Modal from '@app/components/Common/Modal'; import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import { NotificationModalType } from '@app/components/Settings/SettingsNotifications/NotificationModal'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import type { PushoverSound } from '@server/api/pushover'; @@ -12,6 +13,9 @@ import * as Yup from 'yup'; const messages = defineMessages( 'components.Settings.SettingsNotifications.NotificationModal', { + editTitle: 'Edit Notification Instance', + createTitle: 'Create Notification Instance', + createInstance: 'Create Instance', instanceName: 'Name', pushoverAccessToken: 'Application API Token', pushoverAccessTokenTip: @@ -30,7 +34,7 @@ const messages = defineMessages( ); interface PushoverModalProps { - title: string; + type: NotificationModalType; data: NotificationAgentPushover; onClose: () => void; onTest: (testData: NotificationAgentPushover) => void; @@ -38,7 +42,7 @@ interface PushoverModalProps { } const PushoverModal = ({ - title, + type, data, onClose, onTest, @@ -122,6 +126,11 @@ const PushoverModal = ({ setFieldTouched, handleSubmit, }) => { + const title = + type === NotificationModalType.EDIT + ? `${intl.formatMessage(messages.editTitle)} #${data?.id}` + : intl.formatMessage(messages.createTitle); + return ( { handleSubmit(); diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/SlackModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/SlackModal.tsx index 267562943b..debe51f41c 100644 --- a/src/components/Settings/SettingsNotifications/NotificationModal/SlackModal.tsx +++ b/src/components/Settings/SettingsNotifications/NotificationModal/SlackModal.tsx @@ -1,5 +1,6 @@ import Modal from '@app/components/Common/Modal'; import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import { NotificationModalType } from '@app/components/Settings/SettingsNotifications/NotificationModal'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import type { NotificationAgentSlack } from '@server/interfaces/settings'; @@ -10,6 +11,9 @@ import * as Yup from 'yup'; const messages = defineMessages( 'components.Settings.SettingsNotifications.NotificationModal', { + editTitle: 'Edit Notification Instance', + createTitle: 'Create Notification Instance', + createInstance: 'Create Instance', instanceName: 'Name', slackWebhookUrl: 'Webhook URL', slackWebhookUrlTip: @@ -20,7 +24,7 @@ const messages = defineMessages( ); interface SlackModalProps { - title: string; + type: NotificationModalType; data: NotificationAgentSlack; onClose: () => void; onTest: (testData: NotificationAgentSlack) => void; @@ -28,7 +32,7 @@ interface SlackModalProps { } const SlackModal = ({ - title, + type, data, onClose, onTest, @@ -84,6 +88,11 @@ const SlackModal = ({ setFieldTouched, handleSubmit, }) => { + const title = + type === NotificationModalType.EDIT + ? `${intl.formatMessage(messages.editTitle)} #${data?.id}` + : intl.formatMessage(messages.createTitle); + return ( { handleSubmit(); diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/TelegramModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/TelegramModal.tsx index f3d0e9abfc..f1c651c271 100644 --- a/src/components/Settings/SettingsNotifications/NotificationModal/TelegramModal.tsx +++ b/src/components/Settings/SettingsNotifications/NotificationModal/TelegramModal.tsx @@ -1,6 +1,7 @@ import Modal from '@app/components/Common/Modal'; import SensitiveInput from '@app/components/Common/SensitiveInput'; import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import { NotificationModalType } from '@app/components/Settings/SettingsNotifications/NotificationModal'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import type { NotificationAgentTelegram } from '@server/interfaces/settings'; @@ -11,6 +12,9 @@ import * as Yup from 'yup'; const messages = defineMessages( 'components.Settings.SettingsNotifications.NotificationModal', { + editTitle: 'Edit Notification Instance', + createTitle: 'Create Notification Instance', + createInstance: 'Create Instance', instanceName: 'Name', telegramBotUsername: 'Bot Username', telegramBotUsernameTip: @@ -35,7 +39,7 @@ const messages = defineMessages( ); interface TelegramModalProps { - title: string; + type: NotificationModalType; data: NotificationAgentTelegram; onClose: () => void; onTest: (testData: NotificationAgentTelegram) => void; @@ -43,7 +47,7 @@ interface TelegramModalProps { } const TelegramModal = ({ - title, + type, data, onClose, onTest, @@ -135,6 +139,11 @@ const TelegramModal = ({ setFieldTouched, handleSubmit, }) => { + const title = + type === NotificationModalType.EDIT + ? `${intl.formatMessage(messages.editTitle)} #${data?.id}` + : intl.formatMessage(messages.createTitle); + return ( { handleSubmit(); diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/WebPushModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/WebPushModal.tsx index 3f8eb233e3..ca1566e5b5 100644 --- a/src/components/Settings/SettingsNotifications/NotificationModal/WebPushModal.tsx +++ b/src/components/Settings/SettingsNotifications/NotificationModal/WebPushModal.tsx @@ -1,5 +1,6 @@ import Alert from '@app/components/Common/Alert'; import Modal from '@app/components/Common/Modal'; +import { NotificationModalType } from '@app/components/Settings/SettingsNotifications/NotificationModal'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import type { NotificationAgentConfig } from '@server/interfaces/settings'; @@ -10,6 +11,9 @@ import { useIntl } from 'react-intl'; const messages = defineMessages( 'components.Settings.SettingsNotifications.NotificationModal', { + editTitle: 'Edit Notification Instance', + createTitle: 'Create Notification Instance', + createInstance: 'Create Instance', instanceName: 'Name', webPushHttpsRequirement: 'In order to receive web push notifications, Jellyseerr must be served over HTTPS.', @@ -17,7 +21,7 @@ const messages = defineMessages( ); interface WebPushModalProps { - title: string; + type: NotificationModalType; data: NotificationAgentConfig; onClose: () => void; onTest: (testData: NotificationAgentConfig) => void; @@ -25,7 +29,7 @@ interface WebPushModalProps { } const WebPushModal = ({ - title, + type, data, onClose, onTest, @@ -66,6 +70,11 @@ const WebPushModal = ({ }} > {({ values, isSubmitting, handleSubmit }) => { + const title = + type === NotificationModalType.EDIT + ? `${intl.formatMessage(messages.editTitle)} #${data?.id}` + : intl.formatMessage(messages.createTitle); + return ( { handleSubmit(); diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/WebhookModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/WebhookModal.tsx index 960abb3aac..9ff0eb3000 100644 --- a/src/components/Settings/SettingsNotifications/NotificationModal/WebhookModal.tsx +++ b/src/components/Settings/SettingsNotifications/NotificationModal/WebhookModal.tsx @@ -1,6 +1,7 @@ import Button from '@app/components/Common/Button'; import Modal from '@app/components/Common/Modal'; import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import { NotificationModalType } from '@app/components/Settings/SettingsNotifications/NotificationModal'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { isValidURL } from '@app/utils/urlValidationHelper'; @@ -68,6 +69,9 @@ const defaultPayload = { const messages = defineMessages( 'components.Settings.SettingsNotifications.NotificationModal', { + editTitle: 'Edit Notification Instance', + createTitle: 'Create Notification Instance', + createInstance: 'Create Instance', instanceName: 'Name', webhookUrl: 'Webhook URL', webhookAuthheader: 'Authorization Header', @@ -83,7 +87,7 @@ const messages = defineMessages( ); interface WebhookModalProps { - title: string; + type: NotificationModalType; data: NotificationAgentWebhook; onClose: () => void; onTest: (testData: NotificationAgentWebhook) => void; @@ -91,7 +95,7 @@ interface WebhookModalProps { } const WebhookModal = ({ - title, + type, data, onClose, onTest, @@ -178,6 +182,11 @@ const WebhookModal = ({ setFieldTouched, handleSubmit, }) => { + const title = + type === NotificationModalType.EDIT + ? `${intl.formatMessage(messages.editTitle)} #${data?.id}` + : intl.formatMessage(messages.createTitle); + const resetPayload = () => { setFieldValue( 'jsonPayload', @@ -215,7 +224,9 @@ const WebhookModal = ({ okText={ isSubmitting ? intl.formatMessage(globalMessages.saving) - : intl.formatMessage(globalMessages.save) + : type === NotificationModalType.EDIT + ? intl.formatMessage(globalMessages.save) + : intl.formatMessage(messages.createInstance) } onOk={() => { handleSubmit(); diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/index.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/index.tsx index fb3dcd0d3d..6796f20573 100644 --- a/src/components/Settings/SettingsNotifications/NotificationModal/index.tsx +++ b/src/components/Settings/SettingsNotifications/NotificationModal/index.tsx @@ -33,6 +33,7 @@ const messages = defineMessages( { editTitle: 'Edit Notification Instance', createTitle: 'Create Notification Instance', + createInstance: 'Create Instance', toastTestSending: 'Sending test notification…', toastTestSuccess: 'Test notification sent!', toastTestFailed: 'Test notification failed to send.', @@ -41,13 +42,20 @@ const messages = defineMessages( } ); +export enum NotificationModalType { + EDIT = 'edit', + CREATE = 'create', +} + interface NotificationModalProps { - data: NotificationAgentConfig | null; + type: NotificationModalType; + data?: NotificationAgentConfig; afterSave: () => void; onClose: () => void; } const NotificationModal = ({ + type, data, afterSave, onClose, @@ -109,13 +117,11 @@ const NotificationModal = ({ } }; - const editTitle = `${intl.formatMessage(messages.editTitle)} #${data?.id}`; - switch (data?.agent) { case NotificationAgentKey.DISCORD: return ( { const [selectedInstances, setSelectedInstances] = useState([]); const [notificationModal, setNotificationModal] = useState<{ open: boolean; - instance: NotificationAgentConfig | null; + type: NotificationModalType; + instance?: NotificationAgentConfig; }>({ open: false, - instance: null, + type: NotificationModalType.EDIT, }); const page = router.query.page ? Number(router.query.page) : 1; @@ -201,13 +218,20 @@ const SettingsNotifications = () => { show={notificationModal.open} > { revalidate(); - setNotificationModal({ open: false, instance: null }); + setNotificationModal({ + open: false, + type: NotificationModalType.EDIT, + }); }} onClose={() => { - setNotificationModal({ open: false, instance: null }); + setNotificationModal({ + open: false, + type: NotificationModalType.EDIT, + }); }} /> @@ -216,15 +240,150 @@ const SettingsNotifications = () => {
{intl.formatMessage(messages.notificationInstanceList)}
-
- {/**/} + + WebPush + +
@@ -311,7 +470,11 @@ const SettingsNotifications = () => { buttonType="warning" className="mr-2" onClick={() => - setNotificationModal({ open: true, instance: instance }) + setNotificationModal({ + open: true, + instance: instance, + type: NotificationModalType.EDIT, + }) } > diff --git a/src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx b/src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx index ae7e634255..ea8ebd2ed2 100644 --- a/src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx @@ -25,6 +25,7 @@ const messages = defineMessages( linkedAccounts: 'Linked Accounts', linkedAccountsHint: 'These external accounts are linked to your {applicationName} account.', + linkAccount: 'Link Account', noLinkedAccounts: 'You do not have any external accounts linked to your account.', noPermissionDescription: @@ -194,7 +195,10 @@ const UserLinkedAccountsSettings = () => {
{currentUser?.id === user?.id && !!linkable.length && (
- + {intl.formatMessage(messages.linkAccount)}} + buttonType="ghost" + > {linkable.map(({ name, action }) => ( {name} From e4db59a041b98e3a7d5b462b7e2b28fa13f4a383 Mon Sep 17 00:00:00 2001 From: Schrottfresser <39998368+Schrottfresser@users.noreply.github.com> Date: Sun, 18 May 2025 12:08:37 +0200 Subject: [PATCH 32/62] fix(notifications): move web push alert into modal --- .../NotificationModal/WebPushModal.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/Settings/SettingsNotifications/NotificationModal/WebPushModal.tsx b/src/components/Settings/SettingsNotifications/NotificationModal/WebPushModal.tsx index ca1566e5b5..67f5e042d1 100644 --- a/src/components/Settings/SettingsNotifications/NotificationModal/WebPushModal.tsx +++ b/src/components/Settings/SettingsNotifications/NotificationModal/WebPushModal.tsx @@ -44,12 +44,6 @@ const WebPushModal = ({ return ( <> - {!isHttps && ( - - )} + {!isHttps && ( + + )} +