Files
channels-seerr/server/lib/notifications/agents/telegram.ts
fallenbagel ddf347994a chore(deps): update dependencies and fix security vulnerabilities (#2342)
* chore(deps): update dependencies and fix security vulnerabilities

Update TypeScript 4.9 → 5.4. Update Zod 3 → 4. Update nodemailer 6 → 7. Update @typescript-eslint
packages to v7. Update xml2js, undici, lodash, axios, swr, winston- Add pnpm.overrides for
transitive dependency vulnerabilities

* chore: fix import ordering for TypeScript 5.4 compatibility

prettier-plugin-organize-imports behaves differently with TypeScript 5.4 vs 4.9, causing CI
formatting checks to fail. This reformats imports to match the ordering expected by the plugin with
the upgraded TS version.
2026-01-27 19:00:42 +01:00

295 lines
8.8 KiB
TypeScript

import { IssueStatus, IssueTypeName } from '@server/constants/issue';
import { MediaStatus } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
import type { NotificationAgentTelegram } from '@server/lib/settings';
import { NotificationAgentKey, getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import {
Notification,
hasNotificationType,
shouldSendAdminNotification,
} from '..';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
interface TelegramMessagePayload {
text: string;
parse_mode: string;
chat_id: string;
message_thread_id: string;
disable_notification: boolean;
}
interface TelegramPhotoPayload {
photo: string;
caption: string;
parse_mode: string;
chat_id: string;
message_thread_id: string;
disable_notification: boolean;
}
class TelegramAgent
extends BaseAgent<NotificationAgentTelegram>
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<TelegramMessagePayload | TelegramPhotoPayload> {
const settings = getSettings();
const { applicationUrl, applicationTitle } = settings.main;
const { embedPoster } = settings.notifications.agents.telegram;
/* eslint-disable no-useless-escape */
let message = `\*${this.escapeText(
payload.event ? `${payload.event} - ${payload.subject}` : payload.subject
)}\*`;
if (payload.message) {
message += `\n${this.escapeText(payload.message)}`;
}
if (payload.request) {
message += `\n\n\*Requested By:\* ${this.escapeText(
payload.request?.requestedBy.displayName
)}`;
let status = '';
switch (type) {
case Notification.MEDIA_AUTO_REQUESTED:
status =
payload.media?.status === MediaStatus.PENDING
? 'Pending Approval'
: 'Processing';
break;
case Notification.MEDIA_PENDING:
status = 'Pending Approval';
break;
case Notification.MEDIA_APPROVED:
case Notification.MEDIA_AUTO_APPROVED:
status = 'Processing';
break;
case Notification.MEDIA_AVAILABLE:
status = 'Available';
break;
case Notification.MEDIA_DECLINED:
status = 'Declined';
break;
case Notification.MEDIA_FAILED:
status = 'Failed';
break;
}
if (status) {
message += `\n\*Request Status:\* ${status}`;
}
} else if (payload.comment) {
message += `\n\n\*Comment from ${this.escapeText(
payload.comment.user.displayName
)}:\* ${this.escapeText(payload.comment.message)}`;
} else if (payload.issue) {
message += `\n\n\*Reported By:\* ${this.escapeText(
payload.issue.createdBy.displayName
)}`;
message += `\n\*Issue Type:\* ${IssueTypeName[payload.issue.issueType]}`;
message += `\n\*Issue Status:\* ${
payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved'
}`;
}
for (const extra of payload.extra ?? []) {
message += `\n\*${extra.name}:\* ${extra.value}`;
}
const url = applicationUrl
? payload.issue
? `${applicationUrl}/issues/${payload.issue.id}`
: payload.media
? `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`
: undefined
: undefined;
if (url) {
message += `\n\n\[View ${
payload.issue ? 'Issue' : 'Media'
} in ${this.escapeText(applicationTitle)}\]\(${url}\)`;
}
/* eslint-enable */
return embedPoster && payload.image
? {
photo: payload.image,
caption: message,
parse_mode: 'MarkdownV2',
}
: {
text: message,
parse_mode: 'MarkdownV2',
};
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
const settings = this.getSettings();
const endpoint = `${this.baseUrl}bot${settings.options.botAPI}/${
settings.embedPoster && payload.image ? 'sendPhoto' : 'sendMessage'
}`;
const notificationPayload = this.getNotificationPayload(type, payload);
// Send system notification
if (
payload.notifySystem &&
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,
message_thread_id: settings.options.messageThreadId,
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) {
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,
message_thread_id:
payload.notifyUser.settings.telegramMessageThreadId,
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;
}
}
}
if (payload.notifyAdmin) {
const userRepository = getRepository(User);
const users = await userRepository.find();
await Promise.all(
users
.filter(
(user) =>
user.settings?.hasNotificationType(
NotificationAgentKey.TELEGRAM,
type
) && shouldSendAdminNotification(type, user, payload)
)
.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,
message_thread_id: user.settings.telegramMessageThreadId,
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;