source: wokkel/disco.py @ 198:7110457aff51

Last change on this file since 198:7110457aff51 was 198:7110457aff51, checked in by Ralph Meijer <ralphm@…>, 4 years ago

imported patch py3-disco.patch

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