source: wokkel/muc.py @ 140:46bd9821f979

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

Remove unused constants.

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