#!/usr/bin/env python3
# Libervia plugin for XEP-0372
# 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 .
from typing import Optional, Dict, Union
from textwrap import dedent
from libervia.backend.core import exceptions
from libervia.backend.tools.common import uri as xmpp_uri
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
from twisted.words.xish import domish
from zope.interface import implementer
from tx_xmpp import disco, itx_xmpp
from libervia.backend.core.constants import Const as C
from libervia.backend.core.i18n import _
from libervia.backend.core.log import getLogger
from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.tools.common import data_format
log = getLogger(__name__)
PLUGIN_INFO = {
C.PI_NAME: "References",
C.PI_IMPORT_NAME: "XEP-0372",
C.PI_TYPE: C.PLUG_TYPE_XEP,
C.PI_MODES: C.PLUG_MODE_BOTH,
C.PI_PROTOCOLS: ["XEP-0372"],
C.PI_DEPENDENCIES: ["XEP-0334"],
C.PI_MAIN: "XEP_0372",
C.PI_HANDLER: "yes",
C.PI_DESCRIPTION: _(dedent("""\
XEP-0372 (References) implementation
This plugin implement generic references and mentions.
""")),
}
NS_REFS = "urn:xmpp:reference:0"
ALLOWED_TYPES = ("mention", "data")
class XEP_0372:
namespace = NS_REFS
def __init__(self, host):
log.info(_("References plugin initialization"))
host.register_namespace("refs", NS_REFS)
self.host = host
self._h = host.plugins["XEP-0334"]
host.trigger.add("message_received", self._message_received_trigger)
host.bridge.add_method(
"reference_send",
".plugin",
in_sign="sssss",
out_sign="",
method=self._send_reference,
async_=False,
)
def get_handler(self, client):
return XEP_0372_Handler()
def ref_element_to_ref_data(
self, reference_elt: domish.Element
) -> Dict[str, Union[str, int, dict]]:
ref_data: Dict[str, Union[str, int, dict]] = {
"uri": reference_elt["uri"],
"type": reference_elt["type"],
}
if ref_data["uri"].startswith("xmpp:"):
ref_data["parsed_uri"] = xmpp_uri.parse_xmpp_uri(ref_data["uri"])
for attr in ("begin", "end"):
try:
ref_data[attr] = int(reference_elt[attr])
except (KeyError, ValueError, TypeError):
continue
anchor = reference_elt.getAttribute("anchor")
if anchor is not None:
ref_data["anchor"] = anchor
if anchor.startswith("xmpp:"):
ref_data["parsed_anchor"] = xmpp_uri.parse_xmpp_uri(anchor)
return ref_data
async def _message_received_trigger(
self,
client: SatXMPPEntity,
message_elt: domish.Element,
post_treat: defer.Deferred,
) -> bool:
"""Check if a direct invitation is in the message, and handle it"""
reference_elt = next(message_elt.elements(NS_REFS, "reference"), None)
if reference_elt is None:
return True
try:
ref_data = self.ref_element_to_ref_data(reference_elt)
except KeyError:
log.warning("invalid element: {reference_elt.toXml}")
return True
if not await self.host.trigger.async_point(
"XEP-0372_ref_received", client, message_elt, ref_data
):
return False
return True
def build_ref_element(
self,
uri: str,
type_: str = "mention",
begin: Optional[int] = None,
end: Optional[int] = None,
anchor: Optional[str] = None,
) -> domish.Element:
"""Build and return the element"""
if type_ not in ALLOWED_TYPES:
raise ValueError(f"Unknown type: {type_!r}")
reference_elt = domish.Element(
(NS_REFS, "reference"), attribs={"uri": uri, "type": type_}
)
if begin is not None:
reference_elt["begin"] = str(begin)
if end is not None:
reference_elt["end"] = str(end)
if anchor is not None:
reference_elt["anchor"] = anchor
return reference_elt
def _send_reference(
self, recipient: str, anchor: str, type_: str, extra_s: str, profile_key: str
) -> defer.Deferred:
recipient_jid = jid.JID(recipient)
client = self.host.get_client(profile_key)
extra: dict = data_format.deserialise(extra_s, default={})
self.send_reference(
client, uri=extra.get("uri"), type_=type_, anchor=anchor, to_jid=recipient_jid
)
def send_reference(
self,
client: "SatXMPPEntity",
uri: Optional[str] = None,
type_: str = "mention",
begin: Optional[int] = None,
end: Optional[int] = None,
anchor: Optional[str] = None,
message_elt: Optional[domish.Element] = None,
to_jid: Optional[jid.JID] = None,
) -> None:
"""Build and send a reference_elt
@param uri: URI pointing to referenced object (XMPP entity, Pubsub Item, etc)
if not set, "to_jid" will be used to build an URI to the entity
@param type_: type of reference
one of [ALLOWED_TYPES]
@param begin: optional begin index
@param end: optional end index
@param anchor: URI of refering object (message, pubsub item), when the refence
is not already in the wrapping message element. In other words, it's the
object where the reference appears.
@param message_elt: wrapping element, if not set a new one will be
generated
@param to_jid: destinee of the reference. If not specified, "to" attribute of
message_elt will be used.
"""
if uri is None:
if to_jid is None:
raise exceptions.InternalError('"to_jid" must be set if "uri is None"')
uri = xmpp_uri.build_xmpp_uri(path=to_jid.full())
if message_elt is None:
message_elt = domish.Element((None, "message"))
if to_jid is not None:
message_elt["to"] = to_jid.full()
else:
try:
to_jid = jid.JID(message_elt["to"])
except (KeyError, RuntimeError):
raise exceptions.InternalError(
'invalid "to" attribute in given message element: '
"{message_elt.toXml()}"
)
message_elt.addChild(self.build_ref_element(uri, type_, begin, end, anchor))
self._h.add_hint_elements(message_elt, [self._h.HINT_STORE])
client.send(message_elt)
@implementer(itx_xmpp.IDisco)
class XEP_0372_Handler(XMPPHandler):
def getDiscoInfo(self, requestor, service, nodeIdentifier=""):
return [disco.DiscoFeature(NS_REFS)]
def getDiscoItems(self, requestor, service, nodeIdentifier=""):
return []