libervia-backend/libervia/backend/plugins/plugin_xep_0297.py
2025-09-13 10:46:10 +02:00

202 lines
7.3 KiB
Python

#!/usr/bin/env python3
# SAT plugin for Stanza Forwarding (XEP-0297)
# Copyright (C) 2009-2021 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 wokkel import disco, iwokkel
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_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, uri):
"""Update recursively the element URI.
@param element (domish.Element): element to update
@param uri (unicode): 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)
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,
):
"""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 = domish.Element((C.NS_FORWARD, "forwarded"))
if timestamp:
delay_elt = self.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)
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(iwokkel.IDisco)
class XEP_0297_handler(XMPPHandler):
def __init__(self, plugin_parent, profile):
self.plugin_parent = plugin_parent
self.host = plugin_parent.host
self.profile = profile
def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
return [disco.DiscoFeature(C.NS_FORWARD)]
def getDiscoItems(self, requestor, target, nodeIdentifier=""):
return []