source: wokkel/muc.py @ 145:216c953d8ecd

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

Add more tests, remove code redundancy and cleanup variable names.

This adds tests for timeouts in response to joins, nick and presence changes,
error responses from the room JID instead of the occupant JID and receiving
room subject changes.

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