source: wokkel/muc.py @ 144:6786830f3cd5

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

Cleanup some tests, add a few tests, fix subject.

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