source: wokkel/muc.py @ 129:29e517800b96

wokkel-muc-client-support-24
Last change on this file since 129:29e517800b96 was 129:29e517800b96, checked in by Ralph Meijer <ralphm@…>, 13 years ago

Whitespace and pyflakes changes.

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