source: wokkel/muc.py @ 110:ddf5580b7128

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

add some history and other things, plus some test stubs

File size: 10.2 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'
32
33NS_DELAY    = 'urn:xmpp:delay'
34
35# ad hoc commands
36NS_AD_HOC       = "http://jabber.org/protocol/commands"
37
38
39# Iq get and set XPath queries
40IQ     = '/iq'
41IQ_GET = IQ+'[@type="get"]'
42IQ_SET = IQ+'[@type="set"]'
43
44IQ_RESULT = IQ+'[@type="result"]'
45IQ_ERROR  = IQ+'[@type="error"]'
46
47IQ_QUERY     = IQ+'/query'
48IQ_GET_QUERY = IQ_GET + '/query'
49IQ_SET_QUERY = IQ_SET + '/query'
50
51IQ_COMMAND   = IQ+'/command'
52
53MUC_ADMIN = IQ_QUERY+'[@xmlns="' + NS_ADMIN + '"]'
54MUC_OWNER = IQ_QUERY+'[@xmlns="' + NS_OWNER + '"]'
55
56MUC_AO = MUC_ADMIN + '|' + MUC_OWNER
57
58
59MESSAGE  = '/message'
60PRESENCE = '/presence'
61
62CHAT_BODY = MESSAGE +'[@type="chat"]/body'
63CHAT      = MESSAGE +'[@type="chat"]'
64
65GROUPCHAT     = MESSAGE +'[@type="groupchat"]/body'
66SUBJECT       = MESSAGE +'[@type="groupchat"]/body'
67MESSAGE_ERROR = MESSAGE +'[@type="error"]'
68
69STATUS_CODES = { # see http://www.xmpp.org/extensions/xep-0045.html#registrar-statuscodes
70    100:
71        {'name':'fulljid',
72         'stanza':'presence',
73         
74         },
75    201: 
76        {'name':'created', 
77         'stanza': 'presence',
78         'context':'Entering a room',
79         'purpose':'Inform user that a new room has been created'
80         },   
81}
82
83STATUS_CODE_CREATED = 201
84
85
86class MUCError(error.StanzaError):
87    """
88    Exception with muc specific condition.
89    """
90    def __init__(self, condition, mucCondition, feature=None, text=None):
91        appCondition = domish.Element((NS, mucCondition))
92        if feature:
93            appCondition['feature'] = feature
94        error.StanzaError.__init__(self, condition,
95                                         text=text,
96                                         appCondition=appCondition)
97
98
99class BadRequest(MUCError):
100    """
101    Bad request stanza error.
102    """
103    def __init__(self, mucCondition=None, text=None):
104        MUCError.__init__(self, 'bad-request', mucCondition, text)
105
106
107
108class Unsupported(MUCError):
109    def __init__(self, feature, text=None):
110        MUCError.__init__(self, 'feature-not-implemented',
111                          'unsupported',
112                          feature,
113                          text)
114
115
116
117class ConfigureRequest(xmlstream.IQ):
118    """
119    Configure MUC room request.
120
121    @ivar namespace: Request namespace.
122    @type namespace: C{str}
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 GroupChat(domish.Element):
141    """
142    """
143    def __init__(self, to, body=None, subject=None, frm=None):
144        """To needs to be a string
145        """
146        domish.Element.__init__(self, (None, 'message'))
147        self['type'] = 'groupchat'
148        self['to']   = to
149        if frm:
150            self['from'] = frm
151        if body:
152            self.addElement('body',None, body)
153        if subject:
154            self.addElement('subject',None, subject)
155           
156
157
158class Room(object):
159    """
160    A Multi User Chat Room
161    """
162
163   
164    def __init__(self, name, server, nick, state=None):
165        """
166        """
167        self.state  = state
168        self.name   = name
169        self.server = server
170        self.nick   = nick
171        self.status = None
172
173        self.entity_id = jid.internJID(name+'@'+server+'/'+nick)
174               
175        self.roster = {}
176
177       
178
179class BasicPresence(xmppim.Presence):
180    """
181    This behaves like an object providing L{domish.IElement}.
182
183    """
184
185    def __init__(self, to=None, type=None):
186        xmppim.Presence.__init__(self, to, type)
187       
188        # add muc elements
189        x = self.addElement('x', NS)
190
191
192class UserPresence(xmppim.Presence):
193    """
194    This behaves like an object providing L{domish.IElement}.
195
196    """
197
198    def __init__(self, to=None, type=None, frm=None, affiliation=None, role=None):
199        xmppim.Presence.__init__(self, to, type)
200        if frm:
201            self['from'] = frm
202        # add muc elements
203        x = self.addElement('x', NS_USER)
204        if affiliation:
205            x['affiliation'] = affiliation
206        if role:
207            x['role'] = role
208
209
210class PasswordPresence(BasicPresence):
211    """
212    """
213    def __init__(self, to, password):
214        BasicPresence.__init__(self, to)
215       
216        self.x.addElement('password', None, password)
217
218
219class MessageVoice(GroupChat):
220    """
221    """
222    def __init__(self, to=None, frm=None):
223        GroupChat.__init__(self, to=to, frm=frm)
224        # build data form
225        form = data_form.Form('submit', formNamespace=NS_REQUEST)
226        form.addField(data_form.Field(var='muc#role',
227                                      value='participant', 
228                                      label='Requested role'))
229        self.addChild(form.toElement())           
230
231class PresenceError(BasicPresence):
232    """
233    This behaves like an object providing L{domish.IElement}.
234
235    """
236
237    def __init__(self, error, to=None, frm=None):
238        BasicPresence.__init__(self, to, type='error')
239        if frm:
240            self['from'] = frm
241        # add muc elements
242        x = self.addElement('x', NS)
243        # add error
244        self.addChild(error)
245       
246
247class MUCClient(XMPPHandler):
248    """
249    Multi-User chat client protocol.
250    """
251
252    implements(IMUCClient)
253
254    rooms = {}
255
256    def connectionInitialized(self):
257        self.xmlstream.addObserver(PRESENCE+"/x", self._onXPresence)
258        self.xmlstream.addObserver(GROUPCHAT, self._onGroupChat)
259        self.xmlstream.addObserver(SUBJECT, self._onSubject)
260        # add history
261
262    def _setRoom(self, room):
263        self.rooms[room.entity_id.full().lower()] = room
264
265    def _getRoom(self, room_jid):
266        return self.rooms.get(room_jid.full().lower())
267
268
269    def _onXPresence(self, prs):
270        """
271        """
272        if prs.x.uri == NS_USER:
273            self.receivedUserPresence(prs)
274           
275
276    def _onGroupChat(self, msg):
277        """
278        """
279        self.receivedGroupChat(msg)
280
281
282
283    def _onSubject(self, msg):
284        """
285        """
286        self.receivedSubject(msg)
287
288
289    def _makeTimeStamp(self, stamp=None):
290        if stamp is None:
291            stamp = datetime.datetime.now()
292           
293        return stamp.strftime('%Y%m%dT%H:%M:%S')
294
295
296    def _joinedRoom(self, d, prs):
297        """We have presence that says we joined a room.
298        """
299        room_jid = jid.internJID(prs['from'])
300       
301        # check for errors
302        if prs.hasAttribute('type') and prs['type'] == 'error':           
303            d.errback(prs)
304        else:   
305            # change the state of the room
306            r = self._getRoom(room_jid)
307            if r is None:
308                raise 
309            r.state = 'joined'
310           
311            # grab status
312            status = getattr(prs.x,'status',None)
313            if status:
314                r.status = status.getAttribute('code', None)
315
316            d.callback(r)
317
318    def receivedUserPresence(self, prs):
319        """User Presence has been received
320        """
321        pass
322       
323
324    def receivedSubject(self, msg):
325        """
326        """
327        pass
328
329    def _cbDisco(self, iq):
330        # grab query
331       
332        return getattr(iq,'query', None)
333       
334    def disco(self, entity, type='info'):
335        """Send disco queries to a XMPP entity
336        """
337
338        iq = disco.DiscoRequest(self.xmlstream, disco.NS_INFO, 'get')
339        iq['to'] = entity
340
341        return iq.send().addBoth(self._cbDisco)
342       
343
344    def configure(self, room_jid, **kwargs):
345        """Configure a room
346        """
347        request = ConfigureRequest(self.xmlstream, method='set', **kwargs)
348        request['to'] = room_jid
349       
350        return request.send()
351
352    def getConfigureForm(self, room_jid):
353        request = ConfigureRequest(self.xmlstream)
354        request['to'] = room_jid
355        return request.send()
356
357
358    def join(self, server, room, nick):
359        """
360        """
361        d = defer.Deferred()
362        r = Room(room, server, nick, state='joining')
363        self._setRoom(r)
364 
365        p = BasicPresence(to=r.entity_id)
366        # p['from'] = self.jid.full()
367        self.xmlstream.send(p)
368
369        # add observer for joining the room
370        self.xmlstream.addOnetimeObserver(PRESENCE+"[@from='%s']" % (r.entity_id.full()), 
371                                          self._joinedRoom, 1, d)
372
373        return d
374   
375
376    def groupChat(self, to, message, children=None):
377        """Send a groupchat message
378        """
379        msg = GroupChat(to, body=message)
380
381        if children:
382            for c in children:
383                msg.addChild(c)
384       
385        self.xmlstream.send(msg)
386
387   
388    def subject(self, to, subject):
389        """
390        """
391        msg = GroupChat(to, subject=subject)
392        self.xmlstream.send(msg)
393
394    def voice(self, to):
395        """
396        """
397        msg = MessageVoice(to=to)
398        self.xmlstream.send(msg)
399
400
401    def history(self, to, message_list):
402        """
403        """
404       
405        for m in message_list:
406            m['type'] = 'groupchat'
407            mto = m['to']
408            frm = m.getAttribute('from', None)
409            m['to'] = to
410
411            d = m.addElement('delay', NS_DELAY)
412            d['stamp'] = self._makeTimeStamp()
413            d['from'] = mto
414
415            self.xmlstream.send(m)
416
Note: See TracBrowser for help on using the repository browser.