comparison src/core/sat_main.py @ 1955:633b5c21aefd

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.
author Goffi <goffi@goffi.org>
date Tue, 24 May 2016 22:11:04 +0200
parents 2daf7b4c6756
children a2bc5089c2eb
comparison
equal deleted inserted replaced
1943:ccfe45302a5c 1955:633b5c21aefd
38 from glob import glob 38 from glob import glob
39 from uuid import uuid4 39 from uuid import uuid4
40 import sys 40 import sys
41 import os.path 41 import os.path
42 import uuid 42 import uuid
43 import time
43 44
44 try: 45 try:
45 from collections import OrderedDict # only available from python 2.7 46 from collections import OrderedDict # only available from python 2.7
46 except ImportError: 47 except ImportError:
47 from ordereddict import OrderedDict 48 from ordereddict import OrderedDict
83 self.bridge.register("getContactsFromGroup", self.getContactsFromGroup) 84 self.bridge.register("getContactsFromGroup", self.getContactsFromGroup)
84 self.bridge.register("getMainResource", self.memory._getMainResource) 85 self.bridge.register("getMainResource", self.memory._getMainResource)
85 self.bridge.register("getPresenceStatuses", self.memory._getPresenceStatuses) 86 self.bridge.register("getPresenceStatuses", self.memory._getPresenceStatuses)
86 self.bridge.register("getWaitingSub", self.memory.getWaitingSub) 87 self.bridge.register("getWaitingSub", self.memory.getWaitingSub)
87 self.bridge.register("getWaitingConf", self.getWaitingConf) 88 self.bridge.register("getWaitingConf", self.getWaitingConf)
88 self.bridge.register("sendMessage", self._sendMessage) 89 self.bridge.register("messageSend", self._messageSend)
89 self.bridge.register("getConfig", self._getConfig) 90 self.bridge.register("getConfig", self._getConfig)
90 self.bridge.register("setParam", self.setParam) 91 self.bridge.register("setParam", self.setParam)
91 self.bridge.register("getParamA", self.memory.getStringParamA) 92 self.bridge.register("getParamA", self.memory.getStringParamA)
92 self.bridge.register("asyncGetParamA", self.memory.asyncGetStringParamA) 93 self.bridge.register("asyncGetParamA", self.memory.asyncGetStringParamA)
93 self.bridge.register("asyncGetParamsValuesFromCategory", self.memory.asyncGetParamsValuesFromCategory) 94 self.bridge.register("asyncGetParamsValuesFromCategory", self.memory.asyncGetParamsValuesFromCategory)
94 self.bridge.register("getParamsUI", self.memory.getParamsUI) 95 self.bridge.register("getParamsUI", self.memory.getParamsUI)
95 self.bridge.register("getParamsCategories", self.memory.getParamsCategories) 96 self.bridge.register("getParamsCategories", self.memory.getParamsCategories)
96 self.bridge.register("paramsRegisterApp", self.memory.paramsRegisterApp) 97 self.bridge.register("paramsRegisterApp", self.memory.paramsRegisterApp)
97 self.bridge.register("getHistory", self.memory.getHistory) 98 self.bridge.register("historyGet", self.memory.historyGet)
98 self.bridge.register("setPresence", self._setPresence) 99 self.bridge.register("setPresence", self._setPresence)
99 self.bridge.register("subscription", self.subscription) 100 self.bridge.register("subscription", self.subscription)
100 self.bridge.register("addContact", self._addContact) 101 self.bridge.register("addContact", self._addContact)
101 self.bridge.register("updateContact", self._updateContact) 102 self.bridge.register("updateContact", self._updateContact)
102 self.bridge.register("delContact", self._delContact) 103 self.bridge.register("delContact", self._delContact)
525 for conf_id in client._waiting_conf: 526 for conf_id in client._waiting_conf:
526 conf_type, data = client._waiting_conf[conf_id][:2] 527 conf_type, data = client._waiting_conf[conf_id][:2]
527 ret.append((conf_id, conf_type, data)) 528 ret.append((conf_id, conf_type, data))
528 return ret 529 return ret
529 530
530 def generateMessageXML(self, mess_data): 531 def generateMessageXML(self, data):
531 mess_data['xml'] = domish.Element((None, 'message')) 532 """Generate <message/> stanza from message data
532 mess_data['xml']["to"] = mess_data["to"].full() 533
533 mess_data['xml']["from"] = mess_data['from'].full() 534 @param data(dict): message data
534 mess_data['xml']["type"] = mess_data["type"] 535 domish element will be put in data['xml']
535 mess_data['xml']['id'] = str(uuid4()) 536 following keys are needed:
536 if mess_data["subject"]: 537 - from
537 mess_data['xml'].addElement("subject", None, mess_data['subject']) 538 - to
538 if mess_data["message"]: # message without body are used to send chat states 539 - uid: can be set to '' if uid attribute is not wanted
539 mess_data['xml'].addElement("body", None, mess_data["message"]) 540 - message
540 return mess_data 541 - type
541 542 - subject
542 def _sendMessage(self, to_s, msg, subject=None, mess_type='auto', extra={}, profile_key=C.PROF_KEY_NONE): 543 - extra
543 to_jid = jid.JID(to_s) 544 @return (dict) message data
545 """
546 data['xml'] = message_elt = domish.Element((None, 'message'))
547 message_elt["to"] = data["to"].full()
548 message_elt["from"] = data['from'].full()
549 message_elt["type"] = data["type"]
550 if data['uid']: # key must be present but can be set to ''
551 # by a plugin to avoid id on purpose
552 message_elt['id'] = data['uid']
553 for lang, subject in data["subject"].iteritems():
554 subject_elt = message_elt.addElement("subject", content=subject)
555 if lang:
556 subject_elt['xml:lang'] = lang
557 for lang, message in data["message"].iteritems():
558 body_elt = message_elt.addElement("body", content=message)
559 if lang:
560 body_elt['xml:lang'] = lang
561 try:
562 thread = data['extra']['thread']
563 except KeyError:
564 if 'thread_parent' in data['extra']:
565 raise exceptions.InternalError(u"thread_parent found while there is not associated thread")
566 else:
567 thread_elt = message_elt.addElement("thread", content=thread)
568 try:
569 thread_elt["parent"] = data["extra"]["thread_parent"]
570 except KeyError:
571 pass
572 return data
573
574 def _messageSend(self, to_jid_s, message, subject=None, mess_type='auto', extra=None, profile_key=C.PROF_KEY_NONE):
575 client = self.getClient(profile_key)
576 to_jid = jid.JID(to_jid_s)
544 #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 577 #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
545 return self.sendMessage(to_jid, msg, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()}, profile_key=profile_key) 578 return self.messageSend(client, to_jid, message, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()})
546 579
547 def sendMessage(self, to_jid, msg, subject=None, mess_type='auto', extra={}, no_trigger=False, profile_key=C.PROF_KEY_NONE): 580 def messageSend(self, client, to_jid, message, subject=None, mess_type='auto', extra=None, uid=None, no_trigger=False):
548 #FIXME: check validity of recipient 581 """Send a message to an entity
549 profile = self.memory.getProfileName(profile_key) 582
550 assert profile 583 @param to_jid(jid.JID): destinee of the message
551 client = self.profiles[profile] 584 @param message(dict): message body, key is the language (use '' when unknown)
585 @param subject(dict): message subject, key is the language (use '' when unknown)
586 @param mess_type(str): one of standard message type (cf RFC 6121 §5.2.2) or:
587 - auto: for automatic type detection
588 - info: for information ("info_type" can be specified in extra)
589 @param extra(dict, None): extra data. Key can be:
590 - info_type: information type, can be
591 TODO
592 @param uid(unicode, None): unique id:
593 should be unique at least in this XMPP session
594 if None, an uuid will be generated
595 @param no_trigger (bool): if True, messageSend trigger will no be used
596 useful when a message need to be sent without any modification
597 """
598 profile = client.profile
599 if subject is None:
600 subject = {}
552 if extra is None: 601 if extra is None:
553 extra = {} 602 extra = {}
554 mess_data = { # we put data in a dict, so trigger methods can change them 603 data = { # dict is similar to the one used in client.onMessage
604 "from": client.jid,
555 "to": to_jid, 605 "to": to_jid,
556 "from": client.jid, 606 "uid": uid or unicode(uuid.uuid4()),
557 "message": msg, 607 "message": message,
558 "subject": subject, 608 "subject": subject,
559 "type": mess_type, 609 "type": mess_type,
560 "extra": extra, 610 "extra": extra,
611 "timestamp": time.time(),
561 } 612 }
562 pre_xml_treatments = defer.Deferred() # XXX: plugin can add their pre XML treatments to this deferred 613 pre_xml_treatments = defer.Deferred() # XXX: plugin can add their pre XML treatments to this deferred
563 post_xml_treatments = defer.Deferred() # XXX: plugin can add their post XML treatments to this deferred 614 post_xml_treatments = defer.Deferred() # XXX: plugin can add their post XML treatments to this deferred
564 615
565 if mess_data["type"] == "auto": 616 if data["type"] == "auto":
566 # we try to guess the type 617 # we try to guess the type
567 if mess_data["subject"]: 618 if data["subject"]:
568 mess_data["type"] = 'normal' 619 data["type"] = 'normal'
569 elif not mess_data["to"].resource: # if to JID has a resource, the type is not 'groupchat' 620 elif not data["to"].resource: # if to JID has a resource, the type is not 'groupchat'
570 # we may have a groupchat message, we check if the we know this jid 621 # we may have a groupchat message, we check if the we know this jid
571 try: 622 try:
572 entity_type = self.memory.getEntityData(mess_data["to"], ['type'], profile)["type"] 623 entity_type = self.memory.getEntityData(data["to"], ['type'], profile)["type"]
573 #FIXME: should entity_type manage resources ? 624 #FIXME: should entity_type manage resources ?
574 except (exceptions.UnknownEntityError, KeyError): 625 except (exceptions.UnknownEntityError, KeyError):
575 entity_type = "contact" 626 entity_type = "contact"
576 627
577 if entity_type == "chatroom": 628 if entity_type == "chatroom":
578 mess_data["type"] = 'groupchat' 629 data["type"] = 'groupchat'
579 else: 630 else:
580 mess_data["type"] = 'chat' 631 data["type"] = 'chat'
581 else: 632 else:
582 mess_data["type"] == 'chat' 633 data["type"] == 'chat'
583 mess_data["type"] == "chat" if mess_data["subject"] else "normal" 634 data["type"] == "chat" if data["subject"] else "normal"
584 635
585 send_only = mess_data['extra'].get('send_only', None) 636 # FIXME: send_only is used by libervia's OTR plugin to avoid
637 # the triggers from frontend, and no_trigger do the same
638 # thing internally, this could be unified
639 send_only = data['extra'].get('send_only', None)
586 640
587 if not no_trigger and not send_only: 641 if not no_trigger and not send_only:
588 if not self.trigger.point("sendMessage", mess_data, pre_xml_treatments, post_xml_treatments, profile): 642 if not self.trigger.point("messageSend", client, data, pre_xml_treatments, post_xml_treatments):
589 return defer.succeed(None) 643 return defer.succeed(None)
590 644
591 log.debug(_(u"Sending message (type {type}, to {to})").format(type=mess_data["type"], to=to_jid.full())) 645 log.debug(_(u"Sending message (type {type}, to {to})").format(type=data["type"], to=to_jid.full()))
592 646
593 def cancelErrorTrap(failure): 647 pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(data))
594 """A message sending can be cancelled by a plugin treatment"""
595 failure.trap(exceptions.CancelError)
596
597 pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(mess_data))
598 pre_xml_treatments.chainDeferred(post_xml_treatments) 648 pre_xml_treatments.chainDeferred(post_xml_treatments)
599 post_xml_treatments.addCallback(self._sendMessageToStream, client) 649 post_xml_treatments.addCallback(self._sendMessageToStream, client)
600 if send_only: 650 if send_only:
601 log.debug(_("Triggers, storage and echo have been inhibited by the 'send_only' parameter")) 651 log.debug(_("Triggers, storage and echo have been inhibited by the 'send_only' parameter"))
602 else: 652 else:
603 post_xml_treatments.addCallback(self._storeMessage, client) 653 post_xml_treatments.addCallback(self._storeMessage, client)
604 post_xml_treatments.addCallback(self.sendMessageToBridge, client) 654 post_xml_treatments.addCallback(self.sendMessageToBridge, client)
605 post_xml_treatments.addErrback(cancelErrorTrap) 655 post_xml_treatments.addErrback(self._cancelErrorTrap)
606 pre_xml_treatments.callback(mess_data) 656 pre_xml_treatments.callback(data)
607 return pre_xml_treatments 657 return pre_xml_treatments
608 658
609 def _sendMessageToStream(self, mess_data, client): 659 def _cancelErrorTrap(failure):
660 """A message sending can be cancelled by a plugin treatment"""
661 failure.trap(exceptions.CancelError)
662
663 def _sendMessageToStream(self, data, client):
610 """Actualy send the message to the server 664 """Actualy send the message to the server
611 665
612 @param mess_data: message data dictionnary 666 @param data: message data dictionnary
613 @param client: profile's client 667 @param client: profile's client
614 """ 668 """
615 client.xmlstream.send(mess_data['xml']) 669 client.xmlstream.send(data['xml'])
616 return mess_data 670 return data
617 671
618 def _storeMessage(self, mess_data, client): 672 def _storeMessage(self, data, client):
619 """Store message into database (for local history) 673 """Store message into database (for local history)
620 674
621 @param mess_data: message data dictionnary 675 @param data: message data dictionnary
622 @param client: profile's client 676 @param client: profile's client
623 """ 677 """
624 if mess_data["type"] != "groupchat": 678 if data["type"] != C.MESS_TYPE_GROUPCHAT:
625 # we don't add groupchat message to history, as we get them back 679 # we don't add groupchat message to history, as we get them back
626 # and they will be added then 680 # and they will be added then
627 if mess_data['message']: # we need a message to save something 681 if data['message']: # we need a message to save something
628 self.memory.addToHistory(client.jid, mess_data['to'], 682 self.memory.addToHistory(client, data)
629 unicode(mess_data["message"]),
630 unicode(mess_data["type"]),
631 mess_data['extra'],
632 profile=client.profile)
633 else: 683 else:
634 log.warning(_("No message found")) # empty body should be managed by plugins before this point 684 log.warning(u"No message found") # empty body should be managed by plugins before this point
635 return mess_data 685 return data
636 686
637 def sendMessageToBridge(self, mess_data, client): 687 def sendMessageToBridge(self, data, client):
638 """Send message to bridge, so frontends can display it 688 """Send message to bridge, so frontends can display it
639 689
640 @param mess_data: message data dictionnary 690 @param data: message data dictionnary
641 @param client: profile's client 691 @param client: profile's client
642 """ 692 """
643 if mess_data["type"] != "groupchat": 693 if data["type"] != C.MESS_TYPE_GROUPCHAT:
644 # we don't send groupchat message back to bridge, as we get them back 694 # we don't send groupchat message to bridge, as we get them back
645 # and they will be added the 695 # and they will be added the
646 if mess_data['message']: # we need a message to save something 696 if data['message']: # we need a message to send something
647 # We send back the message, so all clients are aware of it 697 # We send back the message, so all frontends are aware of it
648 self.bridge.newMessage(mess_data['from'].full(), 698 self.bridge.messageNew(data['uid'], data['timestamp'], data['from'].full(), data['to'].full(), data['message'], data['subject'], data['type'], data['extra'], profile=client.profile)
649 unicode(mess_data["message"]),
650 mess_type=mess_data["type"],
651 to_jid=mess_data['to'].full(),
652 extra=mess_data['extra'],
653 profile=client.profile)
654 else: 699 else:
655 log.warning(_("No message found")) 700 log.warning(_("No message found"))
656 return mess_data 701 return data
657 702
658 def _setPresence(self, to="", show="", statuses=None, profile_key=C.PROF_KEY_NONE): 703 def _setPresence(self, to="", show="", statuses=None, profile_key=C.PROF_KEY_NONE):
659 return self.setPresence(jid.JID(to) if to else None, show, statuses, profile_key) 704 return self.setPresence(jid.JID(to) if to else None, show, statuses, profile_key)
660 705
661 def setPresence(self, to_jid=None, show="", statuses=None, profile_key=C.PROF_KEY_NONE): 706 def setPresence(self, to_jid=None, show="", statuses=None, profile_key=C.PROF_KEY_NONE):