source: wokkel/muc.py @ 149:83ba20efd1de

wokkel-muc-client-support-24
Last change on this file since 149:83ba20efd1de was 149:83ba20efd1de, checked in by Ralph Meijer <ralphm@…>, 11 years ago

Interface, docstring cleanups.

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