source: wokkel/muc.py @ 156:d79ffefc3ac1

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

Add a simple MUC client example that responds to greetings, fix a typo.

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