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 { Permission } from '../../permissions'; import { getSettings, NotificationAgentKey, NotificationAgentTelegram, } from '../../settings'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; interface TelegramMessagePayload { text: string; parse_mode: string; chat_id: string; disable_notification: boolean; } interface TelegramPhotoPayload { photo: string; caption: string; parse_mode: string; chat_id: string; disable_notification: boolean; } 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; } public shouldSend(): boolean { const settings = this.getSettings(); if (settings.enabled && settings.options.botAPI) { return true; } return false; } private escapeText(text: string | undefined): string { return text ? text.replace(/[_*[\]()~>#+=|{}.!-]/gi, (x) => '\\' + x) : ''; } private getNotificationPayload( type: Notification, payload: NotificationPayload ): Partial { const settings = getSettings(); let message = ''; const title = this.escapeText(payload.subject); const plot = this.escapeText(payload.message); const user = this.escapeText(payload.request?.requestedBy.displayName); const applicationTitle = this.escapeText(settings.main.applicationTitle); /* eslint-disable no-useless-escape */ switch (type) { case Notification.MEDIA_PENDING: message += `\*New ${ payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' } Request\*`; message += `\n\n\*${title}\*`; if (plot) { message += `\n${plot}`; } message += `\n\n\*Requested By\*\n${user}`; message += `\n\n\*Status\*\nPending Approval`; break; case Notification.MEDIA_APPROVED: message += `\*${ payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' } Request Approved\*`; message += `\n\n\*${title}\*`; if (plot) { message += `\n${plot}`; } message += `\n\n\*Requested By\*\n${user}`; message += `\n\n\*Status\*\nProcessing`; break; case Notification.MEDIA_AUTO_APPROVED: message += `\*${ payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' } Request Automatically Approved\*`; message += `\n\n\*${title}\*`; if (plot) { message += `\n${plot}`; } message += `\n\n\*Requested By\*\n${user}`; message += `\n\n\*Status\*\nProcessing`; break; case Notification.MEDIA_AVAILABLE: message += `\*${ payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' } Now Available\*`; message += `\n\n\*${title}\*`; if (plot) { message += `\n${plot}`; } message += `\n\n\*Requested By\*\n${user}`; message += `\n\n\*Status\*\nAvailable`; break; case Notification.MEDIA_DECLINED: message += `\*${ payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' } Request Declined\*`; message += `\n\n\*${title}\*`; if (plot) { message += `\n${plot}`; } message += `\n\n\*Requested By\*\n${user}`; message += `\n\n\*Status\*\nDeclined`; break; case Notification.MEDIA_FAILED: message += `\*Failed ${ payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' } Request\*`; message += `\n\n\*${title}\*`; if (plot) { message += `\n${plot}`; } message += `\n\n\*Requested By\*\n${user}`; message += `\n\n\*Status\*\nFailed`; break; case Notification.TEST_NOTIFICATION: message += `\*Test Notification\*`; message += `\n\n${plot}`; break; } for (const extra of payload.extra ?? []) { message += `\n\n\*${extra.name}\*\n${extra.value}`; } if (settings.main.applicationUrl && payload.media) { const actionUrl = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`; message += `\n\n\[Open in ${applicationTitle}\]\(${actionUrl}\)`; } /* eslint-enable */ return payload.image ? { photo: payload.image, caption: message, parse_mode: 'MarkdownV2', } : { text: message, parse_mode: 'MarkdownV2', }; } public async send( type: Notification, 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) && settings.options.chatId ) { logger.debug('Sending Telegram notification', { label: 'Notifications', type: Notification[type], subject: payload.subject, }); try { 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', 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.TELEGRAM, type ) && payload.notifyUser.settings?.telegramChatId && payload.notifyUser.settings.telegramChatId !== settings.options.chatId ) { logger.debug('Sending Telegram notification', { label: 'Notifications', recipient: payload.notifyUser.displayName, type: Notification[type], subject: payload.subject, }); try { 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', 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.TELEGRAM, 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?.telegramChatId && user.settings.telegramChatId !== settings.options.chatId ) { logger.debug('Sending Telegram notification', { label: 'Notifications', recipient: user.displayName, type: Notification[type], subject: payload.subject, }); try { 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', recipient: user.displayName, type: Notification[type], subject: payload.subject, errorMessage: e.message, response: e.response?.data, }); return false; } } }) ); } return true; } } export default TelegramAgent;