source: wokkel/muc.py @ 117:2503218c0e95

wokkel-muc-client-support-24
Last change on this file since 117:2503218c0e95 was 117:2503218c0e95, checked in by Christopher Zorn <tofu@…>, 13 years ago

we now have basic support with nick change, also added some doc strings, still need more re #24

File size: 22.3 KB
Line 
1# -*- test-case-name: wokkel.test.test_muc -*-
2#
3# Copyright (c) 2003-2008 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
17from twisted.words.protocols.jabber import jid, error, xmlstream
18from twisted.words.xish import domish
19
20from wokkel import disco, data_form, shim, xmppim
21from wokkel.subprotocols import IQHandlerMixin, 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# ad hoc commands
40NS_AD_HOC       = "http://jabber.org/protocol/commands"
41
42
43# Iq get and set XPath queries
44IQ     = '/iq'
45IQ_GET = IQ+'[@type="get"]'
46IQ_SET = IQ+'[@type="set"]'
47
48IQ_RESULT = IQ+'[@type="result"]'
49IQ_ERROR  = IQ+'[@type="error"]'
50
51IQ_QUERY     = IQ+'/query'
52IQ_GET_QUERY = IQ_GET + '/query'
53IQ_SET_QUERY = IQ_SET + '/query'
54
55IQ_COMMAND   = IQ+'/command'
56
57MUC_ADMIN = IQ_QUERY+'[@xmlns="' + NS_MUC_ADMIN + '"]'
58MUC_OWNER = IQ_QUERY+'[@xmlns="' + NS_MUC_OWNER + '"]'
59
60MUC_AO = MUC_ADMIN + '|' + MUC_OWNER
61
62
63MESSAGE  = '/message'
64PRESENCE = '/presence'
65
66CHAT_BODY = MESSAGE +'[@type="chat"]/body'
67CHAT      = MESSAGE +'[@type="chat"]'
68
69GROUPCHAT     = MESSAGE +'[@type="groupchat"]/body'
70SUBJECT       = MESSAGE +'[@type="groupchat"]/subject'
71MESSAGE_ERROR = MESSAGE +'[@type="error"]'
72
73STATUS_CODES = { # see http://www.xmpp.org/extensions/xep-0045.html#registrar-statuscodes
74    100:
75        {'name':'fulljid',
76         'stanza':'presence',
77         
78         },
79    201: 
80        {'name':'created', 
81         'stanza': 'presence',
82         'context':'Entering a room',
83         'purpose':'Inform user that a new room has been created'
84         },   
85}
86
87STATUS_CODE_CREATED = 201
88
89
90class MUCError(error.StanzaError):
91    """
92    Exception with muc specific condition.
93    """
94    def __init__(self, condition, mucCondition, feature=None, text=None):
95        appCondition = domish.Element((NS_MUC, mucCondition))
96        if feature:
97            appCondition['feature'] = feature
98        error.StanzaError.__init__(self, condition,
99                                         text=text,
100                                         appCondition=appCondition)
101
102
103class BadRequest(MUCError):
104    """
105    Bad request stanza error.
106    """
107    def __init__(self, mucCondition=None, text=None):
108        MUCError.__init__(self, 'bad-request', mucCondition, text)
109
110
111
112class Unsupported(MUCError):
113    def __init__(self, feature, text=None):
114        MUCError.__init__(self, 'feature-not-implemented',
115                          'unsupported',
116                          feature,
117                          text)
118
119
120
121class ConfigureRequest(xmlstream.IQ):
122    """
123    Configure MUC room request.
124
125    http://xmpp.org/extensions/xep-0045.html#roomconfig
126
127    @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'}
128    @type method: C{str}
129    """
130
131    def __init__(self, xs, method='get', fields=[]):
132        xmlstream.IQ.__init__(self, xs, method)
133        q = self.addElement((NS_MUC_OWNER, 'query'))
134        if method == 'set':
135            # build data form
136            form = data_form.Form('submit', formNamespace=NS_MUC_CONFIG)
137            q.addChild(form.toElement())
138           
139            for f in fields:
140                # create a field
141                form.addField(f)
142
143
144class RegisterRequest(xmlstream.IQ):
145    """
146    Register room request.
147
148    @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'}
149    @type method: C{str}
150
151    """
152
153    def __init__(self, xs, method='get', fields=[]):
154        xmlstream.IQ.__init__(self, xs, method)
155        q = self.addElement((NS_REQUEST, 'query'))
156        if method == 'set':
157            # build data form
158            form_type = 'submit'       
159            form = data_form.Form(form_type, formNamespace=NS_MUC_REGISTER)
160            q.addChild(form.toElement())       
161           
162            for f in fields:
163                # create a field
164                form.addField(f)
165
166
167class AffiliationRequest(xmlstream.IQ):
168    """
169    Register room request.
170
171    @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'}
172    @type method: C{str}
173
174    @ivar affiliation: The affiliation type to send to room.
175    @type affiliation: C{str}
176
177    """
178
179    def __init__(self, xs, method='get', affiliation='none', a_jid=None, reason=None):
180        xmlstream.IQ.__init__(self, xs, method)
181       
182        q = self.addElement((NS_MUC_ADMIN, 'query'))
183        i = q.addElement('item')
184
185        i['affiliation'] = affiliation
186        if a_jid:
187            i['jid'] = a_jid.full()
188           
189        if reason:
190            i.addElement('reason', None, reason)
191
192           
193       
194
195class GroupChat(domish.Element):
196    """
197    """
198    def __init__(self, to, body=None, subject=None, frm=None):
199        """To needs to be a string
200        """
201        domish.Element.__init__(self, (None, 'message'))
202        self['type'] = 'groupchat'
203        if isinstance(to, jid.JID):
204            self['to'] = to.userhost()
205        else:
206            self['to'] = to
207        if frm:
208            self['from'] = frm
209        if body:
210            self.addElement('body',None, body)
211        if subject:
212            self.addElement('subject',None, subject)
213
214
215class PrivateChat(domish.Element):
216    """
217    """
218    def __init__(self, to, body=None, frm=None):
219        """To needs to be a string
220        """
221        domish.Element.__init__(self, (None, 'message'))
222        self['type'] = 'chat'
223        self['to']   = to
224        if frm:
225            self['from'] = frm
226        if body:
227            self.addElement('body',None, body)
228           
229class InviteMessage(PrivateChat):
230    def __init__(self, to, reason=None, full_jid=None, body=None, frm=None, password=None):
231        PrivateChat.__init__(self, to, body=body, frm=frm)
232        del self['type'] # remove type
233        x = self.addElement('x', NS_MUC_USER)
234        invite = x.addElement('invite')
235        if full_jid:
236            invite['to'] = full_jid
237        if reason:
238            invite.addElement('reason', None, reason)
239        if password:
240            invite.addElement('password', None, password)
241
242class HistoryMessage(GroupChat):
243    """
244    """
245    def __init__(self, to, stamp, body=None, subject=None, frm=None, h_frm=None):
246        GroupChat.__init__(self, to, body=body, subject=subject, frm=frm)
247        d = self.addElement('delay', NS_DELAY)
248        d['stamp'] = stamp
249        if h_frm:
250            d['from'] = h_frm
251
252class User(object):
253    """
254    A user/entity in a multi-user chat room.
255    """
256   
257    def __init__(self, nick, user_jid=None):
258        self.nick = nick
259        self.user_jid = user_jid
260        self.affiliation = 'none'
261        self.role = 'none'
262       
263        self.status = None
264        self.show   = None
265
266
267class Room(object):
268    """
269    A Multi User Chat Room
270    """
271
272   
273    def __init__(self, name, server, nick, state=None):
274        """
275        """
276        self.state  = state
277        self.name   = name
278        self.server = server
279        self.nick   = nick
280        self.status = 0
281
282        self.entity_id = self.entityId()
283               
284        self.roster = {}
285
286    def entityId(self):
287        """
288        """
289        self.entity_id = jid.internJID(self.name+'@'+self.server+'/'+self.nick)
290
291        return self.entity_id
292
293    def addUser(self, user):
294        """
295        """
296        self.roster[user.nick.lower()] = user
297
298    def inRoster(self, user):
299        """
300        """
301
302        return self.roster.has_key(user.nick.lower())
303
304    def getUser(self, nick):
305        """
306        """
307        return self.roster.get(nick.lower())
308
309    def removeUser(self, user):
310        if self.inRoster(user):
311            del self.roster[user.nick.lower()]
312       
313
314class BasicPresence(xmppim.AvailablePresence):
315    """
316    This behaves like an object providing L{domish.IElement}.
317
318    """
319
320    def __init__(self, to=None, show=None, statuses=None):
321        xmppim.AvailablePresence.__init__(self, to=to, show=show, statuses=statuses)
322        # add muc elements
323        x = self.addElement('x', NS_MUC)
324
325
326class UserPresence(xmppim.Presence):
327    """
328    This behaves like an object providing L{domish.IElement}.
329
330    """
331
332    def __init__(self, to=None, type=None, frm=None, affiliation=None, role=None):
333        xmppim.Presence.__init__(self, to, type)
334        if frm:
335            self['from'] = frm
336        # add muc elements
337        x = self.addElement('x', NS_MUC_USER)
338        if affiliation:
339            x['affiliation'] = affiliation
340        if role:
341            x['role'] = role
342
343
344class PasswordPresence(BasicPresence):
345    """
346    """
347    def __init__(self, to, password):
348        BasicPresence.__init__(self, to)
349       
350        self.x.addElement('password', None, password)
351
352
353class MessageVoice(GroupChat):
354    """
355    """
356    def __init__(self, to=None, frm=None):
357        GroupChat.__init__(self, to=to, frm=frm)
358        # build data form
359        form = data_form.Form('submit', formNamespace=NS_MUC_REQUEST)
360        form.addField(data_form.Field(var='muc#role',
361                                      value='participant', 
362                                      label='Requested role'))
363        self.addChild(form.toElement())           
364
365class PresenceError(xmppim.Presence):
366    """
367    This behaves like an object providing L{domish.IElement}.
368
369    """
370
371    def __init__(self, error, to=None, frm=None):
372        xmppim.Presence.__init__(self, to, type='error')
373        if frm:
374            self['from'] = frm
375        # add muc elements
376        x = self.addElement('x', NS_MUC)
377        # add error
378        self.addChild(error)
379       
380
381class MUCClient(XMPPHandler):
382    """
383    Multi-User chat client protocol.
384    """
385
386    implements(IMUCClient)
387
388    rooms = {}
389
390    def connectionInitialized(self):
391        self.xmlstream.addObserver(PRESENCE+"[not(@type) or @type='available']/x", self._onXPresence)
392        self.xmlstream.addObserver(PRESENCE+"[@type='unavailable']", self._onUnavailablePresence)
393        self.xmlstream.addObserver(PRESENCE+"[@type='error']", self._onPresenceError)
394        self.xmlstream.addObserver(GROUPCHAT, self._onGroupChat)
395        self.xmlstream.addObserver(SUBJECT, self._onSubject)
396        # add history
397
398        self.initialized()
399
400    def _setRoom(self, room):
401        self.rooms[room.entity_id.userhost().lower()] = room
402
403    def _getRoom(self, room_jid):
404        return self.rooms.get(room_jid.userhost().lower())
405
406    def _removeRoom(self, room_jid):
407        if self.rooms.has_key(room_jid.userhost().lower()):
408            del self.rooms[room_jid.userhost().lower()]
409
410
411    def _onUnavailablePresence(self, prs):
412        """
413        """
414        if not prs.hasAttribute('from'):
415            return
416        room_jid = jid.internJID(prs.getAttribute('from', ''))
417        self._userLeavesRoom(room_jid)
418
419    def _onPresenceError(self, prs):
420        """
421        """
422        if not prs.hasAttribute('from'):
423            return
424        room_jid = jid.internJID(prs.getAttribute('from', ''))
425        # add an error hook here?
426        self._userLeavesRoom(room_jid)
427
428    def _userLeavesRoom(self, room_jid):
429        room = self._getRoom(room_jid)
430        if room is None:
431            # not in the room yet
432            return
433        # check if user is in roster
434        user = room.getUser(room_jid.resource)
435        if user is None:
436            return
437        if room.inRoster(user):
438            room.removeUser(user)
439            self.userLeftRoom(room, user)
440       
441    def _onXPresence(self, prs):
442        """
443        """
444        if not prs.hasAttribute('from'):
445            return
446        room_jid = jid.internJID(prs.getAttribute('from', ''))
447           
448        status = getattr(prs, 'status', None)
449        show   = getattr(prs, 'show', None)
450       
451        # grab room
452        room = self._getRoom(room_jid)
453        if room is None:
454            # not in the room yet
455            return
456
457        # check if user is in roster
458        user = room.getUser(room_jid.resource)
459        if user is None: # create a user that does not exist
460            user = User(room_jid.resource)
461           
462       
463        if room.inRoster(user):
464            # we changed status or nick
465            muc_status = getattr(prs.x, 'status', None)
466            if muc_status:
467                code = muc_status.getAttribute('code', 0)
468            else:
469                self.userUpdatedStatus(room, user, show, status)
470        else:           
471            room.addUser(user)
472            self.userJoinedRoom(room, user)
473           
474
475    def _onGroupChat(self, msg):
476        """
477        """
478        if not msg.hasAttribute('from'):
479            # need to return an error here
480            return
481        room_jid = jid.internJID(msg.getAttribute('from', ''))
482
483        room = self._getRoom(room_jid)
484        if room is None:
485            # not in the room yet
486            return
487        user = room.getUser(room_jid.resource)
488        delay = None
489        # need to check for delay and x stanzas for delay namespace for backwards compatability
490        for e in msg.elements():
491            if e.uri == NS_DELAY or e.uri == NS_JABBER_DELAY:
492                delay = e
493        body  = unicode(msg.body)
494        # grab room
495        if delay is None:
496            self.receivedGroupChat(room, user, body)
497        else:
498            self.receivedHistory(room, user, body, delay['stamp'], frm=delay.getAttribute('from',None))
499
500
501    def _onSubject(self, msg):
502        """
503        """
504        if not msg.hasAttribute('from'):
505            return
506        room_jid = jid.internJID(msg['from'])
507
508        # grab room
509        room = self._getRoom(room_jid)
510        if room is None:
511            # not in the room yet
512            return
513
514        self.receivedSubject(room_jid, unicode(msg.subject))
515
516
517    def _makeTimeStamp(self, stamp=None):
518        if stamp is None:
519            stamp = datetime.datetime.now()
520           
521        return stamp.strftime('%Y%m%dT%H:%M:%S')
522
523
524    def _joinedRoom(self, d, prs):
525        """We have presence that says we joined a room.
526        """
527        room_jid = jid.internJID(prs['from'])
528       
529        # check for errors
530        if prs.hasAttribute('type') and prs['type'] == 'error':           
531            d.errback(prs)
532        else:   
533            # change the state of the room
534            r = self._getRoom(room_jid)
535            if r is None:
536                raise Exception, 'Room Not Found' 
537            r.state = 'joined'
538           
539            # grab status
540            status = getattr(prs.x,'status',None)
541            if status:
542                r.status = status.getAttribute('code', None)
543
544            d.callback(r)
545
546
547    def _leftRoom(self, d, prs):
548        """We have presence that says we joined a room.
549        """
550        room_jid = jid.internJID(prs['from'])
551       
552        # check for errors
553        if prs.hasAttribute('type') and prs['type'] == 'error':           
554            d.errback(prs)
555        else:   
556            # change the state of the room
557            r = self._getRoom(room_jid)
558            if r is None:
559                raise Exception, 'Room Not Found' 
560            self._removeRoom(room_jid)
561           
562            d.callback(True)
563
564    def initialized(self):
565        """Client is initialized and ready!
566        """
567        pass
568
569    def userJoinedRoom(self, room, user):
570        """User has joined a room
571        """
572        pass
573
574    def userLeftRoom(self, room, user):
575        """User has left a room
576        """
577        pass
578
579
580    def userUpdatedStatus(self, room, user, show, status):
581        """User Presence has been received
582        """
583        pass
584       
585
586    def receivedSubject(self, room, subject):
587        """
588        """
589        pass
590
591
592    def receivedHistory(self, room, user, message, history, frm=None):
593        """
594        """
595        pass
596
597
598    def _cbDisco(self, iq):
599        # grab query
600       
601        return getattr(iq,'query', None)
602       
603    def disco(self, entity, type='info'):
604        """Send disco queries to a XMPP entity
605        """
606
607        iq = disco.DiscoRequest(self.xmlstream, disco.NS_INFO, 'get')
608        iq['to'] = entity
609
610        return iq.send().addBoth(self._cbDisco)
611       
612
613    def configure(self, room_jid, fields=[]):
614        """Configure a room
615
616        @param room_jid: The room jabber/xmpp entity id for the requested configuration form.
617        @type  room_jid: L{jid.JID}
618
619        """
620        request = ConfigureRequest(self.xmlstream, method='set', fields=fields)
621        request['to'] = room_jid
622       
623        return request.send()
624
625    def getConfigureForm(self, room_jid):
626        """Grab the configuration form from the room. This sends an iq request to the room.
627
628        @param room_jid: The room jabber/xmpp entity id for the requested configuration form.
629        @type  room_jid: L{jid.JID}
630
631        """
632        request = ConfigureRequest(self.xmlstream)
633        request['to'] = room_jid
634        return request.send()
635
636
637    def join(self, server, room, nick):
638        """ Join a MUC room by sending presence to it. Returns a defered that is called when
639        the entity is in the room or an error has occurred.
640       
641        @param server: The server where the room is located.
642        @type  server: L{unicode}
643
644        @param room: The room name the entity is joining.
645        @type  room: L{unicode}
646
647        @param nick: The nick name for the entitity joining the room.
648        @type  nick: L{unicode}
649       
650        """
651        d = defer.Deferred()
652        r = Room(room, server, nick, state='joining')
653        self._setRoom(r)
654 
655        p = BasicPresence(to=r.entity_id)
656        # p['from'] = self.jid.full()
657        self.xmlstream.send(p)
658
659        # add observer for joining the room
660        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s']" % (r.entity_id.full()), 
661                                          self._joinedRoom, 1, d)
662
663        return d
664   
665    def _changedNick(self, d, room_jid, prs):
666        """Callback for changing the nick.
667        """
668
669        r = self._getRoom(room_jid)
670
671        d.callback(r)
672
673
674    def nick(self, room_jid, new_nick):
675        """ Change an entities nick name in a MUC room.
676       
677        See: http://xmpp.org/extensions/xep-0045.html#changenick
678
679        @param room_jid: The room jabber/xmpp entity id for the requested configuration form.
680        @type  room_jid: L{jid.JID}
681
682        @param new_nick: The nick name for the entitity joining the room.
683        @type  new_nick: L{unicode}
684       
685        """
686
687        d = defer.Deferred()
688        r = self._getRoom(room_jid)
689        if r is None:
690            raise Exception, 'Room not found'
691        r.nick = new_nick # change the nick
692        # create presence
693        # make sure we call the method to generate the new entity xmpp id
694        p = BasicPresence(to=r.entityId()) 
695        self.xmlstream.send(p)
696
697        # add observer for joining the room
698        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s']" % (r.entity_id.full()), 
699                                          self._changedNick, 1, d, room_jid)
700
701        return d
702       
703
704   
705    def leave(self, room_jid):
706        """
707        """
708        d = defer.Deferred()
709
710        r = self._getRoom(room_jid)
711 
712        p = xmppim.UnavailablePresence(to=r.entity_id)
713        # p['from'] = self.jid.full()
714        self.xmlstream.send(p)
715
716        # add observer for joining the room
717        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s' and @type='unavailable']" % (r.entity_id.full()), 
718                                          self._leftRoom, 1, d)
719
720        return d
721   
722
723   
724
725    def _sendMessage(self, msg, children=None):
726
727        if children:
728            for c in children:
729                msg.addChild(c)
730       
731        self.xmlstream.send(msg)
732
733    def groupChat(self, to, message, children=None):
734        """Send a groupchat message
735        """
736        msg = GroupChat(to, body=message)
737       
738        self._sendMessage(msg, children=children)
739
740    def chat(self, to, message, children=None):
741        msg = PrivateChat(to, body=message)
742
743        self._sendMessage(msg, children=children)
744       
745    def invite(self, to, reason=None, full_jid=None):
746        """
747        """
748        msg = InviteMessage(to, reason=reason, full_jid=full_jid)
749        self._sendMessage(msg)
750
751
752    def password(self, to, password):
753        p = PasswordPresence(to, password)
754
755        self.xmlstream.send(p)
756   
757    def register(self, to, fields=[]):
758        iq = RegisterRequest(self.xmlstream, method='set', fields=fields)
759        iq['to'] = to
760        return iq.send()
761
762    def getRegisterForm(self, room):
763        """
764        """
765        iq = RegisterRequest(self.xmlstream)
766        iq['to'] = room.userhost()
767        return iq.send()
768
769    def subject(self, to, subject):
770        """
771        """
772        msg = GroupChat(to, subject=subject)
773        self.xmlstream.send(msg)
774
775    def voice(self, to):
776        """
777        """
778        msg = MessageVoice(to=to)
779        self.xmlstream.send(msg)
780
781
782    def history(self, to, message_list):
783        """
784        """
785       
786        for m in message_list:
787            m['type'] = 'groupchat'
788            mto = m['to']
789            frm = m.getAttribute('from', None)
790            m['to'] = to
791
792            d = m.addElement('delay', NS_DELAY)
793            d['stamp'] = self._makeTimeStamp()
794            d['from'] = mto
795
796            self.xmlstream.send(m)
797
798    def ban(self, to, ban_jid, frm, reason=None):
799       
800        iq = AffiliationRequest(self.xmlstream,
801                                method='set',
802                                affiliation='outcast', 
803                                a_jid=ban_jid, 
804                                reason=reason)
805        iq['to'] = to.userhost() # this is a room jid, only send to room
806        iq['from'] = frm.full()
807        return iq.send()
808
809
810    def kick(self, to, kick_jid, frm, reason=None):
811       
812        iq = AffiliationRequest(self.xmlstream,
813                                method='set',
814                                a_jid=kick_jid, 
815                                reason=reason)
816        iq['to'] = to.userhost() # this is a room jid, only send to room
817        iq['from'] = frm.full()
818        return iq.send()
Note: See TracBrowser for help on using the repository browser.