source: wokkel/muc.py @ 127:1610e7a3b777

wokkel-muc-client-support-24
Last change on this file since 127:1610e7a3b777 was 127:1610e7a3b777, checked in by Christopher Zorn <tofu@…>, 14 years ago

some docstrings, clean up in keeping params consitent and tests

File size: 44.6 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, nick=None):
269        AdminRequest.__init__(self, xs, method)
270
271        self.affiliation = affiliation
272        if a_jid is not None or nick is not None:
273            i = self.query.addElement('item')
274            i['affiliation'] = affiliation
275
276        if a_jid:
277            i['jid'] = a_jid.full()
278
279        if nick:
280            i['nick'] = nick
281
282        if reason:
283            i.addElement('reason', None, reason)
284
285    def items(self, jid_list=None):
286        """ Set or Get the items list for this affiliation request.
287        """
288        if jid_list:
289            for j in jid_list:
290                i = self.query.addElement('item')
291                i['affiliation'] = self.affiliation
292                if isinstance(j, jid.JID):
293                    i['jid'] = j.full()
294                else:
295                    i['nick'] = j
296
297        return self.query.elements()
298           
299           
300
301class RoleRequest(AdminRequest):
302    def __init__(self, xs, method='get', role='none', a_jid=None, reason=None, nick=None):
303        AdminRequest.__init__(self, xs, method)
304        i = self.query.addElement('item')
305
306        i['role'] = role
307        if a_jid:
308            i['jid'] = a_jid.full()
309        if nick:
310            i['nick'] = nick
311
312        if reason:
313            i.addElement('reason', None, reason)
314       
315class GroupChat(domish.Element):
316    """A message stanza of type groupchat. Used to send a message to a MUC room that will be
317    broadcasted to people in the room.
318
319    """
320    def __init__(self, to, body=None, subject=None, frm=None):
321        """
322        """
323        domish.Element.__init__(self, (None, 'message'))
324        self['type'] = 'groupchat'
325        if isinstance(to, jid.JID):
326            self['to'] = to.userhost()
327        else:
328            self['to'] = to
329        if frm:
330            self['from'] = frm
331        if body:
332            self.addElement('body',None, body)
333        if subject:
334            self.addElement('subject',None, subject)
335
336
337class PrivateChat(domish.Element):
338    """A chat message.
339    """
340    def __init__(self, to, body=None, frm=None):
341        """ Initialize the L{PrivateChat}
342
343        @param to: The xmpp entity you are sending this chat to.
344        @type  to: L{unicode} or L{jid.JID}
345
346        @param body: The message you are sending.
347        @type  body: L{unicode}
348
349        @param frm: The entity that is sending the message.
350        @param frm: L{unicode}
351
352        """
353        domish.Element.__init__(self, (None, 'message'))
354        self['type'] = 'chat'
355        if isinstance(to, jid.JID):
356            self['to'] = to.userhost()
357        else:
358            self['to'] = to
359        if frm:
360            self['from'] = frm
361        if body:
362            self.addElement('body',None, body)
363           
364class InviteMessage(PrivateChat):
365    def __init__(self, to, reason=None, full_jid=None, body=None, frm=None, password=None):
366        PrivateChat.__init__(self, to, body=body, frm=frm)
367        del self['type'] # remove type
368        x = self.addElement('x', NS_MUC_USER)
369        invite = x.addElement('invite')
370        if full_jid:
371            invite['to'] = full_jid
372        if reason:
373            invite.addElement('reason', None, reason)
374        if password:
375            invite.addElement('password', None, password)
376
377class HistoryMessage(GroupChat):
378    """ A history message for MUC groupchat history.
379    """
380    def __init__(self, to, stamp, body=None, subject=None, frm=None, h_frm=None):
381        GroupChat.__init__(self, to, body=body, subject=subject, frm=frm)
382        d = self.addElement('delay', NS_DELAY)
383        d['stamp'] = stamp
384        if h_frm:
385            d['from'] = h_frm
386
387class HistoryOptions(object):
388    """A history configuration object.
389
390    @ivar maxchars: Limit the total number of characters in the history to "X"
391                    (where the character count is the characters of the complete XML stanzas, not only their XML character data).
392    @type maxchars: L{int}
393               
394    @ivar  maxstanzas:  Limit the total number of messages in the history to "X".
395    @type mazstanzas: L{int}
396
397    @ivar  seconds: Send only the messages received in the last "X" seconds.
398    @type seconds: L{int}
399
400    @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]).
401    @type since: L{datetime.datetime}
402
403    """
404    attributes = ['maxchars', 'maxstanzas', 'seconds', 'since']
405
406    def __init__(self, maxchars=None, maxstanzas=None, seconds=None, since=None):
407        self.maxchars   = maxchars
408        self.maxstanzas = maxstanzas
409        self.seconds    = seconds
410        self.since      = since
411
412    def toElement(self):
413        """Returns a L{domish.Element} representing the xml for the history options.
414        """
415        h = domish.Element((None, 'history'))
416        for key in self.attributes:
417            a = getattr(self, key, a)
418            if a is not None:
419                if key == 'since':
420                    h[key] = a.strftime('%Y%m%dT%H:%M:%S')
421                else:
422                    h[key] = str(a)
423       
424        return h
425
426class User(object):
427    """
428    A user/entity in a multi-user chat room.
429    """
430   
431    def __init__(self, nick, user_jid=None):
432        self.nick = nick
433        self.user_jid = user_jid
434        self.affiliation = 'none'
435        self.role = 'none'
436       
437        self.status = None
438        self.show   = None
439
440
441class Room(object):
442    """
443    A Multi User Chat Room. An in memory object representing a MUC room.
444    """
445
446   
447    def __init__(self, name, server, nick, state=None):
448        """ Initialize the room.
449       
450        @ivar name: The name of the MUC room.
451        @type name: L{unicode}
452
453        @ivar server: The server where the MUC room is located.
454        @type server: L{unicode}
455
456        @ivar nick: The nick name for the client in this room.
457        @type nick: L{unicode}
458
459        @ivar state: The status code of the room.
460        @type state: L{int}
461
462        @ivar entity_id: The jid of the room. Generated from name, server, and nick.
463        @type entity_id: L{jid.JID}
464
465        """
466        self.state  = state
467        self.name   = name
468        self.server = server
469        self.nick   = nick
470        self.status = 0
471
472        self.entity_id = self.entityId()
473               
474        self.roster = {}
475
476    def entityId(self):
477        """ Creates the L{jid.JID} for the room. If name, server, or nick change then this method
478        should be called.
479
480        """
481        self.entity_id = jid.internJID(self.name+'@'+self.server+'/'+self.nick)
482
483        return self.entity_id
484
485    def addUser(self, user):
486        """Add a user to the room roster.
487
488        @param user: The user object that is being added to the room.
489        @type  user: L{User}
490
491        """
492        self.roster[user.nick.lower()] = user
493
494    def inRoster(self, user):
495        """ Check if a user is in the MUC room.
496
497        @param user: The user object to check.
498        @type  user: L{User}
499
500        """
501
502        return self.roster.has_key(user.nick.lower())
503
504    def getUser(self, nick):
505        """ Get a user from the room's roster.
506
507        @param nick: The nick for the user in the MUC room.
508        @type  nick: L{unicode}
509
510        """
511        return self.roster.get(nick.lower())
512
513    def removeUser(self, user):
514        """ Remove a user from the MUC room's roster.
515       
516        @param user: The user object to check.
517        @type  user: L{User}
518       
519        """
520        if self.inRoster(user):
521            del self.roster[user.nick.lower()]
522       
523
524class BasicPresence(xmppim.AvailablePresence):
525    """
526    This behaves like an object providing L{domish.IElement}.
527
528    """
529
530    def __init__(self, to=None, show=None, statuses=None):
531        xmppim.AvailablePresence.__init__(self, to=to, show=show, statuses=statuses)
532        # add muc elements
533        x = self.addElement('x', NS_MUC)
534
535
536class UserPresence(xmppim.Presence):
537    """
538    This behaves like an object providing L{domish.IElement}.
539
540    """
541
542    def __init__(self, to=None, type=None, frm=None, affiliation=None, role=None):
543        xmppim.Presence.__init__(self, to, type)
544        if frm:
545            self['from'] = frm
546        # add muc elements
547        x = self.addElement('x', NS_MUC_USER)
548        if affiliation:
549            x['affiliation'] = affiliation
550        if role:
551            x['role'] = role
552
553class UnavailableUserPresence(xmppim.UnavailablePresence):
554    """
555    This behaves like an object providing L{domish.IElement}.
556
557    """
558
559    def __init__(self, to=None, type=None, frm=None, affiliation=None, role=None):
560        xmppim.UnavailablePresence.__init__(self, to, type)
561        if frm:
562            self['from'] = frm
563        # add muc elements
564        x = self.addElement('x', NS_MUC_USER)
565        if affiliation:
566            x['affiliation'] = affiliation
567        if role:
568            x['role'] = role
569
570
571class PasswordPresence(BasicPresence):
572    """ This behaves like an object providing L{domish.IElement}.
573    """
574    def __init__(self, to, password):
575        BasicPresence.__init__(self, to)
576       
577        self.x.addElement('password', None, password)
578
579
580class MessageVoice(GroupChat):
581    """  This behaves like an object providing L{domish.IElement}.
582    """
583    def __init__(self, to=None, frm=None):
584        GroupChat.__init__(self, to=to, frm=frm)
585        # build data form
586        form = data_form.Form('submit', formNamespace=NS_MUC_REQUEST)
587        form.addField(data_form.Field(var='muc#role',
588                                      value='participant', 
589                                      label='Requested role'))
590        self.addChild(form.toElement())           
591
592class PresenceError(xmppim.Presence):
593    """
594    This behaves like an object providing L{domish.IElement}.
595
596    """
597
598    def __init__(self, error, to=None, frm=None):
599        xmppim.Presence.__init__(self, to, type='error')
600        if frm:
601            self['from'] = frm
602        # add muc elements
603        x = self.addElement('x', NS_MUC)
604       
605        # add error
606        e = error.getElement()
607        self.addChild(e)
608       
609
610class MUCClient(XMPPHandler):
611    """
612    Multi-User chat client protocol. This is a subclass of L{XMPPHandler} and implements L{IMUCCLient}.
613
614    """
615
616    implements(IMUCClient)
617
618    rooms = {}
619
620    timeout = None
621
622    _deferreds = []
623
624    def connectionInitialized(self):
625        """ This method is called when the client has successfully authenticated.
626        It initializes several xpath events to handle MUC stanzas that come in.
627        After those are initialized then the method initialized is called to signal that we have finished.
628        """
629        self.xmlstream.addObserver(PRESENCE+"[not(@type) or @type='available']/x", self._onXPresence)
630        self.xmlstream.addObserver(PRESENCE+"[@type='unavailable']", self._onUnavailablePresence)
631        self.xmlstream.addObserver(PRESENCE+"[@type='error']", self._onPresenceError)
632        self.xmlstream.addObserver(GROUPCHAT, self._onGroupChat)
633        self.xmlstream.addObserver(SUBJECT, self._onSubject)
634        # add history
635
636        self.initialized()
637
638    def _setRoom(self, room):
639        """Add a room to the room collection.
640        """
641        self.rooms[room.entity_id.userhost().lower()] = room
642
643    def _getRoom(self, room_jid):
644        """Grab a room from the room collection.
645        """
646        return self.rooms.get(room_jid.userhost().lower())
647
648    def _removeRoom(self, room_jid):
649        """Delete a room from the room collection.
650        """
651        if self.rooms.has_key(room_jid.userhost().lower()):
652            del self.rooms[room_jid.userhost().lower()]
653
654
655    def _onUnavailablePresence(self, prs):
656        """ This method is called when the stanza matches the xpath observer.
657        The client has received a presence stanza with the 'type' attribute of unavailable.
658        It means a user has exited a MUC room.
659        """
660
661        if not prs.hasAttribute('from'):
662            return
663        room_jid = jid.internJID(prs.getAttribute('from', ''))
664        self._userLeavesRoom(room_jid)
665
666    def _onPresenceError(self, prs):
667        """This method is called when a presence stanza with the 'type' attribute of error.
668        There are various reasons for receiving a presence error and it means that the user has left the room.
669        """
670        if not prs.hasAttribute('from'):
671            return
672        room_jid = jid.internJID(prs.getAttribute('from', ''))
673        # add an error hook here?
674        self._userLeavesRoom(room_jid)
675       
676    def _getExceptionFromElement(self, stanza):
677        # find an exception based on the error stanza
678        muc_condition = 'exception'
679
680        error = getattr(stanza, 'error', None)
681        if error is not None:
682            for e in error.elements():
683                muc_condition = e.name
684
685        return MUC_EXCEPTIONS[muc_condition]
686
687    def _userLeavesRoom(self, room_jid):
688        # when a user leaves a room we need to update it
689        room = self._getRoom(room_jid)
690        if room is None:
691            # not in the room yet
692            return
693        # check if user is in roster
694        user = room.getUser(room_jid.resource)
695        if user is None:
696            return
697        if room.inRoster(user):
698            room.removeUser(user)
699            self.userLeftRoom(room, user)
700       
701    def _onXPresence(self, prs):
702        """ A muc presence has been received.
703        """
704        if not prs.hasAttribute('from'):
705            return
706        room_jid = jid.internJID(prs.getAttribute('from', ''))
707           
708        status = getattr(prs, 'status', None)
709        show   = getattr(prs, 'show', None)
710       
711        # grab room
712        room = self._getRoom(room_jid)
713        if room is None:
714            # not in the room yet
715            return
716        user = self._changeUserStatus(room, room_jid, status, show)
717
718        if room.inRoster(user):
719            # we changed status or nick
720            muc_status = getattr(prs.x, 'status', None)
721            if muc_status:
722                code = muc_status.getAttribute('code', 0)
723            else:
724                self.userUpdatedStatus(room, user, show, status)
725        else:           
726            room.addUser(user)
727            self.userJoinedRoom(room, user)
728           
729
730    def _onGroupChat(self, msg):
731        """ A group chat message has been received from a MUC room.
732       
733        There are a few event methods that may get called here. receviedGroupChat and receivedHistory
734        """
735        if not msg.hasAttribute('from'):
736            # need to return an error here
737            return
738        room_jid = jid.internJID(msg.getAttribute('from', ''))
739
740        room = self._getRoom(room_jid)
741        if room is None:
742            # not in the room yet
743            return
744        user = room.getUser(room_jid.resource)
745        delay = None
746        # need to check for delay and x stanzas for delay namespace for backwards compatability
747        for e in msg.elements():
748            if e.uri == NS_DELAY or e.uri == NS_JABBER_DELAY:
749                delay = e
750        body  = unicode(msg.body)
751        # grab room
752        if delay is None:
753            self.receivedGroupChat(room, user, body)
754        else:
755            self.receivedHistory(room, user, body, delay['stamp'], frm=delay.getAttribute('from',None))
756
757
758    def _onSubject(self, msg):
759        """A subject has been sent from a MUC room.
760        """
761        if not msg.hasAttribute('from'):
762            return
763        room_jid = jid.internJID(msg['from'])
764
765        # grab room
766        room = self._getRoom(room_jid)
767        if room is None:
768            # not in the room yet
769            return
770
771        self.receivedSubject(room_jid, unicode(msg.subject))
772
773
774    def _makeTimeStamp(self, stamp=None):
775        # create a timestamp
776        if stamp is None:
777            stamp = datetime.datetime.now()
778           
779        return stamp.strftime('%Y%m%dT%H:%M:%S')
780
781
782    def _joinedRoom(self, d, prs):
783        """We have presence that says we joined a room.
784        """
785        room_jid = jid.internJID(prs['from'])
786       
787        # check for errors
788        if prs.hasAttribute('type') and prs['type'] == 'error':           
789            d.errback(self._getExceptionFromElement(prs))
790        else:   
791            # change the state of the room
792            r = self._getRoom(room_jid)
793            if r is None:
794                raise NotFound
795            r.state = 'joined'
796           
797            # grab status
798            status = getattr(prs.x,'status',None)
799            if status:
800                r.status = status.getAttribute('code', None)
801
802            d.callback(r)
803
804
805    def _leftRoom(self, d, prs):
806        """We have presence that says we joined a room.
807        """
808        room_jid = jid.internJID(prs['from'])
809       
810        # check for errors
811        if prs.hasAttribute('type') and prs['type'] == 'error':           
812            d.errback(self._getExceptionFromElement(prs))
813        else:   
814            # change the state of the room
815            r = self._getRoom(room_jid)
816            if r is None:
817                raise NotFound
818            self._removeRoom(room_jid)
819           
820            d.callback(True)
821
822    def initialized(self):
823        """Client is initialized and ready!
824        """
825        pass
826
827    def userJoinedRoom(self, room, user):
828        """User has joined a MUC room.
829
830        This method will need to be modified inorder for clients to
831        do something when this event occurs.
832
833        @param room: The room the user has joined.
834        @type  room: L{Room}
835
836        @param user: The user that joined the MUC room.
837        @type  user: L{User}
838
839        """
840        pass
841
842    def userLeftRoom(self, room, user):
843        """User has left a room.
844       
845        This method will need to be modified inorder for clients to
846        do something when this event occurs.
847
848        @param room: The room the user has joined.
849        @type  room: L{Room}
850
851        @param user: The user that joined the MUC room.
852        @type  user: L{User}
853
854        """
855        pass
856
857
858    def userUpdatedStatus(self, room, user, show, status):
859        """User Presence has been received
860       
861        This method will need to be modified inorder for clients to
862        do something when this event occurs.
863       
864        """
865        pass
866       
867
868    def receivedSubject(self, room, subject):
869        """
870        This method will need to be modified inorder for clients to
871        do something when this event occurs.
872
873        """
874        pass
875
876
877    def receivedHistory(self, room, user, message, history, frm=None):
878        """
879        This method will need to be modified inorder for clients to
880        do something when this event occurs.
881        """
882        pass
883
884
885    def _cbDisco(self, iq):
886        # grab query
887       
888        return getattr(iq,'query', None)
889       
890
891    def sendDeferred(self,  obj, timeout):
892        """ Send data or a domish element, adding a deferred with a timeout.
893       
894        @param obj: The object to send over the wire.
895        @type  obj: L{domish.Element} or L{unicode}
896
897        @param timeout: The number of seconds to wait before the deferred is timed out.
898        @type  timeout: L{int}
899
900        The deferred object L{defer.Deferred} is returned.
901        """
902        d = defer.Deferred()
903        self._deferreds.append(d)
904
905
906        def onTimeout():
907            i = 0
908            for xd in self._deferreds:
909                if d == xd:
910                    self._deferreds.pop(i)
911                    d.errback(xmlstream.TimeoutError("Timeout waiting for response."))
912                i += 1
913
914        call = reactor.callLater(timeout, onTimeout)
915       
916        def cancelTimeout(result):
917            if call.active():
918                call.cancel()
919
920            return result
921
922        d.addBoth(cancelTimeout)
923
924        self.xmlstream.send(obj)
925        return d
926
927    def disco(self, entity, type='info'):
928        """Send disco queries to a XMPP entity.
929
930        @param entity: The server or entity where we want discovery information from.
931        @type  entity: L{jid.JID}
932
933        @param type: The optional disco type.
934        @type  type: L{unicode}
935
936        """
937
938        iq = disco.DiscoRequest(self.xmlstream, disco.NS_INFO, 'get')
939        iq['to'] = entity
940
941        return iq.send().addBoth(self._cbDisco)
942       
943
944    def configure(self, room_jid, fields=[]):
945        """Configure a room
946
947        @param room_jid: The room jabber/xmpp entity id for the requested configuration form.
948        @type  room_jid: L{jid.JID}
949
950        @param fields: The fields we want to modify.
951        @type  fields: A L{list} or L{dataform.Field}
952
953        """
954        request = ConfigureRequest(self.xmlstream, method='set', fields=fields)
955        request['to'] = room_jid
956       
957        return request.send()
958
959    def getConfigureForm(self, room_jid):
960        """Grab the configuration form from the room. This sends an iq request to the room.
961
962        @param room_jid: The room jabber/xmpp entity id for the requested configuration form.
963        @type  room_jid: L{jid.JID}
964
965        """
966        request = ConfigureRequest(self.xmlstream)
967        request['to'] = room_jid
968        return request.send()
969
970
971    def join(self, server, room, nick, history = None):
972        """ Join a MUC room by sending presence to it. Returns a defered that is called when
973        the entity is in the room or an error has occurred.
974       
975        @param server: The server where the room is located.
976        @type  server: L{unicode}
977
978        @param room: The room name the entity is joining.
979        @type  room: L{unicode}
980
981        @param nick: The nick name for the entitity joining the room.
982        @type  nick: L{unicode}
983       
984        @param history: The maximum number of history stanzas you would like.
985
986        """
987        r = Room(room, server, nick, state='joining')
988        self._setRoom(r)
989 
990        p = BasicPresence(to=r.entity_id)
991        if history is not None:
992            p.x.addChild(history.toElement())
993
994        d = self.sendDeferred(p, timeout=DEFER_TIMEOUT)
995
996        # add observer for joining the room
997        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s']" % (r.entity_id.full()), 
998                                          self._joinedRoom, 1, d)
999
1000        return d
1001   
1002    def _changeUserStatus(self, r, room_jid, status, show):
1003        # change the user status in a room.
1004
1005        # check if user is in roster
1006        user = r.getUser(room_jid.resource)
1007        if user is None: # create a user that does not exist
1008            user = User(room_jid.resource)
1009
1010        if status is not None:
1011            user.status = str(status)
1012        if show is not None:
1013            user.show   = str(show)
1014
1015        return user
1016
1017    def _changed(self, d, room_jid, prs):
1018        """Callback for changing the nick and status.
1019        """
1020
1021        status = getattr(prs, 'status', None)
1022        show   = getattr(prs, 'show', None)
1023
1024        r = self._getRoom(room_jid)
1025
1026        user = self._changeUserStatus(r, room_jid, status, show)
1027
1028        d.callback(r)
1029
1030
1031    def nick(self, room_jid, new_nick):
1032        """ Change an entities nick name in a MUC room.
1033       
1034        See: http://xmpp.org/extensions/xep-0045.html#changenick
1035
1036        @param room_jid: The room jabber/xmpp entity id for the requested configuration form.
1037        @type  room_jid: L{jid.JID}
1038
1039        @param new_nick: The nick name for the entitity joining the room.
1040        @type  new_nick: L{unicode}
1041       
1042        """
1043
1044       
1045        r = self._getRoom(room_jid)
1046        if r is None:
1047            raise NotFound
1048        r.nick = new_nick # change the nick
1049        # create presence
1050        # make sure we call the method to generate the new entity xmpp id
1051        p = BasicPresence(to=r.entityId()) 
1052        d = self.sendDeferred(p, timeout=DEFER_TIMEOUT)
1053
1054        # add observer for joining the room
1055        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s']" % (r.entity_id.full()), 
1056                                          self._changed, 1, d, room_jid)
1057
1058        return d
1059       
1060
1061   
1062    def leave(self, room_jid):
1063        """Leave a MUC room.
1064
1065        See: http://xmpp.org/extensions/xep-0045.html#exit
1066
1067        @param room_jid: The room entity id you want to exit.
1068        @type  room_jid: L{jid.JID}
1069
1070        """
1071        r = self._getRoom(room_jid)
1072 
1073        p = xmppim.UnavailablePresence(to=r.entity_id)
1074
1075        d = self.sendDeferred(p, timeout=DEFER_TIMEOUT)
1076        # add observer for joining the room
1077        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s' and @type='unavailable']" % (r.entity_id.full()), 
1078                                          self._leftRoom, 1, d)
1079
1080        return d
1081   
1082
1083    def status(self, room_jid, show=None, status=None):
1084        """Change user status.
1085
1086        See: http://xmpp.org/extensions/xep-0045.html#changepres
1087
1088        @param room_jid: The room jabber/xmpp entity id for the requested configuration form.
1089        @type  room_jid: L{jid.JID}
1090
1091        @param show: The availability of the entity. Common values are xa, available, etc
1092        @type  show: L{unicode}
1093
1094        @param show: The current status of the entity.
1095        @type  show: L{unicode}
1096
1097        """
1098        r = self._getRoom(room_jid)
1099        if r is None:
1100            raise NotFound
1101
1102        p = BasicPresence(to=r.entityId()) 
1103        if status is not None:
1104            p.addElement('status', None, status)
1105           
1106        if show is not None:
1107            p.addElement('show', None, show)
1108           
1109        d = self.sendDeferred(p, timeout=DEFER_TIMEOUT)
1110
1111        # add observer for joining the room
1112        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s']" % (r.entity_id.full()), 
1113                                          self._changed, 1, d, room_jid)
1114
1115        return d
1116
1117    def _sendMessage(self, msg, children=None):
1118        # send a message
1119        if children:
1120            for c in children:
1121                msg.addChild(c)
1122       
1123        self.xmlstream.send(msg)
1124
1125    def groupChat(self, to, message, children=None):
1126        """Send a groupchat message
1127        """
1128        msg = GroupChat(to, body=message)
1129       
1130        self._sendMessage(msg, children=children)
1131
1132    def chat(self, room_jid, message, children=None):
1133        """Send a private chat message to a user in a MUC room.
1134       
1135        See: http://xmpp.org/extensions/xep-0045.html#privatemessage
1136
1137        @param room_jid: The room entity id.
1138        @type  room_jid: L{jid.JID}
1139
1140        """
1141
1142        msg = PrivateChat(room_jid, body=message)
1143
1144        self._sendMessage(msg, children=children)
1145       
1146    def invite(self, room_jid, reason=None, full_jid=None):
1147        """Invite a xmpp entity to a MUC room.
1148
1149        See: http://xmpp.org/extensions/xep-0045.html#invite
1150
1151        @param room_jid: The room entity id.
1152        @type  room_jid: L{jid.JID}
1153       
1154        @param reason: The reason for the invite.
1155        @type  reason: L{unicode}
1156
1157        @param full_jid: The xmpp user's entity id.
1158        @type  full_jid: L{jid.JID}
1159
1160        """
1161        msg = InviteMessage(room_jid, reason=reason, full_jid=full_jid)
1162        self._sendMessage(msg)
1163
1164
1165    def password(self, room_jid, password):
1166        """Send a password to a room so the entity can join.
1167       
1168        See: http://xmpp.org/extensions/xep-0045.html#enter-pw
1169
1170        @param room_jid: The room entity id.
1171        @type  room_jid: L{jid.JID}
1172       
1173        @param password: The MUC room password.
1174        @type  password: L{unicode}
1175       
1176        """
1177        p = PasswordPresence(room_jid, password)
1178
1179        self.xmlstream.send(p)
1180   
1181    def register(self, room_jid, fields=[]):
1182        """ Send a request to register for a room.
1183
1184        @param room_jid: The room entity id.
1185        @type  room_jid: L{jid.JID}
1186
1187        @param fields: The fields you need to register.
1188        @type  fields: L{list} of L{dataform.Field}
1189
1190        """
1191        iq = RegisterRequest(self.xmlstream, method='set', fields=fields)
1192        iq['to'] = room_jid.userhost()
1193        return iq.send()
1194
1195
1196    def _getAffiliationList(self, room_jid, affiliation):
1197        # send a request for an affiliation list in a room.
1198        iq = AffiliationRequest(self.xmlstream,
1199                                method='get',
1200                                affiliation=affiliation, 
1201                                )
1202        iq['to'] = room_jid.full()
1203        return iq.send()       
1204
1205
1206    def _getRoleList(self, room_jid, role):
1207        # send a role request
1208        iq = RoleRequest(self.xmlstream,
1209                                method='get',
1210                                role=role,
1211                                )
1212        iq['to'] = room_jid.full()
1213        return iq.send()       
1214
1215
1216    def _setAffiliationList(self, affiliation, room_jid, iq):
1217        # set a rooms affiliation list
1218        r = self._getRoom(room_jid)
1219        if r is not None:
1220            affiliation_list = []
1221            setattr(r, affiliation, [])
1222           
1223            for item in iq.query.elements():
1224                nick   = item.getAttribute('nick', None)
1225                entity = item.getAttribute('jid', None)
1226                u      = None
1227                if nick is None and entity is None:
1228                    raise Exception, 'bad attributes in item list'
1229                if nick is not None:
1230                    u = room.getUser(nick)
1231                if u is None:
1232                    u = User(nick, user_jid=jid.internJID(entity))
1233                    u.affiliation = 'member'
1234                   
1235                affiliation_list.append(u)
1236
1237            setattr(r, affiliation, affiliation_list)
1238        return r
1239
1240    def getMemberList(self, room_jid):
1241        """ Get a member list from a room.
1242
1243        @param room_jid: The room jabber/xmpp entity id for the requested member list.
1244        @type  room_jid: L{jid.JID}
1245
1246        """
1247        d = self._getAffiliationList(room_jid, 'member')
1248        d.addCallback(self._setAffiliationList, 'members', room_jid)
1249        return d
1250
1251    def getAdminList(self, room_jid):
1252        """ Get an admin list from a room.
1253
1254        @param room_jid: The room jabber/xmpp entity id for the requested member list.
1255        @type  room_jid: L{jid.JID}
1256
1257        """
1258        d = self._getAffiliationList(room_jid, 'admin')
1259        d.addCallback(self._setAffiliationList, 'members', room_jid)
1260        return d
1261
1262    def getBanList(self, room_jid):
1263        """ Get an outcast list from a room.
1264
1265        @param room_jid: The room jabber/xmpp entity id for the requested member list.
1266        @type  room_jid: L{jid.JID}
1267
1268        """
1269        d = self._getAffiliationList(room_jid, 'outcast')
1270        d.addCallback(self._setAffiliationList, 'members', room_jid)
1271        return d
1272
1273    def getOwnerList(self, room_jid):
1274        """ Get an owner list from a room.
1275
1276        @param room_jid: The room jabber/xmpp entity id for the requested member list.
1277        @type  room_jid: L{jid.JID}
1278
1279        """
1280        d = self._getAffiliationList(room_jid, 'owner')
1281        d.addCallback(self._setAffiliationList, 'members', room_jid)
1282        return d
1283
1284    def getRegisterForm(self, room):
1285        """ Grab the registration form for a MUC room.
1286
1287        @param room: The room jabber/xmpp entity id for the requested registration form.
1288        @type  room: L{jid.JID}
1289
1290        """
1291        iq = RegisterRequest(self.xmlstream)
1292        iq['to'] = room.userhost()
1293        return iq.send()
1294
1295    def destroy(self, room_jid, reason=None):
1296        """ Destroy a room.
1297       
1298        @param room_jid: The room jabber/xmpp entity id.
1299        @type  room_jid: L{jid.JID}
1300       
1301        @ivar reason: The reason we are destroying the room.
1302        @type reason: L{unicode}
1303
1304        """
1305        def destroyed(iq):
1306            self._removeRoom(room_jid)
1307            return True
1308
1309        iq = OwnerRequest(self.xmlstream, method='set')
1310        d  = iq.query.addElement('destroy')
1311        d['jid'] = room_jid.userhost()
1312        if reason is not None:
1313            d.addElement('reason', None, reason)
1314
1315        return iq.send().addCallback(destroyed)
1316
1317    def subject(self, room_jid, subject):
1318        """ Change the subject of a MUC room.
1319
1320        See: http://xmpp.org/extensions/xep-0045.html#subject-mod
1321
1322        @param room_jid: The room jabber/xmpp entity id.
1323        @type  room_jid: L{jid.JID}
1324
1325        @param subject: The subject you want to set.
1326        @type  subject: L{unicode}
1327
1328        """
1329        msg = GroupChat(room_jid, subject=subject)
1330        self.xmlstream.send(msg)
1331
1332    def voice(self, room_jid):
1333        """ Request voice for a moderated room.
1334
1335        @param room_jid: The room jabber/xmpp entity id.
1336        @type  room_jid: L{jid.JID}
1337
1338        """
1339        msg = MessageVoice(to=room_jid)
1340        self.xmlstream.send(msg)
1341
1342
1343
1344    def history(self, room_jid, message_list):
1345        """ Send history to create a MUC based on a one on one chat.
1346       
1347        See: http://xmpp.org/extensions/xep-0045.html#continue
1348
1349        @param room_jid: The room jabber/xmpp entity id.
1350        @type  room_jid: L{jid.JID}
1351
1352        @param message_list: A list of history to send to the room.
1353        @type  message_list: L{list} of L{domish.Element}
1354
1355        """
1356       
1357        for m in message_list:
1358            m['type'] = 'groupchat'
1359            mto = m['to']
1360            frm = m.getAttribute('from', None)
1361            m['to'] = room_jid.userhost()
1362
1363            d = m.addElement('delay', NS_DELAY)
1364            d['stamp'] = self._makeTimeStamp()
1365            d['from'] = mto
1366
1367            self.xmlstream.send(m)
1368
1369    def _setAffiliation(self, frm, room_jid, affiliation, reason=None, a_jid=None, nick=None):
1370        # Send an affiliation request to change an entities affiliation to a MUC room.
1371        iq = AffiliationRequest(self.xmlstream,
1372                                method='set',
1373                                affiliation=affiliation,
1374                                a_jid=a_jid, 
1375                                nick=nick,
1376                                reason=reason)
1377        iq['to'] = room_jid.userhost() # this is a room jid, only send to room
1378        iq['from'] = frm.full()
1379        return iq.send()
1380
1381    def _setRole(self, frm, room_jid, a_jid=None, reason=None, role='none', nick=None):
1382        # send a role request
1383        iq = RoleRequest(self.xmlstream,
1384                         method='set',
1385                         a_jid=a_jid,
1386                         role=role,
1387                         nick=nick,
1388                         reason=reason)
1389       
1390        iq['to'] = room_jid.userhost() # this is a room jid, only send to room
1391        iq['from'] = frm.full()
1392        return iq.send()
1393
1394    def _cbRequest(self, room_jid, iq):
1395        r = self._getRoom(room_jid)
1396        if r is None:
1397            raise NotFound
1398
1399        return r
1400
1401    def modifyAffiliationList(self, frm, room_jid, jid_list, affiliation):
1402        """Modify an affiliation list.
1403
1404        @param frm: The entity sending the request.
1405        @type  frm: L{jid.JID}
1406
1407        @param room_jid: The room jabber/xmpp entity id.
1408        @type  room_jid: L{jid.JID}
1409
1410        @param jid_list: The list of jabber ids to change in a room. This can be a nick or a full jid.
1411        @type  jid_list: L{list} of L{unicode} for nicks. L{list} of L{jid.JID} for jids.
1412
1413        @param affiliation: The affilation to change.
1414        @type  affiliation: L{unicode}
1415
1416
1417        """
1418        iq = AffiliationRequest(self.xmlstream,
1419                                method='set',
1420                                affiliation=affiliation,
1421                                )
1422        iq.items(jid_list)
1423        iq['to'] = room_jid.userhost() # this is a room jid, only send to room
1424        iq['from'] = frm.full()
1425        return iq.send()
1426
1427    def grantVoice(self, frm, room_jid, voice_jid=None, reason=None, nick=None):
1428        """ Grant voice to an entity.
1429       
1430        @param frm: The entity sending the request.
1431        @type  frm: L{jid.JID}
1432
1433        @param room_jid: The room jabber/xmpp entity id.
1434        @type  room_jid: L{jid.JID}
1435       
1436        @param reason: The reason for granting voice to the entity.
1437        @type  reason: L{unicode}
1438
1439        @param nick: The nick name for the client in this room.
1440        @type  nick: L{unicode}
1441
1442        """
1443        return self._setRole(frm, room_jid, role='participant', a_jid=voice_jid, nick=nick, reason=reason)
1444
1445    def grantVisitor(self, frm, room_jid, reason=None, nick=None):
1446        """ Change a participant to a visitor. This will disallow the entity to send messages to a moderated room.
1447        @param frm: The entity sending the request.
1448        @type  frm: L{jid.JID}
1449
1450        @param room_jid: The room jabber/xmpp entity id.
1451        @type  room_jid: L{jid.JID}
1452       
1453        @param reason: The reason for granting voice to the entity.
1454        @type  reason: L{unicode}
1455
1456        @param nick: The nick name for the client in this room.
1457        @type  nick: L{unicode}
1458
1459        """
1460        return self._setRole(frm, room_jid, role='visitor', reason=reason, nick=nick)
1461
1462    def grantModerator(self, frm, room_jid, reason=None, nick=None):
1463        """Grant moderator priviledges to a MUC room.
1464
1465        @param frm: The entity sending the request.
1466        @type  frm: L{jid.JID}
1467
1468        @param room_jid: The room jabber/xmpp entity id.
1469        @type  room_jid: L{jid.JID}
1470
1471        """
1472        return self._setRole(frm, room_jid, role='moderator', reason=reason, nick=nick)
1473
1474    def ban(self, room_jid, ban_jid, frm, reason=None, nick=None):
1475        """Ban a user from a MUC room.
1476
1477        @param room_jid: The room jabber/xmpp entity id.
1478        @type  room_jid: L{jid.JID}
1479
1480        @param ban_jid: The room jabber/xmpp entity id.
1481        @type  ban_jid: L{jid.JID}
1482
1483        @param frm: The entity sending the request.
1484        @type  frm: L{jid.JID}
1485
1486        @param reason: The reason for banning the entity.
1487        @type  reason: L{unicode}
1488
1489        @param nick: The nick name for the client in this room.
1490        @type  nick: L{unicode}
1491
1492        """
1493        return self._setAffiliation(frm, room_jid, 'outcast', nick=nick, a_jid=ban_jid, reason=reason)
1494
1495    def kick(self, room_jid, kick_jid, frm, reason=None, nick=None):       
1496        """Kick a user from a MUC room.
1497
1498        @param room_jid: The room jabber/xmpp entity id.
1499        @type  room_jid: L{jid.JID}
1500
1501        @param kick_jid: The room jabber/xmpp entity id.
1502        @type  kick_jid: L{jid.JID}
1503
1504        @param frm: The entity sending the request.
1505        @type  frm: L{jid.JID}
1506
1507        @param reason: The reason given for the kick.
1508        @type  reason: L{unicode}
1509
1510        @param nick: The nick for the user in the MUC room.
1511        @type  nick: L{unicode}
1512
1513        """
1514        return self._setAffiliation(frm, room_jid, 'none', a_jid=kick_jid, nick=nick, reason=reason)
1515       
Note: See TracBrowser for help on using the repository browser.