source: wokkel/muc.py @ 160:33ef849a77d8

Last change on this file since 160:33ef849a77d8 was 160:33ef849a77d8, checked in by Ralph Meijer <ralphm@…>, 9 years ago

Use symbolic constants instead of integers MUC status code.

Instead of using normal constant values for representing MUC status codes,
UserPresence now uses twisted.python.constants to define these. This
makes code better to understand and helps debugging.

If a user presence includes one or more status codes, they are stored in the
mucStatuses attribute, as an instance of Statuses. This replaces the
former statusCodes attribute.

File size: 42.7 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 not self.options:
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        request = RegisterRequest(recipient=roomJID, options=options)
760        return self.request(request)
761
762
763    def voice(self, roomJID):
764        """
765        Request voice for a moderated room.
766
767        @param roomJID: The room jabber/xmpp entity id.
768        @type roomJID: L{jid.JID}
769        """
770        message = VoiceRequest(recipient=roomJID)
771        self.xmlstream.send(message.toElement())
772
773
774    def history(self, roomJID, messages):
775        """
776        Send history to create a MUC based on a one on one chat.
777
778        See: http://xmpp.org/extensions/xep-0045.html#continue
779
780        @param roomJID: The room jabber/xmpp entity id.
781        @type roomJID: L{jid.JID}
782
783        @param messages: The history to send to the room as an ordered list of
784                         message, represented by a dictionary with the keys
785                         C{'stanza'}, holding the original stanza a
786                         L{domish.Element}, and C{'timestamp'} with the
787                         timestamp.
788        @type messages: L{list} of L{domish.Element}
789        """
790
791        for message in messages:
792            stanza = message['stanza']
793            stanza['type'] = 'groupchat'
794
795            delay = Delay(stamp=message['timestamp'])
796
797            sender = stanza.getAttribute('from')
798            if sender is not None:
799                delay.sender = jid.JID(sender)
800
801            stanza.addChild(delay.toElement())
802
803            stanza['to'] = roomJID.userhost()
804            if stanza.hasAttribute('from'):
805                del stanza['from']
806
807            self.xmlstream.send(stanza)
808
809
810    def getConfiguration(self, roomJID):
811        """
812        Grab the configuration from the room.
813
814        This sends an iq request to the room.
815
816        @param roomJID: The bare JID of the room.
817        @type roomJID: L{jid.JID}
818
819        @return: A deferred that fires with the room's configuration form as
820            a L{data_form.Form} or C{None} if there are no configuration
821            options available.
822        """
823        def cb(response):
824            form = data_form.findForm(response.query, NS_MUC_CONFIG)
825            return form
826
827        request = ConfigureRequest(recipient=roomJID, options=None)
828        d = self.request(request)
829        d.addCallback(cb)
830        return d
831
832
833    def configure(self, roomJID, options):
834        """
835        Configure a room.
836
837        @param roomJID: The room to configure.
838        @type roomJID: L{jid.JID}
839
840        @param options: A mapping of field names to values, or C{None} to cancel.
841        @type options: C{dict}
842        """
843        if not options:
844            options = False
845        request = ConfigureRequest(recipient=roomJID, options=options)
846        return self.request(request)
847
848
849    def _getAffiliationList(self, roomJID, affiliation):
850        """
851        Send a request for an affiliation list in a room.
852        """
853        def cb(response):
854            stanza = AdminStanza.fromElement(response)
855            return stanza.items
856
857        request = AdminStanza(recipient=roomJID, stanzaType='get')
858        request.items = [AdminItem(affiliation=affiliation)]
859        d = self.request(request)
860        d.addCallback(cb)
861        return d
862
863
864    def _getRoleList(self, roomJID, role):
865        """
866        Send a request for a role list in a room.
867        """
868        def cb(response):
869            stanza = AdminStanza.fromElement(response)
870            return stanza.items
871
872        request = AdminStanza(recipient=roomJID, stanzaType='get')
873        request.items = [AdminItem(role=role)]
874        d = self.request(request)
875        d.addCallback(cb)
876        return d
877
878
879    def getMemberList(self, roomJID):
880        """
881        Get the member list of a room.
882
883        @param roomJID: The bare JID of the room.
884        @type roomJID: L{jid.JID}
885        """
886        return self._getAffiliationList(roomJID, 'member')
887
888
889    def getAdminList(self, roomJID):
890        """
891        Get the admin list of a room.
892
893        @param roomJID: The bare JID of the room.
894        @type roomJID: L{jid.JID}
895        """
896        return self._getAffiliationList(roomJID, 'admin')
897
898
899    def getBanList(self, roomJID):
900        """
901        Get an outcast list from a room.
902
903        @param roomJID: The bare JID of the room.
904        @type roomJID: L{jid.JID}
905        """
906        return self._getAffiliationList(roomJID, 'outcast')
907
908
909    def getOwnerList(self, roomJID):
910        """
911        Get an owner list from a room.
912
913        @param roomJID: The bare JID of the room.
914        @type roomJID: L{jid.JID}
915        """
916        return self._getAffiliationList(roomJID, 'owner')
917
918
919    def getModeratorList(self, roomJID):
920        """
921        Get the moderator list of a room.
922
923        @param roomJID: The bare JID of the room.
924        @type roomJID: L{jid.JID}
925        """
926        d = self._getRoleList(roomJID, 'moderator')
927        return d
928
929
930    def _setAffiliation(self, roomJID, entity, affiliation,
931                              reason=None, sender=None):
932        """
933        Send a request to change an entity's affiliation to a MUC room.
934        """
935        request = AdminStanza(recipient=roomJID, sender=sender,
936                               stanzaType='set')
937        item = AdminItem(entity=entity, affiliation=affiliation, reason=reason)
938        request.items = [item]
939        return self.request(request)
940
941
942    def _setRole(self, roomJID, nick, role,
943                       reason=None, sender=None):
944        """
945        Send a request to change an occupant's role in a MUC room.
946        """
947        request = AdminStanza(recipient=roomJID, sender=sender,
948                               stanzaType='set')
949        item = AdminItem(nick=nick, role=role, reason=reason)
950        request.items = [item]
951        return self.request(request)
952
953
954    def modifyAffiliationList(self, roomJID, entities, affiliation,
955                                    sender=None):
956        """
957        Modify an affiliation list.
958
959        @param roomJID: The bare JID of the room.
960        @type roomJID: L{jid.JID}
961
962        @param entities: The list of entities to change for a room.
963        @type entities: L{list} of L{jid.JID}
964
965        @param affiliation: The affilation to the entities will acquire.
966        @type affiliation: C{unicode}
967
968        @param sender: The entity sending the request.
969        @type sender: L{jid.JID}
970
971        """
972        request = AdminStanza(recipient=roomJID, sender=sender,
973                               stanzaType='set')
974        request.items = [AdminItem(entity=entity, affiliation=affiliation)
975                         for entity in entities]
976
977        return self.request(request)
978
979
980    def grantVoice(self, roomJID, nick, reason=None, sender=None):
981        """
982        Grant voice to an entity.
983
984        @param roomJID: The bare JID of the room.
985        @type roomJID: L{jid.JID}
986
987        @param nick: The nick name for the user in this room.
988        @type nick: C{unicode}
989
990        @param reason: The reason for granting voice to the entity.
991        @type reason: C{unicode}
992
993        @param sender: The entity sending the request.
994        @type sender: L{jid.JID}
995        """
996        return self._setRole(roomJID, nick=nick,
997                             role='participant',
998                             reason=reason, sender=sender)
999
1000
1001    def revokeVoice(self, roomJID, nick, reason=None, sender=None):
1002        """
1003        Revoke voice from a participant.
1004
1005        This will disallow the entity to send messages to a moderated room.
1006
1007        @param roomJID: The bare JID of the room.
1008        @type roomJID: L{jid.JID}
1009
1010        @param nick: The nick name for the user in this room.
1011        @type nick: C{unicode}
1012
1013        @param reason: The reason for revoking voice from the entity.
1014        @type reason: C{unicode}
1015
1016        @param sender: The entity sending the request.
1017        @type sender: L{jid.JID}
1018        """
1019        return self._setRole(roomJID, nick=nick, role='visitor',
1020                             reason=reason, sender=sender)
1021
1022
1023    def grantModerator(self, roomJID, nick, reason=None, sender=None):
1024        """
1025        Grant moderator privileges to a MUC room.
1026
1027        @param roomJID: The bare JID of the room.
1028        @type roomJID: L{jid.JID}
1029
1030        @param nick: The nick name for the user in this room.
1031        @type nick: C{unicode}
1032
1033        @param reason: The reason for granting moderation to the entity.
1034        @type reason: C{unicode}
1035
1036        @param sender: The entity sending the request.
1037        @type sender: L{jid.JID}
1038        """
1039        return self._setRole(roomJID, nick=nick, role='moderator',
1040                             reason=reason, sender=sender)
1041
1042
1043    def ban(self, roomJID, entity, reason=None, sender=None):
1044        """
1045        Ban a user from a MUC room.
1046
1047        @param roomJID: The bare JID of the room.
1048        @type roomJID: L{jid.JID}
1049
1050        @param entity: The bare JID of the entity to be banned.
1051        @type entity: L{jid.JID}
1052
1053        @param reason: The reason for banning the entity.
1054        @type reason: C{unicode}
1055
1056        @param sender: The entity sending the request.
1057        @type sender: L{jid.JID}
1058        """
1059        return self._setAffiliation(roomJID, entity, 'outcast',
1060                                    reason=reason, sender=sender)
1061
1062
1063    def kick(self, roomJID, nick, reason=None, sender=None):
1064        """
1065        Kick a user from a MUC room.
1066
1067        @param roomJID: The bare JID of the room.
1068        @type roomJID: L{jid.JID}
1069
1070        @param nick: The occupant to be banned.
1071        @type nick: C{unicode}
1072
1073        @param reason: The reason given for the kick.
1074        @type reason: C{unicode}
1075
1076        @param sender: The entity sending the request.
1077        @type sender: L{jid.JID}
1078        """
1079        return self._setRole(roomJID, nick, 'none',
1080                             reason=reason, sender=sender)
1081
1082
1083    def destroy(self, roomJID, reason=None, alternate=None, password=None):
1084        """
1085        Destroy a room.
1086
1087        @param roomJID: The JID of the room.
1088        @type roomJID: L{jid.JID}
1089
1090        @param reason: The reason for the destruction of the room.
1091        @type reason: C{unicode}
1092
1093        @param alternate: The JID of the room suggested as an alternate venue.
1094        @type alternate: L{jid.JID}
1095
1096        """
1097        request = DestructionRequest(recipient=roomJID, reason=reason,
1098                                     alternate=alternate, password=password)
1099
1100        return self.request(request)
1101
1102
1103
1104class User(object):
1105    """
1106    A user/entity in a multi-user chat room.
1107    """
1108
1109    def __init__(self, nick, entity=None):
1110        self.nick = nick
1111        self.entity = entity
1112        self.affiliation = 'none'
1113        self.role = 'none'
1114
1115        self.status = None
1116        self.show = None
1117
1118
1119
1120class Room(object):
1121    """
1122    A Multi User Chat Room.
1123
1124    An in memory object representing a MUC room from the perspective of
1125    a client.
1126
1127    @ivar roomJID: The Room JID of the MUC room.
1128    @type roomJID: L{JID}
1129
1130    @ivar nick: The nick name for the client in this room.
1131    @type nick: C{unicode}
1132
1133    @ivar state: The status code of the room.
1134    @type state: L{int}
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
1141
1142    def __init__(self, roomJID, nick, state=None):
1143        """
1144        Initialize the room.
1145        """
1146        self.roomJID = roomJID
1147        self.setNick(nick)
1148        self.state = state
1149
1150        self.status = 0
1151
1152        self.roster = {}
1153
1154
1155    def setNick(self, nick):
1156        self.occupantJID = jid.internJID(u"%s/%s" % (self.roomJID, nick))
1157        self.nick = nick
1158
1159
1160    def addUser(self, user):
1161        """
1162        Add a user to the room roster.
1163
1164        @param user: The user object that is being added to the room.
1165        @type user: L{User}
1166        """
1167        self.roster[user.nick] = user
1168
1169
1170    def inRoster(self, user):
1171        """
1172        Check if a user is in the MUC room.
1173
1174        @param user: The user object to check.
1175        @type user: L{User}
1176        """
1177
1178        return user.nick in self.roster
1179
1180
1181    def getUser(self, nick):
1182        """
1183        Get a user from the room's roster.
1184
1185        @param nick: The nick for the user in the MUC room.
1186        @type nick: C{unicode}
1187        """
1188        return self.roster.get(nick)
1189
1190
1191    def removeUser(self, user):
1192        """
1193        Remove a user from the MUC room's roster.
1194
1195        @param user: The user object to check.
1196        @type user: L{User}
1197        """
1198        if self.inRoster(user):
1199            del self.roster[user.nick]
1200
1201
1202
1203class MUCClient(MUCClientProtocol):
1204    """
1205    Multi-User Chat client protocol.
1206
1207    This is a subclass of L{XMPPHandler} and implements L{IMUCCLient}.
1208
1209    @ivar _rooms: Collection of occupied rooms, keyed by the bare JID of the
1210                  room. Note that a particular entity can only join a room once
1211                  at a time.
1212    @type _rooms: C{dict}
1213    """
1214
1215    implements(IMUCClient)
1216
1217    def __init__(self, reactor=None):
1218        MUCClientProtocol.__init__(self, reactor)
1219
1220        self._rooms = {}
1221
1222
1223    def _addRoom(self, room):
1224        """
1225        Add a room to the room collection.
1226
1227        Rooms are stored by the JID of the room itself. I.e. it uses the Room
1228        ID and service parts of the Room JID.
1229
1230        @note: An entity can only join a particular room once.
1231        """
1232        roomJID = room.occupantJID.userhostJID()
1233        self._rooms[roomJID] = room
1234
1235
1236    def _getRoom(self, roomJID):
1237        """
1238        Grab a room from the room collection.
1239
1240        This uses the Room ID and service parts of the given JID to look up
1241        the L{Room} instance associated with it.
1242
1243        @type occupantJID: L{jid.JID}
1244        """
1245        return self._rooms.get(roomJID)
1246
1247
1248    def _removeRoom(self, roomJID):
1249        """
1250        Delete a room from the room collection.
1251        """
1252        if roomJID in self._rooms:
1253            del self._rooms[roomJID]
1254
1255
1256    def _getRoomUser(self, stanza):
1257        """
1258        Lookup the room and user associated with the stanza's sender.
1259        """
1260        occupantJID = stanza.sender
1261
1262        if not occupantJID:
1263            return None, None
1264
1265        # when a user leaves a room we need to update it
1266        room = self._getRoom(occupantJID.userhostJID())
1267        if room is None:
1268            # not in the room yet
1269            return None, None
1270
1271        # Check if user is in roster
1272        nick = occupantJID.resource
1273        user = room.getUser(nick)
1274
1275        return room, user
1276
1277
1278    def unavailableReceived(self, presence):
1279        """
1280        Unavailable presence was received.
1281
1282        If this was received from a MUC room occupant JID, that occupant has
1283        left the room.
1284        """
1285
1286        room, user = self._getRoomUser(presence)
1287
1288        if room is None or user is None:
1289            return
1290
1291        room.removeUser(user)
1292        self.userLeftRoom(room, user)
1293
1294
1295    def availableReceived(self, presence):
1296        """
1297        Available presence was received.
1298        """
1299
1300        room, user = self._getRoomUser(presence)
1301
1302        if room is None:
1303            return
1304
1305        if user is None:
1306            nick = presence.sender.resource
1307            user = User(nick, presence.entity)
1308
1309        # Update user status
1310        user.status = presence.status
1311        user.show = presence.show
1312
1313        if room.inRoster(user):
1314            self.userUpdatedStatus(room, user, presence.show, presence.status)
1315        else:
1316            room.addUser(user)
1317            self.userJoinedRoom(room, user)
1318
1319
1320    def groupChatReceived(self, message):
1321        """
1322        A group chat message has been received from a MUC room.
1323
1324        There are a few event methods that may get called here.
1325        L{receivedGroupChat}, L{receivedSubject} or L{receivedHistory}.
1326        """
1327        room, user = self._getRoomUser(message)
1328
1329        if room is None:
1330            return
1331
1332        if message.subject:
1333            self.receivedSubject(room, user, message.subject)
1334        elif message.delay is None:
1335            self.receivedGroupChat(room, user, message)
1336        else:
1337            self.receivedHistory(room, user, message)
1338
1339
1340    def userJoinedRoom(self, room, user):
1341        """
1342        User has joined a MUC room.
1343
1344        This method will need to be modified inorder for clients to
1345        do something when this event occurs.
1346
1347        @param room: The room the user has joined.
1348        @type room: L{Room}
1349
1350        @param user: The user that joined the MUC room.
1351        @type user: L{User}
1352        """
1353        pass
1354
1355
1356    def userLeftRoom(self, room, user):
1357        """
1358        User has left a room.
1359
1360        This method will need to be modified inorder for clients to
1361        do something when this event occurs.
1362
1363        @param room: The room the user has joined.
1364        @type room: L{Room}
1365
1366        @param user: The user that left the MUC room.
1367        @type user: L{User}
1368        """
1369        pass
1370
1371
1372    def userUpdatedStatus(self, room, user, show, status):
1373        """
1374        User Presence has been received.
1375
1376        This method will need to be modified inorder for clients to
1377        do something when this event occurs.
1378        """
1379        pass
1380
1381
1382    def receivedSubject(self, room, user, subject):
1383        """
1384        A (new) room subject has been received.
1385
1386        This method will need to be modified inorder for clients to
1387        do something when this event occurs.
1388        """
1389        pass
1390
1391
1392    def receivedGroupChat(self, room, user, message):
1393        """
1394        A groupchat message was received.
1395
1396        @param room: The room the message was received from.
1397        @type room: L{Room}
1398
1399        @param user: The user that sent the message, or C{None} if it was a
1400            message from the room itself.
1401        @type user: L{User}
1402
1403        @param message: The message.
1404        @type message: L{GroupChat}
1405        """
1406        pass
1407
1408
1409    def receivedHistory(self, room, user, message):
1410        """
1411        A groupchat message from the room's discussion history was received.
1412
1413        This is identical to L{receivedGroupChat}, with the delayed delivery
1414        information (timestamp and original sender) in C{message.delay}. For
1415        anonymous rooms, C{message.delay.sender} is the room's address.
1416
1417        @param room: The room the message was received from.
1418        @type room: L{Room}
1419
1420        @param user: The user that sent the message, or C{None} if it was a
1421            message from the room itself.
1422        @type user: L{User}
1423
1424        @param message: The message.
1425        @type message: L{GroupChat}
1426        """
1427        pass
1428
1429
1430    def join(self, roomJID, nick, historyOptions=None,
1431                   password=None):
1432        """
1433        Join a MUC room by sending presence to it.
1434
1435        @param roomJID: The JID of the room the entity is joining.
1436        @type roomJID: L{jid.JID}
1437
1438        @param nick: The nick name for the entitity joining the room.
1439        @type nick: C{unicode}
1440
1441        @param historyOptions: Options for conversation history sent by the
1442            room upon joining.
1443        @type historyOptions: L{HistoryOptions}
1444
1445        @param password: Optional password for the room.
1446        @type password: C{unicode}
1447
1448        @return: A deferred that fires with the room when the entity is in the
1449            room, or with a failure if an error has occurred.
1450        """
1451        def cb(presence):
1452            """
1453            We have presence that says we joined a room.
1454            """
1455            room.state = 'joined'
1456            return room
1457
1458        def eb(failure):
1459            self._removeRoom(roomJID)
1460            return failure
1461
1462        room = Room(roomJID, nick, state='joining')
1463        self._addRoom(room)
1464
1465        d = MUCClientProtocol.join(self, roomJID, nick, historyOptions,
1466                                         password)
1467        d.addCallbacks(cb, eb)
1468        return d
1469
1470
1471    def nick(self, roomJID, nick):
1472        """
1473        Change an entity's nick name in a MUC room.
1474
1475        See: http://xmpp.org/extensions/xep-0045.html#changenick
1476
1477        @param roomJID: The JID of the room, i.e. without a resource.
1478        @type roomJID: L{jid.JID}
1479
1480        @param nick: The new nick name within the room.
1481        @type nick: C{unicode}
1482        """
1483        def cb(presence):
1484            # Presence confirmation, change the nickname.
1485            room.setNick(nick)
1486            return room
1487
1488        room = self._getRoom(roomJID)
1489
1490        d = MUCClientProtocol.nick(self, roomJID, nick)
1491        d.addCallback(cb)
1492        return d
1493
1494
1495    def leave(self, roomJID):
1496        """
1497        Leave a MUC room.
1498
1499        See: http://xmpp.org/extensions/xep-0045.html#exit
1500
1501        @param roomJID: The Room JID of the room to leave.
1502        @type roomJID: L{jid.JID}
1503        """
1504        def cb(presence):
1505            self._removeRoom(roomJID)
1506
1507        d = MUCClientProtocol.leave(self, roomJID)
1508        d.addCallback(cb)
1509        return d
1510
1511
1512    def status(self, roomJID, show=None, status=None):
1513        """
1514        Change user status.
1515
1516        See: http://xmpp.org/extensions/xep-0045.html#changepres
1517
1518        @param roomJID: The Room JID of the room.
1519        @type roomJID: L{jid.JID}
1520
1521        @param show: The availability of the entity. Common values are xa,
1522            available, etc
1523        @type show: C{unicode}
1524
1525        @param status: The current status of the entity.
1526        @type status: C{unicode}
1527        """
1528        room = self._getRoom(roomJID)
1529        d = MUCClientProtocol.status(self, roomJID, show, status)
1530        d.addCallback(lambda _: room)
1531        return d
1532
1533
1534    def destroy(self, roomJID, reason=None, alternate=None, password=None):
1535        """
1536        Destroy a room.
1537
1538        @param roomJID: The JID of the room.
1539        @type roomJID: L{jid.JID}
1540
1541        @param reason: The reason for the destruction of the room.
1542        @type reason: C{unicode}
1543
1544        @param alternate: The JID of the room suggested as an alternate venue.
1545        @type alternate: L{jid.JID}
1546
1547        """
1548        def destroyed(iq):
1549            self._removeRoom(roomJID)
1550
1551        d = MUCClientProtocol.destroy(self, roomJID, reason, alternate)
1552        d.addCallback(destroyed)
1553        return d
Note: See TracBrowser for help on using the repository browser.