diff --git a/packages/backend/src/services/notification.service.ts b/packages/backend/src/services/notification.service.ts index b5b9670..006f408 100644 --- a/packages/backend/src/services/notification.service.ts +++ b/packages/backend/src/services/notification.service.ts @@ -1,604 +1,887 @@ -import axios, { AxiosRequestConfig } from 'axios'; -import { NotificationSettingsRepository } from '../repositories/notification.repository'; +import axios, { AxiosRequestConfig } from "axios"; +import { NotificationSettingsRepository } from "../repositories/notification.repository"; import { - NotificationSetting, - NotificationEvent, - NotificationPayload, - WebhookConfig, - EmailConfig, - TelegramConfig, - NotificationChannelConfig, - NotificationChannelType -} from '../types/notification.types'; -import * as nodemailer from 'nodemailer'; -import Mail from 'nodemailer/lib/mailer'; -import i18next, { defaultLng, supportedLngs } from '../i18n'; // Import supportedLngs -import { settingsService } from './settings.service'; -import { formatInTimeZone } from 'date-fns-tz'; // NEW: Import timezone formatting + NotificationSetting, + NotificationEvent, + NotificationPayload, + WebhookConfig, + EmailConfig, + TelegramConfig, + NotificationChannelConfig, + NotificationChannelType, +} from "../types/notification.types"; +import * as nodemailer from "nodemailer"; +import Mail from "nodemailer/lib/mailer"; +import i18next, { defaultLng, supportedLngs } from "../i18n"; +import { settingsService } from "./settings.service"; +import { formatInTimeZone } from "date-fns-tz"; - -const testSubjectKey = 'testNotification.subject'; -const testEmailBodyKey = 'testNotification.email.body'; -const testEmailBodyHtmlKey = 'testNotification.email.bodyHtml'; -const testWebhookDetailsKey = 'testNotification.webhook.detailsMessage'; -const testTelegramDetailsKey = 'testNotification.telegram.detailsMessage'; -const testTelegramBodyTemplateKey = 'testNotification.telegram.bodyTemplate'; +const testSubjectKey = "testNotification.subject"; +const testEmailBodyKey = "testNotification.email.body"; +const testEmailBodyHtmlKey = "testNotification.email.bodyHtml"; +const testWebhookDetailsKey = "testNotification.webhook.detailsMessage"; +const testTelegramDetailsKey = "testNotification.telegram.detailsMessage"; +const testTelegramBodyTemplateKey = "testNotification.telegram.bodyTemplate"; export class NotificationService { - private repository: NotificationSettingsRepository; + private repository: NotificationSettingsRepository; - constructor() { - this.repository = new NotificationSettingsRepository(); + constructor() { + this.repository = new NotificationSettingsRepository(); + } + + async getAllSettings(): Promise { + return this.repository.getAll(); + } + + async getSettingById(id: number): Promise { + return this.repository.getById(id); + } + + async createSetting( + settingData: Omit + ): Promise { + return this.repository.create(settingData); + } + + async updateSetting( + id: number, + settingData: Partial< + Omit + > + ): Promise { + return this.repository.update(id, settingData); + } + + async deleteSetting(id: number): Promise { + return this.repository.delete(id); + } + + async testSetting( + channelType: NotificationChannelType, + config: NotificationChannelConfig + ): Promise<{ success: boolean; message: string }> { + switch (channelType) { + case "email": + return this._testEmailSetting(config as EmailConfig); + case "webhook": + return this._testWebhookSetting(config as WebhookConfig); + case "telegram": + return this._testTelegramSetting(config as TelegramConfig); + default: + console.warn(`[通知测试] 不支持的测试渠道类型: ${channelType}`); + return { + success: false, + message: `不支持测试此渠道类型 (${channelType})`, + }; + } + } + + private async _testEmailSetting( + config: EmailConfig + ): Promise<{ success: boolean; message: string }> { + console.log("[通知测试 - 邮件] 开始测试..."); + if (!config.to || !config.smtpHost || !config.smtpPort || !config.from) { + console.error("[通知测试 - 邮件] 缺少必要的配置。"); + return { + success: false, + message: + "测试邮件失败:缺少必要的 SMTP 配置信息 (收件人, 主机, 端口, 发件人)。", + }; } - async getAllSettings(): Promise { - return this.repository.getAll(); + let userLang = defaultLng; + try { + const langSetting = await settingsService.getSetting("language"); + if (langSetting && supportedLngs.includes(langSetting)) { + userLang = langSetting; + } + console.log(`[通知测试 - 邮件] 使用语言: ${userLang}`); + } catch (error) { + console.error( + `[通知测试 - 邮件] 获取语言设置时出错,使用默认 (${defaultLng}):`, + error + ); } - async getSettingById(id: number): Promise { - return this.repository.getById(id); - } - - async createSetting(settingData: Omit): Promise { - return this.repository.create(settingData); - } - - async updateSetting(id: number, settingData: Partial>): Promise { - return this.repository.update(id, settingData); - } - - async deleteSetting(id: number): Promise { - return this.repository.delete(id); - } - - - async testSetting(channelType: NotificationChannelType, config: NotificationChannelConfig): Promise<{ success: boolean; message: string }> { - switch (channelType) { - case 'email': - return this._testEmailSetting(config as EmailConfig); - case 'webhook': - return this._testWebhookSetting(config as WebhookConfig); - case 'telegram': - return this._testTelegramSetting(config as TelegramConfig); - default: - console.warn(`[通知测试] 不支持的测试渠道类型: ${channelType}`); - return { success: false, message: `不支持测试此渠道类型 (${channelType})` }; - } - } - - private async _testEmailSetting(config: EmailConfig): Promise<{ success: boolean; message: string }> { - console.log('[通知测试 - 邮件] 开始测试...'); - if (!config.to || !config.smtpHost || !config.smtpPort || !config.from) { - console.error('[通知测试 - 邮件] 缺少必要的配置。'); - return { success: false, message: '测试邮件失败:缺少必要的 SMTP 配置信息 (收件人, 主机, 端口, 发件人)。' }; - } - - let userLang = defaultLng; - try { - const langSetting = await settingsService.getSetting('language'); - if (langSetting && supportedLngs.includes(langSetting)) { - userLang = langSetting; + const transporterOptions = { + host: config.smtpHost, + port: config.smtpPort, + secure: config.smtpSecure ?? true, + auth: + config.smtpUser || config.smtpPass + ? { + user: config.smtpUser, + pass: config.smtpPass, } - console.log(`[通知测试 - 邮件] 使用语言: ${userLang}`); - } catch (error) { - console.error(`[通知测试 - 邮件] 获取语言设置时出错,使用默认 (${defaultLng}):`, error); - } + : undefined, + }; - const transporterOptions = { - host: config.smtpHost, - port: config.smtpPort, - secure: config.smtpSecure ?? true, - auth: (config.smtpUser || config.smtpPass) ? { - user: config.smtpUser, - pass: config.smtpPass, - } : undefined, - }; + const transporter = nodemailer.createTransport(transporterOptions); - const transporter = nodemailer.createTransport(transporterOptions); + const eventDisplayName = i18next.t(`event.SETTINGS_UPDATED`, { + lng: userLang, + defaultValue: "SETTINGS_UPDATED", + }); - const eventDisplayName = i18next.t(`event.SETTINGS_UPDATED`, { lng: userLang, defaultValue: 'SETTINGS_UPDATED' }); + const mailOptions: Mail.Options = { + from: config.from, + to: config.to, + subject: i18next.t(testSubjectKey, { + lng: userLang, + defaultValue: "Nexus Terminal Test Notification ({event})", + eventDisplay: eventDisplayName, + }), + text: i18next.t(testEmailBodyKey, { + lng: userLang, + timestamp: new Date().toISOString(), + defaultValue: `This is a test email from Nexus Terminal for event '{{event}}'.\n\nIf you received this, your SMTP configuration is working.\n\nTimestamp: {{timestamp}}`, + eventDisplay: eventDisplayName, + }), + html: i18next.t(testEmailBodyHtmlKey, { + lng: userLang, + timestamp: new Date().toISOString(), + defaultValue: `

