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.
This commit is contained in:
Goffi 2016-05-24 22:11:04 +02:00
commit f12803fa65
30 changed files with 784 additions and 453 deletions

View file

@ -336,15 +336,6 @@ class DBusBridgeFrontend(BridgeFrontend):
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 @@ class DBusBridgeFrontend(BridgeFrontend):
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 @@ class DBusBridgeFrontend(BridgeFrontend):
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 @@ class DBusBridgeFrontend(BridgeFrontend):
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

View file

@ -88,7 +88,6 @@ class Chat(PrimitivusWidget, QuickChat):
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)

View file

@ -96,12 +96,13 @@ class EditBar(sat_widgets.ModalEdit):
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 @@ class PrimitivusApp(QuickApp, InputHistory):
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)

View file

@ -248,7 +248,7 @@ class QuickApp(object):
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 @@ class QuickApp(object):
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 @@ class QuickApp(object):
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 @@ class QuickApp(object):
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']

View file

@ -25,7 +25,6 @@ from sat_frontends.quick_frontend import quick_widgets
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 @@ class QuickChat(quick_widgets.QuickWidget):
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 @@ class QuickChat(quick_widgets.QuickWidget):
"""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 @@ class QuickChat(quick_widgets.QuickWidget):
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 @@ class QuickChat(quick_widgets.QuickWidget):
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 @@ class QuickChat(quick_widgets.QuickWidget):
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 @@ class QuickChat(quick_widgets.QuickWidget):
"""
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 @@ class QuickChat(quick_widgets.QuickWidget):
@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.

View file

@ -549,6 +549,7 @@ class ProfileContactList(object):
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:

View file

@ -247,8 +247,8 @@ class QuickWidgetsManager(object):
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):
"""

View file

@ -165,6 +165,11 @@ class DbusObject(dbus.service.Object):
def entityDataUpdated(self, jid, name, value, profile):
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):
@ -175,11 +180,6 @@ class DbusObject(dbus.service.Object):
def newContact(self, contact_jid, attributes, groups, profile):
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):
@ -320,12 +320,6 @@ class DbusObject(dbus.service.Object):
def getFeatures(self, profile_key, callback=None, errback=None):
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)
@ -404,6 +398,12 @@ class DbusObject(dbus.service.Object):
def getWaitingSub(self, profile_key="@DEFAULT@"):
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)
@ -422,6 +422,12 @@ class DbusObject(dbus.service.Object):
def loadParamsTemplate(self, filename):
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)
@ -470,12 +476,6 @@ class DbusObject(dbus.service.Object):
def saveParamsTemplate(self, filename):
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)
@ -626,15 +626,15 @@ class DBusBridge(Bridge):
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)

View file

@ -34,17 +34,24 @@ doc_param_1=attributes: Dictionary of attributes where keys are:
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_param_0=%(doc_profile_key)s
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_4_default={}
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_0=xml: XML definition of the parameters to be added
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_2=limit: max number of history elements to get (0 for the whole histor
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

View file

@ -40,6 +40,7 @@ from uuid import uuid4
import sys
import os.path
import uuid
import time
try:
from collections import OrderedDict # only available from python 2.7
@ -85,7 +86,7 @@ class SAT(service.Service):
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 @@ class SAT(service.Service):
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 @@ class SAT(service.Service):
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 <message/> 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)
@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 _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.sendMessage(to_jid, msg, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()}, profile_key=profile_key)
return self.messageSend(client, to_jid, message, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()})
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, 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
"to": to_jid,
data = { # dict is similar to the one used in client.onMessage
"from": client.jid,
"message": msg,
"to": to_jid,
"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 @@ class SAT(service.Service):
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)

View file

@ -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 @@ class SatXMPPClient(client.XMPPClient):
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 @@ class SatMessageProtocol(xmppim.MessageProtocol):
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}
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]
try:
data['stanza_id'] = message_elt['id']
except KeyError:
pass
else:
client._mess_id_uid[(data['from'], data['stanza_id'])] = data['uid']
data['type'] = message['type'] if message.hasAttribute('type') else 'normal'
# 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)
# 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 @@ class RegisteringAuthenticator(xmlstream.ConnectAuthenticator):
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 @@ class RegisteringAuthenticator(xmlstream.ConnectAuthenticator):
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):

View file

@ -510,14 +510,12 @@ class Memory(object):
## 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 @@ class Memory(object):
@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 @@ class Memory(object):
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 ##

View file

@ -27,13 +27,12 @@ from sat.tools.config import fixConfigOption
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 @@ DATABASE_SCHEMAS = {
('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 @@ DATABASE_SCHEMAS = {
("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 @@ class SqliteStorage(object):
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 @@ class SqliteStorage(object):
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 @@ class SqliteStorage(object):
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 @@ class Updater(object):
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 @@ class Updater(object):
@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 @@ class Updater(object):
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

View file

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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 @@ class ExportCommandProtocol(protocol.ProcessProtocol):
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 @@ class CommandExport(object):
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

View file

@ -43,22 +43,21 @@ class Exp_Parrot(object):
"""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 @@ class Exp_Parrot(object):
# 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 @@ class Exp_Parrot(object):
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

