diff --git a/docs/using-overseerr/notifications/pushbullet.md b/docs/using-overseerr/notifications/pushbullet.md index 45edcc3a0..6c6ba853e 100644 --- a/docs/using-overseerr/notifications/pushbullet.md +++ b/docs/using-overseerr/notifications/pushbullet.md @@ -1,5 +1,11 @@ # Pushbullet +{% hint style="info" %} +Users can optionally configure personal notifications in their user settings. + +User notifications are separate from system notifications, and the available notification types are dependent on user permissions. +{% endhint %} + ## Configuration ### Access Token diff --git a/docs/using-overseerr/notifications/pushover.md b/docs/using-overseerr/notifications/pushover.md index 55893dbad..cc09bfb69 100644 --- a/docs/using-overseerr/notifications/pushover.md +++ b/docs/using-overseerr/notifications/pushover.md @@ -1,5 +1,11 @@ # Pushover +{% hint style="info" %} +Users can optionally configure personal notifications in their user settings. + +User notifications are separate from system notifications, and the available notification types are dependent on user permissions. +{% endhint %} + ## Configuration ### Application/API Token diff --git a/docs/using-overseerr/notifications/telegram.md b/docs/using-overseerr/notifications/telegram.md index d0e6f6fcb..9bdb96dbc 100644 --- a/docs/using-overseerr/notifications/telegram.md +++ b/docs/using-overseerr/notifications/telegram.md @@ -1,7 +1,9 @@ # Telegram {% hint style="info" %} -Users can optionally configure their own notifications in their user settings. +Users can optionally configure personal notifications in their user settings. + +User notifications are separate from system notifications, and the available notification types are dependent on user permissions. {% endhint %} ## Configuration diff --git a/overseerr-api.yml b/overseerr-api.yml index 87d8061ea..00e1cf03a 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -1630,6 +1630,15 @@ components: discordId: type: string nullable: true + pushbulletAccessToken: + type: string + nullable: true + pushoverApplicationToken: + type: string + nullable: true + pushoverUserKey: + type: string + nullable: true telegramEnabled: type: boolean telegramBotUsername: diff --git a/server/entity/UserSettings.ts b/server/entity/UserSettings.ts index 02f391112..08397b12f 100644 --- a/server/entity/UserSettings.ts +++ b/server/entity/UserSettings.ts @@ -42,6 +42,15 @@ export class UserSettings { @Column({ nullable: true }) public discordId?: string; + @Column({ nullable: true }) + public pushbulletAccessToken?: string; + + @Column({ nullable: true }) + public pushoverApplicationToken?: string; + + @Column({ nullable: true }) + public pushoverUserKey?: string; + @Column({ nullable: true }) public telegramChatId?: string; diff --git a/server/interfaces/api/userSettingsInterfaces.ts b/server/interfaces/api/userSettingsInterfaces.ts index 18e3c7aba..0f743efef 100644 --- a/server/interfaces/api/userSettingsInterfaces.ts +++ b/server/interfaces/api/userSettingsInterfaces.ts @@ -22,6 +22,9 @@ export interface UserSettingsNotificationsResponse { discordEnabled?: boolean; discordEnabledTypes?: number; discordId?: string; + pushbulletAccessToken?: string; + pushoverApplicationToken?: string; + pushoverUserKey?: string; telegramEnabled?: boolean; telegramBotUsername?: string; telegramChatId?: string; diff --git a/server/lib/notifications/agents/pushbullet.ts b/server/lib/notifications/agents/pushbullet.ts index 160eed87f..3684803f7 100644 --- a/server/lib/notifications/agents/pushbullet.ts +++ b/server/lib/notifications/agents/pushbullet.ts @@ -1,11 +1,19 @@ import axios from 'axios'; +import { getRepository } from 'typeorm'; import { hasNotificationType, Notification } from '..'; import { MediaType } from '../../../constants/media'; +import { User } from '../../../entity/User'; import logger from '../../../logger'; -import { getSettings, NotificationAgentPushbullet } from '../../settings'; +import { Permission } from '../../permissions'; +import { + getSettings, + NotificationAgentKey, + NotificationAgentPushbullet, +} from '../../settings'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; interface PushbulletPayload { + type: string; title: string; body: string; } @@ -25,22 +33,13 @@ class PushbulletAgent } public shouldSend(): boolean { - const settings = this.getSettings(); - - if (settings.enabled && settings.options.accessToken) { - return true; - } - - return false; + return true; } - private constructMessageDetails( + private getNotificationPayload( type: Notification, payload: NotificationPayload - ): { - title: string; - body: string; - } { + ): PushbulletPayload { let messageTitle = ''; let message = ''; @@ -126,6 +125,7 @@ class PushbulletAgent } return { + type: 'note', title: messageTitle, body: message, }; @@ -136,46 +136,132 @@ class PushbulletAgent payload: NotificationPayload ): Promise { const settings = this.getSettings(); + const endpoint = 'https://api.pushbullet.com/v2/pushes'; + const notificationPayload = this.getNotificationPayload(type, payload); - if (!hasNotificationType(type, settings.types ?? 0)) { - return true; - } - - logger.debug('Sending Pushbullet notification', { - label: 'Notifications', - type: Notification[type], - subject: payload.subject, - }); - - try { - const { title, body } = this.constructMessageDetails(type, payload); - - await axios.post( - 'https://api.pushbullet.com/v2/pushes', - { - type: 'note', - title: title, - body: body, - } as PushbulletPayload, - { - headers: { - 'Access-Token': settings.options.accessToken, - }, - } - ); - - return true; - } catch (e) { - logger.error('Error sending Pushbullet notification', { + // Send system notification + if ( + hasNotificationType(type, settings.types ?? 0) && + settings.enabled && + settings.options.accessToken + ) { + logger.debug('Sending Pushbullet notification', { label: 'Notifications', type: Notification[type], subject: payload.subject, - errorMessage: e.message, - response: e.response?.data, }); - return false; + try { + await axios.post(endpoint, notificationPayload, { + headers: { + 'Access-Token': settings.options.accessToken, + }, + }); + } catch (e) { + logger.error('Error sending Pushbullet notification', { + label: 'Notifications', + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + response: e.response?.data, + }); + + return false; + } } + + if (payload.notifyUser) { + // Send notification to the user who submitted the request + if ( + payload.notifyUser.settings?.hasNotificationType( + NotificationAgentKey.PUSHBULLET, + type + ) && + payload.notifyUser.settings?.pushbulletAccessToken && + payload.notifyUser.settings.pushbulletAccessToken !== + settings.options.accessToken + ) { + logger.debug('Sending Pushbullet notification', { + label: 'Notifications', + recipient: payload.notifyUser.displayName, + type: Notification[type], + subject: payload.subject, + }); + + try { + await axios.post(endpoint, notificationPayload, { + headers: { + 'Access-Token': payload.notifyUser.settings.pushbulletAccessToken, + }, + }); + } catch (e) { + logger.error('Error sending Pushbullet notification', { + label: 'Notifications', + recipient: payload.notifyUser.displayName, + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + response: e.response?.data, + }); + + return false; + } + } + } else { + // Send notifications to all users with the Manage Requests permission + const userRepository = getRepository(User); + const users = await userRepository.find(); + + await Promise.all( + users + .filter( + (user) => + user.hasPermission(Permission.MANAGE_REQUESTS) && + user.settings?.hasNotificationType( + NotificationAgentKey.PUSHBULLET, + type + ) && + // Check if it's the user's own auto-approved request + (type !== Notification.MEDIA_AUTO_APPROVED || + user.id !== payload.request?.requestedBy.id) + ) + .map(async (user) => { + if ( + user.settings?.pushbulletAccessToken && + user.settings.pushbulletAccessToken !== + settings.options.accessToken + ) { + logger.debug('Sending Pushbullet notification', { + label: 'Notifications', + recipient: user.displayName, + type: Notification[type], + subject: payload.subject, + }); + + try { + await axios.post(endpoint, notificationPayload, { + headers: { + 'Access-Token': user.settings.pushbulletAccessToken, + }, + }); + } catch (e) { + logger.error('Error sending Pushbullet notification', { + label: 'Notifications', + recipient: user.displayName, + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + response: e.response?.data, + }); + + return false; + } + } + }) + ); + } + + return true; } } diff --git a/server/lib/notifications/agents/pushover.ts b/server/lib/notifications/agents/pushover.ts index b37b54461..e66158063 100644 --- a/server/lib/notifications/agents/pushover.ts +++ b/server/lib/notifications/agents/pushover.ts @@ -1,8 +1,15 @@ import axios from 'axios'; +import { getRepository } from 'typeorm'; import { hasNotificationType, Notification } from '..'; import { MediaType } from '../../../constants/media'; +import { User } from '../../../entity/User'; import logger from '../../../logger'; -import { getSettings, NotificationAgentPushover } from '../../settings'; +import { Permission } from '../../permissions'; +import { + getSettings, + NotificationAgentKey, + NotificationAgentPushover, +} from '../../settings'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; interface PushoverPayload { @@ -31,29 +38,13 @@ class PushoverAgent } public shouldSend(): boolean { - const settings = this.getSettings(); - - if ( - settings.enabled && - settings.options.accessToken && - settings.options.userToken - ) { - return true; - } - - return false; + return true; } - private constructMessageDetails( + private getNotificationPayload( type: Notification, payload: NotificationPayload - ): { - title: string; - message: string; - url: string | undefined; - url_title: string | undefined; - priority: number; - } { + ): Partial { const settings = getSettings(); let messageTitle = ''; let message = ''; @@ -155,6 +146,7 @@ class PushoverAgent url, url_title, priority, + html: 1, }; } @@ -163,45 +155,138 @@ class PushoverAgent payload: NotificationPayload ): Promise { const settings = this.getSettings(); + const endpoint = 'https://api.pushover.net/1/messages.json'; + const notificationPayload = this.getNotificationPayload(type, payload); - if (!hasNotificationType(type, settings.types ?? 0)) { - return true; - } - - logger.debug('Sending Pushover notification', { - label: 'Notifications', - type: Notification[type], - subject: payload.subject, - }); - try { - const endpoint = 'https://api.pushover.net/1/messages.json'; - - const { title, message, url, url_title, priority } = - this.constructMessageDetails(type, payload); - - await axios.post(endpoint, { - token: settings.options.accessToken, - user: settings.options.userToken, - title: title, - message: message, - url: url, - url_title: url_title, - priority: priority, - html: 1, - } as PushoverPayload); - - return true; - } catch (e) { - logger.error('Error sending Pushover notification', { + // Send system notification + if ( + hasNotificationType(type, settings.types ?? 0) && + settings.enabled && + settings.options.accessToken && + settings.options.userToken + ) { + logger.debug('Sending Pushover notification', { label: 'Notifications', type: Notification[type], subject: payload.subject, - errorMessage: e.message, - response: e.response?.data, }); - return false; + try { + await axios.post(endpoint, { + ...notificationPayload, + token: settings.options.accessToken, + user: settings.options.userToken, + } as PushoverPayload); + } catch (e) { + logger.error('Error sending Pushover notification', { + label: 'Notifications', + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + response: e.response?.data, + }); + + return false; + } } + + if (payload.notifyUser) { + // Send notification to the user who submitted the request + if ( + payload.notifyUser.settings?.hasNotificationType( + NotificationAgentKey.PUSHOVER, + type + ) && + payload.notifyUser.settings?.pushoverApplicationToken && + payload.notifyUser.settings?.pushoverUserKey && + payload.notifyUser.settings.pushoverApplicationToken !== + settings.options.accessToken && + payload.notifyUser.settings?.pushoverUserKey !== + settings.options.userToken + ) { + logger.debug('Sending Pushover notification', { + label: 'Notifications', + recipient: payload.notifyUser.displayName, + type: Notification[type], + subject: payload.subject, + }); + + try { + await axios.post(endpoint, { + ...notificationPayload, + token: payload.notifyUser.settings.pushoverApplicationToken, + user: payload.notifyUser.settings.pushoverUserKey, + } as PushoverPayload); + } catch (e) { + logger.error('Error sending Pushover notification', { + label: 'Notifications', + recipient: payload.notifyUser.displayName, + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + response: e.response?.data, + }); + + return false; + } + } + } else { + // Send notifications to all users with the Manage Requests permission + const userRepository = getRepository(User); + const users = await userRepository.find(); + + await Promise.all( + users + .filter( + (user) => + user.hasPermission(Permission.MANAGE_REQUESTS) && + user.settings?.hasNotificationType( + NotificationAgentKey.PUSHOVER, + type + ) && + // Check if it's the user's own auto-approved request + (type !== Notification.MEDIA_AUTO_APPROVED || + user.id !== payload.request?.requestedBy.id) + ) + .map(async (user) => { + if ( + user.settings?.pushoverApplicationToken && + user.settings?.pushoverUserKey && + user.settings.pushoverApplicationToken !== + settings.options.accessToken && + user.settings.pushoverUserKey !== settings.options.userToken + ) { + logger.debug('Sending Pushover notification', { + label: 'Notifications', + recipient: user.displayName, + type: Notification[type], + subject: payload.subject, + }); + + try { + await axios.post(endpoint, { + ...notificationPayload, + token: user.settings.pushoverApplicationToken, + user: user.settings.pushoverUserKey, + } as PushoverPayload); + } catch (e) { + logger.error('Error sending Pushover notification', { + label: 'Notifications', + recipient: user.displayName, + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + response: e.response?.data, + }); + + return false; + } + } + }) + ); + } + + return true; } } diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index b63fbd62f..e71d29116 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -46,11 +46,7 @@ class TelegramAgent public shouldSend(): boolean { const settings = this.getSettings(); - if ( - settings.enabled && - settings.options.botAPI && - settings.options.chatId - ) { + if (settings.enabled && settings.options.botAPI) { return true; } @@ -61,12 +57,10 @@ class TelegramAgent return text ? text.replace(/[_*[\]()~>#+=|{}.!-]/gi, (x) => '\\' + x) : ''; } - private buildMessage( + private getNotificationPayload( type: Notification, - payload: NotificationPayload, - chatId: string, - sendSilently: boolean - ): TelegramMessagePayload | TelegramPhotoPayload { + payload: NotificationPayload + ): Partial { const settings = getSettings(); let message = ''; @@ -160,19 +154,15 @@ class TelegramAgent /* eslint-enable */ return payload.image - ? ({ + ? { photo: payload.image, caption: message, parse_mode: 'MarkdownV2', - chat_id: chatId, - disable_notification: !!sendSilently, - } as TelegramPhotoPayload) - : ({ + } + : { text: message, parse_mode: 'MarkdownV2', - chat_id: chatId, - disable_notification: !!sendSilently, - } as TelegramMessagePayload); + }; } public async send( @@ -180,13 +170,16 @@ class TelegramAgent payload: NotificationPayload ): Promise { const settings = this.getSettings(); - const endpoint = `${this.baseUrl}bot${settings.options.botAPI}/${ payload.image ? 'sendPhoto' : 'sendMessage' }`; + const notificationPayload = this.getNotificationPayload(type, payload); // Send system notification - if (hasNotificationType(type, settings.types ?? 0)) { + if ( + hasNotificationType(type, settings.types ?? 0) && + settings.options.chatId + ) { logger.debug('Sending Telegram notification', { label: 'Notifications', type: Notification[type], @@ -194,15 +187,11 @@ class TelegramAgent }); try { - await axios.post( - endpoint, - this.buildMessage( - type, - payload, - settings.options.chatId, - settings.options.sendSilently - ) - ); + await axios.post(endpoint, { + ...notificationPayload, + chat_id: settings.options.chatId, + disable_notification: !!settings.options.sendSilently, + } as TelegramMessagePayload | TelegramPhotoPayload); } catch (e) { logger.error('Error sending Telegram notification', { label: 'Notifications', @@ -224,7 +213,7 @@ class TelegramAgent type ) && payload.notifyUser.settings?.telegramChatId && - payload.notifyUser.settings?.telegramChatId !== settings.options.chatId + payload.notifyUser.settings.telegramChatId !== settings.options.chatId ) { logger.debug('Sending Telegram notification', { label: 'Notifications', @@ -234,15 +223,12 @@ class TelegramAgent }); try { - await axios.post( - endpoint, - this.buildMessage( - type, - payload, - payload.notifyUser.settings.telegramChatId, - !!payload.notifyUser.settings.telegramSendSilently - ) - ); + await axios.post(endpoint, { + ...notificationPayload, + chat_id: payload.notifyUser.settings.telegramChatId, + disable_notification: + !!payload.notifyUser.settings.telegramSendSilently, + } as TelegramMessagePayload | TelegramPhotoPayload); } catch (e) { logger.error('Error sending Telegram notification', { label: 'Notifications', @@ -287,15 +273,11 @@ class TelegramAgent }); try { - await axios.post( - endpoint, - this.buildMessage( - type, - payload, - user.settings.telegramChatId, - !!user.settings?.telegramSendSilently - ) - ); + await axios.post(endpoint, { + ...notificationPayload, + chat_id: user.settings.telegramChatId, + disable_notification: !!user.settings?.telegramSendSilently, + } as TelegramMessagePayload | TelegramPhotoPayload); } catch (e) { logger.error('Error sending Telegram notification', { label: 'Notifications', diff --git a/server/lib/notifications/agents/webpush.ts b/server/lib/notifications/agents/webpush.ts index 624dab223..1ab03ba67 100644 --- a/server/lib/notifications/agents/webpush.ts +++ b/server/lib/notifications/agents/webpush.ts @@ -206,6 +206,11 @@ class WebPushAgent settings.vapidPrivate ); + const notificationPayload = Buffer.from( + JSON.stringify(this.getNotificationPayload(type, payload)), + 'utf-8' + ); + await Promise.all( pushSubs.map(async (sub) => { logger.debug('Sending web push notification', { @@ -224,10 +229,7 @@ class WebPushAgent p256dh: sub.p256dh, }, }, - Buffer.from( - JSON.stringify(this.getNotificationPayload(type, payload)), - 'utf-8' - ) + notificationPayload ); } catch (e) { logger.error( diff --git a/server/migration/1635079863457-AddPushbulletPushoverUserSettings.ts b/server/migration/1635079863457-AddPushbulletPushoverUserSettings.ts new file mode 100644 index 000000000..8934866fa --- /dev/null +++ b/server/migration/1635079863457-AddPushbulletPushoverUserSettings.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPushbulletPushoverUserSettings1635079863457 + implements MigrationInterface +{ + name = 'AddPushbulletPushoverUserSettings1635079863457'; + + 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, 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") SELECT "id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale" 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 (''), 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") SELECT "id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale" FROM "temporary_user_settings"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_settings"`); + } +} diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index 226dcae09..6558115a7 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -257,6 +257,9 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>( ? settings?.discord.types : 0, discordId: user.settings?.discordId, + pushbulletAccessToken: user.settings?.pushbulletAccessToken, + pushoverApplicationToken: user.settings?.pushoverApplicationToken, + pushoverUserKey: user.settings?.pushoverUserKey, telegramEnabled: settings?.telegram.enabled, telegramBotUsername: settings?.telegram.options.botUsername, telegramChatId: user.settings?.telegramChatId, @@ -298,6 +301,9 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>( user: req.user, pgpKey: req.body.pgpKey, discordId: req.body.discordId, + pushbulletAccessToken: req.body.pushbulletAccessToken, + pushoverApplicationToken: req.body.pushoverApplicationToken, + pushoverUserKey: req.body.pushoverUserKey, telegramChatId: req.body.telegramChatId, telegramSendSilently: req.body.telegramSendSilently, notificationTypes: req.body.notificationTypes, @@ -305,6 +311,10 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>( } else { user.settings.pgpKey = req.body.pgpKey; user.settings.discordId = req.body.discordId; + user.settings.pushbulletAccessToken = req.body.pushbulletAccessToken; + user.settings.pushoverApplicationToken = + req.body.pushoverApplicationToken; + user.settings.pushoverUserKey = req.body.pushoverUserKey; user.settings.telegramChatId = req.body.telegramChatId; user.settings.telegramSendSilently = req.body.telegramSendSilently; user.settings.notificationTypes = Object.assign( @@ -319,6 +329,9 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>( 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, notificationTypes: user.settings.notificationTypes, diff --git a/src/components/Settings/Notifications/NotificationsTelegram.tsx b/src/components/Settings/Notifications/NotificationsTelegram.tsx index bcb03df89..d76fdde33 100644 --- a/src/components/Settings/Notifications/NotificationsTelegram.tsx +++ b/src/components/Settings/Notifications/NotificationsTelegram.tsx @@ -51,8 +51,8 @@ const NotificationsTelegram: React.FC = () => { otherwise: Yup.string().nullable(), }), chatId: Yup.string() - .when('enabled', { - is: true, + .when(['enabled', 'types'], { + is: (enabled: boolean, types: number) => enabled && !!types, then: Yup.string() .nullable() .required(intl.formatMessage(messages.validationChatIdRequired)), diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx index 155c013b7..85d39ccfa 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx @@ -35,7 +35,7 @@ const UserNotificationsDiscord: React.FC = () => { const UserNotificationsDiscordSchema = Yup.object().shape({ discordId: Yup.string() .when('types', { - is: (value: unknown) => !!value, + is: (types: number) => !!types, then: Yup.string() .nullable() .required(intl.formatMessage(messages.validationDiscordId)), @@ -63,6 +63,9 @@ const UserNotificationsDiscord: React.FC = () => { await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { pgpKey: data?.pgpKey, discordId: values.discordId, + pushbulletAccessToken: data?.pushbulletAccessToken, + pushoverApplicationToken: data?.pushoverApplicationToken, + pushoverUserKey: data?.pushoverUserKey, telegramChatId: data?.telegramChatId, telegramSendSilently: data?.telegramSendSilently, notificationTypes: { diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx index 576bdf14e..a0132d5f1 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx @@ -63,6 +63,9 @@ const UserEmailSettings: React.FC = () => { await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { pgpKey: values.pgpKey, discordId: data?.discordId, + pushbulletAccessToken: data?.pushbulletAccessToken, + pushoverApplicationToken: data?.pushoverApplicationToken, + pushoverUserKey: data?.pushoverUserKey, telegramChatId: data?.telegramChatId, telegramSendSilently: data?.telegramSendSilently, notificationTypes: { diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushbullet.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushbullet.tsx new file mode 100644 index 000000000..615b6132e --- /dev/null +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushbullet.tsx @@ -0,0 +1,172 @@ +import axios from 'axios'; +import { Form, Formik } from 'formik'; +import { useRouter } from 'next/router'; +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; +import useSWR from 'swr'; +import * as Yup from 'yup'; +import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces'; +import { useUser } from '../../../../hooks/useUser'; +import globalMessages from '../../../../i18n/globalMessages'; +import Button from '../../../Common/Button'; +import LoadingSpinner from '../../../Common/LoadingSpinner'; +import SensitiveInput from '../../../Common/SensitiveInput'; +import NotificationTypeSelector from '../../../NotificationTypeSelector'; + +const messages = defineMessages({ + pushbulletsettingssaved: + 'Pushbullet notification settings saved successfully!', + pushbulletsettingsfailed: 'Pushbullet notification settings failed to save.', + pushbulletAccessToken: 'Access Token', + pushbulletAccessTokenTip: + 'Create a token from your Account Settings', + validationPushbulletAccessToken: 'You must provide an access token', +}); + +const UserPushbulletSettings: React.FC = () => { + const intl = useIntl(); + const { addToast } = useToasts(); + const router = useRouter(); + const { user } = useUser({ id: Number(router.query.userId) }); + const { data, error, revalidate } = useSWR( + user ? `/api/v1/user/${user?.id}/settings/notifications` : null + ); + + const UserNotificationsPushbulletSchema = Yup.object().shape({ + pushbulletAccessToken: Yup.string().when('types', { + is: (types: number) => !!types, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationPushbulletAccessToken)), + otherwise: Yup.string().nullable(), + }), + }); + + if (!data && !error) { + return ; + } + + return ( + { + try { + await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { + pgpKey: data?.pgpKey, + discordId: data?.discordId, + pushbulletAccessToken: values.pushbulletAccessToken, + pushoverApplicationToken: data?.pushoverApplicationToken, + pushoverUserKey: data?.pushoverUserKey, + telegramChatId: data?.telegramChatId, + telegramSendSilently: data?.telegramSendSilently, + notificationTypes: { + pushbullet: values.types, + }, + }); + addToast(intl.formatMessage(messages.pushbulletsettingssaved), { + appearance: 'success', + autoDismiss: true, + }); + } catch (e) { + addToast(intl.formatMessage(messages.pushbulletsettingsfailed), { + appearance: 'error', + autoDismiss: true, + }); + } finally { + revalidate(); + } + }} + > + {({ + errors, + touched, + isSubmitting, + isValid, + values, + setFieldValue, + setFieldTouched, + }) => { + return ( +
+
+ +
+
+ +
+ {errors.pushbulletAccessToken && + touched.pushbulletAccessToken && ( +
{errors.pushbulletAccessToken}
+ )} +
+
+ { + setFieldValue('types', newTypes); + setFieldTouched('types'); + }} + error={ + errors.types && touched.types + ? (errors.types as string) + : undefined + } + /> +
+
+ + + +
+
+ + ); + }} +
+ ); +}; + +export default UserPushbulletSettings; diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover.tsx new file mode 100644 index 000000000..0f88a795d --- /dev/null +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover.tsx @@ -0,0 +1,228 @@ +import axios from 'axios'; +import { Field, Form, Formik } from 'formik'; +import { useRouter } from 'next/router'; +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; +import useSWR from 'swr'; +import * as Yup from 'yup'; +import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces'; +import { useUser } from '../../../../hooks/useUser'; +import globalMessages from '../../../../i18n/globalMessages'; +import Button from '../../../Common/Button'; +import LoadingSpinner from '../../../Common/LoadingSpinner'; +import NotificationTypeSelector from '../../../NotificationTypeSelector'; + +const messages = defineMessages({ + pushoversettingssaved: 'Pushover notification settings saved successfully!', + pushoversettingsfailed: 'Pushover notification settings failed to save.', + pushoverApplicationToken: 'Application API Token', + pushoverApplicationTokenTip: + 'Register an application for use with Overseerr', + pushoverUserKey: 'User or Group Key', + pushoverUserKeyTip: + 'Your 30-character user or group identifier', + validationPushoverApplicationToken: + 'You must provide a valid application token', + validationPushoverUserKey: 'You must provide a valid user or group key', +}); + +const UserPushoverSettings: React.FC = () => { + const intl = useIntl(); + const { addToast } = useToasts(); + const router = useRouter(); + const { user } = useUser({ id: Number(router.query.userId) }); + const { data, error, revalidate } = useSWR( + user ? `/api/v1/user/${user?.id}/settings/notifications` : null + ); + + const UserNotificationsPushoverSchema = Yup.object().shape({ + pushoverApplicationToken: Yup.string() + .when('types', { + is: (types: number) => !!types, + then: Yup.string() + .nullable() + .required( + intl.formatMessage(messages.validationPushoverApplicationToken) + ), + otherwise: Yup.string().nullable(), + }) + .matches( + /^[a-z\d]{30}$/i, + intl.formatMessage(messages.validationPushoverApplicationToken) + ), + pushoverUserKey: Yup.string() + .when('types', { + is: (types: number) => !!types, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationPushoverUserKey)), + otherwise: Yup.string().nullable(), + }) + .matches( + /^[a-z\d]{30}$/i, + intl.formatMessage(messages.validationPushoverUserKey) + ), + }); + + if (!data && !error) { + return ; + } + + return ( + { + try { + await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { + pgpKey: data?.pgpKey, + discordId: data?.discordId, + pushbulletAccessToken: data?.pushbulletAccessToken, + pushoverApplicationToken: values.pushoverApplicationToken, + pushoverUserKey: values.pushoverUserKey, + telegramChatId: data?.telegramChatId, + telegramSendSilently: data?.telegramSendSilently, + notificationTypes: { + pushover: values.types, + }, + }); + addToast(intl.formatMessage(messages.pushoversettingssaved), { + appearance: 'success', + autoDismiss: true, + }); + } catch (e) { + addToast(intl.formatMessage(messages.pushoversettingsfailed), { + appearance: 'error', + autoDismiss: true, + }); + } finally { + revalidate(); + } + }} + > + {({ + errors, + touched, + isSubmitting, + isValid, + values, + setFieldValue, + setFieldTouched, + }) => { + return ( +
+
+ +
+
+ +
+ {errors.pushoverApplicationToken && + touched.pushoverApplicationToken && ( +
+ {errors.pushoverApplicationToken} +
+ )} +
+
+
+ +
+
+ +
+ {errors.pushoverUserKey && touched.pushoverUserKey && ( +
{errors.pushoverUserKey}
+ )} +
+
+ { + setFieldValue('types', newTypes); + setFieldTouched('types'); + }} + error={ + errors.types && touched.types + ? (errors.types as string) + : undefined + } + /> +
+
+ + + +
+
+ + ); + }} +
+ ); +}; + +export default UserPushoverSettings; diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx index b27e5afed..96adfdcf8 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx @@ -37,7 +37,7 @@ const UserTelegramSettings: React.FC = () => { const UserNotificationsTelegramSchema = Yup.object().shape({ telegramChatId: Yup.string() .when('types', { - is: (value: unknown) => !!value, + is: (types: number) => !!types, then: Yup.string() .nullable() .required(intl.formatMessage(messages.validationTelegramChatId)), @@ -67,6 +67,9 @@ const UserTelegramSettings: React.FC = () => { await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { pgpKey: data?.pgpKey, discordId: data?.discordId, + pushbulletAccessToken: data?.pushbulletAccessToken, + pushoverApplicationToken: data?.pushoverApplicationToken, + pushoverUserKey: data?.pushoverUserKey, telegramChatId: values.telegramChatId, telegramSendSilently: values.telegramSendSilently, notificationTypes: { diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush.tsx index d2e36810a..6cfb46532 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush.tsx @@ -44,6 +44,9 @@ const UserWebPushSettings: React.FC = () => { await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { pgpKey: data?.pgpKey, discordId: data?.discordId, + pushbulletAccessToken: data?.pushbulletAccessToken, + pushoverApplicationToken: data?.pushoverApplicationToken, + pushoverUserKey: data?.pushoverUserKey, telegramChatId: data?.telegramChatId, telegramSendSilently: data?.telegramSendSilently, notificationTypes: { diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx index 0f58f7e7b..6f2cc64f1 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx @@ -5,6 +5,8 @@ import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces'; import DiscordLogo from '../../../../assets/extlogos/discord.svg'; +import PushbulletLogo from '../../../../assets/extlogos/pushbullet.svg'; +import PushoverLogo from '../../../../assets/extlogos/pushover.svg'; import TelegramLogo from '../../../../assets/extlogos/telegram.svg'; import { useUser } from '../../../../hooks/useUser'; import globalMessages from '../../../../i18n/globalMessages'; @@ -64,6 +66,28 @@ const UserNotificationSettings: React.FC = ({ children }) => { route: '/settings/notifications/discord', regex: /\/settings\/notifications\/discord/, }, + { + text: 'Pushbullet', + content: ( + + + Pushbullet + + ), + route: '/settings/notifications/pushbullet', + regex: /\/settings\/notifications\/pushbullet/, + }, + { + text: 'Pushover', + content: ( + + + Pushover + + ), + route: '/settings/notifications/pushover', + regex: /\/settings\/notifications\/pushover/, + }, { text: 'Telegram', content: ( diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 286f2b174..3647fe7a5 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -874,6 +874,16 @@ "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Notification Settings", "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "PGP Public Key", "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "Encrypt email messages using OpenPGP", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Access Token", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "Create a token from your Account Settings", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "Pushbullet notification settings failed to save.", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "Pushbullet notification settings saved successfully!", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "Application API Token", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "Register an application for use with Overseerr", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "User or Group Key", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "Your 30-character user or group identifier", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "Pushover notification settings failed to save.", + "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.telegramChatId": "Chat ID", @@ -882,6 +892,9 @@ "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "Telegram notification settings saved successfully!", "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "You must provide a valid user ID", "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "You must provide a valid PGP public key", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "You must provide an access token", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "You must provide a valid application token", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "You must provide a valid user or group key", "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "You must provide a valid chat ID", "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "Web Push", "components.UserProfile.UserSettings.UserNotificationSettings.webpushsettingsfailed": "Web push notification settings failed to save.", diff --git a/src/pages/profile/settings/notifications/pushbullet.tsx b/src/pages/profile/settings/notifications/pushbullet.tsx new file mode 100644 index 000000000..12afd9413 --- /dev/null +++ b/src/pages/profile/settings/notifications/pushbullet.tsx @@ -0,0 +1,17 @@ +import { NextPage } from 'next'; +import React from 'react'; +import UserSettings from '../../../../components/UserProfile/UserSettings'; +import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings'; +import UserNotificationsPushbullet from '../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushbullet'; + +const NotificationsPage: NextPage = () => { + return ( + + + + + + ); +}; + +export default NotificationsPage; diff --git a/src/pages/profile/settings/notifications/pushover.tsx b/src/pages/profile/settings/notifications/pushover.tsx new file mode 100644 index 000000000..83b8d71db --- /dev/null +++ b/src/pages/profile/settings/notifications/pushover.tsx @@ -0,0 +1,17 @@ +import { NextPage } from 'next'; +import React from 'react'; +import UserSettings from '../../../../components/UserProfile/UserSettings'; +import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings'; +import UserNotificationsPushover from '../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover'; + +const NotificationsPage: NextPage = () => { + return ( + + + + + + ); +}; + +export default NotificationsPage; diff --git a/src/pages/users/[userId]/settings/notifications/pushbullet.tsx b/src/pages/users/[userId]/settings/notifications/pushbullet.tsx new file mode 100644 index 000000000..cd7ca10c8 --- /dev/null +++ b/src/pages/users/[userId]/settings/notifications/pushbullet.tsx @@ -0,0 +1,20 @@ +import { NextPage } from 'next'; +import React from 'react'; +import UserSettings from '../../../../../components/UserProfile/UserSettings'; +import UserNotificationSettings from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings'; +import UserNotificationsPushbullet from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushbullet'; +import useRouteGuard from '../../../../../hooks/useRouteGuard'; +import { Permission } from '../../../../../hooks/useUser'; + +const NotificationsPage: NextPage = () => { + useRouteGuard(Permission.MANAGE_USERS); + return ( + + + + + + ); +}; + +export default NotificationsPage; diff --git a/src/pages/users/[userId]/settings/notifications/pushover.tsx b/src/pages/users/[userId]/settings/notifications/pushover.tsx new file mode 100644 index 000000000..b37a866f7 --- /dev/null +++ b/src/pages/users/[userId]/settings/notifications/pushover.tsx @@ -0,0 +1,20 @@ +import { NextPage } from 'next'; +import React from 'react'; +import UserSettings from '../../../../../components/UserProfile/UserSettings'; +import UserNotificationSettings from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings'; +import UserNotificationsPushover from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover'; +import useRouteGuard from '../../../../../hooks/useRouteGuard'; +import { Permission } from '../../../../../hooks/useUser'; + +const NotificationsPage: NextPage = () => { + useRouteGuard(Permission.MANAGE_USERS); + return ( + + + + + + ); +}; + +export default NotificationsPage;