Update notification.processor.service.ts

This commit is contained in:
Baobhan Sith
2025-04-26 22:11:02 +08:00
parent 95fac7981c
commit 75a38dadbf
@@ -1,11 +1,10 @@
import eventService, { AppEventType, AppEventPayload } from './event.service'; import eventService, { AppEventType, AppEventPayload } from './event.service';
import { NotificationSettingsRepository } from '../repositories/notification.repository'; import { NotificationSettingsRepository } from '../repositories/notification.repository';
import { NotificationSetting, NotificationEvent, NotificationChannelType, WebhookConfig, EmailConfig, TelegramConfig, NotificationChannelConfig } from '../types/notification.types'; import { NotificationSetting, NotificationEvent, NotificationChannelType, WebhookConfig, EmailConfig, TelegramConfig, NotificationChannelConfig } from '../types/notification.types';
import i18next, { i18nInitializationPromise } from '../i18n'; // Import the promise import i18next, { i18nInitializationPromise } from '../i18n';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
// 定义处理后的通知数据结构 // 定义处理后的通知数据结构
// Exporting for use in dispatcher
export interface ProcessedNotification { export interface ProcessedNotification {
channelType: NotificationChannelType; channelType: NotificationChannelType;
config: NotificationChannelConfig; // 包含发送所需的配置,如 URL, Token, SMTP 等 config: NotificationChannelConfig; // 包含发送所需的配置,如 URL, Token, SMTP 等
@@ -17,28 +16,25 @@ export interface ProcessedNotification {
class NotificationProcessorService extends EventEmitter { class NotificationProcessorService extends EventEmitter {
private repository: NotificationSettingsRepository; private repository: NotificationSettingsRepository;
private isInitialized = false; // Flag to track initialization private isInitialized = false;
constructor() { constructor() {
super(); super();
this.repository = new NotificationSettingsRepository(); this.repository = new NotificationSettingsRepository();
this.initialize(); // Call async initialization method this.initialize();
// Increase listener limit
this.setMaxListeners(50); this.setMaxListeners(50);
} }
// Async initialization method
private async initialize(): Promise<void> { private async initialize(): Promise<void> {
try { try {
console.log('[NotificationProcessor] Waiting for i18n initialization...'); console.log('[NotificationProcessor] Waiting for i18n initialization...');
await i18nInitializationPromise; // Wait for i18n to load await i18nInitializationPromise;
console.log('[NotificationProcessor] i18n initialized. Registering event listeners...'); console.log('[NotificationProcessor] i18n initialized. Registering event listeners...');
this.registerEventListeners(); this.registerEventListeners();
this.isInitialized = true; this.isInitialized = true;
console.log('[NotificationProcessor] Initialization complete.'); console.log('[NotificationProcessor] Initialization complete.');
} catch (error) { } catch (error) {
console.error('[NotificationProcessor] Failed to initialize due to i18n error:', error); console.error('[NotificationProcessor] Failed to initialize due to i18n error:', error);
// Handle initialization failure, maybe retry or log critical error
} }
} }
@@ -46,11 +42,10 @@ class NotificationProcessorService extends EventEmitter {
private registerEventListeners() { private registerEventListeners() {
if (this.isInitialized) { if (this.isInitialized) {
console.warn('[NotificationProcessor] Attempted to register listeners multiple times.'); console.warn('[NotificationProcessor] Attempted to register listeners multiple times.');
return; // Prevent double registration return;
} }
// 监听所有 AppEventType 事件 // 监听所有 AppEventType 事件
Object.values(AppEventType).forEach(eventType => { Object.values(AppEventType).forEach(eventType => {
// Special handling for TestNotification is done inside processEvent handlers below
if (eventType !== AppEventType.TestNotification) { if (eventType !== AppEventType.TestNotification) {
eventService.onEvent(eventType, (payload) => { eventService.onEvent(eventType, (payload) => {
// 使用 setImmediate 或 process.nextTick 避免阻塞事件循环 // 使用 setImmediate 或 process.nextTick 避免阻塞事件循环
@@ -62,7 +57,6 @@ class NotificationProcessorService extends EventEmitter {
}); });
} }
}); });
// Separate listener specifically for TestNotification
eventService.onEvent(AppEventType.TestNotification, (payload) => { eventService.onEvent(AppEventType.TestNotification, (payload) => {
setImmediate(() => { setImmediate(() => {
this.processTestEvent(payload).catch(error => { this.processTestEvent(payload).catch(error => {
@@ -73,7 +67,6 @@ class NotificationProcessorService extends EventEmitter {
console.log('[NotificationProcessor] Registered listeners.'); console.log('[NotificationProcessor] Registered listeners.');
} }
// Handles standard events by fetching settings from DB
private async processStandardEvent(eventType: AppEventType, payload: AppEventPayload) { private async processStandardEvent(eventType: AppEventType, payload: AppEventPayload) {
if (!this.isInitialized) { if (!this.isInitialized) {
console.warn(`[NotificationProcessor] Received event ${eventType} before initialization. Skipping.`); console.warn(`[NotificationProcessor] Received event ${eventType} before initialization. Skipping.`);
@@ -95,12 +88,6 @@ class NotificationProcessorService extends EventEmitter {
// 1. 翻译事件名称 // 1. 翻译事件名称
const translatedEvent = i18next.t(`event.${eventKey}`, { lng: userLang, defaultValue: eventKey }); const translatedEvent = i18next.t(`event.${eventKey}`, { lng: userLang, defaultValue: eventKey });
// --- DEBUG LOG ---
console.log(`[NotificationProcessor] Translating event key '${eventKey}' for lang '${userLang}'. Result: '${translatedEvent}'`);
// --- END DEBUG LOG ---
// --- DEBUG LOG ---
console.log(`[NotificationProcessor] Translating event key '${eventKey}' for lang '${userLang}'. Result: '${translatedEvent}'`);
// --- END DEBUG LOG ---
for (const setting of applicableSettings) { for (const setting of applicableSettings) {
@@ -111,7 +98,6 @@ class NotificationProcessorService extends EventEmitter {
} }
} }
// Handles the specific TestNotification event using config from payload
private async processTestEvent(payload: AppEventPayload) { private async processTestEvent(payload: AppEventPayload) {
if (!this.isInitialized) { if (!this.isInitialized) {
console.warn(`[NotificationProcessor] Received test event before initialization. Skipping.`); console.warn(`[NotificationProcessor] Received test event before initialization. Skipping.`);
@@ -125,45 +111,36 @@ class NotificationProcessorService extends EventEmitter {
return; return;
} }
// Create a mock setting object for processing
const mockSetting: NotificationSetting = { const mockSetting: NotificationSetting = {
id: -1, // Indicate it's a test/mock id: -1,
name: 'Test Setting', name: 'Test Setting',
enabled: true, enabled: true,
channel_type: testTargetChannelType, channel_type: testTargetChannelType,
config: testTargetConfig, config: testTargetConfig,
enabled_events: [AppEventType.TestNotification as NotificationEvent], // Doesn't really matter here enabled_events: [AppEventType.TestNotification as NotificationEvent],
}; };
const userLang = 'zh-CN'; // TODO: Get user language preference const userLang = 'zh-CN'; // TODO: Get user language preference
// For test events, use 'testNotification' as the key for i18n lookups
const translatedEvent = i18next.t(`event.${AppEventType.TestNotification}`, { lng: userLang, defaultValue: AppEventType.TestNotification }); const translatedEvent = i18next.t(`event.${AppEventType.TestNotification}`, { lng: userLang, defaultValue: AppEventType.TestNotification });
// --- DEBUG LOG ---
console.log(`[NotificationProcessor] Translating event key '${AppEventType.TestNotification}' for lang '${userLang}'. Result: '${translatedEvent}'`);
// --- END DEBUG LOG ---
this.processSingleSetting(mockSetting, AppEventType.TestNotification, payload, translatedEvent, userLang); this.processSingleSetting(mockSetting, AppEventType.TestNotification, payload, translatedEvent, userLang);
} }
// Processes a single setting (called by both standard and test event handlers)
private processSingleSetting( private processSingleSetting(
setting: NotificationSetting, setting: NotificationSetting,
eventType: AppEventType, // The actual event type (e.g., LOGIN_SUCCESS or TestNotification) eventType: AppEventType,
payload: AppEventPayload, payload: AppEventPayload,
translatedEvent: string, translatedEvent: string,
userLang: string userLang: string
) { ) {
try { try {
// i18nEventKey is no longer needed for template lookup here
// const i18nEventKey = eventType === AppEventType.TestNotification ? 'testNotification' : eventType;
const processedNotification = this.prepareNotificationContent( const processedNotification = this.prepareNotificationContent(
setting, setting,
// i18nEventKey, // Pass the original event type for context if needed later
eventType, eventType,
payload, payload,
translatedEvent, // Pass the already translated event name translatedEvent,
userLang userLang
); );
@@ -179,23 +156,19 @@ class NotificationProcessorService extends EventEmitter {
private prepareNotificationContent( private prepareNotificationContent(
setting: NotificationSetting, setting: NotificationSetting,
eventType: AppEventType, // Use the actual event type for context eventType: AppEventType,
payload: AppEventPayload, payload: AppEventPayload,
translatedEvent: string, // The already translated event name (e.g., "登录成功") translatedEvent: string, // The already translated event name (e.g., "登录成功")
lang: string // Keep lang for potential future use lang: string
): ProcessedNotification | null { ): ProcessedNotification | null {
// Base data for interpolation, using the translated event name
const baseInterpolationData = { const baseInterpolationData = {
event: translatedEvent, // Use the translated event name here! event: translatedEvent,
rawEvent: eventType, // Keep original event type rawEvent: eventType,
timestamp: payload.timestamp.toISOString(), timestamp: payload.timestamp.toISOString(),
// Safely stringify details, provide default
details: typeof payload.details === 'object' ? JSON.stringify(payload.details, null, 2) : (payload.details || ''), details: typeof payload.details === 'object' ? JSON.stringify(payload.details, null, 2) : (payload.details || ''),
userId: payload.userId || 'N/A', userId: payload.userId || 'N/A',
// Flatten details for easier access in simple templates
...(typeof payload.details === 'object' ? payload.details : {}), ...(typeof payload.details === 'object' ? payload.details : {}),
// Add specific fields from details if they exist (example for setting deletion)
settingId: payload.details?.settingId, settingId: payload.details?.settingId,
settingName: payload.details?.name, settingName: payload.details?.name,
settingType: payload.details?.type, settingType: payload.details?.type,
@@ -205,35 +178,29 @@ class NotificationProcessorService extends EventEmitter {
let subject: string | undefined = undefined; let subject: string | undefined = undefined;
let body: string = ''; let body: string = '';
// Define GENERIC fallback templates in code
const genericSubject = `通知: {event}`; const genericSubject = `通知: {event}`;
const genericEmailBody = `<p>事件: {event}</p><p>时间: {timestamp}</p><p>用户ID: {userId}</p><p>详情:</p><pre>{details}</pre>`; const genericEmailBody = `<p>事件: {event}</p><p>时间: {timestamp}</p><p>用户ID: {userId}</p><p>详情:</p><pre>{details}</pre>`;
const genericWebhookBody = JSON.stringify({ event: '{event}', timestamp: '{timestamp}', userId: '{userId}', details: '{details}' }); const genericWebhookBody = JSON.stringify({ event: '{event}', timestamp: '{timestamp}', userId: '{userId}', details: '{details}' });
const genericTelegramBody = `*{event}*\n时间: {timestamp}\n用户ID: {userId}\n详情:\n\`\`\`\n{details}\n\`\`\``; const genericTelegramBody = `*{event}*\n时间: {timestamp}\n用户ID: {userId}\n详情:\n\`\`\`\n{details}\n\`\`\``;
// Use user-defined template first, then the GENERIC fallback
switch (setting.channel_type) { switch (setting.channel_type) {
case 'email': case 'email':
const emailConfig = setting.config as EmailConfig; const emailConfig = setting.config as EmailConfig;
// Subject is now fixed to the translated event name
subject = translatedEvent; subject = translatedEvent;
// Use user-defined template (from bodyTemplate field) for the BODY, or generic fallback const bodyTemplate = emailConfig?.bodyTemplate || genericEmailBody;
const bodyTemplate = emailConfig?.bodyTemplate || genericEmailBody; // Use optional chaining and correct property
body = this.interpolate(bodyTemplate, baseInterpolationData); body = this.interpolate(bodyTemplate, baseInterpolationData);
break; break;
case 'webhook': case 'webhook':
const webhookConfig = setting.config as WebhookConfig; const webhookConfig = setting.config as WebhookConfig;
// Use user template OR generic fallback
const webhookTemplate = webhookConfig.bodyTemplate || genericWebhookBody; const webhookTemplate = webhookConfig.bodyTemplate || genericWebhookBody;
body = this.interpolate(webhookTemplate, baseInterpolationData); body = this.interpolate(webhookTemplate, baseInterpolationData);
break; break;
case 'telegram': case 'telegram':
const telegramConfig = setting.config as TelegramConfig; const telegramConfig = setting.config as TelegramConfig;
// Use user template OR generic fallback
const telegramTemplate = telegramConfig.messageTemplate || genericTelegramBody; const telegramTemplate = telegramConfig.messageTemplate || genericTelegramBody;
body = this.interpolate(telegramTemplate, baseInterpolationData); body = this.interpolate(telegramTemplate, baseInterpolationData);
break; break;
@@ -248,7 +215,7 @@ class NotificationProcessorService extends EventEmitter {
config: setting.config, config: setting.config,
subject: subject, subject: subject,
body: body, body: body,
rawPayload: payload // Pass original payload for potential use in senders rawPayload: payload
}; };
} }
@@ -263,16 +230,12 @@ class NotificationProcessorService extends EventEmitter {
// 使用正则表达式全局替换 {key} 格式的占位符 // 使用正则表达式全局替换 {key} 格式的占位符
return template.replace(/\{(\w+)\}/g, (match, key) => { return template.replace(/\{(\w+)\}/g, (match, key) => {
// 如果 data 中存在对应的 key,则返回值,否则返回原始匹配(例如 "{unknownKey}" // 如果 data 中存在对应的 key,则返回值,否则返回原始匹配(例如 "{unknownKey}"
// Improved: Handle potential undefined/null values gracefully
return data.hasOwnProperty(key) && data[key] !== null && data[key] !== undefined ? String(data[key]) : match; return data.hasOwnProperty(key) && data[key] !== null && data[key] !== undefined ? String(data[key]) : match;
}); });
} }
} }
// 创建单例并导出 // 创建单例并导出
// The instance is created immediately, and its async initialize method is called.
// Other parts of the app that import this will get the instance,
// but event processing won't start until initialization completes.
const notificationProcessorService = new NotificationProcessorService(); const notificationProcessorService = new NotificationProcessorService();
export default notificationProcessorService; export default notificationProcessorService;