libervia-backend/libervia/backend/plugins/plugin_xep_0297.py
Goffi f5e0152b32 plugin XEP-0355: Namespace Delegation implementation:
The admin mode is implemented. It is needed to handle PEP with Pubsub Component.

rel 476
2026-04-14 23:04:04 +02:00

236 lines
8.6 KiB
Python

#!/usr/bin/env python3
# Libervia plugin for Stanza Forwarding (XEP-0297)
# Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org)
# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.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 <http://www.gnu.org/licenses/>.
from datetime import datetime
from typing import cast
from dateutil import tz
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 tx_xmpp import disco, itx_xmpp
from zope.interface import implementer
from libervia.backend import G
from libervia.backend.core import exceptions
from libervia.backend.core.constants import Const as C
from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.models.core import MessageData
from libervia.backend.core.i18n import D_, _
from libervia.backend.core.log import getLogger
from libervia.backend.memory.sqla_mapping import History
from libervia.backend.plugins.plugin_xep_0428 import XEP_0428
from libervia.backend.tools.common.date_utils import date_fmt
log = getLogger(__name__)
PLUGIN_INFO = {
C.PI_NAME: "Stanza Forwarding",
C.PI_IMPORT_NAME: "XEP-0297",
C.PI_TYPE: "XEP",
C.PI_MODES: [C.PLUG_MODE_COMPONENT],
C.PI_PROTOCOLS: ["XEP-0297"],
C.PI_DEPENDENCIES: ["XEP-0428"],
C.PI_MAIN: "XEP_0297",
C.PI_HANDLER: "yes",
C.PI_DESCRIPTION: D_("""Implementation of Stanza Forwarding"""),
}
NS_FORWARD = "urn:xmpp:forward:0"
class XEP_0297:
# TODO: Handle forwarded message reception, for now the fallback will be used.
# TODO: Add a method to forward Pubsub Item, and notably blog content.
def __init__(self, host):
log.info(_("Stanza Forwarding plugin initialization"))
self.host = host
self._fallback = cast(XEP_0428, host.plugins["XEP-0428"])
host.register_namespace("forward", NS_FORWARD)
host.bridge.add_method(
"message_forward",
".plugin",
in_sign="sss",
out_sign="",
method=self._forward,
async_=True,
)
def get_handler(self, client):
return XEP_0297_handler(self, client.profile)
@classmethod
def update_uri(cls, element: domish.Element, uri: str) -> None:
"""Update recursively the element URI.
@param element: element to update
@param uri: new URI
"""
# XXX: we need this because changing the URI of an existing element
# containing children doesn't update the children's blank URI.
element.uri = uri
element.defaultUri = uri
for child in element.children:
if isinstance(child, domish.Element) and not child.uri:
XEP_0297.update_uri(child, uri)
@staticmethod
def build_forwarded(
stanza: domish.Element, timestamp: datetime | None = None
) -> domish.Element:
"""Build a <forwarded/> element containing a stanza.
@param stanza: The stanza to wrap in forwarded element.
@param timestamp: Timestamp for delay element (XEP-0203).
@return: The <forwarded/> element with the stanza as child.
"""
forwarded_elt = domish.Element((C.NS_FORWARD, "forwarded"))
if timestamp:
delay_elt = G.host.plugins["XEP-0203"].delay(timestamp)
forwarded_elt.addChild(delay_elt)
if not stanza.uri:
XEP_0297.update_uri(stanza, "jabber:client")
forwarded_elt.addChild(stanza)
return forwarded_elt
def _forward(
self, message_id: str, recipient_jid_s: str, profile_key: str
) -> defer.Deferred[None]:
client = self.host.get_client(profile_key)
recipient_jid = jid.JID(recipient_jid_s)
return defer.ensureDeferred(self.forward_by_id(client, message_id, recipient_jid))
async def forward_by_id(
self,
client: SatXMPPEntity,
message_id: str,
recipient_jid: jid.JID,
) -> None:
history = cast(
History,
await G.storage.get(
client,
History,
History.uid,
message_id,
joined_loads=[History.messages, History.subjects, History.thread],
),
)
if not history:
raise exceptions.NotFound(f"No history found with message {message_id!r}.")
# FIXME: Q&D way to get MessageData from History. History should have a proper
# way to do that.
serialised_history = history.serialise()
serialised_history["from"] = jid.JID(serialised_history["from"])
serialised_history["to"] = jid.JID(serialised_history["to"])
mess_data = MessageData(serialised_history)
client.generate_message_xml(mess_data)
message_elt = cast(domish.Element, mess_data["xml"])
timestamp_float = float(history.timestamp or history.received_timestamp)
timestamp = datetime.fromtimestamp(timestamp_float, tz.tzutc())
fallback_lines = [
f"{client.jid.userhost()} is forwarding this message to you:",
f"[{date_fmt(timestamp_float)}]",
f"From: {history.source_jid.full()}",
f"To: {history.dest_jid.full()}",
]
for subject in history.subjects:
if subject.language:
fallback_lines.append(f"Subject [{subject.language}]: {subject.subject}")
else:
fallback_lines.append(f"Subject: {subject.subject}")
for message in history.messages:
if message.language:
fallback_lines.append(f"Message [{message.language}]: {message.message}")
else:
fallback_lines.append(f"Message: {message.message}")
fallback_msg = "\n".join(fallback_lines)
await self.forward(client, message_elt, recipient_jid, timestamp, fallback_msg)
async def forward(
self,
client: SatXMPPEntity,
stanza: domish.Element,
to_jid: jid.JID,
timestamp: datetime | None,
fallback_msg: str | None = None,
) -> defer.Deferred:
"""Forward a message to the given JID.
@param client: client instance.
@param stanza: original stanza to be forwarded.
@param to_jid: recipient JID.
@param timestamp: offset-aware timestamp of the original reception.
@param body: optional description.
@return: a Deferred when the message has been sent
"""
message_elt = domish.Element((None, "message"))
message_elt["to"] = to_jid.full()
message_elt["type"] = stanza["type"]
forwarded_elt = XEP_0297.build_forwarded(stanza, timestamp)
message_elt.addChild(domish.Element((None, "body")))
message_elt.addChild(forwarded_elt)
self._fallback.add_fallback_elt(message_elt, NS_FORWARD, fallback_msg)
return await client.send_message_data(
MessageData({"xml": message_elt, "extra": {}})
)
@implementer(itx_xmpp.IDisco)
class XEP_0297_handler(XMPPHandler):
def __init__(self, plugin_parent: XEP_0297, profile: str) -> None:
self.plugin_parent = plugin_parent
self.host = plugin_parent.host
self.profile = profile
def getDiscoInfo(
self, requestor: jid.JID, target: jid.JID, nodeIdentifier: str = ""
) -> list[disco.DiscoFeature]:
"""Get disco info for pubsub extended discovery
@param requestor: JID of the requesting entity
@param target: JID of the target entity
@param nodeIdentifier: optional node identifier
@return: list of disco features
"""
return [disco.DiscoFeature(C.NS_FORWARD)]
def getDiscoItems(
self, requestor: jid.JID, target: jid.JID, nodeIdentifier: str = ""
) -> list[disco.DiscoItem]:
"""Get disco items with extended discovery support
@param requestor: JID of the requesting entity
@param target: JID of the target entity
@param nodeIdentifier: optional node identifier
@return: list of disco items
"""
return []