source: wokkel/muc.py @ 166:d9c10a5b5c0d

Last change on this file since 166:d9c10a5b5c0d was 166:d9c10a5b5c0d, checked in by Ralph Meijer <ralphm@…>, 8 years ago

Documentation fixes for pydoctor.

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