feat(notif): add Gotify agent (#2196)

* feat(notifications): adds gotify notifications

adds new settings screen for gotify notifications including url, token and types settings

fix #2183

* feat(notif): add Gotify agent
addresses PR comments, runs i18n:extract

fix #2183

* reword validationTokenRequired

change wording to indicate presence, not validity

Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>

* feat(notifications): gotify notifications fix

applies changes from #2077 in which Yup validation was failing for types

fix #2183

* feat(notifications): adds gotify notifications

adds new settings screen for gotify notifications including url, token and types settings

fix #2183

* feat(notif): add Gotify agent
addresses PR comments, runs i18n:extract

fix #2183

* reword validationTokenRequired

change wording to indicate presence, not validity

Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>

* feat(notifications): gotify notifications fix

applies changes from #2077 in which Yup validation was failing for types

fix #2183

* feat(notifications): incorporate issue feature into gotify notifications

* feat(notifications): adds gotify notifications

adds new settings screen for gotify notifications including url, token and types settings

fix #2183

* feat(notif): add Gotify agent
addresses PR comments, runs i18n:extract

fix #2183

* reword validationTokenRequired

change wording to indicate presence, not validity

Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>

* feat: add missing ts field

include notifyAdmin in test notification endpoint

* feat: apply formatting/line break items

add addition line break before conditional, change ordering of notifyAdmin/notifyUser in test
endpoint

* feat: remove duplicated endpoints

during rebase, notification endpoints were duplicated upon rebasing. remove duplicate routes

* feat: correct linting quirks

* feat: formatting improvements

* feat(gotify): refactor axios post to leverage 'getNotificationPayload'

Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
pull/2082/head^2
Sean Chambers 2 years ago committed by GitHub
parent 879df20022
commit e0b6abe479
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,6 +7,7 @@ Overseerr currently supports the following notification agents:
- [Email](./email.md)
- [Web Push](./webpush.md)
- [Discord](./discord.md)
- [Gotify](./gotify.md)
- [LunaSea](./lunasea.md)
- [Pushbullet](./pushbullet.md)
- [Pushover](./pushover.md)

@ -0,0 +1,15 @@
# Gotify
## Configuration
### Server URL
Set this to the URL of your Gotify server.
### Application Token
Add an application to your Gotify server, and set this field to the generated application token.
{% hint style="info" %}
Please refer to the [Gotify API documentation](https://gotify.net/docs) for more details on configuring these notifications.
{% endhint %}

@ -1231,6 +1231,22 @@ components:
type: string
userToken:
type: string
GotifySettings:
type: object
properties:
enabled:
type: boolean
example: false
types:
type: number
example: 2
options:
type: object
properties:
url:
type: string
token:
type: string
LunaSeaSettings:
type: object
properties:
@ -2681,6 +2697,52 @@ paths:
responses:
'204':
description: Test notification attempted
/settings/notifications/gotify:
get:
summary: Get Gotify notification settings
description: Returns current Gotify notification settings in a JSON object.
tags:
- settings
responses:
'200':
description: Returned Gotify settings
content:
application/json:
schema:
$ref: '#/components/schemas/GotifySettings'
post:
summary: Update Gotify notification settings
description: Update Gotify notification settings with the provided values.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/GotifySettings'
responses:
'200':
description: 'Values were sucessfully updated'
content:
application/json:
schema:
$ref: '#/components/schemas/GotifySettings'
/settings/notifications/gotify/test:
post:
summary: Test Gotify settings
description: Sends a test notification to the Gotify agent.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/GotifySettings'
responses:
'204':
description: Test notification attempted
/settings/notifications/slack:
get:
summary: Get Slack notification settings

@ -17,6 +17,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 GotifyAgent from './lib/notifications/agents/gotify';
import LunaSeaAgent from './lib/notifications/agents/lunasea';
import PushbulletAgent from './lib/notifications/agents/pushbullet';
import PushoverAgent from './lib/notifications/agents/pushover';
@ -76,6 +77,7 @@ app
notificationManager.registerAgents([
new DiscordAgent(),
new EmailAgent(),
new GotifyAgent(),
new LunaSeaAgent(),
new PushbulletAgent(),
new PushoverAgent(),

@ -0,0 +1,148 @@
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
import logger from '../../../logger';
import { getSettings, NotificationAgentGotify } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface GotifyPayload {
title: string;
message: string;
priority: number;
extras: any;
}
class GotifyAgent
extends BaseAgent<NotificationAgentGotify>
implements NotificationAgent
{
protected getSettings(): NotificationAgentGotify {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.gotify;
}
public shouldSend(): boolean {
const settings = this.getSettings();
if (settings.enabled && settings.options.url && settings.options.token) {
return true;
}
return false;
}
private getNotificationPayload(
type: Notification,
payload: NotificationPayload
): GotifyPayload {
const { applicationUrl, applicationTitle } = getSettings().main;
let priority = 0;
const title = payload.event
? `${payload.event} - ${payload.subject}`
: payload.subject;
let message = payload.message ?? '';
if (payload.request) {
message += `\n\nRequested By: ${payload.request.requestedBy.displayName}`;
let status = '';
switch (type) {
case Notification.MEDIA_PENDING:
status = 'Pending Approval';
break;
case Notification.MEDIA_APPROVED:
case Notification.MEDIA_AUTO_APPROVED:
status = 'Processing';
break;
case Notification.MEDIA_AVAILABLE:
status = 'Available';
break;
case Notification.MEDIA_DECLINED:
status = 'Declined';
break;
case Notification.MEDIA_FAILED:
status = 'Failed';
break;
}
if (status) {
message += `\nRequest Status: ${status}`;
}
} else if (payload.comment) {
message += `\nComment from ${payload.comment.user.displayName}:\n${payload.comment.message}`;
} else if (payload.issue) {
message += `\n\nReported By: ${payload.issue.createdBy.displayName}`;
message += `\nIssue Type: ${IssueTypeName[payload.issue.issueType]}`;
message += `\nIssue Status: ${
payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved'
}`;
if (type == Notification.ISSUE_CREATED) {
priority = 1;
}
}
for (const extra of payload.extra ?? []) {
message += `\n\n**${extra.name}**\n${extra.value}`;
}
if (applicationUrl && payload.media) {
const actionUrl = `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`;
message += `\n\nOpen in ${applicationTitle}(${actionUrl})`;
}
return {
extras: {
'client::display': {
contentType: 'text/markdown',
},
},
title,
message,
priority,
};
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
const settings = this.getSettings();
if (!hasNotificationType(type, settings.types ?? 0)) {
return true;
}
logger.debug('Sending Gotify notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
try {
const endpoint = `${settings.options.url}/message?token=${settings.options.token}`;
const notificationPayload = this.getNotificationPayload(type, payload);
await axios.post(endpoint, notificationPayload);
return true;
} catch (e) {
logger.error('Error sending Gotify notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response?.data,
});
return false;
}
}
}
export default GotifyAgent;

@ -189,9 +189,17 @@ export interface NotificationAgentWebhook extends NotificationAgentConfig {
};
}
export interface NotificationAgentGotify extends NotificationAgentConfig {
options: {
url: string;
token: string;
};
}
export enum NotificationAgentKey {
DISCORD = 'discord',
EMAIL = 'email',
GOTIFY = 'gotify',
PUSHBULLET = 'pushbullet',
PUSHOVER = 'pushover',
SLACK = 'slack',
@ -203,6 +211,7 @@ export enum NotificationAgentKey {
interface NotificationAgents {
discord: NotificationAgentDiscord;
email: NotificationAgentEmail;
gotify: NotificationAgentGotify;
lunasea: NotificationAgentLunaSea;
pushbullet: NotificationAgentPushbullet;
pushover: NotificationAgentPushover;
@ -359,6 +368,14 @@ class Settings {
enabled: false,
options: {},
},
gotify: {
enabled: false,
types: 0,
options: {
url: '',
token: '',
},
},
},
},
jobs: {

@ -4,6 +4,7 @@ import { Notification } from '../../lib/notifications';
import { NotificationAgent } from '../../lib/notifications/agents/agent';
import DiscordAgent from '../../lib/notifications/agents/discord';
import EmailAgent from '../../lib/notifications/agents/email';
import GotifyAgent from '../../lib/notifications/agents/gotify';
import LunaSeaAgent from '../../lib/notifications/agents/lunasea';
import PushbulletAgent from '../../lib/notifications/agents/pushbullet';
import PushoverAgent from '../../lib/notifications/agents/pushover';
@ -377,4 +378,46 @@ notificationRoutes.post('/lunasea/test', async (req, res, next) => {
}
});
notificationRoutes.get('/gotify', (_req, res) => {
const settings = getSettings();
res.status(200).json(settings.notifications.agents.gotify);
});
notificationRoutes.post('/gotify', (req, rest) => {
const settings = getSettings();
settings.notifications.agents.gotify = req.body;
settings.save();
rest.status(200).json(settings.notifications.agents.gotify);
});
notificationRoutes.post('/gotify/test', async (req, rest, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information is missing from request',
});
}
const gotifyAgent = new GotifyAgent(req.body);
if (
await gotifyAgent.send(Notification.TEST_NOTIFICATION, {
notifyAdmin: false,
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return rest.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send Gotify notification.',
});
}
});
export default notificationRoutes;

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 331.60596 331.60595"><g transform="translate(-92.2 -101.57)" fill="currentColor" stroke="currentColor" stroke-width="2"><path d="m317.7 376.2c6.2-1.7 15.8 0 19.5 3.2 5.5 4.8 4.9 20.9 1.1 29-0.8 1.7-1.8 3.4-3.2 4.4s-3.4 1.2-4.7 0-1.7-3.3-1.9-5.2c-0.2-3.1-0.2-6.2 0.3-9.2 0.2-1.3 0.2-2.9-0.2-4.2-0.6-2.2-2.5-3.1-4.5-3.5-3.4-0.8-14.3-0.7-19.5-0.7-0.8 0 1.5-9.1 2-9.7 2.6-2.9 7.5-3.1 11.1-4.1z"/><path d="m258.9 119.7l-9-2.7c-4.6-1.4-9.2-2.8-14-2.5-2.8 0.2-6.1 1.3-6.9 4-0.6 2-1.6 7.3-1.3 7.9 1.5 3.4 13.9 6.7 18.3 6.7"/><path d="m392.6 177.9c-1.4 1.4-2.2 3.5-2.5 5.5-0.2 1.4-0.1 3 0.5 4.3s1.8 2.3 3.1 3c1.3 0.6 2.8 0.9 4.3 0.9 1.1 0 2.3-0.1 3.1-0.9 0.6-0.7 0.8-1.6 0.9-2.5 0.2-2.3-0.1-4.7-0.9-6.9-0.4-1.1-0.9-2.3-1.8-3.1-1.7-1.8-4.5-2.2-6.4-0.5-0.1 0-0.2 0.1-0.3 0.2z"/><path d="m358.5 164.2c-1-1 0-2.7 1-3.7 5.8-5.2 15.1-4.6 21.8-0.6 10.9 6.6 15.6 19.9 17.2 32.5 0.6 5.2 0.9 10.6-0.5 15.7s-4.6 9.9-9.3 12.1c-1.1 0.5-2.3 0.9-3.4 0.5s-1.9-1.8-1.2-2.8c-9.4-13.6-19-26.8-20.9-43.2-0.5-4.1-1.8-7.4-4.7-10.5z"/><path d="m134.7 328.4c-5.1-3.1-9.9-6.6-14.3-10.6-1.3-1.2-2.6-2.5-2.6-4.3 0-1.2 0.6-2.2 1.2-3.2 0.8-1.4 1.7-2.8 2.5-4.1 1.1-1.8 2.9-3.9 4.9-3.2 0.9 0.3 1.5 1.1 2 1.8 2.4 3.3 4.9 6.6 7.3 9.8 1.5 2 3.7 4.3 6.1 3.5"/><path d="m209.6 133c33.2-18 77.8-19.6 111.5-8.7 24.3 7.9 43.4 26.7 53.3 50 8.7 20.6 10.5 43.6 8.1 65.7-4.4 40.2-20.2 77.9-40.3 112.6-11.1 19-21.8 36-40.5 48.5-36.8 24.6-87.2 22.1-128.4 11.5-19.9-5.1-39.7-17.3-47.2-37.3-4.8-12.8-4.2-27.6 1.5-40 11.6-24.8 43.2-38.4 45.6-67.9 0.7-8.7-1.6-17.3-3.6-25.7-5.6-23.4-8.9-45.8 1.4-68.7 8.1-17.7 21.9-31 38.6-40z"/><path d="m189.8 151.4c-5.4-5.2-11.9-8.8-19-10.3-2.2-0.5-4.7-0.7-6.9 0.7-1.8 1.2-3.1 3.3-4.2 5.3-1.6 3-3 6.2-4.1 9.4-0.4 1.2-0.6 2.5 0 3.5 0.3 0.6 0.9 0.9 1.5 1.2 8.1 4.2 16.8 7.1 25.5 9.8"/><path d="m183.7 158.7c-2.5-1.8-16.8-12.1-18.7-4.8-0.4 1.6 0.5 3.9 1.5 4.8"/><path d="m264.5 174.9c-0.5 0.5-0.9 1-1.3 1.6-9 11.6-12 27.9-9.3 42.1 1.7 9 5.9 17.9 13.2 23.4 19.3 14.6 51.5 13.5 68.4-1.5 24.4-21.7 13-67.6-14-78.8-17.6-7.2-43.7-1.6-57 13.2z" fill-opacity=".97633"/><path d="m382.1 237.1c1.4-0.1 2.9-0.1 4.3 0.1 0.3 0 0.7 0.1 1 0.4 0.2 0.3 0.4 0.7 0.5 1.1 1 3.9 0.5 8.2 0.1 12.4-0.1 0.9-0.2 1.8-0.6 2.6-1 2.1-3.1 2.7-4.7 2.7-0.1 0-0.2 0-0.3-0.1-0.3-0.2-0.3-0.7-0.2-1.2 0.3-5.9-0.1-11.9-0.1-18v0z"/><path d="m378.7 236.8c-1.4 0.4-2.5 2-2.8 4.4-0.5 4.4-0.7 8.9-0.5 13.4 0 0.9 0.1 1.9 0.5 2.4 0.2 0.3 0.5 0.4 0.8 0.4 1.6 0.3 4.1-0.6 5.6-1 0 0 0-5.2-0.1-8s-0.1-6.1-0.2-8.9v-2.2c0.1-0.7-2.6-0.7-3.3-0.5z"/><path d="m358.3 231.8c-0.3 2.2 0.1 4.7 1.7 7.4 2.6 4.4 7 6.1 11.9 5.8 8.9-0.6 25.3-5.4 27.5-15.7 0.6-3-0.3-6.1-2.2-8.5-6.2-7.8-17.8-5.7-25.6-2-5.9 2.7-12.4 7-13.3 13z"/><path d="m386.4 208.6c2.2 1.4 3.7 3.8 4 7 0.3 3.6-1.4 7.5-5 8.8-2.9 1.1-6.2 0.6-9.1-0.4s-5.8-2.8-6.8-5.7c-0.7-2-0.3-4.3 0.7-6.1 1.1-1.8 2.8-3.2 4.7-4.1 3.9-1.8 8.4-1.6 11.5 0.5z"/><path d="m414.7 262.6c2.4 0.6 4.8 2.1 5.6 4.4s0.1 4.9-1.6 6.7-4.2 2.5-6.6 2.5c-0.8 0-1.7-0.1-2.4-0.5-2.5-1.1-3.5-4-4.2-6.6-1.8-6.8 3.6-7.8 9.2-6.5z"/><path d="m267.1 284.7c2.3-4.5 141.3-36.2 144.7-31.6 3.4 4.5 15.8 88.2 9 90.4-6.8 2.3-119.8 37.3-126.6 35s-29.4-89.3-27.1-93.8z"/><path d="m294.2 378.5s54.3-74.6 59.9-76.9c5.7-2.3 67.3 41.3 67.3 41.3"/><path d="m267 287.7s86 38.8 91.6 36.6c5.7-2.3 53.1-71.2 53.1-71.2"/><path d="m132.8 375.6c-3.5 3.8-7.3 7.8-13 9.2-4.6 1.2-10 0.2-13.6-2.3-1.4-1-2.6-2.2-4-3.2-1.5-1-3.4-1.7-5.3-1.3-2.7 0.5-4.1 3.1-3.6 5.3 2 8.8 17 15.6 27.5 15.5 9 0 19-4.6 21.4-11.8"/><path d="m132.8 375.6c-3.5 3.8-7.3 7.8-13 9.2-4.6 1.2-10 0.2-13.6-2.3-1.4-1-2.6-2.2-4-3.2-1.5-1-3.4-1.7-5.3-1.3-2.7 0.5-4.1 3.1-3.6 5.3 2 8.8 17 15.6 27.5 15.5 9 0 19-4.6 21.4-11.8"/><path d="m261.9 283.5c-0.1 4.2 4.3 7.3 8.4 7.6s8.2-1.3 12.2-2.6c1.4-0.4 2.9-0.8 4.2-0.2 1.8 0.9 2.7 4.1 1.8 5.9s-3.4 3.5-5.3 4.4c-6.5 3-12.9 3.6-19.9 2-5.3-1.2-11.3-4.3-13-13.5"/><path d="m261.9 283.5c-0.1 4.2 4.3 7.3 8.4 7.6s8.2-1.3 12.2-2.6c1.4-0.4 2.9-0.8 4.2-0.2 1.8 0.9 2.7 4.1 1.8 5.9s-3.4 3.5-5.3 4.4c-6.5 3-12.9 3.6-19.9 2-5.3-1.2-11.3-4.3-13-13.5"/><path d="m318.4 198.4c-2-0.3-4.1 0.1-5.9 1.3-3.2 2.1-4.7 6.2-4.7 9.9 0 1.9 0.4 3.8 1.4 5.3 1.2 1.7 3.1 2.9 5.2 3.4 3.4 0.8 8.2 0.7 10.5-2.5 1-1.5 1.4-3.3 1.5-5.1 0.5-5.7-1.8-11.4-8-12.3z"/><path d="m320.4 203.3c0.9 0.3 1.7 0.8 2.1 1.7 0.4 0.8 0.4 1.7 0.3 2.5-0.1 1-0.6 2-1.5 2.7-0.7 0.5-1.7 0.7-2.6 0.5s-1.7-0.8-2.2-1.6c-1.1-1.6-0.9-4.4 0.9-5.5 0.9-0.4 2-0.6 3-0.3z"/></g></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

@ -0,0 +1,256 @@
import { BeakerIcon, SaveIcon } from '@heroicons/react/solid';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import React, { useState } 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',
url: 'Server URL',
token: 'Application Token',
validationUrlRequired: 'You must provide a valid URL',
validationUrlTrailingSlash: 'URL must not end in a trailing slash',
validationTokenRequired: 'You must provide an application token',
gotifysettingssaved: 'Gotify notification settings saved successfully!',
gotifysettingsfailed: 'Gotify notification settings failed to save.',
toastGotifyTestSending: 'Sending Gotify test notification…',
toastGotifyTestSuccess: 'Gotify test notification sent!',
toastGotifyTestFailed: 'Gotify test notification failed to send.',
validationTypes: 'You must select at least one notification type',
});
const NotificationsGotify: React.FC = () => {
const intl = useIntl();
const { addToast, removeToast } = useToasts();
const [isTesting, setIsTesting] = useState(false);
const { data, error, revalidate } = useSWR(
'/api/v1/settings/notifications/gotify'
);
const NotificationsGotifySchema = Yup.object().shape({
url: Yup.string()
.when('enabled', {
is: true,
then: Yup.string()
.nullable()
.required(intl.formatMessage(messages.validationUrlRequired)),
otherwise: Yup.string().nullable(),
})
.matches(
// eslint-disable-next-line no-useless-escape
/^(https?:)?\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i,
intl.formatMessage(messages.validationUrlRequired)
)
.test(
'no-trailing-slash',
intl.formatMessage(messages.validationUrlTrailingSlash),
(value) => !value || !value.endsWith('/')
),
token: Yup.string().when('enabled', {
is: true,
then: Yup.string()
.nullable()
.required(intl.formatMessage(messages.validationTokenRequired)),
otherwise: Yup.string().nullable(),
}),
});
if (!data && !error) {
return <LoadingSpinner />;
}
return (
<Formik
initialValues={{
enabled: data?.enabled,
types: data?.types,
url: data?.options.url,
token: data?.options.token,
}}
validationSchema={NotificationsGotifySchema}
onSubmit={async (values) => {
try {
await axios.post('/api/v1/settings/notifications/gotify', {
enabled: values.enabled,
types: values.types,
options: {
url: values.url,
token: values.token,
},
});
addToast(intl.formatMessage(messages.gotifysettingssaved), {
appearance: 'success',
autoDismiss: true,
});
} catch (e) {
addToast(intl.formatMessage(messages.gotifysettingsfailed), {
appearance: 'error',
autoDismiss: true,
});
} finally {
revalidate();
}
}}
>
{({
errors,
touched,
isSubmitting,
values,
isValid,
setFieldValue,
setFieldTouched,
}) => {
const testSettings = async () => {
setIsTesting(true);
let toastId: string | undefined;
try {
addToast(
intl.formatMessage(messages.toastGotifyTestSending),
{
autoDsmiss: false,
appearance: 'info',
},
(id) => {
toastId = id;
}
);
await axios.post('/api/v1/settings/notifications/gotify/test', {
enabled: true,
types: values.types,
options: {
url: values.url,
token: values.token,
},
});
if (toastId) {
removeToast(toastId);
}
addToast(intl.formatMessage(messages.toastGotifyTestSuccess), {
autoDismiss: true,
appearance: 'success',
});
} catch (e) {
if (toastId) {
removeToast(toastId);
}
addToast(intl.formatMessage(messages.toastGotifyTestFailed), {
autoDismiss: true,
appearance: 'error',
});
} finally {
setIsTesting(false);
}
};
return (
<Form className="section">
<div className="form-row">
<label htmlFor="enabled" className="checkbox-label">
{intl.formatMessage(messages.agentenabled)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />
</div>
</div>
<div className="form-row">
<label htmlFor="url" className="text-label">
{intl.formatMessage(messages.url)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field id="url" name="url" type="text" />
</div>
{errors.url && touched.url && (
<div className="error">{errors.url}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="token" className="text-label">
{intl.formatMessage(messages.token)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field id="token" name="token" type="text" />
</div>
{errors.token && touched.token && (
<div className="error">{errors.token}</div>
)}
</div>
</div>
<NotificationTypeSelector
currentTypes={values.enabled ? values.types : 0}
onUpdate={(newTypes) => {
setFieldValue('types', newTypes);
setFieldTouched('types');
if (newTypes) {
setFieldValue('enabled', true);
}
}}
error={
values.enabled && !values.types && touched.types
? intl.formatMessage(messages.validationTypes)
: undefined
}
/>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="warning"
disabled={isSubmitting || !isValid || isTesting}
onClick={(e) => {
e.preventDefault();
testSettings();
}}
>
<BeakerIcon />
<span>
{isTesting
? intl.formatMessage(globalMessages.testing)
: intl.formatMessage(globalMessages.test)}
</span>
</Button>
</span>
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={
isSubmitting ||
!isValid ||
isTesting ||
(values.enabled && !values.types)
}
>
<SaveIcon />
<span>
{isSubmitting
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)}
</span>
</Button>
</span>
</div>
</div>
</Form>
);
}}
</Formik>
);
};
export default NotificationsGotify;

@ -2,6 +2,7 @@ import { CloudIcon, LightningBoltIcon, MailIcon } from '@heroicons/react/solid';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import DiscordLogo from '../../assets/extlogos/discord.svg';
import GotifyLogo from '../../assets/extlogos/gotify.svg';
import LunaSeaLogo from '../../assets/extlogos/lunasea.svg';
import PushbulletLogo from '../../assets/extlogos/pushbullet.svg';
import PushoverLogo from '../../assets/extlogos/pushover.svg';
@ -58,6 +59,17 @@ const SettingsNotifications: React.FC = ({ children }) => {
route: '/settings/notifications/discord',
regex: /^\/settings\/notifications\/discord/,
},
{
text: 'Gotify',
content: (
<span className="flex items-center">
<GotifyLogo className="h-4 mr-2" />
Gotify
</span>
),
route: '/settings/notifications/gotify',
regex: /^\/settings\/notifications\/gotify/,
},
{
text: 'LunaSea',
content: (

@ -365,6 +365,18 @@
"components.ResetPassword.validationpasswordrequired": "You must provide a password",
"components.Search.search": "Search",
"components.Search.searchresults": "Search Results",
"components.Settings.Notifications.NotificationsGotify.agentenabled": "Enable Agent",
"components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "Gotify notification settings failed to save.",
"components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "Gotify notification settings saved successfully!",
"components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "Gotify test notification failed to send.",
"components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "Sending Gotify test notification…",
"components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "Gotify test notification sent!",
"components.Settings.Notifications.NotificationsGotify.token": "Application Token",
"components.Settings.Notifications.NotificationsGotify.url": "Server URL",
"components.Settings.Notifications.NotificationsGotify.validationTokenRequired": "You must provide a valid application token",
"components.Settings.Notifications.NotificationsGotify.validationTypes": "You must select at least one notification type",
"components.Settings.Notifications.NotificationsGotify.validationUrlRequired": "You must provide a valid URL",
"components.Settings.Notifications.NotificationsGotify.validationUrlTrailingSlash": "URL must not end in a trailing slash",
"components.Settings.Notifications.NotificationsLunaSea.agentenabled": "Enable Agent",
"components.Settings.Notifications.NotificationsLunaSea.profileName": "Profile Name",
"components.Settings.Notifications.NotificationsLunaSea.profileNameTip": "Only required if not using the <code>default</code> profile",

@ -0,0 +1,20 @@
import { NextPage } from 'next';
import React from 'react';
import NotificationsGotify from '../../../components/Settings/Notifications/NotificationsGotify';
import SettingsLayout from '../../../components/Settings/SettingsLayout';
import SettingsNotifications from '../../../components/Settings/SettingsNotifications';
import useRouteGuard from '../../../hooks/useRouteGuard';
import { Permission } from '../../../hooks/useUser';
const NotificationsPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS);
return (
<SettingsLayout>
<SettingsNotifications>
<NotificationsGotify />
</SettingsNotifications>
</SettingsLayout>
);
};
export default NotificationsPage;
Loading…
Cancel
Save