Rename to slixmpp
[slixmpp.git] / slixmpp / plugins / xep_0030 / disco.py
blob1283795886acc4651194456a028dea37f0f16818
1 """
2 Slixmpp: The Slick XMPP Library
3 Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
4 This file is part of Slixmpp.
6 See the file LICENSE for copying permission.
7 """
9 import logging
11 from slixmpp import Iq
12 from slixmpp.plugins import BasePlugin
13 from slixmpp.xmlstream.handler import Callback
14 from slixmpp.xmlstream.matcher import StanzaPath
15 from slixmpp.xmlstream import register_stanza_plugin, JID
16 from slixmpp.plugins.xep_0030 import stanza, DiscoInfo, DiscoItems
17 from slixmpp.plugins.xep_0030 import StaticDisco
20 log = logging.getLogger(__name__)
23 class XEP_0030(BasePlugin):
25 """
26 XEP-0030: Service Discovery
28 Service discovery in XMPP allows entities to discover information about
29 other agents in the network, such as the feature sets supported by a
30 client, or signposts to other, related entities.
32 Also see <http://www.xmpp.org/extensions/xep-0030.html>.
34 The XEP-0030 plugin works using a hierarchy of dynamic
35 node handlers, ranging from global handlers to specific
36 JID+node handlers. The default set of handlers operate
37 in a static manner, storing disco information in memory.
38 However, custom handlers may use any available backend
39 storage mechanism desired, such as SQLite or Redis.
41 Node handler hierarchy:
42 JID | Node | Level
43 ---------------------
44 None | None | Global
45 Given | None | All nodes for the JID
46 None | Given | Node on self.xmpp.boundjid
47 Given | Given | A single node
49 Stream Handlers:
50 Disco Info -- Any Iq stanze that includes a query with the
51 namespace http://jabber.org/protocol/disco#info.
52 Disco Items -- Any Iq stanze that includes a query with the
53 namespace http://jabber.org/protocol/disco#items.
55 Events:
56 disco_info -- Received a disco#info Iq query result.
57 disco_items -- Received a disco#items Iq query result.
58 disco_info_query -- Received a disco#info Iq query request.
59 disco_items_query -- Received a disco#items Iq query request.
61 Attributes:
62 stanza -- A reference to the module containing the
63 stanza classes provided by this plugin.
64 static -- Object containing the default set of
65 static node handlers.
66 default_handlers -- A dictionary mapping operations to the default
67 global handler (by default, the static handlers).
68 xmpp -- The main Slixmpp object.
70 Methods:
71 set_node_handler -- Assign a handler to a JID/node combination.
72 del_node_handler -- Remove a handler from a JID/node combination.
73 get_info -- Retrieve disco#info data, locally or remote.
74 get_items -- Retrieve disco#items data, locally or remote.
75 set_identities --
76 set_features --
77 set_items --
78 del_items --
79 del_identity --
80 del_feature --
81 del_item --
82 add_identity --
83 add_feature --
84 add_item --
85 """
87 name = 'xep_0030'
88 description = 'XEP-0030: Service Discovery'
89 dependencies = set()
90 stanza = stanza
91 default_config = {
92 'use_cache': True,
93 'wrap_results': False
96 def plugin_init(self):
97 """
98 Start the XEP-0030 plugin.
99 """
100 self.xmpp.register_handler(
101 Callback('Disco Info',
102 StanzaPath('iq/disco_info'),
103 self._handle_disco_info))
105 self.xmpp.register_handler(
106 Callback('Disco Items',
107 StanzaPath('iq/disco_items'),
108 self._handle_disco_items))
110 register_stanza_plugin(Iq, DiscoInfo)
111 register_stanza_plugin(Iq, DiscoItems)
113 self.static = StaticDisco(self.xmpp, self)
115 self._disco_ops = [
116 'get_info', 'set_info', 'set_identities', 'set_features',
117 'get_items', 'set_items', 'del_items', 'add_identity',
118 'del_identity', 'add_feature', 'del_feature', 'add_item',
119 'del_item', 'del_identities', 'del_features', 'cache_info',
120 'get_cached_info', 'supports', 'has_identity']
122 for op in self._disco_ops:
123 self.api.register(getattr(self.static, op), op, default=True)
125 def _add_disco_op(self, op, default_handler):
126 self.api.register(default_handler, op)
127 self.api.register_default(default_handler, op)
129 def set_node_handler(self, htype, jid=None, node=None, handler=None):
131 Add a node handler for the given hierarchy level and
132 handler type.
134 Node handlers are ordered in a hierarchy where the
135 most specific handler is executed. Thus, a fallback,
136 global handler can be used for the majority of cases
137 with a few node specific handler that override the
138 global behavior.
140 Node handler hierarchy:
141 JID | Node | Level
142 ---------------------
143 None | None | Global
144 Given | None | All nodes for the JID
145 None | Given | Node on self.xmpp.boundjid
146 Given | Given | A single node
148 Handler types:
149 get_info
150 get_items
151 set_identities
152 set_features
153 set_items
154 del_items
155 del_identities
156 del_identity
157 del_feature
158 del_features
159 del_item
160 add_identity
161 add_feature
162 add_item
164 Arguments:
165 htype -- The operation provided by the handler.
166 jid -- The JID the handler applies to. May be narrowed
167 further if a node is given.
168 node -- The particular node the handler is for. If no JID
169 is given, then the self.xmpp.boundjid.full is
170 assumed.
171 handler -- The handler function to use.
173 self.api.register(handler, htype, jid, node)
175 def del_node_handler(self, htype, jid, node):
177 Remove a handler type for a JID and node combination.
179 The next handler in the hierarchy will be used if one
180 exists. If removing the global handler, make sure that
181 other handlers exist to process existing nodes.
183 Node handler hierarchy:
184 JID | Node | Level
185 ---------------------
186 None | None | Global
187 Given | None | All nodes for the JID
188 None | Given | Node on self.xmpp.boundjid
189 Given | Given | A single node
191 Arguments:
192 htype -- The type of handler to remove.
193 jid -- The JID from which to remove the handler.
194 node -- The node from which to remove the handler.
196 self.api.unregister(htype, jid, node)
198 def restore_defaults(self, jid=None, node=None, handlers=None):
200 Change all or some of a node's handlers to the default
201 handlers. Useful for manually overriding the contents
202 of a node that would otherwise be handled by a JID level
203 or global level dynamic handler.
205 The default is to use the built-in static handlers, but that
206 may be changed by modifying self.default_handlers.
208 Arguments:
209 jid -- The JID owning the node to modify.
210 node -- The node to change to using static handlers.
211 handlers -- Optional list of handlers to change to the
212 default version. If provided, only these
213 handlers will be changed. Otherwise, all
214 handlers will use the default version.
216 if handlers is None:
217 handlers = self._disco_ops
218 for op in handlers:
219 self.api.restore_default(op, jid, node)
221 def supports(self, jid=None, node=None, feature=None, local=False,
222 cached=True, ifrom=None):
224 Check if a JID supports a given feature.
226 Return values:
227 True -- The feature is supported
228 False -- The feature is not listed as supported
229 None -- Nothing could be found due to a timeout
231 Arguments:
232 jid -- Request info from this JID.
233 node -- The particular node to query.
234 feature -- The name of the feature to check.
235 local -- If true, then the query is for a JID/node
236 combination handled by this Slixmpp instance and
237 no stanzas need to be sent.
238 Otherwise, a disco stanza must be sent to the
239 remove JID to retrieve the info.
240 cached -- If true, then look for the disco info data from
241 the local cache system. If no results are found,
242 send the query as usual. The self.use_cache
243 setting must be set to true for this option to
244 be useful. If set to false, then the cache will
245 be skipped, even if a result has already been
246 cached. Defaults to false.
247 ifrom -- Specifiy the sender's JID.
249 data = {'feature': feature,
250 'local': local,
251 'cached': cached}
252 return self.api['supports'](jid, node, ifrom, data)
254 def has_identity(self, jid=None, node=None, category=None, itype=None,
255 lang=None, local=False, cached=True, ifrom=None):
257 Check if a JID provides a given identity.
259 Return values:
260 True -- The identity is provided
261 False -- The identity is not listed
262 None -- Nothing could be found due to a timeout
264 Arguments:
265 jid -- Request info from this JID.
266 node -- The particular node to query.
267 category -- The category of the identity to check.
268 itype -- The type of the identity to check.
269 lang -- The language of the identity to check.
270 local -- If true, then the query is for a JID/node
271 combination handled by this Slixmpp instance and
272 no stanzas need to be sent.
273 Otherwise, a disco stanza must be sent to the
274 remove JID to retrieve the info.
275 cached -- If true, then look for the disco info data from
276 the local cache system. If no results are found,
277 send the query as usual. The self.use_cache
278 setting must be set to true for this option to
279 be useful. If set to false, then the cache will
280 be skipped, even if a result has already been
281 cached. Defaults to false.
282 ifrom -- Specifiy the sender's JID.
284 data = {'category': category,
285 'itype': itype,
286 'lang': lang,
287 'local': local,
288 'cached': cached}
289 return self.api['has_identity'](jid, node, ifrom, data)
291 def get_info(self, jid=None, node=None, local=None,
292 cached=None, **kwargs):
294 Retrieve the disco#info results from a given JID/node combination.
296 Info may be retrieved from both local resources and remote agents;
297 the local parameter indicates if the information should be gathered
298 by executing the local node handlers, or if a disco#info stanza
299 must be generated and sent.
301 If requesting items from a local JID/node, then only a DiscoInfo
302 stanza will be returned. Otherwise, an Iq stanza will be returned.
304 Arguments:
305 jid -- Request info from this JID.
306 node -- The particular node to query.
307 local -- If true, then the query is for a JID/node
308 combination handled by this Slixmpp instance and
309 no stanzas need to be sent.
310 Otherwise, a disco stanza must be sent to the
311 remove JID to retrieve the info.
312 cached -- If true, then look for the disco info data from
313 the local cache system. If no results are found,
314 send the query as usual. The self.use_cache
315 setting must be set to true for this option to
316 be useful. If set to false, then the cache will
317 be skipped, even if a result has already been
318 cached. Defaults to false.
319 ifrom -- Specifiy the sender's JID.
320 block -- If true, block and wait for the stanzas' reply.
321 timeout -- The time in seconds to block while waiting for
322 a reply. If None, then wait indefinitely. The
323 timeout value is only used when block=True.
324 callback -- Optional callback to execute when a reply is
325 received instead of blocking and waiting for
326 the reply.
327 timeout_callback -- Optional callback to execute when no result
328 has been received in timeout seconds.
330 if local is None:
331 if jid is not None and not isinstance(jid, JID):
332 jid = JID(jid)
333 if self.xmpp.is_component:
334 if jid.domain == self.xmpp.boundjid.domain:
335 local = True
336 else:
337 if str(jid) == str(self.xmpp.boundjid):
338 local = True
339 jid = jid.full
340 elif jid in (None, ''):
341 local = True
343 if local:
344 log.debug("Looking up local disco#info data " + \
345 "for %s, node %s.", jid, node)
346 info = self.api['get_info'](jid, node,
347 kwargs.get('ifrom', None),
348 kwargs)
349 info = self._fix_default_info(info)
350 return self._wrap(kwargs.get('ifrom', None), jid, info)
352 if cached:
353 log.debug("Looking up cached disco#info data " + \
354 "for %s, node %s.", jid, node)
355 info = self.api['get_cached_info'](jid, node,
356 kwargs.get('ifrom', None),
357 kwargs)
358 if info is not None:
359 return self._wrap(kwargs.get('ifrom', None), jid, info)
361 iq = self.xmpp.Iq()
362 # Check dfrom parameter for backwards compatibility
363 iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
364 iq['to'] = jid
365 iq['type'] = 'get'
366 iq['disco_info']['node'] = node if node else ''
367 return iq.send(timeout=kwargs.get('timeout', None),
368 block=kwargs.get('block', True),
369 callback=kwargs.get('callback', None),
370 timeout_callback=kwargs.get('timeout_callback', None))
372 def set_info(self, jid=None, node=None, info=None):
374 Set the disco#info data for a JID/node based on an existing
375 disco#info stanza.
377 if isinstance(info, Iq):
378 info = info['disco_info']
379 self.api['set_info'](jid, node, None, info)
381 def get_items(self, jid=None, node=None, local=False, **kwargs):
383 Retrieve the disco#items results from a given JID/node combination.
385 Items may be retrieved from both local resources and remote agents;
386 the local parameter indicates if the items should be gathered by
387 executing the local node handlers, or if a disco#items stanza must
388 be generated and sent.
390 If requesting items from a local JID/node, then only a DiscoItems
391 stanza will be returned. Otherwise, an Iq stanza will be returned.
393 Arguments:
394 jid -- Request info from this JID.
395 node -- The particular node to query.
396 local -- If true, then the query is for a JID/node
397 combination handled by this Slixmpp instance and
398 no stanzas need to be sent.
399 Otherwise, a disco stanza must be sent to the
400 remove JID to retrieve the items.
401 ifrom -- Specifiy the sender's JID.
402 block -- If true, block and wait for the stanzas' reply.
403 timeout -- The time in seconds to block while waiting for
404 a reply. If None, then wait indefinitely.
405 callback -- Optional callback to execute when a reply is
406 received instead of blocking and waiting for
407 the reply.
408 iterator -- If True, return a result set iterator using
409 the XEP-0059 plugin, if the plugin is loaded.
410 Otherwise the parameter is ignored.
411 timeout_callback -- Optional callback to execute when no result
412 has been received in timeout seconds.
414 if local or local is None and jid is None:
415 items = self.api['get_items'](jid, node,
416 kwargs.get('ifrom', None),
417 kwargs)
418 return self._wrap(kwargs.get('ifrom', None), jid, items)
420 iq = self.xmpp.Iq()
421 # Check dfrom parameter for backwards compatibility
422 iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
423 iq['to'] = jid
424 iq['type'] = 'get'
425 iq['disco_items']['node'] = node if node else ''
426 if kwargs.get('iterator', False) and self.xmpp['xep_0059']:
427 return self.xmpp['xep_0059'].iterate(iq, 'disco_items')
428 else:
429 return iq.send(timeout=kwargs.get('timeout', None),
430 block=kwargs.get('block', True),
431 callback=kwargs.get('callback', None),
432 timeout_callback=kwargs.get('timeout_callback', None))
434 def set_items(self, jid=None, node=None, **kwargs):
436 Set or replace all items for the specified JID/node combination.
438 The given items must be in a list or set where each item is a
439 tuple of the form: (jid, node, name).
441 Arguments:
442 jid -- The JID to modify.
443 node -- Optional node to modify.
444 items -- A series of items in tuple format.
446 self.api['set_items'](jid, node, None, kwargs)
448 def del_items(self, jid=None, node=None, **kwargs):
450 Remove all items from the given JID/node combination.
452 Arguments:
453 jid -- The JID to modify.
454 node -- Optional node to modify.
456 self.api['del_items'](jid, node, None, kwargs)
458 def add_item(self, jid='', name='', node=None, subnode='', ijid=None):
460 Add a new item element to the given JID/node combination.
462 Each item is required to have a JID, but may also specify
463 a node value to reference non-addressable entities.
465 Arguments:
466 jid -- The JID for the item.
467 name -- Optional name for the item.
468 node -- The node to modify.
469 subnode -- Optional node for the item.
470 ijid -- The JID to modify.
472 if not jid:
473 jid = self.xmpp.boundjid.full
474 kwargs = {'ijid': jid,
475 'name': name,
476 'inode': subnode}
477 self.api['add_item'](ijid, node, None, kwargs)
479 def del_item(self, jid=None, node=None, **kwargs):
481 Remove a single item from the given JID/node combination.
483 Arguments:
484 jid -- The JID to modify.
485 node -- The node to modify.
486 ijid -- The item's JID.
487 inode -- The item's node.
489 self.api['del_item'](jid, node, None, kwargs)
491 def add_identity(self, category='', itype='', name='',
492 node=None, jid=None, lang=None):
494 Add a new identity to the given JID/node combination.
496 Each identity must be unique in terms of all four identity
497 components: category, type, name, and language.
499 Multiple, identical category/type pairs are allowed only
500 if the xml:lang values are different. Likewise, multiple
501 category/type/xml:lang pairs are allowed so long as the
502 names are different. A category and type is always required.
504 Arguments:
505 category -- The identity's category.
506 itype -- The identity's type.
507 name -- Optional name for the identity.
508 lang -- Optional two-letter language code.
509 node -- The node to modify.
510 jid -- The JID to modify.
512 kwargs = {'category': category,
513 'itype': itype,
514 'name': name,
515 'lang': lang}
516 self.api['add_identity'](jid, node, None, kwargs)
518 def add_feature(self, feature, node=None, jid=None):
520 Add a feature to a JID/node combination.
522 Arguments:
523 feature -- The namespace of the supported feature.
524 node -- The node to modify.
525 jid -- The JID to modify.
527 kwargs = {'feature': feature}
528 self.api['add_feature'](jid, node, None, kwargs)
530 def del_identity(self, jid=None, node=None, **kwargs):
532 Remove an identity from the given JID/node combination.
534 Arguments:
535 jid -- The JID to modify.
536 node -- The node to modify.
537 category -- The identity's category.
538 itype -- The identity's type value.
539 name -- Optional, human readable name for the identity.
540 lang -- Optional, the identity's xml:lang value.
542 self.api['del_identity'](jid, node, None, kwargs)
544 def del_feature(self, jid=None, node=None, **kwargs):
546 Remove a feature from a given JID/node combination.
548 Arguments:
549 jid -- The JID to modify.
550 node -- The node to modify.
551 feature -- The feature's namespace.
553 self.api['del_feature'](jid, node, None, kwargs)
555 def set_identities(self, jid=None, node=None, **kwargs):
557 Add or replace all identities for the given JID/node combination.
559 The identities must be in a set where each identity is a tuple
560 of the form: (category, type, lang, name)
562 Arguments:
563 jid -- The JID to modify.
564 node -- The node to modify.
565 identities -- A set of identities in tuple form.
566 lang -- Optional, xml:lang value.
568 self.api['set_identities'](jid, node, None, kwargs)
570 def del_identities(self, jid=None, node=None, **kwargs):
572 Remove all identities for a JID/node combination.
574 If a language is specified, only identities using that
575 language will be removed.
577 Arguments:
578 jid -- The JID to modify.
579 node -- The node to modify.
580 lang -- Optional. If given, only remove identities
581 using this xml:lang value.
583 self.api['del_identities'](jid, node, None, kwargs)
585 def set_features(self, jid=None, node=None, **kwargs):
587 Add or replace the set of supported features
588 for a JID/node combination.
590 Arguments:
591 jid -- The JID to modify.
592 node -- The node to modify.
593 features -- The new set of supported features.
595 self.api['set_features'](jid, node, None, kwargs)
597 def del_features(self, jid=None, node=None, **kwargs):
599 Remove all features from a JID/node combination.
601 Arguments:
602 jid -- The JID to modify.
603 node -- The node to modify.
605 self.api['del_features'](jid, node, None, kwargs)
607 def _run_node_handler(self, htype, jid, node=None, ifrom=None, data={}):
609 Execute the most specific node handler for the given
610 JID/node combination.
612 Arguments:
613 htype -- The handler type to execute.
614 jid -- The JID requested.
615 node -- The node requested.
616 data -- Optional, custom data to pass to the handler.
618 return self.api[htype](jid, node, ifrom, data)
620 def _handle_disco_info(self, iq):
622 Process an incoming disco#info stanza. If it is a get
623 request, find and return the appropriate identities
624 and features. If it is an info result, fire the
625 disco_info event.
627 Arguments:
628 iq -- The incoming disco#items stanza.
630 if iq['type'] == 'get':
631 log.debug("Received disco info query from " + \
632 "<%s> to <%s>.", iq['from'], iq['to'])
633 info = self.api['get_info'](iq['to'],
634 iq['disco_info']['node'],
635 iq['from'],
637 if isinstance(info, Iq):
638 info['id'] = iq['id']
639 info.send()
640 else:
641 iq.reply()
642 if info:
643 info = self._fix_default_info(info)
644 iq.set_payload(info.xml)
645 iq.send()
646 elif iq['type'] == 'result':
647 log.debug("Received disco info result from " + \
648 "<%s> to <%s>.", iq['from'], iq['to'])
649 if self.use_cache:
650 log.debug("Caching disco info result from " \
651 "<%s> to <%s>.", iq['from'], iq['to'])
652 if self.xmpp.is_component:
653 ito = iq['to'].full
654 else:
655 ito = None
656 self.api['cache_info'](iq['from'],
657 iq['disco_info']['node'],
658 ito,
660 self.xmpp.event('disco_info', iq)
662 def _handle_disco_items(self, iq):
664 Process an incoming disco#items stanza. If it is a get
665 request, find and return the appropriate items. If it
666 is an items result, fire the disco_items event.
668 Arguments:
669 iq -- The incoming disco#items stanza.
671 if iq['type'] == 'get':
672 log.debug("Received disco items query from " + \
673 "<%s> to <%s>.", iq['from'], iq['to'])
674 items = self.api['get_items'](iq['to'],
675 iq['disco_items']['node'],
676 iq['from'],
678 if isinstance(items, Iq):
679 items.send()
680 else:
681 iq.reply()
682 if items:
683 iq.set_payload(items.xml)
684 iq.send()
685 elif iq['type'] == 'result':
686 log.debug("Received disco items result from " + \
687 "%s to %s.", iq['from'], iq['to'])
688 self.xmpp.event('disco_items', iq)
690 def _fix_default_info(self, info):
692 Disco#info results for a JID are required to include at least
693 one identity and feature. As a default, if no other identity is
694 provided, Slixmpp will use either the generic component or the
695 bot client identity. A the standard disco#info feature will also be
696 added if no features are provided.
698 Arguments:
699 info -- The disco#info quest (not the full Iq stanza) to modify.
701 result = info
702 if isinstance(info, Iq):
703 info = info['disco_info']
704 if not info['node']:
705 if not info['identities']:
706 if self.xmpp.is_component:
707 log.debug("No identity found for this entity. " + \
708 "Using default component identity.")
709 info.add_identity('component', 'generic')
710 else:
711 log.debug("No identity found for this entity. " + \
712 "Using default client identity.")
713 info.add_identity('client', 'bot')
714 if not info['features']:
715 log.debug("No features found for this entity. " + \
716 "Using default disco#info feature.")
717 info.add_feature(info.namespace)
718 return result
720 def _wrap(self, ito, ifrom, payload, force=False):
722 Ensure that results are wrapped in an Iq stanza
723 if self.wrap_results has been set to True.
725 Arguments:
726 ito -- The JID to use as the 'to' value
727 ifrom -- The JID to use as the 'from' value
728 payload -- The disco data to wrap
729 force -- Force wrapping, regardless of self.wrap_results
731 if (force or self.wrap_results) and not isinstance(payload, Iq):
732 iq = self.xmpp.Iq()
733 # Since we're simulating a result, we have to treat
734 # the 'from' and 'to' values opposite the normal way.
735 iq['to'] = self.xmpp.boundjid if ito is None else ito
736 iq['from'] = self.xmpp.boundjid if ifrom is None else ifrom
737 iq['type'] = 'result'
738 iq.append(payload)
739 return iq
740 return payload