comparison sat/plugins/plugin_xep_0045.py @ 3028:ab2696e34d29

Python 3 port: /!\ this is a huge commit /!\ starting from this commit, SàT is needs Python 3.6+ /!\ SàT maybe be instable or some feature may not work anymore, this will improve with time This patch port backend, bridge and frontends to Python 3. Roughly this has been done this way: - 2to3 tools has been applied (with python 3.7) - all references to python2 have been replaced with python3 (notably shebangs) - fixed files not handled by 2to3 (notably the shell script) - several manual fixes - fixed issues reported by Python 3 that where not handled in Python 2 - replaced "async" with "async_" when needed (it's a reserved word from Python 3.7) - replaced zope's "implements" with @implementer decorator - temporary hack to handle data pickled in database, as str or bytes may be returned, to be checked later - fixed hash comparison for password - removed some code which is not needed anymore with Python 3 - deactivated some code which needs to be checked (notably certificate validation) - tested with jp, fixed reported issues until some basic commands worked - ported Primitivus (after porting dependencies like urwid satext) - more manual fixes
author Goffi <goffi@goffi.org>
date Tue, 13 Aug 2019 19:08:41 +0200
parents 7b8d40b17451
children 730bbed77a89
comparison
equal deleted inserted replaced
3027:ff5bcb12ae60 3028:ab2696e34d29
1 #!/usr/bin/env python2 1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*- 2 # -*- coding: utf-8 -*-
3 3
4 # SAT plugin for managing xep-0045 4 # SAT plugin for managing xep-0045
5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org) 5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org)
6 6
32 import uuid 32 import uuid
33 33
34 from wokkel import muc, disco, iwokkel 34 from wokkel import muc, disco, iwokkel
35 from sat.tools import xml_tools 35 from sat.tools import xml_tools
36 36
37 from zope.interface import implements 37 from zope.interface import implementer
38 38
39 # XXX: mam and rsm come from sat_tmp.wokkel 39 # XXX: mam and rsm come from sat_tmp.wokkel
40 from wokkel import rsm 40 from wokkel import rsm
41 from wokkel import mam 41 from wokkel import mam
42 42
45 C.PI_NAME: "XEP-0045 Plugin", 45 C.PI_NAME: "XEP-0045 Plugin",
46 C.PI_IMPORT_NAME: "XEP-0045", 46 C.PI_IMPORT_NAME: "XEP-0045",
47 C.PI_TYPE: "XEP", 47 C.PI_TYPE: "XEP",
48 C.PI_PROTOCOLS: ["XEP-0045"], 48 C.PI_PROTOCOLS: ["XEP-0045"],
49 C.PI_DEPENDENCIES: ["XEP-0359"], 49 C.PI_DEPENDENCIES: ["XEP-0359"],
50 C.PI_RECOMMENDATIONS: [C.TEXT_CMDS, u"XEP-0313"], 50 C.PI_RECOMMENDATIONS: [C.TEXT_CMDS, "XEP-0313"],
51 C.PI_MAIN: "XEP_0045", 51 C.PI_MAIN: "XEP_0045",
52 C.PI_HANDLER: "yes", 52 C.PI_HANDLER: "yes",
53 C.PI_DESCRIPTION: _("""Implementation of Multi-User Chat""") 53 C.PI_DESCRIPTION: _("""Implementation of Multi-User Chat""")
54 } 54 }
55 55
60 OCCUPANT_KEYS = ('nick', 'entity', 'affiliation', 'role') 60 OCCUPANT_KEYS = ('nick', 'entity', 'affiliation', 'role')
61 ROOM_STATE_OCCUPANTS = "occupants" 61 ROOM_STATE_OCCUPANTS = "occupants"
62 ROOM_STATE_SELF_PRESENCE = "self-presence" 62 ROOM_STATE_SELF_PRESENCE = "self-presence"
63 ROOM_STATE_LIVE = "live" 63 ROOM_STATE_LIVE = "live"
64 ROOM_STATES = (ROOM_STATE_OCCUPANTS, ROOM_STATE_SELF_PRESENCE, ROOM_STATE_LIVE) 64 ROOM_STATES = (ROOM_STATE_OCCUPANTS, ROOM_STATE_SELF_PRESENCE, ROOM_STATE_LIVE)
65 HISTORY_LEGACY = u"legacy" 65 HISTORY_LEGACY = "legacy"
66 HISTORY_MAM = u"mam" 66 HISTORY_MAM = "mam"
67 67
68 68
69 CONFIG_SECTION = u'plugin muc' 69 CONFIG_SECTION = 'plugin muc'
70 70
71 default_conf = {"default_muc": u'sat@chat.jabberfr.org'} 71 default_conf = {"default_muc": 'sat@chat.jabberfr.org'}
72 72
73 73
74 class AlreadyJoined(exceptions.ConflictError): 74 class AlreadyJoined(exceptions.ConflictError):
75 75
76 def __init__(self, room): 76 def __init__(self, room):
84 84
85 def __init__(self, host): 85 def __init__(self, host):
86 log.info(_("Plugin XEP_0045 initialization")) 86 log.info(_("Plugin XEP_0045 initialization"))
87 self.host = host 87 self.host = host
88 self._sessions = memory.Sessions() 88 self._sessions = memory.Sessions()
89 host.bridge.addMethod("mucJoin", ".plugin", in_sign='ssa{ss}s', out_sign='(bsa{sa{ss}}sss)', method=self._join, async=True) # return same arguments as mucRoomJoined + a boolean set to True is the room was already joined (first argument) 89 host.bridge.addMethod("mucJoin", ".plugin", in_sign='ssa{ss}s', out_sign='(bsa{sa{ss}}sss)', method=self._join, async_=True) # return same arguments as mucRoomJoined + a boolean set to True is the room was already joined (first argument)
90 host.bridge.addMethod("mucNick", ".plugin", in_sign='sss', out_sign='', method=self._nick) 90 host.bridge.addMethod("mucNick", ".plugin", in_sign='sss', out_sign='', method=self._nick)
91 host.bridge.addMethod("mucNickGet", ".plugin", in_sign='ss', out_sign='s', method=self._getRoomNick) 91 host.bridge.addMethod("mucNickGet", ".plugin", in_sign='ss', out_sign='s', method=self._getRoomNick)
92 host.bridge.addMethod("mucLeave", ".plugin", in_sign='ss', out_sign='', method=self._leave, async=True) 92 host.bridge.addMethod("mucLeave", ".plugin", in_sign='ss', out_sign='', method=self._leave, async_=True)
93 host.bridge.addMethod("mucOccupantsGet", ".plugin", in_sign='ss', out_sign='a{sa{ss}}', method=self._getRoomOccupants) 93 host.bridge.addMethod("mucOccupantsGet", ".plugin", in_sign='ss', out_sign='a{sa{ss}}', method=self._getRoomOccupants)
94 host.bridge.addMethod("mucSubject", ".plugin", in_sign='sss', out_sign='', method=self._subject) 94 host.bridge.addMethod("mucSubject", ".plugin", in_sign='sss', out_sign='', method=self._subject)
95 host.bridge.addMethod("mucGetRoomsJoined", ".plugin", in_sign='s', out_sign='a(sa{sa{ss}}ss)', method=self._getRoomsJoined) 95 host.bridge.addMethod("mucGetRoomsJoined", ".plugin", in_sign='s', out_sign='a(sa{sa{ss}}ss)', method=self._getRoomsJoined)
96 host.bridge.addMethod("mucGetUniqueRoomName", ".plugin", in_sign='ss', out_sign='s', method=self._getUniqueName) 96 host.bridge.addMethod("mucGetUniqueRoomName", ".plugin", in_sign='ss', out_sign='s', method=self._getUniqueName)
97 host.bridge.addMethod("mucConfigureRoom", ".plugin", in_sign='ss', out_sign='s', method=self._configureRoom, async=True) 97 host.bridge.addMethod("mucConfigureRoom", ".plugin", in_sign='ss', out_sign='s', method=self._configureRoom, async_=True)
98 host.bridge.addMethod("mucGetDefaultService", ".plugin", in_sign='', out_sign='s', method=self.getDefaultMUC) 98 host.bridge.addMethod("mucGetDefaultService", ".plugin", in_sign='', out_sign='s', method=self.getDefaultMUC)
99 host.bridge.addMethod("mucGetService", ".plugin", in_sign='ss', out_sign='s', method=self._getMUCService, async=True) 99 host.bridge.addMethod("mucGetService", ".plugin", in_sign='ss', out_sign='s', method=self._getMUCService, async_=True)
100 host.bridge.addSignal("mucRoomJoined", ".plugin", signature='sa{sa{ss}}sss') # args: room_jid, occupants, user_nick, subject, profile 100 host.bridge.addSignal("mucRoomJoined", ".plugin", signature='sa{sa{ss}}sss') # args: room_jid, occupants, user_nick, subject, profile
101 host.bridge.addSignal("mucRoomLeft", ".plugin", signature='ss') # args: room_jid, profile 101 host.bridge.addSignal("mucRoomLeft", ".plugin", signature='ss') # args: room_jid, profile
102 host.bridge.addSignal("mucRoomUserChangedNick", ".plugin", signature='ssss') # args: room_jid, old_nick, new_nick, profile 102 host.bridge.addSignal("mucRoomUserChangedNick", ".plugin", signature='ssss') # args: room_jid, old_nick, new_nick, profile
103 host.bridge.addSignal("mucRoomNewSubject", ".plugin", signature='sss') # args: room_jid, subject, profile 103 host.bridge.addSignal("mucRoomNewSubject", ".plugin", signature='sss') # args: room_jid, subject, profile
104 self.__submit_conf_id = host.registerCallback(self._submitConfiguration, with_data=True) 104 self.__submit_conf_id = host.registerCallback(self._submitConfiguration, with_data=True)
105 self._room_join_id = host.registerCallback(self._UIRoomJoinCb, with_data=True) 105 self._room_join_id = host.registerCallback(self._UIRoomJoinCb, with_data=True)
106 host.importMenu((D_("MUC"), D_("configure")), self._configureRoomMenu, security_limit=0, help_string=D_("Configure Multi-User Chat room"), type_=C.MENU_ROOM) 106 host.importMenu((D_("MUC"), D_("configure")), self._configureRoomMenu, security_limit=0, help_string=D_("Configure Multi-User Chat room"), type_=C.MENU_ROOM)
107 try: 107 try:
108 self.text_cmds = self.host.plugins[C.TEXT_CMDS] 108 self.text_cmds = self.host.plugins[C.TEXT_CMDS]
109 except KeyError: 109 except KeyError:
110 log.info(_(u"Text commands not available")) 110 log.info(_("Text commands not available"))
111 else: 111 else:
112 self.text_cmds.registerTextCommands(self) 112 self.text_cmds.registerTextCommands(self)
113 self.text_cmds.addWhoIsCb(self._whois, 100) 113 self.text_cmds.addWhoIsCb(self._whois, 100)
114 114
115 self._mam = self.host.plugins.get(u"XEP-0313") 115 self._mam = self.host.plugins.get("XEP-0313")
116 self._si = self.host.plugins[u"XEP-0359"] 116 self._si = self.host.plugins["XEP-0359"]
117 117
118 host.trigger.add("presence_available", self.presenceTrigger) 118 host.trigger.add("presence_available", self.presenceTrigger)
119 host.trigger.add("presence_received", self.presenceReceivedTrigger) 119 host.trigger.add("presence_received", self.presenceReceivedTrigger)
120 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) 121 host.trigger.add("message_parse", self._message_parseTrigger)
125 client.muc_service = service 125 client.muc_service = service
126 return self.getMUCService(client).addCallback(assign_service) 126 return self.getMUCService(client).addCallback(assign_service)
127 127
128 def _message_parseTrigger(self, client, message_elt, data): 128 def _message_parseTrigger(self, client, message_elt, data):
129 """Add stanza-id from the room if present""" 129 """Add stanza-id from the room if present"""
130 if message_elt.getAttribute(u"type") != C.MESS_TYPE_GROUPCHAT: 130 if message_elt.getAttribute("type") != C.MESS_TYPE_GROUPCHAT:
131 return True 131 return True
132 132
133 # stanza_id will not be filled by parseMessage because the emitter 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 134 # is the room and not our server, so we have to parse it here
135 room_jid = data[u"from"].userhostJID() 135 room_jid = data["from"].userhostJID()
136 stanza_id = self._si.getStanzaId(message_elt, room_jid) 136 stanza_id = self._si.getStanzaId(message_elt, room_jid)
137 if stanza_id: 137 if stanza_id:
138 data[u"extra"][u"stanza_id"] = stanza_id 138 data["extra"]["stanza_id"] = stanza_id
139 139
140 def messageReceivedTrigger(self, client, message_elt, post_treat): 140 def messageReceivedTrigger(self, client, message_elt, post_treat):
141 if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT: 141 if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
142 if message_elt.subject or message_elt.delay: 142 if message_elt.subject or message_elt.delay:
143 return False 143 return False
150 # With MAM history, order is different, and we can get live 150 # With MAM history, order is different, and we can get live
151 # messages before history is complete, so this is not a warning 151 # messages before history is complete, so this is not a warning
152 # but an expected case. 152 # but an expected case.
153 # On the other hand, with legacy history, it's not normal. 153 # On the other hand, with legacy history, it's not normal.
154 log.warning(_( 154 log.warning(_(
155 u"Received non delayed message in a room before its " 155 "Received non delayed message in a room before its "
156 u"initialisation: state={state}, msg={msg}").format( 156 "initialisation: state={state}, msg={msg}").format(
157 state=room.state, 157 state=room.state,
158 msg=message_elt.toXml())) 158 msg=message_elt.toXml()))
159 room._cache.append(message_elt) 159 room._cache.append(message_elt)
160 return False 160 return False
161 else: 161 else:
162 log.warning(u"Received groupchat message for a room which has not been " 162 log.warning("Received groupchat message for a room which has not been "
163 u"joined, ignoring it: {}".format(message_elt.toXml())) 163 "joined, ignoring it: {}".format(message_elt.toXml()))
164 return False 164 return False
165 return True 165 return True
166 166
167 def getRoom(self, client, room_jid): 167 def getRoom(self, client, room_jid):
168 """Retrieve Room instance from its jid 168 """Retrieve Room instance from its jid
171 @raise exceptions.NotFound: the room has not been joined 171 @raise exceptions.NotFound: the room has not been joined
172 """ 172 """
173 try: 173 try:
174 return client._muc_client.joined_rooms[room_jid] 174 return client._muc_client.joined_rooms[room_jid]
175 except KeyError: 175 except KeyError:
176 raise exceptions.NotFound(_(u"This room has not been joined")) 176 raise exceptions.NotFound(_("This room has not been joined"))
177 177
178 def checkRoomJoined(self, client, room_jid): 178 def checkRoomJoined(self, client, room_jid):
179 """Check that given room has been joined in current session 179 """Check that given room has been joined in current session
180 180
181 @param room_jid (JID): room JID 181 @param room_jid (JID): room JID
182 """ 182 """
183 if room_jid not in client._muc_client.joined_rooms: 183 if room_jid not in client._muc_client.joined_rooms:
184 raise exceptions.NotFound(_(u"This room has not been joined")) 184 raise exceptions.NotFound(_("This room has not been joined"))
185 185
186 def isJoinedRoom(self, client, room_jid): 186 def isJoinedRoom(self, client, room_jid):
187 """Tell if a jid is a known and joined room 187 """Tell if a jid is a known and joined room
188 188
189 @room_jid(jid.JID): jid of the room 189 @room_jid(jid.JID): jid of the room
211 return {} 211 return {}
212 212
213 def _passwordUICb(self, data, client, room_jid, nick): 213 def _passwordUICb(self, data, client, room_jid, nick):
214 """Called when the user has given room password (or cancelled)""" 214 """Called when the user has given room password (or cancelled)"""
215 if C.bool(data.get(C.XMLUI_DATA_CANCELLED, "false")): 215 if C.bool(data.get(C.XMLUI_DATA_CANCELLED, "false")):
216 log.info(u"room join for {} is cancelled".format(room_jid.userhost())) 216 log.info("room join for {} is cancelled".format(room_jid.userhost()))
217 raise failure.Failure(exceptions.CancelError(D_(u"Room joining cancelled by user"))) 217 raise failure.Failure(exceptions.CancelError(D_("Room joining cancelled by user")))
218 password = data[xml_tools.formEscape('password')] 218 password = data[xml_tools.formEscape('password')]
219 return client._muc_client.join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, (client, room_jid, nick), errbackArgs=(client, room_jid, nick, password)) 219 return client._muc_client.join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, (client, room_jid, nick), errbackArgs=(client, room_jid, nick, password))
220 220
221 def _showListUI(self, items, client, service): 221 def _showListUI(self, items, client, service):
222 xmlui = xml_tools.XMLUI(title=D_('Rooms in {}'.format(service.full()))) 222 xmlui = xml_tools.XMLUI(title=D_('Rooms in {}'.format(service.full())))
232 """Called when the user is in the requested room""" 232 """Called when the user is in the requested room"""
233 if room.locked: 233 if room.locked:
234 # FIXME: the current behaviour is to create an instant room 234 # FIXME: the current behaviour is to create an instant room
235 # and send the signal only when the room is unlocked 235 # and send the signal only when the room is unlocked
236 # a proper configuration management should be done 236 # a proper configuration management should be done
237 log.debug(_(u"room locked !")) 237 log.debug(_("room locked !"))
238 d = client._muc_client.configure(room.roomJID, {}) 238 d = client._muc_client.configure(room.roomJID, {})
239 d.addErrback(self.host.logErrback, 239 d.addErrback(self.host.logErrback,
240 msg=_(u'Error while configuring the room: {failure_}')) 240 msg=_('Error while configuring the room: {failure_}'))
241 return room.fully_joined 241 return room.fully_joined
242 242
243 def _joinEb(self, failure, client, room_jid, nick, password): 243 def _joinEb(self, failure, client, room_jid, nick, password):
244 """Called when something is going wrong when joining the room""" 244 """Called when something is going wrong when joining the room"""
245 try: 245 try:
251 # we have a nickname conflict, we try again with "_" suffixed to current nickname 251 # we have a nickname conflict, we try again with "_" suffixed to current nickname
252 nick += '_' 252 nick += '_'
253 return client._muc_client.join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, (client, room_jid, nick), errbackArgs=(client, room_jid, nick, password)) 253 return client._muc_client.join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, (client, room_jid, nick), errbackArgs=(client, room_jid, nick, password))
254 elif condition == 'not-allowed': 254 elif condition == 'not-allowed':
255 # room is restricted, we need a password 255 # room is restricted, we need a password
256 password_ui = xml_tools.XMLUI("form", title=D_(u'Room {} is restricted').format(room_jid.userhost()), submit_id='') 256 password_ui = xml_tools.XMLUI("form", title=D_('Room {} is restricted').format(room_jid.userhost()), submit_id='')
257 password_ui.addText(D_("This room is restricted, please enter the password")) 257 password_ui.addText(D_("This room is restricted, please enter the password"))
258 password_ui.addPassword('password') 258 password_ui.addPassword('password')
259 d = xml_tools.deferXMLUI(self.host, password_ui, profile=client.profile) 259 d = xml_tools.deferXMLUI(self.host, password_ui, profile=client.profile)
260 d.addCallback(self._passwordUICb, client, room_jid, nick) 260 d.addCallback(self._passwordUICb, client, room_jid, nick)
261 return d 261 return d
262 262
263 msg_suffix = ' with condition "{}"'.format(failure.value.condition) 263 msg_suffix = ' with condition "{}"'.format(failure.value.condition)
264 264
265 mess = D_(u"Error while joining the room {room}{suffix}".format( 265 mess = D_("Error while joining the room {room}{suffix}".format(
266 room = room_jid.userhost(), suffix = msg_suffix)) 266 room = room_jid.userhost(), suffix = msg_suffix))
267 log.error(mess) 267 log.error(mess)
268 xmlui = xml_tools.note(mess, D_(u"Group chat error"), level=C.XMLUI_DATA_LVL_ERROR) 268 xmlui = xml_tools.note(mess, D_("Group chat error"), level=C.XMLUI_DATA_LVL_ERROR)
269 self.host.actionNew({'xmlui': xmlui.toXml()}, profile=client.profile) 269 self.host.actionNew({'xmlui': xmlui.toXml()}, profile=client.profile)
270 270
271 @staticmethod 271 @staticmethod
272 def _getOccupants(room): 272 def _getOccupants(room):
273 """Get occupants of a room in a form suitable for bridge""" 273 """Get occupants of a room in a form suitable for bridge"""
274 return {u.nick: {k:unicode(getattr(u,k) or '') for k in OCCUPANT_KEYS} for u in room.roster.values()} 274 return {u.nick: {k:str(getattr(u,k) or '') for k in OCCUPANT_KEYS} for u in list(room.roster.values())}
275 275
276 def _getRoomOccupants(self, room_jid_s, profile_key): 276 def _getRoomOccupants(self, room_jid_s, profile_key):
277 client = self.host.getClient(profile_key) 277 client = self.host.getClient(profile_key)
278 room_jid = jid.JID(room_jid_s) 278 room_jid = jid.JID(room_jid_s)
279 return self.getRoomOccupants(client, room_jid) 279 return self.getRoomOccupants(client, room_jid)
287 return self.getRoomsJoined(client) 287 return self.getRoomsJoined(client)
288 288
289 def getRoomsJoined(self, client): 289 def getRoomsJoined(self, client):
290 """Return rooms where user is""" 290 """Return rooms where user is"""
291 result = [] 291 result = []
292 for room in client._muc_client.joined_rooms.values(): 292 for room in list(client._muc_client.joined_rooms.values()):
293 if room.state == ROOM_STATE_LIVE: 293 if room.state == ROOM_STATE_LIVE:
294 result.append((room.roomJID.userhost(), self._getOccupants(room), room.nick, room.subject)) 294 result.append((room.roomJID.userhost(), self._getOccupants(room), room.nick, room.subject))
295 return result 295 return result
296 296
297 def _getRoomNick(self, room_jid_s, profile_key=C.PROF_KEY_NONE): 297 def _getRoomNick(self, room_jid_s, profile_key=C.PROF_KEY_NONE):
328 log.error(_("room_jid key is not present !")) 328 log.error(_("room_jid key is not present !"))
329 return defer.fail(exceptions.DataError) 329 return defer.fail(exceptions.DataError)
330 330
331 def xmluiReceived(xmlui): 331 def xmluiReceived(xmlui):
332 if not xmlui: 332 if not xmlui:
333 msg = D_(u"No configuration available for this room") 333 msg = D_("No configuration available for this room")
334 return {"xmlui": xml_tools.note(msg).toXml()} 334 return {"xmlui": xml_tools.note(msg).toXml()}
335 return {"xmlui": xmlui.toXml()} 335 return {"xmlui": xmlui.toXml()}
336 return self.configureRoom(client, room_jid).addCallback(xmluiReceived) 336 return self.configureRoom(client, room_jid).addCallback(xmluiReceived)
337 337
338 def configureRoom(self, client, room_jid): 338 def configureRoom(self, client, room_jid):
383 return client._muc_client.joined_rooms[room_jid].inRoster(muc.User(nick)) 383 return client._muc_client.joined_rooms[room_jid].inRoster(muc.User(nick))
384 384
385 def _getMUCService(self, jid_=None, profile=C.PROF_KEY_NONE): 385 def _getMUCService(self, jid_=None, profile=C.PROF_KEY_NONE):
386 client = self.host.getClient(profile) 386 client = self.host.getClient(profile)
387 d = self.getMUCService(client, jid_ or None) 387 d = self.getMUCService(client, jid_ or None)
388 d.addCallback(lambda service_jid: service_jid.full() if service_jid is not None else u'') 388 d.addCallback(lambda service_jid: service_jid.full() if service_jid is not None else '')
389 return d 389 return d
390 390
391 @defer.inlineCallbacks 391 @defer.inlineCallbacks
392 def getMUCService(self, client, jid_=None): 392 def getMUCService(self, client, jid_=None):
393 """Return first found MUC service of an entity 393 """Return first found MUC service of an entity
424 424
425 @param muc_service (jid.JID) : leave empty string to use the default service 425 @param muc_service (jid.JID) : leave empty string to use the default service
426 @return: jid.JID (unique room bare JID) 426 @return: jid.JID (unique room bare JID)
427 """ 427 """
428 # TODO: we should use #RFC-0045 10.1.4 when available here 428 # TODO: we should use #RFC-0045 10.1.4 when available here
429 room_name = unicode(uuid.uuid4()) 429 room_name = str(uuid.uuid4())
430 if muc_service is None: 430 if muc_service is None:
431 try: 431 try:
432 muc_service = client.muc_service 432 muc_service = client.muc_service
433 except AttributeError: 433 except AttributeError:
434 raise exceptions.NotReady(u"Main server MUC service has not been checked yet") 434 raise exceptions.NotReady("Main server MUC service has not been checked yet")
435 if muc_service is None: 435 if muc_service is None:
436 log.warning(_("No MUC service found on main server")) 436 log.warning(_("No MUC service found on main server"))
437 raise exceptions.FeatureNotFound 437 raise exceptions.FeatureNotFound
438 438
439 muc_service = muc_service.userhost() 439 muc_service = muc_service.userhost()
440 return jid.JID(u"{}@{}".format(room_name, muc_service)) 440 return jid.JID("{}@{}".format(room_name, muc_service))
441 441
442 def getDefaultMUC(self): 442 def getDefaultMUC(self):
443 """Return the default MUC. 443 """Return the default MUC.
444 444
445 @return: unicode 445 @return: unicode
460 if room_jid_s: 460 if room_jid_s:
461 muc_service = client.muc_service 461 muc_service = client.muc_service
462 try: 462 try:
463 room_jid = jid.JID(room_jid_s) 463 room_jid = jid.JID(room_jid_s)
464 except (RuntimeError, jid.InvalidFormat, AttributeError): 464 except (RuntimeError, jid.InvalidFormat, AttributeError):
465 return defer.fail(jid.InvalidFormat(_(u"Invalid room identifier: {room_id}'. Please give a room short or full identifier like 'room' or 'room@{muc_service}'.").format( 465 return defer.fail(jid.InvalidFormat(_("Invalid room identifier: {room_id}'. Please give a room short or full identifier like 'room' or 'room@{muc_service}'.").format(
466 room_id=room_jid_s, 466 room_id=room_jid_s,
467 muc_service=unicode(muc_service)))) 467 muc_service=str(muc_service))))
468 if not room_jid.user: 468 if not room_jid.user:
469 room_jid.user, room_jid.host = room_jid.host, muc_service 469 room_jid.user, room_jid.host = room_jid.host, muc_service
470 else: 470 else:
471 room_jid = self.getUniqueName(profile_key=client.profile) 471 room_jid = self.getUniqueName(profile_key=client.profile)
472 # TODO: error management + signal in bridge 472 # TODO: error management + signal in bridge
480 nick = client.jid.user 480 nick = client.jid.user
481 if options is None: 481 if options is None:
482 options = {} 482 options = {}
483 if room_jid in client._muc_client.joined_rooms: 483 if room_jid in client._muc_client.joined_rooms:
484 room = client._muc_client.joined_rooms[room_jid] 484 room = client._muc_client.joined_rooms[room_jid]
485 log.info(_(u'{profile} is already in room {room_jid}').format( 485 log.info(_('{profile} is already in room {room_jid}').format(
486 profile=client.profile, room_jid = room_jid.userhost())) 486 profile=client.profile, room_jid = room_jid.userhost()))
487 return defer.fail(AlreadyJoined(room)) 487 return defer.fail(AlreadyJoined(room))
488 log.info(_(u"[{profile}] is joining room {room} with nick {nick}").format( 488 log.info(_("[{profile}] is joining room {room} with nick {nick}").format(
489 profile=client.profile, room=room_jid.userhost(), nick=nick)) 489 profile=client.profile, room=room_jid.userhost(), nick=nick))
490 490
491 password = options.get("password") 491 password = options.get("password")
492 492
493 d = client._muc_client.join(room_jid, nick, password) 493 d = client._muc_client.join(room_jid, nick, password)
502 This methods is to be called before a hot reconnection 502 This methods is to be called before a hot reconnection
503 @return (list[(jid.JID, unicode)]): arguments needed to re-join the rooms 503 @return (list[(jid.JID, unicode)]): arguments needed to re-join the rooms
504 This list can be used directly (unpacked) with self.join 504 This list can be used directly (unpacked) with self.join
505 """ 505 """
506 args_list = [] 506 args_list = []
507 for room in client._muc_client.joined_rooms.values(): 507 for room in list(client._muc_client.joined_rooms.values()):
508 client._muc_client._removeRoom(room.roomJID) 508 client._muc_client._removeRoom(room.roomJID)
509 args_list.append((client, room.roomJID, room.nick)) 509 args_list.append((client, room.roomJID, room.nick))
510 return args_list 510 return args_list
511 511
512 def _nick(self, room_jid_s, nick, profile_key=C.PROF_KEY_NONE): 512 def _nick(self, room_jid_s, nick, profile_key=C.PROF_KEY_NONE):
640 options = mess_data["unparsed"].strip().split() 640 options = mess_data["unparsed"].strip().split()
641 try: 641 try:
642 nick = options[0] 642 nick = options[0]
643 assert self.isNickInRoom(client, mess_data["to"], nick) 643 assert self.isNickInRoom(client, mess_data["to"], nick)
644 except (IndexError, AssertionError): 644 except (IndexError, AssertionError):
645 feedback = _(u"You must provide a member's nick to kick.") 645 feedback = _("You must provide a member's nick to kick.")
646 self.text_cmds.feedBack(client, feedback, mess_data) 646 self.text_cmds.feedBack(client, feedback, mess_data)
647 return False 647 return False
648 648
649 d = self.kick(client, nick, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}) 649 d = self.kick(client, nick, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]})
650 650
651 def cb(__): 651 def cb(__):
652 feedback_msg = _(u'You have kicked {}').format(nick) 652 feedback_msg = _('You have kicked {}').format(nick)
653 if len(options) > 1: 653 if len(options) > 1:
654 feedback_msg += _(u' for the following reason: {}').format(options[1]) 654 feedback_msg += _(' for the following reason: {}').format(options[1])
655 self.text_cmds.feedBack(client, feedback_msg, mess_data) 655 self.text_cmds.feedBack(client, feedback_msg, mess_data)
656 return True 656 return True
657 d.addCallback(cb) 657 d.addCallback(cb)
658 return d 658 return d
659 659
669 jid_s = options[0] 669 jid_s = options[0]
670 entity_jid = jid.JID(jid_s).userhostJID() 670 entity_jid = jid.JID(jid_s).userhostJID()
671 assert(entity_jid.user) 671 assert(entity_jid.user)
672 assert(entity_jid.host) 672 assert(entity_jid.host)
673 except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError): 673 except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError):
674 feedback = _(u"You must provide a valid JID to ban, like in '/ban contact@example.net'") 674 feedback = _("You must provide a valid JID to ban, like in '/ban contact@example.net'")
675 self.text_cmds.feedBack(client, feedback, mess_data) 675 self.text_cmds.feedBack(client, feedback, mess_data)
676 return False 676 return False
677 677
678 d = self.ban(client, entity_jid, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}) 678 d = self.ban(client, entity_jid, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]})
679 679
680 def cb(__): 680 def cb(__):
681 feedback_msg = _(u'You have banned {}').format(entity_jid) 681 feedback_msg = _('You have banned {}').format(entity_jid)
682 if len(options) > 1: 682 if len(options) > 1:
683 feedback_msg += _(u' for the following reason: {}').format(options[1]) 683 feedback_msg += _(' for the following reason: {}').format(options[1])
684 self.text_cmds.feedBack(client, feedback_msg, mess_data) 684 self.text_cmds.feedBack(client, feedback_msg, mess_data)
685 return True 685 return True
686 d.addCallback(cb) 686 d.addCallback(cb)
687 return d 687 return d
688 688
702 jid_s = options[0] 702 jid_s = options[0]
703 entity_jid = jid.JID(jid_s).userhostJID() 703 entity_jid = jid.JID(jid_s).userhostJID()
704 assert(entity_jid.user) 704 assert(entity_jid.user)
705 assert(entity_jid.host) 705 assert(entity_jid.host)
706 except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError): 706 except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError):
707 feedback = _(u"You must provide a valid JID to affiliate, like in '/affiliate contact@example.net member'") 707 feedback = _("You must provide a valid JID to affiliate, like in '/affiliate contact@example.net member'")
708 self.text_cmds.feedBack(client, feedback, mess_data) 708 self.text_cmds.feedBack(client, feedback, mess_data)
709 return False 709 return False
710 710
711 affiliation = options[1] if len(options) > 1 else 'none' 711 affiliation = options[1] if len(options) > 1 else 'none'
712 if affiliation not in AFFILIATIONS: 712 if affiliation not in AFFILIATIONS:
713 feedback = _(u"You must provide a valid affiliation: %s") % ' '.join(AFFILIATIONS) 713 feedback = _("You must provide a valid affiliation: %s") % ' '.join(AFFILIATIONS)
714 self.text_cmds.feedBack(client, feedback, mess_data) 714 self.text_cmds.feedBack(client, feedback, mess_data)
715 return False 715 return False
716 716
717 d = self.affiliate(client, entity_jid, mess_data["to"], {'affiliation': affiliation}) 717 d = self.affiliate(client, entity_jid, mess_data["to"], {'affiliation': affiliation})
718 718
719 def cb(__): 719 def cb(__):
720 feedback_msg = _(u'New affiliation for %(entity)s: %(affiliation)s').format(entity=entity_jid, affiliation=affiliation) 720 feedback_msg = _('New affiliation for %(entity)s: %(affiliation)s').format(entity=entity_jid, affiliation=affiliation)
721 self.text_cmds.feedBack(client, feedback_msg, mess_data) 721 self.text_cmds.feedBack(client, feedback_msg, mess_data)
722 return True 722 return True
723 d.addCallback(cb) 723 d.addCallback(cb)
724 return d 724 return d
725 725
761 room_jid = mess_data["to"] 761 room_jid = mess_data["to"]
762 service = jid.JID(room_jid.host) 762 service = jid.JID(room_jid.host)
763 elif client.muc_service is not None: 763 elif client.muc_service is not None:
764 service = client.muc_service 764 service = client.muc_service
765 else: 765 else:
766 msg = D_(u"No known default MUC service".format(unparsed)) 766 msg = D_("No known default MUC service".format(unparsed))
767 self.text_cmds.feedBack(client, msg, mess_data) 767 self.text_cmds.feedBack(client, msg, mess_data)
768 return False 768 return False
769 except jid.InvalidFormat: 769 except jid.InvalidFormat:
770 msg = D_(u"{} is not a valid JID!".format(unparsed)) 770 msg = D_("{} is not a valid JID!".format(unparsed))
771 self.text_cmds.feedBack(client, msg, mess_data) 771 self.text_cmds.feedBack(client, msg, mess_data)
772 return False 772 return False
773 d = self.host.getDiscoItems(client, service) 773 d = self.host.getDiscoItems(client, service)
774 d.addCallback(self._showListUI, client, service) 774 d.addCallback(self._showListUI, client, service)
775 775
799 799
800 def presenceTrigger(self, presence_elt, client): 800 def presenceTrigger(self, presence_elt, client):
801 # FIXME: should we add a privacy parameters in settings to activate before 801 # FIXME: should we add a privacy parameters in settings to activate before
802 # broadcasting the presence to all MUC rooms ? 802 # broadcasting the presence to all MUC rooms ?
803 muc_client = client._muc_client 803 muc_client = client._muc_client
804 for room_jid, room in muc_client.joined_rooms.iteritems(): 804 for room_jid, room in muc_client.joined_rooms.items():
805 elt = xml_tools.elementCopy(presence_elt) 805 elt = xml_tools.elementCopy(presence_elt)
806 elt['to'] = room_jid.userhost() + '/' + room.nick 806 elt['to'] = room_jid.userhost() + '/' + room.nick
807 client.presence.send(elt) 807 client.presence.send(elt)
808 return True 808 return True
809 809
814 # presence is already handled in (un)availableReceived 814 # presence is already handled in (un)availableReceived
815 return False 815 return False
816 return True 816 return True
817 817
818 818
819 @implementer(iwokkel.IDisco)
819 class SatMUCClient(muc.MUCClient): 820 class SatMUCClient(muc.MUCClient):
820 implements(iwokkel.IDisco)
821 821
822 def __init__(self, plugin_parent): 822 def __init__(self, plugin_parent):
823 self.plugin_parent = plugin_parent 823 self.plugin_parent = plugin_parent
824 muc.MUCClient.__init__(self) 824 muc.MUCClient.__init__(self)
825 self._changing_nicks = set() # used to keep trace of who is changing nick, 825 self._changing_nicks = set() # used to keep trace of who is changing nick,
826 # and to discard userJoinedRoom signal in this case 826 # and to discard userJoinedRoom signal in this case
827 print "init SatMUCClient OK" 827 print("init SatMUCClient OK")
828 828
829 @property 829 @property
830 def joined_rooms(self): 830 def joined_rooms(self):
831 return self._rooms 831 return self._rooms
832 832
859 room=room.userhost(), 859 room=room.userhost(),
860 state=new_state)) 860 state=new_state))
861 expected_state = ROOM_STATES[new_state_idx-1] 861 expected_state = ROOM_STATES[new_state_idx-1]
862 if room.state != expected_state: 862 if room.state != expected_state:
863 log.error(_( 863 log.error(_(
864 u"room {room} is not in expected state: room is in state {current_state} " 864 "room {room} is not in expected state: room is in state {current_state} "
865 u"while we were expecting {expected_state}").format( 865 "while we were expecting {expected_state}").format(
866 room=room.roomJID.userhost(), 866 room=room.roomJID.userhost(),
867 current_state=room.state, 867 current_state=room.state,
868 expected_state=expected_state)) 868 expected_state=expected_state))
869 room.state = new_state 869 room.state = new_state
870 870
917 room_jid, 917 room_jid,
918 None, 918 None,
919 limit=1, 919 limit=1,
920 between=False, 920 between=False,
921 filters={ 921 filters={
922 u'types': C.MESS_TYPE_GROUPCHAT, 922 'types': C.MESS_TYPE_GROUPCHAT,
923 u'last_stanza_id': True}, 923 'last_stanza_id': True},
924 profile=client.profile) 924 profile=client.profile)
925 if last_mess: 925 if last_mess:
926 stanza_id = last_mess[0][-1][u'stanza_id'] 926 stanza_id = last_mess[0][-1]['stanza_id']
927 rsm_req = rsm.RSMRequest(max_=100, after=stanza_id) 927 rsm_req = rsm.RSMRequest(max_=100, after=stanza_id)
928 no_loop=False 928 no_loop=False
929 else: 929 else:
930 log.info(u"We have no MAM archive for room {room_jid}.".format( 930 log.info("We have no MAM archive for room {room_jid}.".format(
931 room_jid=room_jid)) 931 room_jid=room_jid))
932 # we don't want the whole archive if we have no archive yet 932 # we don't want the whole archive if we have no archive yet
933 # as it can be huge 933 # as it can be huge
934 rsm_req = rsm.RSMRequest(max_=50, before=u'') 934 rsm_req = rsm.RSMRequest(max_=50, before='')
935 no_loop=True 935 no_loop=True
936 936
937 mam_req = mam.MAMRequest(rsm_=rsm_req) 937 mam_req = mam.MAMRequest(rsm_=rsm_req)
938 complete = False 938 complete = False
939 count = 0 939 count = 0
940 while not complete: 940 while not complete:
941 mam_data = yield self._mam.getArchives(client, mam_req, 941 mam_data = yield self._mam.getArchives(client, mam_req,
942 service=room_jid) 942 service=room_jid)
943 elt_list, rsm_response, mam_response = mam_data 943 elt_list, rsm_response, mam_response = mam_data
944 complete = True if no_loop else mam_response[u"complete"] 944 complete = True if no_loop else mam_response["complete"]
945 # we update MAM request for next iteration 945 # we update MAM request for next iteration
946 mam_req.rsm.after = rsm_response.last 946 mam_req.rsm.after = rsm_response.last
947 947
948 if not elt_list: 948 if not elt_list:
949 break 949 break
954 try: 954 try:
955 fwd_message_elt = self._mam.getMessageFromResult( 955 fwd_message_elt = self._mam.getMessageFromResult(
956 client, mess_elt, mam_req, service=room_jid) 956 client, mess_elt, mam_req, service=room_jid)
957 except exceptions.DataError: 957 except exceptions.DataError:
958 continue 958 continue
959 if fwd_message_elt.getAttribute(u"to"): 959 if fwd_message_elt.getAttribute("to"):
960 log.warning( 960 log.warning(
961 u'Forwarded message element has a "to" attribute while it is ' 961 'Forwarded message element has a "to" attribute while it is '
962 u'forbidden by specifications') 962 'forbidden by specifications')
963 fwd_message_elt[u"to"] = client.jid.full() 963 fwd_message_elt["to"] = client.jid.full()
964 mess_data = client.messageProt.parseMessage(fwd_message_elt) 964 mess_data = client.messageProt.parseMessage(fwd_message_elt)
965 # we attache parsed message data to element, to avoid parsing 965 # we attache parsed message data to element, to avoid parsing
966 # again in _addToHistory 966 # again in _addToHistory
967 fwd_message_elt._mess_data = mess_data 967 fwd_message_elt._mess_data = mess_data
968 # and we inject to MUC workflow 968 # and we inject to MUC workflow
969 client._muc_client._onGroupChat(fwd_message_elt) 969 client._muc_client._onGroupChat(fwd_message_elt)
970 970
971 if not count: 971 if not count:
972 log.info(_(u"No message received while offline in {room_jid}".format( 972 log.info(_("No message received while offline in {room_jid}".format(
973 room_jid=room_jid))) 973 room_jid=room_jid)))
974 else: 974 else:
975 log.info( 975 log.info(
976 _(u"We have received {num_mess} message(s) in {room_jid} while " 976 _("We have received {num_mess} message(s) in {room_jid} while "
977 u"offline.") 977 "offline.")
978 .format(num_mess=count, room_jid=room_jid)) 978 .format(num_mess=count, room_jid=room_jid))
979 979
980 # for legacy history, the following steps are done in receivedSubject but for MAM 980 # for legacy history, the following steps are done in receivedSubject but for MAM
981 # the order is different (we have to join then get MAM archive, so subject 981 # the order is different (we have to join then get MAM archive, so subject
982 # is received before archive), so we change state and add the callbacks here. 982 # is received before archive), so we change state and add the callbacks here.
1015 return 1015 return
1016 1016
1017 if user is None: 1017 if user is None:
1018 nick = presence.sender.resource 1018 nick = presence.sender.resource
1019 if not nick: 1019 if not nick:
1020 log.warning(_(u"missing nick in presence: {xml}").format( 1020 log.warning(_("missing nick in presence: {xml}").format(
1021 xml = presence.toElement().toXml())) 1021 xml = presence.toElement().toXml()))
1022 return 1022 return
1023 user = muc.User(nick, presence.entity) 1023 user = muc.User(nick, presence.entity)
1024 1024
1025 # Update user data 1025 # Update user data
1059 def userJoinedRoom(self, room, user): 1059 def userJoinedRoom(self, room, user):
1060 if user.nick == room.nick: 1060 if user.nick == room.nick:
1061 # we have received our own nick, 1061 # we have received our own nick,
1062 # this mean that the full room roster was received 1062 # this mean that the full room roster was received
1063 self.changeRoomState(room, ROOM_STATE_SELF_PRESENCE) 1063 self.changeRoomState(room, ROOM_STATE_SELF_PRESENCE)
1064 log.debug(u"room {room} joined with nick {nick}".format( 1064 log.debug("room {room} joined with nick {nick}".format(
1065 room=room.occupantJID.userhost(), nick=user.nick)) 1065 room=room.occupantJID.userhost(), nick=user.nick))
1066 # we set type so we don't have to use a deferred 1066 # we set type so we don't have to use a deferred
1067 # with disco to check entity type 1067 # with disco to check entity type
1068 self.host.memory.updateEntityData( 1068 self.host.memory.updateEntityData(
1069 room.roomJID, C.ENTITY_TYPE, C.ENTITY_TYPE_MUC, 1069 room.roomJID, C.ENTITY_TYPE, C.ENTITY_TYPE_MUC,
1070 profile_key=self.client.profile) 1070 profile_key=self.client.profile)
1071 elif room.state not in (ROOM_STATE_OCCUPANTS, ROOM_STATE_LIVE): 1071 elif room.state not in (ROOM_STATE_OCCUPANTS, ROOM_STATE_LIVE):
1072 log.warning( 1072 log.warning(
1073 u"Received user presence data in a room before its initialisation " 1073 "Received user presence data in a room before its initialisation "
1074 u"(current state: {state})," 1074 "(current state: {state}),"
1075 u"this is not standard! Ignoring it: {room} ({nick})".format( 1075 "this is not standard! Ignoring it: {room} ({nick})".format(
1076 state=room.state, 1076 state=room.state,
1077 room=room.roomJID.userhost(), 1077 room=room.roomJID.userhost(),
1078 nick=user.nick)) 1078 nick=user.nick))
1079 return 1079 return
1080 else: 1080 else:
1082 return 1082 return
1083 try: 1083 try:
1084 self._changing_nicks.remove(user.nick) 1084 self._changing_nicks.remove(user.nick)
1085 except KeyError: 1085 except KeyError:
1086 # this is a new user 1086 # this is a new user
1087 log.debug(_(u"user {nick} has joined room {room_id}").format( 1087 log.debug(_("user {nick} has joined room {room_id}").format(
1088 nick=user.nick, room_id=room.occupantJID.userhost())) 1088 nick=user.nick, room_id=room.occupantJID.userhost()))
1089 if not self.host.trigger.point( 1089 if not self.host.trigger.point(
1090 "MUC user joined", room, user, self.client.profile): 1090 "MUC user joined", room, user, self.client.profile):
1091 return 1091 return
1092 1092
1098 if user.entity is not None: 1098 if user.entity is not None:
1099 extra['user_entity'] = user.entity.full() 1099 extra['user_entity'] = user.entity.full()
1100 mess_data = { # dict is similar to the one used in client.onMessage 1100 mess_data = { # dict is similar to the one used in client.onMessage
1101 "from": room.roomJID, 1101 "from": room.roomJID,
1102 "to": self.client.jid, 1102 "to": self.client.jid,
1103 "uid": unicode(uuid.uuid4()), 1103 "uid": str(uuid.uuid4()),
1104 "message": {'': D_(u"=> {} has joined the room").format(user.nick)}, 1104 "message": {'': D_("=> {} has joined the room").format(user.nick)},
1105 "subject": {}, 1105 "subject": {},
1106 "type": C.MESS_TYPE_INFO, 1106 "type": C.MESS_TYPE_INFO,
1107 "extra": extra, 1107 "extra": extra,
1108 "timestamp": time.time(), 1108 "timestamp": time.time(),
1109 } 1109 }
1118 if not self.host.trigger.point("MUC user left", room, user, self.client.profile): 1118 if not self.host.trigger.point("MUC user left", room, user, self.client.profile):
1119 return 1119 return
1120 if user.nick == room.nick: 1120 if user.nick == room.nick:
1121 # we left the room 1121 # we left the room
1122 room_jid_s = room.roomJID.userhost() 1122 room_jid_s = room.roomJID.userhost()
1123 log.info(_(u"Room ({room}) left ({profile})").format( 1123 log.info(_("Room ({room}) left ({profile})").format(
1124 room = room_jid_s, profile = self.client.profile)) 1124 room = room_jid_s, profile = self.client.profile))
1125 self.host.memory.delEntityCache(room.roomJID, profile_key=self.client.profile) 1125 self.host.memory.delEntityCache(room.roomJID, profile_key=self.client.profile)
1126 self.host.bridge.mucRoomLeft(room.roomJID.userhost(), self.client.profile) 1126 self.host.bridge.mucRoomLeft(room.roomJID.userhost(), self.client.profile)
1127 elif room.state != ROOM_STATE_LIVE: 1127 elif room.state != ROOM_STATE_LIVE:
1128 log.warning(u"Received user presence data in a room before its initialisation (current state: {state})," 1128 log.warning("Received user presence data in a room before its initialisation (current state: {state}),"
1129 "this is not standard! Ignoring it: {room} ({nick})".format( 1129 "this is not standard! Ignoring it: {room} ({nick})".format(
1130 state=room.state, 1130 state=room.state,
1131 room=room.roomJID.userhost(), 1131 room=room.roomJID.userhost(),
1132 nick=user.nick)) 1132 nick=user.nick))
1133 return 1133 return
1134 else: 1134 else:
1135 if not room.fully_joined.called: 1135 if not room.fully_joined.called:
1136 return 1136 return
1137 log.debug(_(u"user {nick} left room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost())) 1137 log.debug(_("user {nick} left room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost()))
1138 extra = {'info_type': ROOM_USER_LEFT, 1138 extra = {'info_type': ROOM_USER_LEFT,
1139 'user_affiliation': user.affiliation, 1139 'user_affiliation': user.affiliation,
1140 'user_role': user.role, 1140 'user_role': user.role,
1141 'user_nick': user.nick 1141 'user_nick': user.nick
1142 } 1142 }
1143 if user.entity is not None: 1143 if user.entity is not None:
1144 extra['user_entity'] = user.entity.full() 1144 extra['user_entity'] = user.entity.full()
1145 mess_data = { # dict is similar to the one used in client.onMessage 1145 mess_data = { # dict is similar to the one used in client.onMessage
1146 "from": room.roomJID, 1146 "from": room.roomJID,
1147 "to": self.client.jid, 1147 "to": self.client.jid,
1148 "uid": unicode(uuid.uuid4()), 1148 "uid": str(uuid.uuid4()),
1149 "message": {'': D_(u"<= {} has left the room").format(user.nick)}, 1149 "message": {'': D_("<= {} has left the room").format(user.nick)},
1150 "subject": {}, 1150 "subject": {},
1151 "type": C.MESS_TYPE_INFO, 1151 "type": C.MESS_TYPE_INFO,
1152 "extra": extra, 1152 "extra": extra,
1153 "timestamp": time.time(), 1153 "timestamp": time.time(),
1154 } 1154 }
1179 entity.full(), show or '', 0, statuses, self.client.profile) 1179 entity.full(), show or '', 0, statuses, self.client.profile)
1180 1180
1181 ## messages ## 1181 ## messages ##
1182 1182
1183 def receivedGroupChat(self, room, user, body): 1183 def receivedGroupChat(self, room, user, body):
1184 log.debug(u'receivedGroupChat: room=%s user=%s body=%s' % (room.roomJID.full(), user, body)) 1184 log.debug('receivedGroupChat: room=%s user=%s body=%s' % (room.roomJID.full(), user, body))
1185 1185
1186 def _addToHistory(self, __, user, message): 1186 def _addToHistory(self, __, user, message):
1187 try: 1187 try:
1188 # message can be already parsed (with MAM), in this case mess_data 1188 # message can be already parsed (with MAM), in this case mess_data
1189 # it attached to the element 1189 # it attached to the element
1190 mess_data = message.element._mess_data 1190 mess_data = message.element._mess_data
1191 except AttributeError: 1191 except AttributeError:
1192 mess_data = self.client.messageProt.parseMessage(message.element) 1192 mess_data = self.client.messageProt.parseMessage(message.element)
1193 if mess_data[u'message'] or mess_data[u'subject']: 1193 if mess_data['message'] or mess_data['subject']:
1194 return self.host.memory.addToHistory(self.client, mess_data) 1194 return self.host.memory.addToHistory(self.client, mess_data)
1195 else: 1195 else:
1196 return defer.succeed(None) 1196 return defer.succeed(None)
1197 1197
1198 def _addToHistoryEb(self, failure): 1198 def _addToHistoryEb(self, failure):
1208 None if the message come from the room 1208 None if the message come from the room
1209 @param message(muc.GroupChat): the parsed message 1209 @param message(muc.GroupChat): the parsed message
1210 """ 1210 """
1211 if room.state != ROOM_STATE_SELF_PRESENCE: 1211 if room.state != ROOM_STATE_SELF_PRESENCE:
1212 log.warning(_( 1212 log.warning(_(
1213 u"received history in unexpected state in room {room} (state: " 1213 "received history in unexpected state in room {room} (state: "
1214 u"{state})").format(room = room.roomJID.userhost(), 1214 "{state})").format(room = room.roomJID.userhost(),
1215 state = room.state)) 1215 state = room.state))
1216 if not hasattr(room, "_history_d"): 1216 if not hasattr(room, "_history_d"):
1217 # XXX: this hack is due to buggy behaviour seen in the wild because of the 1217 # XXX: this hack is due to buggy behaviour seen in the wild because of the
1218 # "mod_delay" prosody module being activated. This module add an 1218 # "mod_delay" prosody module being activated. This module add an
1219 # unexpected <delay> elements which break our workflow. 1219 # unexpected <delay> elements which break our workflow.
1220 log.warning(_(u"storing the unexpected message anyway, to avoid loss")) 1220 log.warning(_("storing the unexpected message anyway, to avoid loss"))
1221 # we have to restore URI which are stripped by wokkel parsing 1221 # we have to restore URI which are stripped by wokkel parsing
1222 for c in message.element.elements(): 1222 for c in message.element.elements():
1223 if c.uri is None: 1223 if c.uri is None:
1224 c.uri = C.NS_CLIENT 1224 c.uri = C.NS_CLIENT
1225 mess_data = self.client.messageProt.parseMessage(message.element) 1225 mess_data = self.client.messageProt.parseMessage(message.element)
1226 message.element._mess_data = mess_data 1226 message.element._mess_data = mess_data
1227 self._addToHistory(None, user, message) 1227 self._addToHistory(None, user, message)
1228 if mess_data[u'message'] or mess_data[u'subject']: 1228 if mess_data['message'] or mess_data['subject']:
1229 self.host.bridge.messageNew( 1229 self.host.bridge.messageNew(
1230 *self.client.messageGetBridgeArgs(mess_data), 1230 *self.client.messageGetBridgeArgs(mess_data),
1231 profile=self.client.profile 1231 profile=self.client.profile
1232 ) 1232 )
1233 return 1233 return
1246 # We override this method to fix subject handling 1246 # We override this method to fix subject handling
1247 # FIXME: remove this merge fixed upstream 1247 # FIXME: remove this merge fixed upstream
1248 room, user = self._getRoomUser(message) 1248 room, user = self._getRoomUser(message)
1249 1249
1250 if room is None: 1250 if room is None:
1251 log.warning(u"No room found for message: {message}" 1251 log.warning("No room found for message: {message}"
1252 .format(message=message.toElement().toXml())) 1252 .format(message=message.toElement().toXml()))
1253 return 1253 return
1254 1254
1255 if message.subject is not None: 1255 if message.subject is not None:
1256 self.receivedSubject(room, user, message.subject) 1256 self.receivedSubject(room, user, message.subject)
1280 del room._cache 1280 del room._cache
1281 cache_presence = room._cache_presence 1281 cache_presence = room._cache_presence
1282 del room._cache_presence 1282 del room._cache_presence
1283 for elem in cache: 1283 for elem in cache:
1284 self.client.xmlstream.dispatch(elem) 1284 self.client.xmlstream.dispatch(elem)
1285 for presence_data in cache_presence.itervalues(): 1285 for presence_data in cache_presence.values():
1286 if not presence_data[u'show'] and not presence_data[u'status']: 1286 if not presence_data['show'] and not presence_data['status']:
1287 # occupants are already sent in mucRoomJoined, so if we don't have 1287 # occupants are already sent in mucRoomJoined, so if we don't have
1288 # extra information like show or statuses, we can discard the signal 1288 # extra information like show or statuses, we can discard the signal
1289 continue 1289 continue
1290 else: 1290 else:
1291 self.userUpdatedStatus(**presence_data) 1291 self.userUpdatedStatus(**presence_data)
1292 1292
1293 def _historyEb(self, failure_, room): 1293 def _historyEb(self, failure_, room):
1294 log.error(u"Error while managing history: {}".format(failure_)) 1294 log.error("Error while managing history: {}".format(failure_))
1295 self._historyCb(None, room) 1295 self._historyCb(None, room)
1296 1296
1297 def receivedSubject(self, room, user, subject): 1297 def receivedSubject(self, room, user, subject):
1298 # when subject is received, we know that we have whole roster and history 1298 # when subject is received, we know that we have whole roster and history
1299 # cf. http://xmpp.org/extensions/xep-0045.html#enter-subject 1299 # cf. http://xmpp.org/extensions/xep-0045.html#enter-subject
1302 if room._history_type == HISTORY_LEGACY: 1302 if room._history_type == HISTORY_LEGACY:
1303 self.changeRoomState(room, ROOM_STATE_LIVE) 1303 self.changeRoomState(room, ROOM_STATE_LIVE)
1304 room._history_d.addCallbacks(self._historyCb, self._historyEb, [room], errbackArgs=[room]) 1304 room._history_d.addCallbacks(self._historyCb, self._historyEb, [room], errbackArgs=[room])
1305 else: 1305 else:
1306 # the subject has been changed 1306 # the subject has been changed
1307 log.debug(_(u"New subject for room ({room_id}): {subject}").format(room_id = room.roomJID.full(), subject = subject)) 1307 log.debug(_("New subject for room ({room_id}): {subject}").format(room_id = room.roomJID.full(), subject = subject))
1308 self.host.bridge.mucRoomNewSubject(room.roomJID.userhost(), subject, self.client.profile) 1308 self.host.bridge.mucRoomNewSubject(room.roomJID.userhost(), subject, self.client.profile)
1309 1309
1310 ## disco ## 1310 ## disco ##
1311 1311
1312 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): 1312 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):