diff --git a/overseerr-api.yml b/overseerr-api.yml index f3a1cc74..6e8f5896 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 00000000..41754368 --- /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 771c382d..ea4a7d33 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 b5b69245..fb0767b2 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 d8deb1bd..f0140a90 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 082e733f..10213a04 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -192,6 +192,7 @@ export interface NotificationAgentPushover extends NotificationAgentConfig { options: { accessToken: string; userToken: string; + sound: string; }; } @@ -372,6 +373,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 00000000..03699a89 --- /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 f76f09fa..702b18d7 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 875309f3..c8b3f50b 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 93e4a285..68a32154 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 47b3df51..2c991efb 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!", @@ -1104,6 +1106,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.", @@ -1127,6 +1130,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.",