comparison src/plugins/plugin_xep_0033.py @ 742:03744d9ebc13

plugin XEP-0033: implementation of the addressing feature: - frontends pass the recipients in the extra parameter of sendMessage - backend checks if the target server supports the feature (this is not done yet by prosody plugin) - features and identities are cached per profile and server - messages are duplicated in history for now (TODO: redesign the database) - echos signals are also duplicated to the sender (FIXME)
author souliane <souliane@mailoo.org>
date Wed, 11 Dec 2013 17:16:53 +0100
parents
children 192b804ee446
comparison
equal deleted inserted replaced
741:00318e60a06a 742:03744d9ebc13
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # SAT plugin for Extended Stanza Addressing (xep-0033)
5 # Copyright (C) 2013 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 import logging
21 from sat.core import exceptions
22 from wokkel import disco, iwokkel
23 from zope.interface import implements
24 from twisted.words.protocols.jabber.jid import JID
25 import copy
26 try:
27 from twisted.words.protocols.xmlstream import XMPPHandler
28 except ImportError:
29 from wokkel.subprotocols import XMPPHandler
30 from threading import Timer
31 from twisted.words.xish import domish
32 from twisted.internet import defer
33
34 from sat.core.sat_main import MessageSentAndStored, AbortSendMessage
35 from sat.tools.misc import TriggerManager
36
37 # TODO: fix Prosody "addressing" plugin to leave the concerned bcc according to the spec:
38 #
39 # http://xmpp.org/extensions/xep-0033.html#addr-type-bcc
40 # "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."
41 #
42 # http://xmpp.org/extensions/xep-0033.html#multicast
43 # "Each 'bcc' recipient MUST receive only the <address type='bcc'/> associated with that addressee."
44
45 # TODO: fix Prosody "addressing" plugin to determine itself if remote servers supports this XEP
46
47
48 NS_XMPP_CLIENT = "jabber:client"
49 NS_ADDRESS = "http://jabber.org/protocol/address"
50 ATTRIBUTES = ["jid", "uri", "node", "desc", "delivered", "type"]
51 ADDRESS_TYPES = ["to", "cc", "bcc", "replyto", "replyroom", "noreply"]
52
53 PLUGIN_INFO = {
54 "name": "Extended Stanza Addressing Protocol Plugin",
55 "import_name": "XEP-0033",
56 "type": "XEP",
57 "protocols": ["XEP-0033"],
58 "dependencies": [],
59 "main": "XEP_0033",
60 "handler": "yes",
61 "description": _("""Implementation of Extended Stanza Addressing""")
62 }
63
64
65 class XEP_0033(object):
66 """
67 Implementation for XEP 0033
68 """
69 def __init__(self, host):
70 logging.info(_("Extended Stanza Addressing plugin initialization"))
71 self.host = host
72 host.trigger.add("sendMessage", self.sendMessageTrigger, TriggerManager.MIN_PRIORITY)
73 host.trigger.add("MessageReceived", self.messageReceivedTrigger)
74
75 def sendMessageTrigger(self, mess_data, treatments, profile):
76 """Process the XEP-0033 related data to be sent"""
77
78 def treatment(mess_data):
79 if not 'address' in mess_data['extra']:
80 return mess_data
81
82 def discoCallback(entity):
83 if entity is None:
84 raise AbortSendMessage(_("XEP-0033 is being used but the server doesn't support it!"))
85 to = JID(mess_data["to"].host)
86 if to != entity:
87 logging.warning(_("Stanzas using XEP-0033 should be addressed to %s, not %s!") % (entity, to))
88 logging.warning(_("TODO: addressing has be fixed by the backend... fix it in the frontend!"))
89 mess_data["to"] = entity
90 element = mess_data['xml'].addElement('addresses', NS_ADDRESS)
91 entries = [entry.split(':') for entry in mess_data['extra']['address'].split('\n') if entry != '']
92 for type_, jid_ in entries:
93 element.addChild(domish.Element((None, 'address'), None, {'type': type_, 'jid': jid_}))
94 # when the prosody plugin is completed, we can immediately return mess_data from here
95 return self.sendAndStoreMessage(mess_data, entries, profile)
96
97 d = self.host.requestServerDisco(NS_ADDRESS, profile_key=profile)
98 d.addCallbacks(discoCallback, lambda dummy: discoCallback(None))
99 return d
100
101 treatments.addCallback(treatment)
102 return True
103
104 def sendAndStoreMessage(self, mess_data, entries, profile):
105 """Check if target servers support XEP-0033, send and store the messages
106 @raise: a friendly failure to let the core know that we sent the message already
107
108 Later we should be able to remove this method because:
109 # XXX: sending the messages should be done by the local server
110 # FIXME: for now we duplicate the messages in the history for each recipient, this should change
111 # FIXME: for now we duplicate the echoes to the sender, this should also change
112 Ideas:
113 - fix Prosody plugin to check if target server support the feature
114 - redesign the database to save only one entry to the database
115 - change the newMessage signal to eventually pass more than one recipient
116 """
117 def discoCallback(entity, to_jid):
118 new_data = copy.deepcopy(mess_data)
119 new_data['to'] = JID(to_jid)
120 new_data['xml']['to'] = to_jid
121 if entity:
122 if 'address' in mess_data['extra']:
123 self.host.sendAndStoreMessage(mess_data, False, profile)
124 # just to remember that the message has been sent
125 del mess_data['extra']['address']
126 # we still need to fill the history and signal the echo...
127 self.host.sendAndStoreMessage(new_data, True, profile)
128 else:
129 # target server misses the addressing feature
130 self.host.sendAndStoreMessage(new_data, False, profile)
131
132 def errback(failure, to_jid):
133 discoCallback(None, to_jid)
134
135 for type_, jid_ in entries:
136 d = defer.Deferred()
137 d.addCallback(self.host.requestServerDisco, JID(JID(jid_).host), profile_key=profile)
138 d.addCallbacks(discoCallback, errback, callbackArgs=[jid_], errbackArgs=[jid_])
139 d.callback(NS_ADDRESS)
140
141 raise MessageSentAndStored("XEP-0033 took over")
142
143 def messageReceivedTrigger(self, message, post_treat, profile):
144 """In order to save the addressing information in the history"""
145 def post_treat_addr(data, addresses):
146 data['extra']['addresses'] = ""
147 for address in addresses:
148 data['extra']['addresses'] += '%s:%s\n' % (address['type'], address['jid'])
149 return data
150
151 try:
152 addresses = message.elements(NS_ADDRESS, 'addresses').next()
153 post_treat.addCallback(post_treat_addr, addresses.children)
154 except StopIteration:
155 pass # no addresses
156 return True
157
158 def getHandler(self, profile):
159 return XEP_0033_handler(self, profile)
160
161
162 class XEP_0033_handler(XMPPHandler):
163 implements(iwokkel.IDisco)
164
165 def __init__(self, plugin_parent, profile):
166 self.plugin_parent = plugin_parent
167 self.host = plugin_parent.host
168 self.profile = profile
169
170 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
171 return [disco.DiscoFeature(NS_ADDRESS)]
172
173 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
174 return []