diff --git a/docs/using-overseerr/notifications/webhooks.md b/docs/using-overseerr/notifications/webhooks.md index 1a30511a..b6c90974 100644 --- a/docs/using-overseerr/notifications/webhooks.md +++ b/docs/using-overseerr/notifications/webhooks.md @@ -45,8 +45,8 @@ These variables are only included in media related notifications, such as reques - `{{media_tmdbid}}` Media's TMDb ID. - `{{media_imdbid}}` Media's IMDb ID. - `{{media_tvdbid}}` Media's TVDB ID. -- `{{media_status}}` Media's availability status. (Ex. `AVAILABLE` or `PENDING`) -- `{{media_status4k}}` Media's 4K availability status. (Ex. `AVAILABLE` or `PENDING`) +- `{{media_status}}` Media's availability status (e.g., `AVAILABLE` or `PENDING`). +- `{{media_status4k}}` Media's 4K availability status (e.g., `AVAILABLE` or `PENDING`). ### Special Key Variables @@ -54,3 +54,4 @@ These variables must be used as a key in the JSON Payload. (Ex, `"{{extra}}": [] - `{{extra}}` This will override the value of the property to be the pre-formatted "extra" array that can come along with certain notifications. Using this variable is _not required_. - `{{media}}` This will override the value of the property to `null` if there is no media object passed along with the notification. +- `{{request}}` This will override the value of the property to `null` if there is no request object passed along with the notification. diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index 02c29acf..206d2d39 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -111,6 +111,7 @@ export class MediaRequest { image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, notifyUser: this.requestedBy, media, + request: this, }); } @@ -130,6 +131,7 @@ export class MediaRequest { .join(', '), }, ], + request: this, }); } } @@ -177,6 +179,7 @@ export class MediaRequest { image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, notifyUser: this.requestedBy, media, + request: this, } ); } else if (this.media.mediaType === MediaType.TV) { @@ -199,6 +202,7 @@ export class MediaRequest { .join(', '), }, ], + request: this, } ); } @@ -454,6 +458,7 @@ export class MediaRequest { notifyUser: admin, media, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, + request: this, }); }); logger.info('Sent request to Radarr', { label: 'Media Request' }); @@ -659,6 +664,7 @@ export class MediaRequest { .join(', '), }, ], + request: this, }); }); logger.info('Sent request to Sonarr', { label: 'Media Request' }); diff --git a/server/lib/notifications/agents/agent.ts b/server/lib/notifications/agents/agent.ts index 98daf106..12933f8e 100644 --- a/server/lib/notifications/agents/agent.ts +++ b/server/lib/notifications/agents/agent.ts @@ -1,5 +1,6 @@ import { Notification } from '..'; import Media from '../../../entity/Media'; +import { MediaRequest } from '../../../entity/MediaRequest'; import { User } from '../../../entity/User'; import { NotificationAgentConfig } from '../../settings'; @@ -10,6 +11,7 @@ export interface NotificationPayload { image?: string; message?: string; extra?: { name: string; value: string }[]; + request?: MediaRequest; } export abstract class BaseAgent { diff --git a/server/lib/notifications/agents/discord.ts b/server/lib/notifications/agents/discord.ts index 81558cdf..86803727 100644 --- a/server/lib/notifications/agents/discord.ts +++ b/server/lib/notifications/agents/discord.ts @@ -98,106 +98,64 @@ class DiscordAgent const fields: Field[] = []; + if (payload.request) { + fields.push({ + name: 'Requested By', + value: payload.notifyUser.displayName ?? '', + inline: true, + }); + } + switch (type) { case Notification.MEDIA_PENDING: color = EmbedColors.ORANGE; - fields.push( - { - name: 'Requested By', - value: payload.notifyUser.displayName ?? '', - inline: true, - }, - { - name: 'Status', - value: 'Pending Approval', - inline: true, - } - ); - - if (settings.main.applicationUrl) { - fields.push({ - name: 'View Media', - value: `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`, - }); - } + fields.push({ + name: 'Status', + value: 'Pending Approval', + inline: true, + }); break; case Notification.MEDIA_APPROVED: color = EmbedColors.PURPLE; - fields.push( - { - name: 'Requested By', - value: payload.notifyUser.displayName ?? '', - inline: true, - }, - { - name: 'Status', - value: 'Processing Request', - inline: true, - } - ); - - if (settings.main.applicationUrl) { - fields.push({ - name: 'View Media', - value: `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`, - }); - } + fields.push({ + name: 'Status', + value: 'Processing', + inline: true, + }); break; case Notification.MEDIA_AVAILABLE: color = EmbedColors.GREEN; - fields.push( - { - name: 'Requested By', - value: payload.notifyUser.displayName ?? '', - inline: true, - }, - { - name: 'Status', - value: 'Available', - inline: true, - } - ); - - if (settings.main.applicationUrl) { - fields.push({ - name: 'View Media', - value: `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`, - }); - } + fields.push({ + name: 'Status', + value: 'Available', + inline: true, + }); break; case Notification.MEDIA_DECLINED: color = EmbedColors.RED; - fields.push( - { - name: 'Requested By', - value: payload.notifyUser.displayName ?? '', - inline: true, - }, - { - name: 'Status', - value: 'Declined', - inline: true, - } - ); - - if (settings.main.applicationUrl) { - fields.push({ - name: 'View Media', - value: `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`, - }); - } + fields.push({ + name: 'Status', + value: 'Declined', + inline: true, + }); break; case Notification.MEDIA_FAILED: color = EmbedColors.RED; - if (settings.main.applicationUrl) { - fields.push({ - name: 'View Media', - value: `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`, - }); - } + fields.push({ + name: 'Status', + value: 'Failed', + inline: true, + }); break; } + if (settings.main.applicationUrl && payload.media) { + fields.push({ + name: `Open in ${settings.main.applicationTitle}`, + value: `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`, + }); + } + return { title: payload.subject, description: payload.message, diff --git a/server/lib/notifications/agents/pushover.ts b/server/lib/notifications/agents/pushover.ts index 52f538aa..19c6d6d9 100644 --- a/server/lib/notifications/agents/pushover.ts +++ b/server/lib/notifications/agents/pushover.ts @@ -9,6 +9,9 @@ interface PushoverPayload { user: string; title: string; message: string; + url: string; + url_title: string; + priority: number; html: number; } @@ -41,10 +44,19 @@ class PushoverAgent private constructMessageDetails( type: Notification, payload: NotificationPayload - ): { title: string; message: string } { + ): { + title: string; + message: string; + url: string | undefined; + url_title: string | undefined; + priority: number; + } { const settings = getSettings(); let messageTitle = ''; let message = ''; + let url: string | undefined; + let url_title: string | undefined; + let priority = 0; const title = payload.subject; const plot = payload.message; @@ -53,45 +65,69 @@ class PushoverAgent switch (type) { case Notification.MEDIA_PENDING: messageTitle = 'New Request'; - message += `${title}\n\n`; - message += `${plot}\n\n`; - message += `Requested By\n${username}\n\n`; - message += `Status\nPending Approval\n`; + message += `${title}`; + if (plot) { + message += `\n${plot}`; + } + message += `\n\nRequested By\n${username}`; + message += `\n\nStatus\nPending Approval`; break; case Notification.MEDIA_APPROVED: messageTitle = 'Request Approved'; - message += `${title}\n\n`; - message += `${plot}\n\n`; - message += `Requested By\n${username}\n\n`; - message += `Status\nProcessing Request\n`; + message += `${title}`; + if (plot) { + message += `\n${plot}`; + } + message += `\n\nRequested By\n${username}`; + message += `\n\nStatus\nProcessing`; break; case Notification.MEDIA_AVAILABLE: messageTitle = 'Now Available'; - message += `${title}\n\n`; - message += `${plot}\n\n`; - message += `Requested By\n${username}\n\n`; - message += `Status\nAvailable\n`; + message += `${title}`; + if (plot) { + message += `\n${plot}`; + } + message += `\n\nRequested By\n${username}`; + message += `\n\nStatus\nAvailable`; break; case Notification.MEDIA_DECLINED: messageTitle = 'Request Declined'; - message += `${title}\n\n`; - message += `${plot}\n\n`; - message += `Requested By\n${username}\n\n`; - message += `Status\nDeclined\n`; + message += `${title}`; + if (plot) { + message += `\n${plot}`; + } + message += `\n\nRequested By\n${username}`; + message += `\n\nStatus\nDeclined`; + priority = 1; + break; + case Notification.MEDIA_FAILED: + messageTitle = 'Failed Request'; + message += `${title}`; + if (plot) { + message += `\n${plot}`; + } + message += `\n\nRequested By\n${username}`; + message += `\n\nStatus\nFailed`; + priority = 1; break; case Notification.TEST_NOTIFICATION: messageTitle = 'Test Notification'; - message += `${plot}\n\n`; - message += `Requested By\n${username}\n`; + message += `${plot}`; break; } if (settings.main.applicationUrl && payload.media) { - const actionUrl = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`; - message += `Open in ${settings.main.applicationTitle}`; + url = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`; + url_title = `Open in ${settings.main.applicationTitle}`; } - return { title: messageTitle, message }; + return { + title: messageTitle, + message, + url, + url_title, + priority, + }; } public async send( @@ -104,13 +140,22 @@ class PushoverAgent const { accessToken, userToken } = this.getSettings().options; - const { title, message } = this.constructMessageDetails(type, payload); + const { + title, + message, + url, + url_title, + priority, + } = this.constructMessageDetails(type, payload); await axios.post(endpoint, { token: accessToken, user: userToken, title: title, message: message, + url: url, + url_title: url_title, + priority: priority, html: 1, } as PushoverPayload); diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index 318bbfeb..70a527f1 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -58,79 +58,63 @@ class SlackAgent payload: NotificationPayload ): SlackBlockEmbed { const settings = getSettings(); - let header = settings.main.applicationTitle; + let header = ''; let actionUrl: string | undefined; const fields: EmbedField[] = []; + if (payload.request) { + fields.push({ + type: 'mrkdwn', + text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`, + }); + } + switch (type) { case Notification.MEDIA_PENDING: header = 'New Request'; - fields.push( - { - type: 'mrkdwn', - text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`, - }, - { - type: 'mrkdwn', - text: '*Status*\nPending Approval', - } - ); - if (settings.main.applicationUrl) { - actionUrl = `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`; - } + fields.push({ + type: 'mrkdwn', + text: '*Status*\nPending Approval', + }); break; case Notification.MEDIA_APPROVED: header = 'Request Approved'; - fields.push( - { - type: 'mrkdwn', - text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`, - }, - { - type: 'mrkdwn', - text: '*Status*\nProcessing Request', - } - ); - if (settings.main.applicationUrl) { - actionUrl = `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`; - } + fields.push({ + type: 'mrkdwn', + text: '*Status*\nProcessing', + }); + break; + case Notification.MEDIA_AVAILABLE: + header = 'Now Available'; + fields.push({ + type: 'mrkdwn', + text: '*Status*\nAvailable', + }); break; case Notification.MEDIA_DECLINED: header = 'Request Declined'; - fields.push( - { - type: 'mrkdwn', - text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`, - }, - { - type: 'mrkdwn', - text: '*Status*\nDeclined', - } - ); - if (settings.main.applicationUrl) { - actionUrl = `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`; - } + fields.push({ + type: 'mrkdwn', + text: '*Status*\nDeclined', + }); break; - case Notification.MEDIA_AVAILABLE: - header = 'Now available!'; - fields.push( - { - type: 'mrkdwn', - text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`, - }, - { - type: 'mrkdwn', - text: '*Status*\nAvailable', - } - ); - - if (settings.main.applicationUrl) { - actionUrl = `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`; - } + case Notification.MEDIA_FAILED: + header = 'Failed Request'; + fields.push({ + type: 'mrkdwn', + text: '*Status*\nFailed', + }); + break; + case Notification.TEST_NOTIFICATION: + header = 'Test Notification'; break; } + if (settings.main.applicationUrl && payload.media) { + actionUrl = `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`; + } + const blocks: EmbedBlock[] = [ { type: 'header', @@ -139,14 +123,17 @@ class SlackAgent text: header, }, }, - { + ]; + + if (type !== Notification.TEST_NOTIFICATION) { + blocks.push({ type: 'section', text: { type: 'mrkdwn', text: `*${payload.subject}*`, }, - }, - ]; + }); + } if (payload.message) { blocks.push({ @@ -191,7 +178,7 @@ class SlackAgent value: 'open_overseerr', text: { type: 'plain_text', - text: `Open ${settings.main.applicationTitle}`, + text: `Open in ${settings.main.applicationTitle}`, }, }, ], diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index 2e08cbdf..b27b68d5 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -8,6 +8,7 @@ interface TelegramPayload { text: string; parse_mode: string; chat_id: string; + disable_notification: boolean; } class TelegramAgent @@ -56,49 +57,59 @@ class TelegramAgent /* eslint-disable no-useless-escape */ switch (type) { case Notification.MEDIA_PENDING: - message += `\*New Request\*\n`; - message += `${title}\n\n`; - message += `${plot}\n\n`; - message += `\*Requested By\*\n${user}\n\n`; - message += `\*Status\*\nPending Approval\n`; - + message += `\*New Request\*`; + message += `\n\n\*${title}\*`; + if (plot) { + message += `\n${plot}`; + } + message += `\n\n\*Requested By\*\n${user}`; + message += `\n\n\*Status\*\nPending Approval`; break; case Notification.MEDIA_APPROVED: - message += `\*Request Approved\*\n`; - message += `${title}\n\n`; - message += `${plot}\n\n`; - message += `\*Requested By\*\n${user}\n\n`; - message += `\*Status\*\nProcessing Request\n`; - + message += `\*Request Approved\*`; + message += `\n\n\*${title}\*`; + if (plot) { + message += `\n${plot}`; + } + message += `\n\n\*Requested By\*\n${user}`; + message += `\n\n\*Status\*\nProcessing`; + break; + case Notification.MEDIA_AVAILABLE: + message += `\*Now Available\*`; + message += `\n\n\*${title}\*`; + if (plot) { + message += `\n${plot}\n\n`; + } + message += `\n\n\*Requested By\*\n${user}`; + message += `\n\n\*Status\*\nAvailable`; break; case Notification.MEDIA_DECLINED: - message += `\*Request Declined\*\n`; - message += `${title}\n\n`; - message += `${plot}\n\n`; - message += `\*Requested By\*\n${user}\n\n`; - message += `\*Status\*\nDeclined\n`; - + message += `\*Request Declined\*`; + message += `\n\n\*${title}\*`; + if (plot) { + message += `\n${plot}`; + } + message += `\n\n\*Requested By\*\n${user}`; + message += `\n\n\*Status\*\nDeclined`; break; - case Notification.MEDIA_AVAILABLE: - message += `\*Now available\\!\*\n`; - message += `${title}\n\n`; - message += `${plot}\n\n`; - message += `\*Requested By\*\n${user}\n\n`; - message += `\*Status\*\nAvailable\n`; - + case Notification.MEDIA_FAILED: + message += `\*Failed Request\*`; + message += `\n\n\*${title}\*`; + if (plot) { + message += `\n${plot}`; + } + message += `\n\n\*Requested By\*\n${user}`; + message += `\n\n\*Status\*\nFailed`; break; case Notification.TEST_NOTIFICATION: - message += `\*Test Notification\*\n`; - message += `${title}\n\n`; - message += `${plot}\n\n`; - message += `\*Requested By\*\n${user}\n`; - + message += `\*Test Notification\*`; + message += `\n\n${plot}`; break; } if (settings.main.applicationUrl && payload.media) { const actionUrl = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`; - message += `\[Open in ${settings.main.applicationTitle}\]\(${actionUrl}\)`; + message += `\n\n\[Open in ${settings.main.applicationTitle}\]\(${actionUrl}\)`; } /* eslint-enable */ @@ -119,6 +130,7 @@ class TelegramAgent text: this.buildMessage(type, payload), parse_mode: 'MarkdownV2', chat_id: `${this.getSettings().options.chatId}`, + disable_notification: this.getSettings().options.sendSilently, } as TelegramPayload); return true; diff --git a/server/lib/notifications/agents/webhook.ts b/server/lib/notifications/agents/webhook.ts index 796593da..36df0c94 100644 --- a/server/lib/notifications/agents/webhook.ts +++ b/server/lib/notifications/agents/webhook.ts @@ -27,6 +27,7 @@ const KeyMap: Record = { payload.media?.status ? MediaStatus[payload.media?.status] : '', media_status4k: (payload) => payload.media?.status ? MediaStatus[payload.media?.status4k] : '', + request_id: 'request.id', }; class WebhookAgent @@ -60,6 +61,14 @@ class WebhookAgent } delete finalPayload[key]; key = 'media'; + } else if (key === '{{request}}') { + if (payload.request) { + finalPayload.request = finalPayload[key]; + } else { + finalPayload.request = null; + } + delete finalPayload[key]; + key = 'request'; } if (typeof finalPayload[key] === 'string') { diff --git a/server/lib/settings.ts b/server/lib/settings.ts index be09d45d..3d168497 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -107,6 +107,7 @@ export interface NotificationAgentTelegram extends NotificationAgentConfig { options: { botAPI: string; chatId: string; + sendSilently: boolean; }; } @@ -220,6 +221,7 @@ class Settings { options: { botAPI: '', chatId: '', + sendSilently: false, }, }, pushover: { diff --git a/server/subscriber/MediaSubscriber.ts b/server/subscriber/MediaSubscriber.ts index 8414d9a9..b434f6c0 100644 --- a/server/subscriber/MediaSubscriber.ts +++ b/server/subscriber/MediaSubscriber.ts @@ -35,6 +35,7 @@ export class MediaSubscriber implements EntitySubscriberInterface { message: movie.overview, media: entity, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, + request: request, }); }); } @@ -96,6 +97,7 @@ export class MediaSubscriber implements EntitySubscriberInterface { .join(', '), }, ], + request: request, }); } } diff --git a/src/components/Settings/Notifications/NotificationsEmail.tsx b/src/components/Settings/Notifications/NotificationsEmail.tsx index 62e17c46..f7080afa 100644 --- a/src/components/Settings/Notifications/NotificationsEmail.tsx +++ b/src/components/Settings/Notifications/NotificationsEmail.tsx @@ -204,12 +204,10 @@ const NotificationsEmail: React.FC = () => {
diff --git a/src/components/Settings/Notifications/NotificationsTelegram.tsx b/src/components/Settings/Notifications/NotificationsTelegram.tsx index 912ee8b3..b0503059 100644 --- a/src/components/Settings/Notifications/NotificationsTelegram.tsx +++ b/src/components/Settings/Notifications/NotificationsTelegram.tsx @@ -28,6 +28,8 @@ const messages = defineMessages({ Additionally, you need the chat ID for the chat you want the bot to send notifications to.\ You can do this by adding @get_id_bot to the chat or group chat.', notificationtypes: 'Notification Types', + sendSilently: 'Send Silently', + sendSilentlyTip: 'Send notifications with no sound', }); const NotificationsTelegram: React.FC = () => { @@ -57,6 +59,7 @@ const NotificationsTelegram: React.FC = () => { types: data?.types, botAPI: data?.options.botAPI, chatId: data?.options.chatId, + sendSilently: data?.options.sendSilently, }} validationSchema={NotificationsTelegramSchema} onSubmit={async (values) => { @@ -67,6 +70,7 @@ const NotificationsTelegram: React.FC = () => { options: { botAPI: values.botAPI, chatId: values.chatId, + sendSilently: values.sendSilently, }, }); addToast(intl.formatMessage(messages.telegramsettingssaved), { @@ -91,6 +95,7 @@ const NotificationsTelegram: React.FC = () => { options: { botAPI: values.botAPI, chatId: values.chatId, + sendSilently: values.sendSilently, }, }); @@ -178,6 +183,21 @@ const NotificationsTelegram: React.FC = () => { )}
+
+ +
+ +
+
create a bot and get the bot API key. Additionally, you need the chat ID for the chat you want the bot to send notifications to. You can do this by adding @get_id_bot to the chat or group chat.",