source: wokkel/muc.py @ 137:8206d7483945

wokkel-muc-client-support-24
Last change on this file since 137:8206d7483945 was 137:8206d7483945, checked in by Ralph Meijer <ralphm@…>, 9 years ago

Use JIDs everywhere and adjust tests.

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