Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions seerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ components:
email:
type: string
example: 'user@example.com'
appriseTags:
type: string
appriseStatelessURL:
type: string
discordId:
type: string
nullable: true
Expand Down Expand Up @@ -1388,6 +1392,22 @@ components:
results:
type: number
example: 100
AppriseSettings:
type: object
properties:
enabled:
type: boolean
example: false
types:
type: number
example: 2
options:
type: object
properties:
url:
type: string
apiToken:
type: string
DiscordSettings:
type: object
properties:
Expand Down Expand Up @@ -1939,6 +1959,17 @@ components:
pgpKey:
type: string
nullable: true
appriseEnabled:
type: boolean
appriseEnabledTypes:
type: number
nullable: true
appriseTags:
type: string
nullable: true
appriseStatelessURL:
type: string
nullable: true
discordEnabled:
type: boolean
discordEnabledTypes:
Expand Down Expand Up @@ -1976,6 +2007,8 @@ components:
NotificationAgentTypes:
type: object
properties:
apprise:
type: number
discord:
type: number
email:
Expand Down Expand Up @@ -3307,6 +3340,52 @@ paths:
responses:
'204':
description: Test notification attempted
/settings/notifications/apprise:
get:
summary: Get Apprise notification settings
description: Returns current Apprise notification settings in a JSON object.
tags:
- settings
responses:
'200':
description: Returned Apprise settings
content:
application/json:
schema:
$ref: '#/components/schemas/AppriseSettings'
post:
summary: Update Apprise notification settings
description: Updates Apprise notification settings with the provided values.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AppriseSettings'
responses:
'200':
description: 'Values were successfully updated'
content:
application/json:
schema:
$ref: '#/components/schemas/AppriseSettings'
/settings/notifications/apprise/test:
post:
summary: Test Apprise settings
description: Sends a test notification to the Apprise agent.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AppriseSettings'
responses:
'204':
description: Test notification attempted
/settings/notifications/discord:
get:
summary: Get Discord notification settings
Expand Down
7 changes: 7 additions & 0 deletions server/entity/UserSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export class UserSettings {
@Column({ nullable: true })
public pgpKey?: string;

@Column({ nullable: true })
public appriseTags?: string;

@Column({ nullable: true })
public appriseStatelessURL?: string;

@Column({ nullable: true })
public discordId?: string;

Expand Down Expand Up @@ -79,6 +85,7 @@ export class UserSettings {
from: (value: string | null): Partial<NotificationAgentTypes> => {
const defaultTypes = {
email: ALL_NOTIFICATIONS,
apprise: 0,
discord: 0,
pushbullet: 0,
pushover: 0,
Expand Down
2 changes: 2 additions & 0 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Session } from '@server/entity/Session';
import { User } from '@server/entity/User';
import { startJobs } from '@server/job/schedule';
import notificationManager from '@server/lib/notifications';
import AppriseAgent from '@server/lib/notifications/agents/apprise';
import DiscordAgent from '@server/lib/notifications/agents/discord';
import EmailAgent from '@server/lib/notifications/agents/email';
import GotifyAgent from '@server/lib/notifications/agents/gotify';
Expand Down Expand Up @@ -126,6 +127,7 @@ app

// Register Notification Agents
notificationManager.registerAgents([
new AppriseAgent(),
new DiscordAgent(),
new EmailAgent(),
new GotifyAgent(),
Expand Down
8 changes: 8 additions & 0 deletions server/interfaces/api/userSettingsInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { NotificationAgentKey } from '@server/lib/settings';
export interface UserSettingsGeneralResponse {
username?: string;
email?: string;
appriseTags?: string;
appriseStatelessURL?: string;
discordId?: string;
locale?: string;
discoverRegion?: string;
Expand All @@ -24,6 +26,12 @@ export type NotificationAgentTypes = Record<NotificationAgentKey, number>;
export interface UserSettingsNotificationsResponse {
emailEnabled?: boolean;
pgpKey?: string;
appriseEnabled?: boolean;
appriseEnabledTypes?: number;
appriseURL?: string;
appriseAPIToken?: string;
appriseTags?: string;
appriseStatelessURL?: string;
discordEnabled?: boolean;
discordEnabledTypes?: number;
discordId?: string;
Expand Down
174 changes: 174 additions & 0 deletions server/lib/notifications/agents/apprise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { IssueStatus, IssueTypeName } from '@server/constants/issue';
import type { NotificationAgentApprise } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import {
getAvailableMediaServerName,
getAvailableMediaServerUrl,
} from '@server/utils/mediaServerHelper';
import axios from 'axios';
import { Notification, hasNotificationType } from '..';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';

interface AppriseOptions {
format: 'text' | 'markdown' | 'html' | 'ignore';
tags?: string;
apiToken?: string;
title?: string;
body?: string;
urls?: string;
type: 'info' | 'success' | 'warning' | 'failure';
}

class AppriseAgent
extends BaseAgent<NotificationAgentApprise>
implements NotificationAgent
{
protected getSettings(): NotificationAgentApprise {
if (this.settings) {
return this.settings;
}

const settings = getSettings();

return settings.notifications.agents.apprise;
}

public shouldSend(): boolean {
const settings = this.getSettings();

return !!(settings.enabled && settings.options.url);
}

private buildRequest(
type: Notification,
payload: NotificationPayload
): AppriseOptions {
const settings = getSettings();
const { applicationUrl, applicationTitle, mediaServerType } = settings.main;
const mediaServerName = getAvailableMediaServerName(mediaServerType);
const mediaServerUrl = getAvailableMediaServerUrl(payload);

let title = '';
let body = '';
if (payload.event) {
title += `**${payload.event}**`;
body += `${payload.subject}\n\n`;
} else {
title += `**${payload.subject}**`;
body += `${payload.message}\n\n`;
}

if (applicationUrl) {
title += ` [[${applicationTitle}]](${applicationUrl})`;
} else {
title += ` [${applicationTitle}]`;
}

if (payload.request) {
body += `Requested By\n${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) {
body += `\n\nRequest Status\n${status}`;
}
} else if (payload.comment) {
body += `\n\nComment From ${payload.comment.user.displayName}\n${payload.comment.message}`;
} else if (payload.issue) {
body += `\n\nReported By\n${payload.issue.createdBy.displayName}\n\nIssue Type\n${IssueTypeName[payload.issue.issueType]}\n\nIssue Status\n${payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved'}`;
}

const url = applicationUrl
? payload.issue
? `${applicationUrl}/issues/${payload.issue.id}`
: payload.media
? `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`
: undefined
: undefined;

if (url) {
body += `\n\nView ${
payload.issue ? 'Issue' : 'Media'
} in [${applicationTitle}](${url})`;
}

if (mediaServerUrl) {
body += `\n\nPlay on [${mediaServerName}](${mediaServerUrl})`;
}

if (payload.notifyUser?.settings?.appriseStatelessURL) {
return {
title: title,
body: body,
format: 'markdown',
urls: payload.notifyUser?.settings?.appriseStatelessURL,
type: 'info',
};
} else {
return {
title: title,
body: body,
format: 'markdown',
tags: payload.notifyUser?.settings?.appriseTags || 'all',
type: 'info',
};
}
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
const settings = this.getSettings();

if (
!payload.notifySystem ||
!hasNotificationType(type, settings.types ?? 0)
) {
return true;
}

logger.debug('Sending Apprise notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});

try {
await axios.post(settings.options.url, this.buildRequest(type, payload));

return true;
} catch (e) {
logger.error('Error sending Apprise notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e?.response?.data,
});

return false;
}
}
}

export default AppriseAgent;
19 changes: 19 additions & 0 deletions server/lib/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ export interface NotificationAgentSlack extends NotificationAgentConfig {
};
}

export interface NotificationAgentApprise extends NotificationAgentConfig {
options: {
url: string;
apiToken?: string;
tags?: string;
};
}

export interface NotificationAgentEmail extends NotificationAgentConfig {
options: {
userEmailRequired: boolean;
Expand Down Expand Up @@ -304,6 +312,7 @@ export interface NotificationAgentNtfy extends NotificationAgentConfig {
}

export enum NotificationAgentKey {
APPRISE = 'apprise',
DISCORD = 'discord',
EMAIL = 'email',
GOTIFY = 'gotify',
Expand All @@ -317,6 +326,7 @@ export enum NotificationAgentKey {
}

interface NotificationAgents {
apprise: NotificationAgentApprise;
discord: NotificationAgentDiscord;
email: NotificationAgentEmail;
gotify: NotificationAgentGotify;
Expand Down Expand Up @@ -536,6 +546,15 @@ class Settings {
priority: 3,
},
},
apprise: {
enabled: false,
embedPoster: false,
types: 0,
options: {
url: '',
apiToken: '',
},
},
},
},
jobs: {
Expand Down
Loading