Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_xep_0033.py @ 4071:4b842c1fb686
refactoring: renamed `sat` package to `libervia.backend`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 11:49:51 +0200 |
parents | sat/plugins/plugin_xep_0033.py@c23cad65ae99 |
children | 0d7bb4df2343 |
comparison
equal
deleted
inserted
replaced
4070:d10748475025 | 4071:4b842c1fb686 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 | |
4 # SAT plugin for Extended Stanza Addressing (xep-0033) | |
5 # Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from libervia.backend.core.i18n import _ | |
21 from libervia.backend.core.constants import Const as C | |
22 from libervia.backend.core.log import getLogger | |
23 | |
24 log = getLogger(__name__) | |
25 from libervia.backend.core import exceptions | |
26 from wokkel import disco, iwokkel | |
27 from zope.interface import implementer | |
28 from twisted.words.protocols.jabber.jid import JID | |
29 from twisted.python import failure | |
30 import copy | |
31 | |
32 try: | |
33 from twisted.words.protocols.xmlstream import XMPPHandler | |
34 except ImportError: | |
35 from wokkel.subprotocols import XMPPHandler | |
36 from twisted.words.xish import domish | |
37 from twisted.internet import defer | |
38 | |
39 from libervia.backend.tools import trigger | |
40 from time import time | |
41 | |
42 # TODO: fix Prosody "addressing" plugin to leave the concerned bcc according to the spec: | |
43 # | |
44 # http://xmpp.org/extensions/xep-0033.html#addr-type-bcc | |
45 # "This means that the server MUST remove these addresses before the stanza is delivered to anyone other than the given bcc addressee or the multicast service of the bcc addressee." | |
46 # | |
47 # http://xmpp.org/extensions/xep-0033.html#multicast | |
48 # "Each 'bcc' recipient MUST receive only the <address type='bcc'/> associated with that addressee." | |
49 | |
50 # TODO: fix Prosody "addressing" plugin to determine itself if remote servers supports this XEP | |
51 | |
52 | |
53 NS_XMPP_CLIENT = "jabber:client" | |
54 NS_ADDRESS = "http://jabber.org/protocol/address" | |
55 ATTRIBUTES = ["jid", "uri", "node", "desc", "delivered", "type"] | |
56 ADDRESS_TYPES = ["to", "cc", "bcc", "replyto", "replyroom", "noreply"] | |
57 | |
58 PLUGIN_INFO = { | |
59 C.PI_NAME: "Extended Stanza Addressing Protocol Plugin", | |
60 C.PI_IMPORT_NAME: "XEP-0033", | |
61 C.PI_TYPE: "XEP", | |
62 C.PI_PROTOCOLS: ["XEP-0033"], | |
63 C.PI_DEPENDENCIES: [], | |
64 C.PI_MAIN: "XEP_0033", | |
65 C.PI_HANDLER: "yes", | |
66 C.PI_DESCRIPTION: _("""Implementation of Extended Stanza Addressing"""), | |
67 } | |
68 | |
69 | |
70 class XEP_0033(object): | |
71 """ | |
72 Implementation for XEP 0033 | |
73 """ | |
74 | |
75 def __init__(self, host): | |
76 log.info(_("Extended Stanza Addressing plugin initialization")) | |
77 self.host = host | |
78 self.internal_data = {} | |
79 host.trigger.add( | |
80 "sendMessage", self.send_message_trigger, trigger.TriggerManager.MIN_PRIORITY | |
81 ) | |
82 host.trigger.add("message_received", self.message_received_trigger) | |
83 | |
84 def send_message_trigger( | |
85 self, client, mess_data, pre_xml_treatments, post_xml_treatments | |
86 ): | |
87 """Process the XEP-0033 related data to be sent""" | |
88 profile = client.profile | |
89 | |
90 def treatment(mess_data): | |
91 if not "address" in mess_data["extra"]: | |
92 return mess_data | |
93 | |
94 def disco_callback(entities): | |
95 if not entities: | |
96 log.warning( | |
97 _("XEP-0033 is being used but the server doesn't support it!") | |
98 ) | |
99 raise failure.Failure( | |
100 exceptions.CancelError("Cancelled by XEP-0033") | |
101 ) | |
102 if mess_data["to"] not in entities: | |
103 expected = _(" or ").join([entity.userhost() for entity in entities]) | |
104 log.warning( | |
105 _( | |
106 "Stanzas using XEP-0033 should be addressed to %(expected)s, not %(current)s!" | |
107 ) | |
108 % {"expected": expected, "current": mess_data["to"]} | |
109 ) | |
110 log.warning( | |
111 _( | |
112 "TODO: addressing has been fixed by the backend... fix it in the frontend!" | |
113 ) | |
114 ) | |
115 mess_data["to"] = list(entities)[0].userhostJID() | |
116 element = mess_data["xml"].addElement("addresses", NS_ADDRESS) | |
117 entries = [ | |
118 entry.split(":") | |
119 for entry in mess_data["extra"]["address"].split("\n") | |
120 if entry != "" | |
121 ] | |
122 for type_, jid_ in entries: | |
123 element.addChild( | |
124 domish.Element( | |
125 (None, "address"), None, {"type": type_, "jid": jid_} | |
126 ) | |
127 ) | |
128 # when the prosody plugin is completed, we can immediately return mess_data from here | |
129 self.send_and_store_message(mess_data, entries, profile) | |
130 log.debug("XEP-0033 took over") | |
131 raise failure.Failure(exceptions.CancelError("Cancelled by XEP-0033")) | |
132 | |
133 d = self.host.find_features_set(client, [NS_ADDRESS]) | |
134 d.addCallbacks(disco_callback, lambda __: disco_callback(None)) | |
135 return d | |
136 | |
137 post_xml_treatments.addCallback(treatment) | |
138 return True | |
139 | |
140 def send_and_store_message(self, mess_data, entries, profile): | |
141 """Check if target servers support XEP-0033, send and store the messages | |
142 @return: a friendly failure to let the core know that we sent the message already | |
143 | |
144 Later we should be able to remove this method because: | |
145 # XXX: sending the messages should be done by the local server | |
146 # FIXME: for now we duplicate the messages in the history for each recipient, this should change | |
147 # FIXME: for now we duplicate the echoes to the sender, this should also change | |
148 Ideas: | |
149 - fix Prosody plugin to check if target server support the feature | |
150 - redesign the database to save only one entry to the database | |
151 - change the message_new signal to eventually pass more than one recipient | |
152 """ | |
153 client = self.host.get_client(profile) | |
154 | |
155 def send(mess_data, skip_send=False): | |
156 d = defer.Deferred() | |
157 if not skip_send: | |
158 d.addCallback( | |
159 lambda ret: defer.ensureDeferred(client.send_message_data(ret)) | |
160 ) | |
161 d.addCallback( | |
162 lambda ret: defer.ensureDeferred(client.message_add_to_history(ret)) | |
163 ) | |
164 d.addCallback(client.message_send_to_bridge) | |
165 d.addErrback(lambda failure: failure.trap(exceptions.CancelError)) | |
166 return d.callback(mess_data) | |
167 | |
168 def disco_callback(entities, to_jid_s): | |
169 history_data = copy.deepcopy(mess_data) | |
170 history_data["to"] = JID(to_jid_s) | |
171 history_data["xml"]["to"] = to_jid_s | |
172 if entities: | |
173 if entities not in self.internal_data[timestamp]: | |
174 sent_data = copy.deepcopy(mess_data) | |
175 sent_data["to"] = JID(JID(to_jid_s).host) | |
176 sent_data["xml"]["to"] = JID(to_jid_s).host | |
177 send(sent_data) | |
178 self.internal_data[timestamp].append(entities) | |
179 # we still need to fill the history and signal the echo... | |
180 send(history_data, skip_send=True) | |
181 else: | |
182 # target server misses the addressing feature | |
183 send(history_data) | |
184 | |
185 def errback(failure, to_jid): | |
186 disco_callback(None, to_jid) | |
187 | |
188 timestamp = time() | |
189 self.internal_data[timestamp] = [] | |
190 defer_list = [] | |
191 for type_, jid_ in entries: | |
192 d = defer.Deferred() | |
193 d.addCallback( | |
194 self.host.find_features_set, client=client, jid_=JID(JID(jid_).host) | |
195 ) | |
196 d.addCallbacks( | |
197 disco_callback, errback, callbackArgs=[jid_], errbackArgs=[jid_] | |
198 ) | |
199 d.callback([NS_ADDRESS]) | |
200 defer_list.append(d) | |
201 d = defer.Deferred().addCallback(lambda __: self.internal_data.pop(timestamp)) | |
202 defer.DeferredList(defer_list).chainDeferred(d) | |
203 | |
204 def message_received_trigger(self, client, message, post_treat): | |
205 """In order to save the addressing information in the history""" | |
206 | |
207 def post_treat_addr(data, addresses): | |
208 data["extra"]["addresses"] = "" | |
209 for address in addresses: | |
210 # Depending how message has been constructed, we could get here | |
211 # some noise like "\n " instead of an address element. | |
212 if isinstance(address, domish.Element): | |
213 data["extra"]["addresses"] += "%s:%s\n" % ( | |
214 address["type"], | |
215 address["jid"], | |
216 ) | |
217 return data | |
218 | |
219 try: | |
220 addresses = next(message.elements(NS_ADDRESS, "addresses")) | |
221 except StopIteration: | |
222 pass # no addresses | |
223 else: | |
224 post_treat.addCallback(post_treat_addr, addresses.children) | |
225 return True | |
226 | |
227 def get_handler(self, client): | |
228 return XEP_0033_handler(self, client.profile) | |
229 | |
230 | |
231 @implementer(iwokkel.IDisco) | |
232 class XEP_0033_handler(XMPPHandler): | |
233 | |
234 def __init__(self, plugin_parent, profile): | |
235 self.plugin_parent = plugin_parent | |
236 self.host = plugin_parent.host | |
237 self.profile = profile | |
238 | |
239 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): | |
240 return [disco.DiscoFeature(NS_ADDRESS)] | |
241 | |
242 def getDiscoItems(self, requestor, target, nodeIdentifier=""): | |
243 return [] |