diff --git a/docs/using-overseerr/notifications/webhooks.md b/docs/using-overseerr/notifications/webhooks.md index c1637480..686f82be 100644 --- a/docs/using-overseerr/notifications/webhooks.md +++ b/docs/using-overseerr/notifications/webhooks.md @@ -24,23 +24,28 @@ Customize the JSON payload to suit your needs. Overseerr provides several [templ ### General -- `{{notification_type}}` The type of notification. (Ex. `MEDIA_PENDING` or `MEDIA_APPROVED`) -- `{{subject}}` The notification subject message. (For request notifications, this is the media title) -- `{{message}}` Notification message body. (For request notifications, this is the media's overview/synopsis) -- `{{image}}` Associated image with the request. (For request notifications, this is the media's poster) +| Variable | Value | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| `{{notification_type}}` | The type of notification (e.g. `MEDIA_PENDING` or `ISSUE_COMMENT`) | +| `{{event}}` | A friendly description of the notification event | +| `{{subject}}` | The notification subject (typically the media title) | +| `{{message}}` | The notification message body (the media overview/synopsis for request notifications; the issue description for issue notificatons) | +| `{{image}}` | The notification image (typically the media poster) | -### User +### Notify User These variables are for the target recipient of the notification. -- `{{notifyuser_username}}` Target user's username. -- `{{notifyuser_email}}` Target user's email address. -- `{{notifyuser_avatar}}` Target user's avatar URL. -- `{{notifyuser_settings_discordId}}` Target user's Discord ID (if one is set). -- `{{notifyuser_settings_telegramChatId}}` Target user's Telegram Chat ID (if one is set). +| Variable | Value | +| ---------------------------------------- | ------------------------------------------------------------- | +| `{{notifyuser_username}}` | The target notification recipient's username | +| `{{notifyuser_email}}` | The target notification recipient's email address | +| `{{notifyuser_avatar}}` | The target notification recipient's avatar URL | +| `{{notifyuser_settings_discordId}}` | The target notification recipient's Discord ID (if set) | +| `{{notifyuser_settings_telegramChatId}}` | The target notification recipient's Telegram Chat ID (if set) | {% hint style="info" %} -The `notifyuser` variables are not set for the following notification types, as they are intended for application administrators rather than end users: +The `notifyuser` variables are not defined for the following request notification types, as they are intended for application administrators rather than end users: - Media Requested - Media Automatically Approved @@ -59,28 +64,69 @@ If you would like to use the requesting user's information in your webhook, plea The following variables must be used as a key in the JSON payload (e.g., `"{{extra}}": []`). -- `{{request}}` This object will be `null` if there is no relevant request object for the notification. -- `{{media}}` This object will be `null` if there is no relevant media object for the notification. -- `{{extra}}` This object will contain the "extra" array of additional data for certain notifications. +| Variable | Value | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `{{media}}` | The relevant media object | +| `{{request}}` | The relevant request object | +| `{{issue}}` | The relevant issue object | +| `{{comment}}` | The relevant issue comment object | +| `{{extra}}` | The "extra" array of additional data for certain notifications (e.g., season/episode numbers for series-related notifications) | #### Media -These `{{media}}` special variables are only included in media-related notifications, such as requests. +The `{{media}}` will be `null` if there is no relevant media object for the notification. -- `{{media_type}}` Media type (`movie` or `tv`). -- `{{media_tmdbid}}` Media's TMDb ID. -- `{{media_imdbid}}` Media's IMDb ID. -- `{{media_tvdbid}}` Media's TVDB ID. -- `{{media_status}}` Media's availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`). -- `{{media_status4k}}` Media's 4K availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`) +These following special variables are only included in media-related notifications, such as requests. + +| Variable | Value | +| -------------------- | -------------------------------------------------------------------------------------------------------------- | +| `{{media_type}}` | The media type (`movie` or `tv`) | +| `{{media_tmdbid}}` | The media's TMDb ID | +| `{{media_tvdbid}}` | The media's TheTVDB ID | +| `{{media_status}}` | The media's availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`) | +| `{{media_status4k}}` | The media's 4K availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`) | #### Request -The `{{request}}` special variables are only included in request-related notifications. +The `{{request}}` will be `null` if there is no relevant media object for the notification. + +The following special variables are only included in request-related notifications. + +| Variable | Value | +| ----------------------------------------- | ----------------------------------------------- | +| `{{request_id}}` | The request ID | +| `{{requestedBy_username}}` | The requesting user's username | +| `{{requestedBy_email}}` | The requesting user's email address | +| `{{requestedBy_avatar}}` | The requesting user's avatar URL | +| `{{requestedBy_settings_discordId}}` | The requesting user's Discord ID (if set) | +| `{{requestedBy_settings_telegramChatId}}` | The requesting user's Telegram Chat ID (if set) | + +#### Issue + +The `{{issue}}` will be `null` if there is no relevant media object for the notification. + +The following special variables are only included in issue-related notifications. + +| Variable | Value | +| ---------------------------------------- | ----------------------------------------------- | +| `{{issue_id}}` | The issue ID | +| `{{reportedBy_username}}` | The requesting user's username | +| `{{reportedBy_email}}` | The requesting user's email address | +| `{{reportedBy_avatar}}` | The requesting user's avatar URL | +| `{{reportedBy_settings_discordId}}` | The requesting user's Discord ID (if set) | +| `{{reportedBy_settings_telegramChatId}}` | The requesting user's Telegram Chat ID (if set) | + +#### Comment + +The `{{comment}}` will be `null` if there is no relevant media object for the notification. + +The following special variables are only included in issue comment-related notifications. -- `{{request_id}}` Request ID. -- `{{requestedBy_username}}` Requesting user's username. -- `{{requestedBy_email}}` Requesting user's email address. -- `{{requestedBy_avatar}}` Requesting user's avatar URL. -- `{{requestedBy_settings_discordId}}` Requesting user's Discord ID (if set). -- `{{requestedBy_settings_telegramChatId}}` Requesting user's Telegram Chat ID (if set). +| Variable | Value | +| ----------------------------------------- | ----------------------------------------------- | +| `{{comment_message}}` | The comment message | +| `{{commentedBy_username}}` | The commenting user's username | +| `{{commentedBy_email}}` | The commenting user's email address | +| `{{commentedBy_avatar}}` | The commenting user's avatar URL | +| `{{commentedBy_settings_discordId}}` | The commenting user's Discord ID (if set) | +| `{{commentedBy_settings_telegramChatId}}` | The commenting user's Telegram Chat ID (if set) | diff --git a/public/sw.js b/public/sw.js index a3c816e8..e04d229e 100644 --- a/public/sw.js +++ b/public/sw.js @@ -90,8 +90,8 @@ self.addEventListener('push', (event) => { if (payload.actionUrl){ options.actions.push( { - action: 'viewmedia', - title: 'View Media', + action: 'view', + title: payload.actionUrlTitle ?? 'View', } ); } @@ -119,21 +119,17 @@ self.addEventListener('notificationclick', (event) => { event.notification.close(); - if (event.action === 'viewmedia') { - clients.openWindow(notificationData.actionUrl); - } else if (event.action === 'approve') { + if (event.action === 'approve') { fetch(`/api/v1/request/${notificationData.requestId}/approve`, { method: 'POST', }); - - clients.openWindow(notificationData.actionUrl); } else if (event.action === 'decline') { fetch(`/api/v1/request/${notificationData.requestId}/decline`, { method: 'POST', }); - - clients.openWindow(notificationData.actionUrl); - } else if (notificationData.actionUrl) { + } + + if (notificationData.actionUrl) { clients.openWindow(notificationData.actionUrl); } }, false); diff --git a/server/constants/issue.ts b/server/constants/issue.ts index 85911853..2c9dcb69 100644 --- a/server/constants/issue.ts +++ b/server/constants/issue.ts @@ -10,9 +10,9 @@ export enum IssueStatus { RESOLVED = 2, } -export const IssueTypeNames = { +export const IssueTypeName = { [IssueType.AUDIO]: 'Audio', [IssueType.VIDEO]: 'Video', - [IssueType.SUBTITLES]: 'Subtitles', + [IssueType.SUBTITLES]: 'Subtitle', [IssueType.OTHER]: 'Other', }; diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index a935b13f..7f1de381 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -142,6 +142,7 @@ export class MediaRequest { if (this.type === MediaType.MOVIE) { const movie = await tmdb.getMovie({ movieId: media.tmdbId }); notificationManager.sendNotification(Notification.MEDIA_PENDING, { + event: 'New Movie Request', subject: `${movie.title}${ movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' }`, @@ -153,12 +154,14 @@ export class MediaRequest { image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, media, request: this, + notifyAdmin: true, }); } if (this.type === MediaType.TV) { const tv = await tmdb.getTvShow({ tvId: media.tmdbId }); notificationManager.sendNotification(Notification.MEDIA_PENDING, { + event: 'New Series Request', subject: `${tv.name}${ tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : '' }`, @@ -171,13 +174,14 @@ export class MediaRequest { media, extra: [ { - name: 'Seasons', + name: 'Requested Seasons', value: this.seasons .map((season) => season.seasonNumber) .join(', '), }, ], request: this, + notifyAdmin: true, }); } } @@ -222,6 +226,13 @@ export class MediaRequest { : Notification.MEDIA_APPROVED : Notification.MEDIA_DECLINED, { + event: `Movie Request ${ + this.status === MediaRequestStatus.APPROVED + ? autoApproved + ? 'Automatically Approved' + : 'Approved' + : 'Declined' + }`, subject: `${movie.title}${ movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' }`, @@ -231,6 +242,7 @@ export class MediaRequest { omission: '…', }), image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, + notifyAdmin: autoApproved, notifyUser: autoApproved ? undefined : this.requestedBy, media, request: this, @@ -245,6 +257,13 @@ export class MediaRequest { : Notification.MEDIA_APPROVED : Notification.MEDIA_DECLINED, { + event: `Series Request ${ + this.status === MediaRequestStatus.APPROVED + ? autoApproved + ? 'Automatically Approved' + : 'Approved' + : 'Declined' + }`, subject: `${tv.name}${ tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : '' }`, @@ -254,11 +273,12 @@ export class MediaRequest { omission: '…', }), image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`, + notifyAdmin: autoApproved, notifyUser: autoApproved ? undefined : this.requestedBy, media, extra: [ { - name: 'Seasons', + name: 'Requested Seasons', value: this.seasons .map((season) => season.seasonNumber) .join(', '), @@ -508,6 +528,7 @@ export class MediaRequest { ); notificationManager.sendNotification(Notification.MEDIA_FAILED, { + event: `Movie Request Failed`, subject: `${movie.title}${ movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' }`, @@ -519,6 +540,7 @@ export class MediaRequest { media, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, request: this, + notifyAdmin: true, }); }); logger.info('Sent request to Radarr', { label: 'Media Request' }); @@ -722,6 +744,7 @@ export class MediaRequest { ); notificationManager.sendNotification(Notification.MEDIA_FAILED, { + event: `Series Request Failed`, subject: `${series.name}${ series.first_air_date ? ` (${series.first_air_date.slice(0, 4)})` @@ -736,13 +759,14 @@ export class MediaRequest { media, extra: [ { - name: 'Seasons', + name: 'Requested Seasons', value: this.seasons .map((season) => season.seasonNumber) .join(', '), }, ], request: this, + notifyAdmin: true, }); }); 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 3de828f9..edfa1262 100644 --- a/server/lib/notifications/agents/agent.ts +++ b/server/lib/notifications/agents/agent.ts @@ -1,12 +1,15 @@ import { Notification } from '..'; import type Issue from '../../../entity/Issue'; -import type Media from '../../../entity/Media'; +import IssueComment from '../../../entity/IssueComment'; +import Media from '../../../entity/Media'; import { MediaRequest } from '../../../entity/MediaRequest'; import { User } from '../../../entity/User'; import { NotificationAgentConfig } from '../../settings'; export interface NotificationPayload { + event?: string; subject: string; + notifyAdmin: boolean; notifyUser?: User; media?: Media; image?: string; @@ -14,6 +17,7 @@ export interface NotificationPayload { extra?: { name: string; value: string }[]; request?: MediaRequest; issue?: Issue; + comment?: IssueComment; } export abstract class BaseAgent { diff --git a/server/lib/notifications/agents/discord.ts b/server/lib/notifications/agents/discord.ts index 8e08e983..b611bf08 100644 --- a/server/lib/notifications/agents/discord.ts +++ b/server/lib/notifications/agents/discord.ts @@ -1,11 +1,13 @@ import axios from 'axios'; import { getRepository } from 'typeorm'; -import { hasNotificationType, Notification } from '..'; -import { IssueStatus, IssueTypeNames } from '../../../constants/issue'; -import { MediaType } from '../../../constants/media'; +import { + hasNotificationType, + Notification, + shouldSendAdminNotification, +} from '..'; +import { IssueStatus, IssueTypeName } from '../../../constants/issue'; import { User } from '../../../entity/User'; import logger from '../../../logger'; -import { Permission } from '../../permissions'; import { getSettings, NotificationAgentDiscord, @@ -109,9 +111,9 @@ class DiscordAgent type: Notification, payload: NotificationPayload ): DiscordRichEmbed { - const settings = getSettings(); - let color = EmbedColors.DARK_PURPLE; + const { applicationUrl } = getSettings().main; + let color = EmbedColors.DARK_PURPLE; const fields: Field[] = []; if (payload.request) { @@ -120,19 +122,55 @@ class DiscordAgent value: payload.request.requestedBy.displayName, inline: true, }); - } - // If payload has an issue attached, push issue specific fields - if (payload.issue) { + let status = ''; + switch (type) { + case Notification.MEDIA_PENDING: + color = EmbedColors.ORANGE; + status = 'Pending Approval'; + break; + case Notification.MEDIA_APPROVED: + case Notification.MEDIA_AUTO_APPROVED: + color = EmbedColors.PURPLE; + status = 'Processing'; + break; + case Notification.MEDIA_AVAILABLE: + color = EmbedColors.GREEN; + status = 'Available'; + break; + case Notification.MEDIA_DECLINED: + color = EmbedColors.RED; + status = 'Declined'; + break; + case Notification.MEDIA_FAILED: + color = EmbedColors.RED; + status = 'Failed'; + break; + } + + if (status) { + fields.push({ + name: 'Request Status', + value: status, + inline: true, + }); + } + } else if (payload.comment) { + fields.push({ + name: `Comment from ${payload.comment.user.displayName}`, + value: payload.comment.message, + inline: false, + }); + } else if (payload.issue) { fields.push( { - name: 'Created By', + name: 'Reported By', value: payload.issue.createdBy.displayName, inline: true, }, { name: 'Issue Type', - value: IssueTypeNames[payload.issue.issueType], + value: IssueTypeName[payload.issue.issueType], inline: true, }, { @@ -143,85 +181,35 @@ class DiscordAgent } ); - if (payload.issue.media.mediaType === MediaType.TV) { - fields.push({ - name: 'Affected Season', - value: - payload.issue.problemSeason > 0 - ? `Season ${payload.issue.problemSeason}` - : 'All Seasons', - }); - - if (payload.issue.problemSeason > 0) { - fields.push({ - name: 'Affected Episode', - value: - payload.issue.problemEpisode > 0 - ? `Episode ${payload.issue.problemEpisode}` - : 'All Episodes', - }); - } + switch (type) { + case Notification.ISSUE_CREATED: + case Notification.ISSUE_REOPENED: + color = EmbedColors.RED; + break; + case Notification.ISSUE_COMMENT: + color = EmbedColors.ORANGE; + break; + case Notification.ISSUE_RESOLVED: + color = EmbedColors.GREEN; + break; } } - switch (type) { - case Notification.MEDIA_PENDING: - color = EmbedColors.ORANGE; - fields.push({ - name: 'Status', - value: 'Pending Approval', - inline: true, - }); - break; - case Notification.MEDIA_APPROVED: - case Notification.MEDIA_AUTO_APPROVED: - color = EmbedColors.PURPLE; - fields.push({ - name: 'Status', - value: 'Processing', - inline: true, - }); - break; - case Notification.MEDIA_AVAILABLE: - color = EmbedColors.GREEN; - fields.push({ - name: 'Status', - value: 'Available', - inline: true, - }); - break; - case Notification.MEDIA_DECLINED: - color = EmbedColors.RED; - fields.push({ - name: 'Status', - value: 'Declined', - inline: true, - }); - break; - case Notification.MEDIA_FAILED: - color = EmbedColors.RED; - fields.push({ - name: 'Status', - value: 'Failed', - inline: true, - }); - break; - case Notification.ISSUE_CREATED: - case Notification.ISSUE_COMMENT: - case Notification.ISSUE_RESOLVED: - color = EmbedColors.ORANGE; - - if (payload.issue && payload.issue.status === IssueStatus.RESOLVED) { - color = EmbedColors.GREEN; - } - - break; + for (const extra of payload.extra ?? []) { + fields.push({ + name: extra.name, + value: extra.value, + inline: true, + }); } - const url = - settings.main.applicationUrl && payload.media - ? `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}` - : undefined; + const url = applicationUrl + ? payload.issue + ? `${applicationUrl}/issue/${payload.issue.id}` + : payload.media + ? `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}` + : undefined + : undefined; return { title: payload.subject, @@ -229,18 +217,12 @@ class DiscordAgent description: payload.message, color, timestamp: new Date().toISOString(), - author: { - name: settings.main.applicationTitle, - url: settings.main.applicationUrl, - }, - fields: [ - ...fields, - // If we have extra data, map it to fields for discord notifications - ...(payload.extra ?? []).map((extra) => ({ - name: extra.name, - value: extra.value, - })), - ], + author: payload.event + ? { + name: payload.event, + } + : undefined, + fields, thumbnail: { url: payload.image, }, @@ -273,54 +255,53 @@ class DiscordAgent subject: payload.subject, }); - let content = undefined; + const userMentions: string[] = []; try { if (payload.notifyUser) { - // Mention user who submitted the request if ( payload.notifyUser.settings?.hasNotificationType( NotificationAgentKey.DISCORD, type ) && - payload.notifyUser.settings?.discordId + payload.notifyUser.settings.discordId ) { - content = `<@${payload.notifyUser.settings.discordId}>`; + userMentions.push(`<@${payload.notifyUser.settings.discordId}>`); } - } else { - // Mention all users with the Manage Requests permission + } + + if (payload.notifyAdmin) { const userRepository = getRepository(User); const users = await userRepository.find(); - content = users - .filter( - (user) => - user.hasPermission(Permission.MANAGE_REQUESTS) && - user.settings?.hasNotificationType( - NotificationAgentKey.DISCORD, - type - ) && - user.settings?.discordId && - // Check if it's the user's own auto-approved request - (type !== Notification.MEDIA_AUTO_APPROVED || - user.id !== payload.request?.requestedBy.id) - ) - .map((user) => `<@${user.settings?.discordId}>`) - .join(' '); + userMentions.push( + ...users + .filter( + (user) => + user.settings?.hasNotificationType( + NotificationAgentKey.DISCORD, + type + ) && + user.settings.discordId && + shouldSendAdminNotification(type, user, payload) + ) + .map((user) => `<@${user.settings?.discordId}>`) + ); } await axios.post(settings.options.webhookUrl, { - username: settings.options.botUsername, + username: settings.options.botUsername + ? settings.options.botUsername + : getSettings().main.applicationTitle, avatar_url: settings.options.botAvatarUrl, embeds: [this.buildEmbed(type, payload)], - content, + content: userMentions.join(' '), } as DiscordWebhookPayload); return true; } catch (e) { logger.error('Error sending Discord notification', { label: 'Notifications', - mentions: content, type: Notification[type], subject: payload.subject, errorMessage: e.message, diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 7cf45b47..c49d5827 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -1,12 +1,12 @@ import { EmailOptions } from 'email-templates'; import path from 'path'; import { getRepository } from 'typeorm'; -import { Notification } from '..'; +import { Notification, shouldSendAdminNotification } from '..'; +import { IssueType, IssueTypeName } from '../../../constants/issue'; import { MediaType } from '../../../constants/media'; import { User } from '../../../entity/User'; import logger from '../../../logger'; import PreparedEmail from '../../email'; -import { Permission } from '../../permissions'; import { getSettings, NotificationAgentEmail, @@ -67,59 +67,34 @@ class EmailAgent }; } - if (payload.media) { - let requestType = ''; + const mediaType = payload.media + ? payload.media.mediaType === MediaType.MOVIE + ? 'movie' + : 'series' + : undefined; + + if (payload.request) { let body = ''; switch (type) { case Notification.MEDIA_PENDING: - requestType = `New ${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request`; - body = `A user has requested a new ${ - payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' - }!`; + body = `A new request for the following ${mediaType} is pending approval:`; break; case Notification.MEDIA_APPROVED: - requestType = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Approved`; - body = `Your request for the following ${ - payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' - } has been approved:`; + body = `Your request for the following ${mediaType} has been approved:`; break; case Notification.MEDIA_AUTO_APPROVED: - requestType = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Automatically Approved`; - body = `A new request for the following ${ - payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' - } has been automatically approved:`; + body = `A new request for the following ${mediaType} has been automatically approved:`; break; case Notification.MEDIA_AVAILABLE: - requestType = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Now Available`; - body = `The following ${ - payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' - } you requested is now available!`; + body = `Your request for the following ${mediaType} is now available:`; break; case Notification.MEDIA_DECLINED: - requestType = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Declined`; - body = `Your request for the following ${ - payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' - } was declined:`; + body = `Your request for the following ${mediaType} was declined:`; break; case Notification.MEDIA_FAILED: - requestType = `Failed ${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request`; - body = `A new request for the following ${ - payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' - } could not be added to ${ - payload.media?.mediaType === MediaType.TV ? 'Sonarr' : 'Radarr' + body = `A request for the following ${mediaType} failed to be added to ${ + payload.media?.mediaType === MediaType.MOVIE ? 'Radarr' : 'Sonarr' }:`; break; } @@ -133,14 +108,13 @@ class EmailAgent to: recipientEmail, }, locals: { - requestType, + event: payload.event, body, mediaName: payload.subject, - mediaPlot: payload.message, mediaExtra: payload.extra ?? [], imageUrl: payload.image, timestamp: new Date().toTimeString(), - requestedBy: payload.request?.requestedBy.displayName, + requestedBy: payload.request.requestedBy.displayName, actionUrl: applicationUrl ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` : undefined, @@ -150,6 +124,52 @@ class EmailAgent recipientEmail, }, }; + } else if (payload.issue) { + const issueType = + payload.issue && payload.issue.issueType !== IssueType.OTHER + ? `${IssueTypeName[payload.issue.issueType].toLowerCase()} issue` + : 'issue'; + + let body = ''; + + switch (type) { + case Notification.ISSUE_CREATED: + body = `A new ${issueType} has been reported by ${payload.issue.createdBy.displayName} for the ${mediaType} ${payload.subject}:`; + break; + case Notification.ISSUE_COMMENT: + body = `${payload.comment?.user.displayName} commented on the ${issueType} for the ${mediaType} ${payload.subject}:`; + break; + case Notification.ISSUE_RESOLVED: + body = `The ${issueType} for the ${mediaType} ${payload.subject} was marked as resolved by ${payload.issue.modifiedBy?.displayName}!`; + break; + case Notification.ISSUE_REOPENED: + body = `The ${issueType} for the ${mediaType} ${payload.subject} was reopened by ${payload.issue.modifiedBy?.displayName}.`; + break; + } + + return { + template: path.join(__dirname, '../../../templates/email/media-issue'), + message: { + to: recipientEmail, + }, + locals: { + event: payload.event, + body, + issueDescription: payload.message, + issueComment: payload.comment?.message, + mediaName: payload.subject, + extra: payload.extra ?? [], + imageUrl: payload.image, + timestamp: new Date().toTimeString(), + actionUrl: applicationUrl + ? `${applicationUrl}/issue/${payload.issue.id}` + : undefined, + applicationUrl, + applicationTitle, + recipientName, + recipientEmail, + }, + }; } return undefined; @@ -160,7 +180,6 @@ class EmailAgent payload: NotificationPayload ): Promise { if (payload.notifyUser) { - // Send notification to the user who submitted the request if ( !payload.notifyUser.settings || // Check if user has email notifications enabled and fallback to true if undefined @@ -203,8 +222,9 @@ class EmailAgent return false; } } - } else { - // Send notifications to all users with the Manage Requests permission + } + + if (payload.notifyAdmin) { const userRepository = getRepository(User); const users = await userRepository.find(); @@ -212,7 +232,6 @@ class EmailAgent users .filter( (user) => - user.hasPermission(Permission.MANAGE_REQUESTS) && (!user.settings || // Check if user has email notifications enabled and fallback to true if undefined // since email should default to true @@ -221,9 +240,7 @@ class EmailAgent type ) ?? true)) && - // Check if it's the user's own auto-approved request - (type !== Notification.MEDIA_AUTO_APPROVED || - user.id !== payload.request?.requestedBy.id) + shouldSendAdminNotification(type, user, payload) ) .map(async (user) => { logger.debug('Sending email notification', { diff --git a/server/lib/notifications/agents/lunasea.ts b/server/lib/notifications/agents/lunasea.ts index 5d5c5216..0269e260 100644 --- a/server/lib/notifications/agents/lunasea.ts +++ b/server/lib/notifications/agents/lunasea.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import { hasNotificationType, Notification } from '..'; +import { IssueStatus, IssueType } from '../../../constants/issue'; import { MediaStatus } from '../../../constants/media'; import logger from '../../../logger'; import { getSettings, NotificationAgentLunaSea } from '../../settings'; @@ -22,17 +23,17 @@ class LunaSeaAgent private buildPayload(type: Notification, payload: NotificationPayload) { return { notification_type: Notification[type], + event: payload.event, subject: payload.subject, message: payload.message, image: payload.image ?? null, email: payload.notifyUser?.email, - username: payload.notifyUser?.username, + username: payload.notifyUser?.displayName, avatar: payload.notifyUser?.avatar, media: payload.media ? { media_type: payload.media.mediaType, tmdbId: payload.media.tmdbId, - imdbId: payload.media.imdbId, tvdbId: payload.media.tvdbId, status: MediaStatus[payload.media.status], status4k: MediaStatus[payload.media.status4k], @@ -47,6 +48,24 @@ class LunaSeaAgent requestedBy_avatar: payload.request.requestedBy.avatar, } : null, + issue: payload.issue + ? { + issue_id: payload.issue.id, + issue_type: IssueType[payload.issue.issueType], + issue_status: IssueStatus[payload.issue.status], + createdBy_email: payload.issue.createdBy.email, + createdBy_username: payload.issue.createdBy.displayName, + createdBy_avatar: payload.issue.createdBy.avatar, + } + : null, + comment: payload.comment + ? { + comment_message: payload.comment.message, + commentedBy_email: payload.comment.user.email, + commentedBy_username: payload.comment.user.displayName, + commentedBy_avatar: payload.comment.user.avatar, + } + : null, }; } diff --git a/server/lib/notifications/agents/pushbullet.ts b/server/lib/notifications/agents/pushbullet.ts index 3684803f..092722c9 100644 --- a/server/lib/notifications/agents/pushbullet.ts +++ b/server/lib/notifications/agents/pushbullet.ts @@ -1,10 +1,13 @@ import axios from 'axios'; import { getRepository } from 'typeorm'; -import { hasNotificationType, Notification } from '..'; -import { MediaType } from '../../../constants/media'; +import { + hasNotificationType, + Notification, + shouldSendAdminNotification, +} from '..'; +import { IssueStatus, IssueTypeName } from '../../../constants/issue'; import { User } from '../../../entity/User'; import logger from '../../../logger'; -import { Permission } from '../../permissions'; import { getSettings, NotificationAgentKey, @@ -40,94 +43,55 @@ class PushbulletAgent type: Notification, payload: NotificationPayload ): PushbulletPayload { - let messageTitle = ''; - let message = ''; - - const title = payload.subject; - const plot = payload.message; - const username = payload.request?.requestedBy.displayName; + const title = payload.event + ? `${payload.event} - ${payload.subject}` + : payload.subject; + let body = payload.message ?? ''; + + if (payload.request) { + body += `\n\nRequested By: ${payload.request.requestedBy.displayName}`; + + let status = ''; + switch (type) { + case Notification.MEDIA_PENDING: + status = 'Pending Approval'; + break; + case Notification.MEDIA_APPROVED: + case Notification.MEDIA_AUTO_APPROVED: + status = 'Processing'; + break; + case Notification.MEDIA_AVAILABLE: + status = 'Available'; + break; + case Notification.MEDIA_DECLINED: + status = 'Declined'; + break; + case Notification.MEDIA_FAILED: + status = 'Failed'; + break; + } - switch (type) { - case Notification.MEDIA_PENDING: - messageTitle = `New ${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request`; - message += `${title}`; - if (plot) { - message += `\n\n${plot}`; - } - message += `\n\nRequested By: ${username}`; - message += `\nStatus: Pending Approval`; - break; - case Notification.MEDIA_APPROVED: - messageTitle = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Approved`; - message += `${title}`; - if (plot) { - message += `\n\n${plot}`; - } - message += `\n\nRequested By: ${username}`; - message += `\nStatus: Processing`; - break; - case Notification.MEDIA_AUTO_APPROVED: - messageTitle = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Automatically Approved`; - message += `${title}`; - if (plot) { - message += `\n\n${plot}`; - } - message += `\n\nRequested By: ${username}`; - message += `\nStatus: Processing`; - break; - case Notification.MEDIA_AVAILABLE: - messageTitle = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Now Available`; - message += `${title}`; - if (plot) { - message += `\n\n${plot}`; - } - message += `\n\nRequested By: ${username}`; - message += `\nStatus: Available`; - break; - case Notification.MEDIA_DECLINED: - messageTitle = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Declined`; - message += `${title}`; - if (plot) { - message += `\n\n${plot}`; - } - message += `\n\nRequested By: ${username}`; - message += `\nStatus: Declined`; - break; - case Notification.MEDIA_FAILED: - messageTitle = `Failed ${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request`; - message += `${title}`; - if (plot) { - message += `\n\n${plot}`; - } - message += `\n\nRequested By: ${username}`; - message += `\nStatus: Failed`; - break; - case Notification.TEST_NOTIFICATION: - messageTitle = 'Test Notification'; - message += `${plot}`; - break; + if (status) { + body += `\nRequest Status: ${status}`; + } + } else if (payload.comment) { + body += `\n\nComment from ${payload.comment.user.displayName}:\n${payload.comment.message}`; + } else if (payload.issue) { + body += `\n\nReported By: ${payload.issue.createdBy.displayName}`; + body += `\nIssue Type: ${IssueTypeName[payload.issue.issueType]}`; + body += `\nIssue Status: ${ + payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved' + }`; } for (const extra of payload.extra ?? []) { - message += `\n${extra.name}: ${extra.value}`; + body += `\n${extra.name}: ${extra.value}`; } return { type: 'note', - title: messageTitle, - body: message, + title, + body, }; } @@ -171,7 +135,6 @@ class PushbulletAgent } if (payload.notifyUser) { - // Send notification to the user who submitted the request if ( payload.notifyUser.settings?.hasNotificationType( NotificationAgentKey.PUSHBULLET, @@ -207,8 +170,9 @@ class PushbulletAgent return false; } } - } else { - // Send notifications to all users with the Manage Requests permission + } + + if (payload.notifyAdmin) { const userRepository = getRepository(User); const users = await userRepository.find(); @@ -216,14 +180,10 @@ class PushbulletAgent users .filter( (user) => - user.hasPermission(Permission.MANAGE_REQUESTS) && user.settings?.hasNotificationType( NotificationAgentKey.PUSHBULLET, type - ) && - // Check if it's the user's own auto-approved request - (type !== Notification.MEDIA_AUTO_APPROVED || - user.id !== payload.request?.requestedBy.id) + ) && shouldSendAdminNotification(type, user, payload) ) .map(async (user) => { if ( diff --git a/server/lib/notifications/agents/pushover.ts b/server/lib/notifications/agents/pushover.ts index e6615806..edcd7c2f 100644 --- a/server/lib/notifications/agents/pushover.ts +++ b/server/lib/notifications/agents/pushover.ts @@ -1,10 +1,13 @@ import axios from 'axios'; import { getRepository } from 'typeorm'; -import { hasNotificationType, Notification } from '..'; -import { MediaType } from '../../../constants/media'; +import { + hasNotificationType, + Notification, + shouldSendAdminNotification, +} from '..'; +import { IssueStatus, IssueTypeName } from '../../../constants/issue'; import { User } from '../../../entity/User'; import logger from '../../../logger'; -import { Permission } from '../../permissions'; import { getSettings, NotificationAgentKey, @@ -45,103 +48,77 @@ class PushoverAgent type: Notification, payload: NotificationPayload ): Partial { - const settings = getSettings(); - let messageTitle = ''; - let message = ''; - let url: string | undefined; - let url_title: string | undefined; + const { applicationUrl, applicationTitle } = getSettings().main; + + const title = payload.event ?? payload.subject; + let message = payload.event ? `${payload.subject}` : ''; let priority = 0; - const title = payload.subject; - const plot = payload.message; - const username = payload.request?.requestedBy.displayName; + if (payload.message) { + message += `${message ? '\n' : ''}${payload.message}`; + } - switch (type) { - case Notification.MEDIA_PENDING: - messageTitle = `New ${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request`; - message += `${title}`; - if (plot) { - message += `\n${plot}`; - } - message += `\n\nRequested By\n${username}`; - message += `\n\nStatus\nPending Approval`; - break; - case Notification.MEDIA_APPROVED: - messageTitle = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Approved`; - message += `${title}`; - if (plot) { - message += `\n${plot}`; - } - message += `\n\nRequested By\n${username}`; - message += `\n\nStatus\nProcessing`; - break; - case Notification.MEDIA_AUTO_APPROVED: - messageTitle = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Automatically Approved`; - message += `${title}`; - if (plot) { - message += `\n${plot}`; - } - message += `\n\nRequested By\n${username}`; - message += `\n\nStatus\nProcessing`; - break; - case Notification.MEDIA_AVAILABLE: - messageTitle = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Now Available`; - message += `${title}`; - if (plot) { - message += `\n${plot}`; - } - message += `\n\nRequested By\n${username}`; - message += `\n\nStatus\nAvailable`; - break; - case Notification.MEDIA_DECLINED: - messageTitle = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Declined`; - 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 ${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request`; - message += `${title}`; - if (plot) { - message += `\n${plot}`; - } - message += `\n\nRequested By\n${username}`; - message += `\n\nStatus\nFailed`; + if (payload.request) { + message += `\n\nRequested By: ${payload.request.requestedBy.displayName}`; + + let status = ''; + switch (type) { + case Notification.MEDIA_PENDING: + status = 'Pending Approval'; + break; + case Notification.MEDIA_APPROVED: + case Notification.MEDIA_AUTO_APPROVED: + status = 'Processing'; + break; + case Notification.MEDIA_AVAILABLE: + status = 'Available'; + break; + case Notification.MEDIA_DECLINED: + status = 'Declined'; + priority = 1; + break; + case Notification.MEDIA_FAILED: + status = 'Failed'; + priority = 1; + break; + } + + if (status) { + message += `\nRequest Status: ${status}`; + } + } else if (payload.comment) { + message += `\n\nComment from ${payload.comment.user.displayName}: ${payload.comment.message}`; + } else if (payload.issue) { + message += `\n\nReported By: ${payload.issue.createdBy.displayName}`; + message += `\nIssue Type: ${ + IssueTypeName[payload.issue.issueType] + }`; + message += `\nIssue Status: ${ + payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved' + }`; + + if (type === Notification.ISSUE_CREATED) { priority = 1; - break; - case Notification.TEST_NOTIFICATION: - messageTitle = 'Test Notification'; - message += `${plot}`; - break; + } } for (const extra of payload.extra ?? []) { - message += `\n\n${extra.name}\n${extra.value}`; + message += `\n${extra.name}: ${extra.value}`; } - if (settings.main.applicationUrl && payload.media) { - url = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`; - url_title = `Open in ${settings.main.applicationTitle}`; - } + const url = applicationUrl + ? payload.issue + ? `${applicationUrl}/issue/${payload.issue.id}` + : payload.media + ? `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}` + : undefined + : undefined; + const url_title = url + ? `View ${payload.issue ? 'Issue' : 'Media'} in ${applicationTitle}` + : undefined; return { - title: messageTitle, + title, message, url, url_title, @@ -191,7 +168,6 @@ class PushoverAgent } if (payload.notifyUser) { - // Send notification to the user who submitted the request if ( payload.notifyUser.settings?.hasNotificationType( NotificationAgentKey.PUSHOVER, @@ -230,8 +206,9 @@ class PushoverAgent return false; } } - } else { - // Send notifications to all users with the Manage Requests permission + } + + if (payload.notifyAdmin) { const userRepository = getRepository(User); const users = await userRepository.find(); @@ -239,14 +216,10 @@ class PushoverAgent users .filter( (user) => - user.hasPermission(Permission.MANAGE_REQUESTS) && user.settings?.hasNotificationType( NotificationAgentKey.PUSHOVER, type - ) && - // Check if it's the user's own auto-approved request - (type !== Notification.MEDIA_AUTO_APPROVED || - user.id !== payload.request?.requestedBy.id) + ) && shouldSendAdminNotification(type, user, payload) ) .map(async (user) => { if ( diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index 8065f9a6..f2be66ea 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { hasNotificationType, Notification } from '..'; -import { MediaType } from '../../../constants/media'; +import { IssueStatus, IssueTypeName } from '../../../constants/issue'; import logger from '../../../logger'; import { getSettings, NotificationAgentSlack } from '../../settings'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; @@ -19,9 +19,10 @@ interface TextItem { interface Element { type: 'button'; text?: TextItem; - value: string; - url: string; - action_id: 'button-action'; + action_id: string; + url?: string; + value?: string; + style?: 'primary' | 'danger'; } interface EmbedBlock { @@ -34,7 +35,7 @@ interface EmbedBlock { image_url: string; alt_text: string; }; - elements?: Element[]; + elements?: (Element | TextItem)[]; } interface SlackBlockEmbed { @@ -59,9 +60,7 @@ class SlackAgent type: Notification, payload: NotificationPayload ): SlackBlockEmbed { - const settings = getSettings(); - let header = ''; - let actionUrl: string | undefined; + const { applicationUrl, applicationTitle } = getSettings().main; const fields: EmbedField[] = []; @@ -70,66 +69,55 @@ class SlackAgent type: 'mrkdwn', text: `*Requested By*\n${payload.request.requestedBy.displayName}`, }); - } - switch (type) { - case Notification.MEDIA_PENDING: - header = `New ${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request`; - fields.push({ - type: 'mrkdwn', - text: '*Status*\nPending Approval', - }); - break; - case Notification.MEDIA_APPROVED: - header = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Approved`; + let status = ''; + switch (type) { + case Notification.MEDIA_PENDING: + status = 'Pending Approval'; + break; + case Notification.MEDIA_APPROVED: + case Notification.MEDIA_AUTO_APPROVED: + status = 'Processing'; + break; + case Notification.MEDIA_AVAILABLE: + status = 'Available'; + break; + case Notification.MEDIA_DECLINED: + status = 'Declined'; + break; + case Notification.MEDIA_FAILED: + status = 'Failed'; + break; + } + + if (status) { fields.push({ type: 'mrkdwn', - text: '*Status*\nProcessing', + text: `*Request Status*\n${status}`, }); - break; - case Notification.MEDIA_AUTO_APPROVED: - header = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Automatically Approved`; - fields.push({ - type: 'mrkdwn', - text: '*Status*\nProcessing', - }); - break; - case Notification.MEDIA_AVAILABLE: - header = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Now Available`; - fields.push({ + } + } else if (payload.comment) { + fields.push({ + type: 'mrkdwn', + text: `*Comment from ${payload.comment.user.displayName}*\n${payload.comment.message}`, + }); + } else if (payload.issue) { + fields.push( + { type: 'mrkdwn', - text: '*Status*\nAvailable', - }); - break; - case Notification.MEDIA_DECLINED: - header = `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Declined`; - fields.push({ + text: `*Reported By*\n${payload.issue.createdBy.displayName}`, + }, + { type: 'mrkdwn', - text: '*Status*\nDeclined', - }); - break; - case Notification.MEDIA_FAILED: - header = `Failed ${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request`; - fields.push({ + text: `*Issue Type*\n${IssueTypeName[payload.issue.issueType]}`, + }, + { type: 'mrkdwn', - text: '*Status*\nFailed', - }); - break; - case Notification.TEST_NOTIFICATION: - header = 'Test Notification'; - break; + text: `*Issue Status*\n${ + payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved' + }`, + } + ); } for (const extra of payload.extra ?? []) { @@ -139,30 +127,28 @@ class SlackAgent }); } - if (settings.main.applicationUrl && payload.media) { - actionUrl = `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`; - } - - const blocks: EmbedBlock[] = [ - { - type: 'header', - text: { - type: 'plain_text', - text: header, - }, - }, - ]; + const blocks: EmbedBlock[] = []; - if (type !== Notification.TEST_NOTIFICATION) { + if (payload.event) { blocks.push({ - type: 'section', - text: { - type: 'mrkdwn', - text: `*${payload.subject}*`, - }, + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: `*${payload.event}*`, + }, + ], }); } + blocks.push({ + type: 'header', + text: { + type: 'plain_text', + text: payload.subject, + }, + }); + if (payload.message) { blocks.push({ type: 'section', @@ -183,30 +169,31 @@ class SlackAgent if (fields.length > 0) { blocks.push({ type: 'section', - fields: [ - ...fields, - ...(payload.extra ?? []).map( - (extra): EmbedField => ({ - type: 'mrkdwn', - text: `*${extra.name}*\n${extra.value}`, - }) - ), - ], + fields, }); } - if (actionUrl) { + const url = applicationUrl + ? payload.issue + ? `${applicationUrl}/issue/${payload.issue.id}` + : payload.media + ? `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}` + : undefined + : undefined; + + if (url) { blocks.push({ type: 'actions', elements: [ { - action_id: 'button-action', + action_id: 'open-in-overseerr', type: 'button', - url: actionUrl, - value: 'open_overseerr', + url, text: { type: 'plain_text', - text: `Open in ${settings.main.applicationTitle}`, + text: `View ${ + payload.issue ? 'Issue' : 'Media' + } in ${applicationTitle}`, }, }, ], diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index e71d2911..e8839470 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -1,10 +1,13 @@ import axios from 'axios'; import { getRepository } from 'typeorm'; -import { hasNotificationType, Notification } from '..'; -import { MediaType } from '../../../constants/media'; +import { + hasNotificationType, + Notification, + shouldSendAdminNotification, +} from '..'; +import { IssueStatus, IssueTypeName } from '../../../constants/issue'; import { User } from '../../../entity/User'; import logger from '../../../logger'; -import { Permission } from '../../permissions'; import { getSettings, NotificationAgentKey, @@ -61,95 +64,74 @@ class TelegramAgent type: Notification, payload: NotificationPayload ): Partial { - const settings = getSettings(); - let message = ''; - - const title = this.escapeText(payload.subject); - const plot = this.escapeText(payload.message); - const user = this.escapeText(payload.request?.requestedBy.displayName); - const applicationTitle = this.escapeText(settings.main.applicationTitle); + const { applicationUrl, applicationTitle } = getSettings().main; /* eslint-disable no-useless-escape */ - switch (type) { - case Notification.MEDIA_PENDING: - message += `\*New ${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } 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 += `\*${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } 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_AUTO_APPROVED: - message += `\*${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Automatically 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 += `\*${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Now Available\*`; - message += `\n\n\*${title}\*`; - if (plot) { - message += `\n${plot}`; - } - message += `\n\n\*Requested By\*\n${user}`; - message += `\n\n\*Status\*\nAvailable`; - break; - case Notification.MEDIA_DECLINED: - message += `\*${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } 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_FAILED: - message += `\*Failed ${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } 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\*`; - message += `\n\n${plot}`; - break; + let message = `\*${this.escapeText( + payload.event ? `${payload.event} - ${payload.subject}` : payload.subject + )}\*`; + if (payload.message) { + message += `\n${this.escapeText(payload.message)}`; + } + + if (payload.request) { + message += `\n\n\*Requested By:\* ${this.escapeText( + payload.request?.requestedBy.displayName + )}`; + + let status = ''; + switch (type) { + case Notification.MEDIA_PENDING: + status = 'Pending Approval'; + break; + case Notification.MEDIA_APPROVED: + case Notification.MEDIA_AUTO_APPROVED: + status = 'Processing'; + break; + case Notification.MEDIA_AVAILABLE: + status = 'Available'; + break; + case Notification.MEDIA_DECLINED: + status = 'Declined'; + break; + case Notification.MEDIA_FAILED: + status = 'Failed'; + break; + } + + if (status) { + message += `\n\*Request Status:\* ${status}`; + } + } else if (payload.comment) { + message += `\n\n\*Comment from ${this.escapeText( + payload.comment.user.displayName + )}:\* ${this.escapeText(payload.comment.message)}`; + } else if (payload.issue) { + message += `\n\n\*Reported By:\* ${this.escapeText( + payload.issue.createdBy.displayName + )}`; + message += `\n\*Issue Type:\* ${IssueTypeName[payload.issue.issueType]}`; + message += `\n\*Issue Status:\* ${ + payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved' + }`; } for (const extra of payload.extra ?? []) { - message += `\n\n\*${extra.name}\*\n${extra.value}`; + message += `\n\*${extra.name}:\* ${extra.value}`; } - if (settings.main.applicationUrl && payload.media) { - const actionUrl = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`; - message += `\n\n\[Open in ${applicationTitle}\]\(${actionUrl}\)`; + const url = applicationUrl + ? payload.issue + ? `${applicationUrl}/issue/${payload.issue.id}` + : payload.media + ? `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}` + : undefined + : undefined; + + if (url) { + message += `\n\n\[View ${ + payload.issue ? 'Issue' : 'Media' + } in ${this.escapeText(applicationTitle)}\]\(${url}\)`; } /* eslint-enable */ @@ -206,7 +188,6 @@ class TelegramAgent } if (payload.notifyUser) { - // Send notification to the user who submitted the request if ( payload.notifyUser.settings?.hasNotificationType( NotificationAgentKey.TELEGRAM, @@ -242,8 +223,9 @@ class TelegramAgent return false; } } - } else { - // Send notifications to all users with the Manage Requests permission + } + + if (payload.notifyAdmin) { const userRepository = getRepository(User); const users = await userRepository.find(); @@ -251,14 +233,10 @@ class TelegramAgent users .filter( (user) => - user.hasPermission(Permission.MANAGE_REQUESTS) && user.settings?.hasNotificationType( NotificationAgentKey.TELEGRAM, type - ) && - // Check if it's the user's own auto-approved request - (type !== Notification.MEDIA_AUTO_APPROVED || - user.id !== payload.request?.requestedBy.id) + ) && shouldSendAdminNotification(type, user, payload) ) .map(async (user) => { if ( diff --git a/server/lib/notifications/agents/webhook.ts b/server/lib/notifications/agents/webhook.ts index 2959f81c..ba2bf5e5 100644 --- a/server/lib/notifications/agents/webhook.ts +++ b/server/lib/notifications/agents/webhook.ts @@ -1,6 +1,7 @@ import axios from 'axios'; import { get } from 'lodash'; import { hasNotificationType, Notification } from '..'; +import { IssueStatus, IssueType } from '../../../constants/issue'; import { MediaStatus } from '../../../constants/media'; import logger from '../../../logger'; import { getSettings, NotificationAgentWebhook } from '../../settings'; @@ -13,6 +14,7 @@ type KeyMapFunction = ( const KeyMap: Record = { notification_type: (_payload, type) => Notification[type], + event: 'event', subject: 'subject', message: 'message', image: 'image', @@ -22,13 +24,12 @@ const KeyMap: Record = { notifyuser_settings_discordId: 'notifyUser.settings.discordId', notifyuser_settings_telegramChatId: 'notifyUser.settings.telegramChatId', media_tmdbid: 'media.tmdbId', - media_imdbid: 'media.imdbId', media_tvdbid: 'media.tvdbId', media_type: 'media.mediaType', media_status: (payload) => - payload.media?.status ? MediaStatus[payload.media?.status] : '', + payload.media ? MediaStatus[payload.media.status] : '', media_status4k: (payload) => - payload.media?.status ? MediaStatus[payload.media?.status4k] : '', + payload.media ? MediaStatus[payload.media.status4k] : '', request_id: 'request.id', requestedBy_username: 'request.requestedBy.displayName', requestedBy_email: 'request.requestedBy.email', @@ -36,6 +37,22 @@ const KeyMap: Record = { requestedBy_settings_discordId: 'request.requestedBy.settings.discordId', requestedBy_settings_telegramChatId: 'request.requestedBy.settings.telegramChatId', + issue_id: 'issue.id', + issue_type: (payload) => + payload.issue ? IssueType[payload.issue.issueType] : '', + issue_status: (payload) => + payload.issue ? IssueStatus[payload.issue.status] : '', + reportedBy_username: 'issue.createdBy.displayName', + reportedBy_email: 'issue.createdBy.email', + reportedBy_avatar: 'issue.createdBy.avatar', + reportedBy_settings_discordId: 'issue.createdBy.settings.discordId', + reportedBy_settings_telegramChatId: 'issue.createdBy.settings.telegramChatId', + comment_message: 'comment.message', + commentedBy_username: 'comment.user.displayName', + commentedBy_email: 'comment.user.email', + commentedBy_avatar: 'comment.user.avatar', + commentedBy_settings_discordId: 'comment.user.settings.discordId', + commentedBy_settings_telegramChatId: 'comment.user.settings.telegramChatId', }; class WebhookAgent @@ -78,6 +95,22 @@ class WebhookAgent } delete finalPayload[key]; key = 'request'; + } else if (key === '{{issue}}') { + if (payload.issue) { + finalPayload.issue = finalPayload[key]; + } else { + finalPayload.issue = null; + } + delete finalPayload[key]; + key = 'issue'; + } else if (key === '{{comment}}') { + if (payload.comment) { + finalPayload.comment = finalPayload[key]; + } else { + finalPayload.comment = null; + } + delete finalPayload[key]; + key = 'comment'; } if (typeof finalPayload[key] === 'string') { diff --git a/server/lib/notifications/agents/webpush.ts b/server/lib/notifications/agents/webpush.ts index 1ab03ba6..e61e86f6 100644 --- a/server/lib/notifications/agents/webpush.ts +++ b/server/lib/notifications/agents/webpush.ts @@ -1,11 +1,11 @@ import { getRepository } from 'typeorm'; import webpush from 'web-push'; -import { Notification } from '..'; +import { Notification, shouldSendAdminNotification } from '..'; +import { IssueType, IssueTypeName } from '../../../constants/issue'; import { MediaType } from '../../../constants/media'; import { User } from '../../../entity/User'; import { UserPushSubscription } from '../../../entity/UserPushSubscription'; import logger from '../../../logger'; -import { Permission } from '../../permissions'; import { getSettings, NotificationAgentConfig, @@ -15,12 +15,11 @@ import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; interface PushNotificationPayload { notificationType: string; - mediaType?: 'movie' | 'tv'; - tmdbId?: number; subject: string; message?: string; image?: string; actionUrl?: string; + actionUrlTitle?: string; requestId?: number; } @@ -42,97 +41,79 @@ class WebPushAgent type: Notification, payload: NotificationPayload ): PushNotificationPayload { + const mediaType = payload.media + ? payload.media.mediaType === MediaType.MOVIE + ? 'movie' + : 'series' + : undefined; + + const issueType = payload.issue + ? payload.issue.issueType !== IssueType.OTHER + ? `${IssueTypeName[payload.issue.issueType].toLowerCase()} issue` + : 'issue' + : undefined; + + let message: string | undefined; switch (type) { case Notification.TEST_NOTIFICATION: - return { - notificationType: Notification[type], - subject: payload.subject, - message: payload.message, - }; + message = payload.message; + break; case Notification.MEDIA_APPROVED: - return { - notificationType: Notification[type], - subject: payload.subject, - message: `Your ${ - payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series' - } request has been approved.`, - image: payload.image, - mediaType: payload.media?.mediaType, - tmdbId: payload.media?.tmdbId, - requestId: payload.request?.id, - actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`, - }; + message = `Your ${mediaType} request has been approved.`; + break; case Notification.MEDIA_AUTO_APPROVED: - return { - notificationType: Notification[type], - subject: payload.subject, - message: `Automatically approved a new ${ - payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series' - } request from ${payload.request?.requestedBy.displayName}.`, - image: payload.image, - mediaType: payload.media?.mediaType, - tmdbId: payload.media?.tmdbId, - requestId: payload.request?.id, - actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`, - }; + message = `Automatically approved a new ${mediaType} request from ${payload.request?.requestedBy.displayName}.`; + break; case Notification.MEDIA_AVAILABLE: - return { - notificationType: Notification[type], - subject: payload.subject, - message: `Your ${ - payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series' - } request is now available!`, - image: payload.image, - mediaType: payload.media?.mediaType, - tmdbId: payload.media?.tmdbId, - requestId: payload.request?.id, - actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`, - }; + message = `Your ${mediaType} request is now available!`; + break; case Notification.MEDIA_DECLINED: - return { - notificationType: Notification[type], - subject: payload.subject, - message: `Your ${ - payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series' - } request was declined.`, - image: payload.image, - mediaType: payload.media?.mediaType, - tmdbId: payload.media?.tmdbId, - requestId: payload.request?.id, - actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`, - }; + message = `Your ${mediaType} request was declined.`; + break; case Notification.MEDIA_FAILED: - return { - notificationType: Notification[type], - subject: payload.subject, - message: `Failed to process ${ - payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series' - } request.`, - image: payload.image, - mediaType: payload.media?.mediaType, - tmdbId: payload.media?.tmdbId, - requestId: payload.request?.id, - actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`, - }; + message = `Failed to process ${mediaType} request.`; + break; case Notification.MEDIA_PENDING: - return { - notificationType: Notification[type], - subject: payload.subject, - message: `Approval required for new ${ - payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series' - } request from ${payload.request?.requestedBy.displayName}.`, - image: payload.image, - mediaType: payload.media?.mediaType, - tmdbId: payload.media?.tmdbId, - requestId: payload.request?.id, - actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`, - }; + message = `Approval required for a new ${mediaType} request from ${payload.request?.requestedBy.displayName}.`; + break; + case Notification.ISSUE_CREATED: + message = `A new ${issueType} was reported by ${payload.issue?.createdBy.displayName}.`; + break; + case Notification.ISSUE_COMMENT: + message = `${payload.comment?.user.displayName} commented on the ${issueType}.`; + break; + case Notification.ISSUE_RESOLVED: + message = `The ${issueType} was marked as resolved by ${payload.issue?.modifiedBy?.displayName}!`; + break; + case Notification.ISSUE_REOPENED: + message = `The ${issueType} was reopened by ${payload.issue?.modifiedBy?.displayName}.`; + break; default: return { notificationType: Notification[type], subject: 'Unknown', }; } + + const actionUrl = payload.issue + ? `/issue/${payload.issue.id}` + : payload.media + ? `/${payload.media.mediaType}/${payload.media.tmdbId}` + : undefined; + + const actionUrlTitle = actionUrl + ? `View ${payload.issue ? 'Issue' : 'Media'}` + : undefined; + + return { + notificationType: Notification[type], + subject: payload.subject, + message, + image: payload.image, + requestId: payload.request?.id, + actionUrl, + actionUrlTitle, + }; } public shouldSend(): boolean { @@ -151,7 +132,7 @@ class WebPushAgent const userPushSubRepository = getRepository(UserPushSubscription); const settings = getSettings(); - let pushSubs: UserPushSubscription[] = []; + const pushSubs: UserPushSubscription[] = []; const mainUser = await userRepository.findOne({ where: { id: 1 } }); @@ -169,13 +150,14 @@ class WebPushAgent where: { user: payload.notifyUser.id }, }); - pushSubs = notifySubs; - } else if (!payload.notifyUser) { + pushSubs.push(...notifySubs); + } + + if (payload.notifyAdmin) { const users = await userRepository.find(); const manageUsers = users.filter( (user) => - user.hasPermission(Permission.MANAGE_REQUESTS) && // Check if user has webpush notifications enabled and fallback to true if undefined // since web push should default to true (user.settings?.hasNotificationType( @@ -183,9 +165,7 @@ class WebPushAgent type ) ?? true) && - // Check if it's the user's own auto-approved request - (type !== Notification.MEDIA_AUTO_APPROVED || - user.id !== payload.request?.requestedBy.id) + shouldSendAdminNotification(type, user, payload) ); const allSubs = await userPushSubRepository @@ -196,7 +176,7 @@ class WebPushAgent }) .getMany(); - pushSubs = allSubs; + pushSubs.push(...allSubs); } if (mainUser && pushSubs.length > 0) { diff --git a/server/lib/notifications/index.ts b/server/lib/notifications/index.ts index 8769360f..b8111d02 100644 --- a/server/lib/notifications/index.ts +++ b/server/lib/notifications/index.ts @@ -1,4 +1,6 @@ +import { User } from '../../entity/User'; import logger from '../../logger'; +import { Permission } from '../permissions'; import type { NotificationAgent, NotificationPayload } from './agents/agent'; export enum Notification { @@ -13,6 +15,7 @@ export enum Notification { ISSUE_CREATED = 256, ISSUE_COMMENT = 512, ISSUE_RESOLVED = 1024, + ISSUE_REOPENED = 2048, } export const hasNotificationType = ( @@ -41,6 +44,50 @@ export const hasNotificationType = ( return !!(value & total); }; +export const getAdminPermission = (type: Notification): Permission => { + switch (type) { + case Notification.MEDIA_PENDING: + case Notification.MEDIA_APPROVED: + case Notification.MEDIA_AVAILABLE: + case Notification.MEDIA_FAILED: + case Notification.MEDIA_DECLINED: + case Notification.MEDIA_AUTO_APPROVED: + return Permission.MANAGE_REQUESTS; + case Notification.ISSUE_CREATED: + case Notification.ISSUE_COMMENT: + case Notification.ISSUE_RESOLVED: + case Notification.ISSUE_REOPENED: + return Permission.MANAGE_ISSUES; + default: + return Permission.ADMIN; + } +}; + +export const shouldSendAdminNotification = ( + type: Notification, + user: User, + payload: NotificationPayload +): boolean => { + return ( + user.id !== payload.notifyUser?.id && + user.hasPermission(getAdminPermission(type)) && + // Check if the user submitted this request (on behalf of themself OR another user) + (type !== Notification.MEDIA_AUTO_APPROVED || + user.id !== + (payload.request?.modifiedBy ?? payload.request?.requestedBy)?.id) && + // Check if the user created this issue + (type !== Notification.ISSUE_CREATED || + user.id !== payload.issue?.createdBy.id) && + // Check if the user submitted this issue comment + (type !== Notification.ISSUE_COMMENT || + user.id !== payload.comment?.user.id) && + // Check if the user resolved/reopened this issue + ((type !== Notification.ISSUE_RESOLVED && + type !== Notification.ISSUE_REOPENED) || + user.id !== payload.issue?.modifiedBy?.id) + ); +}; + class NotificationManager { private activeAgents: NotificationAgent[] = []; diff --git a/server/lib/settings.ts b/server/lib/settings.ts index b4729e58..f7780dfc 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -350,7 +350,7 @@ class Settings { options: { webhookUrl: '', jsonPayload: - 'IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJzdWJqZWN0XCI6IFwie3tzdWJqZWN0fX1cIixcbiAgICBcIm1lc3NhZ2VcIjogXCJ7e21lc3NhZ2V9fVwiLFxuICAgIFwiaW1hZ2VcIjogXCJ7e2ltYWdlfX1cIixcbiAgICBcImVtYWlsXCI6IFwie3tub3RpZnl1c2VyX2VtYWlsfX1cIixcbiAgICBcInVzZXJuYW1lXCI6IFwie3tub3RpZnl1c2VyX3VzZXJuYW1lfX1cIixcbiAgICBcImF2YXRhclwiOiBcInt7bm90aWZ5dXNlcl9hdmF0YXJ9fVwiLFxuICAgIFwie3ttZWRpYX19XCI6IHtcbiAgICAgICAgXCJtZWRpYV90eXBlXCI6IFwie3ttZWRpYV90eXBlfX1cIixcbiAgICAgICAgXCJ0bWRiSWRcIjogXCJ7e21lZGlhX3RtZGJpZH19XCIsXG4gICAgICAgIFwiaW1kYklkXCI6IFwie3ttZWRpYV9pbWRiaWR9fVwiLFxuICAgICAgICBcInR2ZGJJZFwiOiBcInt7bWVkaWFfdHZkYmlkfX1cIixcbiAgICAgICAgXCJzdGF0dXNcIjogXCJ7e21lZGlhX3N0YXR1c319XCIsXG4gICAgICAgIFwic3RhdHVzNGtcIjogXCJ7e21lZGlhX3N0YXR1czRrfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW10sXG4gICAgXCJ7e3JlcXVlc3R9fVwiOiB7XG4gICAgICAgIFwicmVxdWVzdF9pZFwiOiBcInt7cmVxdWVzdF9pZH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfZW1haWxcIjogXCJ7e3JlcXVlc3RlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV91c2VybmFtZVwiOiBcInt7cmVxdWVzdGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2F2YXRhclwiOiBcInt7cmVxdWVzdGVkQnlfYXZhdGFyfX1cIlxuICAgIH1cbn0i', + 'IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJldmVudFwiOiBcInt7ZXZlbnR9fVwiLFxuICAgIFwic3ViamVjdFwiOiBcInt7c3ViamVjdH19XCIsXG4gICAgXCJtZXNzYWdlXCI6IFwie3ttZXNzYWdlfX1cIixcbiAgICBcImltYWdlXCI6IFwie3tpbWFnZX19XCIsXG4gICAgXCJ7e21lZGlhfX1cIjoge1xuICAgICAgICBcIm1lZGlhX3R5cGVcIjogXCJ7e21lZGlhX3R5cGV9fVwiLFxuICAgICAgICBcInRtZGJJZFwiOiBcInt7bWVkaWFfdG1kYmlkfX1cIixcbiAgICAgICAgXCJ0dmRiSWRcIjogXCJ7e21lZGlhX3R2ZGJpZH19XCIsXG4gICAgICAgIFwic3RhdHVzXCI6IFwie3ttZWRpYV9zdGF0dXN9fVwiLFxuICAgICAgICBcInN0YXR1czRrXCI6IFwie3ttZWRpYV9zdGF0dXM0a319XCJcbiAgICB9LFxuICAgIFwie3tyZXF1ZXN0fX1cIjoge1xuICAgICAgICBcInJlcXVlc3RfaWRcIjogXCJ7e3JlcXVlc3RfaWR9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2VtYWlsXCI6IFwie3tyZXF1ZXN0ZWRCeV9lbWFpbH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfdXNlcm5hbWVcIjogXCJ7e3JlcXVlc3RlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV9hdmF0YXJcIjogXCJ7e3JlcXVlc3RlZEJ5X2F2YXRhcn19XCJcbiAgICB9LFxuICAgIFwie3tpc3N1ZX19XCI6IHtcbiAgICAgICAgXCJpc3N1ZV9pZFwiOiBcInt7aXNzdWVfaWR9fVwiLFxuICAgICAgICBcImlzc3VlX3R5cGVcIjogXCJ7e2lzc3VlX3R5cGV9fVwiLFxuICAgICAgICBcImlzc3VlX3N0YXR1c1wiOiBcInt7aXNzdWVfc3RhdHVzfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2VtYWlsXCI6IFwie3tyZXBvcnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X3VzZXJuYW1lXCI6IFwie3tyZXBvcnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2F2YXRhclwiOiBcInt7cmVwb3J0ZWRCeV9hdmF0YXJ9fVwiXG4gICAgfSxcbiAgICBcInt7Y29tbWVudH19XCI6IHtcbiAgICAgICAgXCJjb21tZW50X21lc3NhZ2VcIjogXCJ7e2NvbW1lbnRfbWVzc2FnZX19XCIsXG4gICAgICAgIFwiY29tbWVudGVkQnlfZW1haWxcIjogXCJ7e2NvbW1lbnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJjb21tZW50ZWRCeV91c2VybmFtZVwiOiBcInt7Y29tbWVudGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcImNvbW1lbnRlZEJ5X2F2YXRhclwiOiBcInt7Y29tbWVudGVkQnlfYXZhdGFyfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW11cbn0i', }, }, webpush: { diff --git a/server/routes/issue.ts b/server/routes/issue.ts index b7faa239..c7db5232 100644 --- a/server/routes/issue.ts +++ b/server/routes/issue.ts @@ -277,6 +277,7 @@ issueRoutes.post<{ issueId: string; status: string }, Issue>( } issue.status = newStatus; + issue.modifiedBy = req.user; await issueRepository.save(issue); diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index bb21c7b6..d98debb7 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -1,5 +1,7 @@ import { Router } from 'express'; +import { User } from '../../entity/User'; import { Notification } from '../../lib/notifications'; +import { NotificationAgent } from '../../lib/notifications/agents/agent'; import DiscordAgent from '../../lib/notifications/agents/discord'; import EmailAgent from '../../lib/notifications/agents/email'; import LunaSeaAgent from '../../lib/notifications/agents/lunasea'; @@ -13,6 +15,14 @@ import { getSettings } from '../../lib/settings'; const notificationRoutes = Router(); +const sendTestNotification = async (agent: NotificationAgent, user: User) => + await agent.send(Notification.TEST_NOTIFICATION, { + notifyAdmin: false, + notifyUser: user, + subject: 'Test Notification', + message: 'Check check, 1, 2, 3. Are we coming in clear?', + }); + notificationRoutes.get('/discord', (_req, res) => { const settings = getSettings(); @@ -37,14 +47,7 @@ notificationRoutes.post('/discord/test', async (req, res, next) => { } const discordAgent = new DiscordAgent(req.body); - if ( - await discordAgent.send(Notification.TEST_NOTIFICATION, { - notifyUser: req.user, - subject: 'Test Notification', - message: - 'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?', - }) - ) { + if (await sendTestNotification(discordAgent, req.user)) { return res.status(204).send(); } else { return next({ @@ -78,14 +81,7 @@ notificationRoutes.post('/slack/test', async (req, res, next) => { } const slackAgent = new SlackAgent(req.body); - if ( - await slackAgent.send(Notification.TEST_NOTIFICATION, { - notifyUser: req.user, - subject: 'Test Notification', - message: - 'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?', - }) - ) { + if (await sendTestNotification(slackAgent, req.user)) { return res.status(204).send(); } else { return next({ @@ -119,14 +115,7 @@ notificationRoutes.post('/telegram/test', async (req, res, next) => { } const telegramAgent = new TelegramAgent(req.body); - if ( - await telegramAgent.send(Notification.TEST_NOTIFICATION, { - notifyUser: req.user, - subject: 'Test Notification', - message: - 'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?', - }) - ) { + if (await sendTestNotification(telegramAgent, req.user)) { return res.status(204).send(); } else { return next({ @@ -160,14 +149,7 @@ notificationRoutes.post('/pushbullet/test', async (req, res, next) => { } const pushbulletAgent = new PushbulletAgent(req.body); - if ( - await pushbulletAgent.send(Notification.TEST_NOTIFICATION, { - notifyUser: req.user, - subject: 'Test Notification', - message: - 'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?', - }) - ) { + if (await sendTestNotification(pushbulletAgent, req.user)) { return res.status(204).send(); } else { return next({ @@ -201,14 +183,7 @@ notificationRoutes.post('/pushover/test', async (req, res, next) => { } const pushoverAgent = new PushoverAgent(req.body); - if ( - await pushoverAgent.send(Notification.TEST_NOTIFICATION, { - notifyUser: req.user, - subject: 'Test Notification', - message: - 'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?', - }) - ) { + if (await sendTestNotification(pushoverAgent, req.user)) { return res.status(204).send(); } else { return next({ @@ -242,14 +217,7 @@ notificationRoutes.post('/email/test', async (req, res, next) => { } const emailAgent = new EmailAgent(req.body); - if ( - await emailAgent.send(Notification.TEST_NOTIFICATION, { - notifyUser: req.user, - subject: 'Test Notification', - message: - 'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?', - }) - ) { + if (await sendTestNotification(emailAgent, req.user)) { return res.status(204).send(); } else { return next({ @@ -283,14 +251,7 @@ notificationRoutes.post('/webpush/test', async (req, res, next) => { } const webpushAgent = new WebPushAgent(req.body); - if ( - await webpushAgent.send(Notification.TEST_NOTIFICATION, { - notifyUser: req.user, - subject: 'Test Notification', - message: - 'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?', - }) - ) { + if (await sendTestNotification(webpushAgent, req.user)) { return res.status(204).send(); } else { return next({ @@ -369,14 +330,7 @@ notificationRoutes.post('/webhook/test', async (req, res, next) => { }; const webhookAgent = new WebhookAgent(testBody); - if ( - await webhookAgent.send(Notification.TEST_NOTIFICATION, { - notifyUser: req.user, - subject: 'Test Notification', - message: - 'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?', - }) - ) { + if (await sendTestNotification(webhookAgent, req.user)) { return res.status(204).send(); } else { return next({ @@ -413,14 +367,7 @@ notificationRoutes.post('/lunasea/test', async (req, res, next) => { } const lunaseaAgent = new LunaSeaAgent(req.body); - if ( - await lunaseaAgent.send(Notification.TEST_NOTIFICATION, { - notifyUser: req.user, - subject: 'Test Notification', - message: - 'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?', - }) - ) { + if (await sendTestNotification(lunaseaAgent, req.user)) { return res.status(204).send(); } else { return next({ diff --git a/server/subscriber/IssueCommentSubscriber.ts b/server/subscriber/IssueCommentSubscriber.ts index aab6bd94..c844b614 100644 --- a/server/subscriber/IssueCommentSubscriber.ts +++ b/server/subscriber/IssueCommentSubscriber.ts @@ -1,3 +1,4 @@ +import { sortBy } from 'lodash'; import { EntitySubscriberInterface, EventSubscriber, @@ -5,9 +6,12 @@ import { InsertEvent, } from 'typeorm'; import TheMovieDb from '../api/themoviedb'; +import { IssueType, IssueTypeName } from '../constants/issue'; import { MediaType } from '../constants/media'; import IssueComment from '../entity/IssueComment'; +import Media from '../entity/Media'; import notificationManager, { Notification } from '../lib/notifications'; +import { Permission } from '../lib/permissions'; @EventSubscriber() export class IssueCommentSubscriber @@ -18,41 +22,67 @@ export class IssueCommentSubscriber } private async sendIssueCommentNotification(entity: IssueComment) { - const issueCommentRepository = getRepository(IssueComment); let title: string; let image: string; const tmdb = new TheMovieDb(); - const issuecomment = await issueCommentRepository.findOne({ - where: { id: entity.id }, - relations: ['issue'], - }); - - const issue = issuecomment?.issue; + const issue = ( + await getRepository(IssueComment).findOne({ + where: { id: entity.id }, + relations: ['issue'], + }) + )?.issue; if (!issue) { return; } - if (issue.media.mediaType === MediaType.MOVIE) { - const movie = await tmdb.getMovie({ movieId: issue.media.tmdbId }); + const media = await getRepository(Media).findOne({ + where: { id: issue.media.id }, + }); + if (!media) { + return; + } - title = movie.title; + if (media.mediaType === MediaType.MOVIE) { + const movie = await tmdb.getMovie({ movieId: media.tmdbId }); + + title = `${movie.title}${ + movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' + }`; image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`; } else { - const tvshow = await tmdb.getTvShow({ tvId: issue.media.tmdbId }); + const tvshow = await tmdb.getTvShow({ tvId: media.tmdbId }); - title = tvshow.name; + title = `${tvshow.name}${ + tvshow.first_air_date ? ` (${tvshow.first_air_date.slice(0, 4)})` : '' + }`; image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tvshow.poster_path}`; } - notificationManager.sendNotification(Notification.ISSUE_COMMENT, { - subject: `New Issue Comment: ${title}`, - message: entity.message, - issue, - image, - notifyUser: - issue.createdBy.id !== entity.user.id ? issue.createdBy : undefined, - }); + const [firstComment] = sortBy(issue.comments, 'id'); + + if (entity.id !== firstComment.id) { + // Send notifications to all issue managers + notificationManager.sendNotification(Notification.ISSUE_COMMENT, { + event: `New Comment on ${ + issue.issueType !== IssueType.OTHER + ? `${IssueTypeName[issue.issueType]} ` + : '' + }Issue`, + subject: title, + message: firstComment.message, + comment: entity, + issue, + media, + image, + notifyAdmin: true, + notifyUser: + !issue.createdBy.hasPermission(Permission.MANAGE_ISSUES) && + issue.createdBy.id !== entity.user.id + ? issue.createdBy + : undefined, + }); + } } public afterInsert(event: InsertEvent): void { diff --git a/server/subscriber/IssueSubscriber.ts b/server/subscriber/IssueSubscriber.ts index e76d2fd7..5cf2be59 100644 --- a/server/subscriber/IssueSubscriber.ts +++ b/server/subscriber/IssueSubscriber.ts @@ -1,12 +1,16 @@ +import { sortBy } from 'lodash'; import { EntitySubscriberInterface, EventSubscriber, InsertEvent, + UpdateEvent, } from 'typeorm'; import TheMovieDb from '../api/themoviedb'; +import { IssueStatus, IssueType, IssueTypeName } from '../constants/issue'; import { MediaType } from '../constants/media'; import Issue from '../entity/Issue'; import notificationManager, { Notification } from '../lib/notifications'; +import { Permission } from '../lib/permissions'; @EventSubscriber() export class IssueSubscriber implements EntitySubscriberInterface { @@ -14,29 +18,75 @@ export class IssueSubscriber implements EntitySubscriberInterface { return Issue; } - private async sendIssueCreatedNotification(entity: Issue) { + private async sendIssueNotification(entity: Issue, type: Notification) { let title: string; let image: string; const tmdb = new TheMovieDb(); if (entity.media.mediaType === MediaType.MOVIE) { const movie = await tmdb.getMovie({ movieId: entity.media.tmdbId }); - title = movie.title; + title = `${movie.title}${ + movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' + }`; image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`; } else { const tvshow = await tmdb.getTvShow({ tvId: entity.media.tmdbId }); - title = tvshow.name; + title = `${tvshow.name}${ + tvshow.first_air_date ? ` (${tvshow.first_air_date.slice(0, 4)})` : '' + }`; image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tvshow.poster_path}`; } - const [firstComment] = entity.comments; + const [firstComment] = sortBy(entity.comments, 'id'); + const extra: { name: string; value: string }[] = []; - notificationManager.sendNotification(Notification.ISSUE_CREATED, { + if (entity.media.mediaType === MediaType.TV && entity.problemSeason > 0) { + extra.push({ + name: 'Affected Season', + value: entity.problemSeason.toString(), + }); + + if (entity.problemEpisode > 0) { + extra.push({ + name: 'Affected Episode', + value: entity.problemEpisode.toString(), + }); + } + } + + notificationManager.sendNotification(type, { + event: + type === Notification.ISSUE_CREATED + ? `New ${ + entity.issueType !== IssueType.OTHER + ? `${IssueTypeName[entity.issueType]} ` + : '' + }Issue Reported` + : type === Notification.ISSUE_RESOLVED + ? `${ + entity.issueType !== IssueType.OTHER + ? `${IssueTypeName[entity.issueType]} ` + : '' + }Issue Resolved` + : `${ + entity.issueType !== IssueType.OTHER + ? `${IssueTypeName[entity.issueType]} ` + : '' + }Issue Reopened`, subject: title, message: firstComment.message, issue: entity, + media: entity.media, image, + extra, + notifyAdmin: true, + notifyUser: + !entity.createdBy.hasPermission(Permission.MANAGE_ISSUES) && + (type === Notification.ISSUE_RESOLVED || + type === Notification.ISSUE_REOPENED) + ? entity.createdBy + : undefined, }); } @@ -45,6 +95,30 @@ export class IssueSubscriber implements EntitySubscriberInterface { return; } - this.sendIssueCreatedNotification(event.entity); + this.sendIssueNotification(event.entity, Notification.ISSUE_CREATED); + } + + public beforeUpdate(event: UpdateEvent): void { + if (!event.entity) { + return; + } + + if ( + event.entity.status === IssueStatus.RESOLVED && + event.databaseEntity.status !== IssueStatus.RESOLVED + ) { + this.sendIssueNotification( + event.entity as Issue, + Notification.ISSUE_RESOLVED + ); + } else if ( + event.entity.status === IssueStatus.OPEN && + event.databaseEntity.status !== IssueStatus.OPEN + ) { + this.sendIssueNotification( + event.entity as Issue, + Notification.ISSUE_REOPENED + ); + } } } diff --git a/server/subscriber/MediaSubscriber.ts b/server/subscriber/MediaSubscriber.ts index f50e1d66..fe7043aa 100644 --- a/server/subscriber/MediaSubscriber.ts +++ b/server/subscriber/MediaSubscriber.ts @@ -31,6 +31,8 @@ export class MediaSubscriber implements EntitySubscriberInterface { relatedRequests.forEach((request) => { notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, { + event: 'Movie Now Available', + notifyAdmin: false, notifyUser: request.requestedBy, subject: `${movie.title}${ movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' @@ -42,7 +44,7 @@ export class MediaSubscriber implements EntitySubscriberInterface { }), media: entity, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, - request: request, + request, }); }); } @@ -91,6 +93,7 @@ export class MediaSubscriber implements EntitySubscriberInterface { ); const tv = await tmdb.getTvShow({ tvId: entity.tmdbId }); notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, { + event: 'Series Now Available', subject: `${tv.name}${ tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : '' }`, @@ -99,18 +102,19 @@ export class MediaSubscriber implements EntitySubscriberInterface { separator: /\s/, omission: '…', }), + notifyAdmin: false, notifyUser: request.requestedBy, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`, media: entity, extra: [ { - name: 'Seasons', + name: 'Requested Seasons', value: request.seasons .map((season) => season.seasonNumber) .join(', '), }, ], - request: request, + request, }); } } diff --git a/server/templates/email/media-issue/html.pug b/server/templates/email/media-issue/html.pug new file mode 100644 index 00000000..920542e0 --- /dev/null +++ b/server/templates/email/media-issue/html.pug @@ -0,0 +1,53 @@ +doctype html +head + meta(charset='utf-8') + meta(name='x-apple-disable-message-reformatting') + meta(http-equiv='x-ua-compatible' content='ie=edge') + meta(name='viewport' content='width=device-width, initial-scale=1') + meta(name='format-detection' content='telephone=no, date=no, address=no, email=no') + link(href='https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap' rel='stylesheet' media='screen') + style. + .title:hover * { + text-decoration: underline; + } + @media only screen and (max-width:600px) { + table { + font-size: 20px !important; + width: 100% !important; + } + } +div(style='display: block; background-color: #111827; padding: 2.5rem 0;') + table(style='margin: 0 auto; font-family: Inter, Arial, sans-serif; color: #fff; font-size: 16px; width: 26rem;') + tr + td(style="text-align: center;") + if applicationUrl + a(href=applicationUrl style='margin: 0 1rem;') + img(src=applicationUrl +'/logo_full.png' style='width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') + else + div(style='margin: 0 1rem 2.5rem; font-size: 3em; font-weight: 700;') + | #{applicationTitle} + if recipientName !== recipientEmail + tr + td(style='text-align: center;') + div(style='margin: 1rem 0 0; font-size: 1.25em;') + | Hi, #{recipientName.replace(/\.|@/g, ((x) => x + '\ufeff'))}! + tr + td(style='text-align: center;') + div(style='margin: 1rem 0 0; font-size: 1.25em;') + | #{body} + if issueComment + tr + td(style='text-align: center;') + div(style='margin: 1rem 0 0; font-size: 1.25em;') + | #{issueComment} + else if issueDescription + tr + td(style='text-align: center;') + div(style='margin: 1rem 0 0; font-size: 1.25em;') + | #{issueDescription} + if actionUrl + tr + td + a(href=actionUrl style='display: block; margin: 1.5rem 3rem 0; text-decoration: none; font-size: 1.0em; line-height: 2.25em;') + span(style='padding: 0.2rem; font-weight: 500; text-align: center; border-radius: 10px; background-color: rgb(99,102,241); color: #fff; display: block; border: 1px solid rgba(255,255,255,0.2);') + | View Issue in #{applicationTitle} diff --git a/server/templates/email/media-issue/subject.pug b/server/templates/email/media-issue/subject.pug new file mode 100644 index 00000000..1bf154ba --- /dev/null +++ b/server/templates/email/media-issue/subject.pug @@ -0,0 +1 @@ +!= `${event} - ${mediaName} [${applicationTitle}]` diff --git a/server/templates/email/media-request/html.pug b/server/templates/email/media-request/html.pug index 83db48d4..334095df 100644 --- a/server/templates/email/media-request/html.pug +++ b/server/templates/email/media-request/html.pug @@ -66,4 +66,4 @@ div(style='display: block; background-color: #111827; padding: 2.5rem 0;') td a(href=actionUrl style='display: block; margin: 1.5rem 3rem 0; text-decoration: none; font-size: 1.0em; line-height: 2.25em;') span(style='padding: 0.2rem; font-weight: 500; text-align: center; border-radius: 10px; background-color: rgb(99,102,241); color: #fff; display: block; border: 1px solid rgba(255,255,255,0.2);') - | Open in #{applicationTitle} + | View Media in #{applicationTitle} diff --git a/server/templates/email/media-request/subject.pug b/server/templates/email/media-request/subject.pug index a0f50fba..1bf154ba 100644 --- a/server/templates/email/media-request/subject.pug +++ b/server/templates/email/media-request/subject.pug @@ -1 +1 @@ -!= `${requestType} - ${mediaName} [${applicationTitle}]` +!= `${event} - ${mediaName} [${applicationTitle}]` diff --git a/src/components/IssueModal/constants.ts b/src/components/IssueModal/constants.ts index 4c5b13e4..92cf6bc7 100644 --- a/src/components/IssueModal/constants.ts +++ b/src/components/IssueModal/constants.ts @@ -4,7 +4,7 @@ import { IssueType } from '../../../server/constants/issue'; const messages = defineMessages({ issueAudio: 'Audio', issueVideo: 'Video', - issueSubtitles: 'Subtitles', + issueSubtitles: 'Subtitle', issueOther: 'Other', }); diff --git a/src/components/NotificationTypeSelector/index.tsx b/src/components/NotificationTypeSelector/index.tsx index bef9ed31..567ce052 100644 --- a/src/components/NotificationTypeSelector/index.tsx +++ b/src/components/NotificationTypeSelector/index.tsx @@ -44,12 +44,21 @@ const messages = defineMessages({ issuecommentDescription: 'Send notifications when issues receive new comments.', userissuecommentDescription: - 'Get notified when your issues receive new comments.', + 'Get notified when issues you reported receive new comments.', adminissuecommentDescription: - 'Get notified when issues receive new comments.', + 'Get notified when other users comment on issues.', issueresolved: 'Issue Resolved', issueresolvedDescription: 'Send notifications when issues are resolved.', - userissueresolvedDescription: 'Get notified when your issues are resolved.', + userissueresolvedDescription: + 'Get notified when issues you reported are resolved.', + adminissueresolvedDescription: + 'Get notified when issues are resolved by other users.', + issuereopened: 'Issue Reopened', + issuereopenedDescription: 'Send notifications when issues are reopened.', + userissuereopenedDescription: + 'Get notified when issues you reported are reopened.', + adminissuereopenedDescription: + 'Get notified when issues are reopened by other users.', }); export const hasNotificationType = ( @@ -90,6 +99,7 @@ export enum Notification { ISSUE_CREATED = 256, ISSUE_COMMENT = 512, ISSUE_RESOLVED = 1024, + ISSUE_REOPENED = 2048, } export const ALL_NOTIFICATIONS = Object.values(Notification) @@ -287,7 +297,9 @@ const NotificationTypeSelector: React.FC = ({ name: intl.formatMessage(messages.issueresolved), description: intl.formatMessage( user - ? messages.userissueresolvedDescription + ? hasPermission(Permission.MANAGE_ISSUES) + ? messages.adminissueresolvedDescription + : messages.userissueresolvedDescription : messages.issueresolvedDescription ), value: Notification.ISSUE_RESOLVED, @@ -296,7 +308,27 @@ const NotificationTypeSelector: React.FC = ({ !hasPermission([Permission.MANAGE_ISSUES, Permission.CREATE_ISSUES], { type: 'or', }), - hasNotifyUser: true, + hasNotifyUser: + !user || hasPermission(Permission.MANAGE_ISSUES) ? false : true, + }, + { + id: 'issue-reopened', + name: intl.formatMessage(messages.issuereopened), + description: intl.formatMessage( + user + ? hasPermission(Permission.MANAGE_ISSUES) + ? messages.adminissuereopenedDescription + : messages.userissuereopenedDescription + : messages.issuereopenedDescription + ), + value: Notification.ISSUE_REOPENED, + hidden: + user && + !hasPermission([Permission.MANAGE_ISSUES, Permission.CREATE_ISSUES], { + type: 'or', + }), + hasNotifyUser: + !user || hasPermission(Permission.MANAGE_ISSUES) ? false : true, }, ]; diff --git a/src/components/Settings/Notifications/NotificationsDiscord.tsx b/src/components/Settings/Notifications/NotificationsDiscord.tsx index 67cfa031..9e2a6701 100644 --- a/src/components/Settings/Notifications/NotificationsDiscord.tsx +++ b/src/components/Settings/Notifications/NotificationsDiscord.tsx @@ -6,6 +6,7 @@ import { defineMessages, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; import * as Yup from 'yup'; +import useSettings from '../../../hooks/useSettings'; import globalMessages from '../../../i18n/globalMessages'; import Button from '../../Common/Button'; import LoadingSpinner from '../../Common/LoadingSpinner'; @@ -29,6 +30,7 @@ const messages = defineMessages({ const NotificationsDiscord: React.FC = () => { const intl = useIntl(); + const settings = useSettings(); const { addToast, removeToast } = useToasts(); const [isTesting, setIsTesting] = useState(false); const { data, error, revalidate } = useSWR( @@ -195,7 +197,12 @@ const NotificationsDiscord: React.FC = () => {
- +
{errors.botUsername && touched.botUsername && (
{errors.botUsername}
diff --git a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx index 36e0a3c0..e36e4464 100644 --- a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx +++ b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx @@ -18,27 +18,38 @@ const JSONEditor = dynamic(() => import('../../../JSONEditor'), { ssr: false }); const defaultPayload = { notification_type: '{{notification_type}}', + event: '{{event}}', subject: '{{subject}}', message: '{{message}}', image: '{{image}}', - email: '{{notifyuser_email}}', - username: '{{notifyuser_username}}', - avatar: '{{notifyuser_avatar}}', '{{media}}': { media_type: '{{media_type}}', tmdbId: '{{media_tmdbid}}', - imdbId: '{{media_imdbid}}', tvdbId: '{{media_tvdbid}}', status: '{{media_status}}', status4k: '{{media_status4k}}', }, - '{{extra}}': [], '{{request}}': { request_id: '{{request_id}}', requestedBy_email: '{{requestedBy_email}}', requestedBy_username: '{{requestedBy_username}}', requestedBy_avatar: '{{requestedBy_avatar}}', }, + '{{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}}', + }, + '{{comment}}': { + comment_message: '{{comment_message}}', + commentedBy_email: '{{commentedBy_email}}', + commentedBy_username: '{{commentedBy_username}}', + commentedBy_avatar: '{{commentedBy_avatar}}', + }, + '{{extra}}': [], }; const messages = defineMessages({ diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index de98417a..74bec8f8 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -102,7 +102,7 @@ "components.IssueModal.CreateIssueModal.whatswrong": "What's wrong?", "components.IssueModal.issueAudio": "Audio", "components.IssueModal.issueOther": "Other", - "components.IssueModal.issueSubtitles": "Subtitles", + "components.IssueModal.issueSubtitles": "Subtitle", "components.IssueModal.issueVideo": "Video", "components.LanguageSelector.languageServerDefault": "Default ({language})", "components.LanguageSelector.originalLanguageDefault": "All Languages", @@ -169,11 +169,15 @@ "components.MovieDetails.studio": "{studioCount, plural, one {Studio} other {Studios}}", "components.MovieDetails.viewfullcrew": "View Full Crew", "components.MovieDetails.watchtrailer": "Watch Trailer", - "components.NotificationTypeSelector.adminissuecommentDescription": "Get notified when issues receive new comments.", + "components.NotificationTypeSelector.adminissuecommentDescription": "Get notified when other users comment on issues.", + "components.NotificationTypeSelector.adminissuereopenedDescription": "Get notified when issues are reopened by other users.", + "components.NotificationTypeSelector.adminissueresolvedDescription": "Get notified when issues are resolved by other users.", "components.NotificationTypeSelector.issuecomment": "Issue Comment", "components.NotificationTypeSelector.issuecommentDescription": "Send notifications when issues receive new comments.", "components.NotificationTypeSelector.issuecreated": "Issue Reported", "components.NotificationTypeSelector.issuecreatedDescription": "Send notifications when issues are reported.", + "components.NotificationTypeSelector.issuereopened": "Issue Reopened", + "components.NotificationTypeSelector.issuereopenedDescription": "Send notifications when issues are reopened.", "components.NotificationTypeSelector.issueresolved": "Issue Resolved", "components.NotificationTypeSelector.issueresolvedDescription": "Send notifications when issues are resolved.", "components.NotificationTypeSelector.mediaAutoApproved": "Media Automatically Approved", @@ -189,9 +193,10 @@ "components.NotificationTypeSelector.mediarequested": "Media Requested", "components.NotificationTypeSelector.mediarequestedDescription": "Send notifications when users submit new media requests which require approval.", "components.NotificationTypeSelector.notificationTypes": "Notification Types", - "components.NotificationTypeSelector.userissuecommentDescription": "Get notified when your issues receive new comments.", + "components.NotificationTypeSelector.userissuecommentDescription": "Get notified when issues you reported receive new comments.", "components.NotificationTypeSelector.userissuecreatedDescription": "Get notified when other users report issues.", - "components.NotificationTypeSelector.userissueresolvedDescription": "Get notified when your issues are resolved.", + "components.NotificationTypeSelector.userissuereopenedDescription": "Get notified when issues you reported are reopened.", + "components.NotificationTypeSelector.userissueresolvedDescription": "Get notified when issues you reported are resolved.", "components.NotificationTypeSelector.usermediaAutoApprovedDescription": "Get notified when other users submit new media requests which are automatically approved.", "components.NotificationTypeSelector.usermediaapprovedDescription": "Get notified when your media requests are approved.", "components.NotificationTypeSelector.usermediaavailableDescription": "Get notified when your media requests become available.",