mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(notif): allow users to enable/disable specific agents (#1172)
* refactor(ui): add tabs to user notification settings * feat(notif): allow users to enable/disable specific agents * fix(ui): only enforce required fields when agent is enabled * fix(ui): hide unavailable notification agents * feat(notif): mention admin users for admin Discord notifications * fix(ui): modify styling of PGP key textareas to suit expected input * fix(notif): mention all admins when there are multiple and fix rebase error * fix: add missing form values, and fix Yup validation * refactor: reduce repeated logic/code in email notif agent * refactor: move 'Notification Types' label into NotificationTypeSelector component * fix(email): correct inconsistencies in email template formatting * refactor: use bitfields for storing user-enabled notif agent types * feat: improve notification agent logging * fix(ui): mark string fields as nullable so empty values are not type errors * fix: add validation for PGP-related inputs * fix: correctly fetch user in user settings & log mentioned IDs for Discord notifs * fix(ui): fix mobile nav dropdown text & add hover effect to button-style tabs * fix(notif): process admin email notifications asynchronously * fix(logging): log name of notification type instead of its enum value * fix: mark required fields and pass all user settings values to API * fix(frontend): call mutate after changing email/Discord/Telegram global notif settings * refactor: get global notif settings from relevant API endpoints instead of adding to public settings * fix(notif): fall back to email notifications being enabled (default) if user settings do not exist * fix(notif): do not set notifyUser for MEDIA_PENDING or MEDIA_AUTO_APPROVED * fix: expose notif enabled settings in user notif endpoints & remove global enable notif setting * fix(notif): remove unnecessary allowed_mentions object from Discord payload * fix(notif): use form values for email test notification * fix: make suggested changes and regenerate DB migration * fix: loosen validation of PGP keys * fix: fix user profile settings routes * fix: remove route guard from profile pages
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import axios from 'axios';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { hasNotificationType, Notification } from '..';
|
||||
import { User } from '../../../entity/User';
|
||||
import logger from '../../../logger';
|
||||
import { Permission } from '../../permissions';
|
||||
import { getSettings, NotificationAgentDiscord } from '../../settings';
|
||||
import { NotificationAgentType } from '../agenttypes';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
|
||||
enum EmbedColors {
|
||||
@@ -107,7 +111,7 @@ class DiscordAgent
|
||||
if (payload.request) {
|
||||
fields.push({
|
||||
name: 'Requested By',
|
||||
value: payload.request?.requestedBy.displayName ?? '',
|
||||
value: payload.request.requestedBy.displayName,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
@@ -201,7 +205,14 @@ class DiscordAgent
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
logger.debug('Sending Discord notification', { label: 'Notifications' });
|
||||
logger.debug('Sending Discord notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
});
|
||||
|
||||
let content = undefined;
|
||||
|
||||
try {
|
||||
const {
|
||||
botUsername,
|
||||
@@ -213,16 +224,32 @@ class DiscordAgent
|
||||
return false;
|
||||
}
|
||||
|
||||
const mentionedUsers: string[] = [];
|
||||
let content = undefined;
|
||||
if (payload.notifyUser) {
|
||||
// Mention user who submitted the request
|
||||
if (
|
||||
payload.notifyUser.settings?.hasNotificationAgentEnabled(
|
||||
NotificationAgentType.DISCORD
|
||||
) &&
|
||||
payload.notifyUser.settings?.discordId
|
||||
) {
|
||||
content = `<@${payload.notifyUser.settings.discordId}>`;
|
||||
}
|
||||
} else {
|
||||
// Mention all users with the Manage Requests permission
|
||||
const userRepository = getRepository(User);
|
||||
const users = await userRepository.find();
|
||||
|
||||
if (
|
||||
payload.notifyUser &&
|
||||
(payload.notifyUser.settings?.enableNotifications ?? true) &&
|
||||
payload.notifyUser.settings?.discordId
|
||||
) {
|
||||
mentionedUsers.push(payload.notifyUser.settings.discordId);
|
||||
content = `<@${payload.notifyUser.settings.discordId}>`;
|
||||
content = users
|
||||
.filter(
|
||||
(user) =>
|
||||
user.hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
user.settings?.hasNotificationAgentEnabled(
|
||||
NotificationAgentType.DISCORD
|
||||
) &&
|
||||
user.settings?.discordId
|
||||
)
|
||||
.map((user) => `<@${user.settings?.discordId}>`)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl, {
|
||||
@@ -230,18 +257,19 @@ class DiscordAgent
|
||||
avatar_url: botAvatarUrl,
|
||||
embeds: [this.buildEmbed(type, payload)],
|
||||
content,
|
||||
allowed_mentions: {
|
||||
users: mentionedUsers,
|
||||
},
|
||||
} as DiscordWebhookPayload);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.error('Error sending Discord notification', {
|
||||
label: 'Notifications',
|
||||
message: e.message,
|
||||
mentions: content,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response.data,
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { EmailOptions } from 'email-templates';
|
||||
import path from 'path';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { hasNotificationType, Notification } from '..';
|
||||
@@ -7,6 +8,7 @@ import logger from '../../../logger';
|
||||
import PreparedEmail from '../../email';
|
||||
import { Permission } from '../../permissions';
|
||||
import { getSettings, NotificationAgentEmail } from '../../settings';
|
||||
import { NotificationAgentType } from '../agenttypes';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
|
||||
class EmailAgent
|
||||
@@ -35,379 +37,194 @@ class EmailAgent
|
||||
return false;
|
||||
}
|
||||
|
||||
private async sendMediaRequestEmail(payload: NotificationPayload) {
|
||||
// This is getting main settings for the whole app
|
||||
private buildMessage(
|
||||
type: Notification,
|
||||
payload: NotificationPayload,
|
||||
toEmail: string
|
||||
): EmailOptions | undefined {
|
||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
||||
try {
|
||||
const userRepository = getRepository(User);
|
||||
const users = await userRepository.find();
|
||||
|
||||
// Send to all users with the manage requests permission (or admins)
|
||||
users
|
||||
.filter(
|
||||
(user) =>
|
||||
user.hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
(user.settings?.enableNotifications ?? true)
|
||||
)
|
||||
.forEach((user) => {
|
||||
const email = new PreparedEmail(user.settings?.pgpKey);
|
||||
|
||||
email.send({
|
||||
template: path.join(
|
||||
__dirname,
|
||||
'../../../templates/email/media-request'
|
||||
),
|
||||
message: {
|
||||
to: user.email,
|
||||
},
|
||||
locals: {
|
||||
body: `A user has requested a new ${
|
||||
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
||||
}!`,
|
||||
mediaName: payload.subject,
|
||||
mediaPlot: payload.message,
|
||||
mediaExtra: payload.extra ?? [],
|
||||
imageUrl: payload.image,
|
||||
timestamp: new Date().toTimeString(),
|
||||
requestedBy: payload.request?.requestedBy.displayName,
|
||||
actionUrl: applicationUrl
|
||||
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
||||
: undefined,
|
||||
applicationUrl,
|
||||
applicationTitle,
|
||||
requestType: `New ${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Request`,
|
||||
},
|
||||
});
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.error('Email notification failed to send', {
|
||||
label: 'Notifications',
|
||||
message: e.message,
|
||||
});
|
||||
return false;
|
||||
if (type === Notification.TEST_NOTIFICATION) {
|
||||
return {
|
||||
template: path.join(__dirname, '../../../templates/email/test-email'),
|
||||
message: {
|
||||
to: toEmail,
|
||||
},
|
||||
locals: {
|
||||
body: payload.message,
|
||||
applicationUrl,
|
||||
applicationTitle,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMediaFailedEmail(payload: NotificationPayload) {
|
||||
// This is getting main settings for the whole app
|
||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
||||
try {
|
||||
const userRepository = getRepository(User);
|
||||
const users = await userRepository.find();
|
||||
if (payload.media) {
|
||||
let requestType = '';
|
||||
let body = '';
|
||||
|
||||
// Send to all users with the manage requests permission (or admins)
|
||||
users
|
||||
.filter(
|
||||
(user) =>
|
||||
user.hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
(user.settings?.enableNotifications ?? true)
|
||||
)
|
||||
.forEach((user) => {
|
||||
const email = new PreparedEmail(user.settings?.pgpKey);
|
||||
|
||||
email.send({
|
||||
template: path.join(
|
||||
__dirname,
|
||||
'../../../templates/email/media-request'
|
||||
),
|
||||
message: {
|
||||
to: user.email,
|
||||
},
|
||||
locals: {
|
||||
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'
|
||||
}:`,
|
||||
mediaName: payload.subject,
|
||||
mediaPlot: payload.message,
|
||||
imageUrl: payload.image,
|
||||
timestamp: new Date().toTimeString(),
|
||||
requestedBy: payload.request?.requestedBy.displayName,
|
||||
actionUrl: applicationUrl
|
||||
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
||||
: undefined,
|
||||
applicationUrl,
|
||||
applicationTitle,
|
||||
requestType: `Failed ${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Request`,
|
||||
},
|
||||
});
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.error('Email notification failed to send', {
|
||||
label: 'Notifications',
|
||||
message: e.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMediaApprovedEmail(payload: NotificationPayload) {
|
||||
// This is getting main settings for the whole app
|
||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
||||
try {
|
||||
if (
|
||||
payload.notifyUser &&
|
||||
(payload.notifyUser.settings?.enableNotifications ?? true)
|
||||
) {
|
||||
const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey);
|
||||
|
||||
await email.send({
|
||||
template: path.join(
|
||||
__dirname,
|
||||
'../../../templates/email/media-request'
|
||||
),
|
||||
message: {
|
||||
to: payload.notifyUser.email,
|
||||
},
|
||||
locals: {
|
||||
body: `Your request for the following ${
|
||||
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
||||
} has been approved:`,
|
||||
mediaName: payload.subject,
|
||||
mediaExtra: payload.extra ?? [],
|
||||
imageUrl: payload.image,
|
||||
timestamp: new Date().toTimeString(),
|
||||
requestedBy: payload.request?.requestedBy.displayName,
|
||||
actionUrl: applicationUrl
|
||||
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
||||
: undefined,
|
||||
applicationUrl,
|
||||
applicationTitle,
|
||||
requestType: `${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Request Approved`,
|
||||
},
|
||||
});
|
||||
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'
|
||||
}!`;
|
||||
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:`;
|
||||
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:`;
|
||||
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!`;
|
||||
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:`;
|
||||
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'
|
||||
}:`;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.error('Email notification failed to send', {
|
||||
label: 'Notifications',
|
||||
message: e.message,
|
||||
});
|
||||
return false;
|
||||
return {
|
||||
template: path.join(
|
||||
__dirname,
|
||||
'../../../templates/email/media-request'
|
||||
),
|
||||
message: {
|
||||
to: toEmail,
|
||||
},
|
||||
locals: {
|
||||
requestType,
|
||||
body,
|
||||
mediaName: payload.subject,
|
||||
mediaPlot: payload.message,
|
||||
mediaExtra: payload.extra ?? [],
|
||||
imageUrl: payload.image,
|
||||
timestamp: new Date().toTimeString(),
|
||||
requestedBy: payload.request?.requestedBy.displayName,
|
||||
actionUrl: applicationUrl
|
||||
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
||||
: undefined,
|
||||
applicationUrl,
|
||||
applicationTitle,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMediaAutoApprovedEmail(payload: NotificationPayload) {
|
||||
// This is getting main settings for the whole app
|
||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
||||
try {
|
||||
const userRepository = getRepository(User);
|
||||
const users = await userRepository.find();
|
||||
|
||||
// Send to all users with the manage requests permission (or admins)
|
||||
users
|
||||
.filter(
|
||||
(user) =>
|
||||
user.hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
(user.settings?.enableNotifications ?? true)
|
||||
)
|
||||
.forEach((user) => {
|
||||
const email = new PreparedEmail();
|
||||
|
||||
email.send({
|
||||
template: path.join(
|
||||
__dirname,
|
||||
'../../../templates/email/media-request'
|
||||
),
|
||||
message: {
|
||||
to: user.email,
|
||||
},
|
||||
locals: {
|
||||
body: `A new request for the following ${
|
||||
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
||||
} has been automatically approved:`,
|
||||
mediaName: payload.subject,
|
||||
mediaExtra: payload.extra ?? [],
|
||||
imageUrl: payload.image,
|
||||
timestamp: new Date().toTimeString(),
|
||||
requestedBy: payload.request?.requestedBy.displayName,
|
||||
actionUrl: applicationUrl
|
||||
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
||||
: undefined,
|
||||
applicationUrl,
|
||||
applicationTitle,
|
||||
requestType: `${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Request Automatically Approved`,
|
||||
},
|
||||
});
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.error('Email notification failed to send', {
|
||||
label: 'Notifications',
|
||||
message: e.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMediaDeclinedEmail(payload: NotificationPayload) {
|
||||
// This is getting main settings for the whole app
|
||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
||||
try {
|
||||
if (
|
||||
payload.notifyUser &&
|
||||
(payload.notifyUser.settings?.enableNotifications ?? true)
|
||||
) {
|
||||
const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey);
|
||||
|
||||
await email.send({
|
||||
template: path.join(
|
||||
__dirname,
|
||||
'../../../templates/email/media-request'
|
||||
),
|
||||
message: {
|
||||
to: payload.notifyUser.email,
|
||||
},
|
||||
locals: {
|
||||
body: `Your request for the following ${
|
||||
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
||||
} was declined:`,
|
||||
mediaName: payload.subject,
|
||||
mediaExtra: payload.extra ?? [],
|
||||
imageUrl: payload.image,
|
||||
timestamp: new Date().toTimeString(),
|
||||
requestedBy: payload.request?.requestedBy.displayName,
|
||||
actionUrl: applicationUrl
|
||||
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
||||
: undefined,
|
||||
applicationUrl,
|
||||
applicationTitle,
|
||||
requestType: `${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Request Declined`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.error('Email notification failed to send', {
|
||||
label: 'Notifications',
|
||||
message: e.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMediaAvailableEmail(payload: NotificationPayload) {
|
||||
// This is getting main settings for the whole app
|
||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
||||
try {
|
||||
if (
|
||||
payload.notifyUser &&
|
||||
(payload.notifyUser.settings?.enableNotifications ?? true)
|
||||
) {
|
||||
const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey);
|
||||
|
||||
await email.send({
|
||||
template: path.join(
|
||||
__dirname,
|
||||
'../../../templates/email/media-request'
|
||||
),
|
||||
message: {
|
||||
to: payload.notifyUser.email,
|
||||
},
|
||||
locals: {
|
||||
body: `The following ${
|
||||
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
|
||||
} you requested is now available!`,
|
||||
mediaName: payload.subject,
|
||||
mediaExtra: payload.extra ?? [],
|
||||
imageUrl: payload.image,
|
||||
timestamp: new Date().toTimeString(),
|
||||
requestedBy: payload.request?.requestedBy.displayName,
|
||||
actionUrl: applicationUrl
|
||||
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
||||
: undefined,
|
||||
applicationUrl,
|
||||
applicationTitle,
|
||||
requestType: `${
|
||||
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
|
||||
} Now Available`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.error('Email notification failed to send', {
|
||||
label: 'Notifications',
|
||||
message: e.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async sendTestEmail(payload: NotificationPayload) {
|
||||
// This is getting main settings for the whole app
|
||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
||||
try {
|
||||
if (payload.notifyUser) {
|
||||
const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey);
|
||||
|
||||
await email.send({
|
||||
template: path.join(__dirname, '../../../templates/email/test-email'),
|
||||
message: {
|
||||
to: payload.notifyUser.email,
|
||||
},
|
||||
locals: {
|
||||
body: payload.message,
|
||||
applicationUrl,
|
||||
applicationTitle,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.error('Email notification failed to send', {
|
||||
label: 'Notifications',
|
||||
message: e.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async send(
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
logger.debug('Sending email notification', { label: 'Notifications' });
|
||||
if (payload.notifyUser) {
|
||||
// Send notification to the user who submitted the request
|
||||
if (
|
||||
!payload.notifyUser.settings ||
|
||||
payload.notifyUser.settings.hasNotificationAgentEnabled(
|
||||
NotificationAgentType.EMAIL
|
||||
)
|
||||
) {
|
||||
logger.debug('Sending email notification', {
|
||||
label: 'Notifications',
|
||||
recipient: payload.notifyUser.displayName,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
});
|
||||
|
||||
switch (type) {
|
||||
case Notification.MEDIA_PENDING:
|
||||
this.sendMediaRequestEmail(payload);
|
||||
break;
|
||||
case Notification.MEDIA_APPROVED:
|
||||
this.sendMediaApprovedEmail(payload);
|
||||
break;
|
||||
case Notification.MEDIA_AUTO_APPROVED:
|
||||
this.sendMediaAutoApprovedEmail(payload);
|
||||
break;
|
||||
case Notification.MEDIA_DECLINED:
|
||||
this.sendMediaDeclinedEmail(payload);
|
||||
break;
|
||||
case Notification.MEDIA_AVAILABLE:
|
||||
this.sendMediaAvailableEmail(payload);
|
||||
break;
|
||||
case Notification.MEDIA_FAILED:
|
||||
this.sendMediaFailedEmail(payload);
|
||||
break;
|
||||
case Notification.TEST_NOTIFICATION:
|
||||
this.sendTestEmail(payload);
|
||||
break;
|
||||
try {
|
||||
const email = new PreparedEmail(
|
||||
this.getSettings(),
|
||||
payload.notifyUser.settings?.pgpKey
|
||||
);
|
||||
await email.send(
|
||||
this.buildMessage(type, payload, payload.notifyUser.email)
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error('Error sending email notification', {
|
||||
label: 'Notifications',
|
||||
recipient: payload.notifyUser.displayName,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Send notifications to all users with the Manage Requests permission
|
||||
const userRepository = getRepository(User);
|
||||
const users = await userRepository.find();
|
||||
|
||||
await Promise.all(
|
||||
users
|
||||
.filter(
|
||||
(user) =>
|
||||
user.hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
(!user.settings ||
|
||||
user.settings.hasNotificationAgentEnabled(
|
||||
NotificationAgentType.EMAIL
|
||||
))
|
||||
)
|
||||
.map(async (user) => {
|
||||
logger.debug('Sending email notification', {
|
||||
label: 'Notifications',
|
||||
recipient: user.displayName,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
});
|
||||
|
||||
try {
|
||||
const email = new PreparedEmail(
|
||||
this.getSettings(),
|
||||
user.settings?.pgpKey
|
||||
);
|
||||
await email.send(this.buildMessage(type, payload, user.email));
|
||||
} catch (e) {
|
||||
logger.error('Error sending email notification', {
|
||||
label: 'Notifications',
|
||||
recipient: user.displayName,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import axios from 'axios';
|
||||
import { hasNotificationType, Notification } from '..';
|
||||
import { MediaType } from '../../../constants/media';
|
||||
import logger from '../../../logger';
|
||||
import { getSettings, NotificationAgentPushbullet } from '../../settings';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
import { MediaType } from '../../../constants/media';
|
||||
|
||||
interface PushbulletPayload {
|
||||
title: string;
|
||||
@@ -136,7 +136,12 @@ class PushbulletAgent
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
logger.debug('Sending Pushbullet notification', { label: 'Notifications' });
|
||||
logger.debug('Sending Pushbullet notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
});
|
||||
|
||||
try {
|
||||
const endpoint = 'https://api.pushbullet.com/v2/pushes';
|
||||
|
||||
@@ -162,8 +167,12 @@ class PushbulletAgent
|
||||
} catch (e) {
|
||||
logger.error('Error sending Pushbullet notification', {
|
||||
label: 'Notifications',
|
||||
message: e.message,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response.data,
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import axios from 'axios';
|
||||
import { hasNotificationType, Notification } from '..';
|
||||
import { MediaType } from '../../../constants/media';
|
||||
import logger from '../../../logger';
|
||||
import { getSettings, NotificationAgentPushover } from '../../settings';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
import { MediaType } from '../../../constants/media';
|
||||
|
||||
interface PushoverPayload {
|
||||
token: string;
|
||||
@@ -160,7 +160,11 @@ class PushoverAgent
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
logger.debug('Sending Pushover notification', { label: 'Notifications' });
|
||||
logger.debug('Sending Pushover notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
});
|
||||
try {
|
||||
const endpoint = 'https://api.pushover.net/1/messages.json';
|
||||
|
||||
@@ -189,8 +193,12 @@ class PushoverAgent
|
||||
} catch (e) {
|
||||
logger.error('Error sending Pushover notification', {
|
||||
label: 'Notifications',
|
||||
message: e.message,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response.data,
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import axios from 'axios';
|
||||
import { hasNotificationType, Notification } from '..';
|
||||
import { MediaType } from '../../../constants/media';
|
||||
import logger from '../../../logger';
|
||||
import { getSettings, NotificationAgentSlack } from '../../settings';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
import { MediaType } from '../../../constants/media';
|
||||
|
||||
interface EmbedField {
|
||||
type: 'plain_text' | 'mrkdwn';
|
||||
@@ -67,9 +67,7 @@ class SlackAgent
|
||||
if (payload.request) {
|
||||
fields.push({
|
||||
type: 'mrkdwn',
|
||||
text: `*Requested By*\n${
|
||||
payload.request?.requestedBy.displayName ?? ''
|
||||
}`,
|
||||
text: `*Requested By*\n${payload.request.requestedBy.displayName}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -235,7 +233,11 @@ class SlackAgent
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
logger.debug('Sending Slack notification', { label: 'Notifications' });
|
||||
logger.debug('Sending Slack notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
});
|
||||
try {
|
||||
const webhookUrl = this.getSettings().options.webhookUrl;
|
||||
|
||||
@@ -249,8 +251,12 @@ class SlackAgent
|
||||
} catch (e) {
|
||||
logger.error('Error sending Slack notification', {
|
||||
label: 'Notifications',
|
||||
message: e.message,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response.data,
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import { hasNotificationType, Notification } from '..';
|
||||
import { MediaType } from '../../../constants/media';
|
||||
import logger from '../../../logger';
|
||||
import { getSettings, NotificationAgentTelegram } from '../../settings';
|
||||
import { NotificationAgentType } from '../agenttypes';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
|
||||
interface TelegramMessagePayload {
|
||||
@@ -155,62 +156,98 @@ class TelegramAgent
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
logger.debug('Sending Telegram notification', { label: 'Notifications' });
|
||||
const endpoint = `${this.baseUrl}bot${this.getSettings().options.botAPI}/${
|
||||
payload.image ? 'sendPhoto' : 'sendMessage'
|
||||
}`;
|
||||
|
||||
// Send system notification
|
||||
try {
|
||||
const endpoint = `${this.baseUrl}bot${
|
||||
this.getSettings().options.botAPI
|
||||
}/${payload.image ? 'sendPhoto' : 'sendMessage'}`;
|
||||
logger.debug('Sending Telegram notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
});
|
||||
|
||||
// Send system notification
|
||||
await (payload.image
|
||||
? axios.post(endpoint, {
|
||||
photo: payload.image,
|
||||
caption: this.buildMessage(type, payload),
|
||||
parse_mode: 'MarkdownV2',
|
||||
chat_id: `${this.getSettings().options.chatId}`,
|
||||
disable_notification: this.getSettings().options.sendSilently,
|
||||
} as TelegramPhotoPayload)
|
||||
: axios.post(endpoint, {
|
||||
text: this.buildMessage(type, payload),
|
||||
parse_mode: 'MarkdownV2',
|
||||
chat_id: `${this.getSettings().options.chatId}`,
|
||||
disable_notification: this.getSettings().options.sendSilently,
|
||||
} as TelegramMessagePayload));
|
||||
|
||||
// Send user notification
|
||||
if (
|
||||
payload.notifyUser &&
|
||||
(payload.notifyUser.settings?.enableNotifications ?? true) &&
|
||||
payload.notifyUser.settings?.telegramChatId &&
|
||||
payload.notifyUser.settings?.telegramChatId !==
|
||||
this.getSettings().options.chatId
|
||||
) {
|
||||
await (payload.image
|
||||
? axios.post(endpoint, {
|
||||
await axios.post(
|
||||
endpoint,
|
||||
payload.image
|
||||
? ({
|
||||
photo: payload.image,
|
||||
caption: this.buildMessage(type, payload),
|
||||
parse_mode: 'MarkdownV2',
|
||||
chat_id: `${payload.notifyUser.settings.telegramChatId}`,
|
||||
disable_notification:
|
||||
payload.notifyUser.settings.telegramSendSilently,
|
||||
chat_id: this.getSettings().options.chatId,
|
||||
disable_notification: this.getSettings().options.sendSilently,
|
||||
} as TelegramPhotoPayload)
|
||||
: axios.post(endpoint, {
|
||||
: ({
|
||||
text: this.buildMessage(type, payload),
|
||||
parse_mode: 'MarkdownV2',
|
||||
chat_id: `${payload.notifyUser.settings.telegramChatId}`,
|
||||
disable_notification:
|
||||
payload.notifyUser.settings.telegramSendSilently,
|
||||
} as TelegramMessagePayload));
|
||||
}
|
||||
|
||||
return true;
|
||||
chat_id: `${this.getSettings().options.chatId}`,
|
||||
disable_notification: this.getSettings().options.sendSilently,
|
||||
} as TelegramMessagePayload)
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error('Error sending Telegram notification', {
|
||||
label: 'Notifications',
|
||||
message: e.message,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response.data,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
payload.notifyUser &&
|
||||
payload.notifyUser.settings?.hasNotificationAgentEnabled(
|
||||
NotificationAgentType.TELEGRAM
|
||||
) &&
|
||||
payload.notifyUser.settings?.telegramChatId &&
|
||||
payload.notifyUser.settings?.telegramChatId !==
|
||||
this.getSettings().options.chatId
|
||||
) {
|
||||
// Send notification to the user who submitted the request
|
||||
logger.debug('Sending Telegram notification', {
|
||||
label: 'Notifications',
|
||||
recipient: payload.notifyUser.displayName,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
endpoint,
|
||||
payload.image
|
||||
? ({
|
||||
photo: payload.image,
|
||||
caption: this.buildMessage(type, payload),
|
||||
parse_mode: 'MarkdownV2',
|
||||
chat_id: payload.notifyUser.settings.telegramChatId,
|
||||
disable_notification:
|
||||
payload.notifyUser.settings.telegramSendSilently,
|
||||
} as TelegramPhotoPayload)
|
||||
: ({
|
||||
text: this.buildMessage(type, payload),
|
||||
parse_mode: 'MarkdownV2',
|
||||
chat_id: payload.notifyUser.settings.telegramChatId,
|
||||
disable_notification:
|
||||
payload.notifyUser.settings.telegramSendSilently,
|
||||
} as TelegramMessagePayload)
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error('Error sending Telegram notification', {
|
||||
label: 'Notifications',
|
||||
recipient: payload.notifyUser.displayName,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response.data,
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -128,7 +128,12 @@ class WebhookAgent
|
||||
type: Notification,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
logger.debug('Sending webhook notification', { label: 'Notifications' });
|
||||
logger.debug('Sending webhook notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
});
|
||||
|
||||
try {
|
||||
const { webhookUrl, authHeader } = this.getSettings().options;
|
||||
|
||||
@@ -146,8 +151,12 @@ class WebhookAgent
|
||||
} catch (e) {
|
||||
logger.error('Error sending webhook notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response.data,
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
16
server/lib/notifications/agenttypes.ts
Normal file
16
server/lib/notifications/agenttypes.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export enum NotificationAgentType {
|
||||
NONE = 0,
|
||||
EMAIL = 2,
|
||||
DISCORD = 4,
|
||||
TELEGRAM = 8,
|
||||
PUSHOVER = 16,
|
||||
PUSHBULLET = 32,
|
||||
SLACK = 64,
|
||||
}
|
||||
|
||||
export const hasNotificationAgentEnabled = (
|
||||
agent: NotificationAgentType,
|
||||
value: number
|
||||
): boolean => {
|
||||
return !!(value & agent);
|
||||
};
|
@@ -38,7 +38,7 @@ class NotificationManager {
|
||||
|
||||
public registerAgents = (agents: NotificationAgent[]): void => {
|
||||
this.activeAgents = [...this.activeAgents, ...agents];
|
||||
logger.info('Registered Notification Agents', { label: 'Notifications' });
|
||||
logger.info('Registered notification agents', { label: 'Notifications' });
|
||||
};
|
||||
|
||||
public sendNotification(
|
||||
@@ -46,8 +46,9 @@ class NotificationManager {
|
||||
payload: NotificationPayload
|
||||
): void {
|
||||
const settings = getSettings().notifications;
|
||||
logger.info(`Sending notification for ${Notification[type]}`, {
|
||||
logger.info(`Sending notification(s) for ${Notification[type]}`, {
|
||||
label: 'Notifications',
|
||||
subject: payload.subject,
|
||||
});
|
||||
this.activeAgents.forEach((agent) => {
|
||||
if (settings.enabled && agent.shouldSend(type)) {
|
||||
|
Reference in New Issue
Block a user