comparison sat/plugins/plugin_xep_0045.py @ 2581:395a3d1c2888

plugin XEP-0045: fixed joining workflow: - room keep joining workflow state, and display warning when something is received at unexpected moment - state is change immediatly when subject is received (which must be the last thing before live messages), avoiding timing issue if history storage in database is taking too much time - user joined/left data are not saved anymore in history as it takes a lot of space for little interest. A future option may allow to re-enable it if needed.
author Goffi <goffi@goffi.org>
date Fri, 11 May 2018 17:11:47 +0200
parents 26edcf3a30eb
children 8378806a70fe
comparison
equal deleted inserted replaced
2580:5e54afd17321 2581:395a3d1c2888
56 AFFILIATIONS = ('owner', 'admin', 'member', 'none', 'outcast') 56 AFFILIATIONS = ('owner', 'admin', 'member', 'none', 'outcast')
57 ROOM_USER_JOINED = 'ROOM_USER_JOINED' 57 ROOM_USER_JOINED = 'ROOM_USER_JOINED'
58 ROOM_USER_LEFT = 'ROOM_USER_LEFT' 58 ROOM_USER_LEFT = 'ROOM_USER_LEFT'
59 OCCUPANT_KEYS = ('nick', 'entity', 'affiliation', 'role') 59 OCCUPANT_KEYS = ('nick', 'entity', 'affiliation', 'role')
60 ENTITY_TYPE_MUC = "MUC" 60 ENTITY_TYPE_MUC = "MUC"
61 ROOM_STATE_OCCUPANTS = "occupants"
62 ROOM_STATE_SELF_PRESENCE = "self-presence"
63 ROOM_STATE_LIVE = "live"
64 ROOM_STATES = (ROOM_STATE_OCCUPANTS, ROOM_STATE_SELF_PRESENCE, ROOM_STATE_LIVE)
65
61 66
62 CONFIG_SECTION = u'plugin muc' 67 CONFIG_SECTION = u'plugin muc'
63 68
64 default_conf = {"default_muc": u'sat@chat.jabberfr.org'} 69 default_conf = {"default_muc": u'sat@chat.jabberfr.org'}
65 70
103 else: 108 else:
104 self.text_cmds.registerTextCommands(self) 109 self.text_cmds.registerTextCommands(self)
105 self.text_cmds.addWhoIsCb(self._whois, 100) 110 self.text_cmds.addWhoIsCb(self._whois, 100)
106 111
107 host.trigger.add("presence_available", self.presenceTrigger) 112 host.trigger.add("presence_available", self.presenceTrigger)
108 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=1000000) 113 host.trigger.add("MessageReceived", self.messageReceivedTrigger, priority=1000000)
109 114
110 def profileConnected(self, client): 115 def profileConnected(self, client):
111 def assign_service(service): 116 def assign_service(service):
112 client.muc_service = service 117 client.muc_service = service
113 return self.getMUCService(client).addCallback(assign_service) 118 return self.getMUCService(client).addCallback(assign_service)
114 119
115 def MessageReceivedTrigger(self, client, message_elt, post_treat): 120 def messageReceivedTrigger(self, client, message_elt, post_treat):
116 if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT: 121 if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
117 if message_elt.subject or message_elt.delay: 122 if message_elt.subject or message_elt.delay:
118 return False 123 return False
119 from_jid = jid.JID(message_elt['from']) 124 from_jid = jid.JID(message_elt['from'])
120 room_jid = from_jid.userhostJID() 125 room_jid = from_jid.userhostJID()
121 if room_jid in client._muc_client.joined_rooms: 126 if room_jid in client._muc_client.joined_rooms:
122 room = client._muc_client.joined_rooms[room_jid] 127 room = client._muc_client.joined_rooms[room_jid]
123 if not room._room_ok: 128 if room.state != ROOM_STATE_LIVE:
124 log.warning(u"Received non delayed message in a room before its initialisation: {}".format(message_elt.toXml())) 129 log.warning(_(u"Received non delayed message in a room before its initialisation: state={state}, msg={msg}").format(
130 state=room.state,
131 msg=message_elt.toXml()))
125 room._cache.append(message_elt) 132 room._cache.append(message_elt)
126 return False 133 return False
127 else: 134 else:
128 log.warning(u"Received groupchat message for a room which has not been joined, ignoring it: {}".format(message_elt.toXml())) 135 log.warning(u"Received groupchat message for a room which has not been joined, ignoring it: {}".format(message_elt.toXml()))
129 return False 136 return False
232 239
233 def getRoomsJoined(self, client): 240 def getRoomsJoined(self, client):
234 """Return rooms where user is""" 241 """Return rooms where user is"""
235 result = [] 242 result = []
236 for room in client._muc_client.joined_rooms.values(): 243 for room in client._muc_client.joined_rooms.values():
237 if room._room_ok: 244 if room.state == ROOM_STATE_LIVE:
238 result.append((room.roomJID.userhost(), self._getOccupants(room), room.nick, room.subject)) 245 result.append((room.roomJID.userhost(), self._getOccupants(room), room.nick, room.subject))
239 return result 246 return result
240 247
241 def _getRoomNick(self, room_jid_s, profile_key=C.PROF_KEY_NONE): 248 def _getRoomNick(self, room_jid_s, profile_key=C.PROF_KEY_NONE):
242 client = self.host.getClient(profile_key) 249 client = self.host.getClient(profile_key)
250 @return: nick or empty string in case of error 257 @return: nick or empty string in case of error
251 @raise exceptions.Notfound: use has not joined the room 258 @raise exceptions.Notfound: use has not joined the room
252 """ 259 """
253 self.checkRoomJoined(client, room_jid) 260 self.checkRoomJoined(client, room_jid)
254 return client._muc_client.joined_rooms[room_jid].nick 261 return client._muc_client.joined_rooms[room_jid].nick
255
256 # FIXME: broken, to be removed !
257 # def getRoomEntityNick(self, client, room_jid, entity_jid, =True):
258 # """Returns the nick of the given user in the room.
259
260 # @param room (wokkel.muc.Room): the room
261 # @param user (jid.JID): bare JID of the user
262 # @param secure (bool): set to True for a secure check
263 # @return: unicode or None if the user didn't join the room.
264 # """
265 # for user in room.roster.values():
266 # if user.entity is not None:
267 # if user.entity.userhostJID() == user_jid.userhostJID():
268 # return user.nick
269 # elif not secure:
270 # # FIXME: this is NOT ENOUGH to check an identity!!
271 # # See in which conditions user.entity could be None.
272 # if user.nick == user_jid.user:
273 # return user.nick
274 # return None
275
276 # def getRoomNicksOfUsers(self, room, users=[], secure=True):
277 # """Returns the nicks of the given users in the room.
278
279 # @param room (wokkel.muc.Room): the room
280 # @param users (list[jid.JID]): list of users
281 # @param secure (True): set to True for a secure check
282 # @return: a couple (x, y) with:
283 # - x (list[unicode]): nicks of the users who are in the room
284 # - y (list[jid.JID]): JID of the missing users.
285 # """
286 # nicks = []
287 # missing = []
288 # for user in users:
289 # nick = self.getRoomNickOfUser(room, user, secure)
290 # if nick is None:
291 # missing.append(user)
292 # else:
293 # nicks.append(nick)
294 # return nicks, missing
295 262
296 def _configureRoom(self, room_jid_s, profile_key=C.PROF_KEY_NONE): 263 def _configureRoom(self, room_jid_s, profile_key=C.PROF_KEY_NONE):
297 client = self.host.getClient(profile_key) 264 client = self.host.getClient(profile_key)
298 d = self.configureRoom(client, jid.JID(room_jid_s)) 265 d = self.configureRoom(client, jid.JID(room_jid_s))
299 d.addCallback(lambda xmlui: xmlui.toXml()) 266 d.addCallback(lambda xmlui: xmlui.toXml())
454 def join(self, client, room_jid, nick=None, options=None): 421 def join(self, client, room_jid, nick=None, options=None):
455 if not nick: 422 if not nick:
456 nick = client.jid.user 423 nick = client.jid.user
457 if options is None: 424 if options is None:
458 options = {} 425 options = {}
459 def _errDeferred(exc_obj=Exception, txt=u'Error while joining room'):
460 d = defer.Deferred()
461 d.errback(exc_obj(txt))
462 return d
463
464 if room_jid in client._muc_client.joined_rooms: 426 if room_jid in client._muc_client.joined_rooms:
465 room = client._muc_client.joined_rooms[room_jid] 427 room = client._muc_client.joined_rooms[room_jid]
466 log.warning(_(u'{profile} is already in room {room_jid}').format(profile=client.profile, room_jid = room_jid.userhost())) 428 log.info(_(u'{profile} is already in room {room_jid}').format(profile=client.profile, room_jid = room_jid.userhost()))
467 return defer.fail(AlreadyJoined(room)) 429 return defer.fail(AlreadyJoined(room))
468 log.info(_(u"[{profile}] is joining room {room} with nick {nick}").format(profile=client.profile, room=room_jid.userhost(), nick=nick)) 430 log.info(_(u"[{profile}] is joining room {room} with nick {nick}").format(profile=client.profile, room=room_jid.userhost(), nick=nick))
469 431
470 password = options["password"] if "password" in options else None 432 password = options.get("password")
471 433
472 return client._muc_client.join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, (client, room_jid, nick), errbackArgs=(client, room_jid, nick, password)) 434 return client._muc_client.join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, (client, room_jid, nick), errbackArgs=(client, room_jid, nick, password))
473 435
474 def _nick(self, room_jid_s, nick, profile_key=C.PROF_KEY_NONE): 436 def _nick(self, room_jid_s, nick, profile_key=C.PROF_KEY_NONE):
475 client = self.host.getClient(profile_key) 437 client = self.host.getClient(profile_key)
774 736
775 def __init__(self, plugin_parent): 737 def __init__(self, plugin_parent):
776 self.plugin_parent = plugin_parent 738 self.plugin_parent = plugin_parent
777 self.host = plugin_parent.host 739 self.host = plugin_parent.host
778 muc.MUCClient.__init__(self) 740 muc.MUCClient.__init__(self)
779 self.rec_subjects = {}
780 self._changing_nicks = set() # used to keep trace of who is changing nick, 741 self._changing_nicks = set() # used to keep trace of who is changing nick,
781 # and to discard userJoinedRoom signal in this case 742 # and to discard userJoinedRoom signal in this case
782 print "init SatMUCClient OK" 743 print "init SatMUCClient OK"
783 744
784 @property 745 @property
785 def joined_rooms(self): 746 def joined_rooms(self):
786 return self._rooms 747 return self._rooms
787 748
749 def changeRoomState(self, room, new_state):
750 """Check that room is in expected state, and change it
751
752 @param new_state: one of ROOM_STATE_*
753 """
754 new_state_idx = ROOM_STATES.index(new_state)
755 if new_state_idx == -1:
756 raise exceptions.InternalError("unknown room state")
757 if new_state_idx < 1:
758 raise exceptions.InternalError("unexpected new room state ({room}): {state}".format(
759 room=room.userhost(),
760 state=new_state))
761 expected_state = ROOM_STATES[new_state_idx-1]
762 if room.state != expected_state:
763 log.warning(_(u"room {room} is not in expected state: room is in state {current_state} while we were expecting {expected_state}").format(
764 room=room.userhost(),
765 current_state=room.state,
766 expected_state=expected_state))
767 room.state = new_state
768
788 def _addRoom(self, room): 769 def _addRoom(self, room):
789 super(SatMUCClient, self)._addRoom(room) 770 super(SatMUCClient, self)._addRoom(room)
790 room._roster_ok = False # True when occupants list has been fully received 771 room._roster_ok = False # True when occupants list has been fully received
791 room._room_ok = None # False when roster, history and subject are available 772 room.state = ROOM_STATE_OCCUPANTS
792 # True when new messages are saved to database
793 room._history_d = defer.Deferred() # used to send bridge signal once backlog are written in history 773 room._history_d = defer.Deferred() # used to send bridge signal once backlog are written in history
794 room._history_d.callback(None) 774 room._history_d.callback(None)
795 # FIXME: check if history_d is not redundant with fully_joined 775 # FIXME: check if history_d is not redundant with fully_joined
796 room.fully_joined = defer.Deferred() # called when everything is OK 776 room.fully_joined = defer.Deferred() # called when everything is OK
777 # cache data until room is ready
778 # list of elements which will be re-injected in stream
797 room._cache = [] 779 room._cache = []
798 780
799 def _gotLastDbHistory(self, mess_data_list, room_jid, nick, password): 781 @defer.inlineCallbacks
782 def join(self, room_jid, nick, password=None):
783 mess_data_list = yield self.host.memory.historyGet(self.parent.jid.userhostJID(),
784 room_jid,
785 1,
786 True,
787 profile=self.parent.profile)
800 if mess_data_list: 788 if mess_data_list:
801 timestamp = mess_data_list[0][1] 789 timestamp = mess_data_list[0][1]
802 # we use seconds since last message to get backlog without duplicates 790 # we use seconds since last message to get backlog without duplicates
803 # and we remove 1 second to avoid getting the last message again 791 # and we remove 1 second to avoid getting the last message again
804 seconds = int(time.time() - timestamp) - 1 792 seconds = int(time.time() - timestamp) - 1
805 else: 793 else:
806 seconds = None 794 seconds = None
807 d = super(SatMUCClient, self).join(room_jid, nick, muc.HistoryOptions(seconds=seconds), password) 795 room = yield super(SatMUCClient, self).join(room_jid, nick, muc.HistoryOptions(seconds=seconds), password)
808 return d 796 defer.returnValue(room)
809
810 def join(self, room_jid, nick, password=None):
811 d = self.host.memory.historyGet(self.parent.jid.userhostJID(), room_jid, 1, True, profile=self.parent.profile)
812 d.addCallback(self._gotLastDbHistory, room_jid, nick, password)
813 return d
814 797
815 ## presence/roster ## 798 ## presence/roster ##
816 799
817 def availableReceived(self, presence): 800 def availableReceived(self, presence):
818 """ 801 """
826 if room is None: 809 if room is None:
827 return 810 return
828 811
829 if user is None: 812 if user is None:
830 nick = presence.sender.resource 813 nick = presence.sender.resource
814 if not nick:
815 log.warning(_(u"missing nick in presence: {xml}").format(
816 xml = presence.toElement().toXml()))
817 return
831 user = muc.User(nick, presence.entity) 818 user = muc.User(nick, presence.entity)
832 819
833 # Update user data 820 # Update user data
834 user.role = presence.role 821 user.role = presence.role
835 user.affiliation = presence.affiliation 822 user.affiliation = presence.affiliation
865 self.userLeftRoom(room, user) 852 self.userLeftRoom(room, user)
866 853
867 def userJoinedRoom(self, room, user): 854 def userJoinedRoom(self, room, user):
868 if user.nick == room.nick: 855 if user.nick == room.nick:
869 # we have received our own nick, this mean that the full room roster was received 856 # we have received our own nick, this mean that the full room roster was received
870 room._roster_ok = True 857 self.changeRoomState(room, ROOM_STATE_SELF_PRESENCE)
871 log.debug(u"room {room} joined with nick {nick}".format(room=room.occupantJID.userhost(), nick=user.nick)) 858 log.debug(u"room {room} joined with nick {nick}".format(room=room.occupantJID.userhost(), nick=user.nick))
872 # We set type so we don't have use a deferred with disco to check entity type 859 # We set type so we don't have to use a deferred with disco to check entity type
873 self.host.memory.updateEntityData(room.roomJID, C.ENTITY_TYPE, ENTITY_TYPE_MUC, profile_key=self.parent.profile) 860 self.host.memory.updateEntityData(room.roomJID, C.ENTITY_TYPE, ENTITY_TYPE_MUC, profile_key=self.parent.profile)
874 elif not room._room_ok: 861 elif room.state not in (ROOM_STATE_OCCUPANTS, ROOM_STATE_LIVE):
875 log.warning(u"Received user presence data in a room before its initialisation (and after our own presence)," 862 log.warning(u"Received user presence data in a room before its initialisation (current state: {state}),"
876 "this is not standard! Ignoring it: {} ({})".format( 863 "this is not standard! Ignoring it: {room} ({nick})".format(
877 room.roomJID.userhost(), 864 state=room.state,
878 user.nick)) 865 room=room.roomJID.userhost(),
866 nick=user.nick))
879 return 867 return
880 elif room._roster_ok: 868 else:
869 if not room.fully_joined.called:
870 return
881 try: 871 try:
882 self._changing_nicks.remove(user.nick) 872 self._changing_nicks.remove(user.nick)
883 except KeyError: 873 except KeyError:
884 # this is a new user 874 # this is a new user
885 log.debug(_(u"user {nick} has joined room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost())) 875 log.debug(_(u"user {nick} has joined room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost()))
901 "subject": {}, 891 "subject": {},
902 "type": C.MESS_TYPE_INFO, 892 "type": C.MESS_TYPE_INFO,
903 "extra": extra, 893 "extra": extra,
904 "timestamp": time.time(), 894 "timestamp": time.time(),
905 } 895 }
906 self.parent.messageAddToHistory(mess_data) 896 # FIXME: we disable presence in history as it's taking a lot of space
897 # while not indispensable. In the future an option may allow
898 # to re-enable it
899 # self.parent.messageAddToHistory(mess_data)
907 self.parent.messageSendToBridge(mess_data) 900 self.parent.messageSendToBridge(mess_data)
908 901
909 902
910 def userLeftRoom(self, room, user): 903 def userLeftRoom(self, room, user):
911 if not self.host.trigger.point("MUC user left", room, user, self.parent.profile): 904 if not self.host.trigger.point("MUC user left", room, user, self.parent.profile):
915 room_jid_s = room.roomJID.userhost() 908 room_jid_s = room.roomJID.userhost()
916 log.info(_(u"Room ({room}) left ({profile})").format( 909 log.info(_(u"Room ({room}) left ({profile})").format(
917 room = room_jid_s, profile = self.parent.profile)) 910 room = room_jid_s, profile = self.parent.profile))
918 self.host.memory.delEntityCache(room.roomJID, profile_key=self.parent.profile) 911 self.host.memory.delEntityCache(room.roomJID, profile_key=self.parent.profile)
919 self.host.bridge.mucRoomLeft(room.roomJID.userhost(), self.parent.profile) 912 self.host.bridge.mucRoomLeft(room.roomJID.userhost(), self.parent.profile)
920 elif not room._room_ok: 913 elif room.state != ROOM_STATE_LIVE:
921 log.warning(u"Received user presence data in a room before its initialisation (and after our own presence)," 914 log.warning(u"Received user presence data in a room before its initialisation (current state: {state}),"
922 "this is not standard! Ignoring it: {} ({})".format( 915 "this is not standard! Ignoring it: {room} ({nick})".format(
923 room.roomJID.userhost(), 916 state=room.state,
924 user.nick)) 917 room=room.roomJID.userhost(),
918 nick=user.nick))
925 return 919 return
926 else: 920 else:
921 if not room.fully_joined.called:
922 return
927 log.debug(_(u"user {nick} left room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost())) 923 log.debug(_(u"user {nick} left room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost()))
928 extra = {'info_type': ROOM_USER_LEFT, 924 extra = {'info_type': ROOM_USER_LEFT,
929 'user_affiliation': user.affiliation, 925 'user_affiliation': user.affiliation,
930 'user_role': user.role, 926 'user_role': user.role,
931 'user_nick': user.nick 927 'user_nick': user.nick
940 "subject": {}, 936 "subject": {},
941 "type": C.MESS_TYPE_INFO, 937 "type": C.MESS_TYPE_INFO,
942 "extra": extra, 938 "extra": extra,
943 "timestamp": time.time(), 939 "timestamp": time.time(),
944 } 940 }
945 self.parent.messageAddToHistory(mess_data) 941 # FIXME: disable history, see userJoinRoom comment
942 # self.parent.messageAddToHistory(mess_data)
946 self.parent.messageSendToBridge(mess_data) 943 self.parent.messageSendToBridge(mess_data)
947 944
948 def userChangedNick(self, room, user, new_nick): 945 def userChangedNick(self, room, user, new_nick):
949 self.host.bridge.mucRoomUserChangedNick(room.roomJID.userhost(), user.nick, new_nick, self.parent.profile) 946 self.host.bridge.mucRoomUserChangedNick(room.roomJID.userhost(), user.nick, new_nick, self.parent.profile)
950 947
990 @param room(muc.Room): room instance 987 @param room(muc.Room): room instance
991 @param user(muc.User, None): the user that sent the message 988 @param user(muc.User, None): the user that sent the message
992 None if the message come from the room 989 None if the message come from the room
993 @param message(muc.GroupChat): the parsed message 990 @param message(muc.GroupChat): the parsed message
994 """ 991 """
992 if room.state != ROOM_STATE_SELF_PRESENCE:
993 log.warning(_(u"received history in unexpected state in room {room} (state: {state})").format(
994 room = room.userhost(),
995 state = room.state))
995 room._history_d.addCallback(self._addToHistory, user, message) 996 room._history_d.addCallback(self._addToHistory, user, message)
996 room._history_d.addErrback(self._addToHistoryEb) 997 room._history_d.addErrback(self._addToHistoryEb)
997 998
998 ## subject ## 999 ## subject ##
999 1000
1020 1021
1021 def subject(self, room, subject): 1022 def subject(self, room, subject):
1022 return muc.MUCClientProtocol.subject(self, room, subject) 1023 return muc.MUCClientProtocol.subject(self, room, subject)
1023 1024
1024 def _historyCb(self, dummy, room): 1025 def _historyCb(self, dummy, room):
1026 """Called when history have been written to database and subject is received
1027
1028 this method will finish joining by:
1029 - sending message to bridge
1030 - calling fully_joined deferred
1031 - sending stanza put in cache
1032 - cleaning variables not needed anymore
1033 """
1025 args = self.plugin_parent._getRoomJoinedArgs(room, self.parent.profile) 1034 args = self.plugin_parent._getRoomJoinedArgs(room, self.parent.profile)
1026 self.host.bridge.mucRoomJoined(*args) 1035 self.host.bridge.mucRoomJoined(*args)
1036 room.fully_joined.callback(room)
1027 del room._history_d 1037 del room._history_d
1028 cache = room._cache 1038 cache = room._cache
1029 del room._cache 1039 del room._cache
1030 room._room_ok = True
1031 for elem in cache: 1040 for elem in cache:
1032 self.parent.xmlstream.dispatch(elem) 1041 self.parent.xmlstream.dispatch(elem)
1033 1042
1034 def _historyEb(self, failure_, room): 1043 def _historyEb(self, failure_, room):
1035 log.error(u"Error while managing history: {}".format(failure_)) 1044 log.error(u"Error while managing history: {}".format(failure_))
1037 1046
1038 def receivedSubject(self, room, user, subject): 1047 def receivedSubject(self, room, user, subject):
1039 # when subject is received, we know that we have whole roster and history 1048 # when subject is received, we know that we have whole roster and history
1040 # cf. http://xmpp.org/extensions/xep-0045.html#enter-subject 1049 # cf. http://xmpp.org/extensions/xep-0045.html#enter-subject
1041 room.subject = subject # FIXME: subject doesn't handle xml:lang 1050 room.subject = subject # FIXME: subject doesn't handle xml:lang
1042 self.rec_subjects[room.roomJID.userhost()] = (room.roomJID.userhost(), subject) 1051 if room.state != ROOM_STATE_LIVE:
1043 if room._room_ok is None: 1052 self.changeRoomState(room, ROOM_STATE_LIVE)
1044 # this is the first subject we receive
1045 # that mean that we have received everything we need
1046 room._room_ok = False
1047 room._history_d.addCallbacks(self._historyCb, self._historyEb, [room], errbackArgs=[room]) 1053 room._history_d.addCallbacks(self._historyCb, self._historyEb, [room], errbackArgs=[room])
1048 room.fully_joined.callback(room)
1049 else: 1054 else:
1050 # the subject has been changed 1055 # the subject has been changed
1051 log.debug(_(u"New subject for room ({room_id}): {subject}").format(room_id = room.roomJID.full(), subject = subject)) 1056 log.debug(_(u"New subject for room ({room_id}): {subject}").format(room_id = room.roomJID.full(), subject = subject))
1052 self.host.bridge.mucRoomNewSubject(room.roomJID.userhost(), subject, self.parent.profile) 1057 self.host.bridge.mucRoomNewSubject(room.roomJID.userhost(), subject, self.parent.profile)
1053 1058