comparison src/plugins/plugin_xep_0045.py @ 1963:a2bc5089c2eb

backend, frontends: message refactoring (huge commit): /!\ several features are temporarily disabled, like notifications in frontends next step in refactoring, with the following changes: - jp: updated jp message to follow changes in backend/bridge - jp: added --lang, --subject, --subject_lang, and --type options to jp message + fixed unicode handling for jid - quick_frontend (QuickApp, QuickChat): - follow backend changes - refactored chat, message are now handled in OrderedDict and uid are kept so they can be updated - Message and Occupant classes handle metadata, so frontend just have to display them - Primitivus (Chat): - follow backend/QuickFrontend changes - info & standard messages are handled in the same MessageWidget class - improved/simplified handling of messages, removed update() method - user joined/left messages are merged when next to each other - a separator is shown when message is received while widget is out of focus, so user can quickly see the new messages - affiliation/role are shown (in a basic way for now) in occupants panel - removed "/me" messages handling, as it will be done by a backend plugin - message language is displayed when available (only one language per message for now) - fixed :history and :search commands - core (constants): new constants for messages type, XML namespace, entity type - core: *Message methods renamed to follow new code sytle (e.g. sendMessageToBridge => messageSendToBridge) - core (messages handling): fixed handling of language - core (messages handling): mes_data['from'] and ['to'] are now jid.JID - core (core.xmpp): reorganised message methods, added getNick() method to client.roster - plugin text commands: fixed plugin and adapted to new messages behaviour. client is now used in arguments instead of profile - plugins: added information for cancellation reason in CancelError calls - plugin XEP-0045: various improvments, but this plugin still need work: - trigger is used to avoid message already handled by the plugin to be handled a second time - changed the way to handle history, the last message from DB is checked and we request only messages since this one, in seconds (thanks Poezio folks :)) - subject reception is waited before sending the roomJoined signal, this way we are sure that everything including history is ready - cmd_* method now follow the new convention with client instead of profile - roomUserJoined and roomUserLeft messages are removed, the events are now handled with info message with a "ROOM_USER_JOINED" info subtype - probably other forgotten stuffs :p
author Goffi <goffi@goffi.org>
date Mon, 20 Jun 2016 18:41:53 +0200
parents 2daf7b4c6756
children 200cd707a46d
comparison
equal deleted inserted replaced
1962:a45235d8dc93 1963:a2bc5089c2eb
21 from sat.core.constants import Const as C 21 from sat.core.constants import Const as C
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 dateutil.tz import tzutc
26 27
27 from sat.core import exceptions 28 from sat.core import exceptions
28 from sat.memory import memory 29 from sat.memory import memory
29 30
31 import calendar
32 import time
30 import uuid 33 import uuid
31 import copy 34 import copy
32 35
33 from wokkel import muc, disco, iwokkel 36 from wokkel import muc, disco, iwokkel
34 from sat.tools import xml_tools 37 from sat.tools import xml_tools
48 "description": _("""Implementation of Multi-User Chat""") 51 "description": _("""Implementation of Multi-User Chat""")
49 } 52 }
50 53
51 NS_MUC = 'http://jabber.org/protocol/muc' 54 NS_MUC = 'http://jabber.org/protocol/muc'
52 AFFILIATIONS = ('owner', 'admin', 'member', 'none', 'outcast') 55 AFFILIATIONS = ('owner', 'admin', 'member', 'none', 'outcast')
56 ROOM_USER_JOINED = 'ROOM_USER_JOINED'
57 ROOM_USER_LEFT = 'ROOM_USER_LEFT'
58 OCCUPANT_KEYS = ('nick', 'entity', 'affiliation', 'role')
59 ENTITY_TYPE_MUC = "MUC"
53 60
54 CONFIG_SECTION = u'plugin muc' 61 CONFIG_SECTION = u'plugin muc'
55 62
56 default_conf = {"default_muc": u'sat@chat.jabberfr.org'} 63 default_conf = {"default_muc": u'sat@chat.jabberfr.org'}
57 64
71 # TODO: this plugin is messy, need a big cleanup/refactoring 78 # TODO: this plugin is messy, need a big cleanup/refactoring
72 79
73 def __init__(self, host): 80 def __init__(self, host):
74 log.info(_("Plugin XEP_0045 initialization")) 81 log.info(_("Plugin XEP_0045 initialization"))
75 self.host = host 82 self.host = host
76 self.clients = {} 83 self.clients = {} # FIXME: should be moved to profile's client
77 self._sessions = memory.Sessions() 84 self._sessions = memory.Sessions()
78 host.bridge.addMethod("joinMUC", ".plugin", in_sign='ssa{ss}s', out_sign='s', method=self._join, async=True) 85 host.bridge.addMethod("mucJoin", ".plugin", in_sign='ssa{ss}s', out_sign='s', method=self._join, async=True)
79 host.bridge.addMethod("mucNick", ".plugin", in_sign='sss', out_sign='', method=self.mucNick) 86 host.bridge.addMethod("mucNick", ".plugin", in_sign='sss', out_sign='', method=self.mucNick)
80 host.bridge.addMethod("mucLeave", ".plugin", in_sign='ss', out_sign='', method=self.mucLeave, async=True) 87 host.bridge.addMethod("mucLeave", ".plugin", in_sign='ss', out_sign='', method=self.mucLeave, async=True)
81 host.bridge.addMethod("getRoomsJoined", ".plugin", in_sign='s', out_sign='a(sass)', method=self.getRoomsJoined) 88 host.bridge.addMethod("getRoomsJoined", ".plugin", in_sign='s', out_sign='a(sa{sa{ss}}ss)', method=self.getRoomsJoined)
82 host.bridge.addMethod("getRoomsSubjects", ".plugin", in_sign='s', out_sign='a(ss)', method=self.getRoomsSubjects) 89 host.bridge.addMethod("getRoomsSubjects", ".plugin", in_sign='s', out_sign='a(ss)', method=self.getRoomsSubjects)
83 host.bridge.addMethod("getUniqueRoomName", ".plugin", in_sign='ss', out_sign='s', method=self._getUniqueName) 90 host.bridge.addMethod("getUniqueRoomName", ".plugin", in_sign='ss', out_sign='s', method=self._getUniqueName)
84 host.bridge.addMethod("configureRoom", ".plugin", in_sign='ss', out_sign='s', method=self._configureRoom, async=True) 91 host.bridge.addMethod("configureRoom", ".plugin", in_sign='ss', out_sign='s', method=self._configureRoom, async=True)
85 host.bridge.addMethod("getDefaultMUC", ".plugin", in_sign='', out_sign='s', method=self.getDefaultMUC) 92 host.bridge.addMethod("getDefaultMUC", ".plugin", in_sign='', out_sign='s', method=self.getDefaultMUC)
86 host.bridge.addSignal("roomJoined", ".plugin", signature='sasss') # args: room_jid, room_nicks, user_nick, profile 93 host.bridge.addSignal("roomJoined", ".plugin", signature='sa{sa{ss}}sss') # args: room_jid, occupants, user_nick, subject, profile
87 host.bridge.addSignal("roomLeft", ".plugin", signature='ss') # args: room_jid, profile 94 host.bridge.addSignal("roomLeft", ".plugin", signature='ss') # args: room_jid, profile
88 host.bridge.addSignal("roomUserJoined", ".plugin", signature='ssa{ss}s') # args: room_jid, user_nick, user_data, profile
89 host.bridge.addSignal("roomUserLeft", ".plugin", signature='ssa{ss}s') # args: room_jid, user_nick, user_data, profile
90 host.bridge.addSignal("roomUserChangedNick", ".plugin", signature='ssss') # args: room_jid, old_nick, new_nick, profile 95 host.bridge.addSignal("roomUserChangedNick", ".plugin", signature='ssss') # args: room_jid, old_nick, new_nick, profile
91 host.bridge.addSignal("roomNewSubject", ".plugin", signature='sss') # args: room_jid, subject, profile 96 host.bridge.addSignal("roomNewSubject", ".plugin", signature='sss') # args: room_jid, subject, profile
92 self.__submit_conf_id = host.registerCallback(self._submitConfiguration, with_data=True) 97 self.__submit_conf_id = host.registerCallback(self._submitConfiguration, with_data=True)
93 host.importMenu((D_("MUC"), D_("configure")), self._configureRoomMenu, security_limit=0, help_string=D_("Configure Multi-User Chat room"), type_=C.MENU_ROOM) 98 host.importMenu((D_("MUC"), D_("configure")), self._configureRoomMenu, security_limit=0, help_string=D_("Configure Multi-User Chat room"), type_=C.MENU_ROOM)
94 try: 99 try:
96 self.host.plugins[C.TEXT_CMDS].addWhoIsCb(self._whois, 100) 101 self.host.plugins[C.TEXT_CMDS].addWhoIsCb(self._whois, 100)
97 except KeyError: 102 except KeyError:
98 log.info(_("Text commands not available")) 103 log.info(_("Text commands not available"))
99 104
100 host.trigger.add("presence_available", self.presenceTrigger) 105 host.trigger.add("presence_available", self.presenceTrigger)
106 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=1000000)
101 107
102 def profileConnected(self, profile): 108 def profileConnected(self, profile):
103 def assign_service(service): 109 def assign_service(service):
104 client = self.host.getClient(profile) 110 client = self.host.getClient(profile)
105 client.muc_service = service 111 client.muc_service = service
106 return self.getMUCService(profile=profile).addCallback(assign_service) 112 return self.getMUCService(profile=profile).addCallback(assign_service)
113
114 def MessageReceivedTrigger(self, client, message_elt, post_treat):
115 if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
116 if message_elt.subject or message_elt.delay:
117 return False
118 from_jid = jid.JID(message_elt['from'])
119 room_jid = from_jid.userhostJID()
120 if room_jid in self.clients[client.profile].joined_rooms:
121 room = self.clients[client.profile].joined_rooms[room_jid]
122 if not room._room_ok:
123 log.warning(u"Received non delayed message in a room before its initialisation: {}".format(message_elt.toXml()))
124 room._cache.append(message_elt)
125 return False
126 else:
127 log.warning(u"Received groupchat message for a room which has not been joined, ignoring it: {}".format(message_elt.toXml()))
128 return False
129 return True
107 130
108 def checkClient(self, profile): 131 def checkClient(self, profile):
109 """Check if the profile is connected and has used the MUC feature. 132 """Check if the profile is connected and has used the MUC feature.
110 133
111 If profile was using MUC feature but is now disconnected, remove it from the client list. 134 If profile was using MUC feature but is now disconnected, remove it from the client list.
130 raise exceptions.ProfileUnknownError("Unknown or disconnected profile") 153 raise exceptions.ProfileUnknownError("Unknown or disconnected profile")
131 if room_jid not in self.clients[profile].joined_rooms: 154 if room_jid not in self.clients[profile].joined_rooms:
132 raise UnknownRoom("This room has not been joined") 155 raise UnknownRoom("This room has not been joined")
133 return profile 156 return profile
134 157
135 def __room_joined(self, room, profile): 158 def _joinCb(self, room, profile):
136 """Called when the user is in the requested room""" 159 """Called when the user is in the requested room"""
137
138 def _sendBridgeSignal(ignore=None):
139 self.host.bridge.roomJoined(room.roomJID.userhost(), [user.nick for user in room.roster.values()], room.nick, profile)
140
141 self.clients[profile].joined_rooms[room.roomJID] = room
142 if room.locked: 160 if room.locked:
143 # FIXME: the current behaviour is to create an instant room 161 # FIXME: the current behaviour is to create an instant room
144 # and send the signal only when the room is unlocked 162 # and send the signal only when the room is unlocked
145 # a proper configuration management should be done 163 # a proper configuration management should be done
146 print "room locked !" 164 print "room locked !"
147 self.clients[profile].configure(room.roomJID, {}).addCallbacks(_sendBridgeSignal, lambda x: log.error(_(u'Error while configuring the room'))) 165 d = self.clients[profile].configure(room.roomJID, {})
148 else: 166 d.addErrback(lambda dummy: log.error(_(u'Error while configuring the room')))
149 _sendBridgeSignal()
150 return room 167 return room
151 168
152 def __err_joining_room(self, failure, room_jid, nick, history_options, password, profile): 169 def _joinEb(self, failure, room_jid, nick, password, profile):
153 """Called when something is going wrong when joining the room""" 170 """Called when something is going wrong when joining the room"""
154 if hasattr(failure.value, "condition") and failure.value.condition == 'conflict': 171 if hasattr(failure.value, "condition") and failure.value.condition == 'conflict':
155 # we have a nickname conflict, we try again with "_" suffixed to current nickname 172 # we have a nickname conflict, we try again with "_" suffixed to current nickname
156 nick += '_' 173 nick += '_'
157 return self.clients[profile].join(room_jid, nick, history_options, password).addCallbacks(self.__room_joined, self.__err_joining_room, callbackKeywords={'profile': profile}, errbackArgs=[room_jid, nick, history_options, password, profile]) 174 return self.clients[profile].join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, callbackKeywords={'profile': profile}, errbackArgs=[room_jid, nick, password, profile])
158 mess = D_("Error while joining the room %s" % room_jid.userhost()) 175 mess = D_("Error while joining the room %s" % room_jid.userhost())
159 try: 176 try:
160 mess += " with condition '%s'" % failure.value.condition 177 mess += " with condition '%s'" % failure.value.condition
161 except AttributeError: 178 except AttributeError:
162 pass 179 pass
163 log.error(mess) 180 log.error(mess)
164 self.host.bridge.newAlert(mess, D_("Group chat error"), "ERROR", profile) 181 self.host.bridge.newAlert(mess, D_("Group chat error"), "ERROR", profile)
165 raise failure 182 raise failure
166 183
184 @staticmethod
185 def _getOccupants(room):
186 """Get occupants of a room in a form suitable for bridge"""
187 return {u.nick: {k:unicode(getattr(u,k) or '') for k in OCCUPANT_KEYS} for u in room.roster.values()}
188
167 def isRoom(self, entity_bare, profile_key): 189 def isRoom(self, entity_bare, profile_key):
168 """Tell if a bare entity is a MUC room. 190 """Tell if a bare entity is a MUC room.
169 191
170 @param entity_bare (jid.JID): bare entity 192 @param entity_bare (jid.JID): bare entity
171 @param profile_key (unicode): %(doc_profile_key)s 193 @param profile_key (unicode): %(doc_profile_key)s
179 profile = self.host.memory.getProfileName(profile_key) 201 profile = self.host.memory.getProfileName(profile_key)
180 result = [] 202 result = []
181 if not self.checkClient(profile): 203 if not self.checkClient(profile):
182 return result 204 return result
183 for room in self.clients[profile].joined_rooms.values(): 205 for room in self.clients[profile].joined_rooms.values():
184 result.append((room.roomJID.userhost(), [user.nick for user in room.roster.values()], room.nick)) 206 if room._room_ok:
207 result.append((room.roomJID.userhost(), self._getOccupants(room), room.nick, room.subject))
185 return result 208 return result
186 209
187 def getRoomNick(self, room_jid, profile_key=C.PROF_KEY_NONE): 210 def getRoomNick(self, room_jid, profile_key=C.PROF_KEY_NONE):
188 """return nick used in room by user 211 """return nick used in room by user
189 212
299 profile = self.getProfileAssertInRoom(room_jid, profile) 322 profile = self.getProfileAssertInRoom(room_jid, profile)
300 return self.clients[profile].joined_rooms[room_jid].inRoster(muc.User(nick)) 323 return self.clients[profile].joined_rooms[room_jid].inRoster(muc.User(nick))
301 324
302 def getRoomsSubjects(self, profile_key=C.PROF_KEY_NONE): 325 def getRoomsSubjects(self, profile_key=C.PROF_KEY_NONE):
303 """Return received subjects of rooms""" 326 """Return received subjects of rooms"""
327 # FIXME: to be removed
304 profile = self.host.memory.getProfileName(profile_key) 328 profile = self.host.memory.getProfileName(profile_key)
305 if not self.checkClient(profile): 329 if not self.checkClient(profile):
306 return [] 330 return []
307 return self.clients[profile].rec_subjects.values() 331 return self.clients[profile].rec_subjects.values()
308 332
353 377
354 @return: unicode 378 @return: unicode
355 """ 379 """
356 return self.host.memory.getConfig(CONFIG_SECTION, 'default_muc', default_conf['default_muc']) 380 return self.host.memory.getConfig(CONFIG_SECTION, 'default_muc', default_conf['default_muc'])
357 381
358 def join(self, room_jid, nick, options, profile_key=C.PROF_KEY_NONE): 382 def join(self, client, room_jid, nick, options):
359 def _errDeferred(exc_obj=Exception, txt='Error while joining room'): 383 def _errDeferred(exc_obj=Exception, txt='Error while joining room'):
360 d = defer.Deferred() 384 d = defer.Deferred()
361 d.errback(exc_obj(txt)) 385 d.errback(exc_obj(txt))
362 return d 386 return d
363 387
364 profile = self.host.memory.getProfileName(profile_key) 388 if room_jid in self.clients[client.profile].joined_rooms:
365 if not self.checkClient(profile): 389 log.warning(_(u'%(profile)s is already in room %(room_jid)s') % {'profile': client.profile, 'room_jid': room_jid.userhost()})
366 return _errDeferred()
367 if room_jid in self.clients[profile].joined_rooms:
368 log.warning(_(u'%(profile)s is already in room %(room_jid)s') % {'profile': profile, 'room_jid': room_jid.userhost()})
369 return _errDeferred(AlreadyJoinedRoom, D_(u"The room has already been joined")) 390 return _errDeferred(AlreadyJoinedRoom, D_(u"The room has already been joined"))
370 log.info(_(u"[%(profile)s] is joining room %(room)s with nick %(nick)s") % {'profile': profile, 'room': room_jid.userhost(), 'nick': nick}) 391 log.info(_(u"[%(profile)s] is joining room %(room)s with nick %(nick)s") % {'profile': client.profile, 'room': room_jid.userhost(), 'nick': nick})
371 392
372 if "history" in options:
373 history_limit = int(options["history"])
374 else:
375 history_limit = int(self.host.memory.getParamA(C.HISTORY_LIMIT, 'General', profile_key=profile))
376 # http://xmpp.org/extensions/xep-0045.html#enter-managehistory
377 history_options = muc.HistoryOptions(maxStanzas=history_limit)
378 password = options["password"] if "password" in options else None 393 password = options["password"] if "password" in options else None
379 394
380 return self.clients[profile].join(room_jid, nick, history_options, password).addCallbacks(self.__room_joined, self.__err_joining_room, callbackKeywords={'profile': profile}, errbackArgs=[room_jid, nick, history_options, password, profile]) 395 return self.clients[client.profile].join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, callbackKeywords={'profile': client.profile}, errbackArgs=[room_jid, nick, password, client.profile])
381 # FIXME: how to set the cancel method on the Deferred created by wokkel?
382 # This happens when the room is not reachable, e.g. no internet connection:
383 # > /usr/local/lib/python2.7/dist-packages/twisted/internet/defer.py(480)_startRunCallbacks()
384 # -> raise AlreadyCalledError(extra)
385 396
386 def _join(self, room_jid_s, nick, options=None, profile_key=C.PROF_KEY_NONE): 397 def _join(self, room_jid_s, nick, options=None, profile_key=C.PROF_KEY_NONE):
387 """join method used by bridge: use the join method, but doesn't return any deferred 398 """join method used by bridge
399
388 @return: unicode (the room bare) 400 @return: unicode (the room bare)
389 """ 401 """
402 client = self.host.getClient(profile_key)
390 if options is None: 403 if options is None:
391 options = {} 404 options = {}
392 profile = self.host.memory.getProfileName(profile_key)
393 if not self.checkClient(profile):
394 return
395 if room_jid_s: 405 if room_jid_s:
396 muc_service = self.host.getClient(profile).muc_service 406 muc_service = self.host.getClient(client.profile).muc_service
397 try: 407 try:
398 room_jid = jid.JID(room_jid_s) 408 room_jid = jid.JID(room_jid_s)
399 except (RuntimeError, jid.InvalidFormat, AttributeError): 409 except (RuntimeError, jid.InvalidFormat, AttributeError):
400 return defer.fail(jid.InvalidFormat(_(u"Invalid room identifier: '%s'. Please give a room short or full identifier like 'room' or 'room@%s'.") % (room_jid_s, unicode(muc_service)))) 410 return defer.fail(jid.InvalidFormat(_(u"Invalid room identifier: '%s'. Please give a room short or full identifier like 'room' or 'room@%s'.") % (room_jid_s, unicode(muc_service))))
401 if not room_jid.user: 411 if not room_jid.user:
402 room_jid.user, room_jid.host = room_jid.host, muc_service 412 room_jid.user, room_jid.host = room_jid.host, muc_service
403 else: 413 else:
404 room_jid = self.getUniqueName(profile_key=profile_key) 414 room_jid = self.getUniqueName(profile_key=client.profile)
405 # TODO: error management + signal in bridge 415 # TODO: error management + signal in bridge
406 d = self.join(room_jid, nick, options, profile) 416 d = self.join(client, room_jid, nick, options)
407 return d.addCallback(lambda room: room.roomJID.userhost()) 417 return d.addCallback(lambda room: room.roomJID.userhost())
408 418
409 def nick(self, room_jid, nick, profile_key): 419 def nick(self, room_jid, nick, profile_key):
410 profile = self.getProfileAssertInRoom(room_jid, profile_key) 420 profile = self.getProfileAssertInRoom(room_jid, profile_key)
411 return self.clients[profile].nick(room_jid, nick) 421 return self.clients[profile].nick(room_jid, nick)
475 # TODO: handles reason and nick 485 # TODO: handles reason and nick
476 return self.clients[profile].modifyAffiliationList(room_jid, [entity_jid], options['affiliation']) 486 return self.clients[profile].modifyAffiliationList(room_jid, [entity_jid], options['affiliation'])
477 487
478 # Text commands # 488 # Text commands #
479 489
480 def cmd_nick(self, mess_data, profile): 490 def cmd_nick(self, client, mess_data):
481 """change nickname 491 """change nickname
482 492
483 @command (group): new_nick 493 @command (group): new_nick
484 - new_nick: new nick to use 494 - new_nick: new nick to use
485 """ 495 """
486 nick = mess_data["unparsed"].strip() 496 nick = mess_data["unparsed"].strip()
487 if nick: 497 if nick:
488 room = mess_data["to"] 498 room = mess_data["to"]
489 self.nick(room, nick, profile) 499 self.nick(room, nick, client.profile)
490 500
491 return False 501 return False
492 502
493 def cmd_join(self, mess_data, profile): 503 def cmd_join(self, client, mess_data):
494 """join a new room 504 """join a new room
495 505
496 @command (all): JID 506 @command (all): JID
497 - JID: room to join (on the same service if full jid is not specified) 507 - JID: room to join (on the same service if full jid is not specified)
498 """ 508 """
499 if mess_data["unparsed"].strip(): 509 if mess_data["unparsed"].strip():
500 room_jid = self.host.plugins[C.TEXT_CMDS].getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host) 510 room_jid = self.host.plugins[C.TEXT_CMDS].getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host)
501 nick = (self.getRoomNick(room_jid, profile) or 511 nick = (self.getRoomNick(room_jid, client.profile) or
502 self.host.getClient(profile).jid.user) 512 self.host.getClient(client.profile).jid.user)
503 self.join(room_jid, nick, {}, profile) 513 self.join(client, room_jid, nick, {})
504 514
505 return False 515 return False
506 516
507 def cmd_leave(self, mess_data, profile): 517 def cmd_leave(self, client, mess_data):
508 """quit a room 518 """quit a room
509 519
510 @command (group): [ROOM_JID] 520 @command (group): [ROOM_JID]
511 - ROOM_JID: jid of the room to live (current room if not specified) 521 - ROOM_JID: jid of the room to live (current room if not specified)
512 """ 522 """
513 if mess_data["unparsed"].strip(): 523 if mess_data["unparsed"].strip():
514 room = self.host.plugins[C.TEXT_CMDS].getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host) 524 room = self.host.plugins[C.TEXT_CMDS].getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host)
515 else: 525 else:
516 room = mess_data["to"] 526 room = mess_data["to"]
517 527
518 self.leave(room, profile) 528 self.leave(room, client.profile)
519 529
520 return False 530 return False
521 531
522 def cmd_part(self, mess_data, profile): 532 def cmd_part(self, client, mess_data):
523 """just a synonym of /leave 533 """just a synonym of /leave
524 534
525 @command (group): [ROOM_JID] 535 @command (group): [ROOM_JID]
526 - ROOM_JID: jid of the room to live (current room if not specified) 536 - ROOM_JID: jid of the room to live (current room if not specified)
527 """ 537 """
528 return self.cmd_leave(mess_data, profile) 538 return self.cmd_leave(client, mess_data)
529 539
530 def cmd_kick(self, mess_data, profile): 540 def cmd_kick(self, client, mess_data):
531 """kick a room member 541 """kick a room member
532 542
533 @command (group): ROOM_NICK 543 @command (group): ROOM_NICK
534 - ROOM_NICK: the nick of the person to kick 544 - ROOM_NICK: the nick of the person to kick
535 """ 545 """
536 options = mess_data["unparsed"].strip().split() 546 options = mess_data["unparsed"].strip().split()
537 try: 547 try:
538 nick = options[0] 548 nick = options[0]
539 assert(self.isNickInRoom(mess_data["to"], nick, profile)) 549 assert(self.isNickInRoom(mess_data["to"], nick, client.profile))
540 except (IndexError, AssertionError): 550 except (IndexError, AssertionError):
541 feedback = _(u"You must provide a member's nick to kick.") 551 feedback = _(u"You must provide a member's nick to kick.")
542 self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile) 552 self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data)
543 return False 553 return False
544 554
545 d = self.kick(nick, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, profile) 555 d = self.kick(nick, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, client.profile)
546 556
547 def cb(dummy): 557 def cb(dummy):
548 feedback_msg = _(u'You have kicked {}').format(nick) 558 feedback_msg = _(u'You have kicked {}').format(nick)
549 if len(options) > 1: 559 if len(options) > 1:
550 feedback_msg += _(u' for the following reason: {}').format(options[1]) 560 feedback_msg += _(u' for the following reason: {}').format(options[1])
551 self.host.plugins[C.TEXT_CMDS].feedBack(feedback_msg, mess_data, profile) 561 self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback_msg, mess_data)
552 return True 562 return True
553 d.addCallback(cb) 563 d.addCallback(cb)
554 return d 564 return d
555 565
556 def cmd_ban(self, mess_data, profile): 566 def cmd_ban(self, client, mess_data):
557 """ban an entity from the room 567 """ban an entity from the room
558 568
559 @command (group): (JID) [reason] 569 @command (group): (JID) [reason]
560 - JID: the JID of the entity to ban 570 - JID: the JID of the entity to ban
561 - reason: the reason why this entity is being banned 571 - reason: the reason why this entity is being banned
566 entity_jid = jid.JID(jid_s).userhostJID() 576 entity_jid = jid.JID(jid_s).userhostJID()
567 assert(entity_jid.user) 577 assert(entity_jid.user)
568 assert(entity_jid.host) 578 assert(entity_jid.host)
569 except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError): 579 except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError):
570 feedback = _(u"You must provide a valid JID to ban, like in '/ban contact@example.net'") 580 feedback = _(u"You must provide a valid JID to ban, like in '/ban contact@example.net'")
571 self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile) 581 self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data)
572 return False 582 return False
573 583
574 d = self.ban(entity_jid, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, profile) 584 d = self.ban(entity_jid, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, client.profile)
575 585
576 def cb(dummy): 586 def cb(dummy):
577 feedback_msg = _(u'You have banned {}').format(entity_jid) 587 feedback_msg = _(u'You have banned {}').format(entity_jid)
578 if len(options) > 1: 588 if len(options) > 1:
579 feedback_msg += _(u' for the following reason: {}').format(options[1]) 589 feedback_msg += _(u' for the following reason: {}').format(options[1])
580 self.host.plugins[C.TEXT_CMDS].feedBack(feedback_msg, mess_data, profile) 590 self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback_msg, mess_data)
581 return True 591 return True
582 d.addCallback(cb) 592 d.addCallback(cb)
583 return d 593 return d
584 594
585 def cmd_affiliate(self, mess_data, profile): 595 def cmd_affiliate(self, client, mess_data):
586 """affiliate an entity to the room 596 """affiliate an entity to the room
587 597
588 @command (group): (JID) [owner|admin|member|none|outcast] 598 @command (group): (JID) [owner|admin|member|none|outcast]
589 - JID: the JID of the entity to affiliate 599 - JID: the JID of the entity to affiliate
590 - owner: grant owner privileges 600 - owner: grant owner privileges
599 entity_jid = jid.JID(jid_s).userhostJID() 609 entity_jid = jid.JID(jid_s).userhostJID()
600 assert(entity_jid.user) 610 assert(entity_jid.user)
601 assert(entity_jid.host) 611 assert(entity_jid.host)
602 except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError): 612 except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError):
603 feedback = _(u"You must provide a valid JID to affiliate, like in '/affiliate contact@example.net member'") 613 feedback = _(u"You must provide a valid JID to affiliate, like in '/affiliate contact@example.net member'")
604 self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile) 614 self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data)
605 return False 615 return False
606 616
607 affiliation = options[1] if len(options) > 1 else 'none' 617 affiliation = options[1] if len(options) > 1 else 'none'
608 if affiliation not in AFFILIATIONS: 618 if affiliation not in AFFILIATIONS:
609 feedback = _(u"You must provide a valid affiliation: %s") % ' '.join(AFFILIATIONS) 619 feedback = _(u"You must provide a valid affiliation: %s") % ' '.join(AFFILIATIONS)
610 self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile) 620 self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data)
611 return False 621 return False
612 622
613 d = self.affiliate(entity_jid, mess_data["to"], {'affiliation': affiliation}, profile) 623 d = self.affiliate(entity_jid, mess_data["to"], {'affiliation': affiliation}, client.profile)
614 624
615 def cb(dummy): 625 def cb(dummy):
616 feedback_msg = _(u'New affiliation for %(entity)s: %(affiliation)s').format(entity=entity_jid, affiliation=affiliation) 626 feedback_msg = _(u'New affiliation for %(entity)s: %(affiliation)s').format(entity=entity_jid, affiliation=affiliation)
617 self.host.plugins[C.TEXT_CMDS].feedBack(feedback_msg, mess_data, profile) 627 self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback_msg, mess_data)
618 return True 628 return True
619 d.addCallback(cb) 629 d.addCallback(cb)
620 return d 630 return d
621 631
622 def cmd_title(self, mess_data, profile): 632 def cmd_title(self, client, mess_data):
623 """change room's subject 633 """change room's subject
624 634
625 @command (group): title 635 @command (group): title
626 - title: new room subject 636 - title: new room subject
627 """ 637 """
628 subject = mess_data["unparsed"].strip() 638 subject = mess_data["unparsed"].strip()
629 639
630 if subject: 640 if subject:
631 room = mess_data["to"] 641 room = mess_data["to"]
632 self.subject(room, subject, profile) 642 self.subject(room, subject, client.profile)
633 643
634 return False 644 return False
635 645
636 def cmd_topic(self, mess_data, profile): 646 def cmd_topic(self, client, mess_data):
637 """just a synonym of /title 647 """just a synonym of /title
638 648
639 @command (group): title 649 @command (group): title
640 - title: new room subject 650 - title: new room subject
641 """ 651 """
642 return self.cmd_title(mess_data, profile) 652 return self.cmd_title(client, mess_data)
643 653
644 def _whois(self, whois_msg, mess_data, target_jid, profile): 654 def _whois(self, client, whois_msg, mess_data, target_jid):
645 """ Add MUC user information to whois """ 655 """ Add MUC user information to whois """
646 if mess_data['type'] != "groupchat": 656 if mess_data['type'] != "groupchat":
647 return 657 return
648 if target_jid.userhostJID() not in self.clients[profile].joined_rooms: 658 if target_jid.userhostJID() not in self.clients[client.profile].joined_rooms:
649 log.warning(_("This room has not been joined")) 659 log.warning(_("This room has not been joined"))
650 return 660 return
651 if not target_jid.resource: 661 if not target_jid.resource:
652 return 662 return
653 user = self.clients[profile].joined_rooms[target_jid.userhostJID()].getUser(target_jid.resource) 663 user = self.clients[client.profile].joined_rooms[target_jid.userhostJID()].getUser(target_jid.resource)
654 whois_msg.append(_("Nickname: %s") % user.nick) 664 whois_msg.append(_("Nickname: %s") % user.nick)
655 if user.entity: 665 if user.entity:
656 whois_msg.append(_("Entity: %s") % user.entity) 666 whois_msg.append(_("Entity: %s") % user.entity)
657 if user.affiliation != 'none': 667 if user.affiliation != 'none':
658 whois_msg.append(_("Affiliation: %s") % user.affiliation) 668 whois_msg.append(_("Affiliation: %s") % user.affiliation)
679 def __init__(self, plugin_parent): 689 def __init__(self, plugin_parent):
680 self.plugin_parent = plugin_parent 690 self.plugin_parent = plugin_parent
681 self.host = plugin_parent.host 691 self.host = plugin_parent.host
682 muc.MUCClient.__init__(self) 692 muc.MUCClient.__init__(self)
683 self.rec_subjects = {} 693 self.rec_subjects = {}
684 self.__changing_nicks = set() # used to keep trace of who is changing nick, 694 self._changing_nicks = set() # used to keep trace of who is changing nick,
685 # and to discard userJoinedRoom signal in this case 695 # and to discard userJoinedRoom signal in this case
686 print "init SatMUCClient OK" 696 print "init SatMUCClient OK"
687 697
688 @property 698 @property
689 def joined_rooms(self): 699 def joined_rooms(self):
690 return self._rooms 700 return self._rooms
691 701
692 def subject(self, room, subject): 702 def _addRoom(self, room):
693 return muc.MUCClientProtocol.subject(self, room, subject) 703 super(SatMUCClient, self)._addRoom(room)
704 room._roster_ok = False
705 room._room_ok = None # False when roster, history and subject are available
706 # True when new messages are saved to database
707 room._history_d = defer.Deferred() # use to send bridge signal once backlog are written in history
708 room._history_d.callback(None)
709 room._cache = []
710
711 def _gotLastDbHistory(self, mess_data_list, room_jid, nick, password):
712 if mess_data_list:
713 timestamp = mess_data_list[0][1]
714 # we use seconds since last message to get backlog without duplicates
715 # and we remove 1 second to avoid getting the last message again
716 seconds = int(time.time() - timestamp) - 1
717 else:
718 seconds = None
719 d = super(SatMUCClient, self).join(room_jid, nick, muc.HistoryOptions(seconds=seconds), password)
720 return d
721
722 def join(self, room_jid, nick, password=None):
723 d = self.host.memory.historyGet(self.parent.jid.userhostJID(), room_jid, 1, True, profile=self.parent.profile)
724 d.addCallback(self._gotLastDbHistory, room_jid, nick, password)
725 return d
726
727 ## presence/roster ##
694 728
695 def availableReceived(self, presence): 729 def availableReceived(self, presence):
696 """ 730 """
697 Available presence was received. 731 Available presence was received.
698 """ 732 """
699 # XXX: we override MUCClient.availableReceived to fix bugs 733 # XXX: we override MUCClient.availableReceived to fix bugs
700 # (affiliation and role are not set) 734 # (affiliation and role are not set)
701 # FIXME: propose a patch upstream
702 735
703 room, user = self._getRoomUser(presence) 736 room, user = self._getRoomUser(presence)
704 737
705 if room is None: 738 if room is None:
706 return 739 return
718 if room.inRoster(user): 751 if room.inRoster(user):
719 self.userUpdatedStatus(room, user, presence.show, presence.status) 752 self.userUpdatedStatus(room, user, presence.show, presence.status)
720 else: 753 else:
721 room.addUser(user) 754 room.addUser(user)
722 self.userJoinedRoom(room, user) 755 self.userJoinedRoom(room, user)
756
723 def unavailableReceived(self, presence): 757 def unavailableReceived(self, presence):
724 # XXX: we override this method to manage nickname change 758 # XXX: we override this method to manage nickname change
725 # TODO: feed this back to Wokkel
726 """ 759 """
727 Unavailable presence was received. 760 Unavailable presence was received.
728 761
729 If this was received from a MUC room occupant JID, that occupant has 762 If this was received from a MUC room occupant JID, that occupant has
730 left the room. 763 left the room.
735 return 768 return
736 769
737 room.removeUser(user) 770 room.removeUser(user)
738 771
739 if muc.STATUS_CODE.NEW_NICK in presence.mucStatuses: 772 if muc.STATUS_CODE.NEW_NICK in presence.mucStatuses:
740 self.__changing_nicks.add(presence.nick) 773 self._changing_nicks.add(presence.nick)
741 self.userChangedNick(room, user, presence.nick) 774 self.userChangedNick(room, user, presence.nick)
742 else: 775 else:
743 self.__changing_nicks.discard(presence.nick) 776 self._changing_nicks.discard(presence.nick)
744 self.userLeftRoom(room, user) 777 self.userLeftRoom(room, user)
745 778
746 def userJoinedRoom(self, room, user): 779 def userJoinedRoom(self, room, user):
747 self.host.memory.updateEntityData(room.roomJID, "type", "chatroom", profile_key=self.parent.profile) 780 if user.nick == room.nick:
748 if user.nick in self.__changing_nicks: 781 # we have received our own nick, this mean that the full room roster was received
749 self.__changing_nicks.remove(user.nick) 782 room._roster_ok = True
750 else: 783 log.debug(u"room {room} joined with nick {nick}".format(room=room.occupantJID.userhost(), nick=user.nick))
751 log.debug(_(u"user %(nick)s has joined room (%(room_id)s)") % {'nick': user.nick, 'room_id': room.occupantJID.userhost()}) 784 # We set type so we don't have use a deferred with disco to check entity type
752 if not self.host.trigger.point("MUC user joined", room, user, self.parent.profile): 785 self.host.memory.updateEntityData(room.roomJID, C.ENTITY_TYPE, ENTITY_TYPE_MUC, profile_key=self.parent.profile)
753 return 786
754 user_data = {'entity': user.entity.full() if user.entity else '', 'affiliation': user.affiliation, 'role': user.role} 787 elif room._roster_ok:
755 self.host.bridge.roomUserJoined(room.roomJID.userhost(), user.nick, user_data, self.parent.profile) 788 try:
789 self._changing_nicks.remove(user.nick)
790 except KeyError:
791 # this is a new user
792 log.debug(_(u"user {nick} has joined room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost()))
793 if not self.host.trigger.point("MUC user joined", room, user, self.parent.profile):
794 return
795
796 extra = {'info_type': ROOM_USER_JOINED,
797 'user_affiliation': user.affiliation,
798 'user_role': user.role,
799 'user_nick': user.nick
800 }
801 if user.entity is not None:
802 extra['user_entity'] = user.entity.full()
803 mess_data = { # dict is similar to the one used in client.onMessage
804 "from": room.roomJID,
805 "to": self.parent.jid,
806 "uid": unicode(uuid.uuid4()),
807 "message": {'': D_(u"=> {} has joined the room").format(user.nick)},
808 "subject": {},
809 "type": C.MESS_TYPE_INFO,
810 "extra": extra,
811 "timestamp": time.time(),
812 }
813 self.host.messageAddToHistory(mess_data, self.parent)
814 self.host.messageSendToBridge(mess_data, self.parent)
815
756 816
757 def userLeftRoom(self, room, user): 817 def userLeftRoom(self, room, user):
758 if not self.host.trigger.point("MUC user left", room, user, self.parent.profile): 818 if not self.host.trigger.point("MUC user left", room, user, self.parent.profile):
759 return 819 return
760 if user.nick == room.nick: 820 if user.nick == room.nick:
761 # we left the room 821 # we left the room
762 room_jid_s = room.roomJID.userhost() 822 room_jid_s = room.roomJID.userhost()
763 log.info(_(u"Room [%(room)s] left (%(profile)s))") % {"room": room_jid_s, 823 log.info(_(u"Room ({room}) left ({profile})").format(
764 "profile": self.parent.profile}) 824 room = room_jid_s, profile = self.parent.profile))
765 self.host.memory.delEntityCache(room.roomJID, profile_key=self.parent.profile) 825 self.host.memory.delEntityCache(room.roomJID, profile_key=self.parent.profile)
766 self.host.bridge.roomLeft(room.roomJID.userhost(), self.parent.profile) 826 self.host.bridge.roomLeft(room.roomJID.userhost(), self.parent.profile)
767 else: 827 else:
768 log.debug(_(u"user %(nick)s left room (%(room_id)s)") % {'nick': user.nick, 'room_id': room.occupantJID.userhost()}) 828 log.debug(_(u"user {nick} left room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost()))
769 user_data = {'entity': user.entity.full() if user.entity else '', 'affiliation': user.affiliation, 'role': user.role} 829 extra = {'info_type': ROOM_USER_LEFT,
770 self.host.bridge.roomUserLeft(room.roomJID.userhost(), user.nick, user_data, self.parent.profile) 830 'user_affiliation': user.affiliation,
831 'user_role': user.role,
832 'user_nick': user.nick
833 }
834 if user.entity is not None:
835 extra['user_entity'] = user.entity.full()
836 mess_data = { # dict is similar to the one used in client.onMessage
837 "from": room.roomJID,
838 "to": self.parent.jid,
839 "uid": unicode(uuid.uuid4()),
840 "message": {'': D_(u"<= {} has left the room").format(user.nick)},
841 "subject": {},
842 "type": C.MESS_TYPE_INFO,
843 "extra": extra,
844 "timestamp": time.time(),
845 }
846 self.host.messageAddToHistory(mess_data, self.parent)
847 self.host.messageSendToBridge(mess_data, self.parent)
771 848
772 def userChangedNick(self, room, user, new_nick): 849 def userChangedNick(self, room, user, new_nick):
773 self.host.bridge.roomUserChangedNick(room.roomJID.userhost(), user.nick, new_nick, self.parent.profile) 850 self.host.bridge.roomUserChangedNick(room.roomJID.userhost(), user.nick, new_nick, self.parent.profile)
774 851
775 def userUpdatedStatus(self, room, user, show, status): 852 def userUpdatedStatus(self, room, user, show, status):
776 self.host.bridge.presenceUpdate(room.roomJID.userhost() + '/' + user.nick, show or '', 0, {C.PRESENCE_STATUSES_DEFAULT: status or ''}, self.parent.profile) 853 self.host.bridge.presenceUpdate(room.roomJID.userhost() + '/' + user.nick, show or '', 0, {C.PRESENCE_STATUSES_DEFAULT: status or ''}, self.parent.profile)
777 854
855 ## messages ##
856
778 def receivedGroupChat(self, room, user, body): 857 def receivedGroupChat(self, room, user, body):
779 log.debug(u'receivedGroupChat: room=%s user=%s body=%s' % (room.roomJID.full(), user, body)) 858 log.debug(u'receivedGroupChat: room=%s user=%s body=%s' % (room.roomJID.full(), user, body))
780 859
860 def _addToHistory(self, dummy, user, message):
861 # we check if message is not in history
862 # and raise ConflictError else
863 stamp = message.delay.stamp.astimezone(tzutc()).timetuple()
864 timestamp = float(calendar.timegm(stamp))
865 data = { # dict is similar to the one used in client.onMessage
866 "from": message.sender,
867 "to": message.recipient,
868 "uid": unicode(uuid.uuid4()),
869 "type": C.MESS_TYPE_GROUPCHAT,
870 "extra": {},
871 "timestamp": timestamp,
872 "received_timestamp": unicode(time.time()),
873 }
874 # FIXME: message and subject don't handle xml:lang
875 data['message'] = {'': message.body} if message.body is not None else {}
876 data['subject'] = {'': message.subject} if message.subject is not None else {}
877
878 if data['message'] or data['subject']:
879 return self.host.memory.addToHistory(self.parent, data)
880 else:
881 return defer.succeed(None)
882
883 def _addToHistoryEb(self, failure):
884 failure.trap(exceptions.CancelError)
885
781 def receivedHistory(self, room, user, message): 886 def receivedHistory(self, room, user, message):
782 # http://xmpp.org/extensions/xep-0045.html#enter-history 887 """Called when history (backlog) message are received
783 # log.debug(u'receivedHistory: room=%s user=%s body=%s' % (room.roomJID.full(), user, message)) 888
784 pass 889 we check if message is not already in our history
890 and add it if needed
891 @param room(muc.Room): room instance
892 @param user(muc.User, None): the user that sent the message
893 None if the message come from the room
894 @param message(muc.GroupChat): the parsed message
895 """
896 room._history_d.addCallback(self._addToHistory, user, message)
897 room._history_d.addErrback(self._addToHistoryEb)
898
899 ## subject ##
900
901 def groupChatReceived(self, message):
902 """
903 A group chat message has been received from a MUC room.
904
905 There are a few event methods that may get called here.
906 L{receivedGroupChat}, L{receivedSubject} or L{receivedHistory}.
907 """
908 # We override this method to fix subject handling
909 # FIXME: remove this merge fixed upstream
910 room, user = self._getRoomUser(message)
911
912 if room is None:
913 return
914
915 if message.subject is not None:
916 self.receivedSubject(room, user, message.subject)
917 elif message.delay is None:
918 self.receivedGroupChat(room, user, message)
919 else:
920 self.receivedHistory(room, user, message)
921
922 def subject(self, room, subject):
923 return muc.MUCClientProtocol.subject(self, room, subject)
924
925 def _historyCb(self, dummy, room):
926 self.host.bridge.roomJoined(
927 room.roomJID.userhost(),
928 XEP_0045._getOccupants(room),
929 room.nick,
930 room.subject,
931 self.parent.profile)
932 del room._history_d
933 cache = room._cache
934 del room._cache
935 room._room_ok = True
936 for elem in cache:
937 self.parent.xmlstream.dispatch(elem)
938
939
940 def _historyEb(self, failure_, room):
941 log.error(u"Error while managing history: {}".format(failure_))
942 self._historyCb(None, room)
785 943
786 def receivedSubject(self, room, user, subject): 944 def receivedSubject(self, room, user, subject):
787 # http://xmpp.org/extensions/xep-0045.html#enter-subject 945 # when subject is received, we know that we have whole roster and history
788 log.debug(_(u"New subject for room (%(room_id)s): %(subject)s") % {'room_id': room.roomJID.full(), 'subject': subject}) 946 # cf. http://xmpp.org/extensions/xep-0045.html#enter-subject
947 room.subject = subject # FIXME: subject doesn't handle xml:lang
789 self.rec_subjects[room.roomJID.userhost()] = (room.roomJID.userhost(), subject) 948 self.rec_subjects[room.roomJID.userhost()] = (room.roomJID.userhost(), subject)
790 self.host.bridge.roomNewSubject(room.roomJID.userhost(), subject, self.parent.profile) 949 if room._room_ok is None:
950 # this is the first subject we receive
951 # that mean that we have received everything we need
952 room._room_ok = False
953 room._history_d.addCallbacks(self._historyCb, self._historyEb, [room], errbackArgs=[room])
954 else:
955 # the subject has been changed
956 log.debug(_(u"New subject for room ({room_id}): {subject}").format(room_id = room.roomJID.full(), subject = subject))
957 self.host.bridge.roomNewSubject(room.roomJID.userhost(), subject, self.parent.profile)
958
959 ## disco ##
791 960
792 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): 961 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
793 return [disco.DiscoFeature(NS_MUC)] 962 return [disco.DiscoFeature(NS_MUC)]
794 963
795 def getDiscoItems(self, requestor, target, nodeIdentifier=''): 964 def getDiscoItems(self, requestor, target, nodeIdentifier=''):