#!/usr/bin/env python3 # SAT: a jabber client # Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """XMPP URI parsing and building tools""" import urllib.parse from typing import Literal # FIXME: basic implementation, need to follow RFC 5122 XmppUriType = Literal["pubsub"] | None def parse_xmpp_uri(uri: str) -> dict[str, str]: """Parse an XMPP URI and return a dict with various information. @param uri: URI to parse. @return: data depending on the URI where keys can be: - "type": one of ("pubsub", ""). Always present. - "sub_type": can be "microblog", only used for pubsub for now. - "path": XMPP path (JID of the service or entity). - "node": node used. - "id": ID of the element (item for pubsub). @raise ValueError: the scheme is not "xmpp". """ uri_split = urllib.parse.urlsplit(uri) if uri_split.scheme != "xmpp": raise ValueError("this is not a XMPP URI") # XXX: we don't use jid.JID for path as it can be used both in backend and frontend # which may use different JID classes data: dict[str, str] = {"path": urllib.parse.unquote(uri_split.path)} query_end = uri_split.query.find(";") if query_end == -1: query_type: str | None = None else: query_type = uri_split.query[:query_end] if "=" in query_type: raise ValueError("no query type, invalid XMPP URI") pairs = urllib.parse.parse_qs(uri_split.geturl(), separator=";") for k, v in list(pairs.items()): if len(v) != 1: raise NotImplementedError("multiple values not managed") if k in ("path", "type", "sub_type"): raise NotImplementedError("reserved key used in URI, this is not supported") data[k] = urllib.parse.unquote(v[0]) if query_type: data["type"] = query_type elif "node" in data: data["type"] = "pubsub" else: data["type"] = "" if "node" in data: if data["node"].startswith("urn:xmpp:microblog:"): data["sub_type"] = "microblog" return data def _add_pairs(uri_parts: list[str], pairs: dict[str, str]) -> None: """Append ;key=value pairs to a URI parts list. Underscores in keys are converted to hyphens. """ for k, v in pairs.items(): uri_parts.append( ";" + urllib.parse.quote_plus(k.replace("_", "-").encode("utf-8")) + "=" + urllib.parse.quote_plus(v.encode("utf-8")) ) def build_xmpp_uri( type_: XmppUriType = None, subtype: str | None = None, path: str = "", **kwargs: str, ) -> str: """Build an xmpp: URI. @param type_: URI query type (e.g.; "pubsub"). @param subtype: sub-type hint (e.g. "microblog"). @param path: XMPP path (JID). @param kwargs: additional query parameters """ uri: list[str] = ["xmpp:"] uri.append(urllib.parse.quote_plus(path.encode("utf-8")).replace("%40", "@")) if type_ is None: if kwargs: uri.append("?") _add_pairs(uri, kwargs) elif type_ == "pubsub": if subtype == "microblog" and not kwargs.get("node"): kwargs["node"] = "urn:xmpp:microblog:0" if kwargs: uri.append("?") _add_pairs(uri, kwargs) else: raise NotImplementedError(f"{type_!r} URI type is not handled yet") return "".join(uri)