diff --git a/overseerr-api.yml b/overseerr-api.yml index 6fe132187..627836adc 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -855,6 +855,22 @@ components: properties: webhookUrl: type: string + TelegramSettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + botAPI: + type: string + chatId: + type: string NotificationEmailSettings: type: object properties: @@ -1635,6 +1651,52 @@ paths: responses: '204': description: Test notification attempted + /settings/notifications/telegram: + get: + summary: Return current telegram notification settings + description: Returns current telegram notification settings in JSON format + tags: + - settings + responses: + '200': + description: Returned telegram settings + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + post: + summary: Update telegram notification settings + description: Update current telegram notification settings with provided values + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + /settings/notifications/telegram/test: + post: + summary: Test the provided telegram settings + description: Sends a test notification to the telegram agent + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + responses: + '204': + description: Test notification attempted /settings/notifications/slack: get: summary: Return current slack notification settings diff --git a/server/index.ts b/server/index.ts index 76371a94c..eaf36833e 100644 --- a/server/index.ts +++ b/server/index.ts @@ -17,6 +17,7 @@ import { startJobs } from './job/schedule'; import notificationManager from './lib/notifications'; import DiscordAgent from './lib/notifications/agents/discord'; import EmailAgent from './lib/notifications/agents/email'; +import TelegramAgent from './lib/notifications/agents/telegram'; import { getAppVersion } from './utils/appVersion'; import SlackAgent from './lib/notifications/agents/slack'; @@ -47,6 +48,7 @@ app new DiscordAgent(), new EmailAgent(), new SlackAgent(), + new TelegramAgent(), ]); // Start Jobs diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts new file mode 100644 index 000000000..9d9e12705 --- /dev/null +++ b/server/lib/notifications/agents/telegram.ts @@ -0,0 +1,128 @@ +import axios from 'axios'; +import { Notification } from '..'; +import logger from '../../../logger'; +import { getSettings, NotificationAgentTelegram } from '../../settings'; +import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; + +interface TelegramPayload { + text: string; + parse_mode: string; + chat_id: string; +} + +class TelegramAgent + extends BaseAgent + implements NotificationAgent { + private baseUrl = 'https://api.telegram.org/'; + + protected getSettings(): NotificationAgentTelegram { + if (this.settings) { + return this.settings; + } + + const settings = getSettings(); + + return settings.notifications.agents.telegram; + } + + // TODO: Add checking for type here once we add notification type filters for agents + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public shouldSend(_type: Notification): boolean { + if ( + this.getSettings().enabled && + this.getSettings().options.botAPI && + this.getSettings().options.chatId + ) { + return true; + } + + return false; + } + + private escapeText(text: string | undefined): string { + return text ? text.replace(/[_*[\]()~>#+=|{}.!-]/gi, (x) => '\\' + x) : ''; + } + + private buildMessage( + type: Notification, + payload: NotificationPayload + ): string { + const settings = getSettings(); + let message = ''; + + const title = this.escapeText(payload.subject); + const plot = this.escapeText(payload.message); + const user = this.escapeText(payload.notifyUser.username); + + /* eslint-disable no-useless-escape */ + switch (type) { + case Notification.MEDIA_PENDING: + message += `\*New Request\*\n`; + message += `${title}\n\n`; + message += `${plot}\n\n`; + message += `\*Requested By\*\n${user}\n\n`; + message += `\*Status\*\nPending Approval\n`; + + break; + case Notification.MEDIA_APPROVED: + message += `\*Request Approved\*\n`; + message += `${title}\n\n`; + message += `${plot}\n\n`; + message += `\*Requested By\*\n${user}\n\n`; + message += `\*Status\*\nProcessing Request\n`; + + break; + case Notification.MEDIA_AVAILABLE: + message += `\*Now available\\!\*\n`; + message += `${title}\n\n`; + message += `${plot}\n\n`; + message += `\*Requested By\*\n${user}\n\n`; + message += `\*Status\*\nAvailable\n`; + + break; + case Notification.TEST_NOTIFICATION: + message += `\*Test Notification\*\n`; + message += `${title}\n\n`; + message += `${plot}\n\n`; + message += `\*Requested By\*\n${user}\n`; + + break; + } + + if (settings.main.applicationUrl && payload.media) { + const actionUrl = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`; + message += `\[Open in Overseerr\]\(${actionUrl}\)`; + } + /* eslint-enable */ + + return message; + } + + public async send( + type: Notification, + payload: NotificationPayload + ): Promise { + logger.debug('Sending telegram notification', { label: 'Notifications' }); + try { + const endpoint = `${this.baseUrl}bot${ + this.getSettings().options.botAPI + }/sendMessage`; + + await axios.post(endpoint, { + text: this.buildMessage(type, payload), + parse_mode: 'MarkdownV2', + chat_id: `${this.getSettings().options.chatId}`, + } as TelegramPayload); + + return true; + } catch (e) { + logger.error('Error sending Telegram notification', { + label: 'Notifications', + message: e.message, + }); + return false; + } + } +} + +export default TelegramAgent; diff --git a/server/lib/settings.ts b/server/lib/settings.ts index c3cdfee66..1d25be5d2 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -84,10 +84,18 @@ export interface NotificationAgentEmail extends NotificationAgentConfig { }; } +export interface NotificationAgentTelegram extends NotificationAgentConfig { + options: { + botAPI: string; + chatId: string; + }; +} + interface NotificationAgents { email: NotificationAgentEmail; discord: NotificationAgentDiscord; slack: NotificationAgentSlack; + telegram: NotificationAgentTelegram; } interface NotificationSettings { @@ -156,6 +164,14 @@ class Settings { webhookUrl: '', }, }, + telegram: { + enabled: false, + types: 0, + options: { + botAPI: '', + chatId: '', + }, + }, }, }, }; diff --git a/server/routes/settings.ts b/server/routes/settings.ts index 4f22fe01f..ba9b91bc1 100644 --- a/server/routes/settings.ts +++ b/server/routes/settings.ts @@ -25,6 +25,7 @@ import { Notification } from '../lib/notifications'; import DiscordAgent from '../lib/notifications/agents/discord'; import EmailAgent from '../lib/notifications/agents/email'; import SlackAgent from '../lib/notifications/agents/slack'; +import TelegramAgent from '../lib/notifications/agents/telegram'; const settingsRoutes = Router(); @@ -503,6 +504,40 @@ settingsRoutes.post('/notifications/slack/test', (req, res, next) => { return res.status(204).send(); }); +settingsRoutes.get('/notifications/telegram', (_req, res) => { + const settings = getSettings(); + + res.status(200).json(settings.notifications.agents.telegram); +}); + +settingsRoutes.post('/notifications/telegram', (req, res) => { + const settings = getSettings(); + + settings.notifications.agents.telegram = req.body; + settings.save(); + + res.status(200).json(settings.notifications.agents.telegram); +}); + +settingsRoutes.post('/notifications/telegram/test', (req, res, next) => { + if (!req.user) { + return next({ + status: 500, + message: 'User information missing from request', + }); + } + + const telegramAgent = new TelegramAgent(req.body); + telegramAgent.send(Notification.TEST_NOTIFICATION, { + notifyUser: req.user, + subject: 'Test Notification', + message: + 'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?', + }); + + return res.status(204).send(); +}); + settingsRoutes.get('/notifications/email', (_req, res) => { const settings = getSettings(); diff --git a/src/assets/extlogos/telegram.svg b/src/assets/extlogos/telegram.svg new file mode 100644 index 000000000..d10e5c88b --- /dev/null +++ b/src/assets/extlogos/telegram.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Settings/Notifications/NotificationsDiscord.tsx b/src/components/Settings/Notifications/NotificationsDiscord.tsx index 6bf001e7b..2c7b23f49 100644 --- a/src/components/Settings/Notifications/NotificationsDiscord.tsx +++ b/src/components/Settings/Notifications/NotificationsDiscord.tsx @@ -89,7 +89,7 @@ const NotificationsDiscord: React.FC = () => {