source: wokkel/muc.py @ 130:45c3ce4fe4eb

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

Another round of cleanups, renames. Also removed disco support.

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