Merge branch 'develop' of https://github.com/sct/overseerr into jellyfin-support

This commit is contained in:
Juan D. Jara
2021-09-27 02:24:30 +02:00
411 changed files with 35232 additions and 20531 deletions

View File

@@ -6,7 +6,7 @@ import { NotificationAgentConfig } from '../../settings';
export interface NotificationPayload {
subject: string;
notifyUser: User;
notifyUser?: User;
media?: Media;
image?: string;
message?: string;
@@ -24,6 +24,6 @@ export abstract class BaseAgent<T extends NotificationAgentConfig> {
}
export interface NotificationAgent {
shouldSend(type: Notification, payload: NotificationPayload): boolean;
shouldSend(): boolean;
send(type: Notification, payload: NotificationPayload): Promise<boolean>;
}

View File

@@ -1,7 +1,14 @@
import axios from 'axios';
import { getRepository } from 'typeorm';
import { hasNotificationType, Notification } from '..';
import { User } from '../../../entity/User';
import logger from '../../../logger';
import { getSettings, NotificationAgentDiscord } from '../../settings';
import { Permission } from '../../permissions';
import {
getSettings,
NotificationAgentDiscord,
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
enum EmbedColors {
@@ -71,7 +78,7 @@ interface DiscordRichEmbed {
interface DiscordWebhookPayload {
embeds: DiscordRichEmbed[];
username: string;
username?: string;
avatar_url?: string;
tts: boolean;
content?: string;
@@ -107,7 +114,7 @@ class DiscordAgent
if (payload.request) {
fields.push({
name: 'Requested By',
value: payload.notifyUser.displayName ?? '',
value: payload.request.requestedBy.displayName,
inline: true,
});
}
@@ -122,6 +129,7 @@ class DiscordAgent
});
break;
case Notification.MEDIA_APPROVED:
case Notification.MEDIA_AUTO_APPROVED:
color = EmbedColors.PURPLE;
fields.push({
name: 'Status',
@@ -155,15 +163,14 @@ class DiscordAgent
break;
}
if (settings.main.applicationUrl && payload.media) {
fields.push({
name: `Open in ${settings.main.applicationTitle}`,
value: `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
});
}
const url =
settings.main.applicationUrl && payload.media
? `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`
: undefined;
return {
title: payload.subject,
url,
description: payload.message,
color,
timestamp: new Date().toISOString(),
@@ -185,12 +192,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;
}
@@ -201,42 +206,72 @@ class DiscordAgent
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending discord notification', { label: 'Notifications' });
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
return true;
}
logger.debug('Sending Discord notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
let content = undefined;
try {
const settings = getSettings();
const webhookUrl = this.getSettings().options.webhookUrl;
if (payload.notifyUser) {
// Mention user who submitted the request
if (
payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.DISCORD,
type
) &&
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 (!webhookUrl) {
return false;
content = users
.filter(
(user) =>
user.hasPermission(Permission.MANAGE_REQUESTS) &&
user.settings?.hasNotificationType(
NotificationAgentKey.DISCORD,
type
) &&
user.settings?.discordId &&
// Check if it's the user's own auto-approved request
(type !== Notification.MEDIA_AUTO_APPROVED ||
user.id !== payload.request?.requestedBy.id)
)
.map((user) => `<@${user.settings?.discordId}>`)
.join(' ');
}
const mentionedUsers: string[] = [];
let content = undefined;
if (
payload.notifyUser.settings?.enableNotifications &&
payload.notifyUser.settings?.discordId
) {
mentionedUsers.push(payload.notifyUser.settings.discordId);
content = `<@${payload.notifyUser.settings.discordId}>`;
}
await axios.post(webhookUrl, {
username: settings.main.applicationTitle,
await axios.post(settings.options.webhookUrl, {
username: settings.options.botUsername,
avatar_url: settings.options.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,
response: e.response.data,
mentions: content,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
});
return false;
}
}

View File

@@ -1,12 +1,18 @@
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import { hasNotificationType, Notification } from '..';
import { EmailOptions } from 'email-templates';
import path from 'path';
import { getSettings, NotificationAgentEmail } from '../../settings';
import logger from '../../../logger';
import { getRepository } from 'typeorm';
import { Notification } from '..';
import { MediaType } from '../../../constants/media';
import { User } from '../../../entity/User';
import { Permission } from '../../permissions';
import logger from '../../../logger';
import PreparedEmail from '../../email';
import { Permission } from '../../permissions';
import {
getSettings,
NotificationAgentEmail,
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
class EmailAgent
extends BaseAgent<NotificationAgentEmail>
@@ -21,13 +27,14 @@ class EmailAgent
return settings.notifications.agents.email;
}
public shouldSend(type: Notification, payload: NotificationPayload): boolean {
public shouldSend(): boolean {
const settings = this.getSettings();
if (
settings.enabled &&
hasNotificationType(type, this.getSettings().types) &&
(payload.notifyUser.settings?.enableNotifications ?? true)
settings.options.emailFrom &&
settings.options.smtpHost &&
settings.options.smtpPort
) {
return true;
}
@@ -35,265 +42,205 @@ 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))
.forEach((user) => {
const email = new PreparedEmail();
email.send({
template: path.join(
__dirname,
'../../../templates/email/media-request'
),
message: {
to: user.email,
},
locals: {
body: 'A user has requested new media!',
mediaName: payload.subject,
imageUrl: payload.image,
timestamp: new Date().toTimeString(),
requestedBy: payload.notifyUser.displayName,
actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined,
applicationUrl,
applicationTitle,
requestType: 'New Request',
},
});
});
return true;
} catch (e) {
logger.error('Mail notification failed to send', {
label: 'Notifications',
message: e.message,
});
return false;
}
}
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();
// Send to all users with the manage requests permission (or admins)
users
.filter((user) => user.hasPermission(Permission.MANAGE_REQUESTS))
.forEach((user) => {
const email = new PreparedEmail();
email.send({
template: path.join(
__dirname,
'../../../templates/email/media-request'
),
message: {
to: user.email,
},
locals: {
body:
"A user's new request has failed to add to Sonarr or Radarr",
mediaName: payload.subject,
imageUrl: payload.image,
timestamp: new Date().toTimeString(),
requestedBy: payload.notifyUser.displayName,
actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined,
applicationUrl,
applicationTitle,
requestType: 'Failed Request',
},
});
});
return true;
} catch (e) {
logger.error('Mail 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 {
const email = new PreparedEmail();
await email.send({
template: path.join(
__dirname,
'../../../templates/email/media-request'
),
message: {
to: payload.notifyUser.email,
},
locals: {
body: 'Your request for the following media has been approved:',
mediaName: payload.subject,
imageUrl: payload.image,
timestamp: new Date().toTimeString(),
requestedBy: payload.notifyUser.displayName,
actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined,
applicationUrl,
applicationTitle,
requestType: 'Request Approved',
},
});
return true;
} catch (e) {
logger.error('Mail 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 {
const email = new PreparedEmail();
await email.send({
template: path.join(
__dirname,
'../../../templates/email/media-request'
),
message: {
to: payload.notifyUser.email,
},
locals: {
body: 'Your request for the following media was declined:',
mediaName: payload.subject,
imageUrl: payload.image,
timestamp: new Date().toTimeString(),
requestedBy: payload.notifyUser.displayName,
actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined,
applicationUrl,
applicationTitle,
requestType: 'Request Declined',
},
});
return true;
} catch (e) {
logger.error('Mail 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 {
const email = new PreparedEmail();
await email.send({
template: path.join(
__dirname,
'../../../templates/email/media-request'
),
message: {
to: payload.notifyUser.email,
},
locals: {
body: 'Your requested media is now available!',
mediaName: payload.subject,
imageUrl: payload.image,
timestamp: new Date().toTimeString(),
requestedBy: payload.notifyUser.displayName,
actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined,
applicationUrl,
applicationTitle,
requestType: 'Now Available',
},
});
return true;
} catch (e) {
logger.error('Mail 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 {
const email = new PreparedEmail();
await email.send({
if (type === Notification.TEST_NOTIFICATION) {
return {
template: path.join(__dirname, '../../../templates/email/test-email'),
message: {
to: payload.notifyUser.email,
to: toEmail,
},
locals: {
body: payload.message,
applicationUrl,
applicationTitle,
},
});
return true;
} catch (e) {
logger.error('Mail notification failed to send', {
label: 'Notifications',
message: e.message,
});
return false;
};
}
if (payload.media) {
let requestType = '';
let body = '';
switch (type) {
case Notification.MEDIA_PENDING:
requestType = `New ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request`;
body = `A user has requested a new ${
payload.media?.mediaType === MediaType.TV ? 'series' : 'movie'
}!`;
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 {
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,
},
};
}
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 ||
// Check if user has email notifications enabled and fallback to true if undefined
// since email should default to true
(payload.notifyUser.settings.hasNotificationType(
NotificationAgentKey.EMAIL,
type
) ??
true)
) {
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_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 ||
// Check if user has email notifications enabled and fallback to true if undefined
// since email should default to true
(user.settings.hasNotificationType(
NotificationAgentKey.EMAIL,
type
) ??
true)) &&
// 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) => {
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;

View File

@@ -0,0 +1,108 @@
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import { MediaStatus } from '../../../constants/media';
import logger from '../../../logger';
import { getSettings, NotificationAgentLunaSea } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
class LunaSeaAgent
extends BaseAgent<NotificationAgentLunaSea>
implements NotificationAgent {
protected getSettings(): NotificationAgentLunaSea {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.lunasea;
}
private buildPayload(type: Notification, payload: NotificationPayload) {
return {
notification_type: Notification[type],
subject: payload.subject,
message: payload.message,
image: payload.image ?? null,
email: payload.notifyUser?.email,
username: payload.notifyUser?.username,
avatar: payload.notifyUser?.avatar,
media: payload.media
? {
media_type: payload.media.mediaType,
tmdbId: payload.media.tmdbId,
imdbId: payload.media.imdbId,
tvdbId: payload.media.tvdbId,
status: MediaStatus[payload.media.status],
status4k: MediaStatus[payload.media.status4k],
}
: null,
extra: payload.extra ?? [],
request: payload.request
? {
request_id: payload.request.id,
requestedBy_email: payload.request.requestedBy.email,
requestedBy_username: payload.request.requestedBy.displayName,
requestedBy_avatar: payload.request.requestedBy.avatar,
}
: null,
};
}
public shouldSend(): boolean {
const settings = this.getSettings();
if (settings.enabled && settings.options.webhookUrl) {
return true;
}
return false;
}
public async send(
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],
subject: payload.subject,
});
try {
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) {
logger.error('Error sending LunaSea notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
});
return false;
}
}
}
export default LunaSeaAgent;

View File

@@ -1,5 +1,6 @@
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';
@@ -22,12 +23,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;
}
@@ -46,11 +45,13 @@ class PushbulletAgent
const title = payload.subject;
const plot = payload.message;
const username = payload.notifyUser.displayName;
const username = payload.request?.requestedBy.displayName;
switch (type) {
case Notification.MEDIA_PENDING:
messageTitle = 'New Request';
messageTitle = `New ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request`;
message += `${title}`;
if (plot) {
message += `\n\n${plot}`;
@@ -59,7 +60,20 @@ class PushbulletAgent
message += `\nStatus: Pending Approval`;
break;
case Notification.MEDIA_APPROVED:
messageTitle = 'Request Approved';
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Approved`;
message += `${title}`;
if (plot) {
message += `\n\n${plot}`;
}
message += `\n\nRequested By: ${username}`;
message += `\nStatus: Processing`;
break;
case Notification.MEDIA_AUTO_APPROVED:
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Automatically Approved`;
message += `${title}`;
if (plot) {
message += `\n\n${plot}`;
@@ -68,7 +82,9 @@ class PushbulletAgent
message += `\nStatus: Processing`;
break;
case Notification.MEDIA_AVAILABLE:
messageTitle = 'Now Available';
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Now Available`;
message += `${title}`;
if (plot) {
message += `\n\n${plot}`;
@@ -77,7 +93,9 @@ class PushbulletAgent
message += `\nStatus: Available`;
break;
case Notification.MEDIA_DECLINED:
messageTitle = 'Request Declined';
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Declined`;
message += `${title}`;
if (plot) {
message += `\n\n${plot}`;
@@ -86,7 +104,9 @@ class PushbulletAgent
message += `\nStatus: Declined`;
break;
case Notification.MEDIA_FAILED:
messageTitle = 'Failed Request';
messageTitle = `Failed ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request`;
message += `${title}`;
if (plot) {
message += `\n\n${plot}`;
@@ -100,6 +120,10 @@ class PushbulletAgent
break;
}
for (const extra of payload.extra ?? []) {
message += `\n${extra.name}: ${extra.value}`;
}
return {
title: messageTitle,
body: message,
@@ -110,16 +134,23 @@ class PushbulletAgent
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending Pushbullet notification', { label: 'Notifications' });
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
return true;
}
logger.debug('Sending Pushbullet notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
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,
@@ -127,7 +158,7 @@ class PushbulletAgent
} as PushbulletPayload,
{
headers: {
'Access-Token': accessToken,
'Access-Token': settings.options.accessToken,
},
}
);
@@ -136,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;
}
}

View File

@@ -1,5 +1,6 @@
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';
@@ -28,12 +29,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;
}
@@ -60,62 +62,87 @@ class PushoverAgent
const title = payload.subject;
const plot = payload.message;
const username = payload.notifyUser.displayName;
const username = payload.request?.requestedBy.displayName;
switch (type) {
case Notification.MEDIA_PENDING:
messageTitle = 'New Request';
messageTitle = `New ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request`;
message += `<b>${title}</b>`;
if (plot) {
message += `\n${plot}`;
message += `<small>\n${plot}</small>`;
}
message += `\n\n<b>Requested By</b>\n${username}`;
message += `\n\n<b>Status</b>\nPending Approval`;
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
message += `<small>\n\n<b>Status</b>\nPending Approval</small>`;
break;
case Notification.MEDIA_APPROVED:
messageTitle = 'Request Approved';
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Approved`;
message += `<b>${title}</b>`;
if (plot) {
message += `\n${plot}`;
message += `<small>\n${plot}</small>`;
}
message += `\n\n<b>Requested By</b>\n${username}`;
message += `\n\n<b>Status</b>\nProcessing`;
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
message += `<small>\n\n<b>Status</b>\nProcessing</small>`;
break;
case Notification.MEDIA_AUTO_APPROVED:
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Automatically Approved`;
message += `<b>${title}</b>`;
if (plot) {
message += `<small>\n${plot}</small>`;
}
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
message += `<small>\n\n<b>Status</b>\nProcessing</small>`;
break;
case Notification.MEDIA_AVAILABLE:
messageTitle = 'Now Available';
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Now Available`;
message += `<b>${title}</b>`;
if (plot) {
message += `\n${plot}`;
message += `<small>\n${plot}</small>`;
}
message += `\n\n<b>Requested By</b>\n${username}`;
message += `\n\n<b>Status</b>\nAvailable`;
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
message += `<small>\n\n<b>Status</b>\nAvailable</small>`;
break;
case Notification.MEDIA_DECLINED:
messageTitle = 'Request Declined';
messageTitle = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Declined`;
message += `<b>${title}</b>`;
if (plot) {
message += `\n${plot}`;
message += `<small>\n${plot}</small>`;
}
message += `\n\n<b>Requested By</b>\n${username}`;
message += `\n\n<b>Status</b>\nDeclined`;
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
message += `<small>\n\n<b>Status</b>\nDeclined</small>`;
priority = 1;
break;
case Notification.MEDIA_FAILED:
messageTitle = 'Failed Request';
messageTitle = `Failed ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request`;
message += `<b>${title}</b>`;
if (plot) {
message += `\n${plot}`;
message += `<small>\n${plot}</small>`;
}
message += `\n\n<b>Requested By</b>\n${username}`;
message += `\n\n<b>Status</b>\nFailed`;
message += `<small>\n\n<b>Requested By</b>\n${username}</small>`;
message += `<small>\n\n<b>Status</b>\nFailed</small>`;
priority = 1;
break;
case Notification.TEST_NOTIFICATION:
messageTitle = 'Test Notification';
message += `${plot}`;
message += `<small>${plot}</small>`;
break;
}
for (const extra of payload.extra ?? []) {
message += `<small>\n\n<b>${extra.name}</b>\n${extra.value}</small>`;
}
if (settings.main.applicationUrl && payload.media) {
url = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`;
url_title = `Open in ${settings.main.applicationTitle}`;
@@ -134,12 +161,20 @@ class PushoverAgent
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending Pushover notification', { label: 'Notifications' });
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
return true;
}
logger.debug('Sending Pushover notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
try {
const endpoint = 'https://api.pushover.net/1/messages.json';
const { accessToken, userToken } = this.getSettings().options;
const {
title,
message,
@@ -149,8 +184,8 @@ class PushoverAgent
} = 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,
@@ -163,8 +198,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;
}
}

View File

@@ -1,5 +1,6 @@
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';
@@ -66,41 +67,60 @@ class SlackAgent
if (payload.request) {
fields.push({
type: 'mrkdwn',
text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`,
text: `*Requested By*\n${payload.request.requestedBy.displayName}`,
});
}
switch (type) {
case Notification.MEDIA_PENDING:
header = 'New Request';
header = `New ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request`;
fields.push({
type: 'mrkdwn',
text: '*Status*\nPending Approval',
});
break;
case Notification.MEDIA_APPROVED:
header = 'Request Approved';
header = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Approved`;
fields.push({
type: 'mrkdwn',
text: '*Status*\nProcessing',
});
break;
case Notification.MEDIA_AUTO_APPROVED:
header = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Automatically Approved`;
fields.push({
type: 'mrkdwn',
text: '*Status*\nProcessing',
});
break;
case Notification.MEDIA_AVAILABLE:
header = 'Now Available';
header = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Now Available`;
fields.push({
type: 'mrkdwn',
text: '*Status*\nAvailable',
});
break;
case Notification.MEDIA_DECLINED:
header = 'Request Declined';
header = `${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Declined`;
fields.push({
type: 'mrkdwn',
text: '*Status*\nDeclined',
});
break;
case Notification.MEDIA_FAILED:
header = 'Failed Request';
header = `Failed ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request`;
fields.push({
type: 'mrkdwn',
text: '*Status*\nFailed',
@@ -111,6 +131,13 @@ class SlackAgent
break;
}
for (const extra of payload.extra ?? []) {
fields.push({
type: 'mrkdwn',
text: `*${extra.name}*\n${extra.value}`,
});
}
if (settings.main.applicationUrl && payload.media) {
actionUrl = `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`;
}
@@ -190,12 +217,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;
}
@@ -206,22 +231,33 @@ class SlackAgent
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending slack notification', { label: 'Notifications' });
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) {
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;
}
}

View File

@@ -1,16 +1,32 @@
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 { getSettings, NotificationAgentTelegram } from '../../settings';
import { Permission } from '../../permissions';
import {
getSettings,
NotificationAgentKey,
NotificationAgentTelegram,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface TelegramPayload {
interface TelegramMessagePayload {
text: string;
parse_mode: string;
chat_id: string;
disable_notification: boolean;
}
interface TelegramPhotoPayload {
photo: string;
caption: string;
parse_mode: string;
chat_id: string;
disable_notification: boolean;
}
class TelegramAgent
extends BaseAgent<NotificationAgentTelegram>
implements NotificationAgent {
@@ -26,12 +42,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;
}
@@ -45,20 +62,24 @@ class TelegramAgent
private buildMessage(
type: Notification,
payload: NotificationPayload
): string {
payload: NotificationPayload,
chatId: string,
sendSilently: boolean
): TelegramMessagePayload | TelegramPhotoPayload {
const settings = getSettings();
let message = '';
const title = this.escapeText(payload.subject);
const plot = this.escapeText(payload.message);
const user = this.escapeText(payload.notifyUser.displayName);
const user = this.escapeText(payload.request?.requestedBy.displayName);
const applicationTitle = this.escapeText(settings.main.applicationTitle);
/* eslint-disable no-useless-escape */
switch (type) {
case Notification.MEDIA_PENDING:
message += `\*New Request\*`;
message += `\*New ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request\*`;
message += `\n\n\*${title}\*`;
if (plot) {
message += `\n${plot}`;
@@ -67,7 +88,20 @@ class TelegramAgent
message += `\n\n\*Status\*\nPending Approval`;
break;
case Notification.MEDIA_APPROVED:
message += `\*Request Approved\*`;
message += `\*${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Approved\*`;
message += `\n\n\*${title}\*`;
if (plot) {
message += `\n${plot}`;
}
message += `\n\n\*Requested By\*\n${user}`;
message += `\n\n\*Status\*\nProcessing`;
break;
case Notification.MEDIA_AUTO_APPROVED:
message += `\*${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Automatically Approved\*`;
message += `\n\n\*${title}\*`;
if (plot) {
message += `\n${plot}`;
@@ -76,7 +110,9 @@ class TelegramAgent
message += `\n\n\*Status\*\nProcessing`;
break;
case Notification.MEDIA_AVAILABLE:
message += `\*Now Available\*`;
message += `\*${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Now Available\*`;
message += `\n\n\*${title}\*`;
if (plot) {
message += `\n${plot}`;
@@ -85,7 +121,9 @@ class TelegramAgent
message += `\n\n\*Status\*\nAvailable`;
break;
case Notification.MEDIA_DECLINED:
message += `\*Request Declined\*`;
message += `\*${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request Declined\*`;
message += `\n\n\*${title}\*`;
if (plot) {
message += `\n${plot}`;
@@ -94,7 +132,9 @@ class TelegramAgent
message += `\n\n\*Status\*\nDeclined`;
break;
case Notification.MEDIA_FAILED:
message += `\*Failed Request\*`;
message += `\*Failed ${
payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie'
} Request\*`;
message += `\n\n\*${title}\*`;
if (plot) {
message += `\n${plot}`;
@@ -108,40 +148,171 @@ class TelegramAgent
break;
}
for (const extra of payload.extra ?? []) {
message += `\n\n\*${extra.name}\*\n${extra.value}`;
}
if (settings.main.applicationUrl && payload.media) {
const actionUrl = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`;
message += `\n\n\[Open in ${applicationTitle}\]\(${actionUrl}\)`;
}
/* 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> {
logger.debug('Sending telegram notification', { label: 'Notifications' });
try {
const endpoint = `${this.baseUrl}bot${
this.getSettings().options.botAPI
}/sendMessage`;
const settings = this.getSettings();
await axios.post(endpoint, {
text: this.buildMessage(type, payload),
parse_mode: 'MarkdownV2',
chat_id: `${this.getSettings().options.chatId}`,
disable_notification: this.getSettings().options.sendSilently,
} as TelegramPayload);
const endpoint = `${this.baseUrl}bot${settings.options.botAPI}/${
payload.image ? 'sendPhoto' : 'sendMessage'
}`;
return true;
} catch (e) {
logger.error('Error sending Telegram notification', {
// Send system notification
if (hasNotificationType(type, settings.types ?? 0)) {
logger.debug('Sending Telegram notification', {
label: 'Notifications',
message: e.message,
type: Notification[type],
subject: payload.subject,
});
return false;
try {
await axios.post(
endpoint,
this.buildMessage(
type,
payload,
settings.options.chatId,
settings.options.sendSilently
)
);
} 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) {
// 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

@@ -20,6 +20,7 @@ const KeyMap: Record<string, string | KeyMapFunction> = {
notifyuser_email: 'notifyUser.email',
notifyuser_avatar: 'notifyUser.avatar',
notifyuser_settings_discordId: 'notifyUser.settings.discordId',
notifyuser_settings_telegramChatId: 'notifyUser.settings.telegramChatId',
media_tmdbid: 'media.tmdbId',
media_imdbid: 'media.imdbId',
media_tvdbid: 'media.tvdbId',
@@ -29,6 +30,12 @@ const KeyMap: Record<string, string | KeyMapFunction> = {
media_status4k: (payload) =>
payload.media?.status ? MediaStatus[payload.media?.status4k] : '',
request_id: 'request.id',
requestedBy_username: 'request.requestedBy.displayName',
requestedBy_email: 'request.requestedBy.email',
requestedBy_avatar: 'request.requestedBy.avatar',
requestedBy_settings_discordId: 'request.requestedBy.settings.discordId',
requestedBy_settings_telegramChatId:
'request.requestedBy.settings.telegramChatId',
};
class WebhookAgent
@@ -105,12 +112,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;
}
@@ -121,26 +126,41 @@ class WebhookAgent
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending webhook notification', { label: 'Notifications' });
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
return true;
}
logger.debug('Sending webhook notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
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) {
logger.error('Error sending Webhook notification', {
logger.error('Error sending webhook notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
});
return false;
}
}

View File

@@ -0,0 +1,254 @@
import { getRepository } from 'typeorm';
import webpush from 'web-push';
import { Notification } from '..';
import { MediaType } from '../../../constants/media';
import { User } from '../../../entity/User';
import { UserPushSubscription } from '../../../entity/UserPushSubscription';
import logger from '../../../logger';
import { Permission } from '../../permissions';
import {
getSettings,
NotificationAgentConfig,
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface PushNotificationPayload {
notificationType: string;
mediaType?: 'movie' | 'tv';
tmdbId?: number;
subject: string;
message?: string;
image?: string;
actionUrl?: string;
requestId?: number;
}
class WebPushAgent
extends BaseAgent<NotificationAgentConfig>
implements NotificationAgent {
protected getSettings(): NotificationAgentConfig {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.webpush;
}
private getNotificationPayload(
type: Notification,
payload: NotificationPayload
): PushNotificationPayload {
switch (type) {
case Notification.NONE:
return {
notificationType: Notification[type],
subject: 'Unknown',
};
case Notification.TEST_NOTIFICATION:
return {
notificationType: Notification[type],
subject: payload.subject,
message: payload.message,
};
case Notification.MEDIA_APPROVED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Your ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request has been approved.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_AUTO_APPROVED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Automatically approved a new ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request from ${payload.request?.requestedBy.displayName}.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_AVAILABLE:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Your ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request is now available!`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_DECLINED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Your ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request was declined.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_FAILED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Failed to process ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_PENDING:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Approval required for new ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request from ${payload.request?.requestedBy.displayName}.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
}
}
public shouldSend(): boolean {
if (this.getSettings().enabled) {
return true;
}
return false;
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
const userRepository = getRepository(User);
const userPushSubRepository = getRepository(UserPushSubscription);
const settings = getSettings();
let pushSubs: UserPushSubscription[] = [];
const mainUser = await userRepository.findOne({ where: { id: 1 } });
if (
payload.notifyUser &&
// Check if user has webpush notifications enabled and fallback to true if undefined
// since web push should default to true
(payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.WEBPUSH,
type
) ??
true)
) {
const notifySubs = await userPushSubRepository.find({
where: { user: payload.notifyUser.id },
});
pushSubs = notifySubs;
} else if (!payload.notifyUser) {
const users = await userRepository.find();
const manageUsers = users.filter(
(user) =>
user.hasPermission(Permission.MANAGE_REQUESTS) &&
// Check if user has webpush notifications enabled and fallback to true if undefined
// since web push should default to true
(user.settings?.hasNotificationType(
NotificationAgentKey.WEBPUSH,
type
) ??
true) &&
// Check if it's the user's own auto-approved request
(type !== Notification.MEDIA_AUTO_APPROVED ||
user.id !== payload.request?.requestedBy.id)
);
const allSubs = await userPushSubRepository
.createQueryBuilder('pushSub')
.leftJoinAndSelect('pushSub.user', 'user')
.where('pushSub.userId IN (:users)', {
users: manageUsers.map((user) => user.id),
})
.getMany();
pushSubs = allSubs;
}
if (mainUser && pushSubs.length > 0) {
webpush.setVapidDetails(
`mailto:${mainUser.email}`,
settings.vapidPublic,
settings.vapidPrivate
);
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(
{
endpoint: sub.endpoint,
keys: {
auth: sub.auth,
p256dh: sub.p256dh,
},
},
Buffer.from(
JSON.stringify(this.getNotificationPayload(type, payload)),
'utf-8'
)
);
} 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;
}
}
export default WebPushAgent;

View File

@@ -1,14 +1,15 @@
import logger from '../../logger';
import { getSettings } from '../settings';
import type { NotificationAgent, NotificationPayload } from './agents/agent';
export enum Notification {
NONE = 0,
MEDIA_PENDING = 2,
MEDIA_APPROVED = 4,
MEDIA_AVAILABLE = 8,
MEDIA_FAILED = 16,
TEST_NOTIFICATION = 32,
MEDIA_DECLINED = 64,
MEDIA_AUTO_APPROVED = 128,
}
export const hasNotificationType = (
@@ -29,6 +30,11 @@ export const hasNotificationType = (
total = types;
}
// Test notifications don't need to be enabled
if (!(value & Notification.TEST_NOTIFICATION)) {
value += Notification.TEST_NOTIFICATION;
}
return !!(value & total);
};
@@ -37,19 +43,20 @@ 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(
type: Notification,
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, payload)) {
if (agent.shouldSend()) {
agent.send(type, payload);
}
});