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
|
|
@ -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'):
|
||||
|
|
|
|||
Loading…
Reference in a new issue