Mercurial > libervia-backend
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) |