mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(notif): issue notifications (#2242)
* feat(notif): issue notifications * refactor: dedupe test notification strings * fix: webhook key parsing * fix(notif): skip send for admin who requested on behalf of another user * fix(notif): send comment notifs to admins when other admins reply * fix(notif): also send resolved notifs to admins, and reopened notifs to issue creator * fix: don't send duplicate notifications * fix(lang): tweak notification description strings * fix(notif): tweak Slack notification styling * fix(notif): tweak Pushbullet & Telegram notification styling * docs: reformat webhooks page * fix(notif): add missing issue_type & issue_status variables to LunaSea notif payloads * fix: explicitly attach media & issue objects where applicable * fix(notif): correctly notify both notifyUser and managers where applicable * fix: update default webhook payload for new installs * fix(notif): add missing comment_message to LunaSea notif payload * refactor(sw): simplify notificationclick event listener logic * fix(notif): add missing event description for MEDIA_AVAILABLE notifications
This commit is contained in:
@@ -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.
|
||||
|
||||
- `{{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).
|
||||
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.
|
||||
|
||||
| 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) |
|
||||
|
16
public/sw.js
16
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);
|
||||
|
@@ -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',
|
||||
};
|
||||
|
@@ -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' });
|
||||
|
@@ -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<T extends NotificationAgentConfig> {
|
||||
|
@@ -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,
|
||||
|
@@ -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<boolean> {
|
||||
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', {
|
||||
|
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -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.event
|
||||
? `${payload.event} - ${payload.subject}`
|
||||
: payload.subject;
|
||||
let body = payload.message ?? '';
|
||||
|
||||
const title = payload.subject;
|
||||
const plot = payload.message;
|
||||
const username = payload.request?.requestedBy.displayName;
|
||||
if (payload.request) {
|
||||
body += `\n\nRequested By: ${payload.request.requestedBy.displayName}`;
|
||||
|
||||
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;
|
||||
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) {
|
||||
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 (
|
||||
|
@@ -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<PushoverPayload> {
|
||||
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 ? `<b>${payload.subject}</b>` : '';
|
||||
let priority = 0;
|
||||
|
||||
const title = payload.subject;
|
||||
const plot = payload.message;
|
||||
const username = payload.request?.requestedBy.displayName;
|
||||
if (payload.message) {
|
||||
message += `<small>${message ? '\n' : ''}${payload.message}</small>`;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case Notification.MEDIA_PENDING:
|
||||
messageTitle = `New ${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Request`;
|
||||
message += `<b>${title}</b>`;
|
||||
if (plot) {
|
||||
message += `<small>\n${plot}</small>`;
|
||||
}
|
||||
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
|
||||
message += `<small>\n\n<b>Status</b>\nPending Approval</small>`;
|
||||
break;
|
||||
case Notification.MEDIA_APPROVED:
|
||||
messageTitle = `${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Request Approved`;
|
||||
message += `<b>${title}</b>`;
|
||||
if (plot) {
|
||||
message += `<small>\n${plot}</small>`;
|
||||
}
|
||||
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
|
||||
message += `<small>\n\n<b>Status</b>\nProcessing</small>`;
|
||||
break;
|
||||
case Notification.MEDIA_AUTO_APPROVED:
|
||||
messageTitle = `${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Request Automatically Approved`;
|
||||
message += `<b>${title}</b>`;
|
||||
if (plot) {
|
||||
message += `<small>\n${plot}</small>`;
|
||||
}
|
||||
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
|
||||
message += `<small>\n\n<b>Status</b>\nProcessing</small>`;
|
||||
break;
|
||||
case Notification.MEDIA_AVAILABLE:
|
||||
messageTitle = `${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Now Available`;
|
||||
message += `<b>${title}</b>`;
|
||||
if (plot) {
|
||||
message += `<small>\n${plot}</small>`;
|
||||
}
|
||||
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
|
||||
message += `<small>\n\n<b>Status</b>\nAvailable</small>`;
|
||||
break;
|
||||
case Notification.MEDIA_DECLINED:
|
||||
messageTitle = `${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Request Declined`;
|
||||
message += `<b>${title}</b>`;
|
||||
if (plot) {
|
||||
message += `<small>\n${plot}</small>`;
|
||||
}
|
||||
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
|
||||
message += `<small>\n\n<b>Status</b>\nDeclined</small>`;
|
||||
if (payload.request) {
|
||||
message += `<small>\n\n<b>Requested By:</b> ${payload.request.requestedBy.displayName}</small>`;
|
||||
|
||||
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 += `<small>\n<b>Request Status:</b> ${status}</small>`;
|
||||
}
|
||||
} else if (payload.comment) {
|
||||
message += `<small>\n\n<b>Comment from ${payload.comment.user.displayName}:</b> ${payload.comment.message}</small>`;
|
||||
} else if (payload.issue) {
|
||||
message += `<small>\n\n<b>Reported By:</b> ${payload.issue.createdBy.displayName}</small>`;
|
||||
message += `<small>\n<b>Issue Type:</b> ${
|
||||
IssueTypeName[payload.issue.issueType]
|
||||
}</small>`;
|
||||
message += `<small>\n<b>Issue Status:</b> ${
|
||||
payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved'
|
||||
}</small>`;
|
||||
|
||||
if (type === Notification.ISSUE_CREATED) {
|
||||
priority = 1;
|
||||
break;
|
||||
case Notification.MEDIA_FAILED:
|
||||
messageTitle = `Failed ${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Request`;
|
||||
message += `<b>${title}</b>`;
|
||||
if (plot) {
|
||||
message += `<small>\n${plot}</small>`;
|
||||
}
|
||||
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
|
||||
message += `<small>\n\n<b>Status</b>\nFailed</small>`;
|
||||
priority = 1;
|
||||
break;
|
||||
case Notification.TEST_NOTIFICATION:
|
||||
messageTitle = 'Test Notification';
|
||||
message += `<small>${plot}</small>`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const extra of payload.extra ?? []) {
|
||||
message += `<small>\n\n<b>${extra.name}</b>\n${extra.value}</small>`;
|
||||
message += `<small>\n<b>${extra.name}:</b> ${extra.value}</small>`;
|
||||
}
|
||||
|
||||
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 (
|
||||
|
@@ -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`;
|
||||
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*\nPending Approval',
|
||||
text: `*Request Status*\n${status}`,
|
||||
});
|
||||
break;
|
||||
case Notification.MEDIA_APPROVED:
|
||||
header = `${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Request Approved`;
|
||||
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*\nProcessing',
|
||||
});
|
||||
break;
|
||||
case Notification.MEDIA_AUTO_APPROVED:
|
||||
header = `${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Request Automatically Approved`;
|
||||
fields.push({
|
||||
text: `*Reported By*\n${payload.issue.createdBy.displayName}`,
|
||||
},
|
||||
{
|
||||
type: 'mrkdwn',
|
||||
text: '*Status*\nProcessing',
|
||||
});
|
||||
break;
|
||||
case Notification.MEDIA_AVAILABLE:
|
||||
header = `${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Now Available`;
|
||||
fields.push({
|
||||
text: `*Issue Type*\n${IssueTypeName[payload.issue.issueType]}`,
|
||||
},
|
||||
{
|
||||
type: 'mrkdwn',
|
||||
text: '*Status*\nAvailable',
|
||||
});
|
||||
break;
|
||||
case Notification.MEDIA_DECLINED:
|
||||
header = `${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Request Declined`;
|
||||
fields.push({
|
||||
type: 'mrkdwn',
|
||||
text: '*Status*\nDeclined',
|
||||
});
|
||||
break;
|
||||
case Notification.MEDIA_FAILED:
|
||||
header = `Failed ${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Request`;
|
||||
fields.push({
|
||||
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[] = [];
|
||||
|
||||
const blocks: EmbedBlock[] = [
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: header,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@@ -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<TelegramMessagePayload | TelegramPhotoPayload> {
|
||||
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 (
|
||||
|
@@ -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<string, string | KeyMapFunction> = {
|
||||
notification_type: (_payload, type) => Notification[type],
|
||||
event: 'event',
|
||||
subject: 'subject',
|
||||
message: 'message',
|
||||
image: 'image',
|
||||
@@ -22,13 +24,12 @@ const KeyMap: Record<string, string | KeyMapFunction> = {
|
||||
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<string, string | KeyMapFunction> = {
|
||||
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') {
|
||||
|
@@ -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) {
|
||||
|
@@ -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[] = [];
|
||||
|
||||
|
@@ -350,7 +350,7 @@ class Settings {
|
||||
options: {
|
||||
webhookUrl: '',
|
||||
jsonPayload:
|
||||
'IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJzdWJqZWN0XCI6IFwie3tzdWJqZWN0fX1cIixcbiAgICBcIm1lc3NhZ2VcIjogXCJ7e21lc3NhZ2V9fVwiLFxuICAgIFwiaW1hZ2VcIjogXCJ7e2ltYWdlfX1cIixcbiAgICBcImVtYWlsXCI6IFwie3tub3RpZnl1c2VyX2VtYWlsfX1cIixcbiAgICBcInVzZXJuYW1lXCI6IFwie3tub3RpZnl1c2VyX3VzZXJuYW1lfX1cIixcbiAgICBcImF2YXRhclwiOiBcInt7bm90aWZ5dXNlcl9hdmF0YXJ9fVwiLFxuICAgIFwie3ttZWRpYX19XCI6IHtcbiAgICAgICAgXCJtZWRpYV90eXBlXCI6IFwie3ttZWRpYV90eXBlfX1cIixcbiAgICAgICAgXCJ0bWRiSWRcIjogXCJ7e21lZGlhX3RtZGJpZH19XCIsXG4gICAgICAgIFwiaW1kYklkXCI6IFwie3ttZWRpYV9pbWRiaWR9fVwiLFxuICAgICAgICBcInR2ZGJJZFwiOiBcInt7bWVkaWFfdHZkYmlkfX1cIixcbiAgICAgICAgXCJzdGF0dXNcIjogXCJ7e21lZGlhX3N0YXR1c319XCIsXG4gICAgICAgIFwic3RhdHVzNGtcIjogXCJ7e21lZGlhX3N0YXR1czRrfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW10sXG4gICAgXCJ7e3JlcXVlc3R9fVwiOiB7XG4gICAgICAgIFwicmVxdWVzdF9pZFwiOiBcInt7cmVxdWVzdF9pZH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfZW1haWxcIjogXCJ7e3JlcXVlc3RlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV91c2VybmFtZVwiOiBcInt7cmVxdWVzdGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2F2YXRhclwiOiBcInt7cmVxdWVzdGVkQnlfYXZhdGFyfX1cIlxuICAgIH1cbn0i',
|
||||
'IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJldmVudFwiOiBcInt7ZXZlbnR9fVwiLFxuICAgIFwic3ViamVjdFwiOiBcInt7c3ViamVjdH19XCIsXG4gICAgXCJtZXNzYWdlXCI6IFwie3ttZXNzYWdlfX1cIixcbiAgICBcImltYWdlXCI6IFwie3tpbWFnZX19XCIsXG4gICAgXCJ7e21lZGlhfX1cIjoge1xuICAgICAgICBcIm1lZGlhX3R5cGVcIjogXCJ7e21lZGlhX3R5cGV9fVwiLFxuICAgICAgICBcInRtZGJJZFwiOiBcInt7bWVkaWFfdG1kYmlkfX1cIixcbiAgICAgICAgXCJ0dmRiSWRcIjogXCJ7e21lZGlhX3R2ZGJpZH19XCIsXG4gICAgICAgIFwic3RhdHVzXCI6IFwie3ttZWRpYV9zdGF0dXN9fVwiLFxuICAgICAgICBcInN0YXR1czRrXCI6IFwie3ttZWRpYV9zdGF0dXM0a319XCJcbiAgICB9LFxuICAgIFwie3tyZXF1ZXN0fX1cIjoge1xuICAgICAgICBcInJlcXVlc3RfaWRcIjogXCJ7e3JlcXVlc3RfaWR9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2VtYWlsXCI6IFwie3tyZXF1ZXN0ZWRCeV9lbWFpbH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfdXNlcm5hbWVcIjogXCJ7e3JlcXVlc3RlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV9hdmF0YXJcIjogXCJ7e3JlcXVlc3RlZEJ5X2F2YXRhcn19XCJcbiAgICB9LFxuICAgIFwie3tpc3N1ZX19XCI6IHtcbiAgICAgICAgXCJpc3N1ZV9pZFwiOiBcInt7aXNzdWVfaWR9fVwiLFxuICAgICAgICBcImlzc3VlX3R5cGVcIjogXCJ7e2lzc3VlX3R5cGV9fVwiLFxuICAgICAgICBcImlzc3VlX3N0YXR1c1wiOiBcInt7aXNzdWVfc3RhdHVzfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2VtYWlsXCI6IFwie3tyZXBvcnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X3VzZXJuYW1lXCI6IFwie3tyZXBvcnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2F2YXRhclwiOiBcInt7cmVwb3J0ZWRCeV9hdmF0YXJ9fVwiXG4gICAgfSxcbiAgICBcInt7Y29tbWVudH19XCI6IHtcbiAgICAgICAgXCJjb21tZW50X21lc3NhZ2VcIjogXCJ7e2NvbW1lbnRfbWVzc2FnZX19XCIsXG4gICAgICAgIFwiY29tbWVudGVkQnlfZW1haWxcIjogXCJ7e2NvbW1lbnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJjb21tZW50ZWRCeV91c2VybmFtZVwiOiBcInt7Y29tbWVudGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcImNvbW1lbnRlZEJ5X2F2YXRhclwiOiBcInt7Y29tbWVudGVkQnlfYXZhdGFyfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW11cbn0i',
|
||||
},
|
||||
},
|
||||
webpush: {
|
||||
|
@@ -277,6 +277,7 @@ issueRoutes.post<{ issueId: string; status: string }, Issue>(
|
||||
}
|
||||
|
||||
issue.status = newStatus;
|
||||
issue.modifiedBy = req.user;
|
||||
|
||||
await issueRepository.save(issue);
|
||||
|
||||
|
@@ -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({
|
||||
|
@@ -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<IssueComment>): void {
|
||||
|
@@ -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<Issue> {
|
||||
@@ -14,29 +18,75 @@ export class IssueSubscriber implements EntitySubscriberInterface<Issue> {
|
||||
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<Issue> {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendIssueCreatedNotification(event.entity);
|
||||
this.sendIssueNotification(event.entity, Notification.ISSUE_CREATED);
|
||||
}
|
||||
|
||||
public beforeUpdate(event: UpdateEvent<Issue>): 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -31,6 +31,8 @@ export class MediaSubscriber implements EntitySubscriberInterface<Media> {
|
||||
|
||||
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> {
|
||||
}),
|
||||
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<Media> {
|
||||
);
|
||||
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<Media> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
53
server/templates/email/media-issue/html.pug
Normal file
53
server/templates/email/media-issue/html.pug
Normal file
@@ -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}
|
1
server/templates/email/media-issue/subject.pug
Normal file
1
server/templates/email/media-issue/subject.pug
Normal file
@@ -0,0 +1 @@
|
||||
!= `${event} - ${mediaName} [${applicationTitle}]`
|
@@ -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}
|
||||
|
@@ -1 +1 @@
|
||||
!= `${requestType} - ${mediaName} [${applicationTitle}]`
|
||||
!= `${event} - ${mediaName} [${applicationTitle}]`
|
||||
|
@@ -4,7 +4,7 @@ import { IssueType } from '../../../server/constants/issue';
|
||||
const messages = defineMessages({
|
||||
issueAudio: 'Audio',
|
||||
issueVideo: 'Video',
|
||||
issueSubtitles: 'Subtitles',
|
||||
issueSubtitles: 'Subtitle',
|
||||
issueOther: 'Other',
|
||||
});
|
||||
|
||||
|
@@ -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<NotificationTypeSelectorProps> = ({
|
||||
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<NotificationTypeSelectorProps> = ({
|
||||
!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,
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -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 = () => {
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<div className="form-input-field">
|
||||
<Field id="botUsername" name="botUsername" type="text" />
|
||||
<Field
|
||||
id="botUsername"
|
||||
name="botUsername"
|
||||
type="text"
|
||||
placeholder={settings.currentSettings.applicationTitle}
|
||||
/>
|
||||
</div>
|
||||
{errors.botUsername && touched.botUsername && (
|
||||
<div className="error">{errors.botUsername}</div>
|
||||
|
@@ -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({
|
||||
|
@@ -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.",
|
||||
|
Reference in New Issue
Block a user