source: wokkel/muc.py @ 148:75cf9e14b776

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

Redo room configuration.

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