Update notification.processor.service.ts
This commit is contained in:
@@ -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;
|
||||||
Reference in New Issue
Block a user