mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(notifications): added ability to send test notifications
closes #309
This commit is contained in:
@@ -1488,6 +1488,21 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/NotificationEmailSettings'
|
$ref: '#/components/schemas/NotificationEmailSettings'
|
||||||
|
/settings/notifications/email/test:
|
||||||
|
post:
|
||||||
|
summary: Test the provided email settings
|
||||||
|
description: Sends a test notification to the email agent
|
||||||
|
tags:
|
||||||
|
- settings
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/NotificationEmailSettings'
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Test notification attempted
|
||||||
/settings/notifications/discord:
|
/settings/notifications/discord:
|
||||||
get:
|
get:
|
||||||
summary: Return current discord notification settings
|
summary: Return current discord notification settings
|
||||||
@@ -1519,6 +1534,21 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/DiscordSettings'
|
$ref: '#/components/schemas/DiscordSettings'
|
||||||
|
/settings/notifications/discord/test:
|
||||||
|
post:
|
||||||
|
summary: Test the provided discord settings
|
||||||
|
description: Sends a test notification to the discord agent
|
||||||
|
tags:
|
||||||
|
- settings
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/DiscordSettings'
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Test notification attempted
|
||||||
/settings/about:
|
/settings/about:
|
||||||
get:
|
get:
|
||||||
summary: Return current about stats
|
summary: Return current about stats
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { Notification } from '..';
|
import { Notification } from '..';
|
||||||
import { User } from '../../../entity/User';
|
import { User } from '../../../entity/User';
|
||||||
|
import { NotificationAgentConfig } from '../../settings';
|
||||||
|
|
||||||
export interface NotificationPayload {
|
export interface NotificationPayload {
|
||||||
subject: string;
|
subject: string;
|
||||||
@@ -9,6 +10,15 @@ export interface NotificationPayload {
|
|||||||
extra?: { name: string; value: string }[];
|
extra?: { name: string; value: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export abstract class BaseAgent<T extends NotificationAgentConfig> {
|
||||||
|
protected settings?: T;
|
||||||
|
public constructor(settings?: T) {
|
||||||
|
this.settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract getSettings(): T;
|
||||||
|
}
|
||||||
|
|
||||||
export interface NotificationAgent {
|
export interface NotificationAgent {
|
||||||
shouldSend(type: Notification): boolean;
|
shouldSend(type: Notification): boolean;
|
||||||
send(type: Notification, payload: NotificationPayload): Promise<boolean>;
|
send(type: Notification, payload: NotificationPayload): Promise<boolean>;
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Notification } from '..';
|
import { Notification } from '..';
|
||||||
import logger from '../../../logger';
|
import logger from '../../../logger';
|
||||||
import { getSettings } from '../../settings';
|
import { getSettings, NotificationAgentDiscord } from '../../settings';
|
||||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||||
|
|
||||||
enum EmbedColors {
|
enum EmbedColors {
|
||||||
DEFAULT = 0,
|
DEFAULT = 0,
|
||||||
@@ -37,6 +37,11 @@ interface DiscordImageEmbed {
|
|||||||
width?: number;
|
width?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Field {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
inline?: boolean;
|
||||||
|
}
|
||||||
interface DiscordRichEmbed {
|
interface DiscordRichEmbed {
|
||||||
title?: string;
|
title?: string;
|
||||||
type?: 'rich'; // Always rich for webhooks
|
type?: 'rich'; // Always rich for webhooks
|
||||||
@@ -61,11 +66,7 @@ interface DiscordRichEmbed {
|
|||||||
icon_url?: string;
|
icon_url?: string;
|
||||||
proxy_icon_url?: string;
|
proxy_icon_url?: string;
|
||||||
};
|
};
|
||||||
fields?: {
|
fields?: Field[];
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
inline?: boolean;
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DiscordWebhookPayload {
|
interface DiscordWebhookPayload {
|
||||||
@@ -75,27 +76,75 @@ interface DiscordWebhookPayload {
|
|||||||
tts: boolean;
|
tts: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DiscordAgent implements NotificationAgent {
|
class DiscordAgent
|
||||||
|
extends BaseAgent<NotificationAgentDiscord>
|
||||||
|
implements NotificationAgent {
|
||||||
|
protected getSettings(): NotificationAgentDiscord {
|
||||||
|
if (this.settings) {
|
||||||
|
return this.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
|
return settings.notifications.agents.discord;
|
||||||
|
}
|
||||||
|
|
||||||
public buildEmbed(
|
public buildEmbed(
|
||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): DiscordRichEmbed {
|
): DiscordRichEmbed {
|
||||||
let color = EmbedColors.DEFAULT;
|
let color = EmbedColors.DEFAULT;
|
||||||
let status = 'Unknown';
|
|
||||||
|
const fields: Field[] = [];
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case Notification.MEDIA_PENDING:
|
case Notification.MEDIA_PENDING:
|
||||||
color = EmbedColors.ORANGE;
|
color = EmbedColors.ORANGE;
|
||||||
status = 'Pending Approval';
|
fields.push(
|
||||||
|
{
|
||||||
|
name: 'Requested By',
|
||||||
|
value: payload.notifyUser.username ?? '',
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Status',
|
||||||
|
value: 'Pending Approval',
|
||||||
|
inline: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case Notification.MEDIA_APPROVED:
|
case Notification.MEDIA_APPROVED:
|
||||||
color = EmbedColors.PURPLE;
|
color = EmbedColors.PURPLE;
|
||||||
status = 'Processing Request';
|
fields.push(
|
||||||
|
{
|
||||||
|
name: 'Requested By',
|
||||||
|
value: payload.notifyUser.username ?? '',
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Status',
|
||||||
|
value: 'Processing Request',
|
||||||
|
inline: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case Notification.MEDIA_AVAILABLE:
|
case Notification.MEDIA_AVAILABLE:
|
||||||
color = EmbedColors.GREEN;
|
color = EmbedColors.GREEN;
|
||||||
status = 'Available';
|
fields.push(
|
||||||
|
{
|
||||||
|
name: 'Requested By',
|
||||||
|
value: payload.notifyUser.username ?? '',
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Status',
|
||||||
|
value: 'Available',
|
||||||
|
inline: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
color = EmbedColors.DARK_PURPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -105,16 +154,7 @@ class DiscordAgent implements NotificationAgent {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
author: { name: 'Overseerr' },
|
author: { name: 'Overseerr' },
|
||||||
fields: [
|
fields: [
|
||||||
{
|
...fields,
|
||||||
name: 'Requested By',
|
|
||||||
value: payload.notifyUser.username ?? '',
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Status',
|
|
||||||
value: status,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
// If we have extra data, map it to fields for discord notifications
|
// If we have extra data, map it to fields for discord notifications
|
||||||
...(payload.extra ?? []).map((extra) => ({
|
...(payload.extra ?? []).map((extra) => ({
|
||||||
name: extra.name,
|
name: extra.name,
|
||||||
@@ -130,12 +170,7 @@ class DiscordAgent implements NotificationAgent {
|
|||||||
// TODO: Add checking for type here once we add notification type filters for agents
|
// TODO: Add checking for type here once we add notification type filters for agents
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
public shouldSend(_type: Notification): boolean {
|
public shouldSend(_type: Notification): boolean {
|
||||||
const settings = getSettings();
|
if (this.getSettings().enabled && this.getSettings().options.webhookUrl) {
|
||||||
|
|
||||||
if (
|
|
||||||
settings.notifications.agents.discord?.enabled &&
|
|
||||||
settings.notifications.agents.discord?.options?.webhookUrl
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,11 +181,9 @@ class DiscordAgent implements NotificationAgent {
|
|||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const settings = getSettings();
|
|
||||||
logger.debug('Sending discord notification', { label: 'Notifications' });
|
logger.debug('Sending discord notification', { label: 'Notifications' });
|
||||||
try {
|
try {
|
||||||
const webhookUrl =
|
const webhookUrl = this.getSettings().options.webhookUrl;
|
||||||
settings.notifications.agents.discord?.options?.webhookUrl;
|
|
||||||
|
|
||||||
if (!webhookUrl) {
|
if (!webhookUrl) {
|
||||||
return false;
|
return false;
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||||
import { Notification } from '..';
|
import { Notification } from '..';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getSettings } from '../../settings';
|
import { getSettings, NotificationAgentEmail } from '../../settings';
|
||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
import Email from 'email-templates';
|
import Email from 'email-templates';
|
||||||
import logger from '../../../logger';
|
import logger from '../../../logger';
|
||||||
@@ -9,13 +9,25 @@ import { getRepository } from 'typeorm';
|
|||||||
import { User } from '../../../entity/User';
|
import { User } from '../../../entity/User';
|
||||||
import { Permission } from '../../permissions';
|
import { Permission } from '../../permissions';
|
||||||
|
|
||||||
class EmailAgent implements NotificationAgent {
|
class EmailAgent
|
||||||
|
extends BaseAgent<NotificationAgentEmail>
|
||||||
|
implements NotificationAgent {
|
||||||
|
protected getSettings(): NotificationAgentEmail {
|
||||||
|
if (this.settings) {
|
||||||
|
return this.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
|
return settings.notifications.agents.email;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Add checking for type here once we add notification type filters for agents
|
// TODO: Add checking for type here once we add notification type filters for agents
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
public shouldSend(_type: Notification): boolean {
|
public shouldSend(_type: Notification): boolean {
|
||||||
const settings = getSettings();
|
const settings = this.getSettings();
|
||||||
|
|
||||||
if (settings.notifications.agents.email.enabled) {
|
if (settings.enabled) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +35,7 @@ class EmailAgent implements NotificationAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getSmtpTransport() {
|
private getSmtpTransport() {
|
||||||
const emailSettings = getSettings().notifications.agents.email.options;
|
const emailSettings = this.getSettings().options;
|
||||||
|
|
||||||
return nodemailer.createTransport({
|
return nodemailer.createTransport({
|
||||||
host: emailSettings.smtpHost,
|
host: emailSettings.smtpHost,
|
||||||
@@ -40,7 +52,7 @@ class EmailAgent implements NotificationAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getNewEmail() {
|
private getNewEmail() {
|
||||||
const settings = getSettings().notifications.agents.email;
|
const settings = this.getSettings();
|
||||||
return new Email({
|
return new Email({
|
||||||
message: {
|
message: {
|
||||||
from: settings.options.emailFrom,
|
from: settings.options.emailFrom,
|
||||||
@@ -51,7 +63,8 @@ class EmailAgent implements NotificationAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async sendMediaRequestEmail(payload: NotificationPayload) {
|
private async sendMediaRequestEmail(payload: NotificationPayload) {
|
||||||
const settings = getSettings().main;
|
// This is getting main settings for the whole app
|
||||||
|
const applicationUrl = getSettings().main.applicationUrl;
|
||||||
try {
|
try {
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
const users = await userRepository.find();
|
const users = await userRepository.find();
|
||||||
@@ -76,7 +89,7 @@ class EmailAgent implements NotificationAgent {
|
|||||||
imageUrl: payload.image,
|
imageUrl: payload.image,
|
||||||
timestamp: new Date().toTimeString(),
|
timestamp: new Date().toTimeString(),
|
||||||
requestedBy: payload.notifyUser.username,
|
requestedBy: payload.notifyUser.username,
|
||||||
actionUrl: settings.applicationUrl,
|
actionUrl: applicationUrl,
|
||||||
requestType: 'New Request',
|
requestType: 'New Request',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -92,7 +105,8 @@ class EmailAgent implements NotificationAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async sendMediaApprovedEmail(payload: NotificationPayload) {
|
private async sendMediaApprovedEmail(payload: NotificationPayload) {
|
||||||
const settings = getSettings().main;
|
// This is getting main settings for the whole app
|
||||||
|
const applicationUrl = getSettings().main.applicationUrl;
|
||||||
try {
|
try {
|
||||||
const email = this.getNewEmail();
|
const email = this.getNewEmail();
|
||||||
|
|
||||||
@@ -110,7 +124,7 @@ class EmailAgent implements NotificationAgent {
|
|||||||
imageUrl: payload.image,
|
imageUrl: payload.image,
|
||||||
timestamp: new Date().toTimeString(),
|
timestamp: new Date().toTimeString(),
|
||||||
requestedBy: payload.notifyUser.username,
|
requestedBy: payload.notifyUser.username,
|
||||||
actionUrl: settings.applicationUrl,
|
actionUrl: applicationUrl,
|
||||||
requestType: 'Request Approved',
|
requestType: 'Request Approved',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -125,7 +139,8 @@ class EmailAgent implements NotificationAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async sendMediaAvailableEmail(payload: NotificationPayload) {
|
private async sendMediaAvailableEmail(payload: NotificationPayload) {
|
||||||
const settings = getSettings().main;
|
// This is getting main settings for the whole app
|
||||||
|
const applicationUrl = getSettings().main.applicationUrl;
|
||||||
try {
|
try {
|
||||||
const email = this.getNewEmail();
|
const email = this.getNewEmail();
|
||||||
|
|
||||||
@@ -143,7 +158,7 @@ class EmailAgent implements NotificationAgent {
|
|||||||
imageUrl: payload.image,
|
imageUrl: payload.image,
|
||||||
timestamp: new Date().toTimeString(),
|
timestamp: new Date().toTimeString(),
|
||||||
requestedBy: payload.notifyUser.username,
|
requestedBy: payload.notifyUser.username,
|
||||||
actionUrl: settings.applicationUrl,
|
actionUrl: applicationUrl,
|
||||||
requestType: 'Now Available',
|
requestType: 'Now Available',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -157,6 +172,32 @@ class EmailAgent implements NotificationAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async sendTestEmail(payload: NotificationPayload) {
|
||||||
|
// This is getting main settings for the whole app
|
||||||
|
const applicationUrl = getSettings().main.applicationUrl;
|
||||||
|
try {
|
||||||
|
const email = this.getNewEmail();
|
||||||
|
|
||||||
|
email.send({
|
||||||
|
template: path.join(__dirname, '../../../templates/email/test-email'),
|
||||||
|
message: {
|
||||||
|
to: payload.notifyUser.email,
|
||||||
|
},
|
||||||
|
locals: {
|
||||||
|
body: payload.message,
|
||||||
|
actionUrl: applicationUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Mail notification failed to send', {
|
||||||
|
label: 'Notifications',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async send(
|
public async send(
|
||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
@@ -173,6 +214,9 @@ class EmailAgent implements NotificationAgent {
|
|||||||
case Notification.MEDIA_AVAILABLE:
|
case Notification.MEDIA_AVAILABLE:
|
||||||
this.sendMediaAvailableEmail(payload);
|
this.sendMediaAvailableEmail(payload);
|
||||||
break;
|
break;
|
||||||
|
case Notification.TEST_NOTIFICATION:
|
||||||
|
this.sendTestEmail(payload);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@@ -5,6 +5,7 @@ export enum Notification {
|
|||||||
MEDIA_PENDING = 2,
|
MEDIA_PENDING = 2,
|
||||||
MEDIA_APPROVED = 4,
|
MEDIA_APPROVED = 4,
|
||||||
MEDIA_AVAILABLE = 8,
|
MEDIA_AVAILABLE = 8,
|
||||||
|
TEST_NOTIFICATION = 16,
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationManager {
|
class NotificationManager {
|
||||||
|
@@ -53,18 +53,18 @@ interface PublicSettings {
|
|||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotificationAgent {
|
export interface NotificationAgentConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
types: number;
|
types: number;
|
||||||
options: Record<string, unknown>;
|
options: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
interface NotificationAgentDiscord extends NotificationAgent {
|
export interface NotificationAgentDiscord extends NotificationAgentConfig {
|
||||||
options: {
|
options: {
|
||||||
webhookUrl: string;
|
webhookUrl: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotificationAgentEmail extends NotificationAgent {
|
export interface NotificationAgentEmail extends NotificationAgentConfig {
|
||||||
options: {
|
options: {
|
||||||
emailFrom: string;
|
emailFrom: string;
|
||||||
smtpHost: string;
|
smtpHost: string;
|
||||||
|
@@ -21,6 +21,9 @@ import Media from '../entity/Media';
|
|||||||
import { MediaRequest } from '../entity/MediaRequest';
|
import { MediaRequest } from '../entity/MediaRequest';
|
||||||
import { getAppVersion } from '../utils/appVersion';
|
import { getAppVersion } from '../utils/appVersion';
|
||||||
import { SettingsAboutResponse } from '../interfaces/api/settingsInterfaces';
|
import { SettingsAboutResponse } from '../interfaces/api/settingsInterfaces';
|
||||||
|
import { Notification } from '../lib/notifications';
|
||||||
|
import DiscordAgent from '../lib/notifications/agents/discord';
|
||||||
|
import EmailAgent from '../lib/notifications/agents/email';
|
||||||
|
|
||||||
const settingsRoutes = Router();
|
const settingsRoutes = Router();
|
||||||
|
|
||||||
@@ -448,6 +451,25 @@ settingsRoutes.post('/notifications/discord', (req, res) => {
|
|||||||
res.status(200).json(settings.notifications.agents.discord);
|
res.status(200).json(settings.notifications.agents.discord);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
settingsRoutes.post('/notifications/discord/test', (req, res, next) => {
|
||||||
|
if (!req.user) {
|
||||||
|
return next({
|
||||||
|
status: 500,
|
||||||
|
message: 'User information missing from request',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const discordAgent = new DiscordAgent(req.body);
|
||||||
|
discordAgent.send(Notification.TEST_NOTIFICATION, {
|
||||||
|
notifyUser: req.user,
|
||||||
|
subject: 'Test Notification',
|
||||||
|
message:
|
||||||
|
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(204).send();
|
||||||
|
});
|
||||||
|
|
||||||
settingsRoutes.get('/notifications/email', (_req, res) => {
|
settingsRoutes.get('/notifications/email', (_req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
@@ -463,6 +485,25 @@ settingsRoutes.post('/notifications/email', (req, res) => {
|
|||||||
res.status(200).json(settings.notifications.agents.email);
|
res.status(200).json(settings.notifications.agents.email);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
settingsRoutes.post('/notifications/email/test', (req, res, next) => {
|
||||||
|
if (!req.user) {
|
||||||
|
return next({
|
||||||
|
status: 500,
|
||||||
|
message: 'User information missing from request',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailAgent = new EmailAgent(req.body);
|
||||||
|
emailAgent.send(Notification.TEST_NOTIFICATION, {
|
||||||
|
notifyUser: req.user,
|
||||||
|
subject: 'Test Notification',
|
||||||
|
message:
|
||||||
|
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(204).send();
|
||||||
|
});
|
||||||
|
|
||||||
settingsRoutes.get('/about', async (req, res) => {
|
settingsRoutes.get('/about', async (req, res) => {
|
||||||
const mediaRepository = getRepository(Media);
|
const mediaRepository = getRepository(Media);
|
||||||
const mediaRequestRepository = getRepository(MediaRequest);
|
const mediaRequestRepository = getRepository(MediaRequest);
|
||||||
|
96
server/templates/email/test-email/html.pug
Normal file
96
server/templates/email/test-email/html.pug
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
doctype html
|
||||||
|
head
|
||||||
|
meta(charset='utf-8')
|
||||||
|
meta(name='x-apple-disable-message-reformatting')
|
||||||
|
meta(http-equiv='x-ua-compatible' content='ie=edge')
|
||||||
|
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||||
|
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no')
|
||||||
|
link(href='https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap' rel='stylesheet' media='screen')
|
||||||
|
//if mso
|
||||||
|
xml
|
||||||
|
o:officedocumentsettings
|
||||||
|
o:pixelsperinch 96
|
||||||
|
style.
|
||||||
|
td,
|
||||||
|
th,
|
||||||
|
div,
|
||||||
|
p,
|
||||||
|
a,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-family: 'Segoe UI', sans-serif;
|
||||||
|
mso-line-height-rule: exactly;
|
||||||
|
}
|
||||||
|
style.
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.sm-w-full {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
||||||
|
table(style="\
|
||||||
|
background-color: #f2f4f6;\
|
||||||
|
font-family: 'Nunito Sans', -apple-system, 'Segoe UI', sans-serif;\
|
||||||
|
width: 100%;\
|
||||||
|
" width='100%' bgcolor='#f2f4f6' cellpadding='0' cellspacing='0' role='presentation')
|
||||||
|
tr
|
||||||
|
td(align='center')
|
||||||
|
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
|
||||||
|
tr
|
||||||
|
td(align='center' style='\
|
||||||
|
font-size: 16px;\
|
||||||
|
padding-top: 25px;\
|
||||||
|
padding-bottom: 25px;\
|
||||||
|
text-align: center;\
|
||||||
|
')
|
||||||
|
a(href=actionUrl style='\
|
||||||
|
text-shadow: 0 1px 0 #ffffff;\
|
||||||
|
font-weight: 700;\
|
||||||
|
font-size: 16px;\
|
||||||
|
color: #a8aaaf;\
|
||||||
|
text-decoration: none;\
|
||||||
|
')
|
||||||
|
| Overseerr
|
||||||
|
tr
|
||||||
|
td(style='width: 100%' width='100%')
|
||||||
|
table.sm-w-full(align='center' style='\
|
||||||
|
background-color: #ffffff;\
|
||||||
|
margin-left: auto;\
|
||||||
|
margin-right: auto;\
|
||||||
|
width: 570px;\
|
||||||
|
' width='570' bgcolor='#ffffff' cellpadding='0' cellspacing='0' role='presentation')
|
||||||
|
tr
|
||||||
|
td(style='padding: 45px')
|
||||||
|
div(style='font-size: 16px')
|
||||||
|
| #{body}
|
||||||
|
p(style='\
|
||||||
|
font-size: 13px;\
|
||||||
|
line-height: 24px;\
|
||||||
|
margin-top: 6px;\
|
||||||
|
margin-bottom: 20px;\
|
||||||
|
color: #51545e;\
|
||||||
|
')
|
||||||
|
a(href=actionUrl style='color: #3869d4') Open Overseerr
|
||||||
|
tr
|
||||||
|
td
|
||||||
|
table.sm-w-full(align='center' style='\
|
||||||
|
margin-left: auto;\
|
||||||
|
margin-right: auto;\
|
||||||
|
text-align: center;\
|
||||||
|
width: 570px;\
|
||||||
|
' width='570' cellpadding='0' cellspacing='0' role='presentation')
|
||||||
|
tr
|
||||||
|
td(align='center' style='font-size: 16px; padding: 45px')
|
||||||
|
p(style='\
|
||||||
|
font-size: 13px;\
|
||||||
|
line-height: 24px;\
|
||||||
|
margin-top: 6px;\
|
||||||
|
margin-bottom: 20px;\
|
||||||
|
text-align: center;\
|
||||||
|
color: #a8aaaf;\
|
||||||
|
')
|
||||||
|
| Overseerr.
|
1
server/templates/email/test-email/subject.pug
Normal file
1
server/templates/email/test-email/subject.pug
Normal file
@@ -0,0 +1 @@
|
|||||||
|
= `Test Notification - Overseerr`
|
@@ -4,7 +4,7 @@ import useSWR from 'swr';
|
|||||||
import LoadingSpinner from '../../Common/LoadingSpinner';
|
import LoadingSpinner from '../../Common/LoadingSpinner';
|
||||||
import Button from '../../Common/Button';
|
import Button from '../../Common/Button';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import Axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
|
|
||||||
@@ -17,6 +17,8 @@ const messages = defineMessages({
|
|||||||
webhookUrlPlaceholder: 'Server Settings -> Integrations -> Webhooks',
|
webhookUrlPlaceholder: 'Server Settings -> Integrations -> Webhooks',
|
||||||
discordsettingssaved: 'Discord notification settings saved!',
|
discordsettingssaved: 'Discord notification settings saved!',
|
||||||
discordsettingsfailed: 'Discord notification settings failed to save.',
|
discordsettingsfailed: 'Discord notification settings failed to save.',
|
||||||
|
testsent: 'Test notification sent!',
|
||||||
|
test: 'Test',
|
||||||
});
|
});
|
||||||
|
|
||||||
const NotificationsDiscord: React.FC = () => {
|
const NotificationsDiscord: React.FC = () => {
|
||||||
@@ -46,7 +48,7 @@ const NotificationsDiscord: React.FC = () => {
|
|||||||
validationSchema={NotificationsDiscordSchema}
|
validationSchema={NotificationsDiscordSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
await Axios.post('/api/v1/settings/notifications/discord', {
|
await axios.post('/api/v1/settings/notifications/discord', {
|
||||||
enabled: values.enabled,
|
enabled: values.enabled,
|
||||||
types: values.types,
|
types: values.types,
|
||||||
options: {
|
options: {
|
||||||
@@ -67,7 +69,22 @@ const NotificationsDiscord: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ errors, touched, isSubmitting }) => {
|
{({ errors, touched, isSubmitting, values, isValid }) => {
|
||||||
|
const testSettings = async () => {
|
||||||
|
await axios.post('/api/v1/settings/notifications/discord/test', {
|
||||||
|
enabled: true,
|
||||||
|
types: values.types,
|
||||||
|
options: {
|
||||||
|
webhookUrl: values.webhookUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
addToast(intl.formatMessage(messages.testsent), {
|
||||||
|
appearance: 'info',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||||
@@ -112,11 +129,24 @@ const NotificationsDiscord: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-8 border-t border-gray-700 pt-5">
|
<div className="mt-8 border-t border-gray-700 pt-5">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
|
<Button
|
||||||
|
buttonType="warning"
|
||||||
|
disabled={isSubmitting || !isValid}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
testSettings();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.test)}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
<Button
|
<Button
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || !isValid}
|
||||||
>
|
>
|
||||||
{isSubmitting
|
{isSubmitting
|
||||||
? intl.formatMessage(messages.saving)
|
? intl.formatMessage(messages.saving)
|
||||||
|
@@ -4,7 +4,7 @@ import useSWR from 'swr';
|
|||||||
import LoadingSpinner from '../../Common/LoadingSpinner';
|
import LoadingSpinner from '../../Common/LoadingSpinner';
|
||||||
import Button from '../../Common/Button';
|
import Button from '../../Common/Button';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import Axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
|
|
||||||
@@ -23,6 +23,8 @@ const messages = defineMessages({
|
|||||||
authPass: 'Auth Pass',
|
authPass: 'Auth Pass',
|
||||||
emailsettingssaved: 'Email notification settings saved!',
|
emailsettingssaved: 'Email notification settings saved!',
|
||||||
emailsettingsfailed: 'Email notification settings failed to save.',
|
emailsettingsfailed: 'Email notification settings failed to save.',
|
||||||
|
test: 'Test',
|
||||||
|
testsent: 'Test notification sent!',
|
||||||
});
|
});
|
||||||
|
|
||||||
const NotificationsEmail: React.FC = () => {
|
const NotificationsEmail: React.FC = () => {
|
||||||
@@ -63,7 +65,7 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
validationSchema={NotificationsDiscordSchema}
|
validationSchema={NotificationsDiscordSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
await Axios.post('/api/v1/settings/notifications/email', {
|
await axios.post('/api/v1/settings/notifications/email', {
|
||||||
enabled: values.enabled,
|
enabled: values.enabled,
|
||||||
types: values.types,
|
types: values.types,
|
||||||
options: {
|
options: {
|
||||||
@@ -89,7 +91,27 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ errors, touched, isSubmitting }) => {
|
{({ errors, touched, isSubmitting, values, isValid }) => {
|
||||||
|
const testSettings = async () => {
|
||||||
|
await axios.post('/api/v1/settings/notifications/email/test', {
|
||||||
|
enabled: true,
|
||||||
|
types: values.types,
|
||||||
|
options: {
|
||||||
|
emailFrom: values.emailFrom,
|
||||||
|
smtpHost: values.smtpHost,
|
||||||
|
smtpPort: Number(values.smtpPort),
|
||||||
|
secure: values.secure,
|
||||||
|
authUser: values.authUser,
|
||||||
|
authPass: values.authPass,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
addToast(intl.formatMessage(messages.testsent), {
|
||||||
|
appearance: 'info',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||||
@@ -228,11 +250,24 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-8 border-t border-gray-700 pt-5">
|
<div className="mt-8 border-t border-gray-700 pt-5">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
|
<Button
|
||||||
|
buttonType="warning"
|
||||||
|
disabled={isSubmitting || !isValid}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
testSettings();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.test)}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
<Button
|
<Button
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || !isValid}
|
||||||
>
|
>
|
||||||
{isSubmitting
|
{isSubmitting
|
||||||
? intl.formatMessage(messages.saving)
|
? intl.formatMessage(messages.saving)
|
||||||
|
@@ -102,6 +102,8 @@
|
|||||||
"components.Settings.Notifications.saving": "Saving…",
|
"components.Settings.Notifications.saving": "Saving…",
|
||||||
"components.Settings.Notifications.smtpHost": "SMTP Host",
|
"components.Settings.Notifications.smtpHost": "SMTP Host",
|
||||||
"components.Settings.Notifications.smtpPort": "SMTP Port",
|
"components.Settings.Notifications.smtpPort": "SMTP Port",
|
||||||
|
"components.Settings.Notifications.test": "Test",
|
||||||
|
"components.Settings.Notifications.testsent": "Test notification sent!",
|
||||||
"components.Settings.Notifications.validationFromRequired": "You must provide an email sender address",
|
"components.Settings.Notifications.validationFromRequired": "You must provide an email sender address",
|
||||||
"components.Settings.Notifications.validationSmtpHostRequired": "You must provide an SMTP host",
|
"components.Settings.Notifications.validationSmtpHostRequired": "You must provide an SMTP host",
|
||||||
"components.Settings.Notifications.validationSmtpPortRequired": "You must provide an SMTP port",
|
"components.Settings.Notifications.validationSmtpPortRequired": "You must provide an SMTP port",
|
||||||
|
Reference in New Issue
Block a user