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