source: wokkel/muc.py @ 138:8332717e2739

wokkel-muc-client-support-24
Last change on this file since 138:8332717e2739 was 138:8332717e2739, checked in by Goffi <goffi@…>, 9 years ago

Replace custom error classes by StanzaError?.

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