# HG changeset patch # User Goffi # Date 1402151729 -7200 # Node ID e88bff4c8b777bb45c25a31c432ecf2c51541ded # Parent 854880a31717990c7f0d20ca681733d91bdbcb73 core (XMPP): sendMessage refactoring: - better separation of message sending actions - use of more generic exceptions to hook the behaviour (SkipHistory and CancelError) - use of raise instead of return - use of failure.trap diff -r 854880a31717 -r e88bff4c8b77 src/core/exceptions.py --- a/src/core/exceptions.py Tue Jun 03 17:10:12 2014 +0200 +++ b/src/core/exceptions.py Sat Jun 07 16:35:29 2014 +0200 @@ -92,3 +92,6 @@ class PasswordError(Exception): pass + +class SkipHistory(Exception): # used in MessageReceivedTrigger to avoid history writting + pass diff -r 854880a31717 -r e88bff4c8b77 src/core/sat_main.py --- a/src/core/sat_main.py Tue Jun 03 17:10:12 2014 +0200 +++ b/src/core/sat_main.py Sat Jun 07 16:35:29 2014 +0200 @@ -54,22 +54,6 @@ return "sat_id_" + str(sat_id) -class MessageSentAndStored(Exception): - """ Exception to raise if the message has been already sent and stored in the - history by the trigger, so the rest of the process should be stopped. This - should normally be raised by the trigger with the minimal priority """ - def __init__(self, reason, mess_data): - Exception.__init__(self, reason) - self.mess_data = mess_data # added for testing purpose - - -class AbortSendMessage(Exception): - """ Exception to raise if sending the message should be aborted. This can be - raised by any trigger but a side action should be planned by the trigger - to inform the user about what happened """ - pass - - class SAT(service.Service): @property @@ -433,7 +417,7 @@ return self.profiles[profile].isConnected() - ## jabber methods ## + ## XMPP methods ## def getWaitingConf(self, profile_key=None): assert(profile_key) @@ -444,6 +428,18 @@ ret.append((conf_id, conf_type, data)) return ret + def generateMessageXML(self, mess_data): + mess_data['xml'] = domish.Element((None, 'message')) + mess_data['xml']["to"] = mess_data["to"].full() + mess_data['xml']["from"] = mess_data['from'].full() + mess_data['xml']["type"] = mess_data["type"] + mess_data['xml']['id'] = str(uuid4()) + if mess_data["subject"]: + mess_data['xml'].addElement("subject", None, mess_data['subject']) + if mess_data["message"]: # message without body are used to send chat states + mess_data['xml'].addElement("body", None, mess_data["message"]) + return mess_data + def _sendMessage(self, to_s, msg, subject=None, mess_type='auto', extra={}, profile_key=C.PROF_KEY_NONE): to_jid = jid.JID(to_s) #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 @@ -454,11 +450,11 @@ profile = self.memory.getProfileName(profile_key) assert(profile) client = self.profiles[profile] - current_jid = client.jid if extra is None: extra = {} mess_data = { # we put data in a dict, so trigger methods can change them "to": to_jid, + "from": client.jid, "message": msg, "subject": subject, "type": mess_type, @@ -493,67 +489,67 @@ log.debug(_("Sending jabber message of type [%(type)s] to %(to)s...") % {"type": mess_data["type"], "to": to_jid.full()}) - def generateXML(mess_data): - mess_data['xml'] = domish.Element((None, 'message')) - mess_data['xml']["to"] = mess_data["to"].full() - mess_data['xml']["from"] = current_jid.full() - mess_data['xml']["type"] = mess_data["type"] - if mess_data["subject"]: - mess_data['xml'].addElement("subject", None, subject) - # message without body are used to send chat states - if mess_data["message"]: - mess_data['xml'].addElement("body", None, mess_data["message"]) - return mess_data + def cancelErrorTrap(failure): + """A message sending can be cancelled by a plugin treatment""" + failure.trap(exceptions.CancelError) - def sendErrback(e): - text = '%s: %s' % (e.value.__class__.__name__, e.getErrorMessage()) - if e.check(MessageSentAndStored): - log.debug(text) - elif e.check(AbortSendMessage): - log.warning(text) - return e - else: - log.error("Unmanaged exception: %s" % text) - return e - pre_xml_treatments.addCallback(generateXML) + pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(mess_data)) pre_xml_treatments.chainDeferred(post_xml_treatments) - post_xml_treatments.addCallback(self.sendAndStoreMessage, False, profile) - post_xml_treatments.addErrback(sendErrback) + post_xml_treatments.addCallback(self._sendMessageToStream, client) + post_xml_treatments.addCallback(self._storeMessage, client) + post_xml_treatments.addCallback(self.sendMessageToBridge, client) + post_xml_treatments.addErrback(cancelErrorTrap) pre_xml_treatments.callback(mess_data) return pre_xml_treatments - def sendAndStoreMessage(self, mess_data, skip_send=False, profile=None): - """Actually send and store the message to history, after all the treatments have been done. - This has been moved outside the main sendMessage method because it is used by XEP-0033 to complete a server-side feature not yet - implemented by the prosody plugin. - @param mess_data: message data dictionary - @param skip_send: set to True to skip sending the message to only store it - @param profile: profile + def _sendMessageToStream(self, mess_data, client): + """Actualy send the message to the server + + @param mess_data: message data dictionnary + @param client: profile's client """ - try: - client = self.profiles[profile] - except KeyError: - log.error(_("Trying to send a message with no profile")) - return - current_jid = client.jid - if not skip_send: - client.xmlstream.send(mess_data['xml']) + client.xmlstream.send(mess_data['xml']) + return mess_data + + def _storeMessage(self, mess_data, client): + """Store message into database (for local history) + + @param mess_data: message data dictionnary + @param client: profile's client + """ if mess_data["type"] != "groupchat": # we don't add groupchat message to history, as we get them back # and they will be added then if mess_data['message']: # we need a message to save something - self.memory.addToHistory(current_jid, mess_data['to'], + self.memory.addToHistory(client.jid, mess_data['to'], unicode(mess_data["message"]), unicode(mess_data["type"]), mess_data['extra'], - profile=profile) + profile=client.profile) + else: + log.warning(_("No message found")) # empty body should be managed by plugins before this point + return mess_data + + def sendMessageToBridge(self, mess_data, client): + """Send message to bridge, so frontends can display it + + @param mess_data: message data dictionnary + @param client: profile's client + """ + if mess_data["type"] != "groupchat": + # we don't send groupchat message back to bridge, as we get them back + # and they will be added the + if mess_data['message']: # we need a message to save something # We send back the message, so all clients are aware of it - self.bridge.newMessage(mess_data['xml']['from'], + self.bridge.newMessage(mess_data['from'].full(), unicode(mess_data["message"]), mess_type=mess_data["type"], - to_jid=mess_data['xml']['to'], + to_jid=mess_data['to'].full(), extra=mess_data['extra'], - profile=profile) + profile=client.profile) + else: + log.warning(_("No message found")) + return mess_data def _setPresence(self, to="", show="", statuses=None, profile_key=C.PROF_KEY_NONE): return self.setPresence(jid.JID(to) if to else None, show, statuses, profile_key) diff -r 854880a31717 -r e88bff4c8b77 src/plugins/plugin_misc_text_commands.py --- a/src/plugins/plugin_misc_text_commands.py Tue Jun 03 17:10:12 2014 +0200 +++ b/src/plugins/plugin_misc_text_commands.py Sat Jun 07 16:35:29 2014 +0200 @@ -19,12 +19,12 @@ from sat.core.i18n import _ from sat.core.constants import Const as C -from sat.core.sat_main import MessageSentAndStored +from sat.core import exceptions from twisted.words.protocols.jabber import jid from twisted.internet import defer -from twisted.python.failure import Failure from sat.core.log import getLogger log = getLogger(__name__) +from twisted.python import failure PLUGIN_INFO = { "name": "Text commands", @@ -128,7 +128,8 @@ if ret: return mess_data else: - return Failure(MessageSentAndStored("text commands took over", mess_data)) + log.debug("text commands took over") + raise failure.Failure(exceptions.CancelError()) try: mess_data["unparsed"] = msg[1 + len(command):] # part not yet parsed of the message @@ -136,7 +137,8 @@ d.addCallback(retHandling) except KeyError: self.feedBack(_("Unknown command /%s. ") % command + self.HELP_SUGGESTION, mess_data, profile) - return Failure(MessageSentAndStored("text commands took over", mess_data)) + self.debug("text commands took over") + raise failure.Failure(exceptions.CancelError()) return d or mess_data # if a command is detected, we should have a deferred, else we send the message normally diff -r 854880a31717 -r e88bff4c8b77 src/plugins/plugin_xep_0033.py --- a/src/plugins/plugin_xep_0033.py Tue Jun 03 17:10:12 2014 +0200 +++ b/src/plugins/plugin_xep_0033.py Sat Jun 07 16:35:29 2014 +0200 @@ -20,10 +20,11 @@ from sat.core.i18n import _ from sat.core.log import getLogger log = getLogger(__name__) +from sat.core import exceptions from wokkel import disco, iwokkel from zope.interface import implements from twisted.words.protocols.jabber.jid import JID -from twisted.python.failure import Failure +from twisted.python import failure import copy try: from twisted.words.protocols.xmlstream import XMPPHandler @@ -32,7 +33,6 @@ from twisted.words.xish import domish from twisted.internet import defer -from sat.core.sat_main import MessageSentAndStored, AbortSendMessage from sat.tools.misc import TriggerManager from time import time @@ -84,7 +84,8 @@ def discoCallback(entities): if not entities: - return Failure(AbortSendMessage(_("XEP-0033 is being used but the server doesn't support it!"))) + log.warning(_("XEP-0033 is being used but the server doesn't support it!")) + raise failure.Failure(exceptions.CancelError()) if mess_data["to"] not in entities: expected = _(' or ').join([entity.userhost() for entity in entities]) log.warning(_("Stanzas using XEP-0033 should be addressed to %(expected)s, not %(current)s!") % {'expected': expected, 'current': mess_data["to"]}) @@ -96,7 +97,8 @@ element.addChild(domish.Element((None, 'address'), None, {'type': type_, 'jid': jid_})) # when the prosody plugin is completed, we can immediately return mess_data from here self.sendAndStoreMessage(mess_data, entries, profile) - return Failure(MessageSentAndStored("XEP-0033 took over", mess_data)) + log.debug("XEP-0033 took over") + raise failure.Failure(exceptions.CancelError()) d = self.host.findFeaturesSet([NS_ADDRESS], profile_key=profile) d.addCallbacks(discoCallback, lambda dummy: discoCallback(None)) return d