source: wokkel/muc.py @ 147:aa2cfee614e7

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

Use generic.Request for room destruction request, add TestableStreamManager?.

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