View file

@ -154,11 +154,11 @@ class SatMailbox(object):
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)

View file

@ -98,18 +98,19 @@ class MaildirBox(object):
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

View file

@ -87,7 +87,7 @@ class SatSmtpMessage(object):
"""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()

View file

@ -58,7 +58,7 @@ class TextCommands(object):
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 @@ class TextCommands(object):
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 @@ class TextCommands(object):
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 @@ class TextCommands(object):
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 @@ class TextCommands(object):
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

View file

@ -32,6 +32,8 @@ from twisted.internet import defer
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 @@ class Context(potr.context.Context):
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 @@ class Context(potr.context.Context):
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 @@ class OTR(object):
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 @@ class OTR(object):
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 @@ class OTR(object):
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 @@ class OTR(object):
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 @@ class OTR(object):
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 @@ class OTR(object):
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(),

View file

@ -72,11 +72,12 @@ class XEP_0033(object):
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 @@ class XEP_0033(object):
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 @@ class XEP_0033(object):
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 @@ class XEP_0033(object):
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):

View file

@ -22,6 +22,7 @@ from sat.core import exceptions
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 @@ class XEP_0071(object):
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):

View file

@ -103,7 +103,7 @@ class XEP_0085(object):
# 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 @@ class XEP_0085(object):
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 @@ class XEP_0085(object):
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 @@ class ChatStateMachine(object):
# 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'])

View file

@ -22,7 +22,6 @@ from sat.core.i18n import _
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 @@ class XEP_0203(object):
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 @@ class XEP_0203(object):
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)

View file

@ -51,13 +51,13 @@ class XEP_0334(object):
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 @@ class XEP_0334(object):
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'):

View file

@ -104,7 +104,7 @@ class FakeSAT(object):
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):

View file

@ -53,7 +53,7 @@ class SatMessageProtocolTest(unittest.TestCase):
</message>
"""
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)

View file

@ -27,7 +27,6 @@ from sat.core.exceptions import CancelError
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 @@ class XEP_0033Test(helpers.SatTestCase):
""" % (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 @@ class XEP_0033Test(helpers.SatTestCase):
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 @@ class XEP_0033Test(helpers.SatTestCase):
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 @@ class XEP_0033Test(helpers.SatTestCase):
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 @@ class XEP_0033Test(helpers.SatTestCase):
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)

View file

@ -49,9 +49,9 @@ class XEP_0085Test(helpers.SatTestCase):
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 @@ class XEP_0085Test(helpers.SatTestCase):
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)

View file

@ -36,7 +36,7 @@ class XEP_0334Test(helpers.SatTestCase):
self.host = helpers.FakeSAT()
self.plugin = XEP_0334(self.host)
def test_sendMessageTrigger(self):
def test_messageSendTrigger(self):
template_xml = """
<message
from='romeo@montague.net/orchard'
@ -59,7 +59,7 @@ class XEP_0334Test(helpers.SatTestCase):
'extra': {key: True}
}
treatments = defer.Deferred()
self.plugin.sendMessageTrigger(mess_data, defer.Deferred(), treatments, C.PROFILE[0])
self.plugin.messageSendTrigger(self.host.getClient(C.PROFILE[0]), mess_data, defer.Deferred(), treatments)
if treatments.callbacks: # the trigger added a callback
expected_xml = template_xml % ('<%s xmlns="urn:xmpp:hints"/>' % key)
treatments.addCallback(cb, expected_xml)
@ -90,7 +90,7 @@ class XEP_0334Test(helpers.SatTestCase):
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)