source: wokkel/muc.py

Last change on this file was 219:ae8b954e4ed9, checked in by Ralph Meijer <ralphm@…>, 2 years ago

Merge branch 'master' into affiliation_role_fix

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