From 4e6fb00a4a59545817add1544c0b1555078809a4 Mon Sep 17 00:00:00 2001 From: TheCatLady <52870424+TheCatLady@users.noreply.github.com> Date: Sun, 25 Apr 2021 12:22:54 -0400 Subject: [PATCH] feat(notif): add LunaSea agent (#1495) * feat(notif): add LunaSea agent * feat(notif): change LunaSea 'Authorization Header' input field to 'Profile Name' --- overseerr-api.yml | 90 +++++++-- server/index.ts | 2 + server/lib/notifications/agents/lunasea.ts | 104 ++++++++++ server/lib/settings.ts | 16 ++ server/routes/settings/notifications.ts | 35 ++++ src/assets/extlogos/discord.svg | 2 +- src/assets/extlogos/lunasea.svg | 1 + src/assets/extlogos/pushbullet.svg | 2 +- src/assets/extlogos/pushover.svg | 2 +- src/assets/extlogos/slack.svg | 2 +- src/assets/extlogos/telegram.svg | 2 +- .../NotificationsLunaSea/index.tsx | 177 ++++++++++++++++++ .../NotificationsWebhook/index.tsx | 6 +- .../Settings/SettingsNotifications.tsx | 12 ++ src/i18n/locale/en.json | 8 + src/pages/settings/notifications/lunasea.tsx | 17 ++ 16 files changed, 457 insertions(+), 21 deletions(-) create mode 100644 server/lib/notifications/agents/lunasea.ts create mode 100644 src/assets/extlogos/lunasea.svg create mode 100644 src/components/Settings/Notifications/NotificationsLunaSea/index.tsx create mode 100644 src/pages/settings/notifications/lunasea.tsx diff --git a/overseerr-api.yml b/overseerr-api.yml index e8258adfc..0a739720a 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -1151,6 +1151,8 @@ components: properties: webhookUrl: type: string + authHeader: + type: string jsonPayload: type: string TelegramSettings: @@ -1205,6 +1207,22 @@ components: type: string priority: type: number + LunaSeaSettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + webhookUrl: + type: string + profileName: + type: string NotificationEmailSettings: type: object properties: @@ -2406,22 +2424,22 @@ paths: responses: '204': description: Test notification attempted - /settings/notifications/telegram: + /settings/notifications/lunasea: get: - summary: Get Telegram notification settings - description: Returns current Telegram notification settings in a JSON object. + summary: Get LunaSea notification settings + description: Returns current LunaSea notification settings in a JSON object. tags: - settings responses: '200': - description: Returned Telegram settings + description: Returned LunaSea settings content: application/json: schema: - $ref: '#/components/schemas/TelegramSettings' + $ref: '#/components/schemas/LunaSeaSettings' post: - summary: Update Telegram notification settings - description: Update Telegram notification settings with the provided values. + summary: Update LunaSea notification settings + description: Updates LunaSea notification settings with the provided values. tags: - settings requestBody: @@ -2429,18 +2447,18 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/TelegramSettings' + $ref: '#/components/schemas/LunaSeaSettings' responses: '200': description: 'Values were sucessfully updated' content: application/json: schema: - $ref: '#/components/schemas/TelegramSettings' - /settings/notifications/telegram/test: + $ref: '#/components/schemas/LunaSeaSettings' + /settings/notifications/lunasea/test: post: - summary: Test Telegram settings - description: Sends a test notification to the Telegram agent. + summary: Test LunaSea settings + description: Sends a test notification to the LunaSea agent. tags: - settings requestBody: @@ -2448,7 +2466,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/TelegramSettings' + $ref: '#/components/schemas/LunaSeaSettings' responses: '204': description: Test notification attempted @@ -2590,6 +2608,52 @@ paths: responses: '204': description: Test notification attempted + /settings/notifications/telegram: + get: + summary: Get Telegram notification settings + description: Returns current Telegram notification settings in a JSON object. + tags: + - settings + responses: + '200': + description: Returned Telegram settings + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + post: + summary: Update Telegram notification settings + description: Update Telegram notification settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + /settings/notifications/telegram/test: + post: + summary: Test Telegram settings + description: Sends a test notification to the Telegram agent. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + responses: + '204': + description: Test notification attempted /settings/notifications/webpush: get: summary: Get Web Push notification settings diff --git a/server/index.ts b/server/index.ts index 749b13dcd..e76d0e3db 100644 --- a/server/index.ts +++ b/server/index.ts @@ -16,6 +16,7 @@ import { startJobs } from './job/schedule'; import notificationManager from './lib/notifications'; import DiscordAgent from './lib/notifications/agents/discord'; import EmailAgent from './lib/notifications/agents/email'; +import LunaSeaAgent from './lib/notifications/agents/lunasea'; import PushbulletAgent from './lib/notifications/agents/pushbullet'; import PushoverAgent from './lib/notifications/agents/pushover'; import SlackAgent from './lib/notifications/agents/slack'; @@ -53,6 +54,7 @@ app notificationManager.registerAgents([ new DiscordAgent(), new EmailAgent(), + new LunaSeaAgent(), new PushbulletAgent(), new PushoverAgent(), new SlackAgent(), diff --git a/server/lib/notifications/agents/lunasea.ts b/server/lib/notifications/agents/lunasea.ts new file mode 100644 index 000000000..9fc332f6d --- /dev/null +++ b/server/lib/notifications/agents/lunasea.ts @@ -0,0 +1,104 @@ +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 + 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(type: Notification): boolean { + if ( + this.getSettings().enabled && + this.getSettings().options.webhookUrl && + hasNotificationType(type, this.getSettings().types) + ) { + return true; + } + + return false; + } + + public async send( + type: Notification, + payload: NotificationPayload + ): Promise { + logger.debug('Sending LunaSea notification', { + label: 'Notifications', + type: Notification[type], + subject: payload.subject, + }); + + try { + const { webhookUrl, profileName } = this.getSettings().options; + + if (!webhookUrl) { + return false; + } + + await axios.post(webhookUrl, this.buildPayload(type, payload), { + headers: { + Authorization: `Basic ${Buffer.from(`${profileName}:`).toString( + 'base64' + )}`, + }, + }); + + 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; diff --git a/server/lib/settings.ts b/server/lib/settings.ts index c2ec9b366..c0c86b90f 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -140,6 +140,13 @@ export interface NotificationAgentEmail extends NotificationAgentConfig { }; } +export interface NotificationAgentLunaSea extends NotificationAgentConfig { + options: { + webhookUrl: string; + profileName: string; + }; +} + export interface NotificationAgentTelegram extends NotificationAgentConfig { options: { botUsername?: string; @@ -185,6 +192,7 @@ export enum NotificationAgentKey { interface NotificationAgents { discord: NotificationAgentDiscord; email: NotificationAgentEmail; + lunasea: NotificationAgentLunaSea; pushbullet: NotificationAgentPushbullet; pushover: NotificationAgentPushover; slack: NotificationAgentSlack; @@ -274,6 +282,14 @@ class Settings { webhookUrl: '', }, }, + lunasea: { + enabled: false, + types: 0, + options: { + webhookUrl: '', + profileName: '', + }, + }, slack: { enabled: false, types: 0, diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index a9a67084b..8c60c9608 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -2,6 +2,7 @@ import { Router } from 'express'; import { Notification } from '../../lib/notifications'; import DiscordAgent from '../../lib/notifications/agents/discord'; import EmailAgent from '../../lib/notifications/agents/email'; +import LunaSeaAgent from '../../lib/notifications/agents/lunasea'; import PushbulletAgent from '../../lib/notifications/agents/pushbullet'; import PushoverAgent from '../../lib/notifications/agents/pushover'; import SlackAgent from '../../lib/notifications/agents/slack'; @@ -332,4 +333,38 @@ notificationRoutes.post('/webhook/test', (req, res, next) => { } }); +notificationRoutes.get('/lunasea', (_req, res) => { + const settings = getSettings(); + + res.status(200).json(settings.notifications.agents.lunasea); +}); + +notificationRoutes.post('/lunasea', (req, res) => { + const settings = getSettings(); + + settings.notifications.agents.lunasea = req.body; + settings.save(); + + res.status(200).json(settings.notifications.agents.lunasea); +}); + +notificationRoutes.post('/lunasea/test', (req, res, next) => { + if (!req.user) { + return next({ + status: 500, + message: 'User information missing from request', + }); + } + + const lunaseaAgent = new LunaSeaAgent(req.body); + lunaseaAgent.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(); +}); + export default notificationRoutes; diff --git a/src/assets/extlogos/discord.svg b/src/assets/extlogos/discord.svg index bce41d990..736d9ddd2 100644 --- a/src/assets/extlogos/discord.svg +++ b/src/assets/extlogos/discord.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/extlogos/lunasea.svg b/src/assets/extlogos/lunasea.svg new file mode 100644 index 000000000..359ca8161 --- /dev/null +++ b/src/assets/extlogos/lunasea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/extlogos/pushbullet.svg b/src/assets/extlogos/pushbullet.svg index e6101705c..bd97ab860 100644 --- a/src/assets/extlogos/pushbullet.svg +++ b/src/assets/extlogos/pushbullet.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/extlogos/pushover.svg b/src/assets/extlogos/pushover.svg index 7225c8059..7b2413f30 100644 --- a/src/assets/extlogos/pushover.svg +++ b/src/assets/extlogos/pushover.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/extlogos/slack.svg b/src/assets/extlogos/slack.svg index f292c13cd..5c0db3a2a 100644 --- a/src/assets/extlogos/slack.svg +++ b/src/assets/extlogos/slack.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/extlogos/telegram.svg b/src/assets/extlogos/telegram.svg index f7cc49334..ba9984de4 100644 --- a/src/assets/extlogos/telegram.svg +++ b/src/assets/extlogos/telegram.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/Settings/Notifications/NotificationsLunaSea/index.tsx b/src/components/Settings/Notifications/NotificationsLunaSea/index.tsx new file mode 100644 index 000000000..daab8cad9 --- /dev/null +++ b/src/components/Settings/Notifications/NotificationsLunaSea/index.tsx @@ -0,0 +1,177 @@ +import axios from 'axios'; +import { Field, Form, Formik } from 'formik'; +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; +import useSWR from 'swr'; +import * as Yup from 'yup'; +import globalMessages from '../../../../i18n/globalMessages'; +import Button from '../../../Common/Button'; +import LoadingSpinner from '../../../Common/LoadingSpinner'; +import NotificationTypeSelector from '../../../NotificationTypeSelector'; + +const messages = defineMessages({ + agentenabled: 'Enable Agent', + webhookUrl: 'Webhook URL', + validationWebhookUrl: 'You must provide a valid URL', + profileName: 'Profile Name', + profileNameTip: 'Only required if not using the default profile', + settingsSaved: 'LunaSea notification settings saved successfully!', + settingsFailed: 'LunaSea notification settings failed to save.', + testSent: 'LunaSea test notification sent!', +}); + +const NotificationsLunaSea: React.FC = () => { + const intl = useIntl(); + const { addToast } = useToasts(); + const { data, error, revalidate } = useSWR( + '/api/v1/settings/notifications/lunasea' + ); + + const NotificationsLunaSeaSchema = Yup.object().shape({ + webhookUrl: Yup.string() + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationWebhookUrl)), + otherwise: Yup.string().nullable(), + }) + .url(intl.formatMessage(messages.validationWebhookUrl)), + }); + + if (!data && !error) { + return ; + } + + return ( + { + try { + await axios.post('/api/v1/settings/notifications/lunasea', { + enabled: values.enabled, + types: values.types, + options: { + webhookUrl: values.webhookUrl, + profileName: values.profileName, + }, + }); + addToast(intl.formatMessage(messages.settingsSaved), { + appearance: 'success', + autoDismiss: true, + }); + } catch (e) { + addToast(intl.formatMessage(messages.settingsFailed), { + appearance: 'error', + autoDismiss: true, + }); + } finally { + revalidate(); + } + }} + > + {({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => { + const testSettings = async () => { + await axios.post('/api/v1/settings/notifications/lunasea/test', { + enabled: true, + types: values.types, + options: { + webhookUrl: values.webhookUrl, + profileName: values.profileName, + }, + }); + + addToast(intl.formatMessage(messages.testSent), { + appearance: 'info', + autoDismiss: true, + }); + }; + + return ( +
+
+ +
+ +
+
+
+ +
+
+ +
+ {errors.webhookUrl && touched.webhookUrl && ( +
{errors.webhookUrl}
+ )} +
+
+
+ +
+
+ +
+
+
+ setFieldValue('types', newTypes)} + /> +
+
+ + + + + + +
+
+ + ); + }} +
+ ); +}; + +export default NotificationsLunaSea; diff --git a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx index 8b6dffad7..71191fa62 100644 --- a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx +++ b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx @@ -184,7 +184,7 @@ const NotificationsWebhook: React.FC = () => {
-
-
-