"""Effective notification channel config (DB overrides env).""" from __future__ import annotations from dataclasses import dataclass from sqlalchemy import select from sqlalchemy.orm import Session from app.config import get_settings from app.models.notification_channel import NotificationChannel VALID_SEVERITIES = frozenset({"info", "warning", "high", "critical"}) CHANNEL_TELEGRAM = "telegram" CHANNEL_WEBHOOK = "webhook" CHANNEL_EMAIL = "email" @dataclass(frozen=True) class TelegramConfig: enabled: bool bot_token: str chat_id: str min_severity: str source: str # env | db @property def configured(self) -> bool: return bool(self.bot_token.strip() and self.chat_id.strip()) @property def active(self) -> bool: return self.enabled and self.configured @dataclass(frozen=True) class WebhookConfig: enabled: bool url: str secret_header: str secret: str min_severity: str source: str # env | db @property def configured(self) -> bool: return bool(self.url.strip()) @property def active(self) -> bool: return self.enabled and self.configured @dataclass(frozen=True) class EmailConfig: enabled: bool smtp_host: str smtp_port: int smtp_user: str smtp_password: str mail_from: str mail_to: str smtp_starttls: bool smtp_ssl: bool min_severity: str source: str # env | db @property def configured(self) -> bool: return bool(self.smtp_host.strip() and self.mail_from.strip() and self.mail_to.strip()) @property def active(self) -> bool: return self.enabled and self.configured def _telegram_from_env() -> TelegramConfig: settings = get_settings() return TelegramConfig( enabled=bool(settings.telegram_enabled), bot_token=settings.telegram_bot_token.strip(), chat_id=settings.telegram_chat_id.strip(), min_severity=settings.telegram_min_severity.strip() or "high", source="env", ) def get_effective_telegram_config(db: Session | None = None) -> TelegramConfig: if db is None: from app.database import SessionLocal session = SessionLocal() try: return get_effective_telegram_config(session) finally: session.close() row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_TELEGRAM)) env_cfg = _telegram_from_env() if row is None: return env_cfg token = (row.bot_token or "").strip() or env_cfg.bot_token chat_id = (row.chat_id or "").strip() or env_cfg.chat_id min_sev = (row.min_severity or "").strip() or env_cfg.min_severity if min_sev not in VALID_SEVERITIES: min_sev = env_cfg.min_severity return TelegramConfig( enabled=bool(row.enabled), bot_token=token, chat_id=chat_id, min_severity=min_sev, source="db", ) def upsert_telegram_channel( db: Session, *, enabled: bool | None = None, bot_token: str | None = None, chat_id: str | None = None, min_severity: str | None = None, ) -> TelegramConfig: row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_TELEGRAM)) env_cfg = _telegram_from_env() if row is None: row = NotificationChannel( channel=CHANNEL_TELEGRAM, enabled=env_cfg.enabled, bot_token=env_cfg.bot_token or None, chat_id=env_cfg.chat_id or None, min_severity=env_cfg.min_severity, ) db.add(row) if enabled is not None: row.enabled = enabled if min_severity is not None: if min_severity not in VALID_SEVERITIES: raise ValueError(f"invalid min_severity: {min_severity}") row.min_severity = min_severity if bot_token is not None and bot_token.strip(): row.bot_token = bot_token.strip() if chat_id is not None and chat_id.strip(): row.chat_id = chat_id.strip() db.commit() db.refresh(row) return get_effective_telegram_config(db) def _webhook_from_env() -> WebhookConfig: settings = get_settings() return WebhookConfig( enabled=bool(settings.webhook_enabled), url=settings.webhook_url.strip(), secret_header=settings.webhook_secret_header.strip(), secret=settings.webhook_secret.strip(), min_severity=settings.webhook_min_severity.strip() or "high", source="env", ) def get_effective_webhook_config(db: Session | None = None) -> WebhookConfig: if db is None: from app.database import SessionLocal session = SessionLocal() try: return get_effective_webhook_config(session) finally: session.close() row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_WEBHOOK)) env_cfg = _webhook_from_env() if row is None: return env_cfg url = (row.webhook_url or "").strip() or env_cfg.url secret_header = (row.webhook_secret_header or "").strip() or env_cfg.secret_header secret = (row.webhook_secret or "").strip() or env_cfg.secret min_sev = (row.min_severity or "").strip() or env_cfg.min_severity if min_sev not in VALID_SEVERITIES: min_sev = env_cfg.min_severity return WebhookConfig( enabled=bool(row.enabled), url=url, secret_header=secret_header, secret=secret, min_severity=min_sev, source="db", ) def upsert_webhook_channel( db: Session, *, enabled: bool | None = None, url: str | None = None, secret_header: str | None = None, secret: str | None = None, min_severity: str | None = None, ) -> WebhookConfig: row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_WEBHOOK)) env_cfg = _webhook_from_env() if row is None: row = NotificationChannel( channel=CHANNEL_WEBHOOK, enabled=env_cfg.enabled, min_severity=env_cfg.min_severity, webhook_url=env_cfg.url or None, webhook_secret_header=env_cfg.secret_header or None, webhook_secret=env_cfg.secret or None, ) db.add(row) if enabled is not None: row.enabled = enabled if min_severity is not None: if min_severity not in VALID_SEVERITIES: raise ValueError(f"invalid min_severity: {min_severity}") row.min_severity = min_severity if url is not None and url.strip(): row.webhook_url = url.strip() if secret_header is not None: row.webhook_secret_header = secret_header.strip() or None if secret is not None and secret.strip(): row.webhook_secret = secret.strip() db.commit() db.refresh(row) return get_effective_webhook_config(db) def _email_from_env() -> EmailConfig: settings = get_settings() return EmailConfig( enabled=bool(settings.smtp_enabled), smtp_host=settings.smtp_host.strip(), smtp_port=int(settings.smtp_port or 587), smtp_user=settings.smtp_user.strip(), smtp_password=settings.smtp_password, mail_from=settings.smtp_from.strip(), mail_to=settings.smtp_to.strip(), smtp_starttls=bool(settings.smtp_starttls), smtp_ssl=bool(settings.smtp_ssl), min_severity=settings.smtp_min_severity.strip() or "high", source="env", ) def get_effective_email_config(db: Session | None = None) -> EmailConfig: if db is None: from app.database import SessionLocal session = SessionLocal() try: return get_effective_email_config(session) finally: session.close() row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_EMAIL)) env_cfg = _email_from_env() if row is None: return env_cfg host = (row.smtp_host or "").strip() or env_cfg.smtp_host port = row.smtp_port if row.smtp_port is not None else env_cfg.smtp_port user = (row.smtp_user or "").strip() or env_cfg.smtp_user password = (row.smtp_password or "") or env_cfg.smtp_password mail_from = (row.mail_from or "").strip() or env_cfg.mail_from mail_to = (row.mail_to or "").strip() or env_cfg.mail_to min_sev = (row.min_severity or "").strip() or env_cfg.min_severity if min_sev not in VALID_SEVERITIES: min_sev = env_cfg.min_severity return EmailConfig( enabled=bool(row.enabled), smtp_host=host, smtp_port=int(port or 587), smtp_user=user, smtp_password=password, mail_from=mail_from, mail_to=mail_to, smtp_starttls=bool(row.smtp_starttls), smtp_ssl=bool(row.smtp_ssl), min_severity=min_sev, source="db", ) def upsert_email_channel( db: Session, *, enabled: bool | None = None, smtp_host: str | None = None, smtp_port: int | None = None, smtp_user: str | None = None, smtp_password: str | None = None, mail_from: str | None = None, mail_to: str | None = None, smtp_starttls: bool | None = None, smtp_ssl: bool | None = None, min_severity: str | None = None, ) -> EmailConfig: row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_EMAIL)) env_cfg = _email_from_env() if row is None: row = NotificationChannel( channel=CHANNEL_EMAIL, enabled=env_cfg.enabled, min_severity=env_cfg.min_severity, smtp_host=env_cfg.smtp_host or None, smtp_port=env_cfg.smtp_port, smtp_user=env_cfg.smtp_user or None, smtp_password=env_cfg.smtp_password or None, mail_from=env_cfg.mail_from or None, mail_to=env_cfg.mail_to or None, smtp_starttls=env_cfg.smtp_starttls, smtp_ssl=env_cfg.smtp_ssl, ) db.add(row) if enabled is not None: row.enabled = enabled if min_severity is not None: if min_severity not in VALID_SEVERITIES: raise ValueError(f"invalid min_severity: {min_severity}") row.min_severity = min_severity if smtp_host is not None and smtp_host.strip(): row.smtp_host = smtp_host.strip() if smtp_port is not None: row.smtp_port = smtp_port if smtp_user is not None: row.smtp_user = smtp_user.strip() or None if smtp_password is not None and smtp_password.strip(): row.smtp_password = smtp_password if mail_from is not None and mail_from.strip(): row.mail_from = mail_from.strip() if mail_to is not None and mail_to.strip(): row.mail_to = mail_to.strip() if smtp_starttls is not None: row.smtp_starttls = smtp_starttls if smtp_ssl is not None: row.smtp_ssl = smtp_ssl db.commit() db.refresh(row) return get_effective_email_config(db)