comparison libervia/backend/plugins/plugin_xep_0297.py @ 4382:b897d98b2c51 default tip

plugin XEP-0297: Reworked `forward` method and add bridge method: `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
author Goffi <goffi@goffi.org>
date Fri, 04 Jul 2025 12:33:42 +0200
parents 4b842c1fb686
children
comparison
equal deleted inserted replaced
4381:3c97717fd662 4382:b897d98b2c51
16 # GNU Affero General Public License for more details. 16 # GNU Affero General Public License for more details.
17 17
18 # You should have received a copy of the GNU Affero General Public License 18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>. 19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 20
21 from datetime import datetime
22 from typing import cast
23
24 from dateutil import tz
25 from twisted.internet import defer
26 from twisted.words.protocols.jabber import jid
27 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
28 from twisted.words.xish import domish
29 from wokkel import disco, iwokkel
30 from zope.interface import implementer
31
32 from libervia.backend import G
33 from libervia.backend.core import exceptions
21 from libervia.backend.core.constants import Const as C 34 from libervia.backend.core.constants import Const as C
22 from libervia.backend.core.i18n import _, D_ 35 from libervia.backend.core.core_types import SatXMPPEntity
36 from libervia.backend.models.core import MessageData
37 from libervia.backend.core.i18n import D_, _
23 from libervia.backend.core.log import getLogger 38 from libervia.backend.core.log import getLogger
24 39 from libervia.backend.memory.sqla_mapping import History
25 from twisted.internet import defer 40 from libervia.backend.plugins.plugin_xep_0428 import XEP_0428
41 from libervia.backend.tools.common.date_utils import date_fmt
42
43
26 44
27 log = getLogger(__name__) 45 log = getLogger(__name__)
28 46
29 from wokkel import disco, iwokkel 47
30
31 try:
32 from twisted.words.protocols.xmlstream import XMPPHandler
33 except ImportError:
34 from wokkel.subprotocols import XMPPHandler
35 from zope.interface import implementer
36
37 from twisted.words.xish import domish
38 48
39 PLUGIN_INFO = { 49 PLUGIN_INFO = {
40 C.PI_NAME: "Stanza Forwarding", 50 C.PI_NAME: "Stanza Forwarding",
41 C.PI_IMPORT_NAME: "XEP-0297", 51 C.PI_IMPORT_NAME: "XEP-0297",
42 C.PI_TYPE: "XEP", 52 C.PI_TYPE: "XEP",
43 C.PI_PROTOCOLS: ["XEP-0297"], 53 C.PI_PROTOCOLS: ["XEP-0297"],
54 C.PI_DEPENDENCIES: ["XEP-0428"],
44 C.PI_MAIN: "XEP_0297", 55 C.PI_MAIN: "XEP_0297",
45 C.PI_HANDLER: "yes", 56 C.PI_HANDLER: "yes",
46 C.PI_DESCRIPTION: D_("""Implementation of Stanza Forwarding"""), 57 C.PI_DESCRIPTION: D_("""Implementation of Stanza Forwarding"""),
47 } 58 }
48 59
49 60 NS_FORWARD = "urn:xmpp:forward:0"
50 class XEP_0297(object): 61
51 # FIXME: check this implementation which doesn't seems to be used 62
63 class XEP_0297:
64 # TODO: Handle forwarded message reception, for now the fallback will be used.
65 # TODO: Add a method to forward Pubsub Item, and notably blog content.
52 66
53 def __init__(self, host): 67 def __init__(self, host):
54 log.info(_("Stanza Forwarding plugin initialization")) 68 log.info(_("Stanza Forwarding plugin initialization"))
55 self.host = host 69 self.host = host
70 self._fallback = cast(XEP_0428, host.plugins["XEP-0428"])
71 host.register_namespace("forward", NS_FORWARD)
72 host.bridge.add_method(
73 "message_forward",
74 ".plugin",
75 in_sign="sss",
76 out_sign="",
77 method=self._forward,
78 async_=True,
79 )
56 80
57 def get_handler(self, client): 81 def get_handler(self, client):
58 return XEP_0297_handler(self, client.profile) 82 return XEP_0297_handler(self, client.profile)
59 83
60 @classmethod 84 @classmethod
70 element.defaultUri = uri 94 element.defaultUri = uri
71 for child in element.children: 95 for child in element.children:
72 if isinstance(child, domish.Element) and not child.uri: 96 if isinstance(child, domish.Element) and not child.uri:
73 XEP_0297.update_uri(child, uri) 97 XEP_0297.update_uri(child, uri)
74 98
75 def forward(self, stanza, to_jid, stamp, body="", profile_key=C.PROF_KEY_NONE): 99 def _forward(
100 self,
101 message_id: str,
102 recipient_jid_s: str,
103 profile_key: str
104 ) -> defer.Deferred[None]:
105 client = self.host.get_client(profile_key)
106 recipient_jid = jid.JID(recipient_jid_s)
107 return defer.ensureDeferred(self.forward_by_id(client, message_id, recipient_jid))
108
109 async def forward_by_id(
110 self,
111 client: SatXMPPEntity,
112 message_id: str,
113 recipient_jid: jid.JID
114 ) -> None:
115 history = cast(History, await G.storage.get(
116 client,
117 History,
118 History.uid,
119 message_id,
120 joined_loads=[History.messages, History.subjects, History.thread],
121 ))
122 if not history:
123 raise exceptions.NotFound(
124 f"No history found with message {message_id!r}."
125 )
126 # FIXME: Q&D way to get MessageData from History. History should have a proper
127 # way to do that.
128 serialised_history = history.serialise()
129 serialised_history["from"] = jid.JID(serialised_history["from"])
130 serialised_history["to"] = jid.JID(serialised_history["to"])
131 mess_data = MessageData(serialised_history)
132 client.generate_message_xml(mess_data)
133 message_elt = cast(domish.Element, mess_data["xml"])
134 timestamp_float = float(history.timestamp or history.received_timestamp)
135 timestamp = datetime.fromtimestamp(timestamp_float, tz.tzutc())
136
137 fallback_lines = [
138 f"{client.jid.userhost()} is forwarding this message to you:",
139 f"[{date_fmt(timestamp_float)}]",
140 f"From: {history.source_jid.full()}",
141 f"To: {history.dest_jid.full()}"
142 ]
143
144 for subject in history.subjects:
145 if subject.language:
146 fallback_lines.append(f"Subject [{subject.language}]: {subject.subject}")
147 else:
148 fallback_lines.append(f"Subject: {subject.subject}")
149
150 for message in history.messages:
151 if message.language:
152 fallback_lines.append(f"Message [{message.language}]: {message.message}")
153 else:
154 fallback_lines.append(f"Message: {message.message}")
155
156 fallback_msg = "\n".join(fallback_lines)
157
158 await self.forward(client, message_elt, recipient_jid, timestamp, fallback_msg)
159
160 async def forward(
161 self,
162 client: SatXMPPEntity,
163 stanza: domish.Element,
164 to_jid: jid.JID,
165 timestamp: datetime|None,
166 fallback_msg: str | None = None
167 ):
76 """Forward a message to the given JID. 168 """Forward a message to the given JID.
77 169
78 @param stanza (domish.Element): original stanza to be forwarded. 170 @param client: client instance.
79 @param to_jid (JID): recipient JID. 171 @param stanza: original stanza to be forwarded.
80 @param stamp (datetime): offset-aware timestamp of the original reception. 172 @param to_jid: recipient JID.
81 @param body (unicode): optional description. 173 @param timestamp: offset-aware timestamp of the original reception.
82 @param profile_key (unicode): %(doc_profile_key)s 174 @param body: optional description.
83 @return: a Deferred when the message has been sent 175 @return: a Deferred when the message has been sent
84 """ 176 """
85 # FIXME: this method is not used and doesn't use mess_data which should be used for client.send_message_data 177 message_elt = domish.Element((None, "message"))
86 # should it be deprecated? A method constructing the element without sending it seems more natural 178 message_elt["to"] = to_jid.full()
87 log.warning( 179 message_elt["type"] = stanza["type"]
88 "THIS METHOD IS DEPRECATED"
89 ) #  FIXME: we use this warning until we check the method
90 msg = domish.Element((None, "message"))
91 msg["to"] = to_jid.full()
92 msg["type"] = stanza["type"]
93
94 body_elt = domish.Element((None, "body"))
95 if body:
96 body_elt.addContent(body)
97 180
98 forwarded_elt = domish.Element((C.NS_FORWARD, "forwarded")) 181 forwarded_elt = domish.Element((C.NS_FORWARD, "forwarded"))
99 delay_elt = self.host.plugins["XEP-0203"].delay(stamp) 182 if timestamp:
100 forwarded_elt.addChild(delay_elt) 183 delay_elt = self.host.plugins["XEP-0203"].delay(timestamp)
101 if not stanza.uri: # None or '' 184 forwarded_elt.addChild(delay_elt)
185 if not stanza.uri:
102 XEP_0297.update_uri(stanza, "jabber:client") 186 XEP_0297.update_uri(stanza, "jabber:client")
103 forwarded_elt.addChild(stanza) 187 forwarded_elt.addChild(stanza)
104 188
105 msg.addChild(body_elt) 189 message_elt.addChild(domish.Element((None, "body")))
106 msg.addChild(forwarded_elt) 190 message_elt.addChild(forwarded_elt)
107 191 self._fallback.add_fallback_elt(message_elt, NS_FORWARD, fallback_msg)
108 client = self.host.get_client(profile_key) 192 return await client.send_message_data(
109 return defer.ensureDeferred(client.send_message_data({"xml": msg})) 193 MessageData({"xml": message_elt, "extra": {}})
194 )
110 195
111 196
112 @implementer(iwokkel.IDisco) 197 @implementer(iwokkel.IDisco)
113 class XEP_0297_handler(XMPPHandler): 198 class XEP_0297_handler(XMPPHandler):
114 199