source: wokkel/disco.py @ 62:b98e610f62cc

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

Add sender parameter to disco requests.

Author: ralphm
Fixes #52.

File size: 16.4 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='', sender=None):
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
383        @param nodeIdentifier: Optional node to request info from.
384        @type nodeIdentifier: C{unicode}
385
386        @param sender: Optional sender address.
387        @type sender: L{jid.JID}
388        """
389
390        request = _DiscoRequest(self.xmlstream, NS_DISCO_INFO, nodeIdentifier)
391        if sender is not None:
392            request['from'] = unicode(sender)
393
394        d = request.send(entity.full())
395        d.addCallback(lambda iq: DiscoInfo.fromElement(iq.query))
396        return d
397
398
399    def requestItems(self, entity, nodeIdentifier='', sender=None):
400        """
401        Request items discovery from a node.
402
403        @param entity: Entity to send the request to.
404        @type entity: L{jid.JID}
405
406        @param nodeIdentifier: Optional node to request info from.
407        @type nodeIdentifier: C{unicode}
408
409        @param sender: Optional sender address.
410        @type sender: L{jid.JID}
411        """
412
413        request = _DiscoRequest(self.xmlstream, NS_DISCO_ITEMS, nodeIdentifier)
414        if sender is not None:
415            request['from'] = unicode(sender)
416
417        d = request.send(entity.full())
418        d.addCallback(lambda iq: DiscoItems.fromElement(iq.query))
419        return d
420
421
422
423class DiscoHandler(XMPPHandler, IQHandlerMixin):
424    """
425    Protocol implementation for XMPP Service Discovery.
426
427    This handler will listen to XMPP service discovery requests and
428    query the other handlers in L{parent} (see L{XMPPHandlerContainer}) for
429    their identities, features and items according to L{IDisco}.
430    """
431
432    iqHandlers = {DISCO_INFO: '_onDiscoInfo',
433                  DISCO_ITEMS: '_onDiscoItems'}
434
435    def connectionInitialized(self):
436        self.xmlstream.addObserver(DISCO_INFO, self.handleRequest)
437        self.xmlstream.addObserver(DISCO_ITEMS, self.handleRequest)
438
439
440    def _onDiscoInfo(self, iq):
441        """
442        Called for incoming disco info requests.
443
444        @param iq: The request iq element.
445        @type iq: L{Element<twisted.words.xish.domish.Element>}
446        """
447        requestor = jid.internJID(iq["from"])
448        target = jid.internJID(iq["to"])
449        nodeIdentifier = iq.query.getAttribute("node", '')
450
451        def toResponse(info):
452            if nodeIdentifier and not info:
453                raise error.StanzaError('item-not-found')
454            else:
455                response = DiscoInfo()
456                response.nodeIdentifier = nodeIdentifier
457
458                for item in info:
459                    response.append(item)
460
461            return response.toElement()
462
463        d = self.info(requestor, target, nodeIdentifier)
464        d.addCallback(toResponse)
465        return d
466
467
468    def _onDiscoItems(self, iq):
469        """
470        Called for incoming disco items requests.
471
472        @param iq: The request iq element.
473        @type iq: L{Element<twisted.words.xish.domish.Element>}
474        """
475        requestor = jid.internJID(iq["from"])
476        target = jid.internJID(iq["to"])
477        nodeIdentifier = iq.query.getAttribute("node", '')
478
479        def toResponse(items):
480            response = DiscoItems()
481            response.nodeIdentifier = nodeIdentifier
482
483            for item in items:
484                response.append(item)
485
486            return response.toElement()
487
488        d = self.items(requestor, target, nodeIdentifier)
489        d.addCallback(toResponse)
490        return d
491
492
493    def _gatherResults(self, deferredList):
494        """
495        Gather results from a list of deferreds.
496
497        Similar to L{defer.gatherResults}, but flattens the returned results,
498        consumes errors after the first one and fires the errback of the
499        returned deferred with the failure of the first deferred that fires its
500        errback.
501
502        @param deferredList: List of deferreds for which the results should be
503                             gathered.
504        @type deferredList: C{list}
505        @return: Deferred that fires with a list of gathered results.
506        @rtype: L{defer.Deferred}
507        """
508        def cb(resultList):
509            results = []
510            for success, value in resultList:
511                results.extend(value)
512            return results
513
514        def eb(failure):
515            failure.trap(defer.FirstError)
516            return failure.value.subFailure
517
518        d = defer.DeferredList(deferredList, fireOnOneErrback=1,
519                                             consumeErrors=1)
520        d.addCallbacks(cb, eb)
521        return d
522
523
524    def info(self, requestor, target, nodeIdentifier):
525        """
526        Inspect all sibling protocol handlers for disco info.
527
528        Calls the L{getDiscoInfo<IDisco.getDiscoInfo>} method on all child
529        handlers of the parent, that provide L{IDisco}.
530
531        @param requestor: The entity that sent the request.
532        @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
533        @param target: The entity the request was sent to.
534        @type target: L{JID<twisted.words.protocols.jabber.jid.JID>}
535        @param nodeIdentifier: The optional node being queried, or C{''}.
536        @type nodeIdentifier: C{unicode}
537        @return: Deferred with the gathered results from sibling handlers.
538        @rtype: L{defer.Deferred}
539        """
540        dl = [handler.getDiscoInfo(requestor, target, nodeIdentifier)
541              for handler in self.parent
542              if IDisco.providedBy(handler)]
543        return self._gatherResults(dl)
544
545
546    def items(self, requestor, target, nodeIdentifier):
547        """
548        Inspect all sibling protocol handlers for disco items.
549
550        Calls the L{getDiscoItems<IDisco.getDiscoItems>} method on all child
551        handlers of the parent, that provide L{IDisco}.
552
553        @param requestor: The entity that sent the request.
554        @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
555        @param target: The entity the request was sent to.
556        @type target: L{JID<twisted.words.protocols.jabber.jid.JID>}
557        @param nodeIdentifier: The optional node being queried, or C{''}.
558        @type nodeIdentifier: C{unicode}
559        @return: Deferred with the gathered results from sibling handlers.
560        @rtype: L{defer.Deferred}
561        """
562        dl = [handler.getDiscoItems(requestor, target, nodeIdentifier)
563              for handler in self.parent
564              if IDisco.providedBy(handler)]
565        return self._gatherResults(dl)
Note: See TracBrowser for help on using the repository browser.