comparison src/core/xmpp.py @ 1963:a2bc5089c2eb

backend, frontends: message refactoring (huge commit): /!\ several features are temporarily disabled, like notifications in frontends next step in refactoring, with the following changes: - jp: updated jp message to follow changes in backend/bridge - jp: added --lang, --subject, --subject_lang, and --type options to jp message + fixed unicode handling for jid - quick_frontend (QuickApp, QuickChat): - follow backend changes - refactored chat, message are now handled in OrderedDict and uid are kept so they can be updated - Message and Occupant classes handle metadata, so frontend just have to display them - Primitivus (Chat): - follow backend/QuickFrontend changes - info & standard messages are handled in the same MessageWidget class - improved/simplified handling of messages, removed update() method - user joined/left messages are merged when next to each other - a separator is shown when message is received while widget is out of focus, so user can quickly see the new messages - affiliation/role are shown (in a basic way for now) in occupants panel - removed "/me" messages handling, as it will be done by a backend plugin - message language is displayed when available (only one language per message for now) - fixed :history and :search commands - core (constants): new constants for messages type, XML namespace, entity type - core: *Message methods renamed to follow new code sytle (e.g. sendMessageToBridge => messageSendToBridge) - core (messages handling): fixed handling of language - core (messages handling): mes_data['from'] and ['to'] are now jid.JID - core (core.xmpp): reorganised message methods, added getNick() method to client.roster - plugin text commands: fixed plugin and adapted to new messages behaviour. client is now used in arguments instead of profile - plugins: added information for cancellation reason in CancelError calls - plugin XEP-0045: various improvments, but this plugin still need work: - trigger is used to avoid message already handled by the plugin to be handled a second time - changed the way to handle history, the last message from DB is checked and we request only messages since this one, in seconds (thanks Poezio folks :)) - subject reception is waited before sending the roomJoined signal, this way we are sure that everything including history is ready - cmd_* method now follow the new convention with client instead of profile - roomUserJoined and roomUserLeft messages are removed, the events are now handled with info message with a "ROOM_USER_JOINED" info subtype - probably other forgotten stuffs :p
author Goffi <goffi@goffi.org>
date Mon, 20 Jun 2016 18:41:53 +0200
parents 633b5c21aefd
children 046449cc2bff
comparison
equal deleted inserted replaced
1962:a45235d8dc93 1963:a2bc5089c2eb
23 from twisted.words.protocols.jabber.xmlstream import XMPPHandler 23 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
24 from twisted.words.protocols.jabber import xmlstream 24 from twisted.words.protocols.jabber import xmlstream
25 from twisted.words.protocols.jabber import error 25 from twisted.words.protocols.jabber import error
26 from twisted.words.protocols.jabber import jid 26 from twisted.words.protocols.jabber import jid
27 from twisted.python import failure 27 from twisted.python import failure
28 from wokkel import client, disco, xmppim, generic, iwokkel 28 from wokkel import client as wokkel_client, disco, xmppim, generic, iwokkel
29 from wokkel import delay 29 from wokkel import delay
30 from sat.core.log import getLogger 30 from sat.core.log import getLogger
31 log = getLogger(__name__) 31 log = getLogger(__name__)
32 from sat.core import exceptions 32 from sat.core import exceptions
33 from zope.interface import implements 33 from zope.interface import implements
34 import time 34 import time
35 import calendar 35 import calendar
36 import uuid 36 import uuid
37 37
38 38
39 class SatXMPPClient(client.XMPPClient): 39 class SatXMPPClient(wokkel_client.XMPPClient):
40 implements(iwokkel.IDisco) 40 implements(iwokkel.IDisco)
41 41
42 def __init__(self, host_app, profile, user_jid, password, host=None, port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES): 42 def __init__(self, host_app, profile, user_jid, password, host=None, port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES):
43 # XXX: DNS SRV records are checked when the host is not specified. 43 # XXX: DNS SRV records are checked when the host is not specified.
44 # If no SRV record is found, the host is directly extracted from the JID. 44 # If no SRV record is found, the host is directly extracted from the JID.
45 client.XMPPClient.__init__(self, user_jid, password, host or None, port or C.XMPP_C2S_PORT) 45 wokkel_client.XMPPClient.__init__(self, user_jid, password, host or None, port or C.XMPP_C2S_PORT)
46 self.factory.clientConnectionLost = self.connectionLost 46 self.factory.clientConnectionLost = self.connectionLost
47 self.factory.maxRetries = max_retries 47 self.factory.maxRetries = max_retries
48 self.__connected = False 48 self.__connected = False
49 self.profile = profile 49 self.profile = profile
50 self.host_app = host_app 50 self.host_app = host_app
78 self.xmlstream.send(iq_error_elt) 78 self.xmlstream.send(iq_error_elt)
79 79
80 def _authd(self, xmlstream): 80 def _authd(self, xmlstream):
81 if not self.host_app.trigger.point("XML Initialized", xmlstream, self.profile): 81 if not self.host_app.trigger.point("XML Initialized", xmlstream, self.profile):
82 return 82 return
83 client.XMPPClient._authd(self, xmlstream) 83 wokkel_client.XMPPClient._authd(self, xmlstream)
84 self.__connected = True 84 self.__connected = True
85 log.info(_("********** [%s] CONNECTED **********") % self.profile) 85 log.info(_("********** [%s] CONNECTED **********") % self.profile)
86 self.streamInitialized() 86 self.streamInitialized()
87 self.host_app.bridge.connected(self.profile, unicode(self.jid)) # we send the signal to the clients 87 self.host_app.bridge.connected(self.profile, unicode(self.jid)) # we send the signal to the clients
88 88
110 110
111 def initializationFailed(self, reason): 111 def initializationFailed(self, reason):
112 log.error(_(u"ERROR: XMPP connection failed for profile '%(profile)s': %(reason)s" % {'profile': self.profile, 'reason': reason})) 112 log.error(_(u"ERROR: XMPP connection failed for profile '%(profile)s': %(reason)s" % {'profile': self.profile, 'reason': reason}))
113 self.conn_deferred.errback(reason.value) 113 self.conn_deferred.errback(reason.value)
114 try: 114 try:
115 client.XMPPClient.initializationFailed(self, reason) 115 wokkel_client.XMPPClient.initializationFailed(self, reason)
116 except: 116 except:
117 # we already chained an errback, no need to raise an exception 117 # we already chained an errback, no need to raise an exception
118 pass 118 pass
119 119
120 def isConnected(self): 120 def isConnected(self):
150 return 150 return
151 151
152 message = {} 152 message = {}
153 subject = {} 153 subject = {}
154 extra = {} 154 extra = {}
155 data = {"from": message_elt['from'], 155 data = {"from": jid.JID(message_elt['from']),
156 "to": message_elt['to'], 156 "to": jid.JID(message_elt['to']),
157 "uid": message_elt.getAttribute('uid', unicode(uuid.uuid4())), # XXX: uid is not a standard attribute but may be added by plugins 157 "uid": message_elt.getAttribute('uid', unicode(uuid.uuid4())), # XXX: uid is not a standard attribute but may be added by plugins
158 "message": message, 158 "message": message,
159 "subject": subject, 159 "subject": subject,
160 "type": message_elt.getAttribute('type', 'normal'), 160 "type": message_elt.getAttribute('type', 'normal'),
161 "extra": extra} 161 "extra": extra}
167 else: 167 else:
168 client._mess_id_uid[(data['from'], data['stanza_id'])] = data['uid'] 168 client._mess_id_uid[(data['from'], data['stanza_id'])] = data['uid']
169 169
170 # message 170 # message
171 for e in message_elt.elements(C.NS_CLIENT, 'body'): 171 for e in message_elt.elements(C.NS_CLIENT, 'body'):
172 message[e.getAttribute('xml:lang','')] = unicode(e) 172 message[e.getAttribute((C.NS_XML,'lang'),'')] = unicode(e)
173 173
174 # subject 174 # subject
175 for e in message_elt.elements(C.NS_CLIENT, 'subject'): 175 for e in message_elt.elements(C.NS_CLIENT, 'subject'):
176 subject[e.getAttribute('xml:lang','')] = unicode(e) 176 subject[e.getAttribute((C.NS_XML, 'lang'),'')] = unicode(e)
177 177
178 # delay and timestamp 178 # delay and timestamp
179 try: 179 try:
180 delay_elt = message_elt.elements(delay.NS_DELAY, 'delay').next() 180 delay_elt = message_elt.elements(delay.NS_DELAY, 'delay').next()
181 except StopIteration: 181 except StopIteration:
185 data['timestamp'] = calendar.timegm(parsed_delay.stamp.utctimetuple()) 185 data['timestamp'] = calendar.timegm(parsed_delay.stamp.utctimetuple())
186 data['received_timestamp'] = unicode(time.time()) 186 data['received_timestamp'] = unicode(time.time())
187 if parsed_delay.sender: 187 if parsed_delay.sender:
188 data['delay_sender'] = parsed_delay.sender.full() 188 data['delay_sender'] = parsed_delay.sender.full()
189 189
190 def skipEmptyMessage(data): 190
191 if not data['message'] and not data['extra']: 191 post_treat.addCallback(self.skipEmptyMessage)
192 raise failure.Failure(exceptions.CancelError()) 192 post_treat.addCallback(self.addToHistory, client)
193 return data 193 post_treat.addErrback(self.treatmentsEb)
194 194 post_treat.addCallback(self.bridgeSignal, client, data)
195 def bridgeSignal(data): 195 post_treat.addErrback(self.cancelErrorTrap)
196 try:
197 data['extra']['received_timestamp'] = data['received_timestamp']
198 data['extra']['delay_sender'] = data['delay_sender']
199 except KeyError:
200 pass
201 if data is not None:
202 self.host.bridge.messageNew(data['uid'], data['timestamp'], data['from'].full(), data['to'].full(), data['message'], data['subject'], data['type'], data['extra'], profile=client.profile)
203 return data
204
205 def addToHistory(data):
206 data['from'] = jid.JID(data['from'])
207 data['to'] = jid.JID(data['to'])
208 self.host.memory.addToHistory(client, data)
209 return data
210
211 def treatmentsEb(failure_):
212 failure_.trap(exceptions.SkipHistory)
213 return data
214
215 def cancelErrorTrap(failure_):
216 """A message sending can be cancelled by a plugin treatment"""
217 failure_.trap(exceptions.CancelError)
218
219 post_treat.addCallback(skipEmptyMessage)
220 post_treat.addCallback(addToHistory)
221 post_treat.addErrback(treatmentsEb)
222 post_treat.addCallback(bridgeSignal)
223 post_treat.addErrback(cancelErrorTrap)
224 post_treat.callback(data) 196 post_treat.callback(data)
197
198 def skipEmptyMessage(self, data):
199 if not data['message'] and not data['extra'] and not data['subject']:
200 raise failure.Failure(exceptions.CancelError("Cancelled empty message"))
201 return data
202
203 def addToHistory(self, data, client):
204 return self.host.memory.addToHistory(client, data)
205
206 def treatmentsEb(self, failure_):
207 failure_.trap(exceptions.SkipHistory)
208
209 def bridgeSignal(self, dummy, client, data):
210 try:
211 data['extra']['received_timestamp'] = data['received_timestamp']
212 data['extra']['delay_sender'] = data['delay_sender']
213 except KeyError:
214 pass
215 if data is not None:
216 self.host.bridge.messageNew(data['uid'], data['timestamp'], data['from'].full(), data['to'].full(), data['message'], data['subject'], data['type'], data['extra'], profile=client.profile)
217 return data
218
219 def cancelErrorTrap(self, failure_):
220 """A message sending can be cancelled by a plugin treatment"""
221 failure_.trap(exceptions.CancelError)
225 222
226 223
227 class SatRosterProtocol(xmppim.RosterClientProtocol): 224 class SatRosterProtocol(xmppim.RosterClientProtocol):
228 225
229 def __init__(self, host): 226 def __init__(self, host):
281 """ 278 """
282 return xmppim.RosterClientProtocol.removeItem(self, to_jid) 279 return xmppim.RosterClientProtocol.removeItem(self, to_jid)
283 280
284 def getAttributes(self, item): 281 def getAttributes(self, item):
285 """Return dictionary of attributes as used in bridge from a RosterItem 282 """Return dictionary of attributes as used in bridge from a RosterItem
283
286 @param item: RosterItem 284 @param item: RosterItem
287 @return: dictionary of attributes""" 285 @return: dictionary of attributes
286 """
288 item_attr = {'to': unicode(item.subscriptionTo), 287 item_attr = {'to': unicode(item.subscriptionTo),
289 'from': unicode(item.subscriptionFrom), 288 'from': unicode(item.subscriptionFrom),
290 'ask': unicode(item.ask) 289 'ask': unicode(item.ask)
291 } 290 }
292 if item.name: 291 if item.name:
337 return self._groups.keys() 336 return self._groups.keys()
338 337
339 def getItem(self, entity_jid): 338 def getItem(self, entity_jid):
340 """Return RosterItem for a given jid 339 """Return RosterItem for a given jid
341 340
342 @param entity_jid: jid of the contact 341 @param entity_jid(jid.JID): jid of the contact
343 @return: RosterItem or None if contact is not in cache 342 @return(RosterItem, None): RosterItem instance
343 None if contact is not in cache
344 """ 344 """
345 return self._jids.get(entity_jid, None) 345 return self._jids.get(entity_jid, None)
346 346
347 def getJids(self): 347 def getJids(self):
348 """Return all jids of the roster""" 348 """Return all jids of the roster"""
382 jids.update(self.getJidsFromGroup(group)) 382 jids.update(self.getJidsFromGroup(group))
383 return jids 383 return jids
384 else: 384 else:
385 raise ValueError(u'Unexpected type_ {}'.format(type_)) 385 raise ValueError(u'Unexpected type_ {}'.format(type_))
386 386
387 def getNick(self, entity_jid):
388 """Return a nick name for an entity
389
390 return nick choosed by user if available
391 else return user part of entity_jid
392 """
393 item = self.getItem(entity_jid)
394 if item is None:
395 return entity_jid.user
396 else:
397 return item.name or entity_jid.user
398
387 399
388 class SatPresenceProtocol(xmppim.PresenceClientProtocol): 400 class SatPresenceProtocol(xmppim.PresenceClientProtocol):
389 401
390 def __init__(self, host): 402 def __init__(self, host):
391 xmppim.PresenceClientProtocol.__init__(self) 403 xmppim.PresenceClientProtocol.__init__(self)
472 484
473 if not self.host.trigger.point("presence_available", presence_elt, self.parent): 485 if not self.host.trigger.point("presence_available", presence_elt, self.parent):
474 return 486 return
475 self.send(presence_elt) 487 self.send(presence_elt)
476 488
489 @defer.inlineCallbacks
477 def subscribed(self, entity): 490 def subscribed(self, entity):
491 yield self.parent.roster.got_roster
478 xmppim.PresenceClientProtocol.subscribed(self, entity) 492 xmppim.PresenceClientProtocol.subscribed(self, entity)
479 self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile) 493 self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile)
480 item = self.parent.roster.getItem(entity) 494 item = self.parent.roster.getItem(entity)
481 if not item or not item.subscriptionTo: # we automatically subscribe to 'to' presence 495 if not item or not item.subscriptionTo: # we automatically subscribe to 'to' presence
482 log.debug(_('sending automatic "from" subscription request')) 496 log.debug(_('sending automatic "from" subscription request'))
492 506
493 def unsubscribedReceived(self, entity): 507 def unsubscribedReceived(self, entity):
494 log.debug(_(u"unsubscription confirmed for [%s]") % entity.userhost()) 508 log.debug(_(u"unsubscription confirmed for [%s]") % entity.userhost())
495 self.host.bridge.subscribe('unsubscribed', entity.userhost(), self.parent.profile) 509 self.host.bridge.subscribe('unsubscribed', entity.userhost(), self.parent.profile)
496 510
511 @defer.inlineCallbacks
497 def subscribeReceived(self, entity): 512 def subscribeReceived(self, entity):
498 log.debug(_(u"subscription request from [%s]") % entity.userhost()) 513 log.debug(_(u"subscription request from [%s]") % entity.userhost())
514 yield self.parent.roster.got_roster
499 item = self.parent.roster.getItem(entity) 515 item = self.parent.roster.getItem(entity)
500 if item and item.subscriptionTo: 516 if item and item.subscriptionTo:
501 # We automatically accept subscription if we are already subscribed to contact presence 517 # We automatically accept subscription if we are already subscribed to contact presence
502 log.debug(_('sending automatic subscription acceptance')) 518 log.debug(_('sending automatic subscription acceptance'))
503 self.subscribed(entity) 519 self.subscribed(entity)
504 else: 520 else:
505 self.host.memory.addWaitingSub('subscribe', entity.userhost(), self.parent.profile) 521 self.host.memory.addWaitingSub('subscribe', entity.userhost(), self.parent.profile)
506 self.host.bridge.subscribe('subscribe', entity.userhost(), self.parent.profile) 522 self.host.bridge.subscribe('subscribe', entity.userhost(), self.parent.profile)
507 523
524 @defer.inlineCallbacks
508 def unsubscribeReceived(self, entity): 525 def unsubscribeReceived(self, entity):
509 log.debug(_(u"unsubscription asked for [%s]") % entity.userhost()) 526 log.debug(_(u"unsubscription asked for [%s]") % entity.userhost())
527 yield self.parent.roster.got_roster
510 item = self.parent.roster.getItem(entity) 528 item = self.parent.roster.getItem(entity)
511 if item and item.subscriptionFrom: # we automatically remove contact 529 if item and item.subscriptionFrom: # we automatically remove contact
512 log.debug(_('automatic contact deletion')) 530 log.debug(_('automatic contact deletion'))
513 self.host.delContact(entity, self.parent.profile) 531 self.host.delContact(entity, self.parent.profile)
514 self.host.bridge.subscribe('unsubscribe', entity.userhost(), self.parent.profile) 532 self.host.bridge.subscribe('unsubscribe', entity.userhost(), self.parent.profile)