"""Fabrique de création des canaux de sortie du pipeline ``pronote-sync``. Ce module expose la fonction :func:`get_channel` qui instancie le canal de sortie XMPP à partir de sa configuration, ainsi que les types publics du paquet ``pronote_sync.channels`` : :class:`~pronote_sync.channels.protocol.Channel`, :class:`~pronote_sync.channels.xmpp.XmppChannel` et :class:`~pronote_sync.channels.xmpp.SyncXmppChannel`. """ from __future__ import annotations import logging from pronote_sync.channels.protocol import Channel from pronote_sync.channels.xmpp import SyncXmppChannel, XmppChannel from pronote_sync.config.settings import XmppSettings from pronote_sync.utils.redaction import redact_secrets logger = logging.getLogger(__name__) __all__ = ["Channel", "XmppChannel", "SyncXmppChannel", "get_channel"] def get_channel(settings: XmppSettings, dry_run: bool = False) -> Channel | None: """Instancie le canal de sortie XMPP selon la configuration (D2). Si le canal est désactivé (``enabled`` à ``False``), la fabrique retourne ``None`` sans avertissement ni exception. Si le canal est activé mais que l'un des champs requis (``jid``, ``password``, ``to``, ``host``) est vide ou absent, un avertissement est journalisé puis ``None`` est retourné. Dans tous les autres cas, une instance de :class:`~pronote_sync.channels.xmpp.SyncXmppChannel` est construite et retournée. L'avertissement est expurgé des valeurs sensibles (``jid``, mot de passe, destinataire) via :func:`pronote_sync.utils.redaction.redact_secrets` (SEC-XMPP-02) : le message journalisé ne contient jamais ces valeurs en clair. La fabrique ne lève jamais d'exception (dégradation non bloquante). :param settings: Paramètres de configuration du canal XMPP. :param dry_run: Si ``True``, le canal est créé en mode simulation (aucun envoi réseau lors de l'appel à ``send``). :return: Canal de sortie prêt à l'emploi, ou ``None`` si le canal est désactivé ou mal configuré. :rtype: Channel | None """ if not settings.enabled: return None # SEC-XMPP-02 : valeurs sensibles à masquer dans le journal (les valeurs # ``None`` sont ignorées). extra_secrets = [ secret for secret in (settings.password, settings.jid, settings.to) if secret is not None ] # SEC-XMPP-02 : rejeter aussi les chaînes vides ou composées uniquement # d'espaces : ``bool(SecretStr)`` et ``bool(str)`` ne testent que la # présence de l'objet, pas la valeur contenue. missing_fields = [ name for name, present in ( ("jid", settings.jid is not None and bool(settings.jid.strip())), ( "password", settings.password is not None and bool(settings.password.get_secret_value().strip()), ), ("to", settings.to is not None and bool(settings.to.strip())), ("host", bool(settings.host.strip())), ) if not present ] if missing_fields: logger.warning( "XMPP : configuration incomplète (champs manquants : %s), canal désactivé.", redact_secrets(", ".join(missing_fields), extra_secrets=extra_secrets), ) return None return SyncXmppChannel(settings, dry_run=dry_run)