Source code for slixmpp.plugins.xep_0455.sos

# Copyright © 2025 Mathieu Pasquet
# This file is part of slixmpp
# See the file LICENSE for copying permission.
import json
import logging
from datetime import datetime
from urllib.parse import urlparse

from slixmpp import JID
from slixmpp.plugins import BasePlugin, xep_0082
from dataclasses import dataclass

log = logging.getLogger(__name__)


class AiohttpNotFound(Exception):
    pass


@dataclass
class ExternalStatus:
    planned: bool | None
    beginning: datetime
    expected_end: datetime | None
    message: dict[str, str] | None


[docs] class XEP_0455(BasePlugin): """ XEP-0455: Service Outage Status """ name = 'xep_0455' description = 'XEP-0455: Service Outage Status' dependencies = {'xep_0128', 'xep_0030', 'xep_0082'} namespace = 'urn:xmpp:sos:0'
[docs] async def get_external_status_addresses(self, domain: JID | None = None, **iqkwargs) -> list[str]: """Return the list of external status addresses for this domain. :param domain: Domain to disco to find a service. """ if domain is None: domain = JID(self.xmpp.boundjid.host) uris = [] results = await self.xmpp.plugin['xep_0030'].get_info(jid=domain, **iqkwargs ) disco = results.get_plugin('disco_info', check=True) if disco is None or 'forms' not in disco: return uris forms = disco['forms'] if not forms: return uris field = 'external-status-addresses' for form in forms: values = form.get_values() if values.get('FORM_TYPE') == [self.namespace]: uris.extend(values.get(field, [])) return uris
[docs] @classmethod async def fetch_status(cls, addresses: str | list[str]) -> dict: """ Get the external status from a list of addresses. Only works with http/https for now and stops on the first status fetched successfully. :param addresses: address or list of addresses to fetch the status from. """ try: from aiohttp import ClientSession except ImportError: raise AiohttpNotFound("aiohttp was not found, unable to download statuses") if not isinstance(addresses, list): addresses = [addresses] async with ClientSession(headers={'User-Agent': 'slixmpp'}) as session: for address in addresses: scheme, *_ = urlparse(address) if scheme not in ('http', 'https'): continue response = await session.get(address, timeout=60) if response.status >= 400: log.debug(f'Server "{address}" answered with code {response.status}') continue text = await response.text() status = cls._parse_status(text) if status is not None: return status
@staticmethod def _parse_status(raw: str) -> ExternalStatus | None: """ Parse a status json payload. Return None if the required (beginning) field is not found or not parseable. Returns a dataclass with the fields. """ try: payload = json.loads(raw) except json.JSONDecodeError: log.error('Unable to parse the external status: {text}') return None if not payload: return None beginning_raw = payload.get('beginning') try: beginning = xep_0082.parse(beginning_raw) except (ValueError, TypeError): log.error(f'Bad value for beginning: "{beginning_raw}"') return None expected_end_raw = payload.get('expected_end') expected_end = None try: expected_end = xep_0082.parse(expected_end_raw) except (ValueError, TypeError): log.error(f'Bad value for expected end: "{expected_end_raw}"') planned_raw = payload.get('planned') planned = planned_raw if isinstance(planned_raw, bool) else None message_raw = payload.get('message') message = None if isinstance(message_raw, dict): message = {} for key, value in message_raw.items(): if isinstance(key, str) and isinstance(value, str): message[key] = value return ExternalStatus(planned, beginning, expected_end, message)