This is a test email from Nexus Terminal for event '{{event}}'.

If you received this, your SMTP configuration is working.

Timestamp: {{timestamp}}

`, + eventDisplay: eventDisplayName, + }), + }; - const mailOptions: Mail.Options = { - from: config.from, - to: config.to, - subject: i18next.t(testSubjectKey, { lng: userLang, defaultValue: 'Nexus Terminal Test Notification ({event})', eventDisplay: eventDisplayName }), - text: i18next.t(testEmailBodyKey, { lng: userLang, timestamp: new Date().toISOString(), defaultValue: `This is a test email from Nexus Terminal for event '{{event}}'.\n\nIf you received this, your SMTP configuration is working.\n\nTimestamp: {{timestamp}}`, eventDisplay: eventDisplayName }), - html: i18next.t(testEmailBodyHtmlKey, { lng: userLang, timestamp: new Date().toISOString(), defaultValue: `

This is a test email from Nexus Terminal for event '{{event}}'.

If you received this, your SMTP configuration is working.

Timestamp: {{timestamp}}

`, eventDisplay: eventDisplayName }), - }; + try { + console.log( + `[通知测试 - 邮件] 尝试通过 ${config.smtpHost}:${config.smtpPort} 发送测试邮件至 ${config.to}` + ); + const info = await transporter.sendMail(mailOptions); + console.log(`[通知测试 - 邮件] 测试邮件发送成功: ${info.messageId}`); + return { success: true, message: "测试邮件发送成功!请检查收件箱。" }; + } catch (error: any) { + console.error(`[通知测试 - 邮件] 发送测试邮件时出错:`, error); + return { + success: false, + message: `测试邮件发送失败: ${error.message || "未知错误"}`, + }; + } + } - try { - console.log(`[通知测试 - 邮件] 尝试通过 ${config.smtpHost}:${config.smtpPort} 发送测试邮件至 ${config.to}`); - const info = await transporter.sendMail(mailOptions); - console.log(`[通知测试 - 邮件] 测试邮件发送成功: ${info.messageId}`); - return { success: true, message: '测试邮件发送成功!请检查收件箱。' }; - } catch (error: any) { - console.error(`[通知测试 - 邮件] 发送测试邮件时出错:`, error); - return { success: false, message: `测试邮件发送失败: ${error.message || '未知错误'}` }; - } + private async _testWebhookSetting( + config: WebhookConfig + ): Promise<{ success: boolean; message: string }> { + console.log("[通知测试 - Webhook] 开始测试..."); + if (!config.url) { + console.error("[通知测试 - Webhook] 缺少 URL。"); + return { success: false, message: "测试 Webhook 失败:缺少 URL。" }; } - private async _testWebhookSetting(config: WebhookConfig): Promise<{ success: boolean; message: string }> { - console.log('[通知测试 - Webhook] 开始测试...'); - if (!config.url) { - console.error('[通知测试 - Webhook] 缺少 URL。'); - return { success: false, message: '测试 Webhook 失败:缺少 URL。' }; - } + let userLang = defaultLng; + try { + const langSetting = await settingsService.getSetting("language"); + if (langSetting && supportedLngs.includes(langSetting)) { + userLang = langSetting; + } + console.log(`[通知测试 - Webhook] 使用语言: ${userLang}`); + } catch (error) { + console.error( + `[通知测试 - Webhook] 获取语言设置时出错,使用默认 (${defaultLng}):`, + error + ); + } - let userLang = defaultLng; - try { - const langSetting = await settingsService.getSetting('language'); - if (langSetting && supportedLngs.includes(langSetting)) { - userLang = langSetting; + const testPayload: NotificationPayload = { + event: "SETTINGS_UPDATED", + timestamp: Date.now(), + details: { + message: i18next.t(testWebhookDetailsKey, { + lng: userLang, + defaultValue: + "This is a test notification from Nexus Terminal (Webhook).", + }), + }, + }; + const translatedWebhookMessage = + typeof testPayload.details === "object" && testPayload.details?.message + ? testPayload.details.message + : "Details 不是带有 message 属性的对象"; + console.log( + `[通知测试 - Webhook] 测试负载已创建。翻译后的 details.message:`, + translatedWebhookMessage + ); + + const eventDisplayName = i18next.t(`event.${testPayload.event}`, { + lng: userLang, + defaultValue: testPayload.event, + }); + const defaultBody = JSON.stringify(testPayload, null, 2); + const defaultBodyTemplate = `Default: JSON payload. Use {event}, {timestamp}, {details}.`; + + const templateDataWebhookTest: Record = { + event: testPayload.event, + eventDisplay: eventDisplayName, + timestamp: new Date(testPayload.timestamp).toISOString(), + + details: + typeof testPayload.details === "object" && testPayload.details?.message + ? testPayload.details.message + : typeof testPayload.details === "string" + ? testPayload.details + : JSON.stringify(testPayload.details || {}, null, 2), + }; + const requestBody = this._renderTemplate( + config.bodyTemplate || defaultBodyTemplate, + templateDataWebhookTest, + defaultBody + ); + + const requestConfig: AxiosRequestConfig = { + method: config.method || "POST", + url: config.url, + headers: { + "Content-Type": "application/json", + ...(config.headers || {}), + }, + data: requestBody, + timeout: 15000, + }; + + try { + console.log(`[通知测试 - Webhook] 发送测试 Webhook 到 ${config.url}`); + const response = await axios(requestConfig); + console.log( + `[通知测试 - Webhook] 测试 Webhook 成功发送到 ${config.url}。状态: ${response.status}` + ); + return { + success: true, + message: `测试 Webhook 发送成功 (状态码: ${response.status})。`, + }; + } catch (error: any) { + const errorMessage = + error.response?.data?.message || + error.response?.data || + error.message || + "未知错误"; + console.error( + `[通知测试 - Webhook] 发送测试 Webhook 到 ${config.url} 时出错:`, + errorMessage + ); + return { + success: false, + message: `测试 Webhook 发送失败: ${errorMessage}`, + }; + } + } + + private async _testTelegramSetting( + config: TelegramConfig + ): Promise<{ success: boolean; message: string }> { + console.log("[通知测试 - Telegram] 开始测试..."); + if (!config.botToken || !config.chatId) { + console.error("[通知测试 - Telegram] 缺少 botToken 或 chatId。"); + return { + success: false, + message: "测试 Telegram 失败:缺少机器人 Token 或聊天 ID。", + }; + } + + let userLang = defaultLng; + try { + const langSetting = await settingsService.getSetting("language"); + if (langSetting && supportedLngs.includes(langSetting)) { + userLang = langSetting; + } + console.log(`[通知测试 - Telegram] 使用语言: ${userLang}`); + } catch (error) { + console.error( + `[通知测试 - Telegram] 获取语言设置时出错,使用默认 (${defaultLng}):`, + error + ); + } + + const testPayload: NotificationPayload = { + event: "SETTINGS_UPDATED", + timestamp: Date.now(), + details: undefined, + }; + + const detailsOptions = { + lng: userLang, + defaultValue: + "Fallback: This is a test notification from Nexus Terminal (Telegram).", + }; + const keyWithNamespace = `notifications:${testTelegramDetailsKey}`; + const translatedDetailsMessage = i18next.t( + keyWithNamespace, + detailsOptions + ); + + testPayload.details = { message: translatedDetailsMessage }; + + const messageFromPayload = + typeof testPayload.details === "object" && testPayload.details?.message + ? testPayload.details.message + : "Details is not an object with message property"; + console.log( + `[Notification Test - Telegram] Test payload created. Final details.message in payload:`, + messageFromPayload + ); + + const templateKeyWithNamespace = `notifications:${testTelegramBodyTemplateKey}`; + const defaultMessageTemplateFromI18n = i18next.t(templateKeyWithNamespace, { + lng: userLang, + defaultValue: `Fallback Template: *Nexus Terminal Test Notification*\nEvent: \`{event}\`\nTimestamp: {timestamp}\nDetails:\n\`\`\`\n{details}\n\`\`\``, + }); + console.log( + `[通知测试 - Telegram] 来自 i18n 的默认模板 (使用语言 '${userLang}', 键 '${templateKeyWithNamespace}'):`, + defaultMessageTemplateFromI18n + ); + + const templateToUse = + config.messageTemplate || defaultMessageTemplateFromI18n; + console.log(`[通知测试 - Telegram] 要渲染的模板:`, templateToUse); + + const eventDisplayName = i18next.t(`event.${testPayload.event}`, { + lng: userLang, + defaultValue: testPayload.event, + }); + + const templateDataTelegramTest: Record = { + event: this._escapeBasicMarkdown(testPayload.event), + eventDisplay: this._escapeBasicMarkdown(eventDisplayName), + timestamp: new Date(testPayload.timestamp).toISOString(), + + details: this._escapeBasicMarkdown(messageFromPayload), + }; + + const messageText = this._renderTemplate( + templateToUse, + templateDataTelegramTest, + defaultMessageTemplateFromI18n + ); + console.log(`[通知测试 - Telegram] 渲染的消息文本:`, messageText); + + const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`; + + try { + console.log( + `[通知测试 - Telegram] 发送测试 Telegram 消息到聊天 ID ${config.chatId}` + ); + const response = await axios.post( + telegramApiUrl, + { + chat_id: config.chatId, + text: messageText, + parse_mode: "Markdown", + }, + { timeout: 15000 } + ); + + if (response.data?.ok) { + console.log(`[通知测试 - Telegram] 测试 Telegram 消息发送成功。`); + return { success: true, message: "测试 Telegram 消息发送成功!" }; + } else { + console.error( + `[通知测试 - Telegram] Telegram API 返回错误:`, + response.data?.description + ); + return { + success: false, + message: `测试 Telegram 发送失败: ${ + response.data?.description || "API 返回失败" + }`, + }; + } + } catch (error: any) { + const errorMessage = + error.response?.data?.description || + error.response?.data || + error.message || + "未知错误"; + console.error( + `[通知测试 - Telegram] 发送测试 Telegram 消息时出错:`, + errorMessage + ); + return { + success: false, + message: `测试 Telegram 发送失败: ${errorMessage}`, + }; + } + } + + async sendNotification( + event: NotificationEvent, + details?: Record | string + ): Promise { + console.log(`[通知] 事件触发: ${event}`, details || ""); + + let userLang = defaultLng; + let userTimezone = "UTC"; + try { + const [langSetting, timezoneSetting] = await Promise.all([ + settingsService.getSetting("language"), + settingsService.getSetting("timezone"), + ]); + if (langSetting && supportedLngs.includes(langSetting)) { + userLang = langSetting; + } + + if (timezoneSetting) { + userTimezone = timezoneSetting; + } + } catch (error) { + console.error(`[通知] 获取事件 ${event} 的语言或时区设置时出错:`, error); + } + console.log( + `[通知] 事件 ${event} 使用语言 '${userLang}', 时区 '${userTimezone}'` + ); + + const payload: NotificationPayload = { + event, + timestamp: Date.now(), + details: details || undefined, + }; + + try { + const applicableSettings = await this.repository.getEnabledByEvent(event); + console.log( + `[通知] 找到 ${applicableSettings.length} 个适用于事件 ${event} 的设置` + ); + + if (applicableSettings.length === 0) { + return; // 此事件没有启用的设置 + } + + const sendPromises = applicableSettings.map((setting) => { + switch (setting.channel_type) { + case "webhook": + return this._sendWebhook(setting, payload, userLang, userTimezone); + case "email": + return this._sendEmail(setting, payload, userLang, userTimezone); + case "telegram": + return this._sendTelegram(setting, payload, userLang, userTimezone); + default: + console.warn( + `[通知] 未知渠道类型: ${setting.channel_type} (设置 ID: ${setting.id})` + ); + return Promise.resolve(); + } + }); + + await Promise.allSettled(sendPromises); + console.log(`[通知] 完成尝试发送事件 ${event} 的通知`); + } catch (error) { + console.error(`[通知] 获取或处理事件 ${event} 的设置时出错:`, error); + } + } + + private _escapeBasicMarkdown(text: string): string { + if (typeof text !== "string") return ""; + + return text.replace(/([*_`\[])/g, "\\$1"); + } + + private _renderTemplate( + template: string | undefined, + data: Record, + defaultText: string + ): string { + if (!template) return defaultText; + let rendered = template; + for (const key in data) { + rendered = rendered.replace(new RegExp(`\\{${key}\\}`, "g"), data[key]); + } + return rendered; + } + + private async _sendWebhook( + setting: NotificationSetting, + payload: NotificationPayload, + userLang: string, + userTimezone: string + ): Promise { + const config = setting.config as WebhookConfig; + if (!config.url) { + console.error(`[通知] Webhook 设置 ID ${setting.id} 缺少 URL。`); + return; + } + + const eventDisplayName = i18next.t(`event.${payload.event}`, { + lng: userLang, + defaultValue: payload.event, + }); + + const translatedDetails = this._translatePayloadDetails( + payload.details, + userLang + ); + const translatedPayload = { ...payload, details: translatedDetails }; + + const defaultBody = JSON.stringify(translatedPayload, null, 2); + const defaultBodyTemplate = `Default: JSON payload. Use {event}, {timestamp}, {details}.`; + + const templateDataWebhook: Record = { + event: translatedPayload.event, + eventDisplay: eventDisplayName, + + timestamp: formatInTimeZone( + new Date(translatedPayload.timestamp), + userTimezone, + "yyyy-MM-dd'T'HH:mm:ss.SSSXXX" + ), + + details: + typeof translatedPayload.details === "object" && + translatedPayload.details?.message + ? translatedPayload.details.message + : typeof translatedPayload.details === "string" + ? translatedPayload.details + : JSON.stringify(translatedPayload.details || {}, null, 2), + }; + let templateToRender = config.bodyTemplate || defaultBodyTemplate; + let isCustomTemplate = !!config.bodyTemplate; + + if (isCustomTemplate) { + console.log( + `[_sendWebhook] Original custom body template for event ${payload.event}:`, + templateToRender + ); + + templateToRender = templateToRender.replace( + /\{event\}/g, + "{eventDisplay}" + ); + console.log( + `[_sendWebhook] Pre-processed body template (replaced {event} with {eventDisplay}):`, + templateToRender + ); + } else { + console.log( + `[_sendWebhook] No custom body template found. Using default template for event ${payload.event}` + ); + } + + const requestBody = this._renderTemplate( + templateToRender, + templateDataWebhook, + defaultBody + ); + + const requestConfig: AxiosRequestConfig = { + method: config.method || "POST", + url: config.url, + headers: { + "Content-Type": "application/json", + ...(config.headers || {}), + }, + data: requestBody, + timeout: 10000, + }; + + try { + console.log( + `[通知] 发送 Webhook 到 ${config.url} (事件: ${payload.event})` + ); + const response = await axios(requestConfig); + console.log( + `[通知] Webhook 成功发送到 ${config.url}。状态: ${response.status}` + ); + } catch (error: any) { + const errorMessage = + error.response?.data?.message || error.response?.data || error.message; + console.error( + `[通知] 发送 Webhook 到 ${config.url} (设置 ID: ${setting.id}) 时出错:`, + errorMessage + ); + } + } + + private async _sendEmail( + setting: NotificationSetting, + payload: NotificationPayload, + userLang: string, + userTimezone: string + ): Promise { + const config = setting.config as EmailConfig; + if (!config.to || !config.smtpHost || !config.smtpPort || !config.from) { + console.error( + `[通知] 邮件设置 ID ${setting.id} 缺少必要的 SMTP 配置 (to, smtpHost, smtpPort, from)。` + ); + return; + } + + const transporterOptions = { + host: config.smtpHost, + port: config.smtpPort, + secure: config.smtpSecure ?? true, + auth: + config.smtpUser || config.smtpPass + ? { + user: config.smtpUser, + pass: config.smtpPass, } - console.log(`[通知测试 - Webhook] 使用语言: ${userLang}`); - } catch (error) { - console.error(`[通知测试 - Webhook] 获取语言设置时出错,使用默认 (${defaultLng}):`, error); - } + : undefined, + }; - const testPayload: NotificationPayload = { - event: 'SETTINGS_UPDATED', - timestamp: Date.now(), - details: { message: i18next.t(testWebhookDetailsKey, { lng: userLang, defaultValue: 'This is a test notification from Nexus Terminal (Webhook).' }) } - }; - const translatedWebhookMessage = (typeof testPayload.details === 'object' && testPayload.details?.message) ? testPayload.details.message : 'Details 不是带有 message 属性的对象'; - console.log(`[通知测试 - Webhook] 测试负载已创建。翻译后的 details.message:`, translatedWebhookMessage); + const transporter = nodemailer.createTransport(transporterOptions); - const eventDisplayName = i18next.t(`event.${testPayload.event}`, { lng: userLang, defaultValue: testPayload.event }); - const defaultBody = JSON.stringify(testPayload, null, 2); - const defaultBodyTemplate = `Default: JSON payload. Use {event}, {timestamp}, {details}.`; - // Prepare data object for _renderTemplate - const templateDataWebhookTest: Record = { - event: testPayload.event, - eventDisplay: eventDisplayName, // Assuming no markdown needed for webhook test - timestamp: new Date(testPayload.timestamp).toISOString(), - // Use the message from details if available - details: (typeof testPayload.details === 'object' && testPayload.details?.message) - ? testPayload.details.message - : (typeof testPayload.details === 'string' - ? testPayload.details - : JSON.stringify(testPayload.details || {}, null, 2)) - }; - const requestBody = this._renderTemplate(config.bodyTemplate || defaultBodyTemplate, templateDataWebhookTest, defaultBody); // Use new signature (3 args) - - const requestConfig: AxiosRequestConfig = { - method: config.method || 'POST', - url: config.url, - headers: { - 'Content-Type': 'application/json', - ...(config.headers || {}), - }, - data: requestBody, - timeout: 15000, - }; - - try { - console.log(`[通知测试 - Webhook] 发送测试 Webhook 到 ${config.url}`); - const response = await axios(requestConfig); - console.log(`[通知测试 - Webhook] 测试 Webhook 成功发送到 ${config.url}。状态: ${response.status}`); - return { success: true, message: `测试 Webhook 发送成功 (状态码: ${response.status})。` }; - } catch (error: any) { - const errorMessage = error.response?.data?.message || error.response?.data || error.message || '未知错误'; - console.error(`[通知测试 - Webhook] 发送测试 Webhook 到 ${config.url} 时出错:`, errorMessage); - return { success: false, message: `测试 Webhook 发送失败: ${errorMessage}` }; - } + const i18nOptions: Record = { lng: userLang }; + if (payload.details && typeof payload.details === "object") { + Object.assign(i18nOptions, payload.details); + } else if (payload.details !== undefined) { + i18nOptions.details = payload.details; } - private async _testTelegramSetting(config: TelegramConfig): Promise<{ success: boolean; message: string }> { - console.log('[通知测试 - Telegram] 开始测试...'); - if (!config.botToken || !config.chatId) { - console.error('[通知测试 - Telegram] 缺少 botToken 或 chatId。'); - return { success: false, message: '测试 Telegram 失败:缺少机器人 Token 或聊天 ID。' }; + const eventDisplayName = i18next.t(`event.${payload.event}`, { + lng: userLang, + defaultValue: payload.event, + }); + + const subject = eventDisplayName; + console.log( + `[_sendEmail] Using fixed subject for event ${payload.event}: ${subject}` + ); + + const formattedTimestampForEmail = formatInTimeZone( + new Date(payload.timestamp), + userTimezone, + "yyyy-MM-dd HH:mm:ss zzz" + ); + const detailsString = + typeof payload.details === "string" + ? payload.details + : JSON.stringify(payload.details || {}, null, 2); + + const templateDataEmailBody: Record = { + event: payload.event, + eventDisplay: eventDisplayName, + timestamp: formattedTimestampForEmail, + details: detailsString, + + ...Object.entries(i18nOptions).reduce((acc, [key, value]) => { + if (key !== "lng" && typeof value !== "object") { + acc[key] = String(value); } + return acc; + }, {} as Record), + }; + console.log( + `[_sendEmail] Prepared templateDataEmailBody for event ${payload.event}:`, + templateDataEmailBody + ); - let userLang = defaultLng; - try { - const langSetting = await settingsService.getSetting('language'); - if (langSetting && supportedLngs.includes(langSetting)) { - userLang = langSetting; - } - console.log(`[通知测试 - Telegram] 使用语言: ${userLang}`); - } catch (error) { - console.error(`[通知测试 - Telegram] 获取语言设置时出错,使用默认 (${defaultLng}):`, error); - } + let body = ""; + const defaultBodyText = `Event: ${eventDisplayName}\nTimestamp: ${formattedTimestampForEmail}\nDetails:\n${detailsString}`; - const testPayload: NotificationPayload = { - event: 'SETTINGS_UPDATED', - timestamp: Date.now(), - details: undefined - }; + if (config.bodyTemplate) { + let templateToRender = config.bodyTemplate; + console.log( + `[_sendEmail] Original custom body template for event ${payload.event}:`, + templateToRender + ); - const detailsOptions = { lng: userLang, defaultValue: 'Fallback: This is a test notification from Nexus Terminal (Telegram).' }; - const keyWithNamespace = `notifications:${testTelegramDetailsKey}`; - const translatedDetailsMessage = i18next.t(keyWithNamespace, detailsOptions); + templateToRender = templateToRender.replace( + /\{event\}/g, + "{eventDisplay}" + ); + console.log( + `[_sendEmail] Pre-processed body template (replaced {event} with {eventDisplay}):`, + templateToRender + ); - testPayload.details = { message: translatedDetailsMessage }; + body = this._renderTemplate( + templateToRender, + templateDataEmailBody, + defaultBodyText + ); + } else { + console.log( + `[_sendEmail] No custom body template found. Using default constructed body text for event ${payload.event}` + ); + body = defaultBodyText; + } + console.log( + `[_sendEmail] Final email body for event ${payload.event}:\n${body}` + ); + const mailOptions: Mail.Options = { + from: config.from, + to: config.to, + subject: subject, + text: body, + }; - const messageFromPayload = (typeof testPayload.details === 'object' && testPayload.details?.message) ? testPayload.details.message : 'Details is not an object with message property'; - console.log(`[Notification Test - Telegram] Test payload created. Final details.message in payload:`, messageFromPayload); + try { + console.log( + `[通知] 通过 ${config.smtpHost}:${config.smtpPort} 发送邮件至 ${config.to} (事件: ${payload.event}, 主题: ${subject})` + ); + const info = await transporter.sendMail(mailOptions); + console.log( + `[通知] 邮件成功发送至 ${config.to} (设置 ID: ${setting.id})。消息 ID: ${info.messageId}` + ); + } catch (error: any) { + console.error( + `[通知] 通过 ${config.smtpHost} 发送邮件 (设置 ID: ${setting.id}) 时出错:`, + error + ); + } + } - const templateKeyWithNamespace = `notifications:${testTelegramBodyTemplateKey}`; - const defaultMessageTemplateFromI18n = i18next.t(templateKeyWithNamespace, { - lng: userLang, - defaultValue: `Fallback Template: *Nexus Terminal Test Notification*\nEvent: \`{event}\`\nTimestamp: {timestamp}\nDetails:\n\`\`\`\n{details}\n\`\`\`` - }); - console.log(`[通知测试 - Telegram] 来自 i18n 的默认模板 (使用语言 '${userLang}', 键 '${templateKeyWithNamespace}'):`, defaultMessageTemplateFromI18n); - - const templateToUse = config.messageTemplate || defaultMessageTemplateFromI18n; - console.log(`[通知测试 - Telegram] 要渲染的模板:`, templateToUse); - - const eventDisplayName = i18next.t(`event.${testPayload.event}`, { lng: userLang, defaultValue: testPayload.event }); - // Prepare data object for _renderTemplate - const templateDataTelegramTest: Record = { - // Use 'event' key with escaped raw event name - event: this._escapeBasicMarkdown(testPayload.event), - eventDisplay: this._escapeBasicMarkdown(eventDisplayName), // Keep escaped display name too - timestamp: new Date(testPayload.timestamp).toISOString(), - // Use 'details' key with escaped formatted details message - details: this._escapeBasicMarkdown(messageFromPayload) - }; - // Render using the chosen template and prepared data - const messageText = this._renderTemplate(templateToUse, templateDataTelegramTest, defaultMessageTemplateFromI18n); // Use new signature (3 args) - console.log(`[通知测试 - Telegram] 渲染的消息文本:`, messageText); // Keep original log message - - const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`; - - try { - console.log(`[通知测试 - Telegram] 发送测试 Telegram 消息到聊天 ID ${config.chatId}`); - const response = await axios.post(telegramApiUrl, { - chat_id: config.chatId, - text: messageText, - parse_mode: 'Markdown' - }, { timeout: 15000 }); - - if (response.data?.ok) { - console.log(`[通知测试 - Telegram] 测试 Telegram 消息发送成功。`); - return { success: true, message: '测试 Telegram 消息发送成功!' }; - } else { - console.error(`[通知测试 - Telegram] Telegram API 返回错误:`, response.data?.description); - return { success: false, message: `测试 Telegram 发送失败: ${response.data?.description || 'API 返回失败'}` }; - } - } catch (error: any) { - const errorMessage = error.response?.data?.description || error.response?.data || error.message || '未知错误'; - console.error(`[通知测试 - Telegram] 发送测试 Telegram 消息时出错:`, errorMessage); - return { success: false, message: `测试 Telegram 发送失败: ${errorMessage}` }; - } + private async _sendTelegram( + setting: NotificationSetting, + payload: NotificationPayload, + userLang: string, + userTimezone: string + ): Promise { + console.log( + `[_sendTelegram] Initiating for event: ${payload.event}, Setting ID: ${setting.id}, Lang: ${userLang}, Timezone: ${userTimezone}` + ); + console.log( + `[_sendTelegram] Received payload:`, + JSON.stringify(payload, null, 2) + ); + const config = setting.config as TelegramConfig; + if (!config.botToken || !config.chatId) { + console.error( + `[通知] Telegram 设置 ID ${setting.id} 缺少 botToken 或 chatId。` + ); + return; } + let detailsText = ""; + if (payload.details) { + if ( + payload.event === "SETTINGS_UPDATED" && + typeof payload.details === "object" && + Array.isArray(payload.details.updatedKeys) + ) { + detailsText = payload.details.updatedKeys.join(", "); + } else if (typeof payload.details === "string") { + detailsText = payload.details; + } else { + detailsText = JSON.stringify(payload.details); + } + } + console.log(`[_sendTelegram] Formatted detailsText:`, detailsText); - async sendNotification(event: NotificationEvent, details?: Record | string): Promise { - console.log(`[通知] 事件触发: ${event}`, details || ''); + const translatedEventName = i18next.t(`event.${payload.event}`, { + lng: userLang, + defaultValue: payload.event, + }); - let userLang = defaultLng; - let userTimezone = 'UTC'; // NEW: Default timezone - try { - // Fetch language and timezone settings concurrently - const [langSetting, timezoneSetting] = await Promise.all([ - settingsService.getSetting('language'), - settingsService.getSetting('timezone') // NEW: Fetch timezone - ]); - if (langSetting && supportedLngs.includes(langSetting)) { - userLang = langSetting; - } - // NEW: Validate and set timezone - if (timezoneSetting) { - // Basic validation: Check if it's a non-empty string. - // More robust validation could involve checking against Intl.supportedValuesOf('timeZone') - // but that might be overkill depending on how timezones are set/validated elsewhere. - userTimezone = timezoneSetting; - } - } catch (error) { - console.error(`[通知] 获取事件 ${event} 的语言或时区设置时出错:`, error); // Modified log - } - console.log(`[通知] 事件 ${event} 使用语言 '${userLang}', 时区 '${userTimezone}'`); // Modified log + const templateData: Record = { + event: translatedEventName, + timestamp: formatInTimeZone( + new Date(payload.timestamp), + userTimezone, + "yyyy-MM-dd HH:mm:ss zzz" + ), + details: detailsText, + }; + console.log( + `[_sendTelegram] Prepared templateData (NO escaping):`, + JSON.stringify(templateData, null, 2) + ); - const payload: NotificationPayload = { - event, - timestamp: Date.now(), - details: details || undefined, - }; + let messageText = ""; + if (config.messageTemplate) { + console.log( + `[_sendTelegram] Using custom template:`, + config.messageTemplate + ); + const fallbackForCustom = `Event: ${templateData.event}, Details: ${templateData.details}`; + messageText = this._renderTemplate( + config.messageTemplate, + templateData, + fallbackForCustom + ); + } else { + const i18nKey = `eventBody.${payload.event}`; + console.log(`[_sendTelegram] Using i18n template key:`, i18nKey); + const fallbackBody = `*Fallback Notification*\nEvent: ${templateData.event}\nTime: \`${templateData.timestamp}\`\nDetails: ${templateData.details}`; + messageText = i18next.t(i18nKey, { + lng: userLang, + ...templateData, + defaultValue: fallbackBody, + }); + } + console.log(`[_sendTelegram] Final message text to send:`, messageText); - try { - const applicableSettings = await this.repository.getEnabledByEvent(event); - console.log(`[通知] 找到 ${applicableSettings.length} 个适用于事件 ${event} 的设置`); + const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`; + try { + console.log( + `[通知] 发送 Telegram 消息到聊天 ID ${config.chatId} (事件: ${payload.event})` + ); + const requestBody = { + chat_id: config.chatId, + text: messageText, + parse_mode: "Markdown", + }; + console.log( + `[_sendTelegram] Sending request to Telegram API:`, + JSON.stringify(requestBody, null, 2) + ); + const response = await axios.post(telegramApiUrl, requestBody, { + timeout: 10000, + }); + console.log(`[通知] Telegram 消息发送成功。响应 OK:`, response.data?.ok); + } catch (error: any) { + const errorMessage = + error.response?.data?.description || + error.response?.data || + error.message; + console.error( + `[通知] 发送 Telegram 消息 (设置 ID: ${setting.id}) 时出错:`, + errorMessage + ); + } + } - if (applicableSettings.length === 0) { - return; // 此事件没有启用的设置 - } - - const sendPromises = applicableSettings.map(setting => { - switch (setting.channel_type) { - case 'webhook': - return this._sendWebhook(setting, payload, userLang, userTimezone); // Pass timezone - case 'email': - return this._sendEmail(setting, payload, userLang, userTimezone); // Pass timezone - case 'telegram': - return this._sendTelegram(setting, payload, userLang, userTimezone); // Pass timezone - default: - console.warn(`[通知] 未知渠道类型: ${setting.channel_type} (设置 ID: ${setting.id})`); - return Promise.resolve(); // 如果有一个未知,不要让所有都失败 - } - }); - - await Promise.allSettled(sendPromises); - console.log(`[通知] 完成尝试发送事件 ${event} 的通知`); - - } catch (error) { - console.error(`[通知] 获取或处理事件 ${event} 的设置时出错:`, error); - } + private _translatePayloadDetails(details: any, lng: string): any { + if (!details || typeof details !== "object") { + return details; } - - // Helper function to escape basic Markdown characters (`*`, `_`, `` ` ``, `[`) - private _escapeBasicMarkdown(text: string): string { - if (typeof text !== 'string') return ''; - // Escape *, _, `, [ - // Note: Telegram's Markdown parser might have quirks. - // If issues persist, consider escaping more characters or using MarkdownV2 with its stricter rules. - return text.replace(/([*_`\[])/g, '\\$1'); + if (details.testResult === "success" && details.connectionName) { + return { + ...details, + message: i18next.t("connection.testSuccess", { + lng, + name: details.connectionName, + defaultValue: `Connection test successful for '${details.connectionName}'!`, + }), + }; + } + if ( + details.testResult === "failed" && + details.connectionName && + details.error + ) { + return { + ...details, + message: i18next.t("connection.testFailed", { + lng, + name: details.connectionName, + error: details.error, + defaultValue: `Connection test failed for '${details.connectionName}': ${details.error}`, + }), + }; } - private _renderTemplate(template: string | undefined, data: Record, defaultText: string): string { - if (!template) return defaultText; - let rendered = template; - for (const key in data) { - // Replace placeholders like {key} with data[key] - // Assumes data values are already properly escaped if needed - rendered = rendered.replace(new RegExp(`\\{${key}\\}`, 'g'), data[key]); - } - return rendered; + if (details.updatedKeys && Array.isArray(details.updatedKeys)) { + if (details.updatedKeys.includes("ipWhitelist")) { + return { + ...details, + message: i18next.t("settings.ipWhitelistUpdated", { + lng, + defaultValue: "IP Whitelist updated successfully.", + }), + }; + } + return { + ...details, + message: i18next.t("settings.updated", { + lng, + defaultValue: "Settings updated successfully.", + }), + }; } - private async _sendWebhook(setting: NotificationSetting, payload: NotificationPayload, userLang: string, userTimezone: string): Promise { // Add userTimezone - const config = setting.config as WebhookConfig; - if (!config.url) { - console.error(`[通知] Webhook 设置 ID ${setting.id} 缺少 URL。`); - return; - } - - const eventDisplayName = i18next.t(`event.${payload.event}`, { lng: userLang, defaultValue: payload.event }); - - const translatedDetails = this._translatePayloadDetails(payload.details, userLang); - const translatedPayload = { ...payload, details: translatedDetails }; - - const defaultBody = JSON.stringify(translatedPayload, null, 2); - const defaultBodyTemplate = `Default: JSON payload. Use {event}, {timestamp}, {details}.`; - // Prepare data object for _renderTemplate - const templateDataWebhook: Record = { - event: translatedPayload.event, - eventDisplay: eventDisplayName, // Assuming no markdown needed for webhook - // NEW: Format timestamp using user's timezone - timestamp: formatInTimeZone(new Date(translatedPayload.timestamp), userTimezone, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"), // Example format, adjust as needed - // Use the translated message if available, otherwise stringify - details: (typeof translatedPayload.details === 'object' && translatedPayload.details?.message) - ? translatedPayload.details.message - : (typeof translatedPayload.details === 'string' - ? translatedPayload.details - : JSON.stringify(translatedPayload.details || {}, null, 2)) - }; - let templateToRender = config.bodyTemplate || defaultBodyTemplate; - let isCustomTemplate = !!config.bodyTemplate; - - if (isCustomTemplate) { - console.log(`[_sendWebhook] Original custom body template for event ${payload.event}:`, templateToRender); - // --- PRE-PROCESS TEMPLATE: Replace {event} with {eventDisplay} --- - templateToRender = templateToRender.replace(/\{event\}/g, '{eventDisplay}'); - console.log(`[_sendWebhook] Pre-processed body template (replaced {event} with {eventDisplay}):`, templateToRender); - // ----------------------------------------------------------------- - } else { - console.log(`[_sendWebhook] No custom body template found. Using default template for event ${payload.event}`); - } - - // Render the potentially modified template or the default one - const requestBody = this._renderTemplate(templateToRender, templateDataWebhook, defaultBody); - - const requestConfig: AxiosRequestConfig = { - method: config.method || 'POST', - url: config.url, - headers: { - 'Content-Type': 'application/json', - ...(config.headers || {}), - }, - data: requestBody, - timeout: 10000, - }; - - try { - console.log(`[通知] 发送 Webhook 到 ${config.url} (事件: ${payload.event})`); - const response = await axios(requestConfig); - console.log(`[通知] Webhook 成功发送到 ${config.url}。状态: ${response.status}`); - } catch (error: any) { - const errorMessage = error.response?.data?.message || error.response?.data || error.message; - console.error(`[通知] 发送 Webhook 到 ${config.url} (设置 ID: ${setting.id}) 时出错:`, errorMessage); - } - } - - private async _sendEmail(setting: NotificationSetting, payload: NotificationPayload, userLang: string, userTimezone: string): Promise { // Add userTimezone - const config = setting.config as EmailConfig; - if (!config.to || !config.smtpHost || !config.smtpPort || !config.from) { - console.error(`[通知] 邮件设置 ID ${setting.id} 缺少必要的 SMTP 配置 (to, smtpHost, smtpPort, from)。`); - return; - } - - const transporterOptions = { - host: config.smtpHost, - port: config.smtpPort, - secure: config.smtpSecure ?? true, - auth: (config.smtpUser || config.smtpPass) ? { - user: config.smtpUser, - pass: config.smtpPass, - } : undefined, - }; - - const transporter = nodemailer.createTransport(transporterOptions); - - const i18nOptions: Record = { lng: userLang }; - if (payload.details && typeof payload.details === 'object') { - Object.assign(i18nOptions, payload.details); - } else if (payload.details !== undefined) { - i18nOptions.details = payload.details; - } - - const eventDisplayName = i18next.t(`event.${payload.event}`, { lng: userLang, defaultValue: payload.event }); - - // --- Subject --- - // Subject is now always the translated event display name - const subject = eventDisplayName; - console.log(`[_sendEmail] Using fixed subject for event ${payload.event}: ${subject}`); - - // --- Body --- - const formattedTimestampForEmail = formatInTimeZone(new Date(payload.timestamp), userTimezone, "yyyy-MM-dd HH:mm:ss zzz"); - const detailsString = typeof payload.details === 'string' - ? payload.details - : JSON.stringify(payload.details || {}, null, 2); - - // Prepare data for template rendering or i18n interpolation - const templateDataEmailBody: Record = { - event: payload.event, // Raw event name - eventDisplay: eventDisplayName, // Translated event name - timestamp: formattedTimestampForEmail, // Formatted timestamp - details: detailsString, // Stringified details - // Add other relevant fields from i18nOptions if needed by body template/i18n - ...Object.entries(i18nOptions).reduce((acc, [key, value]) => { - if (key !== 'lng' && typeof value !== 'object') { // Exclude 'lng' and objects - acc[key] = String(value); - } - return acc; - }, {} as Record) - }; - console.log(`[_sendEmail] Prepared templateDataEmailBody for event ${payload.event}:`, templateDataEmailBody); - - let body = ''; - const defaultBodyText = `Event: ${eventDisplayName}\nTimestamp: ${formattedTimestampForEmail}\nDetails:\n${detailsString}`; - - if (config.bodyTemplate) { - - let templateToRender = config.bodyTemplate; - console.log(`[_sendEmail] Original custom body template for event ${payload.event}:`, templateToRender); - - - templateToRender = templateToRender.replace(/\{event\}/g, '{eventDisplay}'); - console.log(`[_sendEmail] Pre-processed body template (replaced {event} with {eventDisplay}):`, templateToRender); - - - // Render the potentially modified template. - body = this._renderTemplate(templateToRender, templateDataEmailBody, defaultBodyText); - } else { - // No custom template, use the directly constructed default text - console.log(`[_sendEmail] No custom body template found. Using default constructed body text for event ${payload.event}`); - body = defaultBodyText; - } - console.log(`[_sendEmail] Final email body for event ${payload.event}:\n${body}`); - - const mailOptions: Mail.Options = { - from: config.from, - to: config.to, - subject: subject, - text: body, - }; - - try { - console.log(`[通知] 通过 ${config.smtpHost}:${config.smtpPort} 发送邮件至 ${config.to} (事件: ${payload.event}, 主题: ${subject})`); - const info = await transporter.sendMail(mailOptions); - console.log(`[通知] 邮件成功发送至 ${config.to} (设置 ID: ${setting.id})。消息 ID: ${info.messageId}`); - } catch (error: any) { - console.error(`[通知] 通过 ${config.smtpHost} 发送邮件 (设置 ID: ${setting.id}) 时出错:`, error); - } - } - - private async _sendTelegram(setting: NotificationSetting, payload: NotificationPayload, userLang: string, userTimezone: string): Promise { // Add userTimezone - console.log(`[_sendTelegram] Initiating for event: ${payload.event}, Setting ID: ${setting.id}, Lang: ${userLang}, Timezone: ${userTimezone}`); // Modified log - console.log(`[_sendTelegram] Received payload:`, JSON.stringify(payload, null, 2)); - const config = setting.config as TelegramConfig; - if (!config.botToken || !config.chatId) { - console.error(`[通知] Telegram 设置 ID ${setting.id} 缺少 botToken 或 chatId。`); - return; - } - - let detailsText = ''; - if (payload.details) { - if (payload.event === 'SETTINGS_UPDATED' && typeof payload.details === 'object' && Array.isArray(payload.details.updatedKeys)) { - detailsText = payload.details.updatedKeys.join(', '); - - } else if (typeof payload.details === 'string') { - detailsText = payload.details; - } else { - detailsText = JSON.stringify(payload.details); - } - } - console.log(`[_sendTelegram] Formatted detailsText:`, detailsText); - - const translatedEventName = i18next.t(`event.${payload.event}`, { lng: userLang, defaultValue: payload.event }); - - const templateData: Record = { - event: translatedEventName, - timestamp: formatInTimeZone(new Date(payload.timestamp), userTimezone, "yyyy-MM-dd HH:mm:ss zzz"), - details: detailsText - }; - console.log(`[_sendTelegram] Prepared templateData (NO escaping):`, JSON.stringify(templateData, null, 2)); - - // 4. Handle template - let messageText = ''; - if (config.messageTemplate) { - console.log(`[_sendTelegram] Using custom template:`, config.messageTemplate); - const fallbackForCustom = `Event: ${templateData.event}, Details: ${templateData.details}`; // Use 'event' key now - messageText = this._renderTemplate(config.messageTemplate, templateData, fallbackForCustom); - } else { - const i18nKey = `eventBody.${payload.event}`; - console.log(`[_sendTelegram] Using i18n template key:`, i18nKey); - const fallbackBody = `*Fallback Notification*\nEvent: ${templateData.event}\nTime: \`${templateData.timestamp}\`\nDetails: ${templateData.details}`; - messageText = i18next.t(i18nKey, { - lng: userLang, - ...templateData, - defaultValue: fallbackBody - }); - } - console.log(`[_sendTelegram] Final message text to send:`, messageText); - - // 6. Send to Telegram (Moved step number) - const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`; - try { - console.log(`[通知] 发送 Telegram 消息到聊天 ID ${config.chatId} (事件: ${payload.event})`); - const requestBody = { - chat_id: config.chatId, - text: messageText, - parse_mode: 'Markdown', // Use standard Markdown - }; - console.log(`[_sendTelegram] Sending request to Telegram API:`, JSON.stringify(requestBody, null, 2)); - const response = await axios.post(telegramApiUrl, requestBody, { timeout: 10000 }); - console.log(`[通知] Telegram 消息发送成功。响应 OK:`, response.data?.ok); - } catch (error: any) { - const errorMessage = error.response?.data?.description || error.response?.data || error.message; - console.error(`[通知] 发送 Telegram 消息 (设置 ID: ${setting.id}) 时出错:`, errorMessage); - } - } - - private _translatePayloadDetails(details: any, lng: string): any { - if (!details || typeof details !== 'object') { - return details; - } - - if (details.testResult === 'success' && details.connectionName) { - return { - ...details, - message: i18next.t('connection.testSuccess', { lng, name: details.connectionName, defaultValue: `Connection test successful for '${details.connectionName}'!` }) - }; - } - if (details.testResult === 'failed' && details.connectionName && details.error) { - return { - ...details, - message: i18next.t('connection.testFailed', { lng, name: details.connectionName, error: details.error, defaultValue: `Connection test failed for '${details.connectionName}': ${details.error}` }) - }; - } - - if (details.updatedKeys && Array.isArray(details.updatedKeys)) { - if (details.updatedKeys.includes('ipWhitelist')) { - return { ...details, message: i18next.t('settings.ipWhitelistUpdated', { lng, defaultValue: 'IP Whitelist updated successfully.' }) }; - } - return { ...details, message: i18next.t('settings.updated', { lng, defaultValue: 'Settings updated successfully.' }) }; - } - - - return details; - } + return details; + } }