Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(notif): add Pushover sound options #2403

Merged
merged 4 commits into from
Oct 15, 2023
Merged
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
32 changes: 32 additions & 0 deletions overseerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,8 @@ components:
type: string
userToken:
type: string
sound:
type: string
GotifySettings:
type: object
properties:
Expand Down Expand Up @@ -1708,6 +1710,9 @@ components:
pushoverUserKey:
type: string
nullable: true
pushoverSound:
type: string
nullable: true
telegramEnabled:
type: boolean
telegramBotUsername:
Expand Down Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions server/api/pushover.ts
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 3 additions & 0 deletions server/entity/UserSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions server/interfaces/api/userSettingsInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface UserSettingsNotificationsResponse {
pushbulletAccessToken?: string;
pushoverApplicationToken?: string;
pushoverUserKey?: string;
pushoverSound?: string;
telegramEnabled?: boolean;
telegramBotUsername?: string;
telegramChatId?: string;
Expand Down
2 changes: 2 additions & 0 deletions server/lib/notifications/agents/pushover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down Expand Up @@ -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', {
Expand Down
2 changes: 2 additions & 0 deletions server/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export interface NotificationAgentPushover extends NotificationAgentConfig {
options: {
accessToken: string;
userToken: string;
sound: string;
};
}

Expand Down Expand Up @@ -372,6 +373,7 @@ class Settings {
options: {
accessToken: '',
userToken: '',
sound: '',
},
},
webhook: {
Expand Down
31 changes: 31 additions & 0 deletions server/migration/1697393491630-AddUserPushoverSound.ts
Original file line number Diff line number Diff line change
@@ -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"`);
}
}
26 changes: 26 additions & 0 deletions server/routes/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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);
Expand Down
27 changes: 15 additions & 12 deletions server/routes/user/usersettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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(
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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!',
Expand All @@ -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()
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -132,6 +141,7 @@ const NotificationsPushover = () => {
options: {
accessToken: values.accessToken,
userToken: values.userToken,
sound: values.sound,
},
});

Expand Down Expand Up @@ -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) => {
Expand Down
Loading