source: wokkel/muc.py @ 112:be0b126081f2

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

all tests pass, needs comments, also need to get the entire xep in the tests

File size: 15.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"]/body'
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    @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'}
124    @type method: C{str}
125    """
126
127    def __init__(self, xs, method='get', fields=[]):
128        xmlstream.IQ.__init__(self, xs, method)
129        q = self.addElement((NS_OWNER, 'query'))
130        if method == 'set':
131            # build data form
132            form = data_form.Form('submit', formNamespace=NS_CONFIG)
133            q.addChild(form.toElement())
134           
135            for f in fields:
136                # create a field
137                form.addField(f)
138
139
140class RegisterRequest(xmlstream.IQ):
141    """
142    Register room request.
143
144    @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'}
145    @type method: C{str}
146
147    """
148
149    def __init__(self, xs, method='get', fields=[]):
150        xmlstream.IQ.__init__(self, xs, method)
151        q = self.addElement((NS_REQUEST, 'query'))
152        if method == 'set':
153            # build data form
154            form_type = 'submit'       
155            form = data_form.Form(form_type, formNamespace=NS_REGISTER)
156            q.addChild(form.toElement())       
157           
158            for f in fields:
159                # create a field
160                form.addField(f)
161
162
163class AffiliationRequest(xmlstream.IQ):
164    """
165    Register room request.
166
167    @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'}
168    @type method: C{str}
169
170    @ivar affiliation: The affiliation type to send to room.
171    @type affiliation: C{str}
172
173    """
174
175    def __init__(self, xs, method='get', affiliation='none', a_jid=None, reason=None):
176        xmlstream.IQ.__init__(self, xs, method)
177       
178        q = self.addElement((NS_ADMIN, 'query'))
179        i = q.addElement('item')
180
181        i['affiliation'] = affiliation
182        if a_jid:
183            i['jid'] = a_jid.full()
184           
185        if reason:
186            i.addElement('reason', None, reason)
187
188           
189       
190
191class GroupChat(domish.Element):
192    """
193    """
194    def __init__(self, to, body=None, subject=None, frm=None):
195        """To needs to be a string
196        """
197        domish.Element.__init__(self, (None, 'message'))
198        self['type'] = 'groupchat'
199        self['to']   = to
200        if frm:
201            self['from'] = frm
202        if body:
203            self.addElement('body',None, body)
204        if subject:
205            self.addElement('subject',None, subject)
206
207
208class PrivateChat(domish.Element):
209    """
210    """
211    def __init__(self, to, body=None, frm=None):
212        """To needs to be a string
213        """
214        domish.Element.__init__(self, (None, 'message'))
215        self['type'] = 'chat'
216        self['to']   = to
217        if frm:
218            self['from'] = frm
219        if body:
220            self.addElement('body',None, body)
221           
222class InviteMessage(PrivateChat):
223    def __init__(self, to, reason=None, full_jid=None, body=None, frm=None, password=None):
224        PrivateChat.__init__(self, to, body=body, frm=frm)
225        del self['type'] # remove type
226        x = self.addElement('x', NS_USER)
227        invite = x.addElement('invite')
228        if full_jid:
229            invite['to'] = full_jid
230        if reason:
231            invite.addElement('reason', None, reason)
232        if password:
233            invite.addElement('password', None, password)
234
235class HistoryMessage(GroupChat):
236    """
237    """
238    def __init__(self, to, stamp, body=None, subject=None, frm=None, h_frm=None):
239        GroupChat.__init__(self, to, body=body, subject=subject, frm=frm)
240        d = self.addElement('delay', NS_DELAY)
241        d['stamp'] = stamp
242        if h_frm:
243            d['from'] = h_frm
244
245class Room(object):
246    """
247    A Multi User Chat Room
248    """
249
250   
251    def __init__(self, name, server, nick, state=None):
252        """
253        """
254        self.state  = state
255        self.name   = name
256        self.server = server
257        self.nick   = nick
258        self.status = None
259
260        self.entity_id = jid.internJID(name+'@'+server+'/'+nick)
261               
262        self.roster = {}
263
264       
265
266class BasicPresence(xmppim.AvailablePresence):
267    """
268    This behaves like an object providing L{domish.IElement}.
269
270    """
271
272    def __init__(self, to=None, show=None, statuses=None):
273        xmppim.AvailablePresence.__init__(self, to=to, show=show, statuses=statuses)
274        # add muc elements
275        x = self.addElement('x', NS)
276
277
278class UserPresence(xmppim.Presence):
279    """
280    This behaves like an object providing L{domish.IElement}.
281
282    """
283
284    def __init__(self, to=None, type=None, frm=None, affiliation=None, role=None):
285        xmppim.Presence.__init__(self, to, type)
286        if frm:
287            self['from'] = frm
288        # add muc elements
289        x = self.addElement('x', NS_USER)
290        if affiliation:
291            x['affiliation'] = affiliation
292        if role:
293            x['role'] = role
294
295
296class PasswordPresence(BasicPresence):
297    """
298    """
299    def __init__(self, to, password):
300        BasicPresence.__init__(self, to)
301       
302        self.x.addElement('password', None, password)
303
304
305class MessageVoice(GroupChat):
306    """
307    """
308    def __init__(self, to=None, frm=None):
309        GroupChat.__init__(self, to=to, frm=frm)
310        # build data form
311        form = data_form.Form('submit', formNamespace=NS_REQUEST)
312        form.addField(data_form.Field(var='muc#role',
313                                      value='participant', 
314                                      label='Requested role'))
315        self.addChild(form.toElement())           
316
317class PresenceError(xmppim.Presence):
318    """
319    This behaves like an object providing L{domish.IElement}.
320
321    """
322
323    def __init__(self, error, to=None, frm=None):
324        xmppim.Presence.__init__(self, to, type='error')
325        if frm:
326            self['from'] = frm
327        # add muc elements
328        x = self.addElement('x', NS)
329        # add error
330        self.addChild(error)
331       
332
333class MUCClient(XMPPHandler):
334    """
335    Multi-User chat client protocol.
336    """
337
338    implements(IMUCClient)
339
340    rooms = {}
341
342    def connectionInitialized(self):
343        self.xmlstream.addObserver(PRESENCE+"/x", self._onXPresence)
344        self.xmlstream.addObserver(GROUPCHAT, self._onGroupChat)
345        self.xmlstream.addObserver(SUBJECT, self._onSubject)
346        # add history
347
348    def _setRoom(self, room):
349        self.rooms[room.entity_id.full().lower()] = room
350
351    def _getRoom(self, room_jid):
352        return self.rooms.get(room_jid.full().lower())
353
354    def _removeRoom(self, room_jid):
355        if self.rooms.has_key(room_jid.full().lower()):
356            del self.rooms[room_jid.full().lower()]
357
358    def _onXPresence(self, prs):
359        """
360        """
361        if prs.x.uri == NS_USER:
362            self.receivedUserPresence(prs)
363           
364
365    def _onGroupChat(self, msg):
366        """
367        """
368        delay = getattr(msg, 'delay', None)
369        if delay is None:
370            self.receivedGroupChat(msg)
371        else:
372            self.receivedHistory(msg)
373
374
375    def _onSubject(self, msg):
376        """
377        """
378        self.receivedSubject(msg)
379
380
381    def _makeTimeStamp(self, stamp=None):
382        if stamp is None:
383            stamp = datetime.datetime.now()
384           
385        return stamp.strftime('%Y%m%dT%H:%M:%S')
386
387
388    def _joinedRoom(self, d, prs):
389        """We have presence that says we joined a room.
390        """
391        room_jid = jid.internJID(prs['from'])
392       
393        # check for errors
394        if prs.hasAttribute('type') and prs['type'] == 'error':           
395            d.errback(prs)
396        else:   
397            # change the state of the room
398            r = self._getRoom(room_jid)
399            if r is None:
400                raise Exception, 'Room Not Found' 
401            r.state = 'joined'
402           
403            # grab status
404            status = getattr(prs.x,'status',None)
405            if status:
406                r.status = status.getAttribute('code', None)
407
408            d.callback(r)
409
410
411    def _leftRoom(self, d, prs):
412        """We have presence that says we joined a room.
413        """
414        room_jid = jid.internJID(prs['from'])
415       
416        # check for errors
417        if prs.hasAttribute('type') and prs['type'] == 'error':           
418            d.errback(prs)
419        else:   
420            # change the state of the room
421            r = self._getRoom(room_jid)
422            if r is None:
423                raise Exception, 'Room Not Found' 
424            self._removeRoom(room_jid)
425           
426            d.callback(True)
427
428    def receivedUserPresence(self, prs):
429        """User Presence has been received
430        """
431        pass
432       
433
434    def receivedSubject(self, msg):
435        """
436        """
437        pass
438
439
440    def receivedHistory(self, msg):
441        """
442        """
443        pass
444
445
446    def _cbDisco(self, iq):
447        # grab query
448       
449        return getattr(iq,'query', None)
450       
451    def disco(self, entity, type='info'):
452        """Send disco queries to a XMPP entity
453        """
454
455        iq = disco.DiscoRequest(self.xmlstream, disco.NS_INFO, 'get')
456        iq['to'] = entity
457
458        return iq.send().addBoth(self._cbDisco)
459       
460
461    def configure(self, room_jid, fields=[]):
462        """Configure a room
463        """
464        request = ConfigureRequest(self.xmlstream, method='set', fields=fields)
465        request['to'] = room_jid
466       
467        return request.send()
468
469    def getConfigureForm(self, room_jid):
470        request = ConfigureRequest(self.xmlstream)
471        request['to'] = room_jid
472        return request.send()
473
474
475    def join(self, server, room, nick):
476        """
477        """
478        d = defer.Deferred()
479        r = Room(room, server, nick, state='joining')
480        self._setRoom(r)
481 
482        p = BasicPresence(to=r.entity_id)
483        # p['from'] = self.jid.full()
484        self.xmlstream.send(p)
485
486        # add observer for joining the room
487        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s']" % (r.entity_id.full()), 
488                                          self._joinedRoom, 1, d)
489
490        return d
491   
492
493   
494    def leave(self, room_jid):
495        """
496        """
497        d = defer.Deferred()
498
499        r = self._getRoom(room_jid)
500 
501        p = xmppim.UnavailablePresence(to=r.entity_id)
502        # p['from'] = self.jid.full()
503        self.xmlstream.send(p)
504
505        # add observer for joining the room
506        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s' and @type='unavailable']" % (r.entity_id.full()), 
507                                          self._leftRoom, 1, d)
508
509        return d
510   
511
512   
513
514    def _sendMessage(self, msg, children=None):
515
516        if children:
517            for c in children:
518                msg.addChild(c)
519       
520        self.xmlstream.send(msg)
521
522    def groupChat(self, to, message, children=None):
523        """Send a groupchat message
524        """
525        msg = GroupChat(to, body=message)
526       
527        self._sendMessage(msg, children=children)
528
529    def chat(self, to, message, children=None):
530        msg = PrivateChat(to, body=message)
531
532        self._sendMessage(msg, children=children)
533       
534    def invite(self, to, reason=None, full_jid=None):
535        msg = InviteMessage(to, reason=reason, full_jid=full_jid)
536        self._sendMessage(msg)
537
538
539    def password(self, to, password):
540        p = PasswordPresence(to, password)
541
542        self.xmlstream.send(p)
543   
544    def register(self, to, fields=[]):
545        iq = RegisterRequest(self.xmlstream, method='set', fields=fields)
546        iq['to'] = to
547        return iq.send()
548
549    def getRegisterForm(self, to):
550        iq = RegisterRequest(self.xmlstream)
551        iq['to'] = to
552        return iq.send()
553
554    def subject(self, to, subject):
555        """
556        """
557        msg = GroupChat(to, subject=subject)
558        self.xmlstream.send(msg)
559
560    def voice(self, to):
561        """
562        """
563        msg = MessageVoice(to=to)
564        self.xmlstream.send(msg)
565
566
567    def history(self, to, message_list):
568        """
569        """
570       
571        for m in message_list:
572            m['type'] = 'groupchat'
573            mto = m['to']
574            frm = m.getAttribute('from', None)
575            m['to'] = to
576
577            d = m.addElement('delay', NS_DELAY)
578            d['stamp'] = self._makeTimeStamp()
579            d['from'] = mto
580
581            self.xmlstream.send(m)
582
583    def ban(self, to, ban_jid, frm, reason=None):
584       
585        iq = AffiliationRequest(self.xmlstream,
586                                method='set',
587                                affiliation='outcast', 
588                                a_jid=ban_jid, 
589                                reason=reason)
590        iq['to'] = to.userhost() # this is a room jid, only send to room
591        iq['from'] = frm.full()
592        return iq.send()
593
594
595    def kick(self, to, kick_jid, frm, reason=None):
596       
597        iq = AffiliationRequest(self.xmlstream,
598                                method='set',
599                                a_jid=kick_jid, 
600                                reason=reason)
601        iq['to'] = to.userhost() # this is a room jid, only send to room
602        iq['from'] = frm.full()
603        return iq.send()
Note: See TracBrowser for help on using the repository browser.