import re from dataclasses import dataclass, field from ..chat.base import Target from ..chat.targets import valid_target from ..globs import validate_glob from .limits import (AI_HINT_MAX, DEFAULT_MIN_STATE, DELAY_MINUTES_RANGE, MIN_STATE_RANGE, NAME_MAX, OUTPUT_CONTAINS_MAX, REMINDER_HOURS_RANGE, TARGET_MAX) from .rules import ALL_KINDS BACKENDS = ("telegram", "matrix") CONTROL_RE = re.compile(r"[\x00-\x08\x0b-\x1f\x7f]") @dataclass(frozen=True) class RouteInput: name: str = "" match_host: str = "" match_service: str = "" min_state: int = DEFAULT_MIN_STATE kinds: tuple[str, ...] = ALL_KINDS stop_on_match: bool = False enabled: bool = True targets: tuple[Target, ...] = field(default_factory=tuple) delay_minutes: int | None = None output_contains: str = "" ai_enabled: bool = True ai_hint: str = "" reminder_hours: int | None = None loud: bool = True use_default_targets: bool = False def _range_error(value: int | None, bounds: tuple[int, int], label: str, unit: str = "") -> list[str]: low, high = bounds if value is not None and not low <= value <= high: suffix = f" {unit}" if unit else "" return [f"{label} muss zwischen {low} und {high}{suffix} liegen."] return [] def validate_route(data: RouteInput) -> list[str]: errors = [] if not data.name.strip(): errors.append("Name fehlt.") elif len(data.name) > NAME_MAX: errors.append(f"Name ist zu lang (max. {NAME_MAX} Zeichen).") errors += validate_glob(data.match_host, "Host-Muster") + validate_glob(data.match_service, "Service-Muster") errors += _range_error(data.min_state, MIN_STATE_RANGE, "Mindest-Schweregrad") if not data.kinds or any(kind not in ALL_KINDS for kind in data.kinds): errors.append("Mindestens eine gültige Alarmart wählen.") errors += _range_error(data.delay_minutes, DELAY_MINUTES_RANGE, "Verzögerung", "Minuten") errors += _range_error(data.reminder_hours, REMINDER_HOURS_RANGE, "Erinnerung", "Stunden") if len(data.output_contains) > OUTPUT_CONTAINS_MAX: errors.append(f"Der Ausgabe-Text ist zu lang (max. {OUTPUT_CONTAINS_MAX} Zeichen).") if CONTROL_RE.search(data.output_contains): errors.append("Der Ausgabe-Text enthält Steuerzeichen.") if len(data.ai_hint) > AI_HINT_MAX: errors.append(f"Der KI-Hinweis ist zu lang (max. {AI_HINT_MAX} Zeichen).") if CONTROL_RE.search(data.ai_hint): errors.append("Der KI-Hinweis enthält Steuerzeichen.") for target in data.targets: if target.backend not in BACKENDS: errors.append(f"Unbekanntes Backend: {target.backend}") elif len(target.target) > TARGET_MAX or not valid_target(target.backend, target.target): errors.append(f"Ungültiges Ziel für {target.backend}.") return errors