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:
TheCatLady
2021-12-04 07:24:26 -05:00
committed by GitHub
parent 6245be1e10
commit c9ffac33f7
30 changed files with 1014 additions and 804 deletions

View File

@@ -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> {

View File

@@ -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,

View File

@@ -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', {

View File

@@ -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,
};
}

View File

@@ -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 (

View File

@@ -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 (

View File

@@ -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}`,
},
},
],

View File

@@ -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 (

View File

@@ -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') {

View File

@@ -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) {

View File

@@ -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[] = [];