source: wokkel/muc.py @ 150:2f7237d9c537

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

Redo admin requests using generic.Request.

This changes all role and affiliation management functionality to use a
subclass of generic.Request.

The methods for retrieving affiliation or role lists now return lists of
AdminItems, instead of modifying a Room instance and returning that. This
is because affiliation changes can also be done when not in the room.

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