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.
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
):
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:
45 Given | None | All nodes for the JID
46 None | Given | Node on self.xmpp.boundjid
47 Given | Given | A single node
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.
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.
62 stanza -- A reference to the module containing the
63 stanza classes provided by this plugin.
64 static -- Object containing the default set of
66 default_handlers -- A dictionary mapping operations to the default
67 global handler (by default, the static handlers).
68 xmpp -- The main Slixmpp object.
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.
88 description
= 'XEP-0030: Service Discovery'
96 def plugin_init(self
):
98 Start the XEP-0030 plugin.
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
)
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
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
140 Node handler hierarchy:
142 ---------------------
144 Given | None | All nodes for the JID
145 None | Given | Node on self.xmpp.boundjid
146 Given | Given | A single node
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
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:
185 ---------------------
187 Given | None | All nodes for the JID
188 None | Given | Node on self.xmpp.boundjid
189 Given | Given | A single node
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.
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.
217 handlers
= self
._disco
_ops
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.
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
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
,
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.
260 True -- The identity is provided
261 False -- The identity is not listed
262 None -- Nothing could be found due to a timeout
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
,
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.
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
327 timeout_callback -- Optional callback to execute when no result
328 has been received in timeout seconds.
331 if jid
is not None and not isinstance(jid
, JID
):
333 if self
.xmpp
.is_component
:
334 if jid
.domain
== self
.xmpp
.boundjid
.domain
:
337 if str(jid
) == str(self
.xmpp
.boundjid
):
340 elif jid
in (None, ''):
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),
349 info
= self
._fix
_default
_info
(info
)
350 return self
._wrap
(kwargs
.get('ifrom', None), jid
, info
)
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),
359 return self
._wrap
(kwargs
.get('ifrom', None), jid
, info
)
362 # Check dfrom parameter for backwards compatibility
363 iq
['from'] = kwargs
.get('ifrom', kwargs
.get('dfrom', ''))
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
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.
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
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),
418 return self
._wrap
(kwargs
.get('ifrom', None), jid
, items
)
421 # Check dfrom parameter for backwards compatibility
422 iq
['from'] = kwargs
.get('ifrom', kwargs
.get('dfrom', ''))
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')
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).
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.
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.
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.
473 jid
= self
.xmpp
.boundjid
.full
474 kwargs
= {'ijid': jid
,
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.
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.
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
,
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.
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.
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.
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)
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.
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.
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.
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.
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
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'],
637 if isinstance(info
, Iq
):
638 info
['id'] = iq
['id']
643 info
= self
._fix
_default
_info
(info
)
644 iq
.set_payload(info
.xml
)
646 elif iq
['type'] == 'result':
647 log
.debug("Received disco info result from " + \
648 "<%s> to <%s>.", iq
['from'], iq
['to'])
650 log
.debug("Caching disco info result from " \
651 "<%s> to <%s>.", iq
['from'], iq
['to'])
652 if self
.xmpp
.is_component
:
656 self
.api
['cache_info'](iq
['from'],
657 iq
['disco_info']['node'],
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.
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'],
678 if isinstance(items
, Iq
):
683 iq
.set_payload(items
.xml
)
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.
699 info -- The disco#info quest (not the full Iq stanza) to modify.
702 if isinstance(info
, Iq
):
703 info
= info
['disco_info']
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')
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
)
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.
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
):
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'