comparison sat/plugins/plugin_xep_0033.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/plugins/plugin_xep_0033.py@6a004a22dd9e
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
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 sat.core.i18n import _
21 from sat.core.constants import Const as C
22 from sat.core.log import getLogger
23 log = getLogger(__name__)
24 from sat.core import exceptions
25 from wokkel import disco, iwokkel
26 from zope.interface import implements
27 from twisted.words.protocols.jabber.jid import JID
28 from twisted.python import failure
29 import copy
30 try:
31 from twisted.words.protocols.xmlstream import XMPPHandler
32 except ImportError:
33 from wokkel.subprotocols import XMPPHandler
34 from twisted.words.xish import domish
35 from twisted.internet import defer
36
37 from sat.tools import trigger
38 from time import time
39
40 # TODO: fix Prosody "addressing" plugin to leave the concerned bcc according to the spec:
41 #
42 # http://xmpp.org/extensions/xep-0033.html#addr-type-bcc
43 # "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."
44 #
45 # http://xmpp.org/extensions/xep-0033.html#multicast
46 # "Each 'bcc' recipient MUST receive only the <address type='bcc'/> associated with that addressee."
47
48 # TODO: fix Prosody "addressing" plugin to determine itself if remote servers supports this XEP
49
50
51 NS_XMPP_CLIENT = "jabber:client"
52 NS_ADDRESS = "http://jabber.org/protocol/address"
53 ATTRIBUTES = ["jid", "uri", "node", "desc", "delivered", "type"]
54 ADDRESS_TYPES = ["to", "cc", "bcc", "replyto", "replyroom", "noreply"]
55
56 PLUGIN_INFO = {
57 C.PI_NAME: "Extended Stanza Addressing Protocol Plugin",
58 C.PI_IMPORT_NAME: "XEP-0033",
59 C.PI_TYPE: "XEP",
60 C.PI_PROTOCOLS: ["XEP-0033"],
61 C.PI_DEPENDENCIES: [],
62 C.PI_MAIN: "XEP_0033",
63 C.PI_HANDLER: "yes",
64 C.PI_DESCRIPTION: _("""Implementation of Extended Stanza Addressing""")
65 }
66
67
68 class XEP_0033(object):
69 """
70 Implementation for XEP 0033
71 """
72 def __init__(self, host):
73 log.info(_("Extended Stanza Addressing plugin initialization"))
74 self.host = host
75 self.internal_data = {}
76 host.trigger.add("sendMessage", self.sendMessageTrigger, trigger.TriggerManager.MIN_PRIORITY)
77 host.trigger.add("MessageReceived", self.messageReceivedTrigger)
78
79 def sendMessageTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
80 """Process the XEP-0033 related data to be sent"""
81 profile = client.profile
82
83 def treatment(mess_data):
84 if not 'address' in mess_data['extra']:
85 return mess_data
86
87 def discoCallback(entities):
88 if not entities:
89 log.warning(_("XEP-0033 is being used but the server doesn't support it!"))
90 raise failure.Failure(exceptions.CancelError(u'Cancelled by XEP-0033'))
91 if mess_data["to"] not in entities:
92 expected = _(' or ').join([entity.userhost() for entity in entities])
93 log.warning(_(u"Stanzas using XEP-0033 should be addressed to %(expected)s, not %(current)s!") % {'expected': expected, 'current': mess_data["to"]})
94 log.warning(_(u"TODO: addressing has been fixed by the backend... fix it in the frontend!"))
95 mess_data["to"] = list(entities)[0].userhostJID()
96 element = mess_data['xml'].addElement('addresses', NS_ADDRESS)
97 entries = [entry.split(':') for entry in mess_data['extra']['address'].split('\n') if entry != '']
98 for type_, jid_ in entries:
99 element.addChild(domish.Element((None, 'address'), None, {'type': type_, 'jid': jid_}))
100 # when the prosody plugin is completed, we can immediately return mess_data from here
101 self.sendAndStoreMessage(mess_data, entries, profile)
102 log.debug("XEP-0033 took over")
103 raise failure.Failure(exceptions.CancelError(u'Cancelled by XEP-0033'))
104 d = self.host.findFeaturesSet(client, [NS_ADDRESS])
105 d.addCallbacks(discoCallback, lambda dummy: discoCallback(None))
106 return d
107
108 post_xml_treatments.addCallback(treatment)
109 return True
110
111 def sendAndStoreMessage(self, mess_data, entries, profile):
112 """Check if target servers support XEP-0033, send and store the messages
113 @return: a friendly failure to let the core know that we sent the message already
114
115 Later we should be able to remove this method because:
116 # XXX: sending the messages should be done by the local server
117 # FIXME: for now we duplicate the messages in the history for each recipient, this should change
118 # FIXME: for now we duplicate the echoes to the sender, this should also change
119 Ideas:
120 - fix Prosody plugin to check if target server support the feature
121 - redesign the database to save only one entry to the database
122 - change the messageNew signal to eventually pass more than one recipient
123 """
124 client = self.host.getClient(profile)
125 def send(mess_data, skip_send=False):
126 d = defer.Deferred()
127 if not skip_send:
128 d.addCallback(client.sendMessageData)
129 d.addCallback(client.messageAddToHistory)
130 d.addCallback(client.messageSendToBridge)
131 d.addErrback(lambda failure: failure.trap(exceptions.CancelError))
132 return d.callback(mess_data)
133
134 def discoCallback(entities, to_jid_s):
135 history_data = copy.deepcopy(mess_data)
136 history_data['to'] = JID(to_jid_s)
137 history_data['xml']['to'] = to_jid_s
138 if entities:
139 if entities not in self.internal_data[timestamp]:
140 sent_data = copy.deepcopy(mess_data)
141 sent_data['to'] = JID(JID(to_jid_s).host)
142 sent_data['xml']['to'] = JID(to_jid_s).host
143 send(sent_data)
144 self.internal_data[timestamp].append(entities)
145 # we still need to fill the history and signal the echo...
146 send(history_data, skip_send=True)
147 else:
148 # target server misses the addressing feature
149 send(history_data)
150
151 def errback(failure, to_jid):
152 discoCallback(None, to_jid)
153
154 timestamp = time()
155 self.internal_data[timestamp] = []
156 defer_list = []
157 for type_, jid_ in entries:
158 d = defer.Deferred()
159 d.addCallback(self.host.findFeaturesSet, client=client, jid_=JID(JID(jid_).host))
160 d.addCallbacks(discoCallback, errback, callbackArgs=[jid_], errbackArgs=[jid_])
161 d.callback([NS_ADDRESS])
162 defer_list.append(d)
163 d = defer.Deferred().addCallback(lambda dummy: self.internal_data.pop(timestamp))
164 defer.DeferredList(defer_list).chainDeferred(d)
165
166 def messageReceivedTrigger(self, client, message, post_treat):
167 """In order to save the addressing information in the history"""
168 def post_treat_addr(data, addresses):
169 data['extra']['addresses'] = ""
170 for address in addresses:
171 # Depending how message has been constructed, we could get here
172 # some noise like "\n " instead of an address element.
173 if isinstance(address, domish.Element):
174 data['extra']['addresses'] += '%s:%s\n' % (address['type'], address['jid'])
175 return data
176
177 try:
178 addresses = message.elements(NS_ADDRESS, 'addresses').next()
179 except StopIteration:
180 pass # no addresses
181 else:
182 post_treat.addCallback(post_treat_addr, addresses.children)
183 return True
184
185 def getHandler(self, client):
186 return XEP_0033_handler(self, client.profile)
187
188
189 class XEP_0033_handler(XMPPHandler):
190 implements(iwokkel.IDisco)
191
192 def __init__(self, plugin_parent, profile):
193 self.plugin_parent = plugin_parent
194 self.host = plugin_parent.host
195 self.profile = profile
196
197 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
198 return [disco.DiscoFeature(NS_ADDRESS)]
199
200 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
201 return []