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:
parent
eff7ec3ded
commit
f12803fa65
30 changed files with 784 additions and 453 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
111
src/core/xmpp.py
111
src/core/xmpp.py
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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 ##
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue