feat(notif): add Pushover sound options (#2403)

Co-authored-by: Danshil Kokil Mungur <me@danshilm.com>
pull/3743/head
TheCatLady 1 year ago committed by Mike Kao
parent 65045c2508
commit e421ac4f0c

@ -1273,6 +1273,8 @@ components:
type: string
userToken:
type: string
sound:
type: string
GotifySettings:
type: object
properties:
@ -1708,6 +1710,9 @@ components:
pushoverUserKey:
type: string
nullable: true
pushoverSound:
type: string
nullable: true
telegramEnabled:
type: boolean
telegramBotUsername:
@ -2861,6 +2866,33 @@ paths:
responses:
'204':
description: Test notification attempted
/settings/notifications/pushover/sounds:
get:
summary: Get Pushover sounds
description: Returns valid Pushover sound options in a JSON array.
tags:
- settings
parameters:
- in: query
name: token
required: true
schema:
type: string
nullable: false
responses:
'200':
description: Returned Pushover settings
content:
application/json:
schema:
type: array
items:
type: object
properties:
name:
type: string
description:
type: string
/settings/notifications/gotify:
get:
summary: Get Gotify notification settings

@ -0,0 +1,56 @@
import ExternalAPI from './externalapi';
interface PushoverSoundsResponse {
sounds: {
[name: string]: string;
};
status: number;
request: string;
}
export interface PushoverSound {
name: string;
description: string;
}
export const mapSounds = (sounds: {
[name: string]: string;
}): PushoverSound[] =>
Object.entries(sounds).map(
([name, description]) =>
({
name,
description,
} as PushoverSound)
);
class PushoverAPI extends ExternalAPI {
constructor() {
super(
'https://api.pushover.net/1',
{},
{
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
}
);
}
public async getSounds(appToken: string): Promise<PushoverSound[]> {
try {
const data = await this.get<PushoverSoundsResponse>('/sounds.json', {
params: {
token: appToken,
},
});
return mapSounds(data.sounds);
} catch (e) {
throw new Error(`[Pushover] Failed to retrieve sounds: ${e.message}`);
}
}
}
export default PushoverAPI;

@ -51,6 +51,9 @@ export class UserSettings {
@Column({ nullable: true })
public pushoverUserKey?: string;
@Column({ nullable: true })
public pushoverSound?: string;
@Column({ nullable: true })
public telegramChatId?: string;

@ -28,6 +28,7 @@ export interface UserSettingsNotificationsResponse {
pushbulletAccessToken?: string;
pushoverApplicationToken?: string;
pushoverUserKey?: string;
pushoverSound?: string;
telegramEnabled?: boolean;
telegramBotUsername?: string;
telegramChatId?: string;

@ -159,6 +159,7 @@ class PushoverAgent
...notificationPayload,
token: settings.options.accessToken,
user: settings.options.userToken,
sound: settings.options.sound,
} as PushoverPayload);
} catch (e) {
logger.error('Error sending Pushover notification', {
@ -198,6 +199,7 @@ class PushoverAgent
...notificationPayload,
token: payload.notifyUser.settings.pushoverApplicationToken,
user: payload.notifyUser.settings.pushoverUserKey,
sound: payload.notifyUser.settings.pushoverSound,
} as PushoverPayload);
} catch (e) {
logger.error('Error sending Pushover notification', {

@ -199,6 +199,7 @@ export interface NotificationAgentPushover extends NotificationAgentConfig {
options: {
accessToken: string;
userToken: string;
sound: string;
};
}
@ -384,6 +385,7 @@ class Settings {
options: {
accessToken: '',
userToken: '',
sound: '',
},
},
webhook: {

@ -0,0 +1,31 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUserPushoverSound1697393491630 implements MigrationInterface {
name = 'AddUserPushoverSound1697393491630';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" text, "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "pushoverSound" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_settings"("id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv") SELECT "id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv" FROM "user_settings"`
);
await queryRunner.query(`DROP TABLE "user_settings"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
);
await queryRunner.query(
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" text, "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_settings"("id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv") SELECT "id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv" FROM "temporary_user_settings"`
);
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
}
}

@ -1,4 +1,5 @@
import GithubAPI from '@server/api/github';
import PushoverAPI from '@server/api/pushover';
import TheMovieDb from '@server/api/themoviedb';
import type {
TmdbMovieResult,
@ -112,6 +113,31 @@ router.get('/settings/discover', isAuthenticated(), async (_req, res) => {
return res.json(sliders);
});
router.get(
'/settings/notifications/pushover/sounds',
isAuthenticated(),
async (req, res, next) => {
const pushoverApi = new PushoverAPI();
try {
if (!req.query.token) {
throw new Error('Pushover application token missing from request');
}
const sounds = await pushoverApi.getSounds(req.query.token as string);
res.status(200).json(sounds);
} catch (e) {
logger.debug('Something went wrong retrieving Pushover sounds', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve Pushover sounds.',
});
}
}
);
router.use('/settings', isAuthenticated(Permission.ADMIN), settingsRoutes);
router.use('/search', isAuthenticated(), searchRoutes);
router.use('/discover', isAuthenticated(), discoverRoutes);

@ -262,7 +262,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
}
return res.status(200).json({
emailEnabled: settings?.email.enabled,
emailEnabled: settings.email.enabled,
pgpKey: user.settings?.pgpKey,
discordEnabled:
settings?.discord.enabled && settings.discord.options.enableMentions,
@ -274,11 +274,12 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
pushbulletAccessToken: user.settings?.pushbulletAccessToken,
pushoverApplicationToken: user.settings?.pushoverApplicationToken,
pushoverUserKey: user.settings?.pushoverUserKey,
telegramEnabled: settings?.telegram.enabled,
telegramBotUsername: settings?.telegram.options.botUsername,
pushoverSound: user.settings?.pushoverSound,
telegramEnabled: settings.telegram.enabled,
telegramBotUsername: settings.telegram.options.botUsername,
telegramChatId: user.settings?.telegramChatId,
telegramSendSilently: user?.settings?.telegramSendSilently,
webPushEnabled: settings?.webpush.enabled,
telegramSendSilently: user.settings?.telegramSendSilently,
webPushEnabled: settings.webpush.enabled,
notificationTypes: user.settings?.notificationTypes ?? {},
});
} catch (e) {
@ -329,6 +330,7 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>(
user.settings.pushoverApplicationToken =
req.body.pushoverApplicationToken;
user.settings.pushoverUserKey = req.body.pushoverUserKey;
user.settings.pushoverSound = req.body.pushoverSound;
user.settings.telegramChatId = req.body.telegramChatId;
user.settings.telegramSendSilently = req.body.telegramSendSilently;
user.settings.notificationTypes = Object.assign(
@ -341,13 +343,14 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>(
userRepository.save(user);
return res.status(200).json({
pgpKey: user.settings?.pgpKey,
discordId: user.settings?.discordId,
pushbulletAccessToken: user.settings?.pushbulletAccessToken,
pushoverApplicationToken: user.settings?.pushoverApplicationToken,
pushoverUserKey: user.settings?.pushoverUserKey,
telegramChatId: user.settings?.telegramChatId,
telegramSendSilently: user?.settings?.telegramSendSilently,
pgpKey: user.settings.pgpKey,
discordId: user.settings.discordId,
pushbulletAccessToken: user.settings.pushbulletAccessToken,
pushoverApplicationToken: user.settings.pushoverApplicationToken,
pushoverUserKey: user.settings.pushoverUserKey,
pushoverSound: user.settings.pushoverSound,
telegramChatId: user.settings.telegramChatId,
telegramSendSilently: user.settings.telegramSendSilently,
notificationTypes: user.settings.notificationTypes,
});
} catch (e) {

@ -3,6 +3,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import globalMessages from '@app/i18n/globalMessages';
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
import type { PushoverSound } from '@server/api/pushover';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useState } from 'react';
@ -19,6 +20,8 @@ const messages = defineMessages({
userToken: 'User or Group Key',
userTokenTip:
'Your 30-character <UsersGroupsLink>user or group identifier</UsersGroupsLink>',
sound: 'Notification Sound',
deviceDefault: 'Device Default',
validationAccessTokenRequired: 'You must provide a valid application token',
validationUserTokenRequired: 'You must provide a valid user or group key',
pushoversettingssaved: 'Pushover notification settings saved successfully!',
@ -38,6 +41,11 @@ const NotificationsPushover = () => {
error,
mutate: revalidate,
} = useSWR('/api/v1/settings/notifications/pushover');
const { data: soundsData } = useSWR<PushoverSound[]>(
data?.options.accessToken
? `/api/v1/settings/notifications/pushover/sounds?token=${data.options.accessToken}`
: null
);
const NotificationsPushoverSchema = Yup.object().shape({
accessToken: Yup.string()
@ -77,6 +85,7 @@ const NotificationsPushover = () => {
types: data?.types,
accessToken: data?.options.accessToken,
userToken: data?.options.userToken,
sound: data?.options.sound,
}}
validationSchema={NotificationsPushoverSchema}
onSubmit={async (values) => {
@ -132,6 +141,7 @@ const NotificationsPushover = () => {
options: {
accessToken: values.accessToken,
userToken: values.userToken,
sound: values.sound,
},
});
@ -226,6 +236,30 @@ const NotificationsPushover = () => {
)}
</div>
</div>
<div className="form-row">
<label htmlFor="sound" className="text-label">
{intl.formatMessage(messages.sound)}
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
as="select"
id="sound"
name="sound"
disabled={!soundsData?.length}
>
<option value="">
{intl.formatMessage(messages.deviceDefault)}
</option>
{soundsData?.map((sound, index) => (
<option key={`sound-${index}`} value={sound.name}>
{sound.description}
</option>
))}
</Field>
</div>
</div>
</div>
<NotificationTypeSelector
currentTypes={values.enabled ? values.types : 0}
onUpdate={(newTypes) => {

@ -4,6 +4,7 @@ import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import useSettings from '@app/hooks/useSettings';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import type { PushoverSound } from '@server/api/pushover';
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
@ -22,6 +23,8 @@ const messages = defineMessages({
pushoverUserKey: 'User or Group Key',
pushoverUserKeyTip:
'Your 30-character <UsersGroupsLink>user or group identifier</UsersGroupsLink>',
sound: 'Notification Sound',
deviceDefault: 'Device Default',
validationPushoverApplicationToken:
'You must provide a valid application token',
validationPushoverUserKey: 'You must provide a valid user or group key',
@ -40,6 +43,11 @@ const UserPushoverSettings = () => {
} = useSWR<UserSettingsNotificationsResponse>(
user ? `/api/v1/user/${user?.id}/settings/notifications` : null
);
const { data: soundsData } = useSWR<PushoverSound[]>(
data?.pushoverApplicationToken
? `/api/v1/settings/notifications/pushover/sounds?token=${data.pushoverApplicationToken}`
: null
);
const UserNotificationsPushoverSchema = Yup.object().shape({
pushoverApplicationToken: Yup.string()
@ -191,6 +199,30 @@ const UserPushoverSettings = () => {
)}
</div>
</div>
<div className="form-row">
<label htmlFor="sound" className="text-label">
{intl.formatMessage(messages.sound)}
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
as="select"
id="sound"
name="sound"
disabled={!soundsData?.length}
>
<option value="">
{intl.formatMessage(messages.deviceDefault)}
</option>
{soundsData?.map((sound, index) => (
<option key={`sound-${index}`} value={sound.name}>
{sound.description}
</option>
))}
</Field>
</div>
</div>
</div>
<NotificationTypeSelector
user={user}
currentTypes={values.types}

@ -560,8 +560,10 @@
"components.Settings.Notifications.NotificationsPushover.accessToken": "Application API Token",
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Overseerr",
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Enable Agent",
"components.Settings.Notifications.NotificationsPushover.deviceDefault": "Device Default",
"components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "Pushover notification settings failed to save.",
"components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "Pushover notification settings saved successfully!",
"components.Settings.Notifications.NotificationsPushover.sound": "Notification Sound",
"components.Settings.Notifications.NotificationsPushover.toastPushoverTestFailed": "Pushover test notification failed to send.",
"components.Settings.Notifications.NotificationsPushover.toastPushoverTestSending": "Sending Pushover test notification…",
"components.Settings.Notifications.NotificationsPushover.toastPushoverTestSuccess": "Pushover test notification sent!",
@ -1113,6 +1115,7 @@
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!",
"components.UserProfile.UserSettings.UserGeneralSettings.user": "User",
"components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "You must provide a valid Discord user ID",
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Device Default",
"components.UserProfile.UserSettings.UserNotificationSettings.discordId": "User ID",
"components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your user account",
"components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "Discord notification settings failed to save.",
@ -1136,6 +1139,7 @@
"components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "Pushover notification settings saved successfully!",
"components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Send Silently",
"components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "Send notifications with no sound",
"components.UserProfile.UserSettings.UserNotificationSettings.sound": "Notification Sound",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "Chat ID",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "<TelegramBotLink>Start a chat</TelegramBotLink>, add <GetIdBotLink>@get_id_bot</GetIdBotLink>, and issue the <code>/my_id</code> command",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "Telegram notification settings failed to save.",

Loading…
Cancel
Save