source: wokkel/disco.py @ 165:76a61f5aa343

Last change on this file since 165:76a61f5aa343 was 165:76a61f5aa343, checked in by Ralph Meijer <ralphm@…>, 8 years ago

Cleanups leading up to Wokkel 0.7.0.

As we now depend on Twisted 10.0.0 or higher, the following classes and
interfaces were deprecated:

This also resolves all Pyflakes warnings, changes links for www.xmpp.org to
xmpp.org and fixes the copyright notice in LICENSE to include 2012.

  • Property exe set to *
File size: 17.2 KB
Line 
1# -*- test-case-name: wokkel.test.test_disco -*-
2#
3# Copyright (c) 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://xmpp.org/extensions/xep-0030.html>}.
11"""
12
13from twisted.internet import defer
14from twisted.words.protocols.jabber import error, jid
15from twisted.words.xish import domish
16
17from wokkel import data_form, generic
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 is not None:
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(generic.Request):
349    """
350    A Service Discovery request.
351
352    @ivar verb: Type of request: C{'info'} or C{'items'}.
353    @type verb: C{str}
354    @ivar nodeIdentifier: Optional node to request info for.
355    @type nodeIdentifier: C{unicode}
356    """
357
358    verb = None
359    nodeIdentifier = ''
360
361    _requestVerbMap = {
362            NS_DISCO_INFO: 'info',
363            NS_DISCO_ITEMS: 'items',
364            }
365
366    _verbRequestMap = dict(((v, k) for k, v in _requestVerbMap.iteritems()))
367
368    def __init__(self, verb=None, nodeIdentifier='',
369                       recipient=None, sender=None):
370        generic.Request.__init__(self, recipient=recipient, sender=sender,
371                                       stanzaType='get')
372        self.verb = verb
373        self.nodeIdentifier = nodeIdentifier
374
375
376    def parseElement(self, element):
377        generic.Request.parseElement(self, element)
378
379        verbElement = None
380        for child in element.elements():
381            if child.name == 'query' and child.uri in self._requestVerbMap:
382                self.verb = self._requestVerbMap[child.uri]
383                verbElement = child
384
385        if verbElement:
386            self.nodeIdentifier = verbElement.getAttribute('node', '')
387
388
389    def toElement(self):
390        element = generic.Request.toElement(self)
391
392        childURI = self._verbRequestMap[self.verb]
393        query = element.addElement((childURI, 'query'))
394
395        if self.nodeIdentifier:
396            query['node'] = self.nodeIdentifier
397
398        return element
399
400
401
402class DiscoClientProtocol(XMPPHandler):
403    """
404    XMPP Service Discovery client protocol.
405    """
406
407    def requestInfo(self, entity, nodeIdentifier='', sender=None):
408        """
409        Request information discovery from a node.
410
411        @param entity: Entity to send the request to.
412        @type entity: L{jid.JID}
413
414        @param nodeIdentifier: Optional node to request info from.
415        @type nodeIdentifier: C{unicode}
416
417        @param sender: Optional sender address.
418        @type sender: L{jid.JID}
419        """
420
421        request = _DiscoRequest('info', nodeIdentifier)
422        request.sender = sender
423        request.recipient = entity
424
425        d = self.request(request)
426        d.addCallback(lambda iq: DiscoInfo.fromElement(iq.query))
427        return d
428
429
430    def requestItems(self, entity, nodeIdentifier='', sender=None):
431        """
432        Request items discovery from a node.
433
434        @param entity: Entity to send the request to.
435        @type entity: L{jid.JID}
436
437        @param nodeIdentifier: Optional node to request info from.
438        @type nodeIdentifier: C{unicode}
439
440        @param sender: Optional sender address.
441        @type sender: L{jid.JID}
442        """
443
444        request = _DiscoRequest('items', nodeIdentifier)
445        request.sender = sender
446        request.recipient = entity
447
448        d = self.request(request)
449        d.addCallback(lambda iq: DiscoItems.fromElement(iq.query))
450        return d
451
452
453
454class DiscoHandler(XMPPHandler, IQHandlerMixin):
455    """
456    Protocol implementation for XMPP Service Discovery.
457
458    This handler will listen to XMPP service discovery requests and
459    query the other handlers in L{parent} (see L{XMPPHandlerContainer}) for
460    their identities, features and items according to L{IDisco}.
461    """
462
463    iqHandlers = {DISCO_INFO: '_onDiscoInfo',
464                  DISCO_ITEMS: '_onDiscoItems'}
465
466    def connectionInitialized(self):
467        self.xmlstream.addObserver(DISCO_INFO, self.handleRequest)
468        self.xmlstream.addObserver(DISCO_ITEMS, self.handleRequest)
469
470
471    def _onDiscoInfo(self, iq):
472        """
473        Called for incoming disco info requests.
474
475        @param iq: The request iq element.
476        @type iq: L{Element<twisted.words.xish.domish.Element>}
477        """
478        request = _DiscoRequest.fromElement(iq)
479
480        def toResponse(info):
481            if request.nodeIdentifier and not info:
482                raise error.StanzaError('item-not-found')
483            else:
484                response = DiscoInfo()
485                response.nodeIdentifier = request.nodeIdentifier
486
487                for item in info:
488                    response.append(item)
489
490            return response.toElement()
491
492        d = self.info(request.sender, request.recipient,
493                      request.nodeIdentifier)
494        d.addCallback(toResponse)
495        return d
496
497
498    def _onDiscoItems(self, iq):
499        """
500        Called for incoming disco items requests.
501
502        @param iq: The request iq element.
503        @type iq: L{Element<twisted.words.xish.domish.Element>}
504        """
505        request = _DiscoRequest.fromElement(iq)
506
507        def toResponse(items):
508            response = DiscoItems()
509            response.nodeIdentifier = request.nodeIdentifier
510
511            for item in items:
512                response.append(item)
513
514            return response.toElement()
515
516        d = self.items(request.sender, request.recipient,
517                       request.nodeIdentifier)
518        d.addCallback(toResponse)
519        return d
520
521
522    def _gatherResults(self, deferredList):
523        """
524        Gather results from a list of deferreds.
525
526        Similar to L{defer.gatherResults}, but flattens the returned results,
527        consumes errors after the first one and fires the errback of the
528        returned deferred with the failure of the first deferred that fires its
529        errback.
530
531        @param deferredList: List of deferreds for which the results should be
532                             gathered.
533        @type deferredList: C{list}
534        @return: Deferred that fires with a list of gathered results.
535        @rtype: L{defer.Deferred}
536        """
537        def cb(resultList):
538            results = []
539            for success, value in resultList:
540                results.extend(value)
541            return results
542
543        def eb(failure):
544            failure.trap(defer.FirstError)
545            return failure.value.subFailure
546
547        d = defer.DeferredList(deferredList, fireOnOneErrback=1,
548                                             consumeErrors=1)
549        d.addCallbacks(cb, eb)
550        return d
551
552
553    def info(self, requestor, target, nodeIdentifier):
554        """
555        Inspect all sibling protocol handlers for disco info.
556
557        Calls the L{getDiscoInfo<IDisco.getDiscoInfo>} method on all child
558        handlers of the parent, that provide L{IDisco}.
559
560        @param requestor: The entity that sent the request.
561        @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
562        @param target: The entity the request was sent to.
563        @type target: L{JID<twisted.words.protocols.jabber.jid.JID>}
564        @param nodeIdentifier: The optional node being queried, or C{''}.
565        @type nodeIdentifier: C{unicode}
566        @return: Deferred with the gathered results from sibling handlers.
567        @rtype: L{defer.Deferred}
568        """
569        dl = [defer.maybeDeferred(handler.getDiscoInfo, requestor, target,
570                                                        nodeIdentifier)
571              for handler in self.parent
572              if IDisco.providedBy(handler)]
573        return self._gatherResults(dl)
574
575
576    def items(self, requestor, target, nodeIdentifier):
577        """
578        Inspect all sibling protocol handlers for disco items.
579
580        Calls the L{getDiscoItems<IDisco.getDiscoItems>} method on all child
581        handlers of the parent, that provide L{IDisco}.
582
583        @param requestor: The entity that sent the request.
584        @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
585        @param target: The entity the request was sent to.
586        @type target: L{JID<twisted.words.protocols.jabber.jid.JID>}
587        @param nodeIdentifier: The optional node being queried, or C{''}.
588        @type nodeIdentifier: C{unicode}
589        @return: Deferred with the gathered results from sibling handlers.
590        @rtype: L{defer.Deferred}
591        """
592        dl = [defer.maybeDeferred(handler.getDiscoItems, requestor, target,
593                                                         nodeIdentifier)
594              for handler in self.parent
595              if IDisco.providedBy(handler)]
596        return self._gatherResults(dl)
Note: See TracBrowser for help on using the repository browser.