diff --git a/cypress/config/settings.cypress.json b/cypress/config/settings.cypress.json index 69c8db42c..f45bcbc0e 100644 --- a/cypress/config/settings.cypress.json +++ b/cypress/config/settings.cypress.json @@ -100,6 +100,7 @@ "options": { "botAPI": "", "chatId": "", + "messageThreadId": "", "sendSilently": false } }, diff --git a/overseerr-api.yml b/overseerr-api.yml index dc59b7afe..ac76f6a7e 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -1338,6 +1338,8 @@ components: type: string chatId: type: string + messageThreadId: + type: string sendSilently: type: boolean PushbulletSettings: @@ -1821,6 +1823,9 @@ components: telegramChatId: type: string nullable: true + telegramMessageThreadId: + type: string + nullable: true telegramSendSilently: type: boolean nullable: true diff --git a/server/entity/UserSettings.ts b/server/entity/UserSettings.ts index d5a7555a8..82671fe3b 100644 --- a/server/entity/UserSettings.ts +++ b/server/entity/UserSettings.ts @@ -60,6 +60,9 @@ export class UserSettings { @Column({ nullable: true }) public telegramChatId?: string; + @Column({ nullable: true }) + public telegramMessageThreadId?: string; + @Column({ nullable: true }) public telegramSendSilently?: boolean; diff --git a/server/interfaces/api/userSettingsInterfaces.ts b/server/interfaces/api/userSettingsInterfaces.ts index 43c567c7f..327764618 100644 --- a/server/interfaces/api/userSettingsInterfaces.ts +++ b/server/interfaces/api/userSettingsInterfaces.ts @@ -34,6 +34,7 @@ export interface UserSettingsNotificationsResponse { telegramEnabled?: boolean; telegramBotUsername?: string; telegramChatId?: string; + telegramMessageThreadId?: string; telegramSendSilently?: boolean; webPushEnabled?: boolean; notificationTypes: Partial; diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index a66f97100..db12b4947 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -17,6 +17,7 @@ interface TelegramMessagePayload { text: string; parse_mode: string; chat_id: string; + message_thread_id: string; disable_notification: boolean; } @@ -25,6 +26,7 @@ interface TelegramPhotoPayload { caption: string; parse_mode: string; chat_id: string; + message_thread_id: string; disable_notification: boolean; } @@ -182,6 +184,7 @@ class TelegramAgent body: JSON.stringify({ ...notificationPayload, chat_id: settings.options.chatId, + message_thread_id: settings.options.messageThreadId, disable_notification: !!settings.options.sendSilently, } as TelegramMessagePayload | TelegramPhotoPayload), }); @@ -233,6 +236,8 @@ class TelegramAgent body: JSON.stringify({ ...notificationPayload, chat_id: payload.notifyUser.settings.telegramChatId, + message_thread_id: + payload.notifyUser.settings.telegramMessageThreadId, disable_notification: !!payload.notifyUser.settings.telegramSendSilently, } as TelegramMessagePayload | TelegramPhotoPayload), @@ -296,6 +301,7 @@ class TelegramAgent body: JSON.stringify({ ...notificationPayload, chat_id: user.settings.telegramChatId, + message_thread_id: user.settings.telegramMessageThreadId, disable_notification: !!user.settings?.telegramSendSilently, } as TelegramMessagePayload | TelegramPhotoPayload), }); diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index f14e0eb69..f1c730223 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -213,6 +213,7 @@ export interface NotificationAgentTelegram extends NotificationAgentConfig { botUsername?: string; botAPI: string; chatId: string; + messageThreadId: string; sendSilently: boolean; }; } @@ -423,6 +424,7 @@ class Settings { options: { botAPI: '', chatId: '', + messageThreadId: '', sendSilently: false, }, }, diff --git a/server/migration/1734287582736-AddTelegramMessageThreadId.ts b/server/migration/1734287582736-AddTelegramMessageThreadId.ts new file mode 100644 index 000000000..94a76b99a --- /dev/null +++ b/server/migration/1734287582736-AddTelegramMessageThreadId.ts @@ -0,0 +1,33 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTelegramMessageThreadId1734287582736 + implements MigrationInterface +{ + name = 'AddTelegramMessageThreadId1734287582736'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "locale" varchar NOT NULL DEFAULT (''), "discoverRegion" varchar, "streamingRegion" varchar, "originalLanguage" varchar, "pgpKey" varchar, "discordId" varchar, "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "pushoverSound" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "notificationTypes" text, "userId" integer, "telegramMessageThreadId" varchar, CONSTRAINT "REL_986a2b6d3c05eb4091bb8066f7" 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", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId") SELECT "id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId" 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, "locale" varchar NOT NULL DEFAULT (''), "discoverRegion" varchar, "streamingRegion" varchar, "originalLanguage" varchar, "pgpKey" varchar, "discordId" varchar, "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "pushoverSound" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "notificationTypes" text, "userId" integer, CONSTRAINT "REL_986a2b6d3c05eb4091bb8066f7" 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", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId") SELECT "id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId" 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 e4c16b1ef..24ca976ba 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -323,6 +323,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>( telegramEnabled: settings.telegram.enabled, telegramBotUsername: settings.telegram.options.botUsername, telegramChatId: user.settings?.telegramChatId, + telegramMessageThreadId: user.settings?.telegramMessageThreadId, telegramSendSilently: user.settings?.telegramSendSilently, webPushEnabled: settings.webpush.enabled, notificationTypes: user.settings?.notificationTypes ?? {}, @@ -365,6 +366,7 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>( pushoverApplicationToken: req.body.pushoverApplicationToken, pushoverUserKey: req.body.pushoverUserKey, telegramChatId: req.body.telegramChatId, + telegramMessageThreadId: req.body.telegramMessageThreadId, telegramSendSilently: req.body.telegramSendSilently, notificationTypes: req.body.notificationTypes, }); @@ -377,6 +379,8 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>( user.settings.pushoverUserKey = req.body.pushoverUserKey; user.settings.pushoverSound = req.body.pushoverSound; user.settings.telegramChatId = req.body.telegramChatId; + user.settings.telegramMessageThreadId = + req.body.telegramMessageThreadId; user.settings.telegramSendSilently = req.body.telegramSendSilently; user.settings.notificationTypes = Object.assign( {}, @@ -395,6 +399,7 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>( pushoverUserKey: user.settings.pushoverUserKey, pushoverSound: user.settings.pushoverSound, telegramChatId: user.settings.telegramChatId, + telegramMessageThreadId: user.settings.telegramMessageThreadId, 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 53ee47877..6636c6b4c 100644 --- a/src/components/Settings/Notifications/NotificationsTelegram.tsx +++ b/src/components/Settings/Notifications/NotificationsTelegram.tsx @@ -23,8 +23,13 @@ const messages = defineMessages('components.Settings.Notifications', { chatId: 'Chat ID', chatIdTip: 'Start a chat with your bot, add @get_id_bot, and issue the /my_id command', + messageThreadId: 'Thread/Topic ID', + messageThreadIdTip: + "If your group-chat has topics enabled, you can specify a thread/topic's ID here", validationBotAPIRequired: 'You must provide a bot authorization token', validationChatIdRequired: 'You must provide a valid chat ID', + validationMessageThreadId: + 'The thread/topic ID must be a positive whole number', telegramsettingssaved: 'Telegram notification settings saved successfully!', telegramsettingsfailed: 'Telegram notification settings failed to save.', toastTelegramTestSending: 'Sending Telegram test notification…', @@ -64,6 +69,15 @@ const NotificationsTelegram = () => { /^-?\d+$/, intl.formatMessage(messages.validationChatIdRequired) ), + messageThreadId: Yup.string() + .when(['types'], { + is: (enabled: boolean, types: number) => enabled && !!types, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationMessageThreadId)), + otherwise: Yup.string().nullable(), + }) + .matches(/^\d+$/, intl.formatMessage(messages.validationMessageThreadId)), }); if (!data && !error) { @@ -78,6 +92,7 @@ const NotificationsTelegram = () => { botUsername: data?.options.botUsername, botAPI: data?.options.botAPI, chatId: data?.options.chatId, + messageThreadId: data?.options.messageThreadId, sendSilently: data?.options.sendSilently, }} validationSchema={NotificationsTelegramSchema} @@ -94,6 +109,7 @@ const NotificationsTelegram = () => { options: { botAPI: values.botAPI, chatId: values.chatId, + messageThreadId: values.messageThreadId, sendSilently: values.sendSilently, botUsername: values.botUsername, }, @@ -151,6 +167,7 @@ const NotificationsTelegram = () => { options: { botAPI: values.botAPI, chatId: values.chatId, + messageThreadId: values.messageThreadId, sendSilently: values.sendSilently, botUsername: values.botUsername, }, @@ -286,6 +303,28 @@ const NotificationsTelegram = () => { )} +
+ +
+
+ +
+ {errors.messageThreadId && + touched.messageThreadId && + typeof errors.messageThreadId === 'string' && ( +
{errors.messageThreadId}
+ )} +
+
+
+ +
+
+ +
+ {errors.telegramMessageThreadId && + touched.telegramMessageThreadId && + typeof errors.telegramMessageThreadId === 'string' && ( +
+ {errors.telegramMessageThreadId} +
+ )} +
+