source: wokkel/disco.py @ 166:d9c10a5b5c0d

Last change on this file since 166:d9c10a5b5c0d was 166:d9c10a5b5c0d, checked in by Ralph Meijer <ralphm@…>, 9 years ago

Documentation fixes for pydoctor.

  • Property exe set to *
File size: 17.3 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 query the
459    other handlers in C{parent} (see
460    L{twisted.words.protocols.jabber.xmlstream.XMPPHandlerCollection})
461    for their identities, features and items according to L{IDisco}.
462    """
463
464    iqHandlers = {DISCO_INFO: '_onDiscoInfo',
465                  DISCO_ITEMS: '_onDiscoItems'}
466
467    def connectionInitialized(self):
468        self.xmlstream.addObserver(DISCO_INFO, self.handleRequest)
469        self.xmlstream.addObserver(DISCO_ITEMS, self.handleRequest)
470
471
472    def _onDiscoInfo(self, iq):
473        """
474        Called for incoming disco info requests.
475
476        @param iq: The request iq element.
477        @type iq: L{Element<twisted.words.xish.domish.Element>}
478        """
479        request = _DiscoRequest.fromElement(iq)
480
481        def toResponse(info):
482            if request.nodeIdentifier and not info:
483                raise error.StanzaError('item-not-found')
484            else:
485                response = DiscoInfo()
486                response.nodeIdentifier = request.nodeIdentifier
487
488                for item in info:
489                    response.append(item)
490
491            return response.toElement()
492
493        d = self.info(request.sender, request.recipient,
494                      request.nodeIdentifier)
495        d.addCallback(toResponse)
496        return d
497
498
499    def _onDiscoItems(self, iq):
500        """
501        Called for incoming disco items requests.
502
503        @param iq: The request iq element.
504        @type iq: L{Element<twisted.words.xish.domish.Element>}
505        """
506        request = _DiscoRequest.fromElement(iq)
507
508        def toResponse(items):
509            response = DiscoItems()
510            response.nodeIdentifier = request.nodeIdentifier
511
512            for item in items:
513                response.append(item)
514
515            return response.toElement()
516
517        d = self.items(request.sender, request.recipient,
518                       request.nodeIdentifier)
519        d.addCallback(toResponse)
520        return d
521
522
523    def _gatherResults(self, deferredList):
524        """
525        Gather results from a list of deferreds.
526
527        Similar to L{defer.gatherResults}, but flattens the returned results,
528        consumes errors after the first one and fires the errback of the
529        returned deferred with the failure of the first deferred that fires its
530        errback.
531
532        @param deferredList: List of deferreds for which the results should be
533                             gathered.
534        @type deferredList: C{list}
535        @return: Deferred that fires with a list of gathered results.
536        @rtype: L{defer.Deferred}
537        """
538        def cb(resultList):
539            results = []
540            for success, value in resultList:
541                results.extend(value)
542            return results
543
544        def eb(failure):
545            failure.trap(defer.FirstError)
546            return failure.value.subFailure
547
548        d = defer.DeferredList(deferredList, fireOnOneErrback=1,
549                                             consumeErrors=1)
550        d.addCallbacks(cb, eb)
551        return d
552
553
554    def info(self, requestor, target, nodeIdentifier):
555        """
556        Inspect all sibling protocol handlers for disco info.
557
558        Calls the L{getDiscoInfo<IDisco.getDiscoInfo>} method on all child
559        handlers of the parent, that provide L{IDisco}.
560
561        @param requestor: The entity that sent the request.
562        @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
563        @param target: The entity the request was sent to.
564        @type target: L{JID<twisted.words.protocols.jabber.jid.JID>}
565        @param nodeIdentifier: The optional node being queried, or C{''}.
566        @type nodeIdentifier: C{unicode}
567        @return: Deferred with the gathered results from sibling handlers.
568        @rtype: L{defer.Deferred}
569        """
570        dl = [defer.maybeDeferred(handler.getDiscoInfo, requestor, target,
571                                                        nodeIdentifier)
572              for handler in self.parent
573              if IDisco.providedBy(handler)]
574        return self._gatherResults(dl)
575
576
577    def items(self, requestor, target, nodeIdentifier):
578        """
579        Inspect all sibling protocol handlers for disco items.
580
581        Calls the L{getDiscoItems<IDisco.getDiscoItems>} method on all child
582        handlers of the parent, that provide L{IDisco}.
583
584        @param requestor: The entity that sent the request.
585        @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
586        @param target: The entity the request was sent to.
587        @type target: L{JID<twisted.words.protocols.jabber.jid.JID>}
588        @param nodeIdentifier: The optional node being queried, or C{''}.
589        @type nodeIdentifier: C{unicode}
590        @return: Deferred with the gathered results from sibling handlers.
591        @rtype: L{defer.Deferred}
592        """
593        dl = [defer.maybeDeferred(handler.getDiscoItems, requestor, target,
594                                                         nodeIdentifier)
595              for handler in self.parent
596              if IDisco.providedBy(handler)]
597        return self._gatherResults(dl)
Note: See TracBrowser for help on using the repository browser.