feat: allow users to select notification types (#1512)

* feat: allow users to select notification types

* fix(ui): display personal notification types before management types

* fix: update allRequestsAutoApproved check to account for new REQUEST_MOVIE & REQUEST_TV perms

* fix(ui): do not display Discord notif type selector if user not eligible for any types

* refactor(ui): remove unnecessary 'enabled' checkboxes from user notif settings

* fix(ui): correct checkbox behavior

* fix: add missing return type on hasNotificationType

* refactor: remove unused isValid prop in NotificationsWebPush

* fix(ui): use SensitiveInput for users' public PGP keys

* fix(ui): add missing tip/hint for email encryption setting

* refactor(svg): use the new Discord logo

* revert(api): undo breaking change removing discordEnabled from UserSettingsNotificationsResponse

* fix(lang): update notification type descriptions for clarity

* fix(telegram): do not send users notifications of their own auto-approved requests
This commit is contained in:
TheCatLady
2021-06-04 06:31:05 -04:00
committed by GitHub
parent 6a3649f620
commit e60598905b
35 changed files with 1072 additions and 653 deletions

View File

@@ -24,6 +24,6 @@ export abstract class BaseAgent<T extends NotificationAgentConfig> {
}
export interface NotificationAgent {
shouldSend(type: Notification): boolean;
shouldSend(): boolean;
send(type: Notification, payload: NotificationPayload): Promise<boolean>;
}

View File

@@ -193,12 +193,10 @@ class DiscordAgent
};
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
this.getSettings().options.webhookUrl &&
hasNotificationType(type, this.getSettings().types)
) {
public shouldSend(): boolean {
const settings = this.getSettings();
if (settings.enabled && settings.options.webhookUrl) {
return true;
}
@@ -209,6 +207,12 @@ class DiscordAgent
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
return true;
}
logger.debug('Sending Discord notification', {
label: 'Notifications',
type: Notification[type],
@@ -218,13 +222,6 @@ class DiscordAgent
let content = undefined;
try {
const { botUsername, botAvatarUrl, webhookUrl } =
this.getSettings().options;
if (!webhookUrl) {
return false;
}
if (payload.notifyUser) {
// Mention user who submitted the request
if (
@@ -258,9 +255,9 @@ class DiscordAgent
.join(' ');
}
await axios.post(webhookUrl, {
username: botUsername,
avatar_url: botAvatarUrl,
await axios.post(settings.options.webhookUrl, {
username: settings.options.botUsername,
avatar_url: settings.options.botAvatarUrl,
embeds: [this.buildEmbed(type, payload)],
content,
} as DiscordWebhookPayload);

View File

@@ -1,7 +1,7 @@
import { EmailOptions } from 'email-templates';
import path from 'path';
import { getRepository } from 'typeorm';
import { hasNotificationType, Notification } from '..';
import { Notification } from '..';
import { MediaType } from '../../../constants/media';
import { User } from '../../../entity/User';
import logger from '../../../logger';
@@ -28,12 +28,14 @@ class EmailAgent
return settings.notifications.agents.email;
}
public shouldSend(type: Notification): boolean {
public shouldSend(): boolean {
const settings = this.getSettings();
if (
settings.enabled &&
hasNotificationType(type, this.getSettings().types)
settings.options.emailFrom &&
settings.options.smtpHost &&
settings.options.smtpPort
) {
return true;
}

View File

@@ -50,12 +50,10 @@ class LunaSeaAgent
};
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
this.getSettings().options.webhookUrl &&
hasNotificationType(type, this.getSettings().types)
) {
public shouldSend(): boolean {
const settings = this.getSettings();
if (settings.enabled && settings.options.webhookUrl) {
return true;
}
@@ -66,6 +64,12 @@ class LunaSeaAgent
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
return true;
}
logger.debug('Sending LunaSea notification', {
label: 'Notifications',
type: Notification[type],
@@ -73,19 +77,19 @@ class LunaSeaAgent
});
try {
const { webhookUrl, profileName } = this.getSettings().options;
if (!webhookUrl) {
return false;
}
await axios.post(webhookUrl, this.buildPayload(type, payload), {
headers: {
Authorization: `Basic ${Buffer.from(`${profileName}:`).toString(
'base64'
)}`,
},
});
await axios.post(
settings.options.webhookUrl,
this.buildPayload(type, payload),
settings.options.profileName
? {
headers: {
Authorization: `Basic ${Buffer.from(
`${settings.options.profileName}:`
).toString('base64')}`,
},
}
: undefined
);
return true;
} catch (e) {
@@ -94,7 +98,7 @@ class LunaSeaAgent
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
response: e.response?.data,
});
return false;

View File

@@ -24,12 +24,10 @@ class PushbulletAgent
return settings.notifications.agents.pushbullet;
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
this.getSettings().options.accessToken &&
hasNotificationType(type, this.getSettings().types)
) {
public shouldSend(): boolean {
const settings = this.getSettings();
if (settings.enabled && settings.options.accessToken) {
return true;
}
@@ -137,6 +135,12 @@ class PushbulletAgent
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
return true;
}
logger.debug('Sending Pushbullet notification', {
label: 'Notifications',
type: Notification[type],
@@ -144,14 +148,10 @@ class PushbulletAgent
});
try {
const endpoint = 'https://api.pushbullet.com/v2/pushes';
const { accessToken } = this.getSettings().options;
const { title, body } = this.constructMessageDetails(type, payload);
await axios.post(
endpoint,
'https://api.pushbullet.com/v2/pushes',
{
type: 'note',
title: title,
@@ -159,7 +159,7 @@ class PushbulletAgent
} as PushbulletPayload,
{
headers: {
'Access-Token': accessToken,
'Access-Token': settings.options.accessToken,
},
}
);

View File

@@ -30,12 +30,13 @@ class PushoverAgent
return settings.notifications.agents.pushover;
}
public shouldSend(type: Notification): boolean {
public shouldSend(): boolean {
const settings = this.getSettings();
if (
this.getSettings().enabled &&
this.getSettings().options.accessToken &&
this.getSettings().options.userToken &&
hasNotificationType(type, this.getSettings().types)
settings.enabled &&
settings.options.accessToken &&
settings.options.userToken
) {
return true;
}
@@ -161,6 +162,12 @@ class PushoverAgent
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
return true;
}
logger.debug('Sending Pushover notification', {
label: 'Notifications',
type: Notification[type],
@@ -169,14 +176,12 @@ class PushoverAgent
try {
const endpoint = 'https://api.pushover.net/1/messages.json';
const { accessToken, userToken } = this.getSettings().options;
const { title, message, url, url_title, priority } =
this.constructMessageDetails(type, payload);
await axios.post(endpoint, {
token: accessToken,
user: userToken,
token: settings.options.accessToken,
user: settings.options.userToken,
title: title,
message: message,
url: url,

View File

@@ -218,12 +218,10 @@ class SlackAgent
};
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
this.getSettings().options.webhookUrl &&
hasNotificationType(type, this.getSettings().types)
) {
public shouldSend(): boolean {
const settings = this.getSettings();
if (settings.enabled && settings.options.webhookUrl) {
return true;
}
@@ -234,19 +232,22 @@ class SlackAgent
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
return true;
}
logger.debug('Sending Slack notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
try {
const webhookUrl = this.getSettings().options.webhookUrl;
if (!webhookUrl) {
return false;
}
await axios.post(webhookUrl, this.buildEmbed(type, payload));
await axios.post(
settings.options.webhookUrl,
this.buildEmbed(type, payload)
);
return true;
} catch (e) {

View File

@@ -1,7 +1,10 @@
import axios from 'axios';
import { getRepository } from 'typeorm';
import { hasNotificationType, Notification } from '..';
import { MediaType } from '../../../constants/media';
import { User } from '../../../entity/User';
import logger from '../../../logger';
import { Permission } from '../../permissions';
import {
getSettings,
NotificationAgentKey,
@@ -40,12 +43,13 @@ class TelegramAgent
return settings.notifications.agents.telegram;
}
public shouldSend(type: Notification): boolean {
public shouldSend(): boolean {
const settings = this.getSettings();
if (
this.getSettings().enabled &&
this.getSettings().options.botAPI &&
this.getSettings().options.chatId &&
hasNotificationType(type, this.getSettings().types)
settings.enabled &&
settings.options.botAPI &&
settings.options.chatId
) {
return true;
}
@@ -59,8 +63,10 @@ class TelegramAgent
private buildMessage(
type: Notification,
payload: NotificationPayload
): string {
payload: NotificationPayload,
chatId: string,
sendSilently: boolean
): TelegramMessagePayload | TelegramPhotoPayload {
const settings = getSettings();
let message = '';
@@ -153,95 +159,53 @@ class TelegramAgent
}
/* eslint-enable */
return message;
return payload.image
? ({
photo: payload.image,
caption: message,
parse_mode: 'MarkdownV2',
chat_id: chatId,
disable_notification: !!sendSilently,
} as TelegramPhotoPayload)
: ({
text: message,
parse_mode: 'MarkdownV2',
chat_id: chatId,
disable_notification: !!sendSilently,
} as TelegramMessagePayload);
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
const endpoint = `${this.baseUrl}bot${this.getSettings().options.botAPI}/${
const settings = this.getSettings();
const endpoint = `${this.baseUrl}bot${settings.options.botAPI}/${
payload.image ? 'sendPhoto' : 'sendMessage'
}`;
// Send system notification
try {
if (hasNotificationType(type, settings.types ?? 0)) {
logger.debug('Sending Telegram notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
await axios.post(
endpoint,
payload.image
? ({
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)
: ({
text: this.buildMessage(type, payload),
parse_mode: 'MarkdownV2',
chat_id: `${this.getSettings().options.chatId}`,
disable_notification: this.getSettings().options.sendSilently,
} as TelegramMessagePayload)
);
} catch (e) {
logger.error('Error sending Telegram notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
});
return false;
}
if (
payload.notifyUser &&
payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.TELEGRAM,
type
) &&
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)
this.buildMessage(
type,
payload,
settings.options.chatId,
settings.options.sendSilently
)
);
} catch (e) {
logger.error('Error sending Telegram notification', {
label: 'Notifications',
recipient: payload.notifyUser.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
@@ -252,6 +216,103 @@ class TelegramAgent
}
}
if (payload.notifyUser) {
// Send notification to the user who submitted the request
if (
payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.TELEGRAM,
type
) &&
payload.notifyUser.settings?.telegramChatId &&
payload.notifyUser.settings?.telegramChatId !== settings.options.chatId
) {
logger.debug('Sending Telegram notification', {
label: 'Notifications',
recipient: payload.notifyUser.displayName,
type: Notification[type],
subject: payload.subject,
});
try {
await axios.post(
endpoint,
this.buildMessage(
type,
payload,
payload.notifyUser.settings.telegramChatId,
!!payload.notifyUser.settings.telegramSendSilently
)
);
} 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;
}
}
} 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?.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)
)
.map(async (user) => {
if (
user.settings?.telegramChatId &&
user.settings.telegramChatId !== settings.options.chatId
) {
logger.debug('Sending Telegram notification', {
label: 'Notifications',
recipient: user.displayName,
type: Notification[type],
subject: payload.subject,
});
try {
await axios.post(
endpoint,
this.buildMessage(
type,
payload,
user.settings.telegramChatId,
!!user.settings?.telegramSendSilently
)
);
} catch (e) {
logger.error('Error sending Telegram notification', {
label: 'Notifications',
recipient: user.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
});
return false;
}
}
})
);
}
return true;
}
}

View File

@@ -113,12 +113,10 @@ class WebhookAgent
return this.parseKeys(parsedJSON, payload, type);
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
this.getSettings().options.webhookUrl &&
hasNotificationType(type, this.getSettings().types)
) {
public shouldSend(): boolean {
const settings = this.getSettings();
if (settings.enabled && settings.options.webhookUrl) {
return true;
}
@@ -129,6 +127,12 @@ class WebhookAgent
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
return true;
}
logger.debug('Sending webhook notification', {
label: 'Notifications',
type: Notification[type],
@@ -136,17 +140,17 @@ class WebhookAgent
});
try {
const { webhookUrl, authHeader } = this.getSettings().options;
if (!webhookUrl) {
return false;
}
await axios.post(webhookUrl, this.buildPayload(type, payload), {
headers: {
Authorization: authHeader,
},
});
await axios.post(
settings.options.webhookUrl,
this.buildPayload(type, payload),
settings.options.authHeader
? {
headers: {
Authorization: settings.options.authHeader,
},
}
: undefined
);
return true;
} catch (e) {

View File

@@ -1,6 +1,6 @@
import { getRepository } from 'typeorm';
import webpush from 'web-push';
import { hasNotificationType, Notification } from '..';
import { Notification } from '..';
import { MediaType } from '../../../constants/media';
import { User } from '../../../entity/User';
import { UserPushSubscription } from '../../../entity/UserPushSubscription';
@@ -135,11 +135,8 @@ class WebPushAgent
}
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
hasNotificationType(type, this.getSettings().types)
) {
public shouldSend(): boolean {
if (this.getSettings().enabled) {
return true;
}
@@ -150,11 +147,6 @@ class WebPushAgent
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending web push notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
const userRepository = getRepository(User);
const userPushSubRepository = getRepository(UserPushSubscription);
const settings = getSettings();
@@ -213,8 +205,15 @@ class WebPushAgent
settings.vapidPrivate
);
Promise.all(
await Promise.all(
pushSubs.map(async (sub) => {
logger.debug('Sending web push notification', {
label: 'Notifications',
recipient: sub.user.displayName,
type: Notification[type],
subject: payload.subject,
});
try {
await webpush.sendNotification(
{
@@ -230,12 +229,24 @@ class WebPushAgent
)
);
} catch (e) {
logger.error(
'Error sending web push notification; removing subscription',
{
label: 'Notifications',
recipient: sub.user.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
}
);
// Failed to send notification so we need to remove the subscription
userPushSubRepository.remove(sub);
}
})
);
}
return true;
}
}