Mercurial > libervia-backend
comparison sat/plugins/plugin_xep_0045.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | src/plugins/plugin_xep_0045.py@0046283a285d |
children | 395a3d1c2888 |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SAT plugin for managing xep-0045 | |
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from sat.core.i18n import _, D_ | |
21 from sat.core.constants import Const as C | |
22 from sat.core.log import getLogger | |
23 log = getLogger(__name__) | |
24 from twisted.internet import defer | |
25 from twisted.words.protocols.jabber import jid | |
26 from twisted.python import failure | |
27 from dateutil.tz import tzutc | |
28 | |
29 from sat.core import exceptions | |
30 from sat.memory import memory | |
31 | |
32 import calendar | |
33 import time | |
34 import uuid | |
35 import copy | |
36 | |
37 from wokkel import muc, disco, iwokkel | |
38 from sat.tools import xml_tools | |
39 | |
40 from zope.interface import implements | |
41 | |
42 | |
43 PLUGIN_INFO = { | |
44 C.PI_NAME: "XEP-0045 Plugin", | |
45 C.PI_IMPORT_NAME: "XEP-0045", | |
46 C.PI_TYPE: "XEP", | |
47 C.PI_PROTOCOLS: ["XEP-0045"], | |
48 C.PI_DEPENDENCIES: [], | |
49 C.PI_RECOMMENDATIONS: [C.TEXT_CMDS], | |
50 C.PI_MAIN: "XEP_0045", | |
51 C.PI_HANDLER: "yes", | |
52 C.PI_DESCRIPTION: _("""Implementation of Multi-User Chat""") | |
53 } | |
54 | |
55 NS_MUC = 'http://jabber.org/protocol/muc' | |
56 AFFILIATIONS = ('owner', 'admin', 'member', 'none', 'outcast') | |
57 ROOM_USER_JOINED = 'ROOM_USER_JOINED' | |
58 ROOM_USER_LEFT = 'ROOM_USER_LEFT' | |
59 OCCUPANT_KEYS = ('nick', 'entity', 'affiliation', 'role') | |
60 ENTITY_TYPE_MUC = "MUC" | |
61 | |
62 CONFIG_SECTION = u'plugin muc' | |
63 | |
64 default_conf = {"default_muc": u'sat@chat.jabberfr.org'} | |
65 | |
66 | |
67 class AlreadyJoined(exceptions.ConflictError): | |
68 | |
69 def __init__(self, room): | |
70 super(AlreadyJoined, self).__init__() | |
71 self.room = room | |
72 | |
73 | |
74 class XEP_0045(object): | |
75 # TODO: handle invitations | |
76 # FIXME: this plugin need a good cleaning, join method is messy | |
77 | |
78 def __init__(self, host): | |
79 log.info(_("Plugin XEP_0045 initialization")) | |
80 self.host = host | |
81 self._sessions = memory.Sessions() | |
82 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) | |
83 host.bridge.addMethod("mucNick", ".plugin", in_sign='sss', out_sign='', method=self._nick) | |
84 host.bridge.addMethod("mucNickGet", ".plugin", in_sign='ss', out_sign='s', method=self._getRoomNick) | |
85 host.bridge.addMethod("mucLeave", ".plugin", in_sign='ss', out_sign='', method=self._leave, async=True) | |
86 host.bridge.addMethod("mucSubject", ".plugin", in_sign='sss', out_sign='', method=self._subject) | |
87 host.bridge.addMethod("mucGetRoomsJoined", ".plugin", in_sign='s', out_sign='a(sa{sa{ss}}ss)', method=self._getRoomsJoined) | |
88 host.bridge.addMethod("mucGetUniqueRoomName", ".plugin", in_sign='ss', out_sign='s', method=self._getUniqueName) | |
89 host.bridge.addMethod("mucConfigureRoom", ".plugin", in_sign='ss', out_sign='s', method=self._configureRoom, async=True) | |
90 host.bridge.addMethod("mucGetDefaultService", ".plugin", in_sign='', out_sign='s', method=self.getDefaultMUC) | |
91 host.bridge.addMethod("mucGetService", ".plugin", in_sign='ss', out_sign='s', method=self._getMUCService, async=True) | |
92 host.bridge.addSignal("mucRoomJoined", ".plugin", signature='sa{sa{ss}}sss') # args: room_jid, occupants, user_nick, subject, profile | |
93 host.bridge.addSignal("mucRoomLeft", ".plugin", signature='ss') # args: room_jid, profile | |
94 host.bridge.addSignal("mucRoomUserChangedNick", ".plugin", signature='ssss') # args: room_jid, old_nick, new_nick, profile | |
95 host.bridge.addSignal("mucRoomNewSubject", ".plugin", signature='sss') # args: room_jid, subject, profile | |
96 self.__submit_conf_id = host.registerCallback(self._submitConfiguration, with_data=True) | |
97 self._room_join_id = host.registerCallback(self._UIRoomJoinCb, with_data=True) | |
98 host.importMenu((D_("MUC"), D_("configure")), self._configureRoomMenu, security_limit=0, help_string=D_("Configure Multi-User Chat room"), type_=C.MENU_ROOM) | |
99 try: | |
100 self.text_cmds = self.host.plugins[C.TEXT_CMDS] | |
101 except KeyError: | |
102 log.info(_(u"Text commands not available")) | |
103 else: | |
104 self.text_cmds.registerTextCommands(self) | |
105 self.text_cmds.addWhoIsCb(self._whois, 100) | |
106 | |
107 host.trigger.add("presence_available", self.presenceTrigger) | |
108 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=1000000) | |
109 | |
110 def profileConnected(self, client): | |
111 def assign_service(service): | |
112 client.muc_service = service | |
113 return self.getMUCService(client).addCallback(assign_service) | |
114 | |
115 def MessageReceivedTrigger(self, client, message_elt, post_treat): | |
116 if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT: | |
117 if message_elt.subject or message_elt.delay: | |
118 return False | |
119 from_jid = jid.JID(message_elt['from']) | |
120 room_jid = from_jid.userhostJID() | |
121 if room_jid in client._muc_client.joined_rooms: | |
122 room = client._muc_client.joined_rooms[room_jid] | |
123 if not room._room_ok: | |
124 log.warning(u"Received non delayed message in a room before its initialisation: {}".format(message_elt.toXml())) | |
125 room._cache.append(message_elt) | |
126 return False | |
127 else: | |
128 log.warning(u"Received groupchat message for a room which has not been joined, ignoring it: {}".format(message_elt.toXml())) | |
129 return False | |
130 return True | |
131 | |
132 def checkRoomJoined(self, client, room_jid): | |
133 """Check that given room has been joined in current session | |
134 | |
135 @param room_jid (JID): room JID | |
136 """ | |
137 if room_jid not in client._muc_client.joined_rooms: | |
138 raise exceptions.NotFound(_(u"This room has not been joined")) | |
139 | |
140 def isJoinedRoom(self, client, room_jid): | |
141 """Tell if a jid is a known and joined room | |
142 | |
143 @room_jid(jid.JID): jid of the room | |
144 """ | |
145 try: | |
146 self.checkRoomJoined(client, room_jid) | |
147 except exceptions.NotFound: | |
148 return False | |
149 else: | |
150 return True | |
151 | |
152 def _getRoomJoinedArgs(self, room, profile): | |
153 return [ | |
154 room.roomJID.userhost(), | |
155 XEP_0045._getOccupants(room), | |
156 room.nick, | |
157 room.subject, | |
158 profile | |
159 ] | |
160 | |
161 def _UIRoomJoinCb(self, data, profile): | |
162 room_jid = jid.JID(data['index']) | |
163 client = self.host.getClient(profile) | |
164 self.join(client, room_jid) | |
165 return {} | |
166 | |
167 def _passwordUICb(self, data, client, room_jid, nick): | |
168 """Called when the user has given room password (or cancelled)""" | |
169 if C.bool(data.get(C.XMLUI_DATA_CANCELLED, "false")): | |
170 log.info(u"room join for {} is cancelled".format(room_jid.userhost())) | |
171 raise failure.Failure(exceptions.CancelError(D_(u"Room joining cancelled by user"))) | |
172 password = data[xml_tools.formEscape('password')] | |
173 return client._muc_client.join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, (client, room_jid, nick), errbackArgs=(client, room_jid, nick, password)) | |
174 | |
175 def _showListUI(self, items, client, service): | |
176 xmlui = xml_tools.XMLUI(title=D_('Rooms in {}'.format(service.full()))) | |
177 adv_list = xmlui.changeContainer('advanced_list', columns=1, selectable='single', callback_id=self._room_join_id) | |
178 items = sorted(items, key=lambda i: i.name.lower()) | |
179 for item in items: | |
180 adv_list.setRowIndex(item.entity.full()) | |
181 xmlui.addText(item.name) | |
182 adv_list.end() | |
183 self.host.actionNew({'xmlui': xmlui.toXml()}, profile=client.profile) | |
184 | |
185 def _joinCb(self, room, client, room_jid, nick): | |
186 """Called when the user is in the requested room""" | |
187 if room.locked: | |
188 # FIXME: the current behaviour is to create an instant room | |
189 # and send the signal only when the room is unlocked | |
190 # a proper configuration management should be done | |
191 log.debug(_(u"room locked !")) | |
192 d = client._muc_client.configure(room.roomJID, {}) | |
193 d.addErrback(lambda dummy: log.error(_(u'Error while configuring the room'))) | |
194 return room.fully_joined | |
195 | |
196 def _joinEb(self, failure, client, room_jid, nick, password): | |
197 """Called when something is going wrong when joining the room""" | |
198 try: | |
199 condition = failure.value.condition | |
200 except AttributeError: | |
201 msg_suffix = '' | |
202 else: | |
203 if condition == 'conflict': | |
204 # we have a nickname conflict, we try again with "_" suffixed to current nickname | |
205 nick += '_' | |
206 return client._muc_client.join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, (client, room_jid, nick), errbackArgs=(client, room_jid, nick, password)) | |
207 elif condition == 'not-allowed': | |
208 # room is restricted, we need a password | |
209 password_ui = xml_tools.XMLUI("form", title=D_(u'Room {} is restricted').format(room_jid.userhost()), submit_id='') | |
210 password_ui.addText(D_("This room is restricted, please enter the password")) | |
211 password_ui.addPassword('password') | |
212 d = xml_tools.deferXMLUI(self.host, password_ui, profile=client.profile) | |
213 d.addCallback(self._passwordUICb, client, room_jid, nick) | |
214 return d | |
215 | |
216 msg_suffix = ' with condition "{}"'.format(failure.value.condition) | |
217 | |
218 mess = D_(u"Error while joining the room {room}{suffix}".format( | |
219 room = room_jid.userhost(), suffix = msg_suffix)) | |
220 log.error(mess) | |
221 xmlui = xml_tools.note(mess, D_(u"Group chat error"), level=C.XMLUI_DATA_LVL_ERROR) | |
222 self.host.actionNew({'xmlui': xmlui.toXml()}, profile=client.profile) | |
223 | |
224 @staticmethod | |
225 def _getOccupants(room): | |
226 """Get occupants of a room in a form suitable for bridge""" | |
227 return {u.nick: {k:unicode(getattr(u,k) or '') for k in OCCUPANT_KEYS} for u in room.roster.values()} | |
228 | |
229 def _getRoomsJoined(self, profile_key=C.PROF_KEY_NONE): | |
230 client = self.host.getClient(profile_key) | |
231 return self.getRoomsJoined(client) | |
232 | |
233 def getRoomsJoined(self, client): | |
234 """Return rooms where user is""" | |
235 result = [] | |
236 for room in client._muc_client.joined_rooms.values(): | |
237 if room._room_ok: | |
238 result.append((room.roomJID.userhost(), self._getOccupants(room), room.nick, room.subject)) | |
239 return result | |
240 | |
241 def _getRoomNick(self, room_jid_s, profile_key=C.PROF_KEY_NONE): | |
242 client = self.host.getClient(profile_key) | |
243 return self.getRoomNick(client, jid.JID(room_jid_s)) | |
244 | |
245 def getRoomNick(self, client, room_jid): | |
246 """return nick used in room by user | |
247 | |
248 @param room_jid (jid.JID): JID of the room | |
249 @profile_key: profile | |
250 @return: nick or empty string in case of error | |
251 @raise exceptions.Notfound: use has not joined the room | |
252 """ | |
253 self.checkRoomJoined(client, room_jid) | |
254 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 | |
296 def _configureRoom(self, room_jid_s, profile_key=C.PROF_KEY_NONE): | |
297 client = self.host.getClient(profile_key) | |
298 d = self.configureRoom(client, jid.JID(room_jid_s)) | |
299 d.addCallback(lambda xmlui: xmlui.toXml()) | |
300 return d | |
301 | |
302 def _configureRoomMenu(self, menu_data, profile): | |
303 """Return room configuration form | |
304 | |
305 @param menu_data: %(menu_data)s | |
306 @param profile: %(doc_profile)s | |
307 """ | |
308 client = self.host.getClient(profile) | |
309 try: | |
310 room_jid = jid.JID(menu_data['room_jid']) | |
311 except KeyError: | |
312 log.error(_("room_jid key is not present !")) | |
313 return defer.fail(exceptions.DataError) | |
314 | |
315 def xmluiReceived(xmlui): | |
316 return {"xmlui": xmlui.toXml()} | |
317 return self.configureRoom(client, room_jid).addCallback(xmluiReceived) | |
318 | |
319 def configureRoom(self, client, room_jid): | |
320 """return the room configuration form | |
321 | |
322 @param room: jid of the room to configure | |
323 @return: configuration form as XMLUI | |
324 """ | |
325 self.checkRoomJoined(client, room_jid) | |
326 | |
327 def config2XMLUI(result): | |
328 if not result: | |
329 return "" | |
330 session_id, session_data = self._sessions.newSession(profile=client.profile) | |
331 session_data["room_jid"] = room_jid | |
332 xmlui = xml_tools.dataForm2XMLUI(result, submit_id=self.__submit_conf_id) | |
333 xmlui.session_id = session_id | |
334 return xmlui | |
335 | |
336 d = client._muc_client.getConfiguration(room_jid) | |
337 d.addCallback(config2XMLUI) | |
338 return d | |
339 | |
340 def _submitConfiguration(self, raw_data, profile): | |
341 client = self.host.getClient(profile) | |
342 try: | |
343 session_data = self._sessions.profileGet(raw_data["session_id"], profile) | |
344 except KeyError: | |
345 log.warning(D_("Session ID doesn't exist, session has probably expired.")) | |
346 _dialog = xml_tools.XMLUI('popup', title=D_('Room configuration failed')) | |
347 _dialog.addText(D_("Session ID doesn't exist, session has probably expired.")) | |
348 return defer.succeed({'xmlui': _dialog.toXml()}) | |
349 | |
350 data = xml_tools.XMLUIResult2DataFormResult(raw_data) | |
351 d = client._muc_client.configure(session_data['room_jid'], data) | |
352 _dialog = xml_tools.XMLUI('popup', title=D_('Room configuration succeed')) | |
353 _dialog.addText(D_("The new settings have been saved.")) | |
354 d.addCallback(lambda ignore: {'xmlui': _dialog.toXml()}) | |
355 del self._sessions[raw_data["session_id"]] | |
356 return d | |
357 | |
358 def isNickInRoom(self, client, room_jid, nick): | |
359 """Tell if a nick is currently present in a room""" | |
360 self.checkRoomJoined(client, room_jid) | |
361 return client._muc_client.joined_rooms[room_jid].inRoster(muc.User(nick)) | |
362 | |
363 def _getMUCService(self, jid_=None, profile=C.PROF_KEY_NONE): | |
364 client = self.host.getClient(profile) | |
365 d = self.getMUCService(client, jid_ or None) | |
366 d.addCallback(lambda service_jid: service_jid.full() if service_jid is not None else u'') | |
367 return d | |
368 | |
369 @defer.inlineCallbacks | |
370 def getMUCService(self, client, jid_=None): | |
371 """Return first found MUC service of an entity | |
372 | |
373 @param jid_: entity which may have a MUC service, or None for our own server | |
374 @return (jid.JID, None): found service jid or None | |
375 """ | |
376 if jid_ is None: | |
377 try: | |
378 muc_service = client.muc_service | |
379 except AttributeError: | |
380 pass | |
381 else: | |
382 # we have a cached value, we return it | |
383 defer.returnValue(muc_service) | |
384 services = yield self.host.findServiceEntities(client, "conference", "text", jid_) | |
385 for service in services: | |
386 if ".irc." not in service.userhost(): | |
387 # FIXME: | |
388 # This ugly hack is here to avoid an issue with openfire: the IRC gateway | |
389 # use "conference/text" identity (instead of "conference/irc") | |
390 muc_service = service | |
391 break | |
392 defer.returnValue(muc_service) | |
393 | |
394 def _getUniqueName(self, muc_service="", profile_key=C.PROF_KEY_NONE): | |
395 client = self.host.getClient(profile_key) | |
396 return self.getUniqueName(client, muc_service or None).full() | |
397 | |
398 def getUniqueName(self, client, muc_service=None): | |
399 """Return unique name for a room, avoiding collision | |
400 | |
401 @param muc_service (jid.JID) : leave empty string to use the default service | |
402 @return: jid.JID (unique room bare JID) | |
403 """ | |
404 # TODO: we should use #RFC-0045 10.1.4 when available here | |
405 room_name = unicode(uuid.uuid4()) | |
406 if muc_service is None: | |
407 try: | |
408 muc_service = client.muc_service | |
409 except AttributeError: | |
410 raise exceptions.NotReady(u"Main server MUC service has not been checked yet") | |
411 if muc_service is None: | |
412 log.warning(_("No MUC service found on main server")) | |
413 raise exceptions.FeatureNotFound | |
414 | |
415 muc_service = muc_service.userhost() | |
416 return jid.JID(u"{}@{}".format(room_name, muc_service)) | |
417 | |
418 def getDefaultMUC(self): | |
419 """Return the default MUC. | |
420 | |
421 @return: unicode | |
422 """ | |
423 return self.host.memory.getConfig(CONFIG_SECTION, 'default_muc', default_conf['default_muc']) | |
424 | |
425 def _join_eb(self, failure_, client): | |
426 failure_.trap(AlreadyJoined) | |
427 room = failure_.value.room | |
428 return [True] + self._getRoomJoinedArgs(room, client.profile) | |
429 | |
430 def _join(self, room_jid_s, nick, options, profile_key=C.PROF_KEY_NONE): | |
431 """join method used by bridge | |
432 | |
433 @return (tuple): already_joined boolean + room joined arguments (see [_getRoomJoinedArgs]) | |
434 """ | |
435 client = self.host.getClient(profile_key) | |
436 if room_jid_s: | |
437 muc_service = client.muc_service | |
438 try: | |
439 room_jid = jid.JID(room_jid_s) | |
440 except (RuntimeError, jid.InvalidFormat, AttributeError): | |
441 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( | |
442 room_id=room_jid_s, | |
443 muc_service=unicode(muc_service)))) | |
444 if not room_jid.user: | |
445 room_jid.user, room_jid.host = room_jid.host, muc_service | |
446 else: | |
447 room_jid = self.getUniqueName(profile_key=client.profile) | |
448 # TODO: error management + signal in bridge | |
449 d = self.join(client, room_jid, nick, options or None) | |
450 d.addCallback(lambda room: [False] + self._getRoomJoinedArgs(room, client.profile)) | |
451 d.addErrback(self._join_eb, client) | |
452 return d | |
453 | |
454 def join(self, client, room_jid, nick=None, options=None): | |
455 if not nick: | |
456 nick = client.jid.user | |
457 if options is None: | |
458 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: | |
465 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())) | |
467 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)) | |
469 | |
470 password = options["password"] if "password" in options else None | |
471 | |
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)) | |
473 | |
474 def _nick(self, room_jid_s, nick, profile_key=C.PROF_KEY_NONE): | |
475 client = self.host.getClient(profile_key) | |
476 return self.nick(client, jid.JID(room_jid_s), nick) | |
477 | |
478 def nick(self, client, room_jid, nick): | |
479 """Change nickname in a room""" | |
480 self.checkRoomJoined(client, room_jid) | |
481 return client._muc_client.nick(room_jid, nick) | |
482 | |
483 def _leave(self, room_jid, profile_key): | |
484 client = self.host.getClient(profile_key) | |
485 return self.leave(client, jid.JID(room_jid)) | |
486 | |
487 def leave(self, client, room_jid): | |
488 self.checkRoomJoined(client, room_jid) | |
489 return client._muc_client.leave(room_jid) | |
490 | |
491 def _subject(self, room_jid_s, new_subject, profile_key): | |
492 client = self.host.getClient(profile_key) | |
493 return self.subject(client, jid.JID(room_jid_s), new_subject) | |
494 | |
495 def subject(self, client, room_jid, subject): | |
496 self.checkRoomJoined(client, room_jid) | |
497 return client._muc_client.subject(room_jid, subject) | |
498 | |
499 def getHandler(self, client): | |
500 # create a MUC client and associate it with profile' session | |
501 muc_client = client._muc_client = SatMUCClient(self) | |
502 return muc_client | |
503 | |
504 def kick(self, client, nick, room_jid, options=None): | |
505 """ | |
506 Kick a participant from the room | |
507 @param nick (str): nick of the user to kick | |
508 @param room_jid_s (JID): jid of the room | |
509 @param options (dict): attribute with extra info (reason, password) as in #XEP-0045 | |
510 """ | |
511 if options is None: | |
512 options = {} | |
513 self.checkRoomJoined(client, room_jid) | |
514 return client._muc_client.kick(room_jid, nick, reason=options.get('reason', None)) | |
515 | |
516 def ban(self, client, entity_jid, room_jid, options=None): | |
517 """Ban an entity from the room | |
518 | |
519 @param entity_jid (JID): bare jid of the entity to be banned | |
520 @param room_jid (JID): jid of the room | |
521 @param options: attribute with extra info (reason, password) as in #XEP-0045 | |
522 """ | |
523 self.checkRoomJoined(client, room_jid) | |
524 if options is None: | |
525 options = {} | |
526 assert not entity_jid.resource | |
527 assert not room_jid.resource | |
528 return client._muc_client.ban(room_jid, entity_jid, reason=options.get('reason', None)) | |
529 | |
530 def affiliate(self, client, entity_jid, room_jid, options): | |
531 """Change the affiliation of an entity | |
532 | |
533 @param entity_jid (JID): bare jid of the entity | |
534 @param room_jid_s (JID): jid of the room | |
535 @param options: attribute with extra info (reason, nick) as in #XEP-0045 | |
536 """ | |
537 self.checkRoomJoined(client, room_jid) | |
538 assert not entity_jid.resource | |
539 assert not room_jid.resource | |
540 assert 'affiliation' in options | |
541 # TODO: handles reason and nick | |
542 return client._muc_client.modifyAffiliationList(room_jid, [entity_jid], options['affiliation']) | |
543 | |
544 # Text commands # | |
545 | |
546 def cmd_nick(self, client, mess_data): | |
547 """change nickname | |
548 | |
549 @command (group): new_nick | |
550 - new_nick: new nick to use | |
551 """ | |
552 nick = mess_data["unparsed"].strip() | |
553 if nick: | |
554 room = mess_data["to"] | |
555 self.nick(client, room, nick) | |
556 | |
557 return False | |
558 | |
559 def cmd_join(self, client, mess_data): | |
560 """join a new room | |
561 | |
562 @command (all): JID | |
563 - JID: room to join (on the same service if full jid is not specified) | |
564 """ | |
565 if mess_data["unparsed"].strip(): | |
566 room_jid = self.text_cmds.getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host) | |
567 nick = (self.getRoomNick(client, room_jid) or | |
568 client.jid.user) | |
569 self.join(client, room_jid, nick, {}) | |
570 | |
571 return False | |
572 | |
573 def cmd_leave(self, client, mess_data): | |
574 """quit a room | |
575 | |
576 @command (group): [ROOM_JID] | |
577 - ROOM_JID: jid of the room to live (current room if not specified) | |
578 """ | |
579 if mess_data["unparsed"].strip(): | |
580 room = self.text_cmds.getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host) | |
581 else: | |
582 room = mess_data["to"] | |
583 | |
584 self.leave(client, room) | |
585 | |
586 return False | |
587 | |
588 def cmd_part(self, client, mess_data): | |
589 """just a synonym of /leave | |
590 | |
591 @command (group): [ROOM_JID] | |
592 - ROOM_JID: jid of the room to live (current room if not specified) | |
593 """ | |
594 return self.cmd_leave(client, mess_data) | |
595 | |
596 def cmd_kick(self, client, mess_data): | |
597 """kick a room member | |
598 | |
599 @command (group): ROOM_NICK | |
600 - ROOM_NICK: the nick of the person to kick | |
601 """ | |
602 options = mess_data["unparsed"].strip().split() | |
603 try: | |
604 nick = options[0] | |
605 assert self.isNickInRoom(client, mess_data["to"], nick) | |
606 except (IndexError, AssertionError): | |
607 feedback = _(u"You must provide a member's nick to kick.") | |
608 self.text_cmds.feedBack(client, feedback, mess_data) | |
609 return False | |
610 | |
611 d = self.kick(client, nick, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}) | |
612 | |
613 def cb(dummy): | |
614 feedback_msg = _(u'You have kicked {}').format(nick) | |
615 if len(options) > 1: | |
616 feedback_msg += _(u' for the following reason: {}').format(options[1]) | |
617 self.text_cmds.feedBack(client, feedback_msg, mess_data) | |
618 return True | |
619 d.addCallback(cb) | |
620 return d | |
621 | |
622 def cmd_ban(self, client, mess_data): | |
623 """ban an entity from the room | |
624 | |
625 @command (group): (JID) [reason] | |
626 - JID: the JID of the entity to ban | |
627 - reason: the reason why this entity is being banned | |
628 """ | |
629 options = mess_data["unparsed"].strip().split() | |
630 try: | |
631 jid_s = options[0] | |
632 entity_jid = jid.JID(jid_s).userhostJID() | |
633 assert(entity_jid.user) | |
634 assert(entity_jid.host) | |
635 except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError): | |
636 feedback = _(u"You must provide a valid JID to ban, like in '/ban contact@example.net'") | |
637 self.text_cmds.feedBack(client, feedback, mess_data) | |
638 return False | |
639 | |
640 d = self.ban(client, entity_jid, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}) | |
641 | |
642 def cb(dummy): | |
643 feedback_msg = _(u'You have banned {}').format(entity_jid) | |
644 if len(options) > 1: | |
645 feedback_msg += _(u' for the following reason: {}').format(options[1]) | |
646 self.text_cmds.feedBack(client, feedback_msg, mess_data) | |
647 return True | |
648 d.addCallback(cb) | |
649 return d | |
650 | |
651 def cmd_affiliate(self, client, mess_data): | |
652 """affiliate an entity to the room | |
653 | |
654 @command (group): (JID) [owner|admin|member|none|outcast] | |
655 - JID: the JID of the entity to affiliate | |
656 - owner: grant owner privileges | |
657 - admin: grant admin privileges | |
658 - member: grant member privileges | |
659 - none: reset entity privileges | |
660 - outcast: ban entity | |
661 """ | |
662 options = mess_data["unparsed"].strip().split() | |
663 try: | |
664 jid_s = options[0] | |
665 entity_jid = jid.JID(jid_s).userhostJID() | |
666 assert(entity_jid.user) | |
667 assert(entity_jid.host) | |
668 except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError): | |
669 feedback = _(u"You must provide a valid JID to affiliate, like in '/affiliate contact@example.net member'") | |
670 self.text_cmds.feedBack(client, feedback, mess_data) | |
671 return False | |
672 | |
673 affiliation = options[1] if len(options) > 1 else 'none' | |
674 if affiliation not in AFFILIATIONS: | |
675 feedback = _(u"You must provide a valid affiliation: %s") % ' '.join(AFFILIATIONS) | |
676 self.text_cmds.feedBack(client, feedback, mess_data) | |
677 return False | |
678 | |
679 d = self.affiliate(client, entity_jid, mess_data["to"], {'affiliation': affiliation}) | |
680 | |
681 def cb(dummy): | |
682 feedback_msg = _(u'New affiliation for %(entity)s: %(affiliation)s').format(entity=entity_jid, affiliation=affiliation) | |
683 self.text_cmds.feedBack(client, feedback_msg, mess_data) | |
684 return True | |
685 d.addCallback(cb) | |
686 return d | |
687 | |
688 def cmd_title(self, client, mess_data): | |
689 """change room's subject | |
690 | |
691 @command (group): title | |
692 - title: new room subject | |
693 """ | |
694 subject = mess_data["unparsed"].strip() | |
695 | |
696 if subject: | |
697 room = mess_data["to"] | |
698 self.subject(client, room, subject) | |
699 | |
700 return False | |
701 | |
702 def cmd_topic(self, client, mess_data): | |
703 """just a synonym of /title | |
704 | |
705 @command (group): title | |
706 - title: new room subject | |
707 """ | |
708 return self.cmd_title(client, mess_data) | |
709 | |
710 def cmd_list(self, client, mess_data): | |
711 """list available rooms in a muc server | |
712 | |
713 @command (all): [MUC_SERVICE] | |
714 - MUC_SERVICE: service to request | |
715 empty value will request room's service for a room, | |
716 or user's server default MUC service in a one2one chat | |
717 """ | |
718 unparsed = mess_data["unparsed"].strip() | |
719 try: | |
720 service = jid.JID(unparsed) | |
721 except RuntimeError: | |
722 if mess_data['type'] == C.MESS_TYPE_GROUPCHAT: | |
723 room_jid = mess_data["to"] | |
724 service = jid.JID(room_jid.host) | |
725 elif client.muc_service is not None: | |
726 service = client.muc_service | |
727 else: | |
728 msg = D_(u"No known default MUC service".format(unparsed)) | |
729 self.text_cmds.feedBack(client, msg, mess_data) | |
730 return False | |
731 except jid.InvalidFormat: | |
732 msg = D_(u"{} is not a valid JID!".format(unparsed)) | |
733 self.text_cmds.feedBack(client, msg, mess_data) | |
734 return False | |
735 d = self.host.getDiscoItems(client, service) | |
736 d.addCallback(self._showListUI, client, service) | |
737 | |
738 return False | |
739 | |
740 def _whois(self, client, whois_msg, mess_data, target_jid): | |
741 """ Add MUC user information to whois """ | |
742 if mess_data['type'] != "groupchat": | |
743 return | |
744 if target_jid.userhostJID() not in client._muc_client.joined_rooms: | |
745 log.warning(_("This room has not been joined")) | |
746 return | |
747 if not target_jid.resource: | |
748 return | |
749 user = client._muc_client.joined_rooms[target_jid.userhostJID()].getUser(target_jid.resource) | |
750 whois_msg.append(_("Nickname: %s") % user.nick) | |
751 if user.entity: | |
752 whois_msg.append(_("Entity: %s") % user.entity) | |
753 if user.affiliation != 'none': | |
754 whois_msg.append(_("Affiliation: %s") % user.affiliation) | |
755 if user.role != 'none': | |
756 whois_msg.append(_("Role: %s") % user.role) | |
757 if user.status: | |
758 whois_msg.append(_("Status: %s") % user.status) | |
759 if user.show: | |
760 whois_msg.append(_("Show: %s") % user.show) | |
761 | |
762 def presenceTrigger(self, presence_elt, client): | |
763 # XXX: shouldn't it be done by the server ?!! | |
764 muc_client = client._muc_client | |
765 for room_jid, room in muc_client.joined_rooms.iteritems(): | |
766 elt = copy.deepcopy(presence_elt) | |
767 elt['to'] = room_jid.userhost() + '/' + room.nick | |
768 client.presence.send(elt) | |
769 return True | |
770 | |
771 | |
772 class SatMUCClient(muc.MUCClient): | |
773 implements(iwokkel.IDisco) | |
774 | |
775 def __init__(self, plugin_parent): | |
776 self.plugin_parent = plugin_parent | |
777 self.host = plugin_parent.host | |
778 muc.MUCClient.__init__(self) | |
779 self.rec_subjects = {} | |
780 self._changing_nicks = set() # used to keep trace of who is changing nick, | |
781 # and to discard userJoinedRoom signal in this case | |
782 print "init SatMUCClient OK" | |
783 | |
784 @property | |
785 def joined_rooms(self): | |
786 return self._rooms | |
787 | |
788 def _addRoom(self, room): | |
789 super(SatMUCClient, self)._addRoom(room) | |
790 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 | |
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 | |
794 room._history_d.callback(None) | |
795 # FIXME: check if history_d is not redundant with fully_joined | |
796 room.fully_joined = defer.Deferred() # called when everything is OK | |
797 room._cache = [] | |
798 | |
799 def _gotLastDbHistory(self, mess_data_list, room_jid, nick, password): | |
800 if mess_data_list: | |
801 timestamp = mess_data_list[0][1] | |
802 # we use seconds since last message to get backlog without duplicates | |
803 # and we remove 1 second to avoid getting the last message again | |
804 seconds = int(time.time() - timestamp) - 1 | |
805 else: | |
806 seconds = None | |
807 d = super(SatMUCClient, self).join(room_jid, nick, muc.HistoryOptions(seconds=seconds), password) | |
808 return d | |
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 | |
815 ## presence/roster ## | |
816 | |
817 def availableReceived(self, presence): | |
818 """ | |
819 Available presence was received. | |
820 """ | |
821 # XXX: we override MUCClient.availableReceived to fix bugs | |
822 # (affiliation and role are not set) | |
823 | |
824 room, user = self._getRoomUser(presence) | |
825 | |
826 if room is None: | |
827 return | |
828 | |
829 if user is None: | |
830 nick = presence.sender.resource | |
831 user = muc.User(nick, presence.entity) | |
832 | |
833 # Update user data | |
834 user.role = presence.role | |
835 user.affiliation = presence.affiliation | |
836 user.status = presence.status | |
837 user.show = presence.show | |
838 | |
839 if room.inRoster(user): | |
840 self.userUpdatedStatus(room, user, presence.show, presence.status) | |
841 else: | |
842 room.addUser(user) | |
843 self.userJoinedRoom(room, user) | |
844 | |
845 def unavailableReceived(self, presence): | |
846 # XXX: we override this method to manage nickname change | |
847 """ | |
848 Unavailable presence was received. | |
849 | |
850 If this was received from a MUC room occupant JID, that occupant has | |
851 left the room. | |
852 """ | |
853 room, user = self._getRoomUser(presence) | |
854 | |
855 if room is None or user is None: | |
856 return | |
857 | |
858 room.removeUser(user) | |
859 | |
860 if muc.STATUS_CODE.NEW_NICK in presence.mucStatuses: | |
861 self._changing_nicks.add(presence.nick) | |
862 self.userChangedNick(room, user, presence.nick) | |
863 else: | |
864 self._changing_nicks.discard(presence.nick) | |
865 self.userLeftRoom(room, user) | |
866 | |
867 def userJoinedRoom(self, room, user): | |
868 if user.nick == room.nick: | |
869 # we have received our own nick, this mean that the full room roster was received | |
870 room._roster_ok = True | |
871 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 | |
873 self.host.memory.updateEntityData(room.roomJID, C.ENTITY_TYPE, ENTITY_TYPE_MUC, profile_key=self.parent.profile) | |
874 elif not room._room_ok: | |
875 log.warning(u"Received user presence data in a room before its initialisation (and after our own presence)," | |
876 "this is not standard! Ignoring it: {} ({})".format( | |
877 room.roomJID.userhost(), | |
878 user.nick)) | |
879 return | |
880 elif room._roster_ok: | |
881 try: | |
882 self._changing_nicks.remove(user.nick) | |
883 except KeyError: | |
884 # 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())) | |
886 if not self.host.trigger.point("MUC user joined", room, user, self.parent.profile): | |
887 return | |
888 | |
889 extra = {'info_type': ROOM_USER_JOINED, | |
890 'user_affiliation': user.affiliation, | |
891 'user_role': user.role, | |
892 'user_nick': user.nick | |
893 } | |
894 if user.entity is not None: | |
895 extra['user_entity'] = user.entity.full() | |
896 mess_data = { # dict is similar to the one used in client.onMessage | |
897 "from": room.roomJID, | |
898 "to": self.parent.jid, | |
899 "uid": unicode(uuid.uuid4()), | |
900 "message": {'': D_(u"=> {} has joined the room").format(user.nick)}, | |
901 "subject": {}, | |
902 "type": C.MESS_TYPE_INFO, | |
903 "extra": extra, | |
904 "timestamp": time.time(), | |
905 } | |
906 self.parent.messageAddToHistory(mess_data) | |
907 self.parent.messageSendToBridge(mess_data) | |
908 | |
909 | |
910 def userLeftRoom(self, room, user): | |
911 if not self.host.trigger.point("MUC user left", room, user, self.parent.profile): | |
912 return | |
913 if user.nick == room.nick: | |
914 # we left the room | |
915 room_jid_s = room.roomJID.userhost() | |
916 log.info(_(u"Room ({room}) left ({profile})").format( | |
917 room = room_jid_s, profile = self.parent.profile)) | |
918 self.host.memory.delEntityCache(room.roomJID, profile_key=self.parent.profile) | |
919 self.host.bridge.mucRoomLeft(room.roomJID.userhost(), self.parent.profile) | |
920 elif not room._room_ok: | |
921 log.warning(u"Received user presence data in a room before its initialisation (and after our own presence)," | |
922 "this is not standard! Ignoring it: {} ({})".format( | |
923 room.roomJID.userhost(), | |
924 user.nick)) | |
925 return | |
926 else: | |
927 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, | |
929 'user_affiliation': user.affiliation, | |
930 'user_role': user.role, | |
931 'user_nick': user.nick | |
932 } | |
933 if user.entity is not None: | |
934 extra['user_entity'] = user.entity.full() | |
935 mess_data = { # dict is similar to the one used in client.onMessage | |
936 "from": room.roomJID, | |
937 "to": self.parent.jid, | |
938 "uid": unicode(uuid.uuid4()), | |
939 "message": {'': D_(u"<= {} has left the room").format(user.nick)}, | |
940 "subject": {}, | |
941 "type": C.MESS_TYPE_INFO, | |
942 "extra": extra, | |
943 "timestamp": time.time(), | |
944 } | |
945 self.parent.messageAddToHistory(mess_data) | |
946 self.parent.messageSendToBridge(mess_data) | |
947 | |
948 def userChangedNick(self, room, user, new_nick): | |
949 self.host.bridge.mucRoomUserChangedNick(room.roomJID.userhost(), user.nick, new_nick, self.parent.profile) | |
950 | |
951 def userUpdatedStatus(self, room, user, show, status): | |
952 self.host.bridge.presenceUpdate(room.roomJID.userhost() + '/' + user.nick, show or '', 0, {C.PRESENCE_STATUSES_DEFAULT: status or ''}, self.parent.profile) | |
953 | |
954 ## messages ## | |
955 | |
956 def receivedGroupChat(self, room, user, body): | |
957 log.debug(u'receivedGroupChat: room=%s user=%s body=%s' % (room.roomJID.full(), user, body)) | |
958 | |
959 def _addToHistory(self, dummy, user, message): | |
960 # we check if message is not in history | |
961 # and raise ConflictError else | |
962 stamp = message.delay.stamp.astimezone(tzutc()).timetuple() | |
963 timestamp = float(calendar.timegm(stamp)) | |
964 data = { # dict is similar to the one used in client.onMessage | |
965 "from": message.sender, | |
966 "to": message.recipient, | |
967 "uid": unicode(uuid.uuid4()), | |
968 "type": C.MESS_TYPE_GROUPCHAT, | |
969 "extra": {}, | |
970 "timestamp": timestamp, | |
971 "received_timestamp": unicode(time.time()), | |
972 } | |
973 # FIXME: message and subject don't handle xml:lang | |
974 data['message'] = {'': message.body} if message.body is not None else {} | |
975 data['subject'] = {'': message.subject} if message.subject is not None else {} | |
976 | |
977 if data['message'] or data['subject']: | |
978 return self.host.memory.addToHistory(self.parent, data) | |
979 else: | |
980 return defer.succeed(None) | |
981 | |
982 def _addToHistoryEb(self, failure): | |
983 failure.trap(exceptions.CancelError) | |
984 | |
985 def receivedHistory(self, room, user, message): | |
986 """Called when history (backlog) message are received | |
987 | |
988 we check if message is not already in our history | |
989 and add it if needed | |
990 @param room(muc.Room): room instance | |
991 @param user(muc.User, None): the user that sent the message | |
992 None if the message come from the room | |
993 @param message(muc.GroupChat): the parsed message | |
994 """ | |
995 room._history_d.addCallback(self._addToHistory, user, message) | |
996 room._history_d.addErrback(self._addToHistoryEb) | |
997 | |
998 ## subject ## | |
999 | |
1000 def groupChatReceived(self, message): | |
1001 """ | |
1002 A group chat message has been received from a MUC room. | |
1003 | |
1004 There are a few event methods that may get called here. | |
1005 L{receivedGroupChat}, L{receivedSubject} or L{receivedHistory}. | |
1006 """ | |
1007 # We override this method to fix subject handling | |
1008 # FIXME: remove this merge fixed upstream | |
1009 room, user = self._getRoomUser(message) | |
1010 | |
1011 if room is None: | |
1012 return | |
1013 | |
1014 if message.subject is not None: | |
1015 self.receivedSubject(room, user, message.subject) | |
1016 elif message.delay is None: | |
1017 self.receivedGroupChat(room, user, message) | |
1018 else: | |
1019 self.receivedHistory(room, user, message) | |
1020 | |
1021 def subject(self, room, subject): | |
1022 return muc.MUCClientProtocol.subject(self, room, subject) | |
1023 | |
1024 def _historyCb(self, dummy, room): | |
1025 args = self.plugin_parent._getRoomJoinedArgs(room, self.parent.profile) | |
1026 self.host.bridge.mucRoomJoined(*args) | |
1027 del room._history_d | |
1028 cache = room._cache | |
1029 del room._cache | |
1030 room._room_ok = True | |
1031 for elem in cache: | |
1032 self.parent.xmlstream.dispatch(elem) | |
1033 | |
1034 def _historyEb(self, failure_, room): | |
1035 log.error(u"Error while managing history: {}".format(failure_)) | |
1036 self._historyCb(None, room) | |
1037 | |
1038 def receivedSubject(self, room, user, subject): | |
1039 # when subject is received, we know that we have whole roster and history | |
1040 # cf. http://xmpp.org/extensions/xep-0045.html#enter-subject | |
1041 room.subject = subject # FIXME: subject doesn't handle xml:lang | |
1042 self.rec_subjects[room.roomJID.userhost()] = (room.roomJID.userhost(), subject) | |
1043 if room._room_ok is None: | |
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]) | |
1048 room.fully_joined.callback(room) | |
1049 else: | |
1050 # the subject has been changed | |
1051 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) | |
1053 | |
1054 ## disco ## | |
1055 | |
1056 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): | |
1057 return [disco.DiscoFeature(NS_MUC)] | |
1058 | |
1059 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | |
1060 # TODO: manage room queries ? Bad for privacy, must be disabled by default | |
1061 # see XEP-0045 § 6.7 | |
1062 return [] |