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
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 []