Ignore:
Timestamp:
Aug 18, 2011, 3:22:45 PM (10 years ago)
Author:
Ralph Meijer <ralphm@…>
Branch:
wokkel-muc-client-support-24
Message:

Split protocol and room/user administration into two classes.

This introduces MUCClientProtocol, which only contains the protocol
implementation. The only state keeping there is remembering the Occupant JID
that was used to join a room, so you can use the Room JID everywhere.

The room and user owner administration that was intertwined with the protocol
is still in MUCClient that is a subclass of MUCClientProtocol. As this
record keeping barely had proper tests, the test coverage for this class is
sub-optimal for now.

This also fixes various test cases for the pure protocol support.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • wokkel/muc.py

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