source: wokkel/disco.py @ 49:50a84c44cbf1

Last change on this file since 49:50a84c44cbf1 was 49:50a84c44cbf1, checked in by Ralph Meijer <ralphm@…>, 12 years ago

Add Service Discovery client protocol and overhaul data classes.

Author: ralphm.
Reviewer: tofu.
Fixes #28.

File size: 16.1 KB
Line 
1# -*- test-case-name: wokkel.test.test_disco -*-
2#
3# Copyright (c) 2003-2009 Ralph Meijer
4# See LICENSE for details.
5
6"""
7XMPP Service Discovery.
8
9The XMPP service discovery protocol is documented in
10U{XEP-0030<http://www.xmpp.org/extensions/xep-0030.html>}.
11"""
12
13from twisted.internet import defer
14from twisted.words.protocols.jabber import error, jid, xmlstream
15from twisted.words.xish import domish
16
17from wokkel import data_form
18from wokkel.iwokkel import IDisco
19from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
20
21NS_DISCO = 'http://jabber.org/protocol/disco'
22NS_DISCO_INFO = NS_DISCO + '#info'
23NS_DISCO_ITEMS = NS_DISCO + '#items'
24
25IQ_GET = '/iq[@type="get"]'
26DISCO_INFO = IQ_GET + '/query[@xmlns="' + NS_DISCO_INFO + '"]'
27DISCO_ITEMS = IQ_GET + '/query[@xmlns="' + NS_DISCO_ITEMS + '"]'
28
29class DiscoFeature(unicode):
30    """
31    XMPP service discovery feature.
32
33    This extends C{unicode} to convert to and from L{domish.Element}, but
34    further behaves identically.
35    """
36
37    def toElement(self):
38        """
39        Render to a DOM representation.
40
41        @rtype: L{domish.Element}.
42        """
43        element = domish.Element((NS_DISCO_INFO, 'feature'))
44        element['var'] = unicode(self)
45        return element
46
47
48    @staticmethod
49    def fromElement(element):
50        """
51        Parse a DOM representation into a L{DiscoFeature} instance.
52
53        @param element: Element that represents the disco feature.
54        @type element: L{domish.Element}.
55        @rtype L{DiscoFeature}.
56        """
57        featureURI = element.getAttribute('var', u'')
58        feature = DiscoFeature(featureURI)
59        return feature
60
61
62
63class DiscoIdentity(object):
64    """
65    XMPP service discovery identity.
66
67    @ivar category: The identity category.
68    @type category: C{unicode}
69    @ivar type: The identity type.
70    @type type: C{unicode}
71    @ivar name: The optional natural language name for this entity.
72    @type name: C{unicode}
73    """
74
75    def __init__(self, category, idType, name=None):
76        self.category = category
77        self.type = idType
78        self.name = name
79
80
81    def toElement(self):
82        """
83        Generate a DOM representation.
84
85        @rtype: L{domish.Element}.
86        """
87        element = domish.Element((NS_DISCO_INFO, 'identity'))
88        if self.category:
89            element['category'] = self.category
90        if self.type:
91            element['type'] = self.type
92        if self.name:
93            element['name'] = self.name
94        return element
95
96
97    @staticmethod
98    def fromElement(element):
99        """
100        Parse a DOM representation into a L{DiscoIdentity} instance.
101
102        @param element: Element that represents the disco identity.
103        @type element: L{domish.Element}.
104        @rtype L{DiscoIdentity}.
105        """
106        category = element.getAttribute('category')
107        idType = element.getAttribute('type')
108        name = element.getAttribute('name')
109        feature = DiscoIdentity(category, idType, name)
110        return feature
111
112
113
114class DiscoInfo(object):
115    """
116    XMPP service discovery info.
117
118    @ivar nodeIdentifier: The optional node this info applies to.
119    @type nodeIdentifier: C{unicode}
120    @ivar features: Features as L{DiscoFeature}.
121    @type features: C{set)
122    @ivar identities: Identities as a mapping from (category, type) to name,
123                      all C{unicode}.
124    @type identities: C{dict}
125    @ivar extensions: Service discovery extensions as a mapping from the
126                      extension form's C{FORM_TYPE} (C{unicode}) to
127                      L{data_form.Form}. Forms with no C{FORM_TYPE} field
128                      are mapped as C{None}. Note that multiple forms
129                      with the same C{FORM_TYPE} have the last in sequence
130                      prevail.
131    @type extensions: C{dict}
132    @ivar _items: Sequence of added items.
133    @type _items: C{list}
134    """
135
136    def __init__(self):
137        self.nodeIdentifier = ''
138        self.features = set()
139        self.identities = {}
140        self.extensions = {}
141        self._items = []
142
143
144    def __iter__(self):
145        """
146        Iterator over sequence of items in the order added.
147        """
148        return iter(self._items)
149
150
151    def append(self, item):
152        """
153        Add a piece of service discovery info.
154
155        @param item: A feature, identity or extension form.
156        @type item: L{DiscoFeature}, L{DiscoIdentity} or L{data_form.Form}
157        """
158        self._items.append(item)
159
160        if isinstance(item, DiscoFeature):
161            self.features.add(item)
162        elif isinstance(item, DiscoIdentity):
163            self.identities[(item.category, item.type)] = item.name
164        elif isinstance(item, data_form.Form):
165            self.extensions[item.formNamespace] = item
166
167
168    def toElement(self):
169        """
170        Generate a DOM representation.
171
172        This takes the items added with C{append} to create a DOM
173        representation of service discovery information.
174
175        @rtype: L{domish.Element}.
176        """
177        element = domish.Element((NS_DISCO_INFO, 'query'))
178
179        if self.nodeIdentifier:
180            element['node'] = self.nodeIdentifier
181
182        for item in self:
183            element.addChild(item.toElement())
184
185        return element
186
187
188    @staticmethod
189    def fromElement(element):
190        """
191        Parse a DOM representation into a L{DiscoInfo} instance.
192
193        @param element: Element that represents the disco info.
194        @type element: L{domish.Element}.
195        @rtype L{DiscoInfo}.
196        """
197
198        info = DiscoInfo()
199
200        info.nodeIdentifier = element.getAttribute('node', '')
201
202        for child in element.elements():
203            item = None
204
205            if (child.uri, child.name) == (NS_DISCO_INFO, 'feature'):
206                item = DiscoFeature.fromElement(child)
207            elif (child.uri, child.name) == (NS_DISCO_INFO, 'identity'):
208                item = DiscoIdentity.fromElement(child)
209            elif (child.uri, child.name) == (data_form.NS_X_DATA, 'x'):
210                item = data_form.Form.fromElement(child)
211
212            if item:
213                info.append(item)
214
215        return info
216
217
218
219class DiscoItem(object):
220    """
221    XMPP service discovery item.
222
223    @ivar entity: The entity holding the item.
224    @type entity: L{jid.JID}
225    @ivar nodeIdentifier: The optional node identifier for the item.
226    @type nodeIdentifier: C{unicode}
227    @ivar name: The optional natural language name for this entity.
228    @type name: C{unicode}
229    """
230
231    def __init__(self, entity, nodeIdentifier='', name=None):
232        self.entity = entity
233        self.nodeIdentifier = nodeIdentifier
234        self.name = name
235
236
237    def toElement(self):
238        """
239        Generate a DOM representation.
240
241        @rtype: L{domish.Element}.
242        """
243        element = domish.Element((NS_DISCO_ITEMS, 'item'))
244        if self.entity:
245            element['jid'] = self.entity.full()
246        if self.nodeIdentifier:
247            element['node'] = self.nodeIdentifier
248        if self.name:
249            element['name'] = self.name
250        return element
251
252
253    @staticmethod
254    def fromElement(element):
255        """
256        Parse a DOM representation into a L{DiscoItem} instance.
257
258        @param element: Element that represents the disco iitem.
259        @type element: L{domish.Element}.
260        @rtype L{DiscoItem}.
261        """
262        try:
263            entity = jid.JID(element.getAttribute('jid', ' '))
264        except jid.InvalidFormat:
265            entity = None
266        nodeIdentifier = element.getAttribute('node', '')
267        name = element.getAttribute('name')
268        feature = DiscoItem(entity, nodeIdentifier, name)
269        return feature
270
271
272
273class DiscoItems(object):
274    """
275    XMPP service discovery items.
276
277    @ivar nodeIdentifier: The optional node this info applies to.
278    @type nodeIdentifier: C{unicode}
279    @ivar _items: Sequence of added items.
280    @type _items: C{list}
281    """
282
283    def __init__(self):
284        self.nodeIdentifier = ''
285        self._items = []
286
287
288    def __iter__(self):
289        """
290        Iterator over sequence of items in the order added.
291        """
292        return iter(self._items)
293
294
295    def append(self, item):
296        """
297        Append item to the sequence of items.
298
299        @param item: Item to be added.
300        @type item: L{DiscoItem}
301        """
302        self._items.append(item)
303
304
305    def toElement(self):
306        """
307        Generate a DOM representation.
308
309        This takes the items added with C{append} to create a DOM
310        representation of service discovery items.
311
312        @rtype: L{domish.Element}.
313        """
314        element = domish.Element((NS_DISCO_ITEMS, 'query'))
315
316        if self.nodeIdentifier:
317            element['node'] = self.nodeIdentifier
318
319        for item in self:
320            element.addChild(item.toElement())
321
322        return element
323
324
325    @staticmethod
326    def fromElement(element):
327        """
328        Parse a DOM representation into a L{DiscoItems} instance.
329
330        @param element: Element that represents the disco items.
331        @type element: L{domish.Element}.
332        @rtype L{DiscoItems}.
333        """
334
335        info = DiscoItems()
336
337        info.nodeIdentifier = element.getAttribute('node', '')
338
339        for child in element.elements():
340            if (child.uri, child.name) == (NS_DISCO_ITEMS, 'item'):
341                item = DiscoItem.fromElement(child)
342                info.append(item)
343
344        return info
345
346
347
348class _DiscoRequest(xmlstream.IQ):
349    """
350    Element representing an XMPP service discovery request.
351    """
352
353    def __init__(self, xs, namespace, nodeIdentifier=''):
354        """
355        Initialize the request.
356
357        @param xs: XML Stream the request should go out on.
358        @type xs: L{xmlstream.XmlStream}
359        @param namespace: Request namespace.
360        @type namespace: C{str}
361        @param nodeIdentifier: Node to request info from.
362        @type nodeIdentifier: C{unicode}
363        """
364        xmlstream.IQ.__init__(self, xs, "get")
365        query = self.addElement((namespace, 'query'))
366        if nodeIdentifier:
367            query['node'] = nodeIdentifier
368
369
370
371class DiscoClientProtocol(XMPPHandler):
372    """
373    XMPP Service Discovery client protocol.
374    """
375
376    def requestInfo(self, entity, nodeIdentifier=''):
377        """
378        Request information discovery from a node.
379
380        @param entity: Entity to send the request to.
381        @type entity: L{jid.JID}
382        @param nodeIdentifier: Optional node to request info from.
383        @type nodeIdentifier: C{unicode}
384        """
385
386        request = _DiscoRequest(self.xmlstream, NS_DISCO_INFO, nodeIdentifier)
387
388        d = request.send(entity.full())
389        d.addCallback(lambda iq: DiscoInfo.fromElement(iq.query))
390        return d
391
392
393    def requestItems(self, entity, nodeIdentifier=''):
394        """
395        Request items discovery from a node.
396
397        @param entity: Entity to send the request to.
398        @type entity: L{jid.JID}
399        @param nodeIdentifier: Optional node to request info from.
400        @type nodeIdentifier: C{unicode}
401        """
402
403        request = _DiscoRequest(self.xmlstream, NS_DISCO_ITEMS, nodeIdentifier)
404
405        d = request.send(entity.full())
406        d.addCallback(lambda iq: DiscoItems.fromElement(iq.query))
407        return d
408
409
410
411class DiscoHandler(XMPPHandler, IQHandlerMixin):
412    """
413    Protocol implementation for XMPP Service Discovery.
414
415    This handler will listen to XMPP service discovery requests and
416    query the other handlers in L{parent} (see L{XMPPHandlerContainer}) for
417    their identities, features and items according to L{IDisco}.
418    """
419
420    iqHandlers = {DISCO_INFO: '_onDiscoInfo',
421                  DISCO_ITEMS: '_onDiscoItems'}
422
423    def connectionInitialized(self):
424        self.xmlstream.addObserver(DISCO_INFO, self.handleRequest)
425        self.xmlstream.addObserver(DISCO_ITEMS, self.handleRequest)
426
427
428    def _onDiscoInfo(self, iq):
429        """
430        Called for incoming disco info requests.
431
432        @param iq: The request iq element.
433        @type iq: L{Element<twisted.words.xish.domish.Element>}
434        """
435        requestor = jid.internJID(iq["from"])
436        target = jid.internJID(iq["to"])
437        nodeIdentifier = iq.query.getAttribute("node", '')
438
439        def toResponse(info):
440            if nodeIdentifier and not info:
441                raise error.StanzaError('item-not-found')
442            else:
443                response = DiscoInfo()
444                response.nodeIdentifier = nodeIdentifier
445
446                for item in info:
447                    response.append(item)
448
449            return response.toElement()
450
451        d = self.info(requestor, target, nodeIdentifier)
452        d.addCallback(toResponse)
453        return d
454
455
456    def _onDiscoItems(self, iq):
457        """
458        Called for incoming disco items requests.
459
460        @param iq: The request iq element.
461        @type iq: L{Element<twisted.words.xish.domish.Element>}
462        """
463        requestor = jid.internJID(iq["from"])
464        target = jid.internJID(iq["to"])
465        nodeIdentifier = iq.query.getAttribute("node", '')
466
467        def toResponse(items):
468            response = DiscoItems()
469            response.nodeIdentifier = nodeIdentifier
470
471            for item in items:
472                response.append(item)
473
474            return response.toElement()
475
476        d = self.items(requestor, target, nodeIdentifier)
477        d.addCallback(toResponse)
478        return d
479
480
481    def _gatherResults(self, deferredList):
482        """
483        Gather results from a list of deferreds.
484
485        Similar to L{defer.gatherResults}, but flattens the returned results,
486        consumes errors after the first one and fires the errback of the
487        returned deferred with the failure of the first deferred that fires its
488        errback.
489
490        @param deferredList: List of deferreds for which the results should be
491                             gathered.
492        @type deferredList: C{list}
493        @return: Deferred that fires with a list of gathered results.
494        @rtype: L{defer.Deferred}
495        """
496        def cb(resultList):
497            results = []
498            for success, value in resultList:
499                results.extend(value)
500            return results
501
502        def eb(failure):
503            failure.trap(defer.FirstError)
504            return failure.value.subFailure
505
506        d = defer.DeferredList(deferredList, fireOnOneErrback=1,
507                                             consumeErrors=1)
508        d.addCallbacks(cb, eb)
509        return d
510
511
512    def info(self, requestor, target, nodeIdentifier):
513        """
514        Inspect all sibling protocol handlers for disco info.
515
516        Calls the L{getDiscoInfo<IDisco.getDiscoInfo>} method on all child
517        handlers of the parent, that provide L{IDisco}.
518
519        @param requestor: The entity that sent the request.
520        @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
521        @param target: The entity the request was sent to.
522        @type target: L{JID<twisted.words.protocols.jabber.jid.JID>}
523        @param nodeIdentifier: The optional node being queried, or C{''}.
524        @type nodeIdentifier: C{unicode}
525        @return: Deferred with the gathered results from sibling handlers.
526        @rtype: L{defer.Deferred}
527        """
528        dl = [handler.getDiscoInfo(requestor, target, nodeIdentifier)
529              for handler in self.parent
530              if IDisco.providedBy(handler)]
531        return self._gatherResults(dl)
532
533
534    def items(self, requestor, target, nodeIdentifier):
535        """
536        Inspect all sibling protocol handlers for disco items.
537
538        Calls the L{getDiscoItems<IDisco.getDiscoItems>} method on all child
539        handlers of the parent, that provide L{IDisco}.
540
541        @param requestor: The entity that sent the request.
542        @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
543        @param target: The entity the request was sent to.
544        @type target: L{JID<twisted.words.protocols.jabber.jid.JID>}
545        @param nodeIdentifier: The optional node being queried, or C{''}.
546        @type nodeIdentifier: C{unicode}
547        @return: Deferred with the gathered results from sibling handlers.
548        @rtype: L{defer.Deferred}
549        """
550        dl = [handler.getDiscoItems(requestor, target, nodeIdentifier)
551              for handler in self.parent
552              if IDisco.providedBy(handler)]
553        return self._gatherResults(dl)
Note: See TracBrowser for help on using the repository browser.