Mercurial > libervia-backend
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): |