# HG changeset patch # User Goffi # Date 1464120664 -7200 # Node ID 633b5c21aefd8ca55e782fa3869bfa224c183835 # Parent ccfe45302a5cfd52ac22c45b161c59b793ab7e52 backend, frontend: messages refactoring (huge commit, not finished): /!\ database schema has been modified, do a backup before updating message have been refactored, here are the main changes: - languages are now handled - all messages have an uid (internal to SàT) - message updating is anticipated - subject is now first class - new naming scheme is used newMessage => messageNew, getHistory => historyGet, sendMessage => messageSend - minimal compatibility refactoring in quick_frontend/Primitivus, better refactoring should follow - threads handling - delayed messages are saved into history - info messages may also be saved in history (e.g. to keep track of people joining/leaving a room) - duplicate messages should be avoided - historyGet return messages in right order, no need to sort again - plugins have been updated to follow new features, some of them need to be reworked (e.g. OTR) - XEP-0203 (Delayed Delivery) is now fully handled in core, the plugin just handle disco and creation of a delay element - /!\ jp and Libervia are currently broken, as some features of Primitivus It has been put in one huge commit to avoid breaking messaging between changes. This is the main part of message refactoring, other commits will follow to take profit of the new features/behaviour. diff -r ccfe45302a5c -r 633b5c21aefd frontends/src/bridge/DBus.py --- a/frontends/src/bridge/DBus.py Mon Apr 18 18:35:19 2016 +0200 +++ b/frontends/src/bridge/DBus.py Tue May 24 22:11:04 2016 +0200 @@ -336,15 +336,6 @@ error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return self.db_core_iface.getFeatures(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - def getHistory(self, from_jid, to_jid, limit, between=True, search='', profile="@NONE@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.getHistory(from_jid, to_jid, limit, between, search, profile, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - def getMainResource(self, contact_jid, profile_key="@DEFAULT@", callback=None, errback=None): if callback is None: error_handler = None @@ -517,6 +508,15 @@ kwargs['error_handler'] = error_handler return self.db_core_iface.getWaitingSub(profile_key, **kwargs) + def historyGet(self, from_jid, to_jid, limit, between=True, search='', profile="@NONE@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.historyGet(from_jid, to_jid, limit, between, search, profile, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + def isConnected(self, profile_key="@DEFAULT@", callback=None, errback=None): if callback is None: error_handler = None @@ -554,6 +554,15 @@ kwargs['error_handler'] = error_handler return self.db_core_iface.loadParamsTemplate(filename, **kwargs) + def messageSend(self, to_jid, message, subject='', mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.messageSend(to_jid, message, subject, mess_type, extra, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + def paramsRegisterApp(self, xml, security_limit=-1, app='', callback=None, errback=None): if callback is None: error_handler = None @@ -661,15 +670,6 @@ kwargs['error_handler'] = error_handler return self.db_core_iface.saveParamsTemplate(filename, **kwargs) - def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.sendMessage(to_jid, message, subject, mess_type, extra, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - def setParam(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): if callback is None: error_handler = None diff -r ccfe45302a5c -r 633b5c21aefd frontends/src/primitivus/chat.py --- a/frontends/src/primitivus/chat.py Mon Apr 18 18:35:19 2016 +0200 +++ b/frontends/src/primitivus/chat.py Tue May 24 22:11:04 2016 +0200 @@ -88,7 +88,6 @@ self.text_list = urwid.ListBox(self.content) self.chat_widget = urwid.Frame(self.text_list) self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)]) - self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)]) self.pile = urwid.Pile([self.chat_colums]) PrimitivusWidget.__init__(self, self.pile, self.target) diff -r ccfe45302a5c -r 633b5c21aefd frontends/src/primitivus/primitivus --- a/frontends/src/primitivus/primitivus Mon Apr 18 18:35:19 2016 +0200 +++ b/frontends/src/primitivus/primitivus Tue May 24 22:11:04 2016 +0200 @@ -96,12 +96,13 @@ if self.mode == C.MODE_INSERTION: if isinstance(self.host.selected_widget, quick_chat.QuickChat): chat_widget = self.host.selected_widget - self.host.sendMessage(chat_widget.target, - editBar.get_edit_text(), - mess_type = "groupchat" if chat_widget.type == 'group' else "chat", # TODO: put this in QuickChat - errback=lambda failure: self.host.notify(_("Error while sending message ({})").format(failure)), - profile_key=chat_widget.profile - ) + self.host.messageSend( + chat_widget.target, + {'': editBar.get_edit_text()}, # TODO: handle language + mess_type = "groupchat" if chat_widget.type == 'group' else "chat", # TODO: put this in QuickChat + errback=lambda failure: self.host.notify(_("Error while sending message ({})").format(failure)), + profile_key=chat_widget.profile + ) editBar.set_edit_text('') elif self.mode == C.MODE_COMMAND: self.commandHandler() @@ -723,7 +724,9 @@ def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile): super(PrimitivusApp, self).roomJoinedHandler(room_jid_s, room_nicks, user_nick, profile) - self.contact_lists[profile].setFocus(jid.JID(room_jid_s), True) + for contact_list in self.widgets.getWidgets(ContactList): + if profile in contact_list.profiles: + contact_list.setFocus(jid.JID(room_jid_s), True) def progressStartedHandler(self, pid, metadata, profile): super(PrimitivusApp, self).progressStartedHandler(pid, metadata, profile) diff -r ccfe45302a5c -r 633b5c21aefd frontends/src/quick_frontend/quick_app.py --- a/frontends/src/quick_frontend/quick_app.py Mon Apr 18 18:35:19 2016 +0200 +++ b/frontends/src/quick_frontend/quick_app.py Tue May 24 22:11:04 2016 +0200 @@ -248,7 +248,7 @@ self.registerSignal("disconnected") self.registerSignal("actionNew") self.registerSignal("newContact") - self.registerSignal("newMessage") + self.registerSignal("messageNew") self.registerSignal("newAlert") self.registerSignal("presenceUpdate") self.registerSignal("subscribe") @@ -482,10 +482,10 @@ groups = list(groups) self.contact_lists[profile].setContact(entity, groups, attributes, in_roster=True) - def newMessageHandler(self, from_jid_s, msg, type_, to_jid_s, extra, profile): + def messageNewHandler(self, uid, timestamp, from_jid_s, to_jid_s, msg, subject, type_, extra, profile): from_jid = jid.JID(from_jid_s) to_jid = jid.JID(to_jid_s) - if not self.trigger.point("newMessageTrigger", from_jid, msg, type_, to_jid, extra, profile=profile): + if not self.trigger.point("messageNewTrigger", uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile=profile): return from_me = from_jid.bare == self.profiles[profile].whoami.bare @@ -505,7 +505,8 @@ contact_list.setContact(from_jid) # we display the message in the widget - chat_widget.newMessage(from_jid, target, msg, type_, extra, profile) + + chat_widget.messageNew(uid, timestamp, from_jid, target, msg, subject, type_, extra, profile) # ContactList alert if not from_me: @@ -520,16 +521,20 @@ else: contact_list.addAlert(from_jid.bare if type_ == C.MESS_TYPE_GROUPCHAT else from_jid) - def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key=C.PROF_KEY_NONE): + def messageSend(self, to_jid, message, subject=None, mess_type="auto", extra=None, callback=None, errback=None, profile_key=C.PROF_KEY_NONE): + if subject is None: + subject = {} + if extra is None: + extra = {} if callback is None: callback = lambda dummy=None: None # FIXME: optional argument is here because pyjamas doesn't support callback without arg with json proxy if errback is None: errback = lambda failure: self.showDialog(failure.fullname, failure.message, "error") - if not self.trigger.point("sendMessageTrigger", to_jid, message, subject, mess_type, extra, callback, errback, profile_key=profile_key): + if not self.trigger.point("messageSendTrigger", to_jid, message, subject, mess_type, extra, callback, errback, profile_key=profile_key): return - self.bridge.sendMessage(unicode(to_jid), message, subject, mess_type, extra, profile_key, callback=callback, errback=errback) + self.bridge.messageSend(unicode(to_jid), message, subject, mess_type, extra, profile_key, callback=callback, errback=errback) def newAlertHandler(self, msg, title, alert_type, profile): assert alert_type in ['INFO', 'ERROR'] diff -r ccfe45302a5c -r 633b5c21aefd frontends/src/quick_frontend/quick_chat.py --- a/frontends/src/quick_frontend/quick_chat.py Mon Apr 18 18:35:19 2016 +0200 +++ b/frontends/src/quick_frontend/quick_chat.py Tue May 24 22:11:04 2016 +0200 @@ -25,7 +25,6 @@ from sat_frontends.quick_frontend.constants import Const as C from collections import OrderedDict from datetime import datetime -from time import time try: # FIXME: to be removed when an acceptable solution is here @@ -52,18 +51,15 @@ self.nick = None self.games = {} # key=game name (unicode), value=instance of quick_games.RoomGame - if type_ == C.CHAT_ONE2ONE: - self.historyPrint(profile=self.profile) - - # FIXME: has been introduced to temporarily fix http://bugs.goffi.org/show_bug.cgi?id=12 - self.initialised = False + self.historyPrint(profile=self.profile) def __str__(self): return u"Chat Widget [target: {}, type: {}, profile: {}]".format(self.target, self.type, self.profile) @staticmethod - def getWidgetHash(target, profile): - return (unicode(profile), target.bare) + def getWidgetHash(target, profiles): + profile = profiles[0] + return (profile, target.bare) @staticmethod def getPrivateHash(target, profile): @@ -99,7 +95,7 @@ """Tell if this chat widget manage an entity and message type couple @param entity (jid.JID): (full) jid of the sending entity - @param mess_type (str): message type as given by newMessage + @param mess_type (str): message type as given by messageNew @return (bool): True if this Chat Widget manage this couple """ if self.type == C.CHAT_GROUP: @@ -112,8 +108,6 @@ def addUser(self, nick): """Add user if it is not in the group list""" - if not self.initialised: - return # FIXME: tmp fix for bug 12, do not flood the room with the messages when we've just entered it self.printInfo("=> %s has joined the room" % nick) def removeUser(self, nick): @@ -141,6 +135,7 @@ def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT, search='', profile='@NONE@'): """Print the current history + @param size (int): number of messages @param search (str): pattern to filter the history results @param profile (str): %(doc_profile)s @@ -152,28 +147,26 @@ target = self.target.bare - def onHistory(history): - self.initialised = True # FIXME: tmp fix for bug 12 + def _historyGetCb(history): day_format = "%A, %d %b %Y" # to display the day change previous_day = datetime.now().strftime(day_format) - for line in history: - timestamp, from_jid, to_jid, message, type_, extra = line # FIXME: extra is unused ! + for data in history: + uid, timestamp, from_jid, to_jid, message, subject, type_, extra = data # FIXME: extra is unused ! if ((self.type == C.CHAT_GROUP and type_ != C.MESS_TYPE_GROUPCHAT) or (self.type == C.CHAT_ONE2ONE and type_ == C.MESS_TYPE_GROUPCHAT)): continue - message_day = datetime.fromtimestamp(float(timestamp or time())).strftime(day_format) + message_day = datetime.fromtimestamp(timestamp).strftime(day_format) if previous_day != message_day: self.printDayChange(message_day) previous_day = message_day extra["timestamp"] = timestamp - self.newMessage(jid.JID(from_jid), target, message, type_, extra, profile) + self.messageNew(uid, timestamp, jid.JID(from_jid), target, message, subject, type_, extra, profile) self.afterHistoryPrint() - def onHistoryError(err): + def _historyGetEb(err): log.error(_("Can't get history")) - self.initialised = False # FIXME: tmp fix for bug 12, here needed for :history and :search commands - self.host.bridge.getHistory(unicode(self.host.profiles[profile].whoami.bare), unicode(target), size, True, search, profile, callback=onHistory, errback=onHistoryError) + self.host.bridge.historyGet(unicode(self.host.profiles[profile].whoami.bare), unicode(target), size, True, search, profile, callback=_historyGetCb, errback=_historyGetEb) def _get_nick(self, entity): """Return nick of this entity when possible""" @@ -195,34 +188,31 @@ """ return self.host.widgets.getOrCreateWidget(QuickChat, entity, type_=C.CHAT_ONE2ONE, force_hash=self.getPrivateHash(self.profile, entity), on_new_widget=self.onPrivateCreated, profile=self.profile) # we force hash to have a new widget, not this one again - def newMessage(self, from_jid, target, msg, type_, extra, profile): + def messageNew(self, uid, timestamp, from_jid, target, msg, subject, type_, extra, profile): + try: + msg = msg.itervalues().next() # FIXME: tmp fix until message refactoring is finished (msg is now a dict) + except StopIteration: + log.warning(u"No message found (uid: {})".format(uid)) + msg = '' if self.type == C.CHAT_GROUP and target.resource and type_ != C.MESS_TYPE_GROUPCHAT: # we have a private message, we forward it to a private conversation widget chat_widget = self.getOrCreatePrivateWidget(target) - chat_widget.newMessage(from_jid, target, msg, type_, extra, profile) + chat_widget.messageNew(uid, timestamp, from_jid, target, msg, subject, type_, extra, profile) return - try: - timestamp = float(extra['timestamp']) - except KeyError: - timestamp = None - - if not self.initialised and self.type == C.CHAT_ONE2ONE: - return # FIXME: tmp fix for bug 12, do not display the first one2one message which is already in the local history if type_ == C.MESS_TYPE_INFO: self.printInfo(msg, extra=extra) else: - self.initialised = True # FIXME: tmp fix for bug 12, do not discard any message from now - nick = self._get_nick(from_jid) if msg.startswith('/me '): - self.printInfo('* %s %s' % (nick, msg[4:]), type_='me', extra=extra) + self.printInfo('* {} {}'.format(nick, msg[4:]), type_='me', extra=extra) else: # my_message is True if message comes from local user my_message = (from_jid.resource == self.nick) if self.type == C.CHAT_GROUP else (from_jid.bare == self.host.profiles[profile].whoami.bare) self.printMessage(nick, my_message, msg, timestamp, extra, profile) - if timestamp: - self.afterHistoryPrint() + # FIXME: to be checked/removed after message refactoring + # if timestamp: + self.afterHistoryPrint() def printMessage(self, nick, my_message, message, timestamp, extra=None, profile=C.PROF_KEY_NONE): """Print message in chat window. @@ -232,11 +222,12 @@ @param message (unicode): message content @param extra (dict): extra data """ - if not timestamp: - # XXX: do not send notifications for each line of the history being displayed - # FIXME: this must be changed in the future if the timestamp is passed with - # all messages and not only with the messages coming from the history. - self.notify(nick, message) + # FIXME: check/remove this if necessary (message refactoring) + # if not timestamp: + # # XXX: do not send notifications for each line of the history being displayed + # # FIXME: this must be changed in the future if the timestamp is passed with + # # all messages and not only with the messages coming from the history. + self.notify(nick, message) def printInfo(self, msg, type_='normal', extra=None): """Print general info. diff -r ccfe45302a5c -r 633b5c21aefd frontends/src/quick_frontend/quick_contact_list.py --- a/frontends/src/quick_frontend/quick_contact_list.py Mon Apr 18 18:35:19 2016 +0200 +++ b/frontends/src/quick_frontend/quick_contact_list.py Tue May 24 22:11:04 2016 +0200 @@ -549,6 +549,7 @@ None to count all of them @return (list[unicode,None]): list of C.ALERT_* or None for undefined ones """ + return [] # FIXME: temporarily disabled if not use_bare_jid: alerts = self._alerts.get(entity, []) else: diff -r ccfe45302a5c -r 633b5c21aefd frontends/src/quick_frontend/quick_widgets.py --- a/frontends/src/quick_frontend/quick_widgets.py Mon Apr 18 18:35:19 2016 +0200 +++ b/frontends/src/quick_frontend/quick_widgets.py Tue May 24 22:11:04 2016 +0200 @@ -247,8 +247,8 @@ class QuickWidget(object): """generic widget base""" SINGLE=True # if True, there can be only one widget per target(s) - PROFILES_MULTIPLE=False - PROFILES_ALLOW_NONE=False + PROFILES_MULTIPLE=False # If True, this widget can handle several profiles at once + PROFILES_ALLOW_NONE=False # If True, this widget can be used without profile def __init__(self, host, target, profiles=None): """ diff -r ccfe45302a5c -r 633b5c21aefd src/bridge/DBus.py --- a/src/bridge/DBus.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/bridge/DBus.py Tue May 24 22:11:04 2016 +0200 @@ -166,6 +166,11 @@ pass @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, + signature='sdssa{ss}a{ss}sa{ss}s') + def messageNew(self, uid, timestamp, from_jid, to_jid, message, subject, mess_type, extra, profile): + pass + + @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, signature='ssss') def newAlert(self, message, title, alert_type, profile): pass @@ -176,11 +181,6 @@ pass @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, - signature='ssssa{ss}s') - def newMessage(self, from_jid, message, mess_type, to_jid, extra, profile): - pass - - @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, signature='ssss') def paramUpdate(self, name, value, category, profile): pass @@ -321,12 +321,6 @@ return self._callback("getFeatures", unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, - in_signature='ssibss', out_signature='a(dssssa{ss})', - async_callbacks=('callback', 'errback')) - def getHistory(self, from_jid, to_jid, limit, between=True, search='', profile="@NONE@", callback=None, errback=None): - return self._callback("getHistory", unicode(from_jid), unicode(to_jid), limit, between, unicode(search), unicode(profile), callback=callback, errback=errback) - - @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ss', out_signature='s', async_callbacks=None) def getMainResource(self, contact_jid, profile_key="@DEFAULT@"): @@ -405,6 +399,12 @@ return self._callback("getWaitingSub", unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, + in_signature='ssibss', out_signature='a(sdssa{ss}a{ss}sa{ss})', + async_callbacks=('callback', 'errback')) + def historyGet(self, from_jid, to_jid, limit, between=True, search='', profile="@NONE@", callback=None, errback=None): + return self._callback("historyGet", unicode(from_jid), unicode(to_jid), limit, between, unicode(search), unicode(profile), callback=callback, errback=errback) + + @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='b', async_callbacks=None) def isConnected(self, profile_key="@DEFAULT@"): @@ -423,6 +423,12 @@ return self._callback("loadParamsTemplate", unicode(filename)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, + in_signature='sa{ss}a{ss}sa{ss}s', out_signature='', + async_callbacks=('callback', 'errback')) + def messageSend(self, to_jid, message, subject='', mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None): + return self._callback("messageSend", unicode(to_jid), message, subject, unicode(mess_type), extra, unicode(profile_key), callback=callback, errback=errback) + + @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='sis', out_signature='', async_callbacks=None) def paramsRegisterApp(self, xml, security_limit=-1, app=''): @@ -471,12 +477,6 @@ return self._callback("saveParamsTemplate", unicode(filename)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, - in_signature='ssssa{ss}s', out_signature='', - async_callbacks=('callback', 'errback')) - def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None): - return self._callback("sendMessage", unicode(to_jid), unicode(message), unicode(subject), unicode(mess_type), extra, unicode(profile_key), callback=callback, errback=errback) - - @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='sssis', out_signature='', async_callbacks=None) def setParam(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@"): @@ -626,15 +626,15 @@ def entityDataUpdated(self, jid, name, value, profile): self.dbus_bridge.entityDataUpdated(jid, name, value, profile) + def messageNew(self, uid, timestamp, from_jid, to_jid, message, subject, mess_type, extra, profile): + self.dbus_bridge.messageNew(uid, timestamp, from_jid, to_jid, message, subject, mess_type, extra, profile) + def newAlert(self, message, title, alert_type, profile): self.dbus_bridge.newAlert(message, title, alert_type, profile) def newContact(self, contact_jid, attributes, groups, profile): self.dbus_bridge.newContact(contact_jid, attributes, groups, profile) - def newMessage(self, from_jid, message, mess_type, to_jid, extra, profile): - self.dbus_bridge.newMessage(from_jid, message, mess_type, to_jid, extra, profile) - def paramUpdate(self, name, value, category, profile): self.dbus_bridge.paramUpdate(name, value, category, profile) diff -r ccfe45302a5c -r 633b5c21aefd src/bridge/bridge_constructor/bridge_template.ini --- a/src/bridge/bridge_constructor/bridge_template.ini Mon Apr 18 18:35:19 2016 +0200 +++ b/src/bridge/bridge_constructor/bridge_template.ini Tue May 24 22:11:04 2016 +0200 @@ -34,17 +34,24 @@ doc_param_2=groups: Roster's groups where the contact is doc_param_3=%(doc_profile)s -[newMessage] +[messageNew] type=signal category=core -sig_in=ssssa{ss}s +sig_in=sdssa{ss}a{ss}sa{ss}s doc=A message has been received -doc_param_0=from_jid: JID where the message is comming from -doc_param_1=message: Message itself -doc_param_2=mess_type: Type of the message (cf RFC 6121 §5.2.2) + C.MESS_TYPE_INFO (system info) +doc_param_0=uid: unique ID of the message (id specific to SàT, this it *NOT* an XMPP id) +doc_param_1=timestamp: when the message was sent (or declared sent for delayed messages) +doc_param_2=from_jid: JID where the message is comming from doc_param_3=to_jid: JID where the message must be sent -doc_param_4=extra: extra message information -doc_param_5=%(doc_profile)s +doc_param_4=message: message itself, can be in several languages (key is language code or '' for default) +doc_param_5=subject: subject of the message, can be in several languages (key is language code or '' for default) +doc_param_6=mess_type: Type of the message (cf RFC 6121 §5.2.2) + C.MESS_TYPE_INFO (system info) +doc_param_7=extra: extra message information, can have data added by plugins and/or: + - thread: id of the thread + - thread_parent: id of the parent of the current thread + - received_timestamp: date of receiption for delayed messages + - delay_sender: entity which has originally sent or which has delayed the message +doc_param_8=%(doc_profile)s [newAlert] deprecated= @@ -434,11 +441,11 @@ doc_return=List of confirmation request data, same as for [askConfirmation] -[sendMessage] +[messageSend] async= type=method category=core -sig_in=ssssa{ss}s +sig_in=sa{ss}a{ss}sa{ss}s sig_out= param_2_default='' param_3_default="auto" @@ -446,8 +453,10 @@ param_5_default="@NONE@" doc=Send a message doc_param_0=to_jid: JID of the recipient -doc_param_1=message: body of the message -doc_param_2=subject: Subject of the message ('' if no subject) +doc_param_1=message: body of the message: + key is the language of the body, use '' when unknown +doc_param_2=subject: Subject of the message + key is the language of the subkect, use '' when unknown doc_param_3=mess_type: Type of the message (cf RFC 6121 §5.2.2) or "auto" for automatic type detection doc_param_4=extra: optional data that can be used by a plugin to build more specific messages doc_param_5=%(doc_profile_key)s @@ -577,12 +586,12 @@ doc_param_1=%(doc_security_limit)s doc_param_2=app: name of the frontend registering the parameters -[getHistory] +[historyGet] async= type=method category=core sig_in=ssibss -sig_out=a(dssssa{ss}) +sig_out=a(sdssa{ss}a{ss}sa{ss}) param_3_default=True param_4_default='' param_5_default="@NONE@" @@ -593,7 +602,7 @@ doc_param_3=between: True if we want history between the two jids (in both direction), False if we only want messages from from_jid to to_jid doc_param_4=search: pattern to filter the history results doc_param_5=%(doc_profile)s -doc_return=Ordered list (by timestamp) of tuples (timestamp, full from_jid, full to_jid, message, type, extra) +doc_return=Ordered list (by timestamp) of data as in [messageNew] (without final profile) [addContact] type=method diff -r ccfe45302a5c -r 633b5c21aefd src/core/sat_main.py --- a/src/core/sat_main.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/core/sat_main.py Tue May 24 22:11:04 2016 +0200 @@ -40,6 +40,7 @@ import sys import os.path import uuid +import time try: from collections import OrderedDict # only available from python 2.7 @@ -85,7 +86,7 @@ self.bridge.register("getPresenceStatuses", self.memory._getPresenceStatuses) self.bridge.register("getWaitingSub", self.memory.getWaitingSub) self.bridge.register("getWaitingConf", self.getWaitingConf) - self.bridge.register("sendMessage", self._sendMessage) + self.bridge.register("messageSend", self._messageSend) self.bridge.register("getConfig", self._getConfig) self.bridge.register("setParam", self.setParam) self.bridge.register("getParamA", self.memory.getStringParamA) @@ -94,7 +95,7 @@ self.bridge.register("getParamsUI", self.memory.getParamsUI) self.bridge.register("getParamsCategories", self.memory.getParamsCategories) self.bridge.register("paramsRegisterApp", self.memory.paramsRegisterApp) - self.bridge.register("getHistory", self.memory.getHistory) + self.bridge.register("historyGet", self.memory.historyGet) self.bridge.register("setPresence", self._setPresence) self.bridge.register("subscription", self.subscription) self.bridge.register("addContact", self._addContact) @@ -527,74 +528,123 @@ 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 generateMessageXML(self, data): + """Generate stanza from message 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 - return self.sendMessage(to_jid, msg, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()}, profile_key=profile_key) + @param data(dict): message data + domish element will be put in data['xml'] + following keys are needed: + - from + - to + - uid: can be set to '' if uid attribute is not wanted + - message + - type + - subject + - extra + @return (dict) message data + """ + data['xml'] = message_elt = domish.Element((None, 'message')) + message_elt["to"] = data["to"].full() + message_elt["from"] = data['from'].full() + message_elt["type"] = data["type"] + if data['uid']: # key must be present but can be set to '' + # by a plugin to avoid id on purpose + message_elt['id'] = data['uid'] + for lang, subject in data["subject"].iteritems(): + subject_elt = message_elt.addElement("subject", content=subject) + if lang: + subject_elt['xml:lang'] = lang + for lang, message in data["message"].iteritems(): + body_elt = message_elt.addElement("body", content=message) + if lang: + body_elt['xml:lang'] = lang + try: + thread = data['extra']['thread'] + except KeyError: + if 'thread_parent' in data['extra']: + raise exceptions.InternalError(u"thread_parent found while there is not associated thread") + else: + thread_elt = message_elt.addElement("thread", content=thread) + try: + thread_elt["parent"] = data["extra"]["thread_parent"] + except KeyError: + pass + return data - def sendMessage(self, to_jid, msg, subject=None, mess_type='auto', extra={}, no_trigger=False, profile_key=C.PROF_KEY_NONE): - #FIXME: check validity of recipient - profile = self.memory.getProfileName(profile_key) - assert profile - client = self.profiles[profile] + def _messageSend(self, to_jid_s, message, subject=None, mess_type='auto', extra=None, profile_key=C.PROF_KEY_NONE): + client = self.getClient(profile_key) + to_jid = jid.JID(to_jid_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 + return self.messageSend(client, to_jid, message, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()}) + + def messageSend(self, client, to_jid, message, subject=None, mess_type='auto', extra=None, uid=None, no_trigger=False): + """Send a message to an entity + + @param to_jid(jid.JID): destinee of the message + @param message(dict): message body, key is the language (use '' when unknown) + @param subject(dict): message subject, key is the language (use '' when unknown) + @param mess_type(str): one of standard message type (cf RFC 6121 §5.2.2) or: + - auto: for automatic type detection + - info: for information ("info_type" can be specified in extra) + @param extra(dict, None): extra data. Key can be: + - info_type: information type, can be + TODO + @param uid(unicode, None): unique id: + should be unique at least in this XMPP session + if None, an uuid will be generated + @param no_trigger (bool): if True, messageSend trigger will no be used + useful when a message need to be sent without any modification + """ + profile = client.profile + if subject is None: + subject = {} if extra is None: extra = {} - mess_data = { # we put data in a dict, so trigger methods can change them + data = { # dict is similar to the one used in client.onMessage + "from": client.jid, "to": to_jid, - "from": client.jid, - "message": msg, + "uid": uid or unicode(uuid.uuid4()), + "message": message, "subject": subject, "type": mess_type, "extra": extra, + "timestamp": time.time(), } pre_xml_treatments = defer.Deferred() # XXX: plugin can add their pre XML treatments to this deferred post_xml_treatments = defer.Deferred() # XXX: plugin can add their post XML treatments to this deferred - if mess_data["type"] == "auto": + if data["type"] == "auto": # we try to guess the type - if mess_data["subject"]: - mess_data["type"] = 'normal' - elif not mess_data["to"].resource: # if to JID has a resource, the type is not 'groupchat' + if data["subject"]: + data["type"] = 'normal' + elif not data["to"].resource: # if to JID has a resource, the type is not 'groupchat' # we may have a groupchat message, we check if the we know this jid try: - entity_type = self.memory.getEntityData(mess_data["to"], ['type'], profile)["type"] + entity_type = self.memory.getEntityData(data["to"], ['type'], profile)["type"] #FIXME: should entity_type manage resources ? except (exceptions.UnknownEntityError, KeyError): entity_type = "contact" if entity_type == "chatroom": - mess_data["type"] = 'groupchat' + data["type"] = 'groupchat' else: - mess_data["type"] = 'chat' + data["type"] = 'chat' else: - mess_data["type"] == 'chat' - mess_data["type"] == "chat" if mess_data["subject"] else "normal" + data["type"] == 'chat' + data["type"] == "chat" if data["subject"] else "normal" - send_only = mess_data['extra'].get('send_only', None) + # FIXME: send_only is used by libervia's OTR plugin to avoid + # the triggers from frontend, and no_trigger do the same + # thing internally, this could be unified + send_only = data['extra'].get('send_only', None) if not no_trigger and not send_only: - if not self.trigger.point("sendMessage", mess_data, pre_xml_treatments, post_xml_treatments, profile): + if not self.trigger.point("messageSend", client, data, pre_xml_treatments, post_xml_treatments): return defer.succeed(None) - log.debug(_(u"Sending message (type {type}, to {to})").format(type=mess_data["type"], to=to_jid.full())) + log.debug(_(u"Sending message (type {type}, to {to})").format(type=data["type"], to=to_jid.full())) - def cancelErrorTrap(failure): - """A message sending can be cancelled by a plugin treatment""" - failure.trap(exceptions.CancelError) - - pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(mess_data)) + pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(data)) pre_xml_treatments.chainDeferred(post_xml_treatments) post_xml_treatments.addCallback(self._sendMessageToStream, client) if send_only: @@ -602,58 +652,53 @@ else: 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) + post_xml_treatments.addErrback(self._cancelErrorTrap) + pre_xml_treatments.callback(data) return pre_xml_treatments - def _sendMessageToStream(self, mess_data, client): + def _cancelErrorTrap(failure): + """A message sending can be cancelled by a plugin treatment""" + failure.trap(exceptions.CancelError) + + def _sendMessageToStream(self, data, client): """Actualy send the message to the server - @param mess_data: message data dictionnary + @param data: message data dictionnary @param client: profile's client """ - client.xmlstream.send(mess_data['xml']) - return mess_data + client.xmlstream.send(data['xml']) + return data - def _storeMessage(self, mess_data, client): + def _storeMessage(self, data, client): """Store message into database (for local history) - @param mess_data: message data dictionnary + @param data: message data dictionnary @param client: profile's client """ - if mess_data["type"] != "groupchat": + if data["type"] != C.MESS_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(client.jid, mess_data['to'], - unicode(mess_data["message"]), - unicode(mess_data["type"]), - mess_data['extra'], - profile=client.profile) + if data['message']: # we need a message to save something + self.memory.addToHistory(client, data) else: - log.warning(_("No message found")) # empty body should be managed by plugins before this point - return mess_data + log.warning(u"No message found") # empty body should be managed by plugins before this point + return data - def sendMessageToBridge(self, mess_data, client): + def sendMessageToBridge(self, data, client): """Send message to bridge, so frontends can display it - @param mess_data: message data dictionnary + @param 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 + if data["type"] != C.MESS_TYPE_GROUPCHAT: + # we don't send groupchat message 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['from'].full(), - unicode(mess_data["message"]), - mess_type=mess_data["type"], - to_jid=mess_data['to'].full(), - extra=mess_data['extra'], - profile=client.profile) + if data['message']: # we need a message to send something + # We send back the message, so all frontends are aware of it + self.bridge.messageNew(data['uid'], data['timestamp'], data['from'].full(), data['to'].full(), data['message'], data['subject'], data['type'], data['extra'], profile=client.profile) else: log.warning(_("No message found")) - return mess_data + return 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 ccfe45302a5c -r 633b5c21aefd src/core/xmpp.py --- a/src/core/xmpp.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/core/xmpp.py Tue May 24 22:11:04 2016 +0200 @@ -20,14 +20,20 @@ from sat.core.i18n import _ from sat.core.constants import Const as C from twisted.internet import task, defer -from twisted.words.protocols.jabber import jid, xmlstream +from twisted.words.protocols.jabber.xmlstream import XMPPHandler +from twisted.words.protocols.jabber import xmlstream from twisted.words.protocols.jabber import error -from wokkel import client, disco, xmppim, generic, delay, iwokkel +from twisted.words.protocols.jabber import jid +from twisted.python import failure +from wokkel import client, disco, xmppim, generic, iwokkel +from wokkel import delay from sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions from zope.interface import implements -from twisted.words.protocols.jabber.xmlstream import XMPPHandler +import time +import calendar +import uuid class SatXMPPClient(client.XMPPClient): @@ -42,6 +48,7 @@ self.__connected = False self.profile = profile self.host_app = host_app + self._mess_id_uid = {} # map from message id to uid use in history. Key: (full_jid,message_id) Value: uid self.conn_deferred = defer.Deferred() self._waiting_conf = {} # callback called when a confirmation is received self._progress_cb = {} # callback called when a progress is requested (key = progress id) @@ -131,53 +138,85 @@ xmppim.MessageProtocol.__init__(self) self.host = host - def onMessage(self, message): - if not message.hasAttribute('from'): - message['from'] = self.parent.jid.host - log.debug(_(u"got message from: %s") % message["from"]) + def onMessage(self, message_elt): + # TODO: handle threads + client = self.parent + if not 'from' in message_elt.attributes: + message_elt['from'] = client.jid.host + log.debug(_(u"got message from: {from_}").format(from_=message_elt['from'])) post_treat = defer.Deferred() # XXX: plugin can add their treatments to this deferred - if not self.host.trigger.point("MessageReceived", message, post_treat, profile=self.parent.profile): + if not self.host.trigger.point("MessageReceived", client, message_elt, post_treat): return - data = {"from": message['from'], - "to": message['to'], - "body": "", - "extra": {}} + message = {} + subject = {} + extra = {} + data = {"from": message_elt['from'], + "to": message_elt['to'], + "uid": message_elt.getAttribute('uid', unicode(uuid.uuid4())), # XXX: uid is not a standard attribute but may be added by plugins + "message": message, + "subject": subject, + "type": message_elt.getAttribute('type', 'normal'), + "extra": extra} + + try: + data['stanza_id'] = message_elt['id'] + except KeyError: + pass + else: + client._mess_id_uid[(data['from'], data['stanza_id'])] = data['uid'] - for e in message.elements(): - if e.name == "body": - data['body'] = e.children[0] if e.children else "" - elif e.name == "subject" and e.children: - data['extra']['subject'] = e.children[0] + # message + for e in message_elt.elements(C.NS_CLIENT, 'body'): + message[e.getAttribute('xml:lang','')] = unicode(e) + + # subject + for e in message_elt.elements(C.NS_CLIENT, 'subject'): + subject[e.getAttribute('xml:lang','')] = unicode(e) - data['type'] = message['type'] if message.hasAttribute('type') else 'normal' + # delay and timestamp + try: + delay_elt = message_elt.elements(delay.NS_DELAY, 'delay').next() + except StopIteration: + data['timestamp'] = time.time() + else: + parsed_delay = delay.Delay.fromElement(delay_elt) + data['timestamp'] = calendar.timegm(parsed_delay.stamp.utctimetuple()) + data['received_timestamp'] = unicode(time.time()) + if parsed_delay.sender: + data['delay_sender'] = parsed_delay.sender.full() + + def skipEmptyMessage(data): + if not data['message'] and not data['extra']: + raise failure.Failure(exceptions.CancelError()) + return data def bridgeSignal(data): + try: + data['extra']['received_timestamp'] = data['received_timestamp'] + data['extra']['delay_sender'] = data['delay_sender'] + except KeyError: + pass if data is not None: - self.host.bridge.newMessage(data['from'], data['body'], data['type'], data['to'], data['extra'], profile=self.parent.profile) + 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) return data def addToHistory(data): - try: - timestamp = data['extra']['timestamp'] # timestamp added by XEP-0203 - except KeyError: - self.host.memory.addToHistory(jid.JID(data['from']), jid.JID(data['to']), data['body'], data['type'], data['extra'], profile=self.parent.profile) - else: - if data['type'] != 'groupchat': # XXX: we don't save delayed messages in history for groupchats - #TODO: add delayed messages to history if they aren't already in it - data['extra']['archive'] = timestamp # FIXME: this "archive" is actually never used - self.host.memory.addToHistory(jid.JID(data['from']), jid.JID(data['to']), data['body'], data['type'], data['extra'], timestamp, profile=self.parent.profile) + data['from'] = jid.JID(data['from']) + data['to'] = jid.JID(data['to']) + self.host.memory.addToHistory(client, data) return data - def treatmentsEb(failure): - failure.trap(exceptions.SkipHistory) + def treatmentsEb(failure_): + failure_.trap(exceptions.SkipHistory) return data - def cancelErrorTrap(failure): + def cancelErrorTrap(failure_): """A message sending can be cancelled by a plugin treatment""" - failure.trap(exceptions.CancelError) + failure_.trap(exceptions.CancelError) + post_treat.addCallback(skipEmptyMessage) post_treat.addCallback(addToHistory) post_treat.addErrback(treatmentsEb) post_treat.addCallback(bridgeSignal) @@ -506,7 +545,7 @@ def connectionMade(self): log.debug(_(u"Connection made with %s" % self.jabber_host)) - self.xmlstream.namespace = "jabber:client" + self.xmlstream.namespace = C.NS_CLIENT self.xmlstream.sendHeader() iq = xmlstream.IQ(self.xmlstream, 'set') @@ -526,10 +565,10 @@ log.debug(_(u"Registration answer: %s") % answer.toXml()) self.xmlstream.sendFooter() - def registrationFailure(self, failure): - log.info(_("Registration failure: %s") % unicode(failure.value)) + def registrationFailure(self, failure_): + log.info(_("Registration failure: %s") % unicode(failure_.value)) self.xmlstream.sendFooter() - raise failure.value + raise failure_.value class SatVersionHandler(generic.VersionHandler): diff -r ccfe45302a5c -r 633b5c21aefd src/memory/memory.py --- a/src/memory/memory.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/memory/memory.py Tue May 24 22:11:04 2016 +0200 @@ -510,14 +510,12 @@ ## History ## - def addToHistory(self, from_jid, to_jid, message, type_='chat', extra=None, timestamp=None, profile=C.PROF_KEY_NONE): - assert profile != C.PROF_KEY_NONE - if extra is None: - extra = {} - return self.storage.addToHistory(from_jid, to_jid, message, type_, extra, timestamp, profile) + def addToHistory(self, client, data): + return self.storage.addToHistory(data, client.profile) - def getHistory(self, from_jid, to_jid, limit=C.HISTORY_LIMIT_NONE, between=True, search=None, profile=C.PROF_KEY_NONE): + def historyGet(self, from_jid, to_jid, limit=C.HISTORY_LIMIT_NONE, between=True, search=None, profile=C.PROF_KEY_NONE): """Retrieve messages in history + @param from_jid (JID): source JID (full, or bare for catchall) @param to_jid (JID): dest JID (full, or bare for catchall) @param limit (int): maximum number of messages to get: @@ -527,7 +525,7 @@ @param between (bool): confound source and dest (ignore the direction) @param search (str): pattern to filter the history results @param profile (str): %(doc_profile)s - @return: list of tuple as in http://wiki.goffi.org/wiki/Bridge_API#getHistory + @return: list of message data as in [messageNew] """ assert profile != C.PROF_KEY_NONE if limit == C.HISTORY_LIMIT_DEFAULT: @@ -536,7 +534,7 @@ limit = None if limit == 0: return defer.succeed([]) - return self.storage.getHistory(jid.JID(from_jid), jid.JID(to_jid), limit, between, search, profile) + return self.storage.historyGet(jid.JID(from_jid), jid.JID(to_jid), limit, between, search, profile) ## Statuses ## diff -r ccfe45302a5c -r 633b5c21aefd src/memory/sqlite.py --- a/src/memory/sqlite.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/memory/sqlite.py Tue May 24 22:11:04 2016 +0200 @@ -27,13 +27,12 @@ from twisted.enterprise import adbapi from twisted.internet import defer from collections import OrderedDict -from time import time import re import os.path import cPickle as pickle import hashlib -CURRENT_DB_VERSION = 2 +CURRENT_DB_VERSION = 3 # XXX: DATABASE schemas are used in the following way: # - 'current' key is for the actual database schema, for a new base @@ -49,9 +48,18 @@ ('profiles', (("id INTEGER PRIMARY KEY ASC", "name TEXT"), ("UNIQUE (name)",))), ('message_types', (("type TEXT PRIMARY KEY",), - tuple())), - ('history', (("id INTEGER PRIMARY KEY ASC", "profile_id INTEGER", "source TEXT", "dest TEXT", "source_res TEXT", "dest_res TEXT", "timestamp DATETIME", "message TEXT", "type TEXT", "extra BLOB"), - ("FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE", "FOREIGN KEY(type) REFERENCES message_types(type)"))), + tuple())), + ('history', (("uid TEXT PRIMARY KEY", "update_uid TEXT", "profile_id INTEGER", "source TEXT", "dest TEXT", "source_res TEXT", "dest_res TEXT", + "timestamp DATETIME NOT NULL", "received_timestamp DATETIME", # XXX: timestamp is the time when the message was emitted. If received time stamp is not NULL, the message was delayed and timestamp is the declared value (and received_timestamp the time of reception) + "type TEXT", "extra BLOB"), + ("FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE", "FOREIGN KEY(type) REFERENCES message_types(type)", + "UNIQUE (profile_id, timestamp, source, dest, source_res, dest_res)" # avoid storing 2 time the same message (specially for delayed cones) + ))), + ('message', (("id INTEGER PRIMARY KEY ASC", "history_uid INTEGER", "message TEXT", "language TEXT"), + ("FOREIGN KEY(history_uid) REFERENCES history(uid) ON DELETE CASCADE",))), + ('subject', (("id INTEGER PRIMARY KEY ASC", "history_uid INTEGER", "subject TEXT", "language TEXT"), + ("FOREIGN KEY(history_uid) REFERENCES history(uid) ON DELETE CASCADE",))), + ('thread', (("id INTEGER PRIMARY KEY ASC", "history_uid INTEGER", "thread_id TEXT", "parent_id TEXT"),("FOREIGN KEY(history_uid) REFERENCES history(uid) ON DELETE CASCADE",))), ('param_gen', (("category TEXT", "name TEXT", "value TEXT"), ("PRIMARY KEY (category,name)",))), ('param_ind', (("category TEXT", "name TEXT", "profile_id INTEGER", "value TEXT"), @@ -66,15 +74,26 @@ ("PRIMARY KEY (namespace, key, profile_id)", "FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE"))) )), 'INSERT': OrderedDict(( - ('message_types', (("'chat'",), ("'error'",), ("'groupchat'",), ("'headline'",), ("'normal'",))), + ('message_types', (("'chat'",), + ("'error'",), + ("'groupchat'",), + ("'headline'",), + ("'normal'",), + ("'info'",) # info is not standard, but used to keep track of info like join/leave in a MUC + )), )), }, + 3: {'specific': 'update_v3' + }, 2: {'specific': 'update2raw_v2' }, - 1: {'cols create': {'history': ('extra BLOB',)} + 1: {'cols create': {'history': ('extra BLOB',)}, }, } +NOT_IN_EXTRA = ('received_timestamp', 'update_uid') # keys which are in message data extra but not stored in sqlite's extra field + # this is specific to this sqlite storage and for now only used for received_timestamp + # because this value is stored in a separate field class SqliteStorage(object): """This class manage storage with Sqlite database""" @@ -114,7 +133,7 @@ if statements is None: return defer.succeed(None) - log.debug(u"===== COMMITING STATEMENTS =====\n%s\n============\n\n" % '\n'.join(statements)) + log.debug(u"===== COMMITTING STATEMENTS =====\n%s\n============\n\n" % '\n'.join(statements)) d = self.dbpool.runInteraction(self._updateDb, tuple(statements)) return d @@ -251,57 +270,131 @@ return d #History - def addToHistory(self, from_jid, to_jid, message, _type='chat', extra=None, timestamp=None, profile=None): + + def _addToHistoryCb(self, dummy, data): + # Message metadata were successfuly added to history + # now we can add message and subject + uid = data['uid'] + for key in ('message', 'subject'): + for lang, value in data[key].iteritems(): + d = self.dbpool.runQuery("INSERT INTO {key}(history_uid, {key}, language) VALUES (?,?,?)".format(key=key), + (uid, value, lang or None)) + d.addErrback(lambda dummy: log.error(_(u"Can't save following {key} in history (uid: {uid}, lang:{lang}): {value}".format( + key=key, uid=uid, lang=lang, value=value)))) + try: + thread = data['extra']['thread'] + except KeyError: + pass + else: + thread_parent = data['extra'].get('thread_parent') + d = self.dbpool.runQuery("INSERT INTO thread(history_uid, thread_id, parent_id) VALUES (?,?,?)", + (uid, thread, thread_parent)) + d.addErrback(lambda dummy: log.error(_(u"Can't save following thread in history (uid: {uid}): thread:{thread}), parent:{parent}".format( + uid=uid, thread=thread, parent=thread_parent)))) + + def _addToHistoryEb(self, failure, data): + try: + sqlite_msg = failure.value.args[0] + except (AttributeError, IndexError): + raise failure + else: + if "UNIQUE constraint failed" in sqlite_msg: + log.debug(u"Message is already in history, not storing it again") + else: + log.error(u"Can't store message in history: {}".format(failure)) + + def addToHistory(self, data, profile): """Store a new message in history - @param from_jid: full source JID - @param to_jid: full dest JID - @param message: message - @param _type: message type (see RFC 6121 §5.2.2) - @param extra: dictionary (keys and values are unicode) of extra message data - @param timestamp: timestamp in seconds since epoch, or None to use current time + + @param data(dict): message data as build by SatMessageProtocol.onMessage """ - assert(profile) - if extra is None: - extra = {} - extra_ = pickle.dumps({k: v.encode('utf-8') for k, v in extra.items()}, 0).decode('utf-8') - d = self.dbpool.runQuery("INSERT INTO history(source, source_res, dest, dest_res, timestamp, message, type, extra, profile_id) VALUES (?,?,?,?,?,?,?,?,?)", - (from_jid.userhost(), from_jid.resource, to_jid.userhost(), to_jid.resource, timestamp or time(), - message, _type, extra_, self.profiles[profile])) - d.addErrback(lambda ignore: log.error(_(u"Can't save following message in history: from [%(from_jid)s] to [%(to_jid)s] ==> [%(message)s]" % - {"from_jid": from_jid.full(), "to_jid": to_jid.full(), "message": message}))) + extra = pickle.dumps({k: v for k, v in data['extra'].iteritems() if k not in NOT_IN_EXTRA}, 0) + from_jid = data['from'] + to_jid = data['to'] + d = self.dbpool.runQuery("INSERT INTO history(uid, update_uid, profile_id, source, dest, source_res, dest_res, timestamp, received_timestamp, type, extra) VALUES (?,?,?,?,?,?,?,?,?,?,?)", + (data['uid'], data['extra'].get('update_uid'), self.profiles[profile], data['from'].userhost(), to_jid.userhost(), from_jid.resource, to_jid.resource, data['timestamp'], data.get('received_timestamp'), data['type'], extra)) + d.addCallbacks(self._addToHistoryCb, self._addToHistoryEb, callbackArgs=[data], errbackArgs=[data]) + d.addErrback(lambda ignore: log.error(_(u"Can't save following message in history: from [{from_jid}] to [{to_jid}] (uid: {uid})" + .format(from_jid=from_jid.full(), to_jid=to_jid.full(), uid=data['uid'])))) return d - def getHistory(self, from_jid, to_jid, limit=None, between=True, search=None, profile=None): + def sqliteHistoryToList(self, query_result): + """Get SQL query result and return a list of message data dicts""" + result = [] + current = {'uid': None} + for row in reversed(query_result): + uid, update_uid, source, dest, source_res, dest_res, timestamp, received_timestamp,\ + type_, extra, message, message_lang, subject, subject_lang, thread, thread_parent = row + if uid != current['uid']: + # new message + try: + extra = pickle.loads(str(extra or "")) + except EOFError: + extra = {} + current = { + 'from': "%s/%s" % (source, source_res) if source_res else source, + 'to': "%s/%s" % (dest, dest_res) if dest_res else dest, + 'uid': uid, + 'message': {}, + 'subject': {}, + 'type': type_, + 'extra': extra, + 'timestamp': timestamp, + } + if update_uid is not None: + current['extra']['update_uid'] = update_uid + if received_timestamp is not None: + current['extra']['received_timestamp'] = str(received_timestamp) + result.append(current) + + if message is not None: + current['message'][message_lang or ''] = message + + if subject is not None: + current['subject'][subject_lang or ''] = subject + + if thread is not None: + current_extra = current['extra'] + current_extra['thread'] = thread + if thread_parent is not None: + current_extra['thread_parent'] = thread_parent + else: + if thread_parent is not None: + log.error(u"Database inconsistency: thread parent without thread (uid: {uid}, thread_parent: {parent})" + .format(uid=uid, parent=thread_parent)) + + return result + + def listDict2listTuple(self, messages_data): + """Return a list of tuple as used in bridge from a list of messages data""" + ret = [] + for m in messages_data: + ret.append((m['uid'], m['timestamp'], m['from'], m['to'], m['message'], m['subject'], m['type'], m['extra'])) + return ret + + def historyGet(self, from_jid, to_jid, limit=None, between=True, search=None, profile=None): """Retrieve messages in history + @param from_jid (JID): source JID (full, or bare for catchall) @param to_jid (JID): dest JID (full, or bare for catchall) @param limit (int): maximum number of messages to get: - 0 for no message (returns the empty list) - None for unlimited @param between (bool): confound source and dest (ignore the direction) - @param search (str): pattern to filter the history results - @param profile (str): %(doc_profile)s - @return: list of tuple as in http://wiki.goffi.org/wiki/Bridge_API#getHistory + @param search (unicode): pattern to filter the history results + @param profile (unicode): %(doc_profile)s + @return: list of tuple as in [messageNew] """ - assert(profile) + assert profile if limit == 0: return defer.succeed([]) - def sqliteToList(query_result): - query_result.reverse() - result = [] - for row in query_result: - timestamp, source, source_res, dest, dest_res, message, type_, extra_raw = row - try: - extra = pickle.loads(str(extra_raw or "")) - except EOFError: - extra = {} - result.append((timestamp, "%s/%s" % (source, source_res) if source_res else source, - "%s/%s" % (dest, dest_res) if dest_res else dest, - message, type_, extra)) - return result - - query_parts = ["SELECT timestamp, source, source_res, dest, dest_res, message, type, extra FROM history WHERE profile_id=? AND"] + query_parts = ["SELECT uid, update_uid, source, dest, source_res, dest_res, timestamp, received_timestamp,\ + type, extra, message, message.language, subject, subject.language, thread_id, thread.parent_id\ + FROM history LEFT JOIN message ON history.uid = message.history_uid\ + LEFT JOIN subject ON history.uid=subject.history_uid\ + LEFT JOIN thread ON history.uid=thread.history_uid\ + WHERE profile_id=? AND"] # FIXME: not sure if it's the best request, messages and subjects can appear several times here values = [self.profiles[profile]] def test_jid(type_, _jid): @@ -324,14 +417,16 @@ query_parts.append("AND message GLOB ?") values.append("*%s*" % search) - query_parts.append("ORDER BY timestamp DESC") - + query_parts.append("ORDER BY timestamp DESC") # we reverse the order in sqliteHistoryToList + # we use DESC here so LIMIT keep the last messages if limit is not None: query_parts.append("LIMIT ?") values.append(limit) d = self.dbpool.runQuery(" ".join(query_parts), values) - return d.addCallback(sqliteToList) + d.addCallback(self.sqliteHistoryToList) + d.addCallback(self.listDict2listTuple) + return d #Private values def loadGenPrivates(self, private_gen, namespace): @@ -665,6 +760,7 @@ def generateUpdateData(self, old_data, new_data, modify=False): """ Generate data for automatic update between two schema data + @param old_data: data of the former schema (which must be updated) @param new_data: data of the current schema @param modify: if True, always use "cols modify" table, else try to ALTER tables @@ -728,10 +824,10 @@ @defer.inlineCallbacks def update2raw(self, update, dev_version=False): """ Transform update data to raw SQLite statements + @param update: update data as returned by generateUpdateData @param dev_version: if True, update will be done in dev mode: no deletion will be done, instead a message will be shown. This prevent accidental lost of data while working on the code/database. @return: list of string with SQL statements needed to update the base - """ ret = self.createData2Raw(update.get('create', {})) drop = [] @@ -767,11 +863,104 @@ specific = update.get('specific', None) if specific: cmds = yield getattr(self, specific)() - ret.extend(cmds) + ret.extend(cmds or []) defer.returnValue(ret) + @defer.inlineCallbacks + def update_v3(self): + """Update database from v2 to v3 (message refactoring)""" + # XXX: this update do all the messages in one huge transaction + # this is really memory consuming, but was OK on a reasonably + # big database for tests. If issues are happening, we can cut it + # in smaller transactions using LIMIT and by deleting already updated + # messages + log.info(u"Database update to v3, this may take a while") + + # we need to fix duplicate timestamp, as it can result in conflicts with the new schema + rows = yield self.dbpool.runQuery("SELECT timestamp, COUNT(*) as c FROM history GROUP BY timestamp HAVING c>1") + if rows: + log.info("fixing duplicate timestamp") + fixed = [] + for timestamp, dummy in rows: + ids_rows = yield self.dbpool.runQuery("SELECT id from history where timestamp=?", (timestamp,)) + for idx, (id_,) in enumerate(ids_rows): + fixed.append(id_) + yield self.dbpool.runQuery("UPDATE history SET timestamp=? WHERE id=?", (float(timestamp) + idx * 0.001, id_)) + log.info(u"fixed messages with ids {}".format(u', '.join([unicode(id_) for id_ in fixed]))) + + def historySchema(txn): + log.info(u"History schema update") + txn.execute("ALTER TABLE history RENAME TO tmp_sat_update") + txn.execute("CREATE TABLE history (uid TEXT PRIMARY KEY, update_uid TEXT, profile_id INTEGER, source TEXT, dest TEXT, source_res TEXT, dest_res TEXT, timestamp DATETIME NOT NULL, received_timestamp DATETIME, type TEXT, extra BLOB, FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE, FOREIGN KEY(type) REFERENCES message_types(type), UNIQUE (profile_id, timestamp, source, dest, source_res, dest_res))") + txn.execute("INSERT INTO history (uid, profile_id, source, dest, source_res, dest_res, timestamp, type, extra) SELECT id, profile_id, source, dest, source_res, dest_res, timestamp, type, extra FROM tmp_sat_update") + + yield self.dbpool.runInteraction(historySchema) + + def newTables(txn): + log.info(u"Creating new tables") + txn.execute("CREATE TABLE message (id INTEGER PRIMARY KEY ASC, history_uid INTEGER, message TEXT, language TEXT, FOREIGN KEY(history_uid) REFERENCES history(uid) ON DELETE CASCADE)") + txn.execute("CREATE TABLE thread (id INTEGER PRIMARY KEY ASC, history_uid INTEGER, thread_id TEXT, parent_id TEXT, FOREIGN KEY(history_uid) REFERENCES history(uid) ON DELETE CASCADE)") + txn.execute("CREATE TABLE subject (id INTEGER PRIMARY KEY ASC, history_uid INTEGER, subject TEXT, language TEXT, FOREIGN KEY(history_uid) REFERENCES history(uid) ON DELETE CASCADE)") + + yield self.dbpool.runInteraction(newTables) + + log.info(u"inserting new message type") + yield self.dbpool.runQuery("INSERT INTO message_types VALUES (?)", ('info',)) + + log.info(u"messages update") + rows = yield self.dbpool.runQuery("SELECT id, timestamp, message, extra FROM tmp_sat_update") + total = len(rows) + + def updateHistory(txn, queries): + for query, args in iter(queries): + txn.execute(query, args) + + queries = [] + for idx, row in enumerate(rows, 1): + if idx % 1000 == 0 or total - idx == 0: + log.info("preparing message {}/{}".format(idx, total)) + id_, timestamp, message, extra = row + try: + extra = pickle.loads(str(extra or "")) + except EOFError: + extra = {} + except Exception: + log.warning(u"Can't handle extra data for message id {}, ignoring it".format(id_)) + extra = {} + + queries.append(("INSERT INTO message(history_uid, message) VALUES (?,?)", (id_, message))) + + try: + subject = extra.pop('subject') + except KeyError: + pass + else: + try: + subject = subject.decode('utf-8') + except UnicodeEncodeError: + log.warning(u"Error while decoding subject, ignoring it") + del extra['subject'] + else: + queries.append(("INSERT INTO subject(history_uid, subject) VALUES (?,?)", (id_, subject))) + + received_timestamp = extra.pop('timestamp', None) + try: + del extra['archive'] + except KeyError: + # archive was not used + pass + + queries.append(("UPDATE history SET received_timestamp=?,extra=? WHERE uid=?",(id_, received_timestamp, pickle.dumps(extra, 0)))) + + yield self.dbpool.runInteraction(updateHistory, queries) + + log.info("Dropping temporary table") + yield self.dbpool.runQuery("DROP TABLE tmp_sat_update") + log.info("Database update finished :)") + def update2raw_v2(self): """Update the database from v1 to v2 (add passwords encryptions): + - the XMPP password value is re-used for the profile password (new parameter) - the profile password is stored hashed - the XMPP password is stored encrypted, with the profile password as key diff -r ccfe45302a5c -r 633b5c21aefd src/plugins/plugin_exp_command_export.py --- a/src/plugins/plugin_exp_command_export.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/plugins/plugin_exp_command_export.py Tue May 24 22:11:04 2016 +0200 @@ -18,6 +18,7 @@ # along with this program. If not, see . from sat.core.i18n import _ +from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from twisted.words.protocols.jabber import jid @@ -57,10 +58,10 @@ log.info("connectionMade :)") def outReceived(self, data): - self.parent.host.sendMessage(self.target, self._clean(data), no_trigger=True, profile_key=self.profile) + self.parent.host.messageSend(self.target, {'': self._clean(data)}, no_trigger=True, profile_key=self.profile) def errReceived(self, data): - self.parent.host.sendMessage(self.target, self._clean(data), no_trigger=True, profile_key=self.profile) + self.parent.host.messageSend(self.target, {'': self._clean(data)}, no_trigger=True, profile_key=self.profile) def processEnded(self, reason): log.info (u"process finished: %d" % (reason.value.exitCode,)) @@ -102,20 +103,19 @@ except ValueError: pass - def MessageReceivedTrigger(self, message, post_treat, profile): + def MessageReceivedTrigger(self, client, message, post_treat): """ Check if source is linked and repeat message, else do nothing """ from_jid = jid.JID(message["from"]) - spawned_key = (from_jid.userhostJID(), profile) - try: - body = [e for e in message.elements() if e.name == 'body'][0] - except IndexError: - # do not block message without body (chat state notification...) - log.debug("No body element found in message, following normal behaviour") - return True - - mess_data = unicode(body) + '\n' + spawned_key = (from_jid.userhostJID(), client.profile) if spawned_key in self.spawned: + try: + body = message.elements(C.NS_CLIENT, 'body').next() + except StopIteration: + # do not block message without body (chat state notification...) + return True + + mess_data = unicode(body) + '\n' processes_set = self.spawned[spawned_key] _continue = False exclusive = False diff -r ccfe45302a5c -r 633b5c21aefd src/plugins/plugin_exp_parrot.py --- a/src/plugins/plugin_exp_parrot.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/plugins/plugin_exp_parrot.py Tue May 24 22:11:04 2016 +0200 @@ -43,22 +43,21 @@ """Parrot mode plugin: repeat messages from one entity or MUC room to another one""" #XXX: This plugin can be potentially dangerous if we don't trust entities linked # this is specially true if we have other triggers. - # sendMessageTrigger avoid other triggers execution, it's deactivated to allow + # messageSendTrigger avoid other triggers execution, it's deactivated to allow # /unparrot command in text commands plugin. def __init__(self, host): log.info(_("Plugin Parrot initialization")) self.host = host host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100) - #host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100) + #host.trigger.add("messageSend", self.messageSendTrigger, priority=100) try: self.host.plugins[C.TEXT_CMDS].registerTextCommands(self) except KeyError: log.info(_("Text commands not available")) - #def sendMessageTrigger(self, mess_data, treatments, profile): + #def messageSendTrigger(self, client, mess_data, treatments): # """ Deactivate other triggers if recipient is in parrot links """ - # client = self.host.getClient(profile) # try: # _links = client.parrot_links # except AttributeError: @@ -68,8 +67,10 @@ # log.debug("Parrot link detected, skipping other triggers") # raise trigger.SkipOtherTriggers - def MessageReceivedTrigger(self, message, post_treat, profile): + def MessageReceivedTrigger(self, client, message, post_treat): """ Check if source is linked and repeat message, else do nothing """ + # TODO: many things are not repeated (subject, thread, etc) + profile = client.profile client = self.host.getClient(profile) from_jid = jid.JID(message["from"]) @@ -81,28 +82,27 @@ if not from_jid.userhostJID() in _links: return True - for e in message.elements(): - if e.name == "body": - mess_body = e.children[0] if e.children else "" + message = {} + for e in message.elements(C.NS_CLIENT, 'body'): + body = unicode(e) + lang = e.getAttribute('lang') or '' - try: - entity_type = self.host.memory.getEntityData(from_jid, ['type'], profile)["type"] - except (UnknownEntityError, KeyError): - entity_type = "contact" - if entity_type == 'chatroom': - src_txt = from_jid.resource - if src_txt == self.host.plugins["XEP-0045"].getRoomNick(from_jid.userhostJID(), profile): - #we won't repeat our own messages - return True - else: - src_txt = from_jid.user - msg = "[%s] %s" % (src_txt, mess_body) + try: + entity_type = self.host.memory.getEntityData(from_jid, ['type'], profile)["type"] + except (UnknownEntityError, KeyError): + entity_type = "contact" + if entity_type == 'chatroom': + src_txt = from_jid.resource + if src_txt == self.host.plugins["XEP-0045"].getRoomNick(from_jid.userhostJID(), profile): + #we won't repeat our own messages + return True + else: + src_txt = from_jid.user + message[lang] = u"[{}] {}".format(src_txt, body) - linked = _links[from_jid.userhostJID()] + linked = _links[from_jid.userhostJID()] - self.host.sendMessage(jid.JID(unicode(linked)), msg, None, "auto", no_trigger=True, profile_key=profile) - else: - log.warning("No body element found in message, following normal behaviour") + self.host.messageSend(jid.JID(unicode(linked)), message, None, "auto", no_trigger=True, profile_key=profile) return True diff -r ccfe45302a5c -r 633b5c21aefd src/plugins/plugin_misc_imap.py --- a/src/plugins/plugin_misc_imap.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/plugins/plugin_misc_imap.py Tue May 24 22:11:04 2016 +0200 @@ -154,11 +154,11 @@ log.debug(u'Mailbox init (%s)' % name) if name != "INBOX": raise imap4.MailboxException("Only INBOX is managed for the moment") - self.mailbox = self.host.plugins["Maildir"].accessMessageBox(name, self.newMessage, profile) + self.mailbox = self.host.plugins["Maildir"].accessMessageBox(name, self.messageNew, profile) - def newMessage(self): + def messageNew(self): """Called when a new message is in the mailbox""" - log.debug("newMessage signal received") + log.debug("messageNew signal received") nb_messages = self.getMessageCount() for listener in self.listeners: listener.newMessages(nb_messages, None) diff -r ccfe45302a5c -r 633b5c21aefd src/plugins/plugin_misc_maildir.py --- a/src/plugins/plugin_misc_maildir.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/plugins/plugin_misc_maildir.py Tue May 24 22:11:04 2016 +0200 @@ -98,18 +98,19 @@ del self.__mailboxes[profile] del self.data[profile] - def messageReceivedTrigger(self, message, post_treat, profile): + def messageReceivedTrigger(self, client, message, post_treat): """This trigger catch normal message and put the in the Maildir box. If the message is not of "normal" type, do nothing @param message: message xmlstrem @return: False if it's a normal message, True else""" - for e in message.elements(): - if e.name == "body": - mess_type = message['type'] if message.hasAttribute('type') else 'normal' - if mess_type != 'normal': - return True - self.accessMessageBox("INBOX", profile_key=profile).addMessage(message) - return not self.host.memory.getParamA(NAME, CATEGORY, profile_key=profile) + profile = client.profile + for e in message.elements(C.NS_CLIENT, 'body'): + mess_type = message.getAttribute('type', 'normal') + if mess_type != 'normal': + return True + self.accessMessageBox("INBOX", profile_key=profile).addMessage(message) + return not self.host.memory.getParamA(NAME, CATEGORY, profile_key=profile) + return True def accessMessageBox(self, boxname, observer=None, profile_key=C.PROF_KEY_NONE): """Create and return a MailboxUser instance diff -r ccfe45302a5c -r 633b5c21aefd src/plugins/plugin_misc_smtp.py --- a/src/plugins/plugin_misc_smtp.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/plugins/plugin_misc_smtp.py Tue May 24 22:11:04 2016 +0200 @@ -87,7 +87,7 @@ """handle end of message""" mail = Parser().parsestr("\n".join(self.message)) try: - self.host._sendMessage(parseaddr(mail['to'].decode('utf-8', 'replace'))[1], mail.get_payload().decode('utf-8', 'replace'), # TODO: manage other charsets + self.host._messageSend(parseaddr(mail['to'].decode('utf-8', 'replace'))[1], mail.get_payload().decode('utf-8', 'replace'), # TODO: manage other charsets subject=mail['subject'].decode('utf-8', 'replace'), mess_type='normal', profile_key=self.profile) except: exc_type, exc_value, exc_traceback = sys.exc_info() diff -r ccfe45302a5c -r 633b5c21aefd src/plugins/plugin_misc_text_commands.py --- a/src/plugins/plugin_misc_text_commands.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/plugins/plugin_misc_text_commands.py Tue May 24 22:11:04 2016 +0200 @@ -58,7 +58,7 @@ def __init__(self, host): log.info(_("Text commands initialization")) self.host = host - host.trigger.add("sendMessage", self.sendMessageTrigger) + host.trigger.add("messageSend", self.messageSendTrigger) self._commands = {} self._whois = [] self.registerTextCommands(self) @@ -166,12 +166,12 @@ self._whois.append((priority, callback)) self._whois.sort(key=lambda item: item[0], reverse=True) - def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile): - """ Install SendMessage command hook """ - pre_xml_treatments.addCallback(self._sendMessageCmdHook, profile) + def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments): + """Install SendMessage command hook """ + pre_xml_treatments.addCallback(self._messageSendCmdHook, client) return True - def _sendMessageCmdHook(self, mess_data, profile): + def _messageSendCmdHook(self, mess_data, client): """ Check text commands in message, and react consequently msg starting with / are potential command. If a command is found, it is executed, else and help message is sent @@ -179,14 +179,25 @@ commands can abord message sending (if they return anything evaluating to False), or continue it (if they return True), eventually after modifying the message an "unparsed" key is added to message, containing part of the message not yet parsed commands can be deferred or not - @param mess_data(dict): data comming from sendMessage trigger + @param mess_data(dict): data comming from messageSend trigger @param profile: %(doc_profile)s """ - msg = mess_data["message"] + profile = client.profile + try: + msg = mess_data["message"][''] + msg_lang = '' + except KeyError: + try: + # we have not default message, we try to take the first found + msg_lang, msg = mess_data["message"].iteritems().next() + except StopIteration: + log.debug(u"No message found, skipping text commands") + return mess_data + try: if msg[:2] == '//': # we have a double '/', it's the escape sequence - mess_data["message"] = msg[1:] + mess_data["message"][msg_lang] = msg[1:] return mess_data if msg[0] != '/': return mess_data @@ -242,7 +253,7 @@ def _contextValid(self, mess_data, cmd_data): """Tell if a command can be used in the given context - @param mess_data(dict): message data as given in sendMessage trigger + @param mess_data(dict): message data as given in messageSend trigger @param cmd_data(dict): command data as returned by self._parseDocString @return (bool): True if command can be used in this context """ @@ -272,7 +283,7 @@ else: _from = self.host.getJidNStream(profile)[0] - self.host.bridge.newMessage(unicode(mess_data["to"]), message, C.MESS_TYPE_INFO, unicode(_from), {}, profile=profile) + self.host.bridge.messageNew(unicode(mess_data["to"]), {'': message}, {}, C.MESS_TYPE_INFO, unicode(_from), {}, profile=profile) def cmd_whois(self, mess_data, profile): """show informations on entity diff -r ccfe45302a5c -r 633b5c21aefd src/plugins/plugin_sec_otr.py --- a/src/plugins/plugin_sec_otr.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/plugins/plugin_sec_otr.py Tue May 24 22:11:04 2016 +0200 @@ -32,6 +32,8 @@ from sat.memory import persistent import potr import copy +import time +import uuid NS_OTR = "otr_plugin" PRIVATE_KEY = "PRIVATE KEY" @@ -73,11 +75,15 @@ msg = msg_str.decode('utf-8') client = self.user.client log.debug(u'inject(%s, appdata=%s, to=%s)' % (msg, appdata, self.peer)) - mess_data = {'message': msg, - 'type': 'chat', + mess_data = { 'from': client.jid, - 'to': self.peer, - 'subject': None, + 'to': self.peer, + 'uid': unicode(uuid.uuid4()), + 'message': {'': msg}, + 'subject': {}, + 'type': 'chat', + 'extra': {}, + 'timestamp': time.time(), } self.host.generateMessageXML(mess_data) client.xmlstream.send(mess_data['xml']) @@ -107,7 +113,7 @@ return client = self.user.client - self.host.bridge.newMessage(client.jid.full(), + self.host.bridge.messageNew(client.jid.full(), feedback, mess_type=C.MESS_TYPE_INFO, to_jid=self.peer.full(), @@ -202,7 +208,7 @@ self.context_managers = {} self.skipped_profiles = set() host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000) - host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000) + host.trigger.add("messageSend", self.messageSendTrigger, priority=100000) host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR) host.importMenu((MAIN_MENU, D_("Start/Refresh")), self._startRefresh, security_limit=0, help_string=D_("Start or refresh an OTR session"), type_=C.MENU_SINGLE) host.importMenu((MAIN_MENU, D_("End session")), self._endSession, security_limit=0, help_string=D_("Finish an OTR session"), type_=C.MENU_SINGLE) @@ -275,7 +281,7 @@ log.error(_("jid key is not present !")) return defer.fail(exceptions.DataError) otrctx = self.context_managers[profile].getContextForUser(to_jid) - query = otrctx.sendMessage(0, '?OTRv?') + query = otrctx.messageSend(0, '?OTRv?') otrctx.inject(query) return {} @@ -406,18 +412,21 @@ encrypted = True try: - res = otrctx.receiveMessage(data['body'].encode('utf-8')) + message = data['message'].itervalues().next() # FIXME: Q&D fix for message refactoring, message is now a dict + res = otrctx.receiveMessage(message.encode('utf-8')) except potr.context.UnencryptedMessage: if otrctx.state == potr.context.STATE_ENCRYPTED: log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': from_jid.full()}) client = self.host.getClient(profile) - self.host.bridge.newMessage(from_jid.full(), + self.host.bridge.messageNew(from_jid.full(), _(u"WARNING: received unencrypted data in a supposedly encrypted context"), mess_type=C.MESS_TYPE_INFO, to_jid=client.jid.full(), extra={}, profile=client.profile) encrypted = False + except StopIteration: + return data if not encrypted: return data @@ -425,7 +434,7 @@ if res[0] != None: # decrypted messages handling. # receiveMessage() will return a tuple, the first part of which will be the decrypted message - data['body'] = res[0].decode('utf-8') + data['message'] = {'':res[0].decode('utf-8')} # FIXME: Q&D fix for message refactoring, message is now a dict raise failure.Failure(exceptions.SkipHistory()) # we send the decrypted message to frontends, but we don't want it in history else: raise failure.Failure(exceptions.CancelError()) # no message at all (no history, no signal) @@ -433,19 +442,24 @@ def _receivedTreatmentForSkippedProfiles(self, data, profile): """This profile must be skipped because the frontend manages OTR itself, but we still need to check if the message must be stored in history or not""" - body = data['body'].encode('utf-8') - if body.startswith(potr.proto.OTRTAG): + try: + message = data['message'].itervalues().next().encode('utf-8') # FIXME: Q&D fix for message refactoring, message is now a dict + except StopIteration: + return data + if message.startswith(potr.proto.OTRTAG): raise failure.Failure(exceptions.SkipHistory()) return data - def MessageReceivedTrigger(self, message, post_treat, profile): + def MessageReceivedTrigger(self, client, message, post_treat): + profile = client.profile if profile in self.skipped_profiles: post_treat.addCallback(self._receivedTreatmentForSkippedProfiles, profile) else: post_treat.addCallback(self._receivedTreatment, profile) return True - def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile): + def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments): + profile = client.profile if profile in self.skipped_profiles: return True to_jid = copy.copy(mess_data['to']) @@ -455,13 +469,19 @@ if mess_data['type'] != 'groupchat' and otrctx.state != potr.context.STATE_PLAINTEXT: if otrctx.state == potr.context.STATE_ENCRYPTED: log.debug(u"encrypting message") - otrctx.sendMessage(0, mess_data['message'].encode('utf-8')) - client = self.host.getClient(profile) + try: + msg = mess_data['message'][''] + except KeyError: + try: + msg = mess_data['message'].itervalues().next() + except StopIteration: + log.warning(u"No message found") + return False + otrctx.sendMessage(0, msg.encode('utf-8')) self.host.sendMessageToBridge(mess_data, client) else: feedback = D_("Your message was not sent because your correspondent closed the encrypted conversation on his/her side. Either close your own side, or refresh the session.") - client = self.host.getClient(profile) - self.host.bridge.newMessage(to_jid.full(), + self.host.bridge.messageNew(to_jid.full(), feedback, mess_type=C.MESS_TYPE_INFO, to_jid=client.jid.full(), diff -r ccfe45302a5c -r 633b5c21aefd src/plugins/plugin_xep_0033.py --- a/src/plugins/plugin_xep_0033.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/plugins/plugin_xep_0033.py Tue May 24 22:11:04 2016 +0200 @@ -72,11 +72,12 @@ log.info(_("Extended Stanza Addressing plugin initialization")) self.host = host self.internal_data = {} - host.trigger.add("sendMessage", self.sendMessageTrigger, trigger.TriggerManager.MIN_PRIORITY) + host.trigger.add("messageSend", self.messageSendTrigger, trigger.TriggerManager.MIN_PRIORITY) host.trigger.add("MessageReceived", self.messageReceivedTrigger) - def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile): + def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments): """Process the XEP-0033 related data to be sent""" + profile = client.profile def treatment(mess_data): if not 'address' in mess_data['extra']: @@ -117,7 +118,7 @@ Ideas: - fix Prosody plugin to check if target server support the feature - redesign the database to save only one entry to the database - - change the newMessage signal to eventually pass more than one recipient + - change the messageNew signal to eventually pass more than one recipient """ def send(mess_data, skip_send=False): client = self.host.profiles[profile] @@ -161,7 +162,7 @@ d = defer.Deferred().addCallback(lambda dummy: self.internal_data.pop(timestamp)) defer.DeferredList(defer_list).chainDeferred(d) - def messageReceivedTrigger(self, message, post_treat, profile): + def messageReceivedTrigger(self, client, message, post_treat): """In order to save the addressing information in the history""" def post_treat_addr(data, addresses): data['extra']['addresses'] = "" @@ -174,9 +175,10 @@ try: addresses = message.elements(NS_ADDRESS, 'addresses').next() - post_treat.addCallback(post_treat_addr, addresses.children) except StopIteration: pass # no addresses + else: + post_treat.addCallback(post_treat_addr, addresses.children) return True def getHandler(self, profile): diff -r ccfe45302a5c -r 633b5c21aefd src/plugins/plugin_xep_0071.py --- a/src/plugins/plugin_xep_0071.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/plugins/plugin_xep_0071.py Tue May 24 22:11:04 2016 +0200 @@ -22,6 +22,7 @@ from sat.core.log import getLogger log = getLogger(__name__) +from twisted.internet import defer from wokkel import disco, iwokkel from zope.interface import implements # from lxml import etree @@ -75,68 +76,108 @@ def __init__(self, host): log.info(_("XHTML-IM plugin initialization")) self.host = host - self.synt_plg = self.host.plugins["TEXT-SYNTAXES"] - self.synt_plg.addSyntax(self.SYNTAX_XHTML_IM, lambda xhtml: xhtml, self.XHTML2XHTML_IM, [self.synt_plg.OPT_HIDDEN]) + self._s = self.host.plugins["TEXT-SYNTAXES"] + self._s.addSyntax(self.SYNTAX_XHTML_IM, lambda xhtml: xhtml, self.XHTML2XHTML_IM, [self._s.OPT_HIDDEN]) host.trigger.add("MessageReceived", self.messageReceivedTrigger) - host.trigger.add("sendMessage", self.sendMessageTrigger) + host.trigger.add("messageSend", self.messageSendTrigger) def getHandler(self, profile): return XEP_0071_handler(self) - def _messagePostTreat(self, data, body_elt): - """ Callback which manage the post treatment of the message in case of XHTML-IM found + def _messagePostTreat(self, data, body_elts): + """Callback which manage the post treatment of the message in case of XHTML-IM found @param data: data send by MessageReceived trigger through post_treat deferred - @param xhtml_im: XHTML-IM body element found + @param body_elts: XHTML-IM body elements found @return: the data with the extra parameter updated """ - #TODO: check if text only body is empty, then try to convert XHTML-IM to pure text and show a warning message + # TODO: check if text only body is empty, then try to convert XHTML-IM to pure text and show a warning message def converted(xhtml): - data['extra']['xhtml'] = xhtml - return data - d = self.synt_plg.convert(body_elt.toXml(), self.SYNTAX_XHTML_IM, safe=True) - d.addCallback(converted) - return d + if lang: + data['extra']['xhtml_{}'.format(lang)] = xhtml + else: + data['extra']['xhtml'] = xhtml - def _sendMessageAddRich(self, mess_data, profile): + defers = [] + for body_elt in body_elts: + lang = body_elt.getAttribute('xml:lang', '') + d = self._s.convert(body_elt.toXml(), self.SYNTAX_XHTML_IM, safe=True) + d.addCallback(converted, lang) + defers.append(d) + + d_list = defer.DeferredList(defers) + d_list.addCallback(lambda dummy: data) + return d_list + + def _messageSendAddRich(self, data, client): """ Construct XHTML-IM node and add it XML element - @param mess_data: message data as sended by sendMessage callback + + @param data: message data as sended by messageSend callback """ - def syntax_converted(xhtml_im): - message_elt = mess_data['xml'] - html_elt = message_elt.addElement('html', NS_XHTML_IM) - body_elt = html_elt.addElement('body', NS_XHTML) + # at this point, either ['extra']['rich'] or ['extra']['xhtml'] exists + # but both can't exist at the same time + message_elt = data['xml'] + html_elt = message_elt.addElement((NS_XHTML_IM, 'html')) + + def syntax_converted(xhtml_im, lang): + body_elt = html_elt.addElement((NS_XHTML, 'body')) + if lang: + body_elt['xml:lang'] = lang + data['extra']['xhtml_{}'.format(lang)] = xhtml_im + else: + data['extra']['xhtml'] = xhtml_im body_elt.addRawXml(xhtml_im) - mess_data['extra']['xhtml'] = xhtml_im - return mess_data - syntax = self.synt_plg.getCurrentSyntax(profile) - rich = mess_data['extra'].get('rich', '') - xhtml = mess_data['extra'].get('xhtml', '') - if rich: - d = self.synt_plg.convert(rich, syntax, self.SYNTAX_XHTML_IM) - if xhtml: - raise exceptions.DataError(_("Can't have xhtml and rich content at the same time")) - d.addCallback(syntax_converted) - return d + syntax = self._s.getCurrentSyntax(client.profile) + defers = [] + try: + rich = data['extra']['rich'] + except KeyError: + # we have directly XHTML + for lang, xhtml in data['extra']['xhtml'].iteritems(): + d = self._s.convert(xhtml, self._s.SYNTAX_XHTML, self.SYNTAX_XHTML_IM) + d.addCallback(syntax_converted, lang) + defers.append(d) + else: + # we have rich syntax to convert + for lang, rich_data in rich.iteritems(): + d = self._s.convert(rich_data, syntax, self.SYNTAX_XHTML_IM) + d.addCallback(syntax_converted, lang) + defers.append(d) + d_list = defer.DeferredList(defers) + d_list.addCallback(lambda dummy: data) + return d_list - def messageReceivedTrigger(self, message, post_treat, profile): + def messageReceivedTrigger(self, client, message, post_treat): """ Check presence of XHTML-IM in message """ try: html_elt = message.elements(NS_XHTML_IM, 'html').next() - body_elt = html_elt.elements(NS_XHTML, 'body').next() - # OK, we have found rich text - post_treat.addCallback(self._messagePostTreat, body_elt) except StopIteration: # No XHTML-IM pass + else: + body_elts = html_elt.elements(NS_XHTML, 'body') + post_treat.addCallback(self._messagePostTreat, body_elts) return True - def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile): + def messageSendTrigger(self, client, data, pre_xml_treatments, post_xml_treatments): """ Check presence of rich text in extra """ - if 'rich' in mess_data['extra'] or 'xhtml' in mess_data['extra']: - post_xml_treatments.addCallback(self._sendMessageAddRich, profile) + rich = {} + xhtml = {} + for key, value in data['extra'].iteritems(): + if key.startswith('rich'): + rich[key[5:]] = value + elif key.startswith('xhtml'): + xhtml[key[6:]] = value + if rich and xhtml: + raise exceptions.DataError(_(u"Can't have XHTML and rich content at the same time")) + if rich or xhtml: + if rich: + data['rich'] = rich + else: + data['xhtml'] = xhtml + post_xml_treatments.addCallback(self._messageSendAddRich, client) return True def _purgeStyle(self, styles_raw): diff -r ccfe45302a5c -r 633b5c21aefd src/plugins/plugin_xep_0085.py --- a/src/plugins/plugin_xep_0085.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/plugins/plugin_xep_0085.py Tue May 24 22:11:04 2016 +0200 @@ -103,7 +103,7 @@ # triggers from core host.trigger.add("MessageReceived", self.messageReceivedTrigger) - host.trigger.add("sendMessage", self.sendMessageTrigger) + host.trigger.add("messageSend", self.messageSendTrigger) host.trigger.add("paramUpdateTrigger", self.paramUpdateTrigger) # args: to_s (jid as string), profile @@ -156,11 +156,12 @@ return False return True - def messageReceivedTrigger(self, message, post_treat, profile): + def messageReceivedTrigger(self, client, message, post_treat): """ Update the entity cache when we receive a message with body. Check for a chat state in the message and signal frontends. """ + profile = client.profile if not self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile): return True @@ -197,11 +198,12 @@ break return True - def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile): + def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments): """ Eventually add the chat state to the message and initiate the state machine when sending an "active" state. """ + profile = client.profile def treatment(mess_data): message = mess_data['xml'] to_jid = JID(message.getAttribute("to")) @@ -362,12 +364,15 @@ # send a new message without body log.debug(u"sending state '{state}' to {jid}".format(state=state, jid=self.to_jid.full())) client = self.host.getClient(self.profile) - mess_data = {'message': None, - 'type': self.mess_type, - 'from': client.jid, - 'to': self.to_jid, - 'subject': None - } + mess_data = { + 'from': client.jid, + 'to': self.to_jid, + 'uid': '', + 'message': {}, + 'type': self.mess_type, + 'subject': {}, + 'extra': {}, + } self.host.generateMessageXML(mess_data) mess_data['xml'].addElement(state, NS_CHAT_STATES) client.xmlstream.send(mess_data['xml']) diff -r ccfe45302a5c -r 633b5c21aefd src/plugins/plugin_xep_0203.py --- a/src/plugins/plugin_xep_0203.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/plugins/plugin_xep_0203.py Tue May 24 22:11:04 2016 +0200 @@ -22,7 +22,6 @@ from sat.core.log import getLogger log = getLogger(__name__) -from calendar import timegm from wokkel import disco, iwokkel, delay try: from twisted.words.protocols.xmlstream import XMPPHandler @@ -49,8 +48,6 @@ def __init__(self, host): log.info(_("Delayed Delivery plugin initialization")) self.host = host - host.trigger.add("MessageReceived", self.messageReceivedTrigger) - def getHandler(self, profile): return XEP_0203_handler(self, profile) @@ -71,30 +68,6 @@ parent.addChild(elt) return elt - def messagePostTreat(self, data, timestamp): - """Set the timestamp of a received message. - - @param data (dict): data send by MessageReceived trigger through post_treat deferred - @param timestamp (int): original timestamp of a delayed message - @return: dict - """ - data['extra']['timestamp'] = unicode(timestamp) - return data - - def messageReceivedTrigger(self, message, post_treat, profile): - """Process a delay element from incoming message. - - @param message (domish.Element): message element - @param post_treat (Deferred): deferred instance to add post treatments - """ - try: - delay_ = delay.Delay.fromElement([elm for elm in message.elements() if elm.name == 'delay'][0]) - except IndexError: - return True - else: - timestamp = timegm(delay_.stamp.utctimetuple()) - post_treat.addCallback(self.messagePostTreat, timestamp) - return True class XEP_0203_handler(XMPPHandler): implements(iwokkel.IDisco) diff -r ccfe45302a5c -r 633b5c21aefd src/plugins/plugin_xep_0334.py --- a/src/plugins/plugin_xep_0334.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/plugins/plugin_xep_0334.py Tue May 24 22:11:04 2016 +0200 @@ -51,13 +51,13 @@ def __init__(self, host): log.info(_("Message Processing Hints plugin initialization")) self.host = host - host.trigger.add("sendMessage", self.sendMessageTrigger) + host.trigger.add("messageSend", self.messageSendTrigger) host.trigger.add("MessageReceived", self.messageReceivedTrigger) def getHandler(self, profile): return XEP_0334_handler(self, profile) - def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile): + def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments): """Add the hints element to the message to be sent""" hints = [] for key in ('no-permanent-storage', 'no-storage', 'no-copy'): @@ -78,7 +78,7 @@ post_xml_treatments.addCallback(treatment) return True - def messageReceivedTrigger(self, message, post_treat, profile): + def messageReceivedTrigger(self, client, message, post_treat): """Check for hints in the received message""" hints = [] for key in ('no-permanent-storage', 'no-storage'): diff -r ccfe45302a5c -r 633b5c21aefd src/test/helpers.py --- a/src/test/helpers.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/test/helpers.py Tue May 24 22:11:04 2016 +0200 @@ -104,7 +104,7 @@ def registerCallback(self, callback, *args, **kwargs): pass - def sendMessage(self, to_s, msg, subject=None, mess_type='auto', extra={}, profile_key='@NONE@'): + def messageSend(self, to_s, msg, subject=None, mess_type='auto', extra={}, profile_key='@NONE@'): self.sendAndStoreMessage({"to": JID(to_s)}) def _sendMessageToStream(self, mess_data, client): diff -r ccfe45302a5c -r 633b5c21aefd src/test/test_core_xmpp.py --- a/src/test/test_core_xmpp.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/test/test_core_xmpp.py Tue May 24 22:11:04 2016 +0200 @@ -53,7 +53,7 @@ """ stanza = parseXml(xml) - self.host.bridge.expectCall("newMessage", u"sender@example.net/house", u"test", u"chat", u"test@example.org/SàT", {}, profile=Const.PROFILE[0]) + self.host.bridge.expectCall("messageNew", u"sender@example.net/house", u"test", u"chat", u"test@example.org/SàT", {}, profile=Const.PROFILE[0]) self.message.onMessage(stanza) diff -r ccfe45302a5c -r 633b5c21aefd src/test/test_plugin_xep_0033.py --- a/src/test/test_plugin_xep_0033.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/test/test_plugin_xep_0033.py Tue May 24 22:11:04 2016 +0200 @@ -27,7 +27,6 @@ from twisted.internet import defer from wokkel.generic import parseXml from twisted.words.protocols.jabber.jid import JID -from logging import ERROR PROFILE_INDEX = 0 PROFILE = Const.PROFILE[PROFILE_INDEX] @@ -60,7 +59,7 @@ """ % (JID_STR_FROM, JID_STR_TO, JID_STR_X_TO, JID_STR_X_CC, JID_STR_X_BCC) stanza = parseXml(xml.encode("utf-8")) treatments = defer.Deferred() - self.plugin.messageReceivedTrigger(stanza, treatments, PROFILE) + self.plugin.messageReceivedTrigger(self.host.getClient(PROFILE), stanza, treatments) data = {'extra': {}} def cb(data): @@ -87,7 +86,7 @@ return mess_data def _assertAddresses(self, mess_data): - """The mess_data that we got here has been modified by self.plugin.sendMessageTrigger, + """The mess_data that we got here has been modified by self.plugin.messageSendTrigger, check that the addresses element has been added to the stanza.""" expected = self._get_mess_data()['xml'] addresses_extra = """ @@ -135,28 +134,28 @@ return defer.DeferredList(d_list).addCallback(cb_list) def _trigger(self, data): - """Execute self.plugin.sendMessageTrigger with a different logging + """Execute self.plugin.messageSendTrigger with a different logging level to not pollute the output, then check that the plugin did its job. It should abort sending the message or add the extended addressing information to the stanza. - @param data: the data to be processed by self.plugin.sendMessageTrigger + @param data: the data to be processed by self.plugin.messageSendTrigger """ pre_treatments = defer.Deferred() post_treatments = defer.Deferred() helpers.muteLogging() - self.plugin.sendMessageTrigger(data, pre_treatments, post_treatments, PROFILE) + self.plugin.messageSendTrigger(self.host.getClient[PROFILE], data, pre_treatments, post_treatments) post_treatments.callback(data) helpers.unmuteLogging() post_treatments.addCallbacks(self._assertAddresses, lambda failure: failure.trap(CancelError)) return post_treatments - def test_sendMessageTriggerFeatureNotSupported(self): + def test_messageSendTriggerFeatureNotSupported(self): # feature is not supported, abort the message self.host.memory.reinit() data = self._get_mess_data() return self._trigger(data) - def test_sendMessageTriggerFeatureSupported(self): + def test_messageSendTriggerFeatureSupported(self): # feature is supported by the main target server self.host.reinit() self.host.addFeature(JID(JID_STR_TO), plugin.NS_ADDRESS, PROFILE) @@ -164,7 +163,7 @@ d = self._trigger(data) return d.addCallback(lambda dummy: self._checkSentAndStored()) - def test_sendMessageTriggerFeatureFullySupported(self): + def test_messageSendTriggerFeatureFullySupported(self): # feature is supported by all target servers self.host.reinit() self.host.addFeature(JID(JID_STR_TO), plugin.NS_ADDRESS, PROFILE) @@ -174,7 +173,7 @@ d = self._trigger(data) return d.addCallback(lambda dummy: self._checkSentAndStored()) - def test_sendMessageTriggerFixWrongEntity(self): + def test_messageSendTriggerFixWrongEntity(self): # check that a wrong recipient entity is fixed by the backend self.host.reinit() self.host.addFeature(JID(JID_STR_TO), plugin.NS_ADDRESS, PROFILE) diff -r ccfe45302a5c -r 633b5c21aefd src/test/test_plugin_xep_0085.py --- a/src/test/test_plugin_xep_0085.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/test/test_plugin_xep_0085.py Tue May 24 22:11:04 2016 +0200 @@ -49,9 +49,9 @@ state, plugin.NS_CHAT_STATES) stanza = parseXml(xml.encode("utf-8")) self.host.bridge.expectCall("chatStateReceived", Const.JID_STR[1], state, Const.PROFILE[0]) - self.plugin.messageReceivedTrigger(stanza, None, Const.PROFILE[0]) + self.plugin.messageReceivedTrigger(self.host.getClient(Const.PROFILE[0]), stanza, None) - def test_sendMessageTrigger(self): + def test_messageSendTrigger(self): def cb(data): xml = data['xml'].toXml().encode("utf-8") self.assertEqualXML(xml, expected.toXml().encode("utf-8")) @@ -73,7 +73,7 @@ expected = deepcopy(mess_data['xml']) expected.addElement(state, plugin.NS_CHAT_STATES) post_treatments = defer.Deferred() - self.plugin.sendMessageTrigger(mess_data, None, post_treatments, Const.PROFILE[0]) + self.plugin.messageSendTrigger(self.host.getClient(Const.PROFILE[0]), mess_data, None, post_treatments) post_treatments.addCallback(cb) post_treatments.callback(mess_data) diff -r ccfe45302a5c -r 633b5c21aefd src/test/test_plugin_xep_0334.py --- a/src/test/test_plugin_xep_0334.py Mon Apr 18 18:35:19 2016 +0200 +++ b/src/test/test_plugin_xep_0334.py Tue May 24 22:11:04 2016 +0200 @@ -36,7 +36,7 @@ self.host = helpers.FakeSAT() self.plugin = XEP_0334(self.host) - def test_sendMessageTrigger(self): + def test_messageSendTrigger(self): template_xml = """ ' % key) treatments.addCallback(cb, expected_xml) @@ -90,7 +90,7 @@ for key in (HINTS + ('dummy_hint',)): message = parseXml(template_xml % ('<%s xmlns="urn:xmpp:hints"/>' % key)) post_treat = defer.Deferred() - self.plugin.messageReceivedTrigger(message, post_treat, C.PROFILE[0]) + self.plugin.messageReceivedTrigger(self.host.getClient(C.PROFILE[0]), message, post_treat) if post_treat.callbacks: assert(key in ('no-permanent-storage', 'no-storage')) post_treat.addCallbacks(cb, eb)