source: wokkel/muc.py @ 125:edb21c02197b

wokkel-muc-client-support-24
Last change on this file since 125:edb21c02197b was 125:edb21c02197b, checked in by Christopher Zorn <tofu@…>, 11 years ago

docstrings, tests and bug fixes

File size: 37.0 KB
Line 
1# -*- test-case-name: wokkel.test.test_muc -*-
2#
3# Copyright (c) 2003-2008 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 disco, data_form, shim, xmppim
21from wokkel.subprotocols import IQHandlerMixin, 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# ad hoc commands
40NS_AD_HOC       = "http://jabber.org/protocol/commands"
41
42
43# Iq get and set XPath queries
44IQ     = '/iq'
45IQ_GET = IQ+'[@type="get"]'
46IQ_SET = IQ+'[@type="set"]'
47
48IQ_RESULT = IQ+'[@type="result"]'
49IQ_ERROR  = IQ+'[@type="error"]'
50
51IQ_QUERY     = IQ+'/query'
52IQ_GET_QUERY = IQ_GET + '/query'
53IQ_SET_QUERY = IQ_SET + '/query'
54
55IQ_COMMAND   = IQ+'/command'
56
57MUC_ADMIN = IQ_QUERY+'[@xmlns="' + NS_MUC_ADMIN + '"]'
58MUC_OWNER = IQ_QUERY+'[@xmlns="' + NS_MUC_OWNER + '"]'
59
60MUC_AO = MUC_ADMIN + '|' + MUC_OWNER
61
62
63MESSAGE  = '/message'
64PRESENCE = '/presence'
65
66CHAT_BODY = MESSAGE +'[@type="chat"]/body'
67CHAT      = MESSAGE +'[@type="chat"]'
68
69GROUPCHAT     = MESSAGE +'[@type="groupchat"]/body'
70SUBJECT       = MESSAGE +'[@type="groupchat"]/subject'
71MESSAGE_ERROR = MESSAGE +'[@type="error"]'
72
73STATUS_CODES = { # see http://www.xmpp.org/extensions/xep-0045.html#registrar-statuscodes
74    100:
75        {'name':'fulljid',
76         'stanza':'presence',
77         
78         },
79    201: 
80        {'name':'created', 
81         'stanza': 'presence',
82         'context':'Entering a room',
83         'purpose':'Inform user that a new room has been created'
84         },   
85}
86
87STATUS_CODE_CREATED = 201
88
89DEFER_TIMEOUT = 30 # basic timeout is 30 seconds
90
91class JidMalformed(Exception):
92    """
93    A jid malformed error from the server.
94
95   
96    """
97    condition    = 'modify'
98    mucCondition = 'jid-malformed'
99   
100class NotAuthorized(Exception):
101    """
102    """
103    condition    = 'auth'
104    mucCondition = 'not-authorized'
105
106class RegistrationRequired(Exception):
107    """
108    """
109    condition    = 'auth'
110    mucCondition = 'registration-required'
111
112
113class Forbidden(Exception):
114    """
115    """
116    condition    = 'auth'
117    mucCondition = 'forbidden'
118
119class Conflict(Exception):
120    """
121    """
122    condition    = 'cancel'
123    mucCondition = 'conflict'
124
125class NotFound(Exception):
126    """
127    """
128    condition    = 'cancel'
129    mucCondition = 'not-found'
130
131class ServiceUnavailable(Exception):
132    """
133    """
134    condition    = 'wait'
135    mucCondition = 'service-unavailable'
136
137
138
139MUC_EXCEPTIONS = {
140    'jid-malformed': JidMalformed,
141    'forbidden': Forbidden,
142    'not-authorized': NotAuthorized,
143    'exception': Exception,
144    'conflict': Conflict,
145    'service-unavailable': ServiceUnavailable,
146    'not-found': NotFound,
147    }
148
149class MUCError(error.StanzaError):
150    """
151    Exception with muc specific condition.
152    """
153    def __init__(self, condition, mucCondition, type='error', feature=None, text=None):
154        appCondition = domish.Element((NS_MUC, mucCondition))
155        if feature:
156            appCondition['feature'] = feature
157        error.StanzaError.__init__(self, condition,
158                                         type=type,
159                                         text=text,
160                                         appCondition=appCondition)
161
162
163class BadRequest(MUCError):
164    """
165    Bad request stanza error.
166    """
167    def __init__(self, mucCondition=None, text=None):
168        MUCError.__init__(self, 'bad-request', mucCondition, text)
169
170
171
172class Unsupported(MUCError):
173    def __init__(self, feature, text=None):
174        MUCError.__init__(self, 'feature-not-implemented',
175                          'unsupported',
176                          feature,
177                          text)
178
179
180
181class ConfigureRequest(xmlstream.IQ):
182    """
183    Configure MUC room request.
184
185    http://xmpp.org/extensions/xep-0045.html#roomconfig
186
187    @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'}
188    @type method: C{str}
189    """
190
191    def __init__(self, xs, method='get', fields=[]):
192        xmlstream.IQ.__init__(self, xs, method)
193        q = self.addElement((NS_MUC_OWNER, 'query'))
194        if method == 'set':
195            # build data form
196            form = data_form.Form('submit', formNamespace=NS_MUC_CONFIG)
197            q.addChild(form.toElement())
198           
199            for f in fields:
200                # create a field
201                form.addField(f)
202
203
204class RegisterRequest(xmlstream.IQ):
205    """
206    Register room request.
207
208    @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'}
209    @type method: C{str}
210
211    """
212
213    def __init__(self, xs, method='get', fields=[]):
214        xmlstream.IQ.__init__(self, xs, method)
215        q = self.addElement((NS_REQUEST, 'query'))
216        if method == 'set':
217            # build data form
218            form_type = 'submit'       
219            form = data_form.Form(form_type, formNamespace=NS_MUC_REGISTER)
220            q.addChild(form.toElement())       
221           
222            for f in fields:
223                # create a field
224                form.addField(f)
225
226
227class AdminRequest(xmlstream.IQ):
228    """
229    A basic admin iq request
230
231    @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'}
232    @type method: C{str}
233
234    """
235
236    def __init__(self, xs, method='get'):
237        xmlstream.IQ.__init__(self, xs, method)
238        q = self.addElement((NS_MUC_ADMIN, 'query'))
239
240
241class OwnerRequest(xmlstream.IQ):
242    """
243    A basic owner iq request
244
245    @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'}
246    @type method: C{str}
247
248    """
249
250    def __init__(self, xs, method='get'):
251        xmlstream.IQ.__init__(self, xs, method)
252        q = self.addElement((NS_MUC_OWNER, 'query'))
253
254   
255
256class AffiliationRequest(AdminRequest):
257    """
258    Register room request.
259
260    @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'}
261    @type method: C{str}
262
263    @ivar affiliation: The affiliation type to send to room.
264    @type affiliation: C{str}
265
266    """
267
268    def __init__(self, xs, method='get', affiliation='none', a_jid=None, reason=None):
269        AdminRequest.__init__(self, xs, method)
270
271        i = self.query.addElement('item')
272
273        i['affiliation'] = affiliation
274        if a_jid:
275            i['jid'] = a_jid.full()
276           
277        if reason:
278            i.addElement('reason', None, reason)
279
280           
281class RoleRequest(AdminRequest):
282    def __init__(self, xs, method='get', role='none', a_jid=None, reason=None):
283        AdminRequest.__init__(self, xs, method)
284        i = self.query.addElement('item')
285
286        i['role'] = role
287        if a_jid:
288            i['jid'] = a_jid.full()
289           
290        if reason:
291            i.addElement('reason', None, reason)
292       
293class GroupChat(domish.Element):
294    """
295    """
296    def __init__(self, to, body=None, subject=None, frm=None):
297        """To needs to be a string
298        """
299        domish.Element.__init__(self, (None, 'message'))
300        self['type'] = 'groupchat'
301        if isinstance(to, jid.JID):
302            self['to'] = to.userhost()
303        else:
304            self['to'] = to
305        if frm:
306            self['from'] = frm
307        if body:
308            self.addElement('body',None, body)
309        if subject:
310            self.addElement('subject',None, subject)
311
312
313class PrivateChat(domish.Element):
314    """
315    """
316    def __init__(self, to, body=None, frm=None):
317        """To needs to be a string
318        """
319        domish.Element.__init__(self, (None, 'message'))
320        self['type'] = 'chat'
321        self['to']   = to
322        if frm:
323            self['from'] = frm
324        if body:
325            self.addElement('body',None, body)
326           
327class InviteMessage(PrivateChat):
328    def __init__(self, to, reason=None, full_jid=None, body=None, frm=None, password=None):
329        PrivateChat.__init__(self, to, body=body, frm=frm)
330        del self['type'] # remove type
331        x = self.addElement('x', NS_MUC_USER)
332        invite = x.addElement('invite')
333        if full_jid:
334            invite['to'] = full_jid
335        if reason:
336            invite.addElement('reason', None, reason)
337        if password:
338            invite.addElement('password', None, password)
339
340class HistoryMessage(GroupChat):
341    """
342    """
343    def __init__(self, to, stamp, body=None, subject=None, frm=None, h_frm=None):
344        GroupChat.__init__(self, to, body=body, subject=subject, frm=frm)
345        d = self.addElement('delay', NS_DELAY)
346        d['stamp'] = stamp
347        if h_frm:
348            d['from'] = h_frm
349
350class HistoryOptions(object):
351    """A history configuration object.
352
353    @ivar maxchars: Limit the total number of characters in the history to "X"
354                    (where the character count is the characters of the complete XML stanzas, not only their XML character data).
355    @type maxchars: L{int}
356               
357    @ivar  maxstanzas:  Limit the total number of messages in the history to "X".
358    @type mazstanzas: L{int}
359
360    @ivar  seconds: Send only the messages received in the last "X" seconds.
361    @type seconds: L{int}
362
363    @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]).
364    @type since: L{datetime.datetime}
365
366    """
367    attributes = ['maxchars', 'maxstanzas', 'seconds', 'since']
368
369    def __init__(self, maxchars=None, maxstanzas=None, seconds=None, since=None):
370        self.maxchars   = maxchars
371        self.maxstanzas = maxstanzas
372        self.seconds    = seconds
373        self.since      = since
374
375    def toElement(self):
376        h = domish.Element((None, 'history'))
377        for key in self.attributes:
378            a = getattr(self, key, a)
379            if a is not None:
380                if key == 'since':
381                    h[key] = a.strftime('%Y%m%dT%H:%M:%S')
382                else:
383                    h[key] = str(a)
384       
385        return h
386
387class User(object):
388    """
389    A user/entity in a multi-user chat room.
390    """
391   
392    def __init__(self, nick, user_jid=None):
393        self.nick = nick
394        self.user_jid = user_jid
395        self.affiliation = 'none'
396        self.role = 'none'
397       
398        self.status = None
399        self.show   = None
400
401
402class Room(object):
403    """
404    A Multi User Chat Room
405    """
406
407   
408    def __init__(self, name, server, nick, state=None):
409        """
410        """
411        self.state  = state
412        self.name   = name
413        self.server = server
414        self.nick   = nick
415        self.status = 0
416
417        self.entity_id = self.entityId()
418               
419        self.roster = {}
420
421    def entityId(self):
422        """
423        """
424        self.entity_id = jid.internJID(self.name+'@'+self.server+'/'+self.nick)
425
426        return self.entity_id
427
428    def addUser(self, user):
429        """
430        """
431        self.roster[user.nick.lower()] = user
432
433    def inRoster(self, user):
434        """
435        """
436
437        return self.roster.has_key(user.nick.lower())
438
439    def getUser(self, nick):
440        """
441        """
442        return self.roster.get(nick.lower())
443
444    def removeUser(self, user):
445        if self.inRoster(user):
446            del self.roster[user.nick.lower()]
447       
448
449class BasicPresence(xmppim.AvailablePresence):
450    """
451    This behaves like an object providing L{domish.IElement}.
452
453    """
454
455    def __init__(self, to=None, show=None, statuses=None):
456        xmppim.AvailablePresence.__init__(self, to=to, show=show, statuses=statuses)
457        # add muc elements
458        x = self.addElement('x', NS_MUC)
459
460
461class UserPresence(xmppim.Presence):
462    """
463    This behaves like an object providing L{domish.IElement}.
464
465    """
466
467    def __init__(self, to=None, type=None, frm=None, affiliation=None, role=None):
468        xmppim.Presence.__init__(self, to, type)
469        if frm:
470            self['from'] = frm
471        # add muc elements
472        x = self.addElement('x', NS_MUC_USER)
473        if affiliation:
474            x['affiliation'] = affiliation
475        if role:
476            x['role'] = role
477
478class UnavailableUserPresence(xmppim.UnavailablePresence):
479    """
480    This behaves like an object providing L{domish.IElement}.
481
482    """
483
484    def __init__(self, to=None, type=None, frm=None, affiliation=None, role=None):
485        xmppim.UnavailablePresence.__init__(self, to, type)
486        if frm:
487            self['from'] = frm
488        # add muc elements
489        x = self.addElement('x', NS_MUC_USER)
490        if affiliation:
491            x['affiliation'] = affiliation
492        if role:
493            x['role'] = role
494
495
496class PasswordPresence(BasicPresence):
497    """
498    """
499    def __init__(self, to, password):
500        BasicPresence.__init__(self, to)
501       
502        self.x.addElement('password', None, password)
503
504
505class MessageVoice(GroupChat):
506    """
507    """
508    def __init__(self, to=None, frm=None):
509        GroupChat.__init__(self, to=to, frm=frm)
510        # build data form
511        form = data_form.Form('submit', formNamespace=NS_MUC_REQUEST)
512        form.addField(data_form.Field(var='muc#role',
513                                      value='participant', 
514                                      label='Requested role'))
515        self.addChild(form.toElement())           
516
517class PresenceError(xmppim.Presence):
518    """
519    This behaves like an object providing L{domish.IElement}.
520
521    """
522
523    def __init__(self, error, to=None, frm=None):
524        xmppim.Presence.__init__(self, to, type='error')
525        if frm:
526            self['from'] = frm
527        # add muc elements
528        x = self.addElement('x', NS_MUC)
529       
530        # add error
531        e = error.getElement()
532        self.addChild(e)
533       
534
535class MUCClient(XMPPHandler):
536    """
537    Multi-User chat client protocol. This is a subclass of L{XMPPHandler} and implements L{IMUCCLient}.
538
539    """
540
541    implements(IMUCClient)
542
543    rooms = {}
544
545    timeout = None
546
547    _deferreds = []
548
549    def connectionInitialized(self):
550        """ This method is called when the client has successfully authenticated.
551        It initializes several xpath events to handle MUC stanzas that come in.
552        After those are initialized then the method initialized is called to signal that we have finished.
553        """
554        self.xmlstream.addObserver(PRESENCE+"[not(@type) or @type='available']/x", self._onXPresence)
555        self.xmlstream.addObserver(PRESENCE+"[@type='unavailable']", self._onUnavailablePresence)
556        self.xmlstream.addObserver(PRESENCE+"[@type='error']", self._onPresenceError)
557        self.xmlstream.addObserver(GROUPCHAT, self._onGroupChat)
558        self.xmlstream.addObserver(SUBJECT, self._onSubject)
559        # add history
560
561        self.initialized()
562
563    def _setRoom(self, room):
564        """Add a room to the room collection.
565        """
566        self.rooms[room.entity_id.userhost().lower()] = room
567
568    def _getRoom(self, room_jid):
569        """Grab a room from the room collection.
570        """
571        return self.rooms.get(room_jid.userhost().lower())
572
573    def _removeRoom(self, room_jid):
574        """Delete a room from the room collection.
575        """
576        if self.rooms.has_key(room_jid.userhost().lower()):
577            del self.rooms[room_jid.userhost().lower()]
578
579
580    def _onUnavailablePresence(self, prs):
581        """ This method is called when the stanza matches the xpath observer.
582        The client has received a presence stanza with the 'type' attribute of unavailable.
583        It means a user has exited a MUC room.
584        """
585
586        if not prs.hasAttribute('from'):
587            return
588        room_jid = jid.internJID(prs.getAttribute('from', ''))
589        self._userLeavesRoom(room_jid)
590
591    def _onPresenceError(self, prs):
592        """This method is called when a presence stanza with the 'type' attribute of error.
593        There are various reasons for receiving a presence error and it means that the user has left the room.
594        """
595        if not prs.hasAttribute('from'):
596            return
597        room_jid = jid.internJID(prs.getAttribute('from', ''))
598        # add an error hook here?
599        self._userLeavesRoom(room_jid)
600       
601    def _getExceptionFromElement(self, stanza):
602        # find an exception based on the error stanza
603        muc_condition = 'exception'
604
605        error = getattr(stanza, 'error', None)
606        if error is not None:
607            for e in error.elements():
608                muc_condition = e.name
609
610        return MUC_EXCEPTIONS[muc_condition]
611
612    def _userLeavesRoom(self, room_jid):
613        # when a user leaves a room we need to update it
614        room = self._getRoom(room_jid)
615        if room is None:
616            # not in the room yet
617            return
618        # check if user is in roster
619        user = room.getUser(room_jid.resource)
620        if user is None:
621            return
622        if room.inRoster(user):
623            room.removeUser(user)
624            self.userLeftRoom(room, user)
625       
626    def _onXPresence(self, prs):
627        """ A muc presence has been received.
628        """
629        if not prs.hasAttribute('from'):
630            return
631        room_jid = jid.internJID(prs.getAttribute('from', ''))
632           
633        status = getattr(prs, 'status', None)
634        show   = getattr(prs, 'show', None)
635       
636        # grab room
637        room = self._getRoom(room_jid)
638        if room is None:
639            # not in the room yet
640            return
641
642        # check if user is in roster
643        user = room.getUser(room_jid.resource)
644        if user is None: # create a user that does not exist
645            user = User(room_jid.resource)
646           
647       
648        if room.inRoster(user):
649            # we changed status or nick
650            muc_status = getattr(prs.x, 'status', None)
651            if muc_status:
652                code = muc_status.getAttribute('code', 0)
653            else:
654                self.userUpdatedStatus(room, user, show, status)
655        else:           
656            room.addUser(user)
657            self.userJoinedRoom(room, user)
658           
659
660    def _onGroupChat(self, msg):
661        """ A group chat message has been received from a MUC room.
662       
663        There are a few event methods that may get called here. receviedGroupChat and receivedHistory
664        """
665        if not msg.hasAttribute('from'):
666            # need to return an error here
667            return
668        room_jid = jid.internJID(msg.getAttribute('from', ''))
669
670        room = self._getRoom(room_jid)
671        if room is None:
672            # not in the room yet
673            return
674        user = room.getUser(room_jid.resource)
675        delay = None
676        # need to check for delay and x stanzas for delay namespace for backwards compatability
677        for e in msg.elements():
678            if e.uri == NS_DELAY or e.uri == NS_JABBER_DELAY:
679                delay = e
680        body  = unicode(msg.body)
681        # grab room
682        if delay is None:
683            self.receivedGroupChat(room, user, body)
684        else:
685            self.receivedHistory(room, user, body, delay['stamp'], frm=delay.getAttribute('from',None))
686
687
688    def _onSubject(self, msg):
689        """A subject has been sent from a MUC room.
690        """
691        if not msg.hasAttribute('from'):
692            return
693        room_jid = jid.internJID(msg['from'])
694
695        # grab room
696        room = self._getRoom(room_jid)
697        if room is None:
698            # not in the room yet
699            return
700
701        self.receivedSubject(room_jid, unicode(msg.subject))
702
703
704    def _makeTimeStamp(self, stamp=None):
705        # create a timestamp
706        if stamp is None:
707            stamp = datetime.datetime.now()
708           
709        return stamp.strftime('%Y%m%dT%H:%M:%S')
710
711
712    def _joinedRoom(self, d, prs):
713        """We have presence that says we joined a room.
714        """
715        room_jid = jid.internJID(prs['from'])
716       
717        # check for errors
718        if prs.hasAttribute('type') and prs['type'] == 'error':           
719            d.errback(self._getExceptionFromElement(prs))
720        else:   
721            # change the state of the room
722            r = self._getRoom(room_jid)
723            if r is None:
724                raise NotFound
725            r.state = 'joined'
726           
727            # grab status
728            status = getattr(prs.x,'status',None)
729            if status:
730                r.status = status.getAttribute('code', None)
731
732            d.callback(r)
733
734
735    def _leftRoom(self, d, prs):
736        """We have presence that says we joined a room.
737        """
738        room_jid = jid.internJID(prs['from'])
739       
740        # check for errors
741        if prs.hasAttribute('type') and prs['type'] == 'error':           
742            d.errback(self._getExceptionFromElement(prs))
743        else:   
744            # change the state of the room
745            r = self._getRoom(room_jid)
746            if r is None:
747                raise NotFound
748            self._removeRoom(room_jid)
749           
750            d.callback(True)
751
752    def initialized(self):
753        """Client is initialized and ready!
754        """
755        pass
756
757    def userJoinedRoom(self, room, user):
758        """User has joined a MUC room.
759
760        This method will need to be modified inorder for clients to
761        do something when this event occurs.
762
763        @param room: The room the user has joined.
764        @type  room: L{Room}
765
766        @param user: The user that joined the MUC room.
767        @type  user: L{User}
768
769        """
770        pass
771
772    def userLeftRoom(self, room, user):
773        """User has left a room.
774       
775        This method will need to be modified inorder for clients to
776        do something when this event occurs.
777
778        @param room: The room the user has joined.
779        @type  room: L{Room}
780
781        @param user: The user that joined the MUC room.
782        @type  user: L{User}
783
784        """
785        pass
786
787
788    def userUpdatedStatus(self, room, user, show, status):
789        """User Presence has been received
790       
791        This method will need to be modified inorder for clients to
792        do something when this event occurs.
793       
794        """
795        pass
796       
797
798    def receivedSubject(self, room, subject):
799        """
800        This method will need to be modified inorder for clients to
801        do something when this event occurs.
802
803        """
804        pass
805
806
807    def receivedHistory(self, room, user, message, history, frm=None):
808        """
809        This method will need to be modified inorder for clients to
810        do something when this event occurs.
811        """
812        pass
813
814
815    def _cbDisco(self, iq):
816        # grab query
817       
818        return getattr(iq,'query', None)
819       
820
821    def sendDeferred(self,  obj, timeout):
822        """ Send data or a domish element, adding a deferred with a timeout.
823       
824        @param obj: The object to send over the wire.
825        @type  obj: L{domish.Element} or L{unicode}
826
827        @param timeout: The number of seconds to wait before the deferred is timed out.
828        @type  timeout: L{int}
829
830        The deferred object L{defer.Deferred} is returned.
831        """
832        d = defer.Deferred()
833        self._deferreds.append(d)
834
835
836        def onTimeout():
837            i = 0
838            for xd in self._deferreds:
839                if d == xd:
840                    self._deferreds.pop(i)
841                    d.errback(xmlstream.TimeoutError("Timeout waiting for response."))
842                i += 1
843
844        call = reactor.callLater(timeout, onTimeout)
845       
846        def cancelTimeout(result):
847            if call.active():
848                call.cancel()
849
850            return result
851
852        d.addBoth(cancelTimeout)
853
854        self.xmlstream.send(obj)
855        return d
856
857    def disco(self, entity, type='info'):
858        """Send disco queries to a XMPP entity.
859
860        @param entity: The server or entity where we want discovery information from.
861        @type  entity: L{jid.JID}
862
863        @param type: The optional disco type.
864        @type  type: L{unicode}
865
866        """
867
868        iq = disco.DiscoRequest(self.xmlstream, disco.NS_INFO, 'get')
869        iq['to'] = entity
870
871        return iq.send().addBoth(self._cbDisco)
872       
873
874    def configure(self, room_jid, fields=[]):
875        """Configure a room
876
877        @param room_jid: The room jabber/xmpp entity id for the requested configuration form.
878        @type  room_jid: L{jid.JID}
879
880        @param fields: The fields we want to modify.
881        @type  fields: A L{list} or L{dataform.Field}
882
883        """
884        request = ConfigureRequest(self.xmlstream, method='set', fields=fields)
885        request['to'] = room_jid
886       
887        return request.send()
888
889    def getConfigureForm(self, room_jid):
890        """Grab the configuration form from the room. This sends an iq request to the room.
891
892        @param room_jid: The room jabber/xmpp entity id for the requested configuration form.
893        @type  room_jid: L{jid.JID}
894
895        """
896        request = ConfigureRequest(self.xmlstream)
897        request['to'] = room_jid
898        return request.send()
899
900
901    def join(self, server, room, nick, history = None):
902        """ Join a MUC room by sending presence to it. Returns a defered that is called when
903        the entity is in the room or an error has occurred.
904       
905        @param server: The server where the room is located.
906        @type  server: L{unicode}
907
908        @param room: The room name the entity is joining.
909        @type  room: L{unicode}
910
911        @param nick: The nick name for the entitity joining the room.
912        @type  nick: L{unicode}
913       
914        @param history: The maximum number of history stanzas you would like.
915
916        """
917        r = Room(room, server, nick, state='joining')
918        self._setRoom(r)
919 
920        p = BasicPresence(to=r.entity_id)
921        if history is not None:
922            p.x.addChild(history.toElement())
923
924        d = self.sendDeferred(p, timeout=DEFER_TIMEOUT)
925
926        # add observer for joining the room
927        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s']" % (r.entity_id.full()), 
928                                          self._joinedRoom, 1, d)
929
930        return d
931   
932    def _changed(self, d, room_jid, prs):
933        """Callback for changing the nick and status.
934        """
935
936        r = self._getRoom(room_jid)
937
938        d.callback(r)
939
940
941    def nick(self, room_jid, new_nick):
942        """ Change an entities nick name in a MUC room.
943       
944        See: http://xmpp.org/extensions/xep-0045.html#changenick
945
946        @param room_jid: The room jabber/xmpp entity id for the requested configuration form.
947        @type  room_jid: L{jid.JID}
948
949        @param new_nick: The nick name for the entitity joining the room.
950        @type  new_nick: L{unicode}
951       
952        """
953
954       
955        r = self._getRoom(room_jid)
956        if r is None:
957            raise NotFound
958        r.nick = new_nick # change the nick
959        # create presence
960        # make sure we call the method to generate the new entity xmpp id
961        p = BasicPresence(to=r.entityId()) 
962        d = self.sendDeferred(p, timeout=DEFER_TIMEOUT)
963
964        # add observer for joining the room
965        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s']" % (r.entity_id.full()), 
966                                          self._changed, 1, d, room_jid)
967
968        return d
969       
970
971   
972    def leave(self, room_jid):
973        """Leave a MUC room.
974
975        See: http://xmpp.org/extensions/xep-0045.html#exit
976
977        @param room_jid: The room entity id you want to exit.
978        @type  room_jid: L{jid.JID}
979
980        """
981        r = self._getRoom(room_jid)
982 
983        p = xmppim.UnavailablePresence(to=r.entity_id)
984
985        d = self.sendDeferred(p, timeout=DEFER_TIMEOUT)
986        # add observer for joining the room
987        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s' and @type='unavailable']" % (r.entity_id.full()), 
988                                          self._leftRoom, 1, d)
989
990        return d
991   
992
993    def status(self, room_jid, show=None, status=None):
994        """Change user status.
995
996        See: http://xmpp.org/extensions/xep-0045.html#changepres
997
998        @param room_jid: The room jabber/xmpp entity id for the requested configuration form.
999        @type  room_jid: L{jid.JID}
1000
1001        @param show: The availability of the entity. Common values are xa, available, etc
1002        @type  show: L{unicode}
1003
1004        @param show: The current status of the entity.
1005        @type  show: L{unicode}
1006
1007        """
1008        r = self._getRoom(room_jid)
1009        if r is None:
1010            raise NotFound
1011
1012        p = BasicPresence(to=r.entityId()) 
1013        if status is not None:
1014            p.addElement('status', None, status)
1015           
1016        if show is not None:
1017            p.addElement('show', None, show)
1018           
1019        d = self.sendDeferred(p, timeout=DEFER_TIMEOUT)
1020
1021        # add observer for joining the room
1022        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s']" % (r.entity_id.full()), 
1023                                          self._changed, 1, d, room_jid)
1024
1025        return d
1026
1027    def _sendMessage(self, msg, children=None):
1028        # send a message
1029        if children:
1030            for c in children:
1031                msg.addChild(c)
1032       
1033        self.xmlstream.send(msg)
1034
1035    def groupChat(self, to, message, children=None):
1036        """Send a groupchat message
1037        """
1038        msg = GroupChat(to, body=message)
1039       
1040        self._sendMessage(msg, children=children)
1041
1042    def chat(self, room_jid, message, children=None):
1043        """Send a private chat message to a user in a MUC room.
1044       
1045        See: http://xmpp.org/extensions/xep-0045.html#privatemessage
1046
1047        @param room_jid: The room entity id.
1048        @type  room_jid: L{jid.JID}
1049
1050        """
1051
1052        msg = PrivateChat(room_jid, body=message)
1053
1054        self._sendMessage(msg, children=children)
1055       
1056    def invite(self, room_jid, reason=None, full_jid=None):
1057        """Invite a xmpp entity to a MUC room.
1058
1059        See: http://xmpp.org/extensions/xep-0045.html#invite
1060
1061        @param room_jid: The room entity id.
1062        @type  room_jid: L{jid.JID}
1063       
1064        @param reason: The reason for the invite.
1065        @type  reason: L{unicode}
1066
1067        @param full_jid: The xmpp user's entity id.
1068        @type  full_jid: L{jid.JID}
1069
1070        """
1071        msg = InviteMessage(room_jid, reason=reason, full_jid=full_jid)
1072        self._sendMessage(msg)
1073
1074
1075    def password(self, room_jid, password):
1076        """Send a password to a room so the entity can join.
1077       
1078        See: http://xmpp.org/extensions/xep-0045.html#enter-pw
1079
1080        @param room_jid: The room entity id.
1081        @type  room_jid: L{jid.JID}
1082       
1083        @param password: The MUC room password.
1084        @type  password: L{unicode}
1085       
1086        """
1087        p = PasswordPresence(room_jid, password)
1088
1089        self.xmlstream.send(p)
1090   
1091    def register(self, to, fields=[]):
1092        """
1093        """
1094        iq = RegisterRequest(self.xmlstream, method='set', fields=fields)
1095        iq['to'] = to
1096        return iq.send()
1097
1098
1099    def _getAffiliationList(self, room_jid, affiliation):
1100        iq = AffiliationRequest(self.xmlstream,
1101                                method='get',
1102                                affiliation=affiliation, 
1103                                )
1104        iq['to'] = room_jid.full()
1105        return iq.send()       
1106
1107
1108    def _getRoleList(self, room_jid, role):
1109        # send a role request
1110        iq = RoleRequest(self.xmlstream,
1111                                method='get',
1112                                role=role,
1113                                )
1114        iq['to'] = room_jid.full()
1115        return iq.send()       
1116
1117
1118    def _setAffiliationList(self, affiliation, room_jid, iq):
1119        # set a rooms affiliation list
1120        r = self._getRoom(room_jid)
1121        if r is not None:
1122            affiliation_list = []
1123            setattr(r, affiliation, [])
1124           
1125            for item in iq.query.elements():
1126                nick   = item.getAttribute('nick', None)
1127                entity = item.getAttribute('jid', None)
1128                u      = None
1129                if nick is None and entity is None:
1130                    raise Exception, 'bad attributes in item list'
1131                if nick is not None:
1132                    u = room.getUser(nick)
1133                if u is None:
1134                    u = User(nick, user_jid=jid.internJID(entity))
1135                    u.affiliation = 'member'
1136                   
1137                affiliation_list.append(u)
1138
1139            setattr(r, affiliation, affiliation_list)
1140        return r
1141
1142    def getMemberList(self, room_jid):
1143        """ Get a member list from a room.
1144
1145        @param room_jid: The room jabber/xmpp entity id for the requested member list.
1146        @type  room_jid: L{jid.JID}
1147
1148        """
1149        d = self._getAffiliationList(room_jid, 'member')
1150        d.addCallback(self._setAffiliationList, 'members', room_jid)
1151        return d
1152
1153    def getAdminList(self, room_jid):
1154        """ Get an admin list from a room.
1155
1156        @param room_jid: The room jabber/xmpp entity id for the requested member list.
1157        @type  room_jid: L{jid.JID}
1158
1159        """
1160        d = self._getAffiliationList(room_jid, 'admin')
1161        d.addCallback(self._setAffiliationList, 'members', room_jid)
1162        return d
1163
1164    def getBanList(self, room_jid):
1165        """ Get an outcast list from a room.
1166
1167        @param room_jid: The room jabber/xmpp entity id for the requested member list.
1168        @type  room_jid: L{jid.JID}
1169
1170        """
1171        d = self._getAffiliationList(room_jid, 'outcast')
1172        d.addCallback(self._setAffiliationList, 'members', room_jid)
1173        return d
1174
1175    def getOwnerList(self, room_jid):
1176        """ Get an owner list from a room.
1177
1178        @param room_jid: The room jabber/xmpp entity id for the requested member list.
1179        @type  room_jid: L{jid.JID}
1180
1181        """
1182        d = self._getAffiliationList(room_jid, 'owner')
1183        d.addCallback(self._setAffiliationList, 'members', room_jid)
1184        return d
1185
1186    def getRegisterForm(self, room):
1187        """
1188
1189        @param room: The room jabber/xmpp entity id for the requested registration form.
1190        @type  room: L{jid.JID}
1191
1192        """
1193        iq = RegisterRequest(self.xmlstream)
1194        iq['to'] = room.userhost()
1195        return iq.send()
1196
1197    def destroy(self, room_jid, reason=None):
1198        """ Destroy a room.
1199       
1200        @param room_jid: The room jabber/xmpp entity id.
1201        @type  room_jid: L{jid.JID}
1202       
1203        """
1204        def destroyed(iq):
1205            self._removeRoom(room_jid)
1206            return True
1207
1208        iq = OwnerRequest(self.xmlstream, method='set')
1209        d  = iq.query.addElement('destroy')
1210        d['jid'] = room_jid.userhost()
1211        if reason is not None:
1212            d.addElement('reason', None, reason)
1213
1214        return iq.send().addCallback(destroyed)
1215
1216    def subject(self, to, subject):
1217        """
1218        """
1219        msg = GroupChat(to, subject=subject)
1220        self.xmlstream.send(msg)
1221
1222    def voice(self, to):
1223        """
1224        """
1225        msg = MessageVoice(to=to)
1226        self.xmlstream.send(msg)
1227
1228
1229
1230    def history(self, to, message_list):
1231        """
1232        """
1233       
1234        for m in message_list:
1235            m['type'] = 'groupchat'
1236            mto = m['to']
1237            frm = m.getAttribute('from', None)
1238            m['to'] = to
1239
1240            d = m.addElement('delay', NS_DELAY)
1241            d['stamp'] = self._makeTimeStamp()
1242            d['from'] = mto
1243
1244            self.xmlstream.send(m)
1245
1246    def _setAffiliation(self, frm, room_jid, affiliation, reason=None, a_jid=None):
1247        iq = AffiliationRequest(self.xmlstream,
1248                                method='set',
1249                                affiliation=affiliation,
1250                                a_jid=a_jid, 
1251                                reason=reason)
1252        iq['to'] = room_jid.userhost() # this is a room jid, only send to room
1253        iq['from'] = frm.full()
1254        return iq.send()
1255
1256    def _setRole(self, frm, room_jid, a_jid=None, reason=None, role='none'):
1257        iq = RoleRequest(self.xmlstream,
1258                         method='set',
1259                         a_jid=a_jid,
1260                         role=role,
1261                         reason=reason)
1262       
1263        iq['to'] = room_jid.userhost() # this is a room jid, only send to room
1264        iq['from'] = frm.full()
1265        return iq.send()
1266
1267    def _cbRequest(self, room_jid, iq):
1268        r = self._getRoom(room_jid)
1269        if r is None:
1270            raise NotFound
1271
1272        return r
1273
1274    def grantVoice(self, frm, room_jid, voice_jid=None, reason=None):
1275        return self._setRole(frm, room_jid, role='participant', a_jid=voice_jid, reason=reason)
1276
1277    def grantVisitor(self, frm, room_jid, reason=None):
1278        return self._setRole(frm, room_jid, role='visitor', reason=reason)
1279
1280    def grantModerator(self, frm, room_jid, reason=None):
1281        return self._setRole(frm, room_jid, role='moderator', reason=reason)
1282
1283    def ban(self, to, ban_jid, frm, reason=None):
1284        return self._setAffiliation(frm, to, 'outcast', a_jid=ban_jid, reason=reason)
1285
1286    def kick(self, to, kick_jid, frm, reason=None):       
1287        return self._setAffiliation(frm, to, 'none', a_jid=kick_jid, reason=reason)
1288       
Note: See TracBrowser for help on using the repository browser.