update
This commit is contained in:
@@ -18,7 +18,10 @@
|
||||
"express": "^5.1.0",
|
||||
"express-session": "^1.18.1",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"i18next": "^25.0.0",
|
||||
"i18next-fs-backend": "^2.6.0",
|
||||
"multer": "^1.4.5-lts.2",
|
||||
"nodemailer": "^6.10.1",
|
||||
"socks": "^2.8.4",
|
||||
"sqlite3": "^5.1.7",
|
||||
"ssh2": "^1.16.0",
|
||||
@@ -31,6 +34,7 @@
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/express-session": "^1.18.1",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/sqlite3": "^3.1.11",
|
||||
"@types/ssh2": "^1.15.5",
|
||||
"@types/ws": "^8.18.1",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import i18next from 'i18next';
|
||||
import Backend from 'i18next-fs-backend';
|
||||
import path from 'path';
|
||||
|
||||
// 定义支持的语言
|
||||
export const supportedLngs = ['en', 'zh'];
|
||||
export const defaultLng = 'en';
|
||||
|
||||
i18next
|
||||
.use(Backend)
|
||||
.init({
|
||||
// debug: process.env.NODE_ENV === 'development', // 可选:开发模式下开启调试
|
||||
supportedLngs,
|
||||
fallbackLng: defaultLng,
|
||||
lng: defaultLng, // 默认语言
|
||||
ns: ['notifications'], // 命名空间,用于组织翻译
|
||||
defaultNS: 'notifications',
|
||||
backend: {
|
||||
// path where resources get loaded from
|
||||
loadPath: path.join(__dirname, 'locales/{{lng}}/{{ns}}.json'),
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false, // 不对插值进行转义,因为我们可能需要 HTML 或 Markdown
|
||||
},
|
||||
});
|
||||
|
||||
export default i18next;
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"connection": {
|
||||
"testSuccess": "Connection test successful for '{name}'!",
|
||||
"testFailed": "Connection test failed for '{name}': {error}"
|
||||
},
|
||||
"settings": {
|
||||
"updated": "Settings updated successfully.",
|
||||
"ipWhitelistUpdated": "IP Whitelist updated successfully."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"connection": {
|
||||
"testSuccess": "连接 '{name}' 测试成功!",
|
||||
"testFailed": "连接 '{name}' 测试失败: {error}"
|
||||
},
|
||||
"settings": {
|
||||
"updated": "设置已成功更新。",
|
||||
"ipWhitelistUpdated": "IP 白名单已成功更新。"
|
||||
}
|
||||
}
|
||||
@@ -120,17 +120,11 @@ export class NotificationController {
|
||||
const originalSetting = await this.notificationService.getSettingById(id);
|
||||
if (!originalSetting) {
|
||||
res.status(404).json({ message: `未找到 ID 为 ${id} 的通知设置` });
|
||||
return;
|
||||
return; // Return early if setting not found
|
||||
}
|
||||
|
||||
// Currently, only email testing is implemented
|
||||
if (originalSetting.channel_type !== 'email') {
|
||||
res.status(400).json({ message: `当前仅支持测试邮件通知渠道` });
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the service method to send the test email using the provided config
|
||||
const result = await this.notificationService.testEmailSetting(config);
|
||||
// Call the generic testSetting method from the service, passing the channel type
|
||||
const result = await this.notificationService.testSetting(originalSetting.channel_type, config);
|
||||
|
||||
if (result.success) {
|
||||
// 记录审计日志 (可选,根据需要决定是否记录测试操作)
|
||||
@@ -147,4 +141,35 @@ export class NotificationController {
|
||||
res.status(500).json({ message: '测试通知设置时发生内部错误', error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// POST /api/v1/notifications/test-unsaved
|
||||
testUnsavedSetting = async (req: Request, res: Response): Promise<void> => {
|
||||
const { channel_type, config } = req.body;
|
||||
|
||||
if (!channel_type || !config) {
|
||||
res.status(400).json({ message: '缺少必要的测试信息 (channel_type, config)' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation for channel type
|
||||
if (!['webhook', 'email', 'telegram'].includes(channel_type)) {
|
||||
res.status(400).json({ message: '无效的渠道类型' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Call the generic testSetting method directly with provided type and config
|
||||
const result = await this.notificationService.testSetting(channel_type, config);
|
||||
|
||||
if (result.success) {
|
||||
res.status(200).json({ message: result.message });
|
||||
} else {
|
||||
// Return 500 for test failure to indicate an issue with the config/sending
|
||||
res.status(500).json({ message: result.message });
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`Error testing unsaved notification setting:`, error);
|
||||
res.status(500).json({ message: '测试通知设置时发生内部错误', error: error.message });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ router.post('/', notificationController.create);
|
||||
router.put('/:id', notificationController.update);
|
||||
router.delete('/:id', notificationController.delete);
|
||||
|
||||
// Route for testing a notification setting (currently only email)
|
||||
// Route for testing a saved notification setting
|
||||
router.post('/:id/test', notificationController.testSetting);
|
||||
|
||||
// Route for testing an unsaved notification setting configuration
|
||||
router.post('/test-unsaved', notificationController.testUnsavedSetting);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { getDb } from '../database';
|
||||
import { settingsService } from './settings.service'; // 用于获取配置
|
||||
import * as sqlite3 from 'sqlite3'; // 导入 sqlite3 类型
|
||||
import { settingsService } from './settings.service';
|
||||
import { NotificationService } from './notification.service'; // 导入 NotificationService
|
||||
import * as sqlite3 from 'sqlite3';
|
||||
|
||||
const db = getDb();
|
||||
const notificationService = new NotificationService(); // 实例化 NotificationService
|
||||
|
||||
// 黑名单相关设置的 Key
|
||||
const MAX_LOGIN_ATTEMPTS_KEY = 'maxLoginAttempts';
|
||||
@@ -96,11 +98,23 @@ export class IpBlacklistService {
|
||||
let blockedUntil = entry.blocked_until;
|
||||
|
||||
// 检查是否达到封禁阈值
|
||||
if (newAttempts >= maxAttempts) {
|
||||
if (newAttempts >= maxAttempts && !entry.blocked_until) { // 只有在之前未被封禁时才触发通知
|
||||
blockedUntil = now + banDuration;
|
||||
console.warn(`[IP Blacklist] IP ${ip} 登录失败次数达到 ${newAttempts} 次 (阈值 ${maxAttempts}),将被封禁 ${banDuration} 秒。`);
|
||||
// 触发 IP_BLACKLISTED 通知
|
||||
notificationService.sendNotification('IP_BLACKLISTED', {
|
||||
ip: ip,
|
||||
attempts: newAttempts,
|
||||
duration: banDuration, // 封禁时长(秒)
|
||||
blockedUntil: new Date(blockedUntil * 1000).toISOString() // 封禁截止时间
|
||||
}).catch(err => console.error(`[IP Blacklist] 发送 IP_BLACKLISTED 通知失败 for IP ${ip}:`, err));
|
||||
} else if (newAttempts >= maxAttempts && entry.blocked_until) {
|
||||
// 如果已经达到阈值且已被封禁,可能需要更新封禁时间(如果策略是每次失败都延长)
|
||||
// 当前逻辑是只在首次达到阈值时设置封禁时间,后续失败只增加次数
|
||||
console.log(`[IP Blacklist] IP ${ip} 再次登录失败,当前已处于封禁状态。`);
|
||||
}
|
||||
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
db.run(
|
||||
'UPDATE ip_blacklist SET attempts = ?, last_attempt_at = ?, blocked_until = ? WHERE ip = ?',
|
||||
@@ -117,9 +131,17 @@ export class IpBlacklistService {
|
||||
} else {
|
||||
// 插入新记录
|
||||
let blockedUntil: number | null = null;
|
||||
if (1 >= maxAttempts) { // 首次尝试就达到阈值 (虽然不常见)
|
||||
const attempts = 1; // 首次尝试
|
||||
if (attempts >= maxAttempts) { // 首次尝试就达到阈值
|
||||
blockedUntil = now + banDuration;
|
||||
console.warn(`[IP Blacklist] IP ${ip} 首次登录失败即达到阈值 ${maxAttempts},将被封禁 ${banDuration} 秒。`);
|
||||
// 触发 IP_BLACKLISTED 通知
|
||||
notificationService.sendNotification('IP_BLACKLISTED', {
|
||||
ip: ip,
|
||||
attempts: attempts,
|
||||
duration: banDuration,
|
||||
blockedUntil: new Date(blockedUntil * 1000).toISOString()
|
||||
}).catch(err => console.error(`[IP Blacklist] 发送 IP_BLACKLISTED 通知失败 for IP ${ip}:`, err));
|
||||
}
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
db.run(
|
||||
|
||||
@@ -5,12 +5,15 @@ import {
|
||||
NotificationEvent,
|
||||
NotificationPayload,
|
||||
WebhookConfig,
|
||||
EmailConfig, // Ensure EmailConfig is imported
|
||||
EmailConfig,
|
||||
TelegramConfig,
|
||||
NotificationChannelConfig
|
||||
NotificationChannelConfig,
|
||||
NotificationChannelType // Import the missing type
|
||||
} from '../types/notification.types';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import Mail from 'nodemailer/lib/mailer'; // Import Mail type for transporter
|
||||
import i18next, { defaultLng, supportedLngs } from '../i18n'; // Import i18next instance and config
|
||||
import { settingsService } from './settings.service'; // Import settings service
|
||||
|
||||
export class NotificationService {
|
||||
private repository: NotificationSettingsRepository;
|
||||
@@ -43,10 +46,27 @@ export class NotificationService {
|
||||
return this.repository.delete(id);
|
||||
}
|
||||
|
||||
// --- Test Notification Method ---
|
||||
async testEmailSetting(config: EmailConfig): Promise<{ success: boolean; message: string }> {
|
||||
// --- Test Notification Methods ---
|
||||
|
||||
// Generic test method dispatcher
|
||||
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(`[Notification Test] Unsupported channel type for testing: ${channelType}`);
|
||||
return { success: false, message: `不支持测试此渠道类型 (${channelType})` };
|
||||
}
|
||||
}
|
||||
|
||||
// Specific test method for Email
|
||||
private async _testEmailSetting(config: EmailConfig): Promise<{ success: boolean; message: string }> {
|
||||
if (!config.to || !config.smtpHost || !config.smtpPort || !config.from) {
|
||||
return { success: false, message: '测试邮件失败:缺少必要的 SMTP 配置信息 (收件人, 服务器, 端口, 发件人)。' };
|
||||
return { success: false, message: '测试邮件失败:缺少必要的 SMTP 配置信息 (收件人, 主机, 端口, 发件人)。' };
|
||||
}
|
||||
|
||||
// Let TypeScript infer the options type for SMTP
|
||||
@@ -69,9 +89,9 @@ export class NotificationService {
|
||||
const mailOptions: Mail.Options = {
|
||||
from: config.from,
|
||||
to: config.to, // Use the 'to' from config for testing
|
||||
subject: '星枢终端 (Nexus Terminal) 测试邮件',
|
||||
text: `这是一封来自星枢终端 (Nexus Terminal) 的测试邮件。\n\n如果收到此邮件,表示您的 SMTP 配置工作正常。\n\n时间: ${new Date().toISOString()}`,
|
||||
html: `<p>这是一封来自 <b>星枢终端 (Nexus Terminal)</b> 的测试邮件。</p><p>如果收到此邮件,表示您的 SMTP 配置工作正常。</p><p>时间: ${new Date().toISOString()}</p>`,
|
||||
subject: 'Nexus Terminal Test Notification',
|
||||
text: `This is a test email from Nexus Terminal.\n\nIf you received this, your SMTP configuration is working.\n\nTimestamp: ${new Date().toISOString()}`,
|
||||
html: `<p>This is a test email from <b>Nexus Terminal</b>.</p><p>If you received this, your SMTP configuration is working.</p><p>Timestamp: ${new Date().toISOString()}</p>`,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -88,11 +108,107 @@ export class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
// Specific test method for Webhook
|
||||
private async _testWebhookSetting(config: WebhookConfig): Promise<{ success: boolean; message: string }> {
|
||||
if (!config.url) {
|
||||
return { success: false, message: '测试 Webhook 失败:缺少 URL。' };
|
||||
}
|
||||
|
||||
// Use a valid event type for the test payload
|
||||
const testPayload: NotificationPayload = {
|
||||
event: 'SETTINGS_UPDATED', // Use a valid event type
|
||||
timestamp: Date.now(),
|
||||
details: { message: 'This is a test notification from Nexus Terminal (Webhook).' } // Add channel type
|
||||
};
|
||||
|
||||
// Use the same rendering logic as actual sending
|
||||
const defaultBody = JSON.stringify(testPayload, null, 2);
|
||||
const requestBody = this._renderTemplate(config.bodyTemplate, testPayload, defaultBody);
|
||||
|
||||
const requestConfig: AxiosRequestConfig = {
|
||||
method: config.method || 'POST',
|
||||
url: config.url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(config.headers || {}),
|
||||
},
|
||||
data: requestBody,
|
||||
timeout: 15000, // Slightly longer timeout for testing
|
||||
};
|
||||
|
||||
try {
|
||||
console.log(`[Notification Test] Sending test Webhook to ${config.url}`);
|
||||
const response = await axios(requestConfig);
|
||||
console.log(`[Notification Test] Test Webhook sent successfully to ${config.url}. Status: ${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(`[Notification Test] Error sending test Webhook to ${config.url}:`, errorMessage);
|
||||
return { success: false, message: `测试 Webhook 发送失败: ${errorMessage}` };
|
||||
}
|
||||
}
|
||||
|
||||
// Specific test method for Telegram
|
||||
private async _testTelegramSetting(config: TelegramConfig): Promise<{ success: boolean; message: string }> {
|
||||
if (!config.botToken || !config.chatId) {
|
||||
return { success: false, message: '测试 Telegram 失败:缺少机器人 Token 或聊天 ID。' };
|
||||
}
|
||||
|
||||
// Use a valid event type for the test payload
|
||||
const testPayload: NotificationPayload = {
|
||||
event: 'SETTINGS_UPDATED', // Use a valid event type
|
||||
timestamp: Date.now(),
|
||||
details: { message: 'This is a test notification from Nexus Terminal (Telegram).' } // Add channel type
|
||||
};
|
||||
|
||||
// Use the same rendering logic as actual sending
|
||||
const defaultMessage = `*Nexus Terminal Test Notification*\n\nEvent: \`${testPayload.event}\`\nTimestamp: ${new Date(testPayload.timestamp).toISOString()}\nDetails: \`\`\`\n${JSON.stringify(testPayload.details, null, 2)}\n\`\`\``;
|
||||
const messageText = this._renderTemplate(config.messageTemplate, testPayload, defaultMessage);
|
||||
|
||||
const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`;
|
||||
|
||||
try {
|
||||
console.log(`[Notification Test] Sending test Telegram message to chat ID ${config.chatId}`);
|
||||
const response = await axios.post(telegramApiUrl, {
|
||||
chat_id: config.chatId,
|
||||
text: messageText,
|
||||
parse_mode: 'Markdown',
|
||||
}, { timeout: 15000 }); // Slightly longer timeout for testing
|
||||
|
||||
if (response.data?.ok) {
|
||||
console.log(`[Notification Test] Test Telegram message sent successfully.`);
|
||||
return { success: true, message: '测试 Telegram 消息发送成功!' };
|
||||
} else {
|
||||
console.error(`[Notification Test] Telegram API returned error:`, 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(`[Notification Test] Error sending test Telegram message:`, errorMessage);
|
||||
return { success: false, message: `测试 Telegram 发送失败: ${errorMessage}` };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Core Notification Sending Logic ---
|
||||
|
||||
async sendNotification(event: NotificationEvent, details?: Record<string, any> | string): Promise<void> {
|
||||
console.log(`[Notification] Event triggered: ${event}`, details || '');
|
||||
|
||||
// 1. Get user's preferred language (or default)
|
||||
let userLang = defaultLng;
|
||||
try {
|
||||
// Assuming settingsService is available or needs instantiation if not singleton
|
||||
const langSetting = await settingsService.getSetting('language');
|
||||
if (langSetting && supportedLngs.includes(langSetting)) {
|
||||
userLang = langSetting;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Notification] Error fetching language setting for event ${event}:`, error);
|
||||
// Proceed with default language
|
||||
}
|
||||
console.log(`[Notification] Using language '${userLang}' for event ${event}`);
|
||||
|
||||
const payload: NotificationPayload = {
|
||||
event,
|
||||
timestamp: Date.now(),
|
||||
@@ -110,11 +226,11 @@ export class NotificationService {
|
||||
const sendPromises = applicableSettings.map(setting => {
|
||||
switch (setting.channel_type) {
|
||||
case 'webhook':
|
||||
return this._sendWebhook(setting, payload);
|
||||
return this._sendWebhook(setting, payload, userLang); // Pass userLang
|
||||
case 'email':
|
||||
return this._sendEmail(setting, payload);
|
||||
return this._sendEmail(setting, payload, userLang); // Pass userLang
|
||||
case 'telegram':
|
||||
return this._sendTelegram(setting, payload);
|
||||
return this._sendTelegram(setting, payload, userLang); // Pass userLang
|
||||
default:
|
||||
console.warn(`[Notification] Unknown channel type: ${setting.channel_type} for setting ID ${setting.id}`);
|
||||
return Promise.resolve(); // Don't fail all if one is unknown
|
||||
@@ -145,16 +261,22 @@ export class NotificationService {
|
||||
return rendered;
|
||||
}
|
||||
|
||||
|
||||
private async _sendWebhook(setting: NotificationSetting, payload: NotificationPayload): Promise<void> {
|
||||
// Updated to accept userLang
|
||||
private async _sendWebhook(setting: NotificationSetting, payload: NotificationPayload, userLang: string): Promise<void> {
|
||||
const config = setting.config as WebhookConfig;
|
||||
if (!config.url) {
|
||||
console.error(`[Notification] Webhook setting ID ${setting.id} is missing URL.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultBody = JSON.stringify(payload, null, 2);
|
||||
const requestBody = this._renderTemplate(config.bodyTemplate, payload, defaultBody);
|
||||
// Translate payload details if they match a known key structure
|
||||
const translatedDetails = this._translatePayloadDetails(payload.details, userLang);
|
||||
const translatedPayload = { ...payload, details: translatedDetails };
|
||||
|
||||
const defaultBody = JSON.stringify(translatedPayload, null, 2);
|
||||
// Note: Webhook body templates might need adjustments if they expect specific structures
|
||||
// or if they should also be translated. For now, we only translate the 'details'.
|
||||
const requestBody = this._renderTemplate(config.bodyTemplate, translatedPayload, defaultBody);
|
||||
|
||||
const requestConfig: AxiosRequestConfig = {
|
||||
method: config.method || 'POST',
|
||||
@@ -177,7 +299,8 @@ export class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
private async _sendEmail(setting: NotificationSetting, payload: NotificationPayload): Promise<void> {
|
||||
// Updated to accept userLang
|
||||
private async _sendEmail(setting: NotificationSetting, payload: NotificationPayload, userLang: string): Promise<void> {
|
||||
const config = setting.config as EmailConfig;
|
||||
if (!config.to || !config.smtpHost || !config.smtpPort || !config.from) {
|
||||
console.error(`[Notification] Email setting ID ${setting.id} is missing required SMTP configuration (to, smtpHost, smtpPort, from).`);
|
||||
@@ -198,14 +321,27 @@ export class NotificationService {
|
||||
|
||||
const transporter = nodemailer.createTransport(transporterOptions);
|
||||
|
||||
const defaultSubject = `星枢终端通知: ${payload.event}`;
|
||||
const subject = this._renderTemplate(config.subjectTemplate, payload, defaultSubject);
|
||||
// Translate subject and body using i18next
|
||||
// const i18nOptions = { lng: userLang, ...payload.details }; // Original line causing error
|
||||
const i18nOptions: Record<string, any> = { lng: userLang };
|
||||
if (payload.details && typeof payload.details === 'object') {
|
||||
Object.assign(i18nOptions, payload.details); // Merge details if it's an object
|
||||
} else if (payload.details !== undefined) {
|
||||
i18nOptions.details = payload.details; // Pass non-object details directly if needed
|
||||
}
|
||||
|
||||
// Basic default body (plain text)
|
||||
// Try to translate the event itself for the subject, fallback to event name
|
||||
const defaultSubjectKey = `event.${payload.event}`;
|
||||
const defaultSubjectFallback = `星枢终端通知: ${payload.event}`;
|
||||
const subjectText = i18next.t(defaultSubjectKey, { ...i18nOptions, defaultValue: defaultSubjectFallback });
|
||||
const subject = this._renderTemplate(config.subjectTemplate, payload, subjectText); // Allow template override
|
||||
|
||||
// Translate the main body content based on event type if a key exists
|
||||
const bodyKey = `eventBody.${payload.event}`;
|
||||
const detailsString = typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2);
|
||||
const defaultBody = `事件: ${payload.event}\n时间: ${new Date(payload.timestamp).toISOString()}\n详情:\n${detailsString}`;
|
||||
// Note: Email body templates are not implemented in this version. Using default text.
|
||||
const body = defaultBody;
|
||||
const defaultBodyText = `事件: ${payload.event}\n时间: ${new Date(payload.timestamp).toISOString()}\n详情:\n${detailsString}`;
|
||||
const body = i18next.t(bodyKey, { ...i18nOptions, defaultValue: defaultBodyText });
|
||||
// Note: Email body templates are not implemented in this version. Using translated/default text.
|
||||
|
||||
const mailOptions: Mail.Options = {
|
||||
from: config.from,
|
||||
@@ -224,18 +360,29 @@ export class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
private async _sendTelegram(setting: NotificationSetting, payload: NotificationPayload): Promise<void> {
|
||||
// Updated to accept userLang
|
||||
private async _sendTelegram(setting: NotificationSetting, payload: NotificationPayload, userLang: string): Promise<void> {
|
||||
const config = setting.config as TelegramConfig;
|
||||
if (!config.botToken || !config.chatId) {
|
||||
console.error(`[Notification] Telegram setting ID ${setting.id} is missing botToken or chatId.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Default message format
|
||||
// Translate message using i18next
|
||||
// const i18nOptions = { lng: userLang, ...payload.details }; // Original line causing error
|
||||
const i18nOptions: Record<string, any> = { lng: userLang };
|
||||
if (payload.details && typeof payload.details === 'object') {
|
||||
Object.assign(i18nOptions, payload.details); // Merge details if it's an object
|
||||
} else if (payload.details !== undefined) {
|
||||
i18nOptions.details = payload.details; // Pass non-object details directly if needed
|
||||
}
|
||||
const messageKey = `eventBody.${payload.event}`; // Use same key as email body for consistency
|
||||
const detailsStr = payload.details ? `\n详情: \`\`\`\n${typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details, null, 2)}\n\`\`\`` : '';
|
||||
const defaultMessage = `*星枢终端通知*\n\n事件: \`${payload.event}\`\n时间: ${new Date(payload.timestamp).toISOString()}${detailsStr}`;
|
||||
const defaultMessageText = `*星枢终端通知*\n\n事件: \`${payload.event}\`\n时间: ${new Date(payload.timestamp).toISOString()}${detailsStr}`;
|
||||
const translatedBody = i18next.t(messageKey, { ...i18nOptions, defaultValue: defaultMessageText });
|
||||
|
||||
const messageText = this._renderTemplate(config.messageTemplate, payload, defaultMessage);
|
||||
// Allow template override
|
||||
const messageText = this._renderTemplate(config.messageTemplate, payload, translatedBody);
|
||||
const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`;
|
||||
|
||||
try {
|
||||
@@ -251,6 +398,41 @@ export class NotificationService {
|
||||
console.error(`[Notification] Error sending Telegram message for setting ID ${setting.id}:`, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to attempt translation of known payload structures
|
||||
private _translatePayloadDetails(details: any, lng: string): any {
|
||||
if (!details || typeof details !== 'object') {
|
||||
return details; // Return as is if not an object or null/undefined
|
||||
}
|
||||
|
||||
// Example: Translate connection test results
|
||||
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}` })
|
||||
};
|
||||
}
|
||||
|
||||
// Example: Translate settings update messages (can be expanded)
|
||||
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.' }) };
|
||||
}
|
||||
// Generic settings update
|
||||
return { ...details, message: i18next.t('settings.updated', { lng, defaultValue: 'Settings updated successfully.' }) };
|
||||
}
|
||||
|
||||
|
||||
// Add more translation logic for other event details structures here...
|
||||
|
||||
return details; // Return original details if no specific translation logic matched
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: Export a singleton instance if needed throughout the backend
|
||||
|
||||
@@ -17,6 +17,7 @@ export type NotificationEvent =
|
||||
| 'API_KEY_DELETED'
|
||||
| 'PASSKEY_ADDED'
|
||||
| 'PASSKEY_DELETED'
|
||||
| 'IP_BLACKLISTED' // New event for IP blacklisting
|
||||
| 'SERVER_ERROR'; // Generic error event
|
||||
|
||||
export interface WebhookConfig {
|
||||
|
||||
Reference in New Issue
Block a user