mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
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>
This commit is contained in:
@@ -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)
|
||||
|
15
docs/using-overseerr/notifications/gotify.md
Normal file
15
docs/using-overseerr/notifications/gotify.md
Normal file
@@ -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(),
|
||||
|
148
server/lib/notifications/agents/gotify.ts
Normal file
148
server/lib/notifications/agents/gotify.ts
Normal file
@@ -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;
|
||||
|
1
src/assets/extlogos/gotify.svg
Normal file
1
src/assets/extlogos/gotify.svg
Normal file
@@ -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",
|
||||
|
20
src/pages/settings/notifications/gotify.tsx
Normal file
20
src/pages/settings/notifications/gotify.tsx
Normal file
@@ -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;
|
Reference in New Issue
Block a user