Ctrl K
ComunidadGuías…

server/src/notifications/index.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import logger from '../utils/logger';
import { isServiceConfigured } from '../config';
import settingsRepo from '../db/repositories/settings';
import {
  type NotificationData,
  type NotificationEvent,
  type DiscordMessage,
  getPlainTextMessage,
  getDiscordMessage,
} from './templates';
import { fanOutWebhooks } from './webhooks';

// ============================================================================
// Types
// ============================================================================

export interface NotificationConfig {
  discord: {
    enabled: boolean;
  };
}

export interface NotificationResult {
  channel: 'discord';
  success: boolean;
  error?: string;
}

// ============================================================================
// Notification Service Class
// ============================================================================

/**
 * Service for sending notifications via Discord
 */
export class NotificationService {
  private config: NotificationConfig;

  constructor(notificationConfig?: Partial<NotificationConfig>) {
    this.config = {
      discord: {
        enabled: isServiceConfigured('discord'),
        ...notificationConfig?.discord,
      },
    };
  }

  // ============================================================================
  // Main Notification Method
  // ============================================================================

  /**
   * Send notifications to all configured channels
   */
  async notify(event: NotificationEvent | string, data: NotificationData): Promise<NotificationResult[]> {
    logger.info(`Sending notification for event: ${event}`);

    const notificationEvent = event as NotificationEvent;
    const results: NotificationResult[] = [];
    // One timestamp shared by Discord embeds and the webhook envelope.
    const timestamp = new Date().toISOString();

    // --- Discord: gated by the per-event preference + Discord config ---
    // (Webhook targets carry their own per-event opt-in, so the preference
    // gate must NOT short-circuit the whole method.)
    if (this.isEventEnabled(notificationEvent)) {
      const discordEnabled = settingsRepo.getBoolean('notifications_discordEnabled', false);
      const discordWebhook = settingsRepo.getValue('notifications_discordWebhook');

      if (discordEnabled && discordWebhook) {
        try {
          const lng = settingsRepo.getValue('notifications_language') || 'en';
          const message = getDiscordMessage(notificationEvent, data, lng);
          const result = await this.sendDiscord(discordWebhook, message);
          results.push({
            channel: 'discord',
            success: result,
            error: result ? undefined : 'Failed to send Discord message',
          });
        } catch (error) {
          const errorMessage = error instanceof Error ? error.message : String(error);
          logger.error('Discord notification error:', error);
          results.push({
            channel: 'discord',
            success: false,
            error: errorMessage,
          });
        }
      }
    } else {
      logger.info(`Discord notification for ${event} suppressed by user preference`);
    }

    // --- Outbound webhooks: independent per-target opt-in, fire-and-forget ---
    try {
      await fanOutWebhooks(notificationEvent, data, timestamp);
    } catch (error) {
      logger.error('Webhook fan-out error:', error);
    }

    // Log summary
    const successful = results.filter((r) => r.success).length;
    const failed = results.filter((r) => !r.success).length;
    logger.info(`Notification results: ${successful} successful, ${failed} failed`);

    return results;
  }

  // ============================================================================
  // Event Preference Checks
  // ============================================================================

  /**
   * Check if a notification event is enabled based on user preferences.
   * Note: SCAN_COMPLETE is also gated in the scheduler task itself for the
   * 'flagged_only' vs 'always' distinction. This method handles the 'never' case
   * and all deletion-related event preferences.
   */
  private isEventEnabled(event: NotificationEvent): boolean {
    switch (event) {
      case 'SCAN_COMPLETE':
      case 'SCAN_ERROR': {
        const scanNotify = settingsRepo.getValue('notifications_scanNotify') || 'flagged_only';
        return scanNotify !== 'never';
      }
      case 'ITEMS_MARKED': {
        return settingsRepo.getBoolean('notifications_notifyOnQueue', true);
      }
      case 'DELETION_IMMINENT': {
        return settingsRepo.getBoolean('notifications_notifyBeforeDeletion', true);
      }
      case 'DELETION_COMPLETE':
      case 'DELETION_ERROR': {
        return settingsRepo.getBoolean('notifications_notifyOnDeletion', true);
      }
      case 'DISK_PRESSURE_TRIGGERED': {
        return settingsRepo.getBoolean('notifications_notifyOnDiskPressure', true);
      }
      default:
        return true;
    }
  }

  // ============================================================================
  // Discord Methods
  // ============================================================================

  /**
   * Send a Discord webhook message
   */
  async sendDiscord(webhookUrl: string, message: DiscordMessage): Promise<boolean> {
    if (!webhookUrl) {
      logger.warn('Discord webhook URL not configured');
      return false;
    }

    try {
      logger.debug('Sending Discord notification');

      const response = await fetch(webhookUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(message),
      });

      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`Discord API error: ${response.status} - ${errorText}`);
      }

      logger.info('Discord notification sent successfully');
      return true;
    } catch (error) {
      logger.error('Failed to send Discord notification:', error);
      return false;
    }
  }

  /**
   * Send a simple Discord text message
   */
  async sendDiscordText(webhookUrl: string, content: string): Promise<boolean> {
    return this.sendDiscord(webhookUrl, {
      content,
      username: 'Prunerr',
    });
  }

  // ============================================================================
  // Configuration Methods
  // ============================================================================

  /**
   * Update notification configuration
   */
  updateConfig(config: Partial<NotificationConfig>): void {
    if (config.discord) {
      this.config.discord = { ...this.config.discord, ...config.discord };
    }

    logger.info('Notification configuration updated');
  }

  /**
   * Get current notification configuration
   */
  getConfig(): NotificationConfig {
    return { ...this.config };
  }

  /**
   * Enable or disable a notification channel
   */
  setChannelEnabled(channel: 'discord', enabled: boolean): void {
    this.config.discord.enabled = enabled;
    logger.info(`${channel} notifications ${enabled ? 'enabled' : 'disabled'}`);
  }

  /**
   * Test notification to all configured channels
   */
  async testNotifications(): Promise<NotificationResult[]> {
    const testData: NotificationData = {
      itemsScanned: 100,
      itemsFlagged: 5,
      itemsProtected: 10,
      durationMs: 1234,
    };

    return this.notify('SCAN_COMPLETE', testData);
  }
}

// Re-export types from templates for convenience
export { type NotificationEvent, type NotificationData } from './templates';

// ============================================================================
// Singleton Instance
// ============================================================================

let notificationServiceInstance: NotificationService | null = null;

/**
 * Get the singleton notification service instance
 */
export function getNotificationService(): NotificationService {
  if (!notificationServiceInstance) {
    notificationServiceInstance = new NotificationService();
  }
  return notificationServiceInstance;
}

/**
 * Create a new notification service instance with custom configuration
 */
export function createNotificationService(
  config?: Partial<NotificationConfig>
): NotificationService {
  return new NotificationService(config);
}

export default NotificationService;