comparison sat/plugins/plugin_xep_0045.py @ 2715:b35c84ea73cf

plugin XEP-0045: MAM implementation for MUC
author Goffi <goffi@goffi.org>
date Fri, 07 Dec 2018 19:13:28 +0100
parents d715d912afac
children bb6adaa580ee
comparison
equal deleted inserted replaced
2714:57eac4fd0ec0 2715:b35c84ea73cf
22 from sat.core.log import getLogger 22 from sat.core.log import getLogger
23 log = getLogger(__name__) 23 log = getLogger(__name__)
24 from twisted.internet import defer 24 from twisted.internet import defer
25 from twisted.words.protocols.jabber import jid 25 from twisted.words.protocols.jabber import jid
26 from twisted.python import failure 26 from twisted.python import failure
27 from dateutil.tz import tzutc
28 27
29 from sat.core import exceptions 28 from sat.core import exceptions
30 from sat.memory import memory 29 from sat.memory import memory
31 30
32 import calendar
33 import time 31 import time
34 import uuid 32 import uuid
35 import copy 33 import copy
36 34
37 from wokkel import muc, disco, iwokkel 35 from wokkel import muc, disco, iwokkel
38 from sat.tools import xml_tools 36 from sat.tools import xml_tools
39 37
40 from zope.interface import implements 38 from zope.interface import implements
39
40 # XXX: mam and rsm come from sat_tmp.wokkel
41 from wokkel import rsm
42 from wokkel import mam
41 43
42 44
43 PLUGIN_INFO = { 45 PLUGIN_INFO = {
44 C.PI_NAME: "XEP-0045 Plugin", 46 C.PI_NAME: "XEP-0045 Plugin",
45 C.PI_IMPORT_NAME: "XEP-0045", 47 C.PI_IMPORT_NAME: "XEP-0045",
46 C.PI_TYPE: "XEP", 48 C.PI_TYPE: "XEP",
47 C.PI_PROTOCOLS: ["XEP-0045"], 49 C.PI_PROTOCOLS: ["XEP-0045"],
48 C.PI_DEPENDENCIES: [], 50 C.PI_DEPENDENCIES: ["XEP-0359"],
49 C.PI_RECOMMENDATIONS: [C.TEXT_CMDS], 51 C.PI_RECOMMENDATIONS: [C.TEXT_CMDS, u"XEP-0313"],
50 C.PI_MAIN: "XEP_0045", 52 C.PI_MAIN: "XEP_0045",
51 C.PI_HANDLER: "yes", 53 C.PI_HANDLER: "yes",
52 C.PI_DESCRIPTION: _("""Implementation of Multi-User Chat""") 54 C.PI_DESCRIPTION: _("""Implementation of Multi-User Chat""")
53 } 55 }
54 56
60 ENTITY_TYPE_MUC = "MUC" 62 ENTITY_TYPE_MUC = "MUC"
61 ROOM_STATE_OCCUPANTS = "occupants" 63 ROOM_STATE_OCCUPANTS = "occupants"
62 ROOM_STATE_SELF_PRESENCE = "self-presence" 64 ROOM_STATE_SELF_PRESENCE = "self-presence"
63 ROOM_STATE_LIVE = "live" 65 ROOM_STATE_LIVE = "live"
64 ROOM_STATES = (ROOM_STATE_OCCUPANTS, ROOM_STATE_SELF_PRESENCE, ROOM_STATE_LIVE) 66 ROOM_STATES = (ROOM_STATE_OCCUPANTS, ROOM_STATE_SELF_PRESENCE, ROOM_STATE_LIVE)
67 HISTORY_LEGACY = u"legacy"
68 HISTORY_MAM = u"mam"
65 69
66 70
67 CONFIG_SECTION = u'plugin muc' 71 CONFIG_SECTION = u'plugin muc'
68 72
69 default_conf = {"default_muc": u'sat@chat.jabberfr.org'} 73 default_conf = {"default_muc": u'sat@chat.jabberfr.org'}
107 log.info(_(u"Text commands not available")) 111 log.info(_(u"Text commands not available"))
108 else: 112 else:
109 self.text_cmds.registerTextCommands(self) 113 self.text_cmds.registerTextCommands(self)
110 self.text_cmds.addWhoIsCb(self._whois, 100) 114 self.text_cmds.addWhoIsCb(self._whois, 100)
111 115
116 self._mam = self.host.plugins.get(u"XEP-0313")
117 self._si = self.host.plugins[u"XEP-0359"]
118
112 host.trigger.add("presence_available", self.presenceTrigger) 119 host.trigger.add("presence_available", self.presenceTrigger)
113 host.trigger.add("MessageReceived", self.messageReceivedTrigger, priority=1000000) 120 host.trigger.add("MessageReceived", self.messageReceivedTrigger, priority=1000000)
121 host.trigger.add("message_parse", self._message_parseTrigger)
114 122
115 def profileConnected(self, client): 123 def profileConnected(self, client):
116 def assign_service(service): 124 def assign_service(service):
117 client.muc_service = service 125 client.muc_service = service
118 return self.getMUCService(client).addCallback(assign_service) 126 return self.getMUCService(client).addCallback(assign_service)
127
128 def _message_parseTrigger(self, client, message_elt, data):
129 """Add stanza-id from the room if present"""
130 if message_elt.getAttribute(u"type") != C.MESS_TYPE_GROUPCHAT:
131 return True
132
133 # stanza_id will not be filled by parseMessage because the emitter
134 # is the room and not our server, so we have to parse it here
135 room_jid = data[u"from"].userhostJID()
136 stanza_id = self._si.getStanzaId(message_elt, room_jid)
137 if stanza_id:
138 data[u"extra"][u"stanza_id"] = stanza_id
119 139
120 def messageReceivedTrigger(self, client, message_elt, post_treat): 140 def messageReceivedTrigger(self, client, message_elt, post_treat):
121 if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT: 141 if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
122 if message_elt.subject or message_elt.delay: 142 if message_elt.subject or message_elt.delay:
123 return False 143 return False
124 from_jid = jid.JID(message_elt['from']) 144 from_jid = jid.JID(message_elt['from'])
125 room_jid = from_jid.userhostJID() 145 room_jid = from_jid.userhostJID()
126 if room_jid in client._muc_client.joined_rooms: 146 if room_jid in client._muc_client.joined_rooms:
127 room = client._muc_client.joined_rooms[room_jid] 147 room = client._muc_client.joined_rooms[room_jid]
128 if room.state != ROOM_STATE_LIVE: 148 if room.state != ROOM_STATE_LIVE:
129 log.warning(_(u"Received non delayed message in a room before its initialisation: state={state}, msg={msg}").format( 149 if getattr(room, "_history_type", HISTORY_LEGACY) == HISTORY_LEGACY:
150 # With MAM history, order is different, and we can get live
151 # messages before history is complete, so this is not a warning
152 # but an expected case.
153 # On the other hand, with legacy history, it's not normal.
154 log.warning(_(
155 u"Received non delayed message in a room before its "
156 u"initialisation: state={state}, msg={msg}").format(
130 state=room.state, 157 state=room.state,
131 msg=message_elt.toXml())) 158 msg=message_elt.toXml()))
132 room._cache.append(message_elt) 159 room._cache.append(message_elt)
133 return False 160 return False
134 else: 161 else:
135 log.warning(u"Received groupchat message for a room which has not been joined, ignoring it: {}".format(message_elt.toXml())) 162 log.warning(u"Received groupchat message for a room which has not been "
163 u"joined, ignoring it: {}".format(message_elt.toXml()))
136 return False 164 return False
137 return True 165 return True
138 166
139 def checkRoomJoined(self, client, room_jid): 167 def checkRoomJoined(self, client, room_jid):
140 """Check that given room has been joined in current session 168 """Check that given room has been joined in current session
195 # FIXME: the current behaviour is to create an instant room 223 # FIXME: the current behaviour is to create an instant room
196 # and send the signal only when the room is unlocked 224 # and send the signal only when the room is unlocked
197 # a proper configuration management should be done 225 # a proper configuration management should be done
198 log.debug(_(u"room locked !")) 226 log.debug(_(u"room locked !"))
199 d = client._muc_client.configure(room.roomJID, {}) 227 d = client._muc_client.configure(room.roomJID, {})
200 d.addErrback(lambda dummy: log.error(_(u'Error while configuring the room'))) 228 d.addErrback(lambda __: log.error(_(u'Error while configuring the room')))
201 return room.fully_joined 229 return room.fully_joined
202 230
203 def _joinEb(self, failure, client, room_jid, nick, password): 231 def _joinEb(self, failure, client, room_jid, nick, password):
204 """Called when something is going wrong when joining the room""" 232 """Called when something is going wrong when joining the room"""
205 try: 233 try:
578 self.text_cmds.feedBack(client, feedback, mess_data) 606 self.text_cmds.feedBack(client, feedback, mess_data)
579 return False 607 return False
580 608
581 d = self.kick(client, nick, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}) 609 d = self.kick(client, nick, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]})
582 610
583 def cb(dummy): 611 def cb(__):
584 feedback_msg = _(u'You have kicked {}').format(nick) 612 feedback_msg = _(u'You have kicked {}').format(nick)
585 if len(options) > 1: 613 if len(options) > 1:
586 feedback_msg += _(u' for the following reason: {}').format(options[1]) 614 feedback_msg += _(u' for the following reason: {}').format(options[1])
587 self.text_cmds.feedBack(client, feedback_msg, mess_data) 615 self.text_cmds.feedBack(client, feedback_msg, mess_data)
588 return True 616 return True
607 self.text_cmds.feedBack(client, feedback, mess_data) 635 self.text_cmds.feedBack(client, feedback, mess_data)
608 return False 636 return False
609 637
610 d = self.ban(client, entity_jid, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}) 638 d = self.ban(client, entity_jid, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]})
611 639
612 def cb(dummy): 640 def cb(__):
613 feedback_msg = _(u'You have banned {}').format(entity_jid) 641 feedback_msg = _(u'You have banned {}').format(entity_jid)
614 if len(options) > 1: 642 if len(options) > 1:
615 feedback_msg += _(u' for the following reason: {}').format(options[1]) 643 feedback_msg += _(u' for the following reason: {}').format(options[1])
616 self.text_cmds.feedBack(client, feedback_msg, mess_data) 644 self.text_cmds.feedBack(client, feedback_msg, mess_data)
617 return True 645 return True
646 self.text_cmds.feedBack(client, feedback, mess_data) 674 self.text_cmds.feedBack(client, feedback, mess_data)
647 return False 675 return False
648 676
649 d = self.affiliate(client, entity_jid, mess_data["to"], {'affiliation': affiliation}) 677 d = self.affiliate(client, entity_jid, mess_data["to"], {'affiliation': affiliation})
650 678
651 def cb(dummy): 679 def cb(__):
652 feedback_msg = _(u'New affiliation for %(entity)s: %(affiliation)s').format(entity=entity_jid, affiliation=affiliation) 680 feedback_msg = _(u'New affiliation for %(entity)s: %(affiliation)s').format(entity=entity_jid, affiliation=affiliation)
653 self.text_cmds.feedBack(client, feedback_msg, mess_data) 681 self.text_cmds.feedBack(client, feedback_msg, mess_data)
654 return True 682 return True
655 d.addCallback(cb) 683 d.addCallback(cb)
656 return d 684 return d
742 class SatMUCClient(muc.MUCClient): 770 class SatMUCClient(muc.MUCClient):
743 implements(iwokkel.IDisco) 771 implements(iwokkel.IDisco)
744 772
745 def __init__(self, plugin_parent): 773 def __init__(self, plugin_parent):
746 self.plugin_parent = plugin_parent 774 self.plugin_parent = plugin_parent
747 self.host = plugin_parent.host
748 muc.MUCClient.__init__(self) 775 muc.MUCClient.__init__(self)
749 self._changing_nicks = set() # used to keep trace of who is changing nick, 776 self._changing_nicks = set() # used to keep trace of who is changing nick,
750 # and to discard userJoinedRoom signal in this case 777 # and to discard userJoinedRoom signal in this case
751 print "init SatMUCClient OK" 778 print "init SatMUCClient OK"
752 779
753 @property 780 @property
754 def joined_rooms(self): 781 def joined_rooms(self):
755 return self._rooms 782 return self._rooms
783
784 @property
785 def host(self):
786 return self.plugin_parent.host
787
788 @property
789 def client(self):
790 return self.parent
791
792 @property
793 def _mam(self):
794 return self.plugin_parent._mam
795
796 @property
797 def _si(self):
798 return self.plugin_parent._si
756 799
757 def changeRoomState(self, room, new_state): 800 def changeRoomState(self, room, new_state):
758 """Check that room is in expected state, and change it 801 """Check that room is in expected state, and change it
759 802
760 @param new_state: one of ROOM_STATE_* 803 @param new_state: one of ROOM_STATE_*
766 raise exceptions.InternalError("unexpected new room state ({room}): {state}".format( 809 raise exceptions.InternalError("unexpected new room state ({room}): {state}".format(
767 room=room.userhost(), 810 room=room.userhost(),
768 state=new_state)) 811 state=new_state))
769 expected_state = ROOM_STATES[new_state_idx-1] 812 expected_state = ROOM_STATES[new_state_idx-1]
770 if room.state != expected_state: 813 if room.state != expected_state:
771 log.warning(_(u"room {room} is not in expected state: room is in state {current_state} while we were expecting {expected_state}").format( 814 log.error(_(
815 u"room {room} is not in expected state: room is in state {current_state} "
816 u"while we were expecting {expected_state}").format(
772 room=room.userhost(), 817 room=room.userhost(),
773 current_state=room.state, 818 current_state=room.state,
774 expected_state=expected_state)) 819 expected_state=expected_state))
775 room.state = new_state 820 room.state = new_state
776 821
777 def _addRoom(self, room): 822 def _addRoom(self, room):
778 super(SatMUCClient, self)._addRoom(room) 823 super(SatMUCClient, self)._addRoom(room)
779 room._roster_ok = False # True when occupants list has been fully received 824 room._roster_ok = False # True when occupants list has been fully received
780 room.state = ROOM_STATE_OCCUPANTS 825 room.state = ROOM_STATE_OCCUPANTS
781 room._history_d = defer.Deferred() # used to send bridge signal once backlog are written in history
782 room._history_d.callback(None)
783 # FIXME: check if history_d is not redundant with fully_joined 826 # FIXME: check if history_d is not redundant with fully_joined
784 room.fully_joined = defer.Deferred() # called when everything is OK 827 room.fully_joined = defer.Deferred() # called when everything is OK
785 # cache data until room is ready 828 # cache data until room is ready
786 # list of elements which will be re-injected in stream 829 # list of elements which will be re-injected in stream
787 room._cache = [] 830 room._cache = []
788 831
789 @defer.inlineCallbacks 832 @defer.inlineCallbacks
790 def join(self, room_jid, nick, password=None): 833 def _joinLegacy(self, client, room_jid, nick, password):
791 mess_data_list = yield self.host.memory.historyGet(self.parent.jid.userhostJID(), 834 """Join room an retrieve history with legacy method"""
792 room_jid, 835 mess_data_list = yield self.host.memory.historyGet(room_jid,
793 1, 836 client.jid.userhostJID(),
794 True, 837 limit=1,
795 profile=self.parent.profile) 838 between=True,
839 profile=client.profile)
796 if mess_data_list: 840 if mess_data_list:
797 timestamp = mess_data_list[0][1] 841 timestamp = mess_data_list[0][1]
798 # we use seconds since last message to get backlog without duplicates 842 # we use seconds since last message to get backlog without duplicates
799 # and we remove 1 second to avoid getting the last message again 843 # and we remove 1 second to avoid getting the last message again
800 seconds = int(time.time() - timestamp) - 1 844 seconds = int(time.time() - timestamp) - 1
801 else: 845 else:
802 seconds = None 846 seconds = None
803 room = yield super(SatMUCClient, self).join(room_jid, nick, muc.HistoryOptions(seconds=seconds), password) 847
848 room = yield super(SatMUCClient, self).join(
849 room_jid, nick, muc.HistoryOptions(seconds=seconds), password)
850 # used to send bridge signal once backlog are written in history
851 room._history_type = HISTORY_LEGACY
852 room._history_d = defer.Deferred()
853 room._history_d.callback(None)
804 defer.returnValue(room) 854 defer.returnValue(room)
855
856 @defer.inlineCallbacks
857 def _joinMAM(self, client, room_jid, nick, password):
858 """Join room and retrieve history using MAM"""
859 room = yield super(SatMUCClient, self).join(
860 # we don't want any history from room as we'll get it with MAM
861 room_jid, nick, muc.HistoryOptions(maxStanzas=0), password=password)
862 room._history_type = HISTORY_MAM
863 room._history_d = defer.Deferred()
864
865 last_mess = yield self.host.memory.historyGet(room_jid,
866 None,
867 limit=1,
868 between=False,
869 filters={u'last_stanza_id': True},
870 profile=client.profile)
871 if last_mess:
872 stanza_id = last_mess[0][-1][u'stanza_id']
873 rsm_req = rsm.RSMRequest(after=stanza_id)
874 else:
875 log.info(u"We have no MAM archive for room {room_jid}.".format(
876 room_jid=room_jid))
877 rsm_req = rsm.RSMRequest(max_=20)
878
879 mam_req = mam.MAMRequest(rsm_=rsm_req)
880 mam_data = yield self._mam.getArchives(client, mam_req,
881 service=room_jid)
882 elt_list, rsm_response = mam_data
883
884 if not elt_list:
885 log.info(_(u"No message received while offline in {room_jid}".format(
886 room_jid=room_jid)))
887 else:
888 log.info(
889 _(u"We have received {num_mess} message(s) in {room_jid} while offline.")
890 .format(num_mess=len(elt_list), room_jid=room_jid))
891
892 for mess_elt in elt_list:
893 try:
894 fwd_message_elt = self._mam.getMessageFromResult(
895 client, mess_elt, mam_req, service=room_jid)
896 except exceptions.DataError:
897 continue
898 if fwd_message_elt.getAttribute(u"to"):
899 log.warning(
900 u'Forwarded message element has a "to" attribute while it is '
901 u'forbidden by specifications')
902 fwd_message_elt[u"to"] = client.jid.full()
903 mess_data = client.messageProt.parseMessage(fwd_message_elt)
904 # we attache parsed message data to element, to avoid parsing
905 # again in _addToHistory
906 fwd_message_elt._mess_data = mess_data
907 # and we inject to MUC workflow
908 client._muc_client._onGroupChat(fwd_message_elt)
909
910
911 # for legacy history, the following steps are done in receivedSubject but for MAM
912 # the order is different (we have to join then get MAM archive, so subject
913 # is received before archive), so we change state and add the callbacks here.
914 self.changeRoomState(room, ROOM_STATE_LIVE)
915 room._history_d.addCallbacks(self._historyCb, self._historyEb, [room],
916 errbackArgs=[room])
917
918 # callback is done now that all needed Deferred have been added to _history_d
919 room._history_d.callback(None)
920
921 defer.returnValue(room)
922
923 def join(self, room_jid, nick, password=None):
924 if (not self._mam or not self.host.hasFeature(self.client,
925 mam.NS_MAM, room_jid)):
926 return self._joinLegacy(self.client, room_jid, nick, password)
927 else:
928 return self._joinMAM(self.client, room_jid, nick, password)
805 929
806 ## presence/roster ## 930 ## presence/roster ##
807 931
808 def availableReceived(self, presence): 932 def availableReceived(self, presence):
809 """ 933 """
863 if user.nick == room.nick: 987 if user.nick == room.nick:
864 # we have received our own nick, this mean that the full room roster was received 988 # we have received our own nick, this mean that the full room roster was received
865 self.changeRoomState(room, ROOM_STATE_SELF_PRESENCE) 989 self.changeRoomState(room, ROOM_STATE_SELF_PRESENCE)
866 log.debug(u"room {room} joined with nick {nick}".format(room=room.occupantJID.userhost(), nick=user.nick)) 990 log.debug(u"room {room} joined with nick {nick}".format(room=room.occupantJID.userhost(), nick=user.nick))
867 # We set type so we don't have to use a deferred with disco to check entity type 991 # We set type so we don't have to use a deferred with disco to check entity type
868 self.host.memory.updateEntityData(room.roomJID, C.ENTITY_TYPE, ENTITY_TYPE_MUC, profile_key=self.parent.profile) 992 self.host.memory.updateEntityData(room.roomJID, C.ENTITY_TYPE, ENTITY_TYPE_MUC, profile_key=self.client.profile)
869 elif room.state not in (ROOM_STATE_OCCUPANTS, ROOM_STATE_LIVE): 993 elif room.state not in (ROOM_STATE_OCCUPANTS, ROOM_STATE_LIVE):
870 log.warning(u"Received user presence data in a room before its initialisation (current state: {state})," 994 log.warning(u"Received user presence data in a room before its initialisation (current state: {state}),"
871 "this is not standard! Ignoring it: {room} ({nick})".format( 995 "this is not standard! Ignoring it: {room} ({nick})".format(
872 state=room.state, 996 state=room.state,
873 room=room.roomJID.userhost(), 997 room=room.roomJID.userhost(),
879 try: 1003 try:
880 self._changing_nicks.remove(user.nick) 1004 self._changing_nicks.remove(user.nick)
881 except KeyError: 1005 except KeyError:
882 # this is a new user 1006 # this is a new user
883 log.debug(_(u"user {nick} has joined room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost())) 1007 log.debug(_(u"user {nick} has joined room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost()))
884 if not self.host.trigger.point("MUC user joined", room, user, self.parent.profile): 1008 if not self.host.trigger.point("MUC user joined", room, user, self.client.profile):
885 return 1009 return
886 1010
887 extra = {'info_type': ROOM_USER_JOINED, 1011 extra = {'info_type': ROOM_USER_JOINED,
888 'user_affiliation': user.affiliation, 1012 'user_affiliation': user.affiliation,
889 'user_role': user.role, 1013 'user_role': user.role,
891 } 1015 }
892 if user.entity is not None: 1016 if user.entity is not None:
893 extra['user_entity'] = user.entity.full() 1017 extra['user_entity'] = user.entity.full()
894 mess_data = { # dict is similar to the one used in client.onMessage 1018 mess_data = { # dict is similar to the one used in client.onMessage
895 "from": room.roomJID, 1019 "from": room.roomJID,
896 "to": self.parent.jid, 1020 "to": self.client.jid,
897 "uid": unicode(uuid.uuid4()), 1021 "uid": unicode(uuid.uuid4()),
898 "message": {'': D_(u"=> {} has joined the room").format(user.nick)}, 1022 "message": {'': D_(u"=> {} has joined the room").format(user.nick)},
899 "subject": {}, 1023 "subject": {},
900 "type": C.MESS_TYPE_INFO, 1024 "type": C.MESS_TYPE_INFO,
901 "extra": extra, 1025 "extra": extra,
902 "timestamp": time.time(), 1026 "timestamp": time.time(),
903 } 1027 }
904 # FIXME: we disable presence in history as it's taking a lot of space 1028 # FIXME: we disable presence in history as it's taking a lot of space
905 # while not indispensable. In the future an option may allow 1029 # while not indispensable. In the future an option may allow
906 # to re-enable it 1030 # to re-enable it
907 # self.parent.messageAddToHistory(mess_data) 1031 # self.client.messageAddToHistory(mess_data)
908 self.parent.messageSendToBridge(mess_data) 1032 self.client.messageSendToBridge(mess_data)
909 1033
910 1034
911 def userLeftRoom(self, room, user): 1035 def userLeftRoom(self, room, user):
912 if not self.host.trigger.point("MUC user left", room, user, self.parent.profile): 1036 if not self.host.trigger.point("MUC user left", room, user, self.client.profile):
913 return 1037 return
914 if user.nick == room.nick: 1038 if user.nick == room.nick:
915 # we left the room 1039 # we left the room
916 room_jid_s = room.roomJID.userhost() 1040 room_jid_s = room.roomJID.userhost()
917 log.info(_(u"Room ({room}) left ({profile})").format( 1041 log.info(_(u"Room ({room}) left ({profile})").format(
918 room = room_jid_s, profile = self.parent.profile)) 1042 room = room_jid_s, profile = self.client.profile))
919 self.host.memory.delEntityCache(room.roomJID, profile_key=self.parent.profile) 1043 self.host.memory.delEntityCache(room.roomJID, profile_key=self.client.profile)
920 self.host.bridge.mucRoomLeft(room.roomJID.userhost(), self.parent.profile) 1044 self.host.bridge.mucRoomLeft(room.roomJID.userhost(), self.client.profile)
921 elif room.state != ROOM_STATE_LIVE: 1045 elif room.state != ROOM_STATE_LIVE:
922 log.warning(u"Received user presence data in a room before its initialisation (current state: {state})," 1046 log.warning(u"Received user presence data in a room before its initialisation (current state: {state}),"
923 "this is not standard! Ignoring it: {room} ({nick})".format( 1047 "this is not standard! Ignoring it: {room} ({nick})".format(
924 state=room.state, 1048 state=room.state,
925 room=room.roomJID.userhost(), 1049 room=room.roomJID.userhost(),
936 } 1060 }
937 if user.entity is not None: 1061 if user.entity is not None:
938 extra['user_entity'] = user.entity.full() 1062 extra['user_entity'] = user.entity.full()
939 mess_data = { # dict is similar to the one used in client.onMessage 1063 mess_data = { # dict is similar to the one used in client.onMessage
940 "from": room.roomJID, 1064 "from": room.roomJID,
941 "to": self.parent.jid, 1065 "to": self.client.jid,
942 "uid": unicode(uuid.uuid4()), 1066 "uid": unicode(uuid.uuid4()),
943 "message": {'': D_(u"<= {} has left the room").format(user.nick)}, 1067 "message": {'': D_(u"<= {} has left the room").format(user.nick)},
944 "subject": {}, 1068 "subject": {},
945 "type": C.MESS_TYPE_INFO, 1069 "type": C.MESS_TYPE_INFO,
946 "extra": extra, 1070 "extra": extra,
947 "timestamp": time.time(), 1071 "timestamp": time.time(),
948 } 1072 }
949 # FIXME: disable history, see userJoinRoom comment 1073 # FIXME: disable history, see userJoinRoom comment
950 # self.parent.messageAddToHistory(mess_data) 1074 # self.client.messageAddToHistory(mess_data)
951 self.parent.messageSendToBridge(mess_data) 1075 self.client.messageSendToBridge(mess_data)
952 1076
953 def userChangedNick(self, room, user, new_nick): 1077 def userChangedNick(self, room, user, new_nick):
954 self.host.bridge.mucRoomUserChangedNick(room.roomJID.userhost(), user.nick, new_nick, self.parent.profile) 1078 self.host.bridge.mucRoomUserChangedNick(room.roomJID.userhost(), user.nick, new_nick, self.client.profile)
955 1079
956 def userUpdatedStatus(self, room, user, show, status): 1080 def userUpdatedStatus(self, room, user, show, status):
957 self.host.bridge.presenceUpdate(room.roomJID.userhost() + '/' + user.nick, show or '', 0, {C.PRESENCE_STATUSES_DEFAULT: status or ''}, self.parent.profile) 1081 self.host.bridge.presenceUpdate(room.roomJID.userhost() + '/' + user.nick, show or '', 0, {C.PRESENCE_STATUSES_DEFAULT: status or ''}, self.client.profile)
958 1082
959 ## messages ## 1083 ## messages ##
960 1084
961 def receivedGroupChat(self, room, user, body): 1085 def receivedGroupChat(self, room, user, body):
962 log.debug(u'receivedGroupChat: room=%s user=%s body=%s' % (room.roomJID.full(), user, body)) 1086 log.debug(u'receivedGroupChat: room=%s user=%s body=%s' % (room.roomJID.full(), user, body))
963 1087
964 def _addToHistory(self, dummy, user, message): 1088 def _addToHistory(self, __, user, message):
965 # we check if message is not in history 1089 try:
966 # and raise ConflictError else 1090 # message can be already parsed (with MAM), in this case mess_data
967 stamp = message.delay.stamp.astimezone(tzutc()).timetuple() 1091 # it attached to the element
968 timestamp = float(calendar.timegm(stamp)) 1092 mess_data = message.element._mess_data
969 data = { # dict is similar to the one used in client.onMessage 1093 except AttributeError:
970 "from": message.sender, 1094 mess_data = self.client.messageProt.parseMessage(message.element)
971 "to": message.recipient, 1095 if mess_data[u'message'] or mess_data[u'subject']:
972 "uid": unicode(uuid.uuid4()), 1096 return self.host.memory.addToHistory(self.client, mess_data)
973 "type": C.MESS_TYPE_GROUPCHAT,
974 "extra": {},
975 "timestamp": timestamp,
976 "received_timestamp": unicode(time.time()),
977 }
978 # FIXME: message and subject don't handle xml:lang
979 data['message'] = {'': message.body} if message.body is not None else {}
980 data['subject'] = {'': message.subject} if message.subject is not None else {}
981
982 if data['message'] or data['subject']:
983 return self.host.memory.addToHistory(self.parent, data)
984 else: 1097 else:
985 return defer.succeed(None) 1098 return defer.succeed(None)
986 1099
987 def _addToHistoryEb(self, failure): 1100 def _addToHistoryEb(self, failure):
988 failure.trap(exceptions.CancelError) 1101 failure.trap(exceptions.CancelError)
996 @param user(muc.User, None): the user that sent the message 1109 @param user(muc.User, None): the user that sent the message
997 None if the message come from the room 1110 None if the message come from the room
998 @param message(muc.GroupChat): the parsed message 1111 @param message(muc.GroupChat): the parsed message
999 """ 1112 """
1000 if room.state != ROOM_STATE_SELF_PRESENCE: 1113 if room.state != ROOM_STATE_SELF_PRESENCE:
1001 log.warning(_(u"received history in unexpected state in room {room} (state: {state})").format( 1114 log.warning(_(
1002 room = room.userhost(), 1115 u"received history in unexpected state in room {room} (state: "
1003 state = room.state)) 1116 u"{state})").format(room = room.roomJID.userhost(),
1117 state = room.state))
1004 room._history_d.addCallback(self._addToHistory, user, message) 1118 room._history_d.addCallback(self._addToHistory, user, message)
1005 room._history_d.addErrback(self._addToHistoryEb) 1119 room._history_d.addErrback(self._addToHistoryEb)
1006 1120
1007 ## subject ## 1121 ## subject ##
1008 1122
1028 self.receivedHistory(room, user, message) 1142 self.receivedHistory(room, user, message)
1029 1143
1030 def subject(self, room, subject): 1144 def subject(self, room, subject):
1031 return muc.MUCClientProtocol.subject(self, room, subject) 1145 return muc.MUCClientProtocol.subject(self, room, subject)
1032 1146
1033 def _historyCb(self, dummy, room): 1147 def _historyCb(self, __, room):
1034 """Called when history have been written to database and subject is received 1148 """Called when history have been written to database and subject is received
1035 1149
1036 this method will finish joining by: 1150 this method will finish joining by:
1037 - sending message to bridge 1151 - sending message to bridge
1038 - calling fully_joined deferred 1152 - calling fully_joined deferred
1039 - sending stanza put in cache 1153 - sending stanza put in cache
1040 - cleaning variables not needed anymore 1154 - cleaning variables not needed anymore
1041 """ 1155 """
1042 args = self.plugin_parent._getRoomJoinedArgs(room, self.parent.profile) 1156 args = self.plugin_parent._getRoomJoinedArgs(room, self.client.profile)
1043 self.host.bridge.mucRoomJoined(*args) 1157 self.host.bridge.mucRoomJoined(*args)
1044 room.fully_joined.callback(room) 1158 room.fully_joined.callback(room)
1045 del room._history_d 1159 del room._history_d
1160 del room._history_type
1046 cache = room._cache 1161 cache = room._cache
1047 del room._cache 1162 del room._cache
1048 for elem in cache: 1163 for elem in cache:
1049 self.parent.xmlstream.dispatch(elem) 1164 self.client.xmlstream.dispatch(elem)
1050 1165
1051 def _historyEb(self, failure_, room): 1166 def _historyEb(self, failure_, room):
1052 log.error(u"Error while managing history: {}".format(failure_)) 1167 log.error(u"Error while managing history: {}".format(failure_))
1053 self._historyCb(None, room) 1168 self._historyCb(None, room)
1054 1169
1055 def receivedSubject(self, room, user, subject): 1170 def receivedSubject(self, room, user, subject):
1056 # when subject is received, we know that we have whole roster and history 1171 # when subject is received, we know that we have whole roster and history
1057 # cf. http://xmpp.org/extensions/xep-0045.html#enter-subject 1172 # cf. http://xmpp.org/extensions/xep-0045.html#enter-subject
1058 room.subject = subject # FIXME: subject doesn't handle xml:lang 1173 room.subject = subject # FIXME: subject doesn't handle xml:lang
1059 if room.state != ROOM_STATE_LIVE: 1174 if room.state != ROOM_STATE_LIVE:
1060 self.changeRoomState(room, ROOM_STATE_LIVE) 1175 if room._history_type == HISTORY_LEGACY:
1061 room._history_d.addCallbacks(self._historyCb, self._historyEb, [room], errbackArgs=[room]) 1176 self.changeRoomState(room, ROOM_STATE_LIVE)
1177 room._history_d.addCallbacks(self._historyCb, self._historyEb, [room], errbackArgs=[room])
1062 else: 1178 else:
1063 # the subject has been changed 1179 # the subject has been changed
1064 log.debug(_(u"New subject for room ({room_id}): {subject}").format(room_id = room.roomJID.full(), subject = subject)) 1180 log.debug(_(u"New subject for room ({room_id}): {subject}").format(room_id = room.roomJID.full(), subject = subject))
1065 self.host.bridge.mucRoomNewSubject(room.roomJID.userhost(), subject, self.parent.profile) 1181 self.host.bridge.mucRoomNewSubject(room.roomJID.userhost(), subject, self.client.profile)
1066 1182
1067 ## disco ## 1183 ## disco ##
1068 1184
1069 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): 1185 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
1070 return [disco.DiscoFeature(NS_MUC)] 1186 return [disco.DiscoFeature(NS_MUC)]