comparison src/core/sat_main.py @ 2144:1d3f73e065e1

core, jp: component handling + client handling refactoring: - SàT can now handle components - plugin have now a "modes" key in PLUGIN_INFO where they declare if they can be used with clients and or components. They default to be client only. - components are really similar to clients, but with some changes in behaviour: * component has "entry point", which is a special plugin with a componentStart method, which is called just after component is connected * trigger end with a different suffixes (e.g. profileConnected vs profileConnectedComponent), so a plugin which manage both clients and components can have different workflow * for clients, only triggers of plugins handling client mode are launched * for components, only triggers of plugins needed in dependencies are launched. They all must handle component mode. * component have a sendHistory attribute (False by default) which can be set to True to allow saving sent messages into history * for convenience, "client" is still used in method even if it can now be a component * a new "component" boolean attribute tells if we have a component or a client * components have to add themselve Message protocol * roster and presence protocols are not added for components * component default port is 5347 (which is Prosody's default port) - asyncCreateProfile has been renamed for profileCreate, both to follow new naming convention and to prepare the transition to fully asynchronous bridge - createProfile has a new "component" attribute. When used to create a component, it must be set to a component entry point - jp: added --component argument to profile/create - disconnect bridge method is now asynchronous, this way frontends can know when disconnection is finished - new PI_* constants for PLUGIN_INFO values (not used everywhere yet) - client/component connection workflow has been moved to their classes instead of being a host methods - host.messageSend is now client.sendMessage, and former client.sendMessage is now client.sendMessageData. - identities are now handled in client.identities list, so it can be updated dynamically by plugins (in the future, frontends should be able to update them too through bridge) - profileConnecting* profileConnected* profileDisconnected* and getHandler now all use client instead of profile
author Goffi <goffi@goffi.org>
date Sun, 12 Feb 2017 17:55:43 +0100
parents be96beb7ca14
children 1bb9bf1b4150
comparison
equal deleted inserted replaced
2143:c3cac21157d4 2144:1d3f73e065e1
20 import sat 20 import sat
21 from sat.core.i18n import _, languageSwitch 21 from sat.core.i18n import _, languageSwitch
22 from twisted.application import service 22 from twisted.application import service
23 from twisted.internet import defer 23 from twisted.internet import defer
24 from twisted.words.protocols.jabber import jid 24 from twisted.words.protocols.jabber import jid
25 from twisted.words.xish import domish
26 from twisted.internet import reactor 25 from twisted.internet import reactor
27 from wokkel.xmppim import RosterItem 26 from wokkel.xmppim import RosterItem
28 from sat.core import xmpp 27 from sat.core import xmpp
29 from sat.core import exceptions 28 from sat.core import exceptions
30 from sat.core.log import getLogger 29 from sat.core.log import getLogger
39 from glob import glob 38 from glob import glob
40 from uuid import uuid4 39 from uuid import uuid4
41 import sys 40 import sys
42 import os.path 41 import os.path
43 import uuid 42 import uuid
44 import time
45 43
46 try: 44 try:
47 from collections import OrderedDict # only available from python 2.7 45 from collections import OrderedDict # only available from python 2.7
48 except ImportError: 46 except ImportError:
49 from ordereddict import OrderedDict 47 from ordereddict import OrderedDict
79 self.bridge.register_method("getFeatures", self.getFeatures) 77 self.bridge.register_method("getFeatures", self.getFeatures)
80 self.bridge.register_method("getProfileName", self.memory.getProfileName) 78 self.bridge.register_method("getProfileName", self.memory.getProfileName)
81 self.bridge.register_method("getProfilesList", self.memory.getProfilesList) 79 self.bridge.register_method("getProfilesList", self.memory.getProfilesList)
82 self.bridge.register_method("getEntityData", lambda jid_, keys, profile: self.memory.getEntityData(jid.JID(jid_), keys, profile)) 80 self.bridge.register_method("getEntityData", lambda jid_, keys, profile: self.memory.getEntityData(jid.JID(jid_), keys, profile))
83 self.bridge.register_method("getEntitiesData", self.memory._getEntitiesData) 81 self.bridge.register_method("getEntitiesData", self.memory._getEntitiesData)
84 self.bridge.register_method("asyncCreateProfile", self.memory.asyncCreateProfile) 82 self.bridge.register_method("profileCreate", self.memory.createProfile)
85 self.bridge.register_method("asyncDeleteProfile", self.memory.asyncDeleteProfile) 83 self.bridge.register_method("asyncDeleteProfile", self.memory.asyncDeleteProfile)
86 self.bridge.register_method("profileStartSession", self.memory.startSession) 84 self.bridge.register_method("profileStartSession", self.memory.startSession)
87 self.bridge.register_method("profileIsSessionStarted", self.memory._isSessionStarted) 85 self.bridge.register_method("profileIsSessionStarted", self.memory._isSessionStarted)
88 self.bridge.register_method("profileSetDefault", self.memory.profileSetDefault) 86 self.bridge.register_method("profileSetDefault", self.memory.profileSetDefault)
89 self.bridge.register_method("connect", self._connect) 87 self.bridge.register_method("connect", self._connect)
154 ui_contact_list.ContactList(self) 152 ui_contact_list.ContactList(self)
155 ui_profile_manager.ProfileManager(self) 153 ui_profile_manager.ProfileManager(self)
156 self.initialised.callback(None) 154 self.initialised.callback(None)
157 log.info(_(u"Backend is ready")) 155 log.info(_(u"Backend is ready"))
158 156
157 def _unimport_plugin(self, plugin_path):
158 """remove a plugin from sys.modules if it is there"""
159 try:
160 del sys.modules[plugin_path]
161 except KeyError:
162 pass
163
159 def _import_plugins(self): 164 def _import_plugins(self):
160 """Import all plugins found in plugins directory""" 165 """Import all plugins found in plugins directory"""
166 # FIXME: module imported but cancelled should be deleted
167 # TODO: make this more generic and reusable in tools.common
168 # FIXME: should use imp
169 # TODO: do not import all plugins if no needed: component plugins are not needed if we
170 # just use a client, and plugin blacklisting should be possible in sat.conf
161 plugins_path = os.path.dirname(sat.plugins.__file__) 171 plugins_path = os.path.dirname(sat.plugins.__file__)
162 plugin_glob = "plugin*." + C.PLUGIN_EXT 172 plugin_glob = "plugin*." + C.PLUGIN_EXT
163 plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename, glob(os.path.join(plugins_path, plugin_glob)))] 173 plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename, glob(os.path.join(plugins_path, plugin_glob)))]
164 plugins_to_import = {} # plugins we still have to import 174 plugins_to_import = {} # plugins we still have to import
165 for plug in plug_lst: 175 for plug in plug_lst:
166 plugin_path = 'sat.plugins.' + plug 176 plugin_path = 'sat.plugins.' + plug
167 try: 177 try:
168 __import__(plugin_path) 178 __import__(plugin_path)
169 except exceptions.MissingModule as e: 179 except exceptions.MissingModule as e:
170 try: 180 self._unimport_plugin(plugin_path)
171 del sys.modules[plugin_path]
172 except KeyError:
173 pass
174 log.warning(u"Can't import plugin [{path}] because of an unavailale third party module:\n{msg}".format( 181 log.warning(u"Can't import plugin [{path}] because of an unavailale third party module:\n{msg}".format(
175 path=plugin_path, msg=e)) 182 path=plugin_path, msg=e))
176 continue 183 continue
177 except exceptions.CancelError as e: 184 except exceptions.CancelError as e:
178 log.info(u"Plugin [{path}] cancelled its own import: {msg}".format(path=plugin_path, msg=e)) 185 log.info(u"Plugin [{path}] cancelled its own import: {msg}".format(path=plugin_path, msg=e))
186 self._unimport_plugin(plugin_path)
179 continue 187 continue
180 except Exception as e: 188 except Exception as e:
181 import traceback 189 import traceback
182 log.error(_(u"Can't import plugin [{path}]:\n{error}").format(path=plugin_path, error=traceback.format_exc())) 190 log.error(_(u"Can't import plugin [{path}]:\n{error}").format(path=plugin_path, error=traceback.format_exc()))
191 self._unimport_plugin(plugin_path)
183 continue 192 continue
184 mod = sys.modules[plugin_path] 193 mod = sys.modules[plugin_path]
185 plugin_info = mod.PLUGIN_INFO 194 plugin_info = mod.PLUGIN_INFO
186 import_name = plugin_info['import_name'] 195 import_name = plugin_info['import_name']
196
197 plugin_modes = plugin_info[u'modes'] = set(plugin_info.setdefault(u"modes", C.PLUG_MODE_DEFAULT))
198
199 # if the plugin is an entry point, it must work in component mode
200 if plugin_info[u'type'] == C.PLUG_TYPE_ENTRY_POINT:
201 # if plugin is an entrypoint, we cache it
202 if C.PLUG_MODE_COMPONENT not in plugin_modes:
203 log.error(_(u"{type} type must be used with {mode} mode, ignoring plugin").format(
204 type = C.PLUG_TYPE_ENTRY_POINT, mode = C.PLUG_MODE_COMPONENT))
205 self._unimport_plugin(plugin_path)
206 continue
207
187 if import_name in plugins_to_import: 208 if import_name in plugins_to_import:
188 log.error(_(u"Name conflict for import name [{import_name}], can't import plugin [{name}]").format(**plugin_info)) 209 log.error(_(u"Name conflict for import name [{import_name}], can't import plugin [{name}]").format(**plugin_info))
189 continue 210 continue
190 plugins_to_import[import_name] = (plugin_path, mod, plugin_info) 211 plugins_to_import[import_name] = (plugin_path, mod, plugin_info)
191 while True: 212 while True:
241 raise ImportError(u"Error during initiation") 262 raise ImportError(u"Error during initiation")
242 if 'handler' in plugin_info and plugin_info['handler'] == 'yes': 263 if 'handler' in plugin_info and plugin_info['handler'] == 'yes':
243 self.plugins[import_name].is_handler = True 264 self.plugins[import_name].is_handler = True
244 else: 265 else:
245 self.plugins[import_name].is_handler = False 266 self.plugins[import_name].is_handler = False
267 # we keep metadata as a Class attribute
268 self.plugins[import_name]._info = plugin_info
246 #TODO: test xmppclient presence and register handler parent 269 #TODO: test xmppclient presence and register handler parent
247 270
248 def pluginsUnload(self): 271 def pluginsUnload(self):
249 """Call unload method on every loaded plugin, if exists 272 """Call unload method on every loaded plugin, if exists
250 273
266 def _connect(self, profile_key, password='', options=None): 289 def _connect(self, profile_key, password='', options=None):
267 profile = self.memory.getProfileName(profile_key) 290 profile = self.memory.getProfileName(profile_key)
268 return self.connect(profile, password, options) 291 return self.connect(profile, password, options)
269 292
270 def connect(self, profile, password='', options=None, max_retries=C.XMPP_MAX_RETRIES): 293 def connect(self, profile, password='', options=None, max_retries=C.XMPP_MAX_RETRIES):
271 """Retrieve the individual parameters, authenticate the profile 294 """Connect a profile (i.e. connect client.component to XMPP server)
295
296 Retrieve the individual parameters, authenticate the profile
272 and initiate the connection to the associated XMPP server. 297 and initiate the connection to the associated XMPP server.
273
274 @param profile: %(doc_profile)s 298 @param profile: %(doc_profile)s
275 @param password (string): the SàT profile password 299 @param password (string): the SàT profile password
276 @param options (dict): connection options 300 @param options (dict): connection options. Key can be:
301 -
277 @param max_retries (int): max number of connection retries 302 @param max_retries (int): max number of connection retries
278 @return (D(bool)): 303 @return (D(bool)):
279 - True if the XMPP connection was already established 304 - True if the XMPP connection was already established
280 - False if the XMPP connection has been initiated (it may still fail) 305 - False if the XMPP connection has been initiated (it may still fail)
281 @raise exceptions.PasswordError: Profile password is wrong 306 @raise exceptions.PasswordError: Profile password is wrong
282 """ 307 """
283 if options is None: 308 if options is None:
284 options={} 309 options={}
285 def connectXMPPClient(dummy=None): 310 def connectProfile(dummy=None):
286 if self.isConnected(profile): 311 if self.isConnected(profile):
287 log.info(_("already connected !")) 312 log.info(_("already connected !"))
288 return True 313 return True
289 d = self._connectXMPPClient(profile, max_retries) 314
315 if self.memory.isComponent(profile):
316 d = xmpp.SatXMPPComponent.startConnection(self, profile, max_retries)
317 else:
318 d = xmpp.SatXMPPClient.startConnection(self, profile, max_retries)
290 return d.addCallback(lambda dummy: False) 319 return d.addCallback(lambda dummy: False)
291 320
292 d = self.memory.startSession(password, profile) 321 d = self.memory.startSession(password, profile)
293 d.addCallback(connectXMPPClient) 322 d.addCallback(connectProfile)
294 return d 323 return d
295
296 @defer.inlineCallbacks
297 def _connectXMPPClient(self, profile, max_retries):
298 """This part is called from connect when we have loaded individual parameters from memory"""
299 try:
300 port = int(self.memory.getParamA(C.FORCE_PORT_PARAM, "Connection", profile_key=profile))
301 except ValueError:
302 log.debug(_("Can't parse port value, using default value"))
303 port = None # will use default value 5222 or be retrieved from a DNS SRV record
304
305 password = yield self.memory.asyncGetParamA("Password", "Connection", profile_key=profile)
306 current = self.profiles[profile] = xmpp.SatXMPPClient(self, profile,
307 jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key=profile)),
308 password, self.memory.getParamA(C.FORCE_SERVER_PARAM, "Connection", profile_key=profile),
309 port, max_retries)
310
311 current.messageProt = xmpp.SatMessageProtocol(self)
312 current.messageProt.setHandlerParent(current)
313
314 current.roster = xmpp.SatRosterProtocol(self)
315 current.roster.setHandlerParent(current)
316
317 current.presence = xmpp.SatPresenceProtocol(self)
318 current.presence.setHandlerParent(current)
319
320 current.fallBack = xmpp.SatFallbackHandler(self)
321 current.fallBack.setHandlerParent(current)
322
323 current.versionHandler = xmpp.SatVersionHandler(C.APP_NAME_FULL,
324 self.full_version)
325 current.versionHandler.setHandlerParent(current)
326
327 current.identityHandler = xmpp.SatIdentityHandler()
328 current.identityHandler.setHandlerParent(current)
329
330 log.debug(_("setting plugins parents"))
331
332 plugin_conn_cb = []
333 for plugin in self.plugins.iteritems():
334 if plugin[1].is_handler:
335 plugin[1].getHandler(profile).setHandlerParent(current)
336 connected_cb = getattr(plugin[1], "profileConnected", None) # profile connected is called after client is ready and roster is got
337 if connected_cb:
338 plugin_conn_cb.append((plugin[0], connected_cb))
339 try:
340 yield plugin[1].profileConnecting(profile) # profile connecting is called before actually starting client
341 except AttributeError:
342 pass
343
344 current.startService()
345
346 yield current.getConnectionDeferred()
347 yield current.roster.got_roster # we want to be sure that we got the roster
348
349 # Call profileConnected callback for all plugins, and print error message if any of them fails
350 conn_cb_list = []
351 for dummy, callback in plugin_conn_cb:
352 conn_cb_list.append(defer.maybeDeferred(callback, profile))
353 list_d = defer.DeferredList(conn_cb_list)
354
355 def logPluginResults(results):
356 all_succeed = all([success for success, result in results])
357 if not all_succeed:
358 log.error(_(u"Plugins initialisation error"))
359 for idx, (success, result) in enumerate(results):
360 if not success:
361 log.error(u"error (plugin %(name)s): %(failure)s" %
362 {'name': plugin_conn_cb[idx][0], 'failure': result})
363
364 yield list_d.addCallback(logPluginResults) # FIXME: we should have a timeout here, and a way to know if a plugin freeze
365 # TODO: mesure launch time of each plugin
366 324
367 def disconnect(self, profile_key): 325 def disconnect(self, profile_key):
368 """disconnect from jabber server""" 326 """disconnect from jabber server"""
327 # FIXME: client should not be deleted if only disconnected
328 # it shoud be deleted only when session is finished
369 if not self.isConnected(profile_key): 329 if not self.isConnected(profile_key):
370 log.info(_("not connected !")) 330 # isConnected is checked here and not on client
371 return 331 # because client is deleted when session is ended
372 profile = self.memory.getProfileName(profile_key) 332 log.info(_(u"not connected !"))
373 log.info(_("Disconnecting...")) 333 return defer.succeed(None)
374 self.profiles[profile].stopService() 334 client = self.getClient(profile_key)
375 for plugin in self.plugins.iteritems(): 335 return client.entityDisconnect()
376 disconnected_cb = getattr(plugin[1], "profileDisconnected", None)
377 if disconnected_cb:
378 disconnected_cb(profile)
379 336
380 def getFeatures(self, profile_key=C.PROF_KEY_NONE): 337 def getFeatures(self, profile_key=C.PROF_KEY_NONE):
381 """Get available features 338 """Get available features
382 339
383 Return list of activated plugins and plugin specific data 340 Return list of activated plugins and plugin specific data
437 394
438 def getContactsFromGroup(self, group, profile_key): 395 def getContactsFromGroup(self, group, profile_key):
439 client = self.getClient(profile_key) 396 client = self.getClient(profile_key)
440 return [jid_.full() for jid_ in client.roster.getJidsFromGroup(group)] 397 return [jid_.full() for jid_ in client.roster.getJidsFromGroup(group)]
441 398
442 def purgeClient(self, profile): 399 def purgeEntity(self, profile):
443 """Remove reference to a profile client and purge cache 400 """Remove reference to a profile client/component and purge cache
444 the garbage collector can then free the memory""" 401
402 the garbage collector can then free the memory
403 """
445 try: 404 try:
446 del self.profiles[profile] 405 del self.profiles[profile]
447 except KeyError: 406 except KeyError:
448 log.error(_("Trying to remove reference to a client not referenced")) 407 log.error(_("Trying to remove reference to a client not referenced"))
449 self.memory.purgeProfileSession(profile) 408 else:
409 self.memory.purgeProfileSession(profile)
450 410
451 def startService(self): 411 def startService(self):
452 log.info(u"Salut à toi ô mon frère !") 412 log.info(u"Salut à toi ô mon frère !")
453 413
454 def stopService(self): 414 def stopService(self):
513 @param name: name of the option 473 @param name: name of the option
514 @return: unicode representation of the option 474 @return: unicode representation of the option
515 """ 475 """
516 return unicode(self.memory.getConfig(section, name, '')) 476 return unicode(self.memory.getConfig(section, name, ''))
517 477
478 def logErrback(self, failure_):
479 """generic errback logging
480
481 can be used as last errback to show unexpected error
482 """
483 log.error(_(u"Unexpected error: {}".format(failure_)))
484 return failure_
485
518 ## Client management ## 486 ## Client management ##
519 487
520 def setParam(self, name, value, category, security_limit, profile_key): 488 def setParam(self, name, value, category, security_limit, profile_key):
521 """set wanted paramater and notice observers""" 489 """set wanted paramater and notice observers"""
522 self.memory.setParam(name, value, category, security_limit, profile_key) 490 self.memory.setParam(name, value, category, security_limit, profile_key)
535 return self.profiles[profile].isConnected() 503 return self.profiles[profile].isConnected()
536 504
537 505
538 ## XMPP methods ## 506 ## XMPP methods ##
539 507
540 def generateMessageXML(self, data):
541 """Generate <message/> stanza from message data
542
543 @param data(dict): message data
544 domish element will be put in data['xml']
545 following keys are needed:
546 - from
547 - to
548 - uid: can be set to '' if uid attribute is not wanted
549 - message
550 - type
551 - subject
552 - extra
553 @return (dict) message data
554 """
555 data['xml'] = message_elt = domish.Element((None, 'message'))
556 message_elt["to"] = data["to"].full()
557 message_elt["from"] = data['from'].full()
558 message_elt["type"] = data["type"]
559 if data['uid']: # key must be present but can be set to ''
560 # by a plugin to avoid id on purpose
561 message_elt['id'] = data['uid']
562 for lang, subject in data["subject"].iteritems():
563 subject_elt = message_elt.addElement("subject", content=subject)
564 if lang:
565 subject_elt[(C.NS_XML, 'lang')] = lang
566 for lang, message in data["message"].iteritems():
567 body_elt = message_elt.addElement("body", content=message)
568 if lang:
569 body_elt[(C.NS_XML, 'lang')] = lang
570 try:
571 thread = data['extra']['thread']
572 except KeyError:
573 if 'thread_parent' in data['extra']:
574 raise exceptions.InternalError(u"thread_parent found while there is not associated thread")
575 else:
576 thread_elt = message_elt.addElement("thread", content=thread)
577 try:
578 thread_elt["parent"] = data["extra"]["thread_parent"]
579 except KeyError:
580 pass
581 return data
582
583 def _messageSend(self, to_jid_s, message, subject=None, mess_type='auto', extra=None, profile_key=C.PROF_KEY_NONE): 508 def _messageSend(self, to_jid_s, message, subject=None, mess_type='auto', extra=None, profile_key=C.PROF_KEY_NONE):
584 client = self.getClient(profile_key) 509 client = self.getClient(profile_key)
585 to_jid = jid.JID(to_jid_s) 510 to_jid = jid.JID(to_jid_s)
586 #XXX: we need to use the dictionary comprehension because D-Bus return its own types, and pickle can't manage them. TODO: Need to find a better way 511 #XXX: we need to use the dictionary comprehension because D-Bus return its own types, and pickle can't manage them. TODO: Need to find a better way
587 return self.messageSend(client, to_jid, message, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()}) 512 return client.sendMessage(to_jid, message, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()})
588
589 def messageSend(self, client, to_jid, message, subject=None, mess_type='auto', extra=None, uid=None, no_trigger=False):
590 """Send a message to an entity
591
592 @param to_jid(jid.JID): destinee of the message
593 @param message(dict): message body, key is the language (use '' when unknown)
594 @param subject(dict): message subject, key is the language (use '' when unknown)
595 @param mess_type(str): one of standard message type (cf RFC 6121 §5.2.2) or:
596 - auto: for automatic type detection
597 - info: for information ("info_type" can be specified in extra)
598 @param extra(dict, None): extra data. Key can be:
599 - info_type: information type, can be
600 TODO
601 @param uid(unicode, None): unique id:
602 should be unique at least in this XMPP session
603 if None, an uuid will be generated
604 @param no_trigger (bool): if True, messageSend trigger will no be used
605 useful when a message need to be sent without any modification
606 """
607 profile = client.profile
608 if subject is None:
609 subject = {}
610 if extra is None:
611 extra = {}
612 data = { # dict is similar to the one used in client.onMessage
613 "from": client.jid,
614 "to": to_jid,
615 "uid": uid or unicode(uuid.uuid4()),
616 "message": message,
617 "subject": subject,
618 "type": mess_type,
619 "extra": extra,
620 "timestamp": time.time(),
621 }
622 pre_xml_treatments = defer.Deferred() # XXX: plugin can add their pre XML treatments to this deferred
623 post_xml_treatments = defer.Deferred() # XXX: plugin can add their post XML treatments to this deferred
624
625 if data["type"] == "auto":
626 # we try to guess the type
627 if data["subject"]:
628 data["type"] = 'normal'
629 elif not data["to"].resource: # if to JID has a resource, the type is not 'groupchat'
630 # we may have a groupchat message, we check if the we know this jid
631 try:
632 entity_type = self.memory.getEntityData(data["to"], ['type'], profile)["type"]
633 #FIXME: should entity_type manage resources ?
634 except (exceptions.UnknownEntityError, KeyError):
635 entity_type = "contact"
636
637 if entity_type == "chatroom":
638 data["type"] = 'groupchat'
639 else:
640 data["type"] = 'chat'
641 else:
642 data["type"] == 'chat'
643 data["type"] == "chat" if data["subject"] else "normal"
644
645 # FIXME: send_only is used by libervia's OTR plugin to avoid
646 # the triggers from frontend, and no_trigger do the same
647 # thing internally, this could be unified
648 send_only = data['extra'].get('send_only', False)
649
650 if not no_trigger and not send_only:
651 if not self.trigger.point("messageSend", client, data, pre_xml_treatments, post_xml_treatments):
652 return defer.succeed(None)
653
654 log.debug(_(u"Sending message (type {type}, to {to})").format(type=data["type"], to=to_jid.full()))
655
656 pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(data))
657 pre_xml_treatments.chainDeferred(post_xml_treatments)
658 post_xml_treatments.addCallback(client.sendMessage)
659 if send_only:
660 log.debug(_("Triggers, storage and echo have been inhibited by the 'send_only' parameter"))
661 else:
662 post_xml_treatments.addCallback(self.messageAddToHistory, client)
663 post_xml_treatments.addCallback(self.messageSendToBridge, client)
664 post_xml_treatments.addErrback(self._cancelErrorTrap)
665 pre_xml_treatments.callback(data)
666 return pre_xml_treatments
667
668 def _cancelErrorTrap(self, failure):
669 """A message sending can be cancelled by a plugin treatment"""
670 failure.trap(exceptions.CancelError)
671
672 def messageAddToHistory(self, data, client):
673 """Store message into database (for local history)
674
675 @param data: message data dictionnary
676 @param client: profile's client
677 """
678 if data[u"type"] != C.MESS_TYPE_GROUPCHAT:
679 # we don't add groupchat message to history, as we get them back
680 # and they will be added then
681 if data[u'message'] or data[u'subject']: # we need a message to store
682 self.memory.addToHistory(client, data)
683 else:
684 log.warning(u"No message found") # empty body should be managed by plugins before this point
685 return data
686
687 def messageSendToBridge(self, data, client):
688 """Send message to bridge, so frontends can display it
689
690 @param data: message data dictionnary
691 @param client: profile's client
692 """
693 if data[u"type"] != C.MESS_TYPE_GROUPCHAT:
694 # we don't send groupchat message to bridge, as we get them back
695 # and they will be added the
696 if data[u'message'] or data[u'subject']: # we need a message to send something
697 # We send back the message, so all frontends are aware of it
698 self.bridge.messageNew(data[u'uid'], data[u'timestamp'], data[u'from'].full(), data[u'to'].full(), data[u'message'], data[u'subject'], data[u'type'], data[u'extra'], profile=client.profile)
699 else:
700 log.warning(_(u"No message found"))
701 return data
702 513
703 def _setPresence(self, to="", show="", statuses=None, profile_key=C.PROF_KEY_NONE): 514 def _setPresence(self, to="", show="", statuses=None, profile_key=C.PROF_KEY_NONE):
704 return self.setPresence(jid.JID(to) if to else None, show, statuses, profile_key) 515 return self.setPresence(jid.JID(to) if to else None, show, statuses, profile_key)
705 516
706 def setPresence(self, to_jid=None, show="", statuses=None, profile_key=C.PROF_KEY_NONE): 517 def setPresence(self, to_jid=None, show="", statuses=None, profile_key=C.PROF_KEY_NONE):