From 17172e93f9b2034d39b074d7d32d7b981fd0cb15 Mon Sep 17 00:00:00 2001 From: 0xsysr3ll <31414959+0xSysR3ll@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:20:58 +0200 Subject: [PATCH] feat(webhook): add support for dynamic placeholders in webhook URL (#1491) * feat(wehbook): add support for dynamic placeholders in webhook URL * refactor(webhook): rename supportPlaceholders to supportVariables and update related logic Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me> * feat(i18n): add missing translations Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me> * refactor(notifications): simplify webhook URL validation logic Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me> * fix: wrong docs url Co-authored-by: Gauthier * fix: update webhook documentation URL to point to Jellyseerr Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me> --------- Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me> Co-authored-by: Gauthier --- jellyseerr-api.yml | 3 + server/lib/notifications/agents/webhook.ts | 20 +++++- server/lib/settings/index.ts | 1 + server/routes/settings/notifications.ts | 3 + .../NotificationsWebhook/index.tsx | 68 ++++++++++++++++++- src/i18n/locale/en.json | 3 + 6 files changed, 95 insertions(+), 3 deletions(-) diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index 752b85f8..2535059a 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -1451,6 +1451,9 @@ components: type: string jsonPayload: type: string + supportVariables: + type: boolean + example: false TelegramSettings: type: object properties: diff --git a/server/lib/notifications/agents/webhook.ts b/server/lib/notifications/agents/webhook.ts index c441cb65..d8ddcb23 100644 --- a/server/lib/notifications/agents/webhook.ts +++ b/server/lib/notifications/agents/webhook.ts @@ -177,9 +177,27 @@ class WebhookAgent subject: payload.subject, }); + let webhookUrl = settings.options.webhookUrl; + + if (settings.options.supportVariables) { + Object.keys(KeyMap).forEach((keymapKey) => { + const keymapValue = KeyMap[keymapKey as keyof typeof KeyMap]; + const variableValue = + type === Notification.TEST_NOTIFICATION + ? 'test' + : typeof keymapValue === 'function' + ? keymapValue(payload, type) + : get(payload, keymapValue) || 'test'; + webhookUrl = webhookUrl.replace( + new RegExp(`{{${keymapKey}}}`, 'g'), + encodeURIComponent(variableValue) + ); + }); + } + try { await axios.post( - settings.options.webhookUrl, + webhookUrl, this.buildPayload(type, payload), settings.options.authHeader ? { diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 29c7ed04..ce3e4b24 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -275,6 +275,7 @@ export interface NotificationAgentWebhook extends NotificationAgentConfig { webhookUrl: string; jsonPayload: string; authHeader?: string; + supportVariables?: boolean; }; } diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index 122ef017..5984e538 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -279,6 +279,7 @@ notificationRoutes.get('/webhook', (_req, res) => { 'utf8' ) ), + supportVariables: webhookSettings.options.supportVariables ?? false, }, }; @@ -300,6 +301,7 @@ notificationRoutes.post('/webhook', async (req, res, next) => { ), webhookUrl: req.body.options.webhookUrl, authHeader: req.body.options.authHeader, + supportVariables: req.body.options.supportVariables ?? false, }, }; await settings.save(); @@ -331,6 +333,7 @@ notificationRoutes.post('/webhook/test', async (req, res, next) => { ), webhookUrl: req.body.options.webhookUrl, authHeader: req.body.options.authHeader, + supportVariables: req.body.options.supportVariables ?? false, }, }; diff --git a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx index 0595090f..2c3d436d 100644 --- a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx +++ b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx @@ -1,6 +1,7 @@ import Button from '@app/components/Common/Button'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; +import SettingsBadge from '@app/components/Settings/SettingsBadge'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { isValidURL } from '@app/utils/urlValidationHelper'; @@ -73,6 +74,11 @@ const messages = defineMessages( { agentenabled: 'Enable Agent', webhookUrl: 'Webhook URL', + webhookUrlTip: + 'Test Notification URL is set to {testUrl} instead of the actual webhook URL.', + supportVariables: 'Support URL Variables', + supportVariablesTip: + 'Available variables are documented in the webhook template variables section', authheader: 'Authorization Header', validationJsonPayloadRequired: 'You must provide a valid JSON payload', webhooksettingssaved: 'Webhook notification settings saved successfully!', @@ -111,8 +117,14 @@ const NotificationsWebhook = () => { .test( 'valid-url', intl.formatMessage(messages.validationWebhookUrl), - isValidURL + function (value) { + const { supportVariables } = this.parent; + return supportVariables || isValidURL(value); + } ), + + supportVariables: Yup.boolean(), + jsonPayload: Yup.string() .when('enabled', { is: true, @@ -147,6 +159,7 @@ const NotificationsWebhook = () => { webhookUrl: data.options.webhookUrl, jsonPayload: data.options.jsonPayload, authHeader: data.options.authHeader, + supportVariables: data.options.supportVariables ?? false, }} validationSchema={NotificationsWebhookSchema} onSubmit={async (values) => { @@ -158,6 +171,7 @@ const NotificationsWebhook = () => { webhookUrl: values.webhookUrl, jsonPayload: JSON.stringify(values.jsonPayload), authHeader: values.authHeader, + supportVariables: values.supportVariables, }, }); addToast(intl.formatMessage(messages.webhooksettingssaved), { @@ -215,6 +229,7 @@ const NotificationsWebhook = () => { webhookUrl: values.webhookUrl, jsonPayload: JSON.stringify(values.jsonPayload), authHeader: values.authHeader, + supportVariables: values.supportVariables ?? false, }, }); @@ -249,10 +264,59 @@ const NotificationsWebhook = () => { +
+ +
+ ) => + setFieldValue('supportVariables', e.target.checked) + } + /> +
+
+ {values.supportVariables && ( +
+ + + +
+ )}
@@ -312,7 +376,7 @@ const NotificationsWebhook = () => { {intl.formatMessage(messages.resetPayload)} diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 63a207c1..7153d5a3 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -683,6 +683,8 @@ "components.Settings.Notifications.NotificationsWebhook.customJson": "JSON Payload", "components.Settings.Notifications.NotificationsWebhook.resetPayload": "Reset to Default", "components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "JSON payload reset successfully!", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "Support URL Variables", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "Available variables are documented in the webhook template variables section", "components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "Template Variable Help", "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestFailed": "Webhook test notification failed to send.", "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "Sending webhook test notification…", @@ -691,6 +693,7 @@ "components.Settings.Notifications.NotificationsWebhook.validationTypes": "You must select at least one notification type", "components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "You must provide a valid URL", "components.Settings.Notifications.NotificationsWebhook.webhookUrl": "Webhook URL", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "Test Notification URL is set to {testUrl} instead of the actual webhook URL.", "components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "Webhook notification settings failed to save.", "components.Settings.Notifications.NotificationsWebhook.webhooksettingssaved": "Webhook notification settings saved successfully!", "components.Settings.Notifications.NotificationsWebPush.agentenabled": "Enable Agent",