source: wokkel/disco.py @ 133:2ea562934152

wokkel-muc-client-support-24
Last change on this file since 133:2ea562934152 was 133:2ea562934152, checked in by Ralph Meijer <ralphm@…>, 11 years ago

Merge in trunk changes.

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