feat(notifications): add option to send notifications for auto-approved requests

closes #267
pull/738/head
sct 4 years ago
parent d0c830e80d
commit 21db3676d1

@ -923,6 +923,15 @@ components:
type: number type: number
sound: sound:
type: string type: string
NotificationSettings:
type: object
properties:
enabled:
type: boolean
example: true
autoapprovalEnabled:
type: boolean
example: false
NotificationEmailSettings: NotificationEmailSettings:
type: object type: object
properties: properties:
@ -1763,6 +1772,37 @@ paths:
nextExecutionTime: nextExecutionTime:
type: string type: string
example: '2020-09-02T05:02:23.000Z' example: '2020-09-02T05:02:23.000Z'
/settings/notifications:
get:
summary: Return current notification settings
description: Returns current notification settings in JSON format
tags:
- settings
responses:
'200':
description: Returned settings
content:
application/json:
schema:
$ref: '#/components/schemas/NotificationSettings'
post:
summary: Update notification settings
description: Update current notification settings with 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: /settings/notifications/email:
get: get:
summary: Return current email notification settings summary: Return current email notification settings

@ -201,6 +201,18 @@ export class MediaRequest {
} }
} }
@AfterInsert()
public async autoapprovalNotification(): Promise<void> {
const settings = getSettings().notifications;
if (
settings.autoapprovalEnabled &&
this.status === MediaRequestStatus.APPROVED
) {
this.notifyApprovedOrDeclined();
}
}
@AfterUpdate() @AfterUpdate()
@AfterInsert() @AfterInsert()
public async updateParentStatus(): Promise<void> { public async updateParentStatus(): Promise<void> {

@ -1,4 +1,5 @@
import logger from '../../logger'; import logger from '../../logger';
import { getSettings } from '../settings';
import type { NotificationAgent, NotificationPayload } from './agents/agent'; import type { NotificationAgent, NotificationPayload } from './agents/agent';
export enum Notification { export enum Notification {
@ -43,11 +44,12 @@ class NotificationManager {
type: Notification, type: Notification,
payload: NotificationPayload payload: NotificationPayload
): void { ): void {
const settings = getSettings().notifications;
logger.info(`Sending notification for ${Notification[type]}`, { logger.info(`Sending notification for ${Notification[type]}`, {
label: 'Notifications', label: 'Notifications',
}); });
this.activeAgents.forEach((agent) => { this.activeAgents.forEach((agent) => {
if (agent.shouldSend(type)) { if (settings.enabled && agent.shouldSend(type)) {
agent.send(type, payload); agent.send(type, payload);
} }
}); });

@ -126,6 +126,8 @@ interface NotificationAgents {
} }
interface NotificationSettings { interface NotificationSettings {
enabled: boolean;
autoapprovalEnabled: boolean;
agents: NotificationAgents; agents: NotificationAgents;
} }
@ -168,6 +170,8 @@ class Settings {
initialized: false, initialized: false,
}, },
notifications: { notifications: {
enabled: true,
autoapprovalEnabled: false,
agents: { agents: {
email: { email: {
enabled: false, enabled: false,

@ -10,6 +10,29 @@ import WebhookAgent from '../../lib/notifications/agents/webhook';
const notificationRoutes = Router(); const notificationRoutes = Router();
notificationRoutes.get('/', (_req, res) => {
const settings = getSettings().notifications;
return res.status(200).json({
enabled: settings.enabled,
autoapprovalEnabled: settings.autoapprovalEnabled,
});
});
notificationRoutes.post('/', (req, res) => {
const settings = getSettings();
Object.assign(settings.notifications, {
enabled: req.body.enabled,
autoapprovalEnabled: req.body.autoapprovalEnabled,
});
settings.save();
return res.status(200).json({
enabled: settings.notifications.enabled,
autoapprovalEnabled: settings.notifications.autoapprovalEnabled,
});
});
notificationRoutes.get('/discord', (_req, res) => { notificationRoutes.get('/discord', (_req, res) => {
const settings = getSettings(); const settings = getSettings();

@ -7,11 +7,25 @@ import SlackLogo from '../../assets/extlogos/slack.svg';
import TelegramLogo from '../../assets/extlogos/telegram.svg'; import TelegramLogo from '../../assets/extlogos/telegram.svg';
import PushoverLogo from '../../assets/extlogos/pushover.svg'; import PushoverLogo from '../../assets/extlogos/pushover.svg';
import Bolt from '../../assets/bolt.svg'; import Bolt from '../../assets/bolt.svg';
import { Field, Form, Formik } from 'formik';
import useSWR from 'swr';
import Error from '../../pages/_error';
import LoadingSpinner from '../Common/LoadingSpinner';
import axios from 'axios';
import { useToasts } from 'react-toast-notifications';
import Button from '../Common/Button';
const messages = defineMessages({ const messages = defineMessages({
save: 'Save Changes',
saving: 'Saving…',
notificationsettings: 'Notification Settings', notificationsettings: 'Notification Settings',
notificationsettingsDescription: notificationsettingsDescription:
'Global notification configuration. The settings below affect all notification agents.',
notificationAgentsSettings: 'Notification Agents',
notificationAgentSettingsDescription:
'Here you can pick and choose what types of notifications to send and through what types of services.', 'Here you can pick and choose what types of notifications to send and through what types of services.',
notificationsettingssaved: 'Notification settings saved!',
notificationsettingsfailed: 'Notification settings failed to save.',
}); });
interface SettingsRoute { interface SettingsRoute {
@ -106,6 +120,8 @@ const settingsRoutes: SettingsRoute[] = [
const SettingsNotifications: React.FC = ({ children }) => { const SettingsNotifications: React.FC = ({ children }) => {
const router = useRouter(); const router = useRouter();
const intl = useIntl(); const intl = useIntl();
const { addToast } = useToasts();
const { data, error, revalidate } = useSWR('/api/v1/settings/notifications');
const activeLinkColor = 'bg-indigo-700'; const activeLinkColor = 'bg-indigo-700';
@ -134,6 +150,14 @@ const SettingsNotifications: React.FC = ({ children }) => {
); );
}; };
if (!data && !error) {
return <LoadingSpinner />;
}
if (!data) {
return <Error statusCode={500} />;
}
return ( return (
<> <>
<div className="mb-6"> <div className="mb-6">
@ -144,6 +168,110 @@ const SettingsNotifications: React.FC = ({ children }) => {
{intl.formatMessage(messages.notificationsettingsDescription)} {intl.formatMessage(messages.notificationsettingsDescription)}
</p> </p>
</div> </div>
<div className="mt-6 sm:mt-5">
<Formik
initialValues={{
enabled: data.enabled,
autoapprovalEnabled: data.autoapprovalEnabled,
}}
enableReinitialize
onSubmit={async (values) => {
try {
await axios.post('/api/v1/settings/notifications', {
enabled: values.enabled,
autoapprovalEnabled: values.autoapprovalEnabled,
});
addToast(intl.formatMessage(messages.notificationsettingssaved), {
appearance: 'success',
autoDismiss: true,
});
} catch (e) {
addToast(
intl.formatMessage(messages.notificationsettingsfailed),
{
appearance: 'error',
autoDismiss: true,
}
);
} finally {
revalidate();
}
}}
>
{({ isSubmitting, values, setFieldValue }) => {
return (
<Form>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
<span className="mr-2">Enable Notifications</span>
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
type="checkbox"
id="enabled"
name="enabled"
onChange={() => {
setFieldValue('enabled', !values.enabled);
}}
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
/>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
<span className="mr-2">
Send Notifications for auto-approved media
</span>
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
type="checkbox"
id="autoapprovalEnabled"
name="autoapprovalEnabled"
onChange={() => {
setFieldValue(
'autoapprovalEnabled',
!values.autoapprovalEnabled
);
}}
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
/>
</div>
</div>
<div className="pt-5 mt-8 border-t border-gray-700">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting}
>
{isSubmitting
? intl.formatMessage(messages.saving)
: intl.formatMessage(messages.save)}
</Button>
</span>
</div>
</div>
</Form>
);
}}
</Formik>
</div>
<div className="mt-10 mb-6">
<h3 className="text-lg font-medium leading-6 text-gray-200">
{intl.formatMessage(messages.notificationAgentsSettings)}
</h3>
<p className="max-w-2xl mt-1 text-sm leading-5 text-gray-500">
{intl.formatMessage(messages.notificationAgentSettingsDescription)}
</p>
</div>
<div> <div>
<div className="sm:hidden"> <div className="sm:hidden">
<label htmlFor="tabs" className="sr-only"> <label htmlFor="tabs" className="sr-only">

@ -83,8 +83,8 @@
"components.PersonDetails.crewmember": "Crew Member", "components.PersonDetails.crewmember": "Crew Member",
"components.PersonDetails.nobiography": "No biography available.", "components.PersonDetails.nobiography": "No biography available.",
"components.PlexLoginButton.loading": "Loading…", "components.PlexLoginButton.loading": "Loading…",
"components.PlexLoginButton.loggingin": "Signing in…", "components.PlexLoginButton.signingin": "Signing in…",
"components.PlexLoginButton.loginwithplex": "Sign in", "components.PlexLoginButton.signinwithplex": "Sign in",
"components.RequestBlock.profilechanged": "Profile Changed", "components.RequestBlock.profilechanged": "Profile Changed",
"components.RequestBlock.requestoverrides": "Request Overrides", "components.RequestBlock.requestoverrides": "Request Overrides",
"components.RequestBlock.rootfolder": "Root Folder", "components.RequestBlock.rootfolder": "Root Folder",
@ -376,8 +376,12 @@
"components.Settings.nextexecution": "Next Execution", "components.Settings.nextexecution": "Next Execution",
"components.Settings.nodefault": "No default server selected!", "components.Settings.nodefault": "No default server selected!",
"components.Settings.nodefaultdescription": "At least one server must be marked as default before any requests will make it to your services.", "components.Settings.nodefaultdescription": "At least one server must be marked as default before any requests will make it to your services.",
"components.Settings.notificationAgentSettingsDescription": "Here you can pick and choose what types of notifications to send and through what types of services.",
"components.Settings.notificationAgentsSettings": "Notification Agents",
"components.Settings.notificationsettings": "Notification Settings", "components.Settings.notificationsettings": "Notification Settings",
"components.Settings.notificationsettingsDescription": "Here you can pick and choose what types of notifications to send and through what types of services.", "components.Settings.notificationsettingsDescription": "Global notification configuration. The settings below affect all notification agents.",
"components.Settings.notificationsettingsfailed": "Notification settings failed to save.",
"components.Settings.notificationsettingssaved": "Notification settings saved!",
"components.Settings.notrunning": "Not Running", "components.Settings.notrunning": "Not Running",
"components.Settings.plexlibraries": "Plex Libraries", "components.Settings.plexlibraries": "Plex Libraries",
"components.Settings.plexlibrariesDescription": "The libraries Overseerr scans for titles. Set up and save your Plex connection settings and click the button below if none are listed.", "components.Settings.plexlibrariesDescription": "The libraries Overseerr scans for titles. Set up and save your Plex connection settings and click the button below if none are listed.",

Loading…
Cancel
Save