Skip to content
Open
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
4 changes: 4 additions & 0 deletions docs/using-overseerr/notifications/telegram.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ At the end of the bot creation process, [@BotFather](https://telegram.me/botfath

To obtain your chat ID, simply create a new group chat, add [@get_id_bot](https://telegram.me/get_id_bot), and issue the `/my_id` command.

### Message Thread ID (optional)

Optionally send messages to a specific thread. To obtain your message thread ID, you must have threads enabled on your group. Click on the thread you want and copy the last number after the last '/' in the url.

### Send Silently (optional)

Optionally, notifications can be sent silently. Silent notifications send messages without notification sounds.
15 changes: 8 additions & 7 deletions docs/using-overseerr/notifications/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ Customize the JSON payload to suit your needs. Overseerr provides several [templ

These variables are for the target recipient of the notification.

| Variable | Value |
| ---------------------------------------- | ------------------------------------------------------------- |
| `{{notifyuser_username}}` | The target notification recipient's username |
| `{{notifyuser_email}}` | The target notification recipient's email address |
| `{{notifyuser_avatar}}` | The target notification recipient's avatar URL |
| `{{notifyuser_settings_discordId}}` | The target notification recipient's Discord ID (if set) |
| `{{notifyuser_settings_telegramChatId}}` | The target notification recipient's Telegram Chat ID (if set) |
| Variable | Value |
| ------------------------------------------------- | ----------------------------------------------------------------------- |
| `{{notifyuser_username}}` | The target notification recipient's username |
| `{{notifyuser_email}}` | The target notification recipient's email address |
| `{{notifyuser_avatar}}` | The target notification recipient's avatar URL |
| `{{notifyuser_settings_discordId}}` | The target notification recipient's Discord ID (if set) |
| `{{notifyuser_settings_telegramChatId}}` | The target notification recipient's Telegram Chat ID (if set) |
| `{{notifyuser_settings_telegramMessageThreadId}}` | The target notification recipient's Telegram Message Thread ID (if set) |

{% hint style="info" %}
The `notifyuser` variables are not defined for the following request notification types, as they are intended for application administrators rather than end users:
Expand Down
3 changes: 3 additions & 0 deletions server/entity/UserSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export class UserSettings {
@Column({ nullable: true })
public telegramSendSilently?: boolean;

@Column({ nullable: true })
public telegramMessageThreadId?: string;

@Column({ nullable: true })
public watchlistSyncMovies?: boolean;

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 @@ -33,6 +33,7 @@ export interface UserSettingsNotificationsResponse {
telegramBotUsername?: string;
telegramChatId?: string;
telegramSendSilently?: boolean;
telegramMessageThreadId?: string;
webPushEnabled?: boolean;
notificationTypes: Partial<NotificationAgentTypes>;
}
21 changes: 21 additions & 0 deletions server/lib/notifications/agents/telegram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface TelegramMessagePayload {
parse_mode: string;
chat_id: string;
disable_notification: boolean;
message_thread_id?: number;
}

interface TelegramPhotoPayload {
Expand All @@ -27,6 +28,7 @@ interface TelegramPhotoPayload {
parse_mode: string;
chat_id: string;
disable_notification: boolean;
message_thread_id?: number;
}

class TelegramAgent
Expand Down Expand Up @@ -179,6 +181,11 @@ class TelegramAgent
...notificationPayload,
chat_id: settings.options.chatId,
disable_notification: !!settings.options.sendSilently,
...(settings.options.messageThreadId
? {
message_thread_id: Number(settings.options.messageThreadId),
}
: {}),
} as TelegramMessagePayload | TelegramPhotoPayload);
} catch (e) {
logger.error('Error sending Telegram notification', {
Expand Down Expand Up @@ -215,6 +222,13 @@ class TelegramAgent
chat_id: payload.notifyUser.settings.telegramChatId,
disable_notification:
!!payload.notifyUser.settings.telegramSendSilently,
...(payload.notifyUser.settings.telegramMessageThreadId
? {
message_thread_id: Number(
payload.notifyUser.settings.telegramMessageThreadId
),
}
: {}),
} as TelegramMessagePayload | TelegramPhotoPayload);
} catch (e) {
logger.error('Error sending Telegram notification', {
Expand Down Expand Up @@ -261,6 +275,13 @@ class TelegramAgent
...notificationPayload,
chat_id: user.settings.telegramChatId,
disable_notification: !!user.settings?.telegramSendSilently,
...(user.settings?.telegramMessageThreadId
? {
message_thread_id: Number(
user.settings.telegramMessageThreadId
),
}
: {}),
} as TelegramMessagePayload | TelegramPhotoPayload);
} catch (e) {
logger.error('Error sending Telegram notification', {
Expand Down
7 changes: 7 additions & 0 deletions server/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ export interface NotificationAgentTelegram extends NotificationAgentConfig {
botAPI: string;
chatId: string;
sendSilently: boolean;
/**
* Optional Telegram message thread ID (for discussion topics in supergroups)
* If provided, Overseerr will send all system notifications to
* the specified thread within the configured chat.
*/
messageThreadId?: string;
};
}

Expand Down Expand Up @@ -359,6 +365,7 @@ class Settings {
botAPI: '',
chatId: '',
sendSilently: false,
messageThreadId: '',
},
},
pushbullet: {
Expand Down
5 changes: 5 additions & 0 deletions server/routes/user/usersettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,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 ?? {},
Expand Down Expand Up @@ -320,6 +321,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,
});
Expand All @@ -332,6 +334,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(
{},
Expand All @@ -350,6 +354,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,
});
Expand Down
34 changes: 34 additions & 0 deletions src/components/Settings/Notifications/NotificationsTelegram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const messages = defineMessages({
toastTelegramTestFailed: 'Telegram test notification failed to send.',
sendSilently: 'Send Silently',
sendSilentlyTip: 'Send notifications with no sound',
messageThreadId: 'Message Thread ID',
messageThreadIdTip: 'Optional message thread ID',
});

const NotificationsTelegram = () => {
Expand Down Expand Up @@ -64,6 +66,9 @@ const NotificationsTelegram = () => {
/^-?\d+$/,
intl.formatMessage(messages.validationChatIdRequired)
),
messageThreadId: Yup.string()
.nullable()
.matches(/^\d*$/, intl.formatMessage(messages.validationChatIdRequired)),
Copy link

Copilot AI Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This validation for 'messageThreadId' reuses the chat ID error message. You should define a specific validation message for the thread ID (e.g., validationMessageThreadIdRequired) to provide clear feedback.

Suggested change
.matches(/^\d*$/, intl.formatMessage(messages.validationChatIdRequired)),
.matches(/^\d*$/, intl.formatMessage(messages.validationMessageThreadIdRequired)),

Copilot uses AI. Check for mistakes.
});

if (!data && !error) {
Expand All @@ -79,6 +84,7 @@ const NotificationsTelegram = () => {
botAPI: data?.options.botAPI,
chatId: data?.options.chatId,
sendSilently: data?.options.sendSilently,
messageThreadId: data?.options.messageThreadId,
}}
validationSchema={NotificationsTelegramSchema}
onSubmit={async (values) => {
Expand All @@ -91,6 +97,7 @@ const NotificationsTelegram = () => {
chatId: values.chatId,
sendSilently: values.sendSilently,
botUsername: values.botUsername,
messageThreadId: values.messageThreadId,
},
});

Expand Down Expand Up @@ -139,6 +146,7 @@ const NotificationsTelegram = () => {
chatId: values.chatId,
sendSilently: values.sendSilently,
botUsername: values.botUsername,
messageThreadId: values.messageThreadId,
},
});

Expand Down Expand Up @@ -285,6 +293,32 @@ const NotificationsTelegram = () => {
)}
</div>
</div>
<div className="form-row">
<label htmlFor="messageThreadId" className="text-label">
{intl.formatMessage(messages.messageThreadId)}
<span className="label-tip">
{intl.formatMessage(messages.messageThreadIdTip)}
</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
id="messageThreadId"
name="messageThreadId"
type="text"
autoComplete="off"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
/>
</div>
{errors.messageThreadId &&
touched.messageThreadId &&
typeof errors.messageThreadId === 'string' && (
<div className="error">{errors.messageThreadId}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="sendSilently" className="checkbox-label">
<span>{intl.formatMessage(messages.sendSilently)}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const messages = defineMessages({
sendSilently: 'Send Silently',
sendSilentlyDescription: 'Send notifications with no sound',
validationTelegramChatId: 'You must provide a valid chat ID',
messageThreadId: 'Message Thread ID',
messageThreadIdTip: 'Optional message thread ID for topics',
});

const UserTelegramSettings = () => {
Expand Down Expand Up @@ -50,6 +52,9 @@ const UserTelegramSettings = () => {
/^-?\d+$/,
intl.formatMessage(messages.validationTelegramChatId)
),
telegramMessageThreadId: Yup.string()
.nullable()
.matches(/^\d*$/, intl.formatMessage(messages.validationTelegramChatId)),
Copy link

Copilot AI Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation message for 'telegramMessageThreadId' reuses the chat ID error key and message, which may confuse users. Consider adding a dedicated validation message key (e.g., validationTelegramThreadId) with an appropriate message.

Suggested change
.matches(/^\d*$/, intl.formatMessage(messages.validationTelegramChatId)),
.matches(/^\d*$/, intl.formatMessage(messages.validationTelegramThreadId)),

Copilot uses AI. Check for mistakes.
});

if (!data && !error) {
Expand All @@ -61,6 +66,7 @@ const UserTelegramSettings = () => {
initialValues={{
telegramChatId: data?.telegramChatId,
telegramSendSilently: data?.telegramSendSilently,
telegramMessageThreadId: data?.telegramMessageThreadId,
types: data?.notificationTypes.telegram ?? 0,
}}
validationSchema={UserNotificationsTelegramSchema}
Expand All @@ -75,6 +81,7 @@ const UserTelegramSettings = () => {
pushoverUserKey: data?.pushoverUserKey,
telegramChatId: values.telegramChatId,
telegramSendSilently: values.telegramSendSilently,
telegramMessageThreadId: values.telegramMessageThreadId,
notificationTypes: {
telegram: values.types,
},
Expand Down Expand Up @@ -149,6 +156,30 @@ const UserTelegramSettings = () => {
)}
</div>
</div>
<div className="form-row">
<label htmlFor="telegramMessageThreadId" className="text-label">
{intl.formatMessage(messages.messageThreadId)}
<span className="label-tip">
{intl.formatMessage(messages.messageThreadIdTip)}
</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
id="telegramMessageThreadId"
name="telegramMessageThreadId"
type="text"
/>
</div>
{errors.telegramMessageThreadId &&
touched.telegramMessageThreadId &&
typeof errors.telegramMessageThreadId === 'string' && (
<div className="error">
{errors.telegramMessageThreadId}
</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="telegramSendSilently" className="checkbox-label">
{intl.formatMessage(messages.sendSilently)}
Expand Down