source: wokkel/muc.py @ 162:8bffd58d7b6d

Last change on this file since 162:8bffd58d7b6d was 162:8bffd58d7b6d, checked in by Ralph Meijer <ralphm@…>, 9 years ago

Expose locked state of a room on join response with room created status.

When the presence in response to a join includes the room created status code
(201), the client should configure the room before it can be used. To enable
this, the locked state of the room is now exposed through the Room.locked
attribute.

Also, update example MUC client to deal with newly created, locked rooms.

File size: 43.0 KB
Line 
1# -*- test-case-name: wokkel.test.test_muc -*-
2#
3# Copyright (c) Ralph Meijer.
4# See LICENSE for details.
5
6"""
7XMPP Multi-User Chat protocol.
8
9This protocol is specified in
10U{XEP-0045<http://xmpp.org/extensions/xep-0045.html>}.
11"""
12from dateutil.tz import tzutc
13
14from zope.interface import implements
15
16from twisted.internet import defer
17from twisted.words.protocols.jabber import jid, error, xmlstream
18from twisted.words.xish import domish
19
20from wokkel import data_form, generic, iwokkel, xmppim
21from wokkel.compat import Values, ValueConstant
22from wokkel.delay import Delay, DelayMixin
23from wokkel.subprotocols import XMPPHandler
24from wokkel.iwokkel import IMUCClient
25
26# Multi User Chat namespaces
27NS_MUC = 'http://jabber.org/protocol/muc'
28NS_MUC_USER = NS_MUC + '#user'
29NS_MUC_ADMIN = NS_MUC + '#admin'
30NS_MUC_OWNER = NS_MUC + '#owner'
31NS_MUC_ROOMINFO = NS_MUC + '#roominfo'
32NS_MUC_CONFIG = NS_MUC + '#roomconfig'
33NS_MUC_REQUEST = NS_MUC + '#request'
34NS_MUC_REGISTER = NS_MUC + '#register'
35
36NS_REGISTER = 'jabber:iq:register'
37
38MESSAGE = '/message'
39PRESENCE = '/presence'
40
41GROUPCHAT = MESSAGE +'[@type="groupchat"]'
42
43DEFER_TIMEOUT = 30 # basic timeout is 30 seconds
44
45class STATUS_CODE(Values):
46    REALJID_PUBLIC = ValueConstant(100)
47    AFFILIATION_CHANGED = ValueConstant(101)
48    UNAVAILABLE_SHOWN = ValueConstant(102)
49    UNAVAILABLE_NOT_SHOWN = ValueConstant(103)
50    CONFIGURATION_CHANGED = ValueConstant(104)
51    SELF_PRESENCE = ValueConstant(110)
52    LOGGING_ENABLED = ValueConstant(170)
53    LOGGING_DISABLED = ValueConstant(171)
54    NON_ANONYMOUS = ValueConstant(172)
55    SEMI_ANONYMOUS = ValueConstant(173)
56    FULLY_ANONYMOUS = ValueConstant(174)
57    ROOM_CREATED = ValueConstant(201)
58    NICK_ASSIGNED = ValueConstant(210)
59    BANNED = ValueConstant(301)
60    NEW_NICK = ValueConstant(303)
61    KICKED = ValueConstant(307)
62    REMOVED_AFFILIATION = ValueConstant(321)
63    REMOVED_MEMBERSHIP = ValueConstant(322)
64    REMOVED_SHUTDOWN = ValueConstant(332)
65
66
67class Statuses(set):
68    """
69    Container of MUC status conditions.
70
71    This is currently implemented as a set of constant values from
72    L{STATUS_CODE}. Instances of this class provide L{IMUCStatuses}, that
73    defines the supported operations. Even though this class currently derives
74    from C{set}, future versions might not. This provides an upgrade path to
75    cater for extensible status conditions, as defined in
76    U{XEP-0306<http://xmpp.org/extensions/xep-0306.html>}.
77    """
78    implements(iwokkel.IMUCStatuses)
79
80
81
82class _FormRequest(generic.Request):
83    """
84    Base class for form exchange requests.
85    """
86    requestNamespace = None
87    formNamespace = None
88
89    def __init__(self, recipient, sender=None, options=None):
90        if options is None:
91            stanzaType = 'get'
92        else:
93            stanzaType = 'set'
94
95        generic.Request.__init__(self, recipient, sender, stanzaType)
96        self.options = options
97
98
99    def toElement(self):
100        element = generic.Request.toElement(self)
101
102        query = element.addElement((self.requestNamespace, 'query'))
103        if self.options is None:
104            # This is a request for the configuration form.
105            form = None
106        elif self.options is False:
107            form = data_form.Form(formType='cancel')
108        else:
109            form = data_form.Form(formType='submit',
110                                  formNamespace=self.formNamespace)
111            form.makeFields(self.options)
112
113        if form is not None:
114            query.addChild(form.toElement())
115
116        return element
117
118
119
120class ConfigureRequest(_FormRequest):
121    """
122    Configure MUC room request.
123
124    http://xmpp.org/extensions/xep-0045.html#roomconfig
125    """
126
127    requestNamespace = NS_MUC_OWNER
128    formNamespace = NS_MUC_CONFIG
129
130
131
132class RegisterRequest(_FormRequest):
133    """
134    Register request.
135
136    http://xmpp.org/extensions/xep-0045.html#register
137    """
138
139    requestNamespace = NS_REGISTER
140    formNamespace = NS_MUC_REGISTER
141
142
143
144class AdminItem(object):
145    """
146    Item representing role and/or affiliation for admin request.
147    """
148
149    def __init__(self, affiliation=None, role=None, entity=None, nick=None,
150                       reason=None):
151        self.affiliation = affiliation
152        self.role = role
153        self.entity = entity
154        self.nick = nick
155        self.reason = reason
156
157
158    def toElement(self):
159        element = domish.Element((NS_MUC_ADMIN, 'item'))
160
161        if self.entity:
162            element['jid'] = self.entity.full()
163
164        if self.nick:
165            element['nick'] = self.nick
166
167        if self.affiliation:
168            element['affiliation'] = self.affiliation
169
170        if self.role:
171            element['role'] = self.role
172
173        if self.reason:
174            element.addElement('reason', content=self.reason)
175
176        return element
177
178
179    @classmethod
180    def fromElement(Class, element):
181        item = Class()
182
183        if element.hasAttribute('jid'):
184            item.entity = jid.JID(element['jid'])
185
186        item.nick = element.getAttribute('nick')
187        item.affiliation = element.getAttribute('affiliation')
188        item.role = element.getAttribute('role')
189
190        for child in element.elements(NS_MUC_ADMIN, 'reason'):
191            item.reason = unicode(child)
192
193        return item
194
195
196
197class AdminStanza(generic.Request):
198    """
199    An admin request or response.
200    """
201
202    childParsers = {(NS_MUC_ADMIN, 'query'): '_childParser_query'}
203
204    def toElement(self):
205        element = generic.Request.toElement(self)
206        element.addElement((NS_MUC_ADMIN, 'query'))
207
208        if self.items:
209            for item in self.items:
210                element.query.addChild(item.toElement())
211
212        return element
213
214
215    def _childParser_query(self, element):
216        self.items = []
217        for child in element.elements(NS_MUC_ADMIN, 'item'):
218            self.items.append(AdminItem.fromElement(child))
219
220
221
222class DestructionRequest(generic.Request):
223    """
224    Room destruction request.
225
226    @param reason: Optional reason for the destruction of this room.
227    @type reason: C{unicode}.
228
229    @param alternate: Optional room JID of an alternate venue.
230    @type alternate: L{jid.JID}
231
232    @param password: Optional password for entering the alternate venue.
233    @type password: C{unicode}
234    """
235
236    stanzaType = 'set'
237
238    def __init__(self, recipient, sender=None, reason=None, alternate=None,
239                       password=None):
240        generic.Request.__init__(self, recipient, sender)
241        self.reason = reason
242        self.alternate = alternate
243        self.password = password
244
245
246    def toElement(self):
247        element = generic.Request.toElement(self)
248        element.addElement((NS_MUC_OWNER, 'query'))
249        element.query.addElement('destroy')
250
251        if self.alternate:
252            element.query.destroy['jid'] = self.alternate.full()
253
254            if self.password:
255                element.query.destroy.addElement('password',
256                                                 content=self.password)
257
258        if self.reason:
259            element.query.destroy.addElement('reason', content=self.reason)
260
261        return element
262
263
264
265class GroupChat(xmppim.Message, DelayMixin):
266    """
267    A groupchat message.
268    """
269
270    stanzaType = 'groupchat'
271
272    def toElement(self, legacyDelay=False):
273        """
274        Render into a domish Element.
275
276        @param legacyDelay: If C{True} send the delayed delivery information
277        in legacy format.
278        """
279        element = xmppim.Message.toElement(self)
280
281        if self.delay:
282            element.addChild(self.delay.toElement(legacy=legacyDelay))
283
284        return element
285
286
287
288class PrivateChat(xmppim.Message):
289    """
290    A chat message.
291    """
292
293    stanzaType = 'chat'
294
295
296
297class InviteMessage(xmppim.Message):
298
299    def __init__(self, recipient=None, sender=None, invitee=None, reason=None):
300        xmppim.Message.__init__(self, recipient, sender)
301        self.invitee = invitee
302        self.reason = reason
303
304
305    def toElement(self):
306        element = xmppim.Message.toElement(self)
307
308        child = element.addElement((NS_MUC_USER, 'x'))
309        child.addElement('invite')
310        child.invite['to'] = self.invitee.full()
311
312        if self.reason:
313            child.invite.addElement('reason', content=self.reason)
314
315        return element
316
317
318
319class HistoryOptions(object):
320    """
321    A history configuration object.
322
323    @ivar maxchars: Limit the total number of characters in the history to "X"
324        (where the character count is the characters of the complete XML
325        stanzas, not only their XML character data).
326    @type maxchars: C{int}
327
328    @ivar maxstanzas: Limit the total number of messages in the history to "X".
329    @type mazstanzas: C{int}
330
331    @ivar seconds: Send only the messages received in the last "X" seconds.
332    @type seconds: C{int}
333
334    @ivar since: Send only the messages received since the datetime specified.
335        Note that this must be an offset-aware instance.
336    @type since: L{datetime.datetime}
337    """
338    attributes = ['maxChars', 'maxStanzas', 'seconds', 'since']
339
340    def __init__(self, maxChars=None, maxStanzas=None, seconds=None,
341                       since=None):
342        self.maxChars = maxChars
343        self.maxStanzas = maxStanzas
344        self.seconds = seconds
345        self.since = since
346
347
348    def toElement(self):
349        """
350        Returns a L{domish.Element} representing the history options.
351        """
352        element = domish.Element((NS_MUC, 'history'))
353
354        for key in self.attributes:
355            value = getattr(self, key, None)
356            if value is not None:
357                if key == 'since':
358                    stamp = value.astimezone(tzutc())
359                    element[key] = stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
360                else:
361                    element[key.lower()] = str(value)
362
363        return element
364
365
366
367class BasicPresence(xmppim.AvailabilityPresence):
368    """
369    Availability presence sent from MUC client to service.
370
371    @type history: L{HistoryOptions}
372    """
373    history = None
374    password = None
375
376    def toElement(self):
377        element = xmppim.AvailabilityPresence.toElement(self)
378
379        muc = element.addElement((NS_MUC, 'x'))
380        if self.password:
381            muc.addElement('password', content=self.password)
382        if self.history:
383            muc.addChild(self.history.toElement())
384
385        return element
386
387
388
389class UserPresence(xmppim.AvailabilityPresence):
390    """
391    Availability presence sent from MUC service to client.
392
393    @ivar affiliation: Affiliation of the entity to the room.
394    @type affiliation: C{unicode}
395
396    @ivar role: Role of the entity in the room.
397    @type role: C{unicode}
398
399    @ivar entity: The real JID of the entity this presence is from.
400    @type entity: L{jid.JID}
401
402    @ivar mucStatuses: Set of one or more status codes from L{STATUS_CODE}.
403        See L{Statuses} for usage notes.
404    @type mucStatuses: L{Statuses}
405
406    @ivar nick: The nick name of the entity in the room.
407    @type nick: C{unicode}
408    """
409
410    affiliation = None
411    role = None
412    entity = None
413    nick = None
414
415    mucStatuses = None
416
417    childParsers = {(NS_MUC_USER, 'x'): '_childParser_mucUser'}
418
419    def __init__(self, *args, **kwargs):
420        self.mucStatuses = Statuses()
421        xmppim.AvailabilityPresence.__init__(self, *args, **kwargs)
422
423
424    def _childParser_mucUser(self, element):
425        """
426        Parse the MUC user extension element.
427        """
428        for child in element.elements():
429            if child.uri != NS_MUC_USER:
430                continue
431
432            elif child.name == 'status':
433                try:
434                    value = int(child.getAttribute('code'))
435                    statusCode = STATUS_CODE.lookupByValue(value)
436                except (TypeError, ValueError):
437                    continue
438
439                self.mucStatuses.add(statusCode)
440
441            elif child.name == 'item':
442                if child.hasAttribute('jid'):
443                    self.entity = jid.JID(child['jid'])
444
445                self.nick = child.getAttribute('nick')
446                self.affiliation = child.getAttribute('affiliation')
447                self.role = child.getAttribute('role')
448
449                for reason in child.elements(NS_MUC_ADMIN, 'reason'):
450                    self.reason = unicode(reason)
451
452            # TODO: destroy
453
454
455
456class VoiceRequest(xmppim.Message):
457    """
458    Voice request message.
459    """
460
461    def toElement(self):
462        element = xmppim.Message.toElement(self)
463
464        # build data form
465        form = data_form.Form('submit', formNamespace=NS_MUC_REQUEST)
466        form.addField(data_form.Field(var='muc#role',
467                                      value='participant',
468                                      label='Requested role'))
469        element.addChild(form.toElement())
470
471        return element
472
473
474
475class MUCClientProtocol(xmppim.BasePresenceProtocol):
476    """
477    Multi-User Chat client protocol.
478    """
479
480    timeout = None
481
482    presenceTypeParserMap = {
483                'error': generic.ErrorStanza,
484                'available': UserPresence,
485                'unavailable': UserPresence,
486                }
487
488    def __init__(self, reactor=None):
489        XMPPHandler.__init__(self)
490
491        if reactor:
492            self._reactor = reactor
493        else:
494            from twisted.internet import reactor
495            self._reactor = reactor
496
497
498    def connectionInitialized(self):
499        """
500        Called when the XML stream has been initialized.
501
502        It initializes several XPath events to handle MUC stanzas that come in.
503        After those are initialized the method L{initialized} is called to
504        signal that we have finished.
505        """
506        xmppim.BasePresenceProtocol.connectionInitialized(self)
507        self.xmlstream.addObserver(GROUPCHAT, self._onGroupChat)
508        self._roomOccupantMap = {}
509
510
511    def _onGroupChat(self, element):
512        """
513        A group chat message has been received from a MUC room.
514
515        There are a few event methods that may get called here.
516        L{receivedGroupChat}, L{receivedSubject} or L{receivedHistory}.
517        """
518        message = GroupChat.fromElement(element)
519        self.groupChatReceived(message)
520
521
522    def groupChatReceived(self, message):
523        """
524        Called when a groupchat message was received.
525
526        This method is called with a parsed representation of a received
527        groupchat message and can be overridden for further processing.
528
529        For regular groupchat message, the C{body} attribute contains the
530        message body. Conversation history sent by the room upon joining, will
531        have the C{delay} attribute set, room subject changes the C{subject}
532        attribute. See L{GroupChat} for details.
533
534        @param message: Groupchat message.
535        @type message: L{GroupChat}
536        """
537        pass
538
539
540    def _sendDeferred(self, stanza):
541        """
542        Send presence stanza, adding a deferred with a timeout.
543
544        @param stanza: The presence stanza to send over the wire.
545        @type stanza: L{generic.Stanza}
546
547        @param timeout: The number of seconds to wait before the deferred is
548            timed out.
549        @type timeout: L{int}
550
551        The deferred object L{defer.Deferred} is returned.
552        """
553        def onResponse(element):
554            if element.getAttribute('type') == 'error':
555                d.errback(error.exceptionFromStanza(element))
556            else:
557                d.callback(UserPresence.fromElement(element))
558
559        def onTimeout():
560            d.errback(xmlstream.TimeoutError("Timeout waiting for response."))
561
562        def cancelTimeout(result):
563            if call.active():
564                call.cancel()
565
566            return result
567
568        def recordOccupant(presence):
569            occupantJID = presence.sender
570            roomJID = occupantJID.userhostJID()
571            self._roomOccupantMap[roomJID] = occupantJID
572            return presence
573
574        call = self._reactor.callLater(DEFER_TIMEOUT, onTimeout)
575
576        d = defer.Deferred()
577        d.addBoth(cancelTimeout)
578        d.addCallback(recordOccupant)
579
580        query = "/presence[@from='%s' or (@from='%s' and @type='error')]" % (
581                stanza.recipient.full(), stanza.recipient.userhost())
582        self.xmlstream.addOnetimeObserver(query, onResponse, priority=-1)
583        self.xmlstream.send(stanza.toElement())
584        return d
585
586
587    def join(self, roomJID, nick, historyOptions=None, password=None):
588        """
589        Join a MUC room by sending presence to it.
590
591        @param roomJID: The JID of the room the entity is joining.
592        @type roomJID: L{jid.JID}
593
594        @param nick: The nick name for the entitity joining the room.
595        @type nick: C{unicode}
596
597        @param historyOptions: Options for conversation history sent by the
598            room upon joining.
599        @type historyOptions: L{HistoryOptions}
600
601        @param password: Optional password for the room.
602        @type password: C{unicode}
603
604        @return: A deferred that fires when the entity is in the room or an
605                 error has occurred.
606        """
607        occupantJID = jid.JID(tuple=(roomJID.user, roomJID.host, nick))
608
609        presence = BasicPresence(recipient=occupantJID)
610        if password:
611            presence.password = password
612        if historyOptions:
613            presence.history = historyOptions
614
615        return self._sendDeferred(presence)
616
617
618    def nick(self, roomJID, nick):
619        """
620        Change an entity's nick name in a MUC room.
621
622        See: http://xmpp.org/extensions/xep-0045.html#changenick
623
624        @param roomJID: The JID of the room.
625        @type roomJID: L{jid.JID}
626
627        @param nick: The new nick name within the room.
628        @type nick: C{unicode}
629        """
630        occupantJID = jid.JID(tuple=(roomJID.user, roomJID.host, nick))
631        presence = BasicPresence(recipient=occupantJID)
632        return self._sendDeferred(presence)
633
634
635    def status(self, roomJID, show=None, status=None):
636        """
637        Change user status.
638
639        See: http://xmpp.org/extensions/xep-0045.html#changepres
640
641        @param roomJID: The Room JID of the room.
642        @type roomJID: L{jid.JID}
643
644        @param show: The availability of the entity. Common values are xa,
645            available, etc
646        @type show: C{unicode}
647
648        @param status: The current status of the entity.
649        @type status: C{unicode}
650        """
651        occupantJID = self._roomOccupantMap[roomJID]
652        presence = BasicPresence(recipient=occupantJID, show=show,
653                                 status=status)
654        return self._sendDeferred(presence)
655
656
657    def leave(self, roomJID):
658        """
659        Leave a MUC room.
660
661        See: http://xmpp.org/extensions/xep-0045.html#exit
662
663        @param roomJID: The JID of the room.
664        @type roomJID: L{jid.JID}
665        """
666        occupantJID = self._roomOccupantMap[roomJID]
667        presence = xmppim.AvailabilityPresence(recipient=occupantJID,
668                                               available=False)
669
670        return self._sendDeferred(presence)
671
672
673    def groupChat(self, roomJID, body):
674        """
675        Send a groupchat message.
676        """
677        message = GroupChat(recipient=roomJID, body=body)
678        self.send(message.toElement())
679
680
681    def chat(self, occupantJID, body):
682        """
683        Send a private chat message to a user in a MUC room.
684
685        See: http://xmpp.org/extensions/xep-0045.html#privatemessage
686
687        @param occupantJID: The Room JID of the other user.
688        @type occupantJID: L{jid.JID}
689        """
690        message = PrivateChat(recipient=occupantJID, body=body)
691        self.send(message.toElement())
692
693
694    def subject(self, roomJID, subject):
695        """
696        Change the subject of a MUC room.
697
698        See: http://xmpp.org/extensions/xep-0045.html#subject-mod
699
700        @param roomJID: The bare JID of the room.
701        @type roomJID: L{jid.JID}
702
703        @param subject: The subject you want to set.
704        @type subject: C{unicode}
705        """
706        message = GroupChat(roomJID.userhostJID(), subject=subject)
707        self.send(message.toElement())
708
709
710    def invite(self, roomJID, invitee, reason=None):
711        """
712        Invite a xmpp entity to a MUC room.
713
714        See: http://xmpp.org/extensions/xep-0045.html#invite
715
716        @param roomJID: The bare JID of the room.
717        @type roomJID: L{jid.JID}
718
719        @param invitee: The entity that is being invited.
720        @type invitee: L{jid.JID}
721
722        @param reason: The reason for the invite.
723        @type reason: C{unicode}
724        """
725        message = InviteMessage(recipient=roomJID, invitee=invitee,
726                                reason=reason)
727        self.send(message.toElement())
728
729
730    def getRegisterForm(self, roomJID):
731        """
732        Grab the registration form for a MUC room.
733
734        @param room: The room jabber/xmpp entity id for the requested
735            registration form.
736        @type room: L{jid.JID}
737        """
738        def cb(response):
739            form = data_form.findForm(response.query, NS_MUC_REGISTER)
740            return form
741
742        request = RegisterRequest(recipient=roomJID, options=None)
743        d = self.request(request)
744        d.addCallback(cb)
745        return d
746
747
748    def register(self, roomJID, options):
749        """
750        Send a request to register for a room.
751
752        @param roomJID: The bare JID of the room.
753        @type roomJID: L{jid.JID}
754
755        @param options: A mapping of field names to values, or C{None} to
756            cancel.
757        @type options: C{dict}
758        """
759        if options is None:
760            options = False
761        request = RegisterRequest(recipient=roomJID, options=options)
762        return self.request(request)
763
764
765    def voice(self, roomJID):
766        """
767        Request voice for a moderated room.
768
769        @param roomJID: The room jabber/xmpp entity id.
770        @type roomJID: L{jid.JID}
771        """
772        message = VoiceRequest(recipient=roomJID)
773        self.xmlstream.send(message.toElement())
774
775
776    def history(self, roomJID, messages):
777        """
778        Send history to create a MUC based on a one on one chat.
779
780        See: http://xmpp.org/extensions/xep-0045.html#continue
781
782        @param roomJID: The room jabber/xmpp entity id.
783        @type roomJID: L{jid.JID}
784
785        @param messages: The history to send to the room as an ordered list of
786                         message, represented by a dictionary with the keys
787                         C{'stanza'}, holding the original stanza a
788                         L{domish.Element}, and C{'timestamp'} with the
789                         timestamp.
790        @type messages: L{list} of L{domish.Element}
791        """
792
793        for message in messages:
794            stanza = message['stanza']
795            stanza['type'] = 'groupchat'
796
797            delay = Delay(stamp=message['timestamp'])
798
799            sender = stanza.getAttribute('from')
800            if sender is not None:
801                delay.sender = jid.JID(sender)
802
803            stanza.addChild(delay.toElement())
804
805            stanza['to'] = roomJID.userhost()
806            if stanza.hasAttribute('from'):
807                del stanza['from']
808
809            self.xmlstream.send(stanza)
810
811
812    def getConfiguration(self, roomJID):
813        """
814        Grab the configuration from the room.
815
816        This sends an iq request to the room.
817
818        @param roomJID: The bare JID of the room.
819        @type roomJID: L{jid.JID}
820
821        @return: A deferred that fires with the room's configuration form as
822            a L{data_form.Form} or C{None} if there are no configuration
823            options available.
824        """
825        def cb(response):
826            form = data_form.findForm(response.query, NS_MUC_CONFIG)
827            return form
828
829        request = ConfigureRequest(recipient=roomJID, options=None)
830        d = self.request(request)
831        d.addCallback(cb)
832        return d
833
834
835    def configure(self, roomJID, options):
836        """
837        Configure a room.
838
839        @param roomJID: The room to configure.
840        @type roomJID: L{jid.JID}
841
842        @param options: A mapping of field names to values, or C{None} to
843            cancel.
844        @type options: C{dict}
845        """
846        if options is None:
847            options = False
848        request = ConfigureRequest(recipient=roomJID, options=options)
849        return self.request(request)
850
851
852    def _getAffiliationList(self, roomJID, affiliation):
853        """
854        Send a request for an affiliation list in a room.
855        """
856        def cb(response):
857            stanza = AdminStanza.fromElement(response)
858            return stanza.items
859
860        request = AdminStanza(recipient=roomJID, stanzaType='get')
861        request.items = [AdminItem(affiliation=affiliation)]
862        d = self.request(request)
863        d.addCallback(cb)
864        return d
865
866
867    def _getRoleList(self, roomJID, role):
868        """
869        Send a request for a role list in a room.
870        """
871        def cb(response):
872            stanza = AdminStanza.fromElement(response)
873            return stanza.items
874
875        request = AdminStanza(recipient=roomJID, stanzaType='get')
876        request.items = [AdminItem(role=role)]
877        d = self.request(request)
878        d.addCallback(cb)
879        return d
880
881
882    def getMemberList(self, roomJID):
883        """
884        Get the member list of a room.
885
886        @param roomJID: The bare JID of the room.
887        @type roomJID: L{jid.JID}
888        """
889        return self._getAffiliationList(roomJID, 'member')
890
891
892    def getAdminList(self, roomJID):
893        """
894        Get the admin list of a room.
895
896        @param roomJID: The bare JID of the room.
897        @type roomJID: L{jid.JID}
898        """
899        return self._getAffiliationList(roomJID, 'admin')
900
901
902    def getBanList(self, roomJID):
903        """
904        Get an outcast list from a room.
905
906        @param roomJID: The bare JID of the room.
907        @type roomJID: L{jid.JID}
908        """
909        return self._getAffiliationList(roomJID, 'outcast')
910
911
912    def getOwnerList(self, roomJID):
913        """
914        Get an owner list from a room.
915
916        @param roomJID: The bare JID of the room.
917        @type roomJID: L{jid.JID}
918        """
919        return self._getAffiliationList(roomJID, 'owner')
920
921
922    def getModeratorList(self, roomJID):
923        """
924        Get the moderator list of a room.
925
926        @param roomJID: The bare JID of the room.
927        @type roomJID: L{jid.JID}
928        """
929        d = self._getRoleList(roomJID, 'moderator')
930        return d
931
932
933    def _setAffiliation(self, roomJID, entity, affiliation,
934                              reason=None, sender=None):
935        """
936        Send a request to change an entity's affiliation to a MUC room.
937        """
938        request = AdminStanza(recipient=roomJID, sender=sender,
939                               stanzaType='set')
940        item = AdminItem(entity=entity, affiliation=affiliation, reason=reason)
941        request.items = [item]
942        return self.request(request)
943
944
945    def _setRole(self, roomJID, nick, role,
946                       reason=None, sender=None):
947        """
948        Send a request to change an occupant's role in a MUC room.
949        """
950        request = AdminStanza(recipient=roomJID, sender=sender,
951                               stanzaType='set')
952        item = AdminItem(nick=nick, role=role, reason=reason)
953        request.items = [item]
954        return self.request(request)
955
956
957    def modifyAffiliationList(self, roomJID, entities, affiliation,
958                                    sender=None):
959        """
960        Modify an affiliation list.
961
962        @param roomJID: The bare JID of the room.
963        @type roomJID: L{jid.JID}
964
965        @param entities: The list of entities to change for a room.
966        @type entities: L{list} of L{jid.JID}
967
968        @param affiliation: The affilation to the entities will acquire.
969        @type affiliation: C{unicode}
970
971        @param sender: The entity sending the request.
972        @type sender: L{jid.JID}
973
974        """
975        request = AdminStanza(recipient=roomJID, sender=sender,
976                               stanzaType='set')
977        request.items = [AdminItem(entity=entity, affiliation=affiliation)
978                         for entity in entities]
979
980        return self.request(request)
981
982
983    def grantVoice(self, roomJID, nick, reason=None, sender=None):
984        """
985        Grant voice to an entity.
986
987        @param roomJID: The bare JID of the room.
988        @type roomJID: L{jid.JID}
989
990        @param nick: The nick name for the user in this room.
991        @type nick: C{unicode}
992
993        @param reason: The reason for granting voice to the entity.
994        @type reason: C{unicode}
995
996        @param sender: The entity sending the request.
997        @type sender: L{jid.JID}
998        """
999        return self._setRole(roomJID, nick=nick,
1000                             role='participant',
1001                             reason=reason, sender=sender)
1002
1003
1004    def revokeVoice(self, roomJID, nick, reason=None, sender=None):
1005        """
1006        Revoke voice from a participant.
1007
1008        This will disallow the entity to send messages to a moderated room.
1009
1010        @param roomJID: The bare JID of the room.
1011        @type roomJID: L{jid.JID}
1012
1013        @param nick: The nick name for the user in this room.
1014        @type nick: C{unicode}
1015
1016        @param reason: The reason for revoking voice from the entity.
1017        @type reason: C{unicode}
1018
1019        @param sender: The entity sending the request.
1020        @type sender: L{jid.JID}
1021        """
1022        return self._setRole(roomJID, nick=nick, role='visitor',
1023                             reason=reason, sender=sender)
1024
1025
1026    def grantModerator(self, roomJID, nick, reason=None, sender=None):
1027        """
1028        Grant moderator privileges to a MUC room.
1029
1030        @param roomJID: The bare JID of the room.
1031        @type roomJID: L{jid.JID}
1032
1033        @param nick: The nick name for the user in this room.
1034        @type nick: C{unicode}
1035
1036        @param reason: The reason for granting moderation to the entity.
1037        @type reason: C{unicode}
1038
1039        @param sender: The entity sending the request.
1040        @type sender: L{jid.JID}
1041        """
1042        return self._setRole(roomJID, nick=nick, role='moderator',
1043                             reason=reason, sender=sender)
1044
1045
1046    def ban(self, roomJID, entity, reason=None, sender=None):
1047        """
1048        Ban a user from a MUC room.
1049
1050        @param roomJID: The bare JID of the room.
1051        @type roomJID: L{jid.JID}
1052
1053        @param entity: The bare JID of the entity to be banned.
1054        @type entity: L{jid.JID}
1055
1056        @param reason: The reason for banning the entity.
1057        @type reason: C{unicode}
1058
1059        @param sender: The entity sending the request.
1060        @type sender: L{jid.JID}
1061        """
1062        return self._setAffiliation(roomJID, entity, 'outcast',
1063                                    reason=reason, sender=sender)
1064
1065
1066    def kick(self, roomJID, nick, reason=None, sender=None):
1067        """
1068        Kick a user from a MUC room.
1069
1070        @param roomJID: The bare JID of the room.
1071        @type roomJID: L{jid.JID}
1072
1073        @param nick: The occupant to be banned.
1074        @type nick: C{unicode}
1075
1076        @param reason: The reason given for the kick.
1077        @type reason: C{unicode}
1078
1079        @param sender: The entity sending the request.
1080        @type sender: L{jid.JID}
1081        """
1082        return self._setRole(roomJID, nick, 'none',
1083                             reason=reason, sender=sender)
1084
1085
1086    def destroy(self, roomJID, reason=None, alternate=None, password=None):
1087        """
1088        Destroy a room.
1089
1090        @param roomJID: The JID of the room.
1091        @type roomJID: L{jid.JID}
1092
1093        @param reason: The reason for the destruction of the room.
1094        @type reason: C{unicode}
1095
1096        @param alternate: The JID of the room suggested as an alternate venue.
1097        @type alternate: L{jid.JID}
1098
1099        """
1100        request = DestructionRequest(recipient=roomJID, reason=reason,
1101                                     alternate=alternate, password=password)
1102
1103        return self.request(request)
1104
1105
1106
1107class User(object):
1108    """
1109    A user/entity in a multi-user chat room.
1110    """
1111
1112    def __init__(self, nick, entity=None):
1113        self.nick = nick
1114        self.entity = entity
1115        self.affiliation = 'none'
1116        self.role = 'none'
1117
1118        self.status = None
1119        self.show = None
1120
1121
1122
1123class Room(object):
1124    """
1125    A Multi User Chat Room.
1126
1127    An in memory object representing a MUC room from the perspective of
1128    a client.
1129
1130    @ivar roomJID: The Room JID of the MUC room.
1131    @type roomJID: L{JID}
1132
1133    @ivar nick: The nick name for the client in this room.
1134    @type nick: C{unicode}
1135
1136    @ivar occupantJID: The JID of the occupant in the room. Generated from
1137        roomJID and nick.
1138    @type occupantJID: L{jid.JID}
1139
1140    @ivar locked: Flag signalling a locked room. A locked room first needs
1141        to be configured before it can be used. See
1142        L{MUCClientProtocol.getConfiguration} and
1143        L{MUCClientProtocol.configure}.
1144    @type locked: C{bool}
1145    """
1146
1147    locked = False
1148
1149    def __init__(self, roomJID, nick):
1150        """
1151        Initialize the room.
1152        """
1153        self.roomJID = roomJID
1154        self.setNick(nick)
1155        self.roster = {}
1156
1157
1158    def setNick(self, nick):
1159        self.occupantJID = jid.internJID(u"%s/%s" % (self.roomJID, nick))
1160        self.nick = nick
1161
1162
1163    def addUser(self, user):
1164        """
1165        Add a user to the room roster.
1166
1167        @param user: The user object that is being added to the room.
1168        @type user: L{User}
1169        """
1170        self.roster[user.nick] = user
1171
1172
1173    def inRoster(self, user):
1174        """
1175        Check if a user is in the MUC room.
1176
1177        @param user: The user object to check.
1178        @type user: L{User}
1179        """
1180
1181        return user.nick in self.roster
1182
1183
1184    def getUser(self, nick):
1185        """
1186        Get a user from the room's roster.
1187
1188        @param nick: The nick for the user in the MUC room.
1189        @type nick: C{unicode}
1190        """
1191        return self.roster.get(nick)
1192
1193
1194    def removeUser(self, user):
1195        """
1196        Remove a user from the MUC room's roster.
1197
1198        @param user: The user object to check.
1199        @type user: L{User}
1200        """
1201        if self.inRoster(user):
1202            del self.roster[user.nick]
1203
1204
1205
1206class MUCClient(MUCClientProtocol):
1207    """
1208    Multi-User Chat client protocol.
1209
1210    This is a subclass of L{XMPPHandler} and implements L{IMUCCLient}.
1211
1212    @ivar _rooms: Collection of occupied rooms, keyed by the bare JID of the
1213                  room. Note that a particular entity can only join a room once
1214                  at a time.
1215    @type _rooms: C{dict}
1216    """
1217
1218    implements(IMUCClient)
1219
1220    def __init__(self, reactor=None):
1221        MUCClientProtocol.__init__(self, reactor)
1222
1223        self._rooms = {}
1224
1225
1226    def _addRoom(self, room):
1227        """
1228        Add a room to the room collection.
1229
1230        Rooms are stored by the JID of the room itself. I.e. it uses the Room
1231        ID and service parts of the Room JID.
1232
1233        @note: An entity can only join a particular room once.
1234        """
1235        roomJID = room.occupantJID.userhostJID()
1236        self._rooms[roomJID] = room
1237
1238
1239    def _getRoom(self, roomJID):
1240        """
1241        Grab a room from the room collection.
1242
1243        This uses the Room ID and service parts of the given JID to look up
1244        the L{Room} instance associated with it.
1245
1246        @type occupantJID: L{jid.JID}
1247        """
1248        return self._rooms.get(roomJID)
1249
1250
1251    def _removeRoom(self, roomJID):
1252        """
1253        Delete a room from the room collection.
1254        """
1255        if roomJID in self._rooms:
1256            del self._rooms[roomJID]
1257
1258
1259    def _getRoomUser(self, stanza):
1260        """
1261        Lookup the room and user associated with the stanza's sender.
1262        """
1263        occupantJID = stanza.sender
1264
1265        if not occupantJID:
1266            return None, None
1267
1268        # when a user leaves a room we need to update it
1269        room = self._getRoom(occupantJID.userhostJID())
1270        if room is None:
1271            # not in the room yet
1272            return None, None
1273
1274        # Check if user is in roster
1275        nick = occupantJID.resource
1276        user = room.getUser(nick)
1277
1278        return room, user
1279
1280
1281    def unavailableReceived(self, presence):
1282        """
1283        Unavailable presence was received.
1284
1285        If this was received from a MUC room occupant JID, that occupant has
1286        left the room.
1287        """
1288
1289        room, user = self._getRoomUser(presence)
1290
1291        if room is None or user is None:
1292            return
1293
1294        room.removeUser(user)
1295        self.userLeftRoom(room, user)
1296
1297
1298    def availableReceived(self, presence):
1299        """
1300        Available presence was received.
1301        """
1302
1303        room, user = self._getRoomUser(presence)
1304
1305        if room is None:
1306            return
1307
1308        if user is None:
1309            nick = presence.sender.resource
1310            user = User(nick, presence.entity)
1311
1312        # Update user status
1313        user.status = presence.status
1314        user.show = presence.show
1315
1316        if room.inRoster(user):
1317            self.userUpdatedStatus(room, user, presence.show, presence.status)
1318        else:
1319            room.addUser(user)
1320            self.userJoinedRoom(room, user)
1321
1322
1323    def groupChatReceived(self, message):
1324        """
1325        A group chat message has been received from a MUC room.
1326
1327        There are a few event methods that may get called here.
1328        L{receivedGroupChat}, L{receivedSubject} or L{receivedHistory}.
1329        """
1330        room, user = self._getRoomUser(message)
1331
1332        if room is None:
1333            return
1334
1335        if message.subject:
1336            self.receivedSubject(room, user, message.subject)
1337        elif message.delay is None:
1338            self.receivedGroupChat(room, user, message)
1339        else:
1340            self.receivedHistory(room, user, message)
1341
1342
1343    def userJoinedRoom(self, room, user):
1344        """
1345        User has joined a MUC room.
1346
1347        This method will need to be modified inorder for clients to
1348        do something when this event occurs.
1349
1350        @param room: The room the user has joined.
1351        @type room: L{Room}
1352
1353        @param user: The user that joined the MUC room.
1354        @type user: L{User}
1355        """
1356        pass
1357
1358
1359    def userLeftRoom(self, room, user):
1360        """
1361        User has left a room.
1362
1363        This method will need to be modified inorder for clients to
1364        do something when this event occurs.
1365
1366        @param room: The room the user has joined.
1367        @type room: L{Room}
1368
1369        @param user: The user that left the MUC room.
1370        @type user: L{User}
1371        """
1372        pass
1373
1374
1375    def userUpdatedStatus(self, room, user, show, status):
1376        """
1377        User Presence has been received.
1378
1379        This method will need to be modified inorder for clients to
1380        do something when this event occurs.
1381        """
1382        pass
1383
1384
1385    def receivedSubject(self, room, user, subject):
1386        """
1387        A (new) room subject has been received.
1388
1389        This method will need to be modified inorder for clients to
1390        do something when this event occurs.
1391        """
1392        pass
1393
1394
1395    def receivedGroupChat(self, room, user, message):
1396        """
1397        A groupchat message was received.
1398
1399        @param room: The room the message was received from.
1400        @type room: L{Room}
1401
1402        @param user: The user that sent the message, or C{None} if it was a
1403            message from the room itself.
1404        @type user: L{User}
1405
1406        @param message: The message.
1407        @type message: L{GroupChat}
1408        """
1409        pass
1410
1411
1412    def receivedHistory(self, room, user, message):
1413        """
1414        A groupchat message from the room's discussion history was received.
1415
1416        This is identical to L{receivedGroupChat}, with the delayed delivery
1417        information (timestamp and original sender) in C{message.delay}. For
1418        anonymous rooms, C{message.delay.sender} is the room's address.
1419
1420        @param room: The room the message was received from.
1421        @type room: L{Room}
1422
1423        @param user: The user that sent the message, or C{None} if it was a
1424            message from the room itself.
1425        @type user: L{User}
1426
1427        @param message: The message.
1428        @type message: L{GroupChat}
1429        """
1430        pass
1431
1432
1433    def join(self, roomJID, nick, historyOptions=None,
1434                   password=None):
1435        """
1436        Join a MUC room by sending presence to it.
1437
1438        @param roomJID: The JID of the room the entity is joining.
1439        @type roomJID: L{jid.JID}
1440
1441        @param nick: The nick name for the entitity joining the room.
1442        @type nick: C{unicode}
1443
1444        @param historyOptions: Options for conversation history sent by the
1445            room upon joining.
1446        @type historyOptions: L{HistoryOptions}
1447
1448        @param password: Optional password for the room.
1449        @type password: C{unicode}
1450
1451        @return: A deferred that fires with the room when the entity is in the
1452            room, or with a failure if an error has occurred.
1453        """
1454        def cb(presence):
1455            """
1456            We have presence that says we joined a room.
1457            """
1458            if STATUS_CODE.ROOM_CREATED in presence.mucStatuses:
1459                room.locked = True
1460
1461            return room
1462
1463        def eb(failure):
1464            self._removeRoom(roomJID)
1465            return failure
1466
1467        room = Room(roomJID, nick)
1468        self._addRoom(room)
1469
1470        d = MUCClientProtocol.join(self, roomJID, nick, historyOptions,
1471                                         password)
1472        d.addCallbacks(cb, eb)
1473        return d
1474
1475
1476    def nick(self, roomJID, nick):
1477        """
1478        Change an entity's nick name in a MUC room.
1479
1480        See: http://xmpp.org/extensions/xep-0045.html#changenick
1481
1482        @param roomJID: The JID of the room, i.e. without a resource.
1483        @type roomJID: L{jid.JID}
1484
1485        @param nick: The new nick name within the room.
1486        @type nick: C{unicode}
1487        """
1488        def cb(presence):
1489            # Presence confirmation, change the nickname.
1490            room.setNick(nick)
1491            return room
1492
1493        room = self._getRoom(roomJID)
1494
1495        d = MUCClientProtocol.nick(self, roomJID, nick)
1496        d.addCallback(cb)
1497        return d
1498
1499
1500    def leave(self, roomJID):
1501        """
1502        Leave a MUC room.
1503
1504        See: http://xmpp.org/extensions/xep-0045.html#exit
1505
1506        @param roomJID: The Room JID of the room to leave.
1507        @type roomJID: L{jid.JID}
1508        """
1509        def cb(presence):
1510            self._removeRoom(roomJID)
1511
1512        d = MUCClientProtocol.leave(self, roomJID)
1513        d.addCallback(cb)
1514        return d
1515
1516
1517    def status(self, roomJID, show=None, status=None):
1518        """
1519        Change user status.
1520
1521        See: http://xmpp.org/extensions/xep-0045.html#changepres
1522
1523        @param roomJID: The Room JID of the room.
1524        @type roomJID: L{jid.JID}
1525
1526        @param show: The availability of the entity. Common values are xa,
1527            available, etc
1528        @type show: C{unicode}
1529
1530        @param status: The current status of the entity.
1531        @type status: C{unicode}
1532        """
1533        room = self._getRoom(roomJID)
1534        d = MUCClientProtocol.status(self, roomJID, show, status)
1535        d.addCallback(lambda _: room)
1536        return d
1537
1538
1539    def destroy(self, roomJID, reason=None, alternate=None, password=None):
1540        """
1541        Destroy a room.
1542
1543        @param roomJID: The JID of the room.
1544        @type roomJID: L{jid.JID}
1545
1546        @param reason: The reason for the destruction of the room.
1547        @type reason: C{unicode}
1548
1549        @param alternate: The JID of the room suggested as an alternate venue.
1550        @type alternate: L{jid.JID}
1551
1552        """
1553        def destroyed(iq):
1554            self._removeRoom(roomJID)
1555
1556        d = MUCClientProtocol.destroy(self, roomJID, reason, alternate)
1557        d.addCallback(destroyed)
1558        return d
Note: See TracBrowser for help on using the repository browser.