source: wokkel/muc.py @ 115:2a3171b40d2b

wokkel-muc-client-support-24
Last change on this file since 115:2a3171b40d2b was 115:2a3171b40d2b, checked in by Christopher Zorn <tofu@…>, 14 years ago

add initialized and fix a typo bug

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