Skip to content

Commit

Permalink
feat(notif): allow users to enable/disable specific agents (#1172)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
TheCatLady committed Apr 13, 2021
1 parent bed850d commit 46c4ee1
Show file tree
Hide file tree
Showing 50 changed files with 1,725 additions and 1,499 deletions.
66 changes: 17 additions & 49 deletions overseerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,12 @@ components:
UserSettings:
type: object
properties:
enableNotifications:
type: boolean
default: true
discordId:
type: string
telegramChatId:
region:
type: string
language:
type: string
telegramSendSilently:
type: boolean
required:
- enableNotifications
MainSettings:
type: object
properties:
Expand Down Expand Up @@ -1201,12 +1196,6 @@ components:
type: string
priority:
type: number
NotificationSettings:
type: object
properties:
enabled:
type: boolean
example: true
NotificationEmailSettings:
type: object
properties:
Expand Down Expand Up @@ -1559,20 +1548,30 @@ components:
UserSettingsNotifications:
type: object
properties:
enableNotifications:
notificationAgents:
type: number
example: 0
emailEnabled:
type: boolean
pgpKey:
type: string
nullable: true
discordEnabled:
type: boolean
default: true
discordId:
type: string
nullable: true
telegramEnabled:
type: boolean
telegramBotUsername:
type: string
nullable: true
telegramChatId:
type: string
nullable: true
telegramSendSilently:
type: boolean
nullable: true
required:
- enableNotifications
securitySchemes:
cookieAuth:
type: apiKey
Expand Down Expand Up @@ -2306,37 +2305,6 @@ paths:
timestamp:
type: string
example: 2020-12-15T16:20:00.069Z
/settings/notifications:
get:
summary: Return notification settings
description: Returns current notification settings in a JSON object.
tags:
- settings
responses:
'200':
description: Returned settings
content:
application/json:
schema:
$ref: '#/components/schemas/NotificationSettings'
post:
summary: Update notification settings
description: Updates notification settings with the provided values.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NotificationSettings'
responses:
'200':
description: 'Values were sucessfully updated'
content:
application/json:
schema:
$ref: '#/components/schemas/NotificationSettings'
/settings/notifications/email:
get:
summary: Get email notification settings
Expand Down
4 changes: 1 addition & 3 deletions server/entity/MediaRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ export class MediaRequest {
subject: movie.title,
message: movie.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
notifyUser: this.requestedBy,
media,
request: this,
});
Expand All @@ -157,7 +156,6 @@ export class MediaRequest {
subject: tv.name,
message: tv.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
notifyUser: this.requestedBy,
media,
extra: [
{
Expand Down Expand Up @@ -232,7 +230,7 @@ export class MediaRequest {
subject: tv.name,
message: tv.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
notifyUser: this.requestedBy,
notifyUser: autoApproved ? undefined : this.requestedBy,
media,
extra: [
{
Expand Down
5 changes: 3 additions & 2 deletions server/entity/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ export class User {
logger.info(`Sending generated password email for ${this.email}`, {
label: 'User Management',
});
const email = new PreparedEmail();

const email = new PreparedEmail(getSettings().notifications.agents.email);
await email.send({
template: path.join(__dirname, '../templates/email/generatedpassword'),
message: {
Expand Down Expand Up @@ -193,7 +194,7 @@ export class User {
logger.info(`Sending reset password email for ${this.email}`, {
label: 'User Management',
});
const email = new PreparedEmail();
const email = new PreparedEmail(getSettings().notifications.agents.email);
await email.send({
template: path.join(__dirname, '../templates/email/resetpassword'),
message: {
Expand Down
26 changes: 17 additions & 9 deletions server/entity/UserSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import {
hasNotificationAgentEnabled,
NotificationAgentType,
} from '../lib/notifications/agenttypes';
import { User } from './User';

@Entity()
Expand All @@ -20,24 +24,28 @@ export class UserSettings {
@JoinColumn()
public user: User;

@Column({ default: true })
public enableNotifications: boolean;

@Column({ nullable: true })
public discordId?: string;
public region?: string;

@Column({ nullable: true })
public telegramChatId?: string;
public originalLanguage?: string;

@Column({ type: 'integer', default: NotificationAgentType.EMAIL })
public notificationAgents = NotificationAgentType.EMAIL;

@Column({ nullable: true })
public telegramSendSilently?: boolean;
public pgpKey?: string;

@Column({ nullable: true })
public region?: string;
public discordId?: string;

@Column({ nullable: true })
public originalLanguage?: string;
public telegramChatId?: string;

@Column({ nullable: true })
public pgpKey?: string;
public telegramSendSilently?: boolean;

public hasNotificationAgentEnabled(agent: NotificationAgentType): boolean {
return !!hasNotificationAgentEnabled(agent, this.notificationAgents);
}
}
9 changes: 6 additions & 3 deletions server/interfaces/api/userSettingsInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ export interface UserSettingsGeneralResponse {
}

export interface UserSettingsNotificationsResponse {
enableNotifications: boolean;
telegramBotUsername?: string;
notificationAgents: number;
emailEnabled?: boolean;
pgpKey?: string;
discordEnabled?: boolean;
discordId?: string;
telegramEnabled?: boolean;
telegramBotUsername?: string;
telegramChatId?: string;
telegramSendSilently?: boolean;
pgpKey?: string;
}
9 changes: 4 additions & 5 deletions server/lib/email/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import nodemailer from 'nodemailer';
import Email from 'email-templates';
import { getSettings } from '../settings';
import nodemailer from 'nodemailer';
import { NotificationAgentEmail } from '../settings';
import { openpgpEncrypt } from './openpgpEncrypt';
class PreparedEmail extends Email {
public constructor(pgpKey?: string) {
const settings = getSettings().notifications.agents.email;

class PreparedEmail extends Email {
public constructor(settings: NotificationAgentEmail, pgpKey?: string) {
const transport = nodemailer.createTransport({
host: settings.options.smtpHost,
port: settings.options.smtpPort,
Expand Down
58 changes: 43 additions & 15 deletions server/lib/notifications/agents/discord.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
});
}
Expand Down Expand Up @@ -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,
Expand All @@ -213,35 +224,52 @@ 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, {
username: botUsername,
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;
}
}
Expand Down

0 comments on commit 46c4ee1

Please sign in to comment.