source: wokkel/muc.py @ 111:a51e5d87ded8

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

add leave room, register for a room and other things for re #24, still need admin and owner stuff, more tests and more docs

File size: 12.6 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 namespace: Request namespace.
124    @type namespace: C{str}
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 namespace: Request namespace.
147    @type namespace: C{str}
148    @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'}
149    @type method: C{str}
150    """
151
152    def __init__(self, xs, method='get', fields=[]):
153        xmlstream.IQ.__init__(self, xs, method)
154        q = self.addElement((NS_REQUEST, 'query'))
155        if method == 'set':
156            # build data form
157            form_type = 'submit'       
158            form = data_form.Form(form_type, formNamespace=NS_REGISTER)
159            q.addChild(form.toElement())       
160           
161            for f in fields:
162                # create a field
163                form.addField(f)
164
165class GroupChat(domish.Element):
166    """
167    """
168    def __init__(self, to, body=None, subject=None, frm=None):
169        """To needs to be a string
170        """
171        domish.Element.__init__(self, (None, 'message'))
172        self['type'] = 'groupchat'
173        self['to']   = to
174        if frm:
175            self['from'] = frm
176        if body:
177            self.addElement('body',None, body)
178        if subject:
179            self.addElement('subject',None, subject)
180           
181
182
183class Room(object):
184    """
185    A Multi User Chat Room
186    """
187
188   
189    def __init__(self, name, server, nick, state=None):
190        """
191        """
192        self.state  = state
193        self.name   = name
194        self.server = server
195        self.nick   = nick
196        self.status = None
197
198        self.entity_id = jid.internJID(name+'@'+server+'/'+nick)
199               
200        self.roster = {}
201
202       
203
204class BasicPresence(xmppim.AvailablePresence):
205    """
206    This behaves like an object providing L{domish.IElement}.
207
208    """
209
210    def __init__(self, to=None, show=None, statuses=None):
211        xmppim.AvailablePresence.__init__(self, to=to, show=show, statuses=statuses)
212        # add muc elements
213        x = self.addElement('x', NS)
214
215
216class UserPresence(xmppim.Presence):
217    """
218    This behaves like an object providing L{domish.IElement}.
219
220    """
221
222    def __init__(self, to=None, type=None, frm=None, affiliation=None, role=None):
223        xmppim.Presence.__init__(self, to, type)
224        if frm:
225            self['from'] = frm
226        # add muc elements
227        x = self.addElement('x', NS_USER)
228        if affiliation:
229            x['affiliation'] = affiliation
230        if role:
231            x['role'] = role
232
233
234class PasswordPresence(BasicPresence):
235    """
236    """
237    def __init__(self, to, password):
238        BasicPresence.__init__(self, to)
239       
240        self.x.addElement('password', None, password)
241
242
243class MessageVoice(GroupChat):
244    """
245    """
246    def __init__(self, to=None, frm=None):
247        GroupChat.__init__(self, to=to, frm=frm)
248        # build data form
249        form = data_form.Form('submit', formNamespace=NS_REQUEST)
250        form.addField(data_form.Field(var='muc#role',
251                                      value='participant', 
252                                      label='Requested role'))
253        self.addChild(form.toElement())           
254
255class PresenceError(xmppim.Presence):
256    """
257    This behaves like an object providing L{domish.IElement}.
258
259    """
260
261    def __init__(self, error, to=None, frm=None):
262        xmppim.Presence.__init__(self, to, type='error')
263        if frm:
264            self['from'] = frm
265        # add muc elements
266        x = self.addElement('x', NS)
267        # add error
268        self.addChild(error)
269       
270
271class MUCClient(XMPPHandler):
272    """
273    Multi-User chat client protocol.
274    """
275
276    implements(IMUCClient)
277
278    rooms = {}
279
280    def connectionInitialized(self):
281        self.xmlstream.addObserver(PRESENCE+"/x", self._onXPresence)
282        self.xmlstream.addObserver(GROUPCHAT, self._onGroupChat)
283        self.xmlstream.addObserver(SUBJECT, self._onSubject)
284        # add history
285
286    def _setRoom(self, room):
287        self.rooms[room.entity_id.full().lower()] = room
288
289    def _getRoom(self, room_jid):
290        return self.rooms.get(room_jid.full().lower())
291
292    def _removeRoom(self, room_jid):
293        if self.rooms.has_key(room_jid.full().lower()):
294            del self.rooms[room_jid.full().lower()]
295
296    def _onXPresence(self, prs):
297        """
298        """
299        if prs.x.uri == NS_USER:
300            self.receivedUserPresence(prs)
301           
302
303    def _onGroupChat(self, msg):
304        """
305        """
306        self.receivedGroupChat(msg)
307
308
309
310    def _onSubject(self, msg):
311        """
312        """
313        self.receivedSubject(msg)
314
315
316    def _makeTimeStamp(self, stamp=None):
317        if stamp is None:
318            stamp = datetime.datetime.now()
319           
320        return stamp.strftime('%Y%m%dT%H:%M:%S')
321
322
323    def _joinedRoom(self, d, prs):
324        """We have presence that says we joined a room.
325        """
326        room_jid = jid.internJID(prs['from'])
327       
328        # check for errors
329        if prs.hasAttribute('type') and prs['type'] == 'error':           
330            d.errback(prs)
331        else:   
332            # change the state of the room
333            r = self._getRoom(room_jid)
334            if r is None:
335                raise Exception, 'Room Not Found' 
336            r.state = 'joined'
337           
338            # grab status
339            status = getattr(prs.x,'status',None)
340            if status:
341                r.status = status.getAttribute('code', None)
342
343            d.callback(r)
344
345
346    def _leftRoom(self, d, prs):
347        """We have presence that says we joined a room.
348        """
349        room_jid = jid.internJID(prs['from'])
350       
351        # check for errors
352        if prs.hasAttribute('type') and prs['type'] == 'error':           
353            d.errback(prs)
354        else:   
355            # change the state of the room
356            r = self._getRoom(room_jid)
357            if r is None:
358                raise Exception, 'Room Not Found' 
359            self._removeRoom(room_jid)
360           
361            d.callback(True)
362
363    def receivedUserPresence(self, prs):
364        """User Presence has been received
365        """
366        pass
367       
368
369    def receivedSubject(self, msg):
370        """
371        """
372        pass
373
374    def _cbDisco(self, iq):
375        # grab query
376       
377        return getattr(iq,'query', None)
378       
379    def disco(self, entity, type='info'):
380        """Send disco queries to a XMPP entity
381        """
382
383        iq = disco.DiscoRequest(self.xmlstream, disco.NS_INFO, 'get')
384        iq['to'] = entity
385
386        return iq.send().addBoth(self._cbDisco)
387       
388
389    def configure(self, room_jid, fields=[]):
390        """Configure a room
391        """
392        request = ConfigureRequest(self.xmlstream, method='set', fields=fields)
393        request['to'] = room_jid
394       
395        return request.send()
396
397    def getConfigureForm(self, room_jid):
398        request = ConfigureRequest(self.xmlstream)
399        request['to'] = room_jid
400        return request.send()
401
402
403    def join(self, server, room, nick):
404        """
405        """
406        d = defer.Deferred()
407        r = Room(room, server, nick, state='joining')
408        self._setRoom(r)
409 
410        p = BasicPresence(to=r.entity_id)
411        # p['from'] = self.jid.full()
412        self.xmlstream.send(p)
413
414        # add observer for joining the room
415        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s']" % (r.entity_id.full()), 
416                                          self._joinedRoom, 1, d)
417
418        return d
419   
420
421   
422    def leave(self, room_jid):
423        """
424        """
425        d = defer.Deferred()
426
427        self._getRoom(room_jid)
428 
429        p = xmppim.UnavailablePresence(to=r.entity_id)
430        # p['from'] = self.jid.full()
431        self.xmlstream.send(p)
432
433        # add observer for joining the room
434        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s' and type='unavailable']" % (r.entity_id.full()), 
435                                          self._leftRoom, 1, d)
436
437        return d
438   
439
440   
441
442    def groupChat(self, to, message, children=None):
443        """Send a groupchat message
444        """
445        msg = GroupChat(to, body=message)
446
447        if children:
448            for c in children:
449                msg.addChild(c)
450       
451        self.xmlstream.send(msg)
452
453   
454    def register(self, to, fields=[]):
455        iq = RegisterRequest(self.xmlstream, method='set', fields=fields)
456        iq['to'] = to
457        return iq.send()
458
459    def getRegisterForm(self, to):
460        iq = RegisterRequest(self.xmlstream)
461        iq['to'] = to
462        return iq.send()
463
464    def subject(self, to, subject):
465        """
466        """
467        msg = GroupChat(to, subject=subject)
468        self.xmlstream.send(msg)
469
470    def voice(self, to):
471        """
472        """
473        msg = MessageVoice(to=to)
474        self.xmlstream.send(msg)
475
476
477    def history(self, to, message_list):
478        """
479        """
480       
481        for m in message_list:
482            m['type'] = 'groupchat'
483            mto = m['to']
484            frm = m.getAttribute('from', None)
485            m['to'] = to
486
487            d = m.addElement('delay', NS_DELAY)
488            d['stamp'] = self._makeTimeStamp()
489            d['from'] = mto
490
491            self.xmlstream.send(m)
492
Note: See TracBrowser for help on using the repository browser.