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 []