source: wokkel/muc.py @ 152:ae3907d81abc

wokkel-muc-client-support-24
Last change on this file since 152:ae3907d81abc was 152:ae3907d81abc, checked in by Ralph Meijer <ralphm@…>, 9 years ago

Split protocol and room/user administration into two classes.

This introduces MUCClientProtocol, which only contains the protocol
implementation. The only state keeping there is remembering the Occupant JID
that was used to join a room, so you can use the Room JID everywhere.

The room and user owner administration that was intertwined with the protocol
is still in MUCClient that is a subclass of MUCClientProtocol. As this
record keeping barely had proper tests, the test coverage for this class is
sub-optimal for now.

This also fixes various test cases for the pure protocol support.

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