source: wokkel/muc.py @ 151:fb6eaad60bb8

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

Reimplement RegisterRequest? by generalizing the form handling for room config.

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