`Forward` method has been reworked and now includes a fallback. XEP-0297 ask to not use fallback, but following a discussion on xsf@, we agreed that this is a legacy thing and a fallback should nowadays be used, I'll propose a patch to the specification. A `message_forward` has been added to bridge. rel 461
209 lines
7.3 KiB
Python
209 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 []
|