diff --git a/overseerr-api.yml b/overseerr-api.yml index 201c6d0aa..ea800b708 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -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 diff --git a/server/api/pushover.ts b/server/api/pushover.ts new file mode 100644 index 000000000..41754368d --- /dev/null +++ b/server/api/pushover.ts @@ -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 { + try { + const data = await this.get('/sounds.json', { + params: { + token: appToken, + }, + }); + + return mapSounds(data.sounds); + } catch (e) { + throw new Error(`[Pushover] Failed to retrieve sounds: ${e.message}`); + } + } +} + +export default PushoverAPI; diff --git a/server/entity/UserSettings.ts b/server/entity/UserSettings.ts index 771c382d1..ea4a7d33b 100644 --- a/server/entity/UserSettings.ts +++ b/server/entity/UserSettings.ts @@ -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; diff --git a/server/interfaces/api/userSettingsInterfaces.ts b/server/interfaces/api/userSettingsInterfaces.ts index b5b69245b..fb0767b21 100644 --- a/server/interfaces/api/userSettingsInterfaces.ts +++ b/server/interfaces/api/userSettingsInterfaces.ts @@ -28,6 +28,7 @@ export interface UserSettingsNotificationsResponse { pushbulletAccessToken?: string; pushoverApplicationToken?: string; pushoverUserKey?: string; + pushoverSound?: string; telegramEnabled?: boolean; telegramBotUsername?: string; telegramChatId?: string; diff --git a/server/lib/notifications/agents/pushover.ts b/server/lib/notifications/agents/pushover.ts index d8deb1bdc..f0140a904 100644 --- a/server/lib/notifications/agents/pushover.ts +++ b/server/lib/notifications/agents/pushover.ts @@ -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', { diff --git a/server/lib/settings.ts b/server/lib/settings.ts index 1ad53e1f1..6c53f09d1 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -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: { diff --git a/server/migration/1697393491630-AddUserPushoverSound.ts b/server/migration/1697393491630-AddUserPushoverSound.ts new file mode 100644 index 000000000..03699a89d --- /dev/null +++ b/server/migration/1697393491630-AddUserPushoverSound.ts @@ -0,0 +1,31 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddUserPushoverSound1697393491630 implements MigrationInterface { + name = 'AddUserPushoverSound1697393491630'; + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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"`); + } +} diff --git a/server/routes/index.ts b/server/routes/index.ts index f76f09fa0..702b18d78 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -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); diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index 875309f3d..c8b3f50bd 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -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) { diff --git a/src/components/Settings/Notifications/NotificationsPushover/index.tsx b/src/components/Settings/Notifications/NotificationsPushover/index.tsx index 93e4a285b..68a321540 100644 --- a/src/components/Settings/Notifications/NotificationsPushover/index.tsx +++ b/src/components/Settings/Notifications/NotificationsPushover/index.tsx @@ -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 user or group identifier', + 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( + 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 = () => { )} +
+ +
+
+ + + {soundsData?.map((sound, index) => ( + + ))} + +
+
+
{ diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover.tsx index 47b3df51e..2c991efb3 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover.tsx @@ -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 user or group identifier', + 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( user ? `/api/v1/user/${user?.id}/settings/notifications` : null ); + const { data: soundsData } = useSWR( + 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 = () => { )} +
+ +
+
+ + + {soundsData?.map((sound, index) => ( + + ))} + +
+
+
Register an application 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 multi-digit ID number 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": "Start a chat, add @get_id_bot, and issue the /my_id command", "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "Telegram notification settings failed to save.",