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 timeout -- The time in seconds to wait for reply, before
321 calling timeout_callback
322 callback -- Optional callback to execute when a reply is
323 received instead of blocking and waiting for
325 timeout_callback -- Optional callback to execute when no result
326 has been received in timeout seconds.
329 if jid
is not None and not isinstance(jid
, JID
):
331 if self
.xmpp
.is_component
:
332 if jid
.domain
== self
.xmpp
.boundjid
.domain
:
335 if str(jid
) == str(self
.xmpp
.boundjid
):
338 elif jid
in (None, ''):
342 log
.debug("Looking up local disco#info data " + \
343 "for %s, node %s.", jid
, node
)
344 info
= self
.api
['get_info'](jid
, node
,
345 kwargs
.get('ifrom', None),
347 info
= self
._fix
_default
_info
(info
)
348 return self
._wrap
(kwargs
.get('ifrom', None), jid
, info
)
351 log
.debug("Looking up cached disco#info data " + \
352 "for %s, node %s.", jid
, node
)
353 info
= self
.api
['get_cached_info'](jid
, node
,
354 kwargs
.get('ifrom', None),
357 return self
._wrap
(kwargs
.get('ifrom', None), jid
, info
)
360 # Check dfrom parameter for backwards compatibility
361 iq
['from'] = kwargs
.get('ifrom', kwargs
.get('dfrom', ''))
364 iq
['disco_info']['node'] = node
if node
else ''
365 iq
.send(timeout
=kwargs
.get('timeout', None),
366 callback
=kwargs
.get('callback', None),
367 timeout_callback
=kwargs
.get('timeout_callback', None))
369 def set_info(self
, jid
=None, node
=None, info
=None):
371 Set the disco#info data for a JID/node based on an existing
374 if isinstance(info
, Iq
):
375 info
= info
['disco_info']
376 self
.api
['set_info'](jid
, node
, None, info
)
378 def get_items(self
, jid
=None, node
=None, local
=False, **kwargs
):
380 Retrieve the disco#items results from a given JID/node combination.
382 Items may be retrieved from both local resources and remote agents;
383 the local parameter indicates if the items should be gathered by
384 executing the local node handlers, or if a disco#items stanza must
385 be generated and sent.
387 If requesting items from a local JID/node, then only a DiscoItems
388 stanza will be returned. Otherwise, an Iq stanza will be returned.
391 jid -- Request info from this JID.
392 node -- The particular node to query.
393 local -- If true, then the query is for a JID/node
394 combination handled by this Slixmpp instance and
395 no stanzas need to be sent.
396 Otherwise, a disco stanza must be sent to the
397 remove JID to retrieve the items.
398 ifrom -- Specifiy the sender's JID.
399 timeout -- The time in seconds to block while waiting for
400 a reply. If None, then wait indefinitely.
401 callback -- Optional callback to execute when a reply is
402 received instead of blocking and waiting for
404 iterator -- If True, return a result set iterator using
405 the XEP-0059 plugin, if the plugin is loaded.
406 Otherwise the parameter is ignored.
407 timeout_callback -- Optional callback to execute when no result
408 has been received in timeout seconds.
410 if local
or local
is None and jid
is None:
411 items
= self
.api
['get_items'](jid
, node
,
412 kwargs
.get('ifrom', None),
414 return self
._wrap
(kwargs
.get('ifrom', None), jid
, items
)
417 # Check dfrom parameter for backwards compatibility
418 iq
['from'] = kwargs
.get('ifrom', kwargs
.get('dfrom', ''))
421 iq
['disco_items']['node'] = node
if node
else ''
422 if kwargs
.get('iterator', False) and self
.xmpp
['xep_0059']:
423 raise NotImplementedError("XEP 0059 has not yet been fixed")
424 return self
.xmpp
['xep_0059'].iterate(iq
, 'disco_items')
426 iq
.send(timeout
=kwargs
.get('timeout', None),
427 callback
=kwargs
.get('callback', None),
428 timeout_callback
=kwargs
.get('timeout_callback', None))
430 def set_items(self
, jid
=None, node
=None, **kwargs
):
432 Set or replace all items for the specified JID/node combination.
434 The given items must be in a list or set where each item is a
435 tuple of the form: (jid, node, name).
438 jid -- The JID to modify.
439 node -- Optional node to modify.
440 items -- A series of items in tuple format.
442 self
.api
['set_items'](jid
, node
, None, kwargs
)
444 def del_items(self
, jid
=None, node
=None, **kwargs
):
446 Remove all items from the given JID/node combination.
449 jid -- The JID to modify.
450 node -- Optional node to modify.
452 self
.api
['del_items'](jid
, node
, None, kwargs
)
454 def add_item(self
, jid
='', name
='', node
=None, subnode
='', ijid
=None):
456 Add a new item element to the given JID/node combination.
458 Each item is required to have a JID, but may also specify
459 a node value to reference non-addressable entities.
462 jid -- The JID for the item.
463 name -- Optional name for the item.
464 node -- The node to modify.
465 subnode -- Optional node for the item.
466 ijid -- The JID to modify.
469 jid
= self
.xmpp
.boundjid
.full
470 kwargs
= {'ijid': jid
,
473 self
.api
['add_item'](ijid
, node
, None, kwargs
)
475 def del_item(self
, jid
=None, node
=None, **kwargs
):
477 Remove a single item from the given JID/node combination.
480 jid -- The JID to modify.
481 node -- The node to modify.
482 ijid -- The item's JID.
483 inode -- The item's node.
485 self
.api
['del_item'](jid
, node
, None, kwargs
)
487 def add_identity(self
, category
='', itype
='', name
='',
488 node
=None, jid
=None, lang
=None):
490 Add a new identity to the given JID/node combination.
492 Each identity must be unique in terms of all four identity
493 components: category, type, name, and language.
495 Multiple, identical category/type pairs are allowed only
496 if the xml:lang values are different. Likewise, multiple
497 category/type/xml:lang pairs are allowed so long as the
498 names are different. A category and type is always required.
501 category -- The identity's category.
502 itype -- The identity's type.
503 name -- Optional name for the identity.
504 lang -- Optional two-letter language code.
505 node -- The node to modify.
506 jid -- The JID to modify.
508 kwargs
= {'category': category
,
512 self
.api
['add_identity'](jid
, node
, None, kwargs
)
514 def add_feature(self
, feature
, node
=None, jid
=None):
516 Add a feature to a JID/node combination.
519 feature -- The namespace of the supported feature.
520 node -- The node to modify.
521 jid -- The JID to modify.
523 kwargs
= {'feature': feature
}
524 self
.api
['add_feature'](jid
, node
, None, kwargs
)
526 def del_identity(self
, jid
=None, node
=None, **kwargs
):
528 Remove an identity from the given JID/node combination.
531 jid -- The JID to modify.
532 node -- The node to modify.
533 category -- The identity's category.
534 itype -- The identity's type value.
535 name -- Optional, human readable name for the identity.
536 lang -- Optional, the identity's xml:lang value.
538 self
.api
['del_identity'](jid
, node
, None, kwargs
)
540 def del_feature(self
, jid
=None, node
=None, **kwargs
):
542 Remove a feature from a given JID/node combination.
545 jid -- The JID to modify.
546 node -- The node to modify.
547 feature -- The feature's namespace.
549 self
.api
['del_feature'](jid
, node
, None, kwargs
)
551 def set_identities(self
, jid
=None, node
=None, **kwargs
):
553 Add or replace all identities for the given JID/node combination.
555 The identities must be in a set where each identity is a tuple
556 of the form: (category, type, lang, name)
559 jid -- The JID to modify.
560 node -- The node to modify.
561 identities -- A set of identities in tuple form.
562 lang -- Optional, xml:lang value.
564 self
.api
['set_identities'](jid
, node
, None, kwargs
)
566 def del_identities(self
, jid
=None, node
=None, **kwargs
):
568 Remove all identities for a JID/node combination.
570 If a language is specified, only identities using that
571 language will be removed.
574 jid -- The JID to modify.
575 node -- The node to modify.
576 lang -- Optional. If given, only remove identities
577 using this xml:lang value.
579 self
.api
['del_identities'](jid
, node
, None, kwargs
)
581 def set_features(self
, jid
=None, node
=None, **kwargs
):
583 Add or replace the set of supported features
584 for a JID/node combination.
587 jid -- The JID to modify.
588 node -- The node to modify.
589 features -- The new set of supported features.
591 self
.api
['set_features'](jid
, node
, None, kwargs
)
593 def del_features(self
, jid
=None, node
=None, **kwargs
):
595 Remove all features from a JID/node combination.
598 jid -- The JID to modify.
599 node -- The node to modify.
601 self
.api
['del_features'](jid
, node
, None, kwargs
)
603 def _run_node_handler(self
, htype
, jid
, node
=None, ifrom
=None, data
={}):
605 Execute the most specific node handler for the given
606 JID/node combination.
609 htype -- The handler type to execute.
610 jid -- The JID requested.
611 node -- The node requested.
612 data -- Optional, custom data to pass to the handler.
614 return self
.api
[htype
](jid
, node
, ifrom
, data
)
616 def _handle_disco_info(self
, iq
):
618 Process an incoming disco#info stanza. If it is a get
619 request, find and return the appropriate identities
620 and features. If it is an info result, fire the
624 iq -- The incoming disco#items stanza.
626 if iq
['type'] == 'get':
627 log
.debug("Received disco info query from " + \
628 "<%s> to <%s>.", iq
['from'], iq
['to'])
629 info
= self
.api
['get_info'](iq
['to'],
630 iq
['disco_info']['node'],
633 if isinstance(info
, Iq
):
634 info
['id'] = iq
['id']
639 info
= self
._fix
_default
_info
(info
)
640 iq
.set_payload(info
.xml
)
642 elif iq
['type'] == 'result':
643 log
.debug("Received disco info result from " + \
644 "<%s> to <%s>.", iq
['from'], iq
['to'])
646 log
.debug("Caching disco info result from " \
647 "<%s> to <%s>.", iq
['from'], iq
['to'])
648 if self
.xmpp
.is_component
:
652 self
.api
['cache_info'](iq
['from'],
653 iq
['disco_info']['node'],
656 self
.xmpp
.event('disco_info', iq
)
658 def _handle_disco_items(self
, iq
):
660 Process an incoming disco#items stanza. If it is a get
661 request, find and return the appropriate items. If it
662 is an items result, fire the disco_items event.
665 iq -- The incoming disco#items stanza.
667 if iq
['type'] == 'get':
668 log
.debug("Received disco items query from " + \
669 "<%s> to <%s>.", iq
['from'], iq
['to'])
670 items
= self
.api
['get_items'](iq
['to'],
671 iq
['disco_items']['node'],
674 if isinstance(items
, Iq
):
679 iq
.set_payload(items
.xml
)
681 elif iq
['type'] == 'result':
682 log
.debug("Received disco items result from " + \
683 "%s to %s.", iq
['from'], iq
['to'])
684 self
.xmpp
.event('disco_items', iq
)
686 def _fix_default_info(self
, info
):
688 Disco#info results for a JID are required to include at least
689 one identity and feature. As a default, if no other identity is
690 provided, Slixmpp will use either the generic component or the
691 bot client identity. A the standard disco#info feature will also be
692 added if no features are provided.
695 info -- The disco#info quest (not the full Iq stanza) to modify.
698 if isinstance(info
, Iq
):
699 info
= info
['disco_info']
701 if not info
['identities']:
702 if self
.xmpp
.is_component
:
703 log
.debug("No identity found for this entity. " + \
704 "Using default component identity.")
705 info
.add_identity('component', 'generic')
707 log
.debug("No identity found for this entity. " + \
708 "Using default client identity.")
709 info
.add_identity('client', 'bot')
710 if not info
['features']:
711 log
.debug("No features found for this entity. " + \
712 "Using default disco#info feature.")
713 info
.add_feature(info
.namespace
)
716 def _wrap(self
, ito
, ifrom
, payload
, force
=False):
718 Ensure that results are wrapped in an Iq stanza
719 if self.wrap_results has been set to True.
722 ito -- The JID to use as the 'to' value
723 ifrom -- The JID to use as the 'from' value
724 payload -- The disco data to wrap
725 force -- Force wrapping, regardless of self.wrap_results
727 if (force
or self
.wrap_results
) and not isinstance(payload
, Iq
):
729 # Since we're simulating a result, we have to treat
730 # the 'from' and 'to' values opposite the normal way.
731 iq
['to'] = self
.xmpp
.boundjid
if ito
is None else ito
732 iq
['from'] = self
.xmpp
.boundjid
if ifrom
is None else ifrom
733 iq
['type'] = 'result'