From f6d00d8d1559879189f83739193c6e2acafde51d Mon Sep 17 00:00:00 2001 From: Jakob Ankarhem Date: Fri, 5 Mar 2021 01:18:56 +0100 Subject: [PATCH] feat(telegram): add support for individual chat notifications (#1027) --- .../using-overseerr/notifications/webhooks.md | 1 + overseerr-api.yml | 10 ++ server/entity/UserSettings.ts | 6 ++ .../interfaces/api/userSettingsInterfaces.ts | 3 + server/lib/notifications/agents/telegram.ts | 15 +++ server/lib/notifications/agents/webhook.ts | 1 + server/lib/settings.ts | 2 + ...95680-AddTelegramSettingsToUserSettings.ts | 32 ++++++ server/routes/user/usersettings.ts | 12 +++ .../Notifications/NotificationsTelegram.tsx | 31 +++++- .../UserNotificationSettings/index.tsx | 99 +++++++++++++++++++ src/i18n/locale/en.json | 7 ++ 12 files changed, 216 insertions(+), 3 deletions(-) create mode 100644 server/migration/1614334195680-AddTelegramSettingsToUserSettings.ts diff --git a/docs/using-overseerr/notifications/webhooks.md b/docs/using-overseerr/notifications/webhooks.md index 7adbc24a..68f54683 100644 --- a/docs/using-overseerr/notifications/webhooks.md +++ b/docs/using-overseerr/notifications/webhooks.md @@ -35,6 +35,7 @@ These variables are usually the target user of the notification. - `{{notifyuser_email}}` Target user's email. - `{{notifyuser_avatar}}` Target user's avatar. - `{{notifyuser_settings_discordId}}` Target user's discord ID (if one is set). +- `{{notifyuser_settings_telegramChatId}}` Target user's telegram Chat ID (if one is set). ### Media diff --git a/overseerr-api.yml b/overseerr-api.yml index 63c874e6..03667870 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -97,6 +97,10 @@ components: default: true discordId: type: string + telegramChatId: + type: string + telegramSendSilently: + type: boolean required: - enableNotifications MainSettings: @@ -1545,6 +1549,12 @@ components: discordId: type: string nullable: true + telegramChatId: + type: string + nullable: true + telegramSendSilently: + type: boolean + nullable: true required: - enableNotifications securitySchemes: diff --git a/server/entity/UserSettings.ts b/server/entity/UserSettings.ts index 163de134..d2fe3892 100644 --- a/server/entity/UserSettings.ts +++ b/server/entity/UserSettings.ts @@ -26,6 +26,12 @@ export class UserSettings { @Column({ nullable: true }) public discordId?: string; + @Column({ nullable: true }) + public telegramChatId?: string; + + @Column({ nullable: true }) + public telegramSendSilently?: boolean; + @Column({ nullable: true }) public region?: string; diff --git a/server/interfaces/api/userSettingsInterfaces.ts b/server/interfaces/api/userSettingsInterfaces.ts index 023b7631..99d01251 100644 --- a/server/interfaces/api/userSettingsInterfaces.ts +++ b/server/interfaces/api/userSettingsInterfaces.ts @@ -6,5 +6,8 @@ export interface UserSettingsGeneralResponse { export interface UserSettingsNotificationsResponse { enableNotifications: boolean; + telegramBotUsername?: string; discordId?: string; + telegramChatId?: string; + telegramSendSilently?: boolean; } diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index fd3b4dd9..1e82a04d 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -134,6 +134,21 @@ class TelegramAgent disable_notification: this.getSettings().options.sendSilently, } as TelegramPayload); + if ( + payload.notifyUser.settings?.enableNotifications && + payload.notifyUser.settings?.telegramChatId && + payload.notifyUser.settings?.telegramChatId !== + this.getSettings().options.chatId + ) { + await axios.post(endpoint, { + text: this.buildMessage(type, payload), + parse_mode: 'MarkdownV2', + chat_id: `${payload.notifyUser.settings.telegramChatId}`, + disable_notification: + payload.notifyUser.settings.telegramSendSilently, + } as TelegramPayload); + } + return true; } catch (e) { logger.error('Error sending Telegram notification', { diff --git a/server/lib/notifications/agents/webhook.ts b/server/lib/notifications/agents/webhook.ts index 6186be49..5b846d72 100644 --- a/server/lib/notifications/agents/webhook.ts +++ b/server/lib/notifications/agents/webhook.ts @@ -20,6 +20,7 @@ const KeyMap: Record = { notifyuser_email: 'notifyUser.email', notifyuser_avatar: 'notifyUser.avatar', notifyuser_settings_discordId: 'notifyUser.settings.discordId', + notifyuser_settings_telegramChatId: 'notifyUser.settings.telegramChatId', media_tmdbid: 'media.tmdbId', media_imdbid: 'media.imdbId', media_tvdbid: 'media.tvdbId', diff --git a/server/lib/settings.ts b/server/lib/settings.ts index a65c5ffb..6d3e9536 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -120,6 +120,7 @@ export interface NotificationAgentEmail extends NotificationAgentConfig { export interface NotificationAgentTelegram extends NotificationAgentConfig { options: { + botUsername: string; botAPI: string; chatId: string; sendSilently: boolean; @@ -242,6 +243,7 @@ class Settings { enabled: false, types: 0, options: { + botUsername: '', botAPI: '', chatId: '', sendSilently: false, diff --git a/server/migration/1614334195680-AddTelegramSettingsToUserSettings.ts b/server/migration/1614334195680-AddTelegramSettingsToUserSettings.ts new file mode 100644 index 00000000..1e0175cc --- /dev/null +++ b/server/migration/1614334195680-AddTelegramSettingsToUserSettings.ts @@ -0,0 +1,32 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTelegramSettingsToUserSettings1614334195680 + implements MigrationInterface { + name = 'AddTelegramSettingsToUserSettings1614334195680'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "enableNotifications" boolean NOT NULL DEFAULT (1), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" 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 "temporary_user_settings"("id", "enableNotifications", "discordId", "userId", "region", "originalLanguage") SELECT "id", "enableNotifications", "discordId", "userId", "region", "originalLanguage" 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, "enableNotifications" boolean NOT NULL DEFAULT (1), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" 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 "user_settings"("id", "enableNotifications", "discordId", "userId", "region", "originalLanguage") SELECT "id", "enableNotifications", "discordId", "userId", "region", "originalLanguage" 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 e102e2e2..e20ee375 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -2,6 +2,7 @@ import { Router } from 'express'; import { getRepository } from 'typeorm'; import { canMakePermissionsChange } from '.'; import { User } from '../../entity/User'; +import { getSettings } from '../../lib/settings'; import { UserSettings } from '../../entity/UserSettings'; import { UserSettingsGeneralResponse, @@ -198,6 +199,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>( isOwnProfileOrAdmin(), async (req, res, next) => { const userRepository = getRepository(User); + const settings = getSettings(); try { const user = await userRepository.findOne({ @@ -210,7 +212,11 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>( return res.status(200).json({ enableNotifications: user.settings?.enableNotifications ?? true, + telegramBotUsername: + settings?.notifications.agents.telegram.options.botUsername, discordId: user.settings?.discordId, + telegramChatId: user.settings?.telegramChatId, + telegramSendSilently: user?.settings?.telegramSendSilently, }); } catch (e) { next({ status: 500, message: e.message }); @@ -239,10 +245,14 @@ userSettingsRoutes.post< user: req.user, enableNotifications: req.body.enableNotifications, discordId: req.body.discordId, + telegramChatId: req.body.telegramChatId, + telegramSendSilently: req.body.telegramSendSilently, }); } else { user.settings.enableNotifications = req.body.enableNotifications; user.settings.discordId = req.body.discordId; + user.settings.telegramChatId = req.body.telegramChatId; + user.settings.telegramSendSilently = req.body.telegramSendSilently; } userRepository.save(user); @@ -250,6 +260,8 @@ userSettingsRoutes.post< return res.status(200).json({ enableNotifications: user.settings.enableNotifications, discordId: user.settings.discordId, + telegramChatId: user.settings.telegramChatId, + telegramSendSilently: user.settings.telegramSendSilently, }); } catch (e) { next({ status: 500, message: e.message }); diff --git a/src/components/Settings/Notifications/NotificationsTelegram.tsx b/src/components/Settings/Notifications/NotificationsTelegram.tsx index abfd8b0a..b5382a5b 100644 --- a/src/components/Settings/Notifications/NotificationsTelegram.tsx +++ b/src/components/Settings/Notifications/NotificationsTelegram.tsx @@ -14,6 +14,7 @@ const messages = defineMessages({ save: 'Save Changes', saving: 'Saving…', agentenabled: 'Enable Agent', + botUsername: 'Bot Username', botAPI: 'Bot Authentication Token', chatId: 'Chat ID', validationBotAPIRequired: 'You must provide a bot authentication token', @@ -43,9 +44,12 @@ const NotificationsTelegram: React.FC = () => { botAPI: Yup.string().required( intl.formatMessage(messages.validationBotAPIRequired) ), - chatId: Yup.string().required( - intl.formatMessage(messages.validationChatIdRequired) - ), + chatId: Yup.string() + .required(intl.formatMessage(messages.validationChatIdRequired)) + .matches( + /^[-]?\d+$/, + intl.formatMessage(messages.validationChatIdRequired) + ), }); if (!data && !error) { @@ -57,6 +61,7 @@ const NotificationsTelegram: React.FC = () => { initialValues={{ enabled: data?.enabled, types: data?.types, + botUsername: data?.options.botUsername, botAPI: data?.options.botAPI, chatId: data?.options.chatId, sendSilently: data?.options.sendSilently, @@ -71,6 +76,7 @@ const NotificationsTelegram: React.FC = () => { botAPI: values.botAPI, chatId: values.chatId, sendSilently: values.sendSilently, + botUsername: values.botUsername, }, }); addToast(intl.formatMessage(messages.telegramsettingssaved), { @@ -96,6 +102,7 @@ const NotificationsTelegram: React.FC = () => { botAPI: values.botAPI, chatId: values.chatId, sendSilently: values.sendSilently, + botUsername: values.botUsername, }, }); @@ -147,6 +154,24 @@ const NotificationsTelegram: React.FC = () => { +
+ +
+
+ +
+ {errors.botUsername && touched.botUsername && ( +
{errors.botUsername}
+ )} +
+
+
+ +
+
+ +
+ {errors.telegramChatId && touched.telegramChatId && ( +
{errors.telegramChatId}
+ )} +
+
+
+ +
+ +
+
diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 4b2b5932..695fbbe8 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -306,6 +306,7 @@ "components.Settings.Notifications.authPass": "SMTP Password", "components.Settings.Notifications.authUser": "SMTP Username", "components.Settings.Notifications.botAPI": "Bot Authentication Token", + "components.Settings.Notifications.botUsername": "Bot Username", "components.Settings.Notifications.chatId": "Chat ID", "components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.", "components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved successfully!", @@ -718,9 +719,15 @@ "components.UserProfile.UserSettings.UserNotificationSettings.plexuser": "Plex User", "components.UserProfile.UserSettings.UserNotificationSettings.save": "Save Changes", "components.UserProfile.UserSettings.UserNotificationSettings.saving": "Saving…", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Send Silently", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "Send telegram notifications silently", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "Telegram Chat ID", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTip": "The Chat ID can be aquired by adding @get_id_bot to the chat.", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "Start a chat by clicking here. Then get the group Chat ID by adding @get_id_bot to that chat and send /my_id to the chat", "components.UserProfile.UserSettings.UserNotificationSettings.toastSettingsFailure": "Something went wrong while saving settings.", "components.UserProfile.UserSettings.UserNotificationSettings.toastSettingsSuccess": "Settings successfully saved!", "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "You must provide a valid Discord user ID", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "You must provide a valid Telegram Chat ID", "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Confirm Password", "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Current Password", "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "New Password",