source: wokkel/muc.py @ 139:d3dd88d429fb

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

Remove some left over error related classes.

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