source: wokkel/muc.py @ 124:e0750258b9ee

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

typo fix

File size: 34.1 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
285        i = self.query.addElement('item')
286
287        i['role'] = role
288        if a_jid:
289            i['jid'] = a_jid.full()
290           
291        if reason:
292            i.addElement('reason', None, reason)
293
294class GroupChat(domish.Element):
295    """
296    """
297    def __init__(self, to, body=None, subject=None, frm=None):
298        """To needs to be a string
299        """
300        domish.Element.__init__(self, (None, 'message'))
301        self['type'] = 'groupchat'
302        if isinstance(to, jid.JID):
303            self['to'] = to.userhost()
304        else:
305            self['to'] = to
306        if frm:
307            self['from'] = frm
308        if body:
309            self.addElement('body',None, body)
310        if subject:
311            self.addElement('subject',None, subject)
312
313
314class PrivateChat(domish.Element):
315    """
316    """
317    def __init__(self, to, body=None, frm=None):
318        """To needs to be a string
319        """
320        domish.Element.__init__(self, (None, 'message'))
321        self['type'] = 'chat'
322        self['to']   = to
323        if frm:
324            self['from'] = frm
325        if body:
326            self.addElement('body',None, body)
327           
328class InviteMessage(PrivateChat):
329    def __init__(self, to, reason=None, full_jid=None, body=None, frm=None, password=None):
330        PrivateChat.__init__(self, to, body=body, frm=frm)
331        del self['type'] # remove type
332        x = self.addElement('x', NS_MUC_USER)
333        invite = x.addElement('invite')
334        if full_jid:
335            invite['to'] = full_jid
336        if reason:
337            invite.addElement('reason', None, reason)
338        if password:
339            invite.addElement('password', None, password)
340
341class HistoryMessage(GroupChat):
342    """
343    """
344    def __init__(self, to, stamp, body=None, subject=None, frm=None, h_frm=None):
345        GroupChat.__init__(self, to, body=body, subject=subject, frm=frm)
346        d = self.addElement('delay', NS_DELAY)
347        d['stamp'] = stamp
348        if h_frm:
349            d['from'] = h_frm
350
351class HistoryOptions(object):
352    """A history configuration object.
353
354    @ivar maxchars: Limit the total number of characters in the history to "X"
355                    (where the character count is the characters of the complete XML stanzas, not only their XML character data).
356    @type maxchars: L{int}
357               
358    @ivar  maxstanzas:  Limit the total number of messages in the history to "X".
359    @type mazstanzas: L{int}
360
361    @ivar  seconds: Send only the messages received in the last "X" seconds.
362    @type seconds: L{int}
363
364    @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]).
365    @type since: L{datetime.datetime}
366
367    """
368    attributes = ['maxchars', 'maxstanzas', 'seconds', 'since']
369
370    def __init__(self, maxchars=None, maxstanzas=None, seconds=None, since=None):
371        self.maxchars   = maxchars
372        self.maxstanzas = maxstanzas
373        self.seconds    = seconds
374        self.since      = since
375
376    def toElement(self):
377        h = domish.Element((None, 'history'))
378        for key in self.attributes:
379            a = getattr(self, key, a)
380            if a is not None:
381                if key == 'since':
382                    h[key] = a.strftime('%Y%m%dT%H:%M:%S')
383                else:
384                    h[key] = str(a)
385       
386        return h
387
388class User(object):
389    """
390    A user/entity in a multi-user chat room.
391    """
392   
393    def __init__(self, nick, user_jid=None):
394        self.nick = nick
395        self.user_jid = user_jid
396        self.affiliation = 'none'
397        self.role = 'none'
398       
399        self.status = None
400        self.show   = None
401
402
403class Room(object):
404    """
405    A Multi User Chat Room
406    """
407
408   
409    def __init__(self, name, server, nick, state=None):
410        """
411        """
412        self.state  = state
413        self.name   = name
414        self.server = server
415        self.nick   = nick
416        self.status = 0
417
418        self.entity_id = self.entityId()
419               
420        self.roster = {}
421
422    def entityId(self):
423        """
424        """
425        self.entity_id = jid.internJID(self.name+'@'+self.server+'/'+self.nick)
426
427        return self.entity_id
428
429    def addUser(self, user):
430        """
431        """
432        self.roster[user.nick.lower()] = user
433
434    def inRoster(self, user):
435        """
436        """
437
438        return self.roster.has_key(user.nick.lower())
439
440    def getUser(self, nick):
441        """
442        """
443        return self.roster.get(nick.lower())
444
445    def removeUser(self, user):
446        if self.inRoster(user):
447            del self.roster[user.nick.lower()]
448       
449
450class BasicPresence(xmppim.AvailablePresence):
451    """
452    This behaves like an object providing L{domish.IElement}.
453
454    """
455
456    def __init__(self, to=None, show=None, statuses=None):
457        xmppim.AvailablePresence.__init__(self, to=to, show=show, statuses=statuses)
458        # add muc elements
459        x = self.addElement('x', NS_MUC)
460
461
462class UserPresence(xmppim.Presence):
463    """
464    This behaves like an object providing L{domish.IElement}.
465
466    """
467
468    def __init__(self, to=None, type=None, frm=None, affiliation=None, role=None):
469        xmppim.Presence.__init__(self, to, type)
470        if frm:
471            self['from'] = frm
472        # add muc elements
473        x = self.addElement('x', NS_MUC_USER)
474        if affiliation:
475            x['affiliation'] = affiliation
476        if role:
477            x['role'] = role
478
479class UnavailableUserPresence(xmppim.UnavailablePresence):
480    """
481    This behaves like an object providing L{domish.IElement}.
482
483    """
484
485    def __init__(self, to=None, type=None, frm=None, affiliation=None, role=None):
486        xmppim.UnavailablePresence.__init__(self, to, type)
487        if frm:
488            self['from'] = frm
489        # add muc elements
490        x = self.addElement('x', NS_MUC_USER)
491        if affiliation:
492            x['affiliation'] = affiliation
493        if role:
494            x['role'] = role
495
496
497class PasswordPresence(BasicPresence):
498    """
499    """
500    def __init__(self, to, password):
501        BasicPresence.__init__(self, to)
502       
503        self.x.addElement('password', None, password)
504
505
506class MessageVoice(GroupChat):
507    """
508    """
509    def __init__(self, to=None, frm=None):
510        GroupChat.__init__(self, to=to, frm=frm)
511        # build data form
512        form = data_form.Form('submit', formNamespace=NS_MUC_REQUEST)
513        form.addField(data_form.Field(var='muc#role',
514                                      value='participant', 
515                                      label='Requested role'))
516        self.addChild(form.toElement())           
517
518class PresenceError(xmppim.Presence):
519    """
520    This behaves like an object providing L{domish.IElement}.
521
522    """
523
524    def __init__(self, error, to=None, frm=None):
525        xmppim.Presence.__init__(self, to, type='error')
526        if frm:
527            self['from'] = frm
528        # add muc elements
529        x = self.addElement('x', NS_MUC)
530       
531        # add error
532        e = error.getElement()
533        self.addChild(e)
534       
535
536class MUCClient(XMPPHandler):
537    """
538    Multi-User chat client protocol. This is a subclass of L{XMPPHandler} and implements L{IMUCCLient}.
539
540    """
541
542    implements(IMUCClient)
543
544    rooms = {}
545
546    timeout = None
547
548    _deferreds = []
549
550    def connectionInitialized(self):
551        """ This method is called when the client has successfully authenticated.
552        It initializes several xpath events to handle MUC stanzas that come in.
553        After those are initialized then the method initialized is called to signal that we have finished.
554        """
555        self.xmlstream.addObserver(PRESENCE+"[not(@type) or @type='available']/x", self._onXPresence)
556        self.xmlstream.addObserver(PRESENCE+"[@type='unavailable']", self._onUnavailablePresence)
557        self.xmlstream.addObserver(PRESENCE+"[@type='error']", self._onPresenceError)
558        self.xmlstream.addObserver(GROUPCHAT, self._onGroupChat)
559        self.xmlstream.addObserver(SUBJECT, self._onSubject)
560        # add history
561
562        self.initialized()
563
564    def _setRoom(self, room):
565        """Add a room to the room collection.
566        """
567        self.rooms[room.entity_id.userhost().lower()] = room
568
569    def _getRoom(self, room_jid):
570        """Grab a room from the room collection.
571        """
572        return self.rooms.get(room_jid.userhost().lower())
573
574    def _removeRoom(self, room_jid):
575        """Delete a room from the room collection.
576        """
577        if self.rooms.has_key(room_jid.userhost().lower()):
578            del self.rooms[room_jid.userhost().lower()]
579
580
581    def _onUnavailablePresence(self, prs):
582        """ This method is called when the stanza matches the xpath observer.
583        The client has received a presence stanza with the 'type' attribute of unavailable.
584        It means a user has exited a MUC room.
585        """
586
587        if not prs.hasAttribute('from'):
588            return
589        room_jid = jid.internJID(prs.getAttribute('from', ''))
590        self._userLeavesRoom(room_jid)
591
592    def _onPresenceError(self, prs):
593        """This method is called when a presence stanza with the 'type' attribute of error.
594        There are various reasons for receiving a presence error and it means that the user has left the room.
595        """
596        if not prs.hasAttribute('from'):
597            return
598        room_jid = jid.internJID(prs.getAttribute('from', ''))
599        # add an error hook here?
600        self._userLeavesRoom(room_jid)
601       
602    def _getExceptionFromElement(self, stanza):
603        # find an exception based on the error stanza
604        muc_condition = 'exception'
605
606        error = getattr(stanza, 'error', None)
607        if error is not None:
608            for e in error.elements():
609                muc_condition = e.name
610
611        return MUC_EXCEPTIONS[muc_condition]
612
613    def _userLeavesRoom(self, room_jid):
614        # when a user leaves a room we need to update it
615        room = self._getRoom(room_jid)
616        if room is None:
617            # not in the room yet
618            return
619        # check if user is in roster
620        user = room.getUser(room_jid.resource)
621        if user is None:
622            return
623        if room.inRoster(user):
624            room.removeUser(user)
625            self.userLeftRoom(room, user)
626       
627    def _onXPresence(self, prs):
628        """ A muc presence has been received.
629        """
630        if not prs.hasAttribute('from'):
631            return
632        room_jid = jid.internJID(prs.getAttribute('from', ''))
633           
634        status = getattr(prs, 'status', None)
635        show   = getattr(prs, 'show', None)
636       
637        # grab room
638        room = self._getRoom(room_jid)
639        if room is None:
640            # not in the room yet
641            return
642
643        # check if user is in roster
644        user = room.getUser(room_jid.resource)
645        if user is None: # create a user that does not exist
646            user = User(room_jid.resource)
647           
648       
649        if room.inRoster(user):
650            # we changed status or nick
651            muc_status = getattr(prs.x, 'status', None)
652            if muc_status:
653                code = muc_status.getAttribute('code', 0)
654            else:
655                self.userUpdatedStatus(room, user, show, status)
656        else:           
657            room.addUser(user)
658            self.userJoinedRoom(room, user)
659           
660
661    def _onGroupChat(self, msg):
662        """
663        """
664        if not msg.hasAttribute('from'):
665            # need to return an error here
666            return
667        room_jid = jid.internJID(msg.getAttribute('from', ''))
668
669        room = self._getRoom(room_jid)
670        if room is None:
671            # not in the room yet
672            return
673        user = room.getUser(room_jid.resource)
674        delay = None
675        # need to check for delay and x stanzas for delay namespace for backwards compatability
676        for e in msg.elements():
677            if e.uri == NS_DELAY or e.uri == NS_JABBER_DELAY:
678                delay = e
679        body  = unicode(msg.body)
680        # grab room
681        if delay is None:
682            self.receivedGroupChat(room, user, body)
683        else:
684            self.receivedHistory(room, user, body, delay['stamp'], frm=delay.getAttribute('from',None))
685
686
687    def _onSubject(self, msg):
688        """
689        """
690        if not msg.hasAttribute('from'):
691            return
692        room_jid = jid.internJID(msg['from'])
693
694        # grab room
695        room = self._getRoom(room_jid)
696        if room is None:
697            # not in the room yet
698            return
699
700        self.receivedSubject(room_jid, unicode(msg.subject))
701
702
703    def _makeTimeStamp(self, stamp=None):
704        if stamp is None:
705            stamp = datetime.datetime.now()
706           
707        return stamp.strftime('%Y%m%dT%H:%M:%S')
708
709
710    def _joinedRoom(self, d, prs):
711        """We have presence that says we joined a room.
712        """
713        room_jid = jid.internJID(prs['from'])
714       
715        # check for errors
716        if prs.hasAttribute('type') and prs['type'] == 'error':           
717            d.errback(self._getExceptionFromElement(prs))
718        else:   
719            # change the state of the room
720            r = self._getRoom(room_jid)
721            if r is None:
722                raise NotFound
723            r.state = 'joined'
724           
725            # grab status
726            status = getattr(prs.x,'status',None)
727            if status:
728                r.status = status.getAttribute('code', None)
729
730            d.callback(r)
731
732
733    def _leftRoom(self, d, prs):
734        """We have presence that says we joined a room.
735        """
736        room_jid = jid.internJID(prs['from'])
737       
738        # check for errors
739        if prs.hasAttribute('type') and prs['type'] == 'error':           
740            d.errback(self._getExceptionFromElement(prs))
741        else:   
742            # change the state of the room
743            r = self._getRoom(room_jid)
744            if r is None:
745                raise NotFound
746            self._removeRoom(room_jid)
747           
748            d.callback(True)
749
750    def initialized(self):
751        """Client is initialized and ready!
752        """
753        pass
754
755    def userJoinedRoom(self, room, user):
756        """User has joined a room
757        """
758        pass
759
760    def userLeftRoom(self, room, user):
761        """User has left a room
762        """
763        pass
764
765
766    def userUpdatedStatus(self, room, user, show, status):
767        """User Presence has been received
768        """
769        pass
770       
771
772    def receivedSubject(self, room, subject):
773        """
774        """
775        pass
776
777
778    def receivedHistory(self, room, user, message, history, frm=None):
779        """
780        """
781        pass
782
783
784    def _cbDisco(self, iq):
785        # grab query
786       
787        return getattr(iq,'query', None)
788       
789
790    def sendDeferred(self,  obj, timeout):
791        """ Send data or a domish element, adding a deferred with a timeout.
792        """
793        d = defer.Deferred()
794        self._deferreds.append(d)
795
796
797        def onTimeout():
798            i = 0
799            for xd in self._deferreds:
800                if d == xd:
801                    self._deferreds.pop(i)
802                    d.errback(xmlstream.TimeoutError("Timeout waiting for response."))
803                i += 1
804
805        call = reactor.callLater(timeout, onTimeout)
806       
807        def cancelTimeout(result):
808            if call.active():
809                call.cancel()
810
811            return result
812
813        d.addBoth(cancelTimeout)
814
815        self.xmlstream.send(obj)
816        return d
817
818    def disco(self, entity, type='info'):
819        """Send disco queries to a XMPP entity
820        """
821
822        iq = disco.DiscoRequest(self.xmlstream, disco.NS_INFO, 'get')
823        iq['to'] = entity
824
825        return iq.send().addBoth(self._cbDisco)
826       
827
828    def configure(self, room_jid, fields=[]):
829        """Configure a room
830
831        @param room_jid: The room jabber/xmpp entity id for the requested configuration form.
832        @type  room_jid: L{jid.JID}
833
834        """
835        request = ConfigureRequest(self.xmlstream, method='set', fields=fields)
836        request['to'] = room_jid
837       
838        return request.send()
839
840    def getConfigureForm(self, room_jid):
841        """Grab the configuration form from the room. This sends an iq request to the room.
842
843        @param room_jid: The room jabber/xmpp entity id for the requested configuration form.
844        @type  room_jid: L{jid.JID}
845
846        """
847        request = ConfigureRequest(self.xmlstream)
848        request['to'] = room_jid
849        return request.send()
850
851
852    def join(self, server, room, nick, history = None):
853        """ Join a MUC room by sending presence to it. Returns a defered that is called when
854        the entity is in the room or an error has occurred.
855       
856        @param server: The server where the room is located.
857        @type  server: L{unicode}
858
859        @param room: The room name the entity is joining.
860        @type  room: L{unicode}
861
862        @param nick: The nick name for the entitity joining the room.
863        @type  nick: L{unicode}
864       
865        @param history: The maximum number of history stanzas you would like.
866
867        """
868        r = Room(room, server, nick, state='joining')
869        self._setRoom(r)
870 
871        p = BasicPresence(to=r.entity_id)
872        if history is not None:
873            p.x.addChild(history.toElement())
874
875        d = self.sendDeferred(p, timeout=DEFER_TIMEOUT)
876
877        # add observer for joining the room
878        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s']" % (r.entity_id.full()), 
879                                          self._joinedRoom, 1, d)
880
881        return d
882   
883    def _changed(self, d, room_jid, prs):
884        """Callback for changing the nick and status.
885        """
886
887        r = self._getRoom(room_jid)
888
889        d.callback(r)
890
891
892    def nick(self, room_jid, new_nick):
893        """ Change an entities nick name in a MUC room.
894       
895        See: http://xmpp.org/extensions/xep-0045.html#changenick
896
897        @param room_jid: The room jabber/xmpp entity id for the requested configuration form.
898        @type  room_jid: L{jid.JID}
899
900        @param new_nick: The nick name for the entitity joining the room.
901        @type  new_nick: L{unicode}
902       
903        """
904
905       
906        r = self._getRoom(room_jid)
907        if r is None:
908            raise NotFound
909        r.nick = new_nick # change the nick
910        # create presence
911        # make sure we call the method to generate the new entity xmpp id
912        p = BasicPresence(to=r.entityId()) 
913        d = self.sendDeferred(p, timeout=DEFER_TIMEOUT)
914
915        # add observer for joining the room
916        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s']" % (r.entity_id.full()), 
917                                          self._changed, 1, d, room_jid)
918
919        return d
920       
921
922   
923    def leave(self, room_jid):
924        """Leave a MUC room.
925
926        See: http://xmpp.org/extensions/xep-0045.html#exit
927
928        @param room_jid: The room entity id you want to exit.
929        @type  room_jid: L{jid.JID}
930
931        """
932        r = self._getRoom(room_jid)
933 
934        p = xmppim.UnavailablePresence(to=r.entity_id)
935
936        d = self.sendDeferred(p, timeout=DEFER_TIMEOUT)
937        # add observer for joining the room
938        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s' and @type='unavailable']" % (r.entity_id.full()), 
939                                          self._leftRoom, 1, d)
940
941        return d
942   
943
944    def status(self, room_jid, show=None, status=None):
945        """Change user status.
946
947        See: http://xmpp.org/extensions/xep-0045.html#changepres
948
949        @param room_jid: The room jabber/xmpp entity id for the requested configuration form.
950        @type  room_jid: L{jid.JID}
951
952        @param show: The availability of the entity. Common values are xa, available, etc
953        @type  show: L{unicode}
954
955        @param show: The current status of the entity.
956        @type  show: L{unicode}
957
958        """
959        r = self._getRoom(room_jid)
960        if r is None:
961            raise NotFound
962
963        p = BasicPresence(to=r.entityId()) 
964        if status is not None:
965            p.addElement('status', None, status)
966           
967        if show is not None:
968            p.addElement('show', None, show)
969           
970        d = self.sendDeferred(p, timeout=DEFER_TIMEOUT)
971
972        # add observer for joining the room
973        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s']" % (r.entity_id.full()), 
974                                          self._changed, 1, d, room_jid)
975
976        return d
977
978    def _sendMessage(self, msg, children=None):
979
980        if children:
981            for c in children:
982                msg.addChild(c)
983       
984        self.xmlstream.send(msg)
985
986    def groupChat(self, to, message, children=None):
987        """Send a groupchat message
988        """
989        msg = GroupChat(to, body=message)
990       
991        self._sendMessage(msg, children=children)
992
993    def chat(self, to, message, children=None):
994        msg = PrivateChat(to, body=message)
995
996        self._sendMessage(msg, children=children)
997       
998    def invite(self, to, reason=None, full_jid=None):
999        """
1000        """
1001        msg = InviteMessage(to, reason=reason, full_jid=full_jid)
1002        self._sendMessage(msg)
1003
1004
1005    def password(self, to, password):
1006        p = PasswordPresence(to, password)
1007
1008        self.xmlstream.send(p)
1009   
1010    def register(self, to, fields=[]):
1011        iq = RegisterRequest(self.xmlstream, method='set', fields=fields)
1012        iq['to'] = to
1013        return iq.send()
1014
1015
1016    def _getAffiliationList(self, room_jid, affiliation):
1017        iq = AffiliationRequest(self.xmlstream,
1018                                method='get',
1019                                affiliation=affiliation, 
1020                                )
1021        iq['to'] = room_jid.full()
1022        return iq.send()       
1023
1024
1025    def _getRoleList(self, room_jid, role):
1026        iq = RoleRequest(self.xmlstream,
1027                                method='get',
1028                                role=role,
1029                                )
1030        iq['to'] = room_jid.full()
1031        return iq.send()       
1032
1033
1034    def _setAffiliationList(self, affiliation, room_jid, iq):
1035        r = self._getRoom(room_jid)
1036        if r is not None:
1037            affiliation_list = []
1038            setattr(r, affiliation, [])
1039           
1040            for item in iq.query.elements():
1041                nick   = item.getAttribute('nick', None)
1042                entity = item.getAttribute('jid', None)
1043                u      = None
1044                if nick is None and entity is None:
1045                    raise Exception, 'bad attributes in item list'
1046                if nick is not None:
1047                    u = room.getUser(nick)
1048                if u is None:
1049                    u = User(nick, user_jid=jid.internJID(entity))
1050                    u.affiliation = 'member'
1051                   
1052                affiliation_list.append(u)
1053
1054            setattr(r, affiliation, affiliation_list)
1055        return r
1056
1057    def getMemberList(self, room_jid):
1058        """ Get a member list from a room.
1059
1060        @param room_jid: The room jabber/xmpp entity id for the requested member list.
1061        @type  room_jid: L{jid.JID}
1062
1063        """
1064        d = self._getAffiliationList(room_jid, 'member')
1065        d.addCallback(self._setAffiliationList, 'members', room_jid)
1066        return d
1067
1068    def getAdminList(self, room_jid):
1069        """ Get an admin list from a room.
1070
1071        @param room_jid: The room jabber/xmpp entity id for the requested member list.
1072        @type  room_jid: L{jid.JID}
1073
1074        """
1075        d = self._getAffiliationList(room_jid, 'admin')
1076        d.addCallback(self._setAffiliationList, 'members', room_jid)
1077        return d
1078
1079    def getBanList(self, room_jid):
1080        """ Get an outcast list from a room.
1081
1082        @param room_jid: The room jabber/xmpp entity id for the requested member list.
1083        @type  room_jid: L{jid.JID}
1084
1085        """
1086        d = self._getAffiliationList(room_jid, 'outcast')
1087        d.addCallback(self._setAffiliationList, 'members', room_jid)
1088        return d
1089
1090    def getOwnerList(self, room_jid):
1091        """ Get an owner list from a room.
1092
1093        @param room_jid: The room jabber/xmpp entity id for the requested member list.
1094        @type  room_jid: L{jid.JID}
1095
1096        """
1097        d = self._getAffiliationList(room_jid, 'owner')
1098        d.addCallback(self._setAffiliationList, 'members', room_jid)
1099        return d
1100
1101    def getRegisterForm(self, room):
1102        """
1103
1104        @param room: The room jabber/xmpp entity id for the requested registration form.
1105        @type  room: L{jid.JID}
1106
1107        """
1108        iq = RegisterRequest(self.xmlstream)
1109        iq['to'] = room.userhost()
1110        return iq.send()
1111
1112    def destroy(self, room_jid, reason=None):
1113        """ Destroy a room.
1114       
1115        @param room_jid: The room jabber/xmpp entity id.
1116        @type  room_jid: L{jid.JID}
1117       
1118        """
1119        def destroyed(iq):
1120            self._removeRoom(room_jid)
1121            return True
1122
1123        iq = OwnerRequest(self.xmlstream, method='set')
1124        d  = iq.query.addElement('destroy')
1125        d['jid'] = room_jid.userhost()
1126        if reason is not None:
1127            d.addElement('reason', None, reason)
1128
1129        return iq.send().addCallback(destroyed)
1130
1131    def subject(self, to, subject):
1132        """
1133        """
1134        msg = GroupChat(to, subject=subject)
1135        self.xmlstream.send(msg)
1136
1137    def voice(self, to):
1138        """
1139        """
1140        msg = MessageVoice(to=to)
1141        self.xmlstream.send(msg)
1142
1143
1144
1145    def history(self, to, message_list):
1146        """
1147        """
1148       
1149        for m in message_list:
1150            m['type'] = 'groupchat'
1151            mto = m['to']
1152            frm = m.getAttribute('from', None)
1153            m['to'] = to
1154
1155            d = m.addElement('delay', NS_DELAY)
1156            d['stamp'] = self._makeTimeStamp()
1157            d['from'] = mto
1158
1159            self.xmlstream.send(m)
1160
1161    def _setAffiliation(self, frm, room_jid, affiliation, reason=None, a_jid=None):
1162        iq = AffiliationRequest(self.xmlstream,
1163                                method='set',
1164                                affiliation=affiliation,
1165                                a_jid=a_jid, 
1166                                reason=reason)
1167        iq['to'] = room_jid.userhost() # this is a room jid, only send to room
1168        iq['from'] = frm.full()
1169        return iq.send()
1170
1171    def _setRole(self, frm, room_jid, a_jid=None, reason=None, role='none'):
1172        iq = RoleRequest(self.xmlstream,
1173                         method='set',
1174                         a_jid=a_jid,
1175                         role=role,
1176                         reason=reason)
1177        iq['to'] = room_jid.userhost() # this is a room jid, only send to room
1178        iq['from'] = frm.full()
1179        return iq.send()
1180
1181    def _cbRequest(self, room_jid, iq):
1182        r = self._getRoom(room_jid)
1183        if r is None:
1184            raise NotFound
1185
1186        return r
1187
1188    def grantVoice(self, frm, room_jid, reason=None):
1189        return self._setRole(frm, room_jid, role='participant', reason=reason)
1190
1191    def grantVisitor(self, frm, room_jid, reason=None):
1192        return self._setRole(frm, room_jid, role='visitor', reason=reason)
1193
1194    def grantModerator(self, frm, room_jid, reason=None):
1195        return self._setRole(frm, room_jid, role='moderator', reason=reason)
1196
1197    def ban(self, to, ban_jid, frm, reason=None):
1198        return self._setAffiliation(frm, to, 'outcast', a_jid=ban_jid, reason=reason)
1199
1200    def kick(self, to, kick_jid, frm, reason=None):       
1201        return self._setAffiliation(frm, to, 'none', a_jid=kick_jid, reason=reason)
1202       
Note: See TracBrowser for help on using the repository browser.