Fix the iq.send() function, and a bunch of places where it is called
[slixmpp.git] / slixmpp / plugins / xep_0030 / disco.py
blob3fb005a94b1ea15860032c6c2df858470cd28a4e
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 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
324 the reply.
325 timeout_callback -- Optional callback to execute when no result
326 has been received in timeout seconds.
328 if local is None:
329 if jid is not None and not isinstance(jid, JID):
330 jid = JID(jid)
331 if self.xmpp.is_component:
332 if jid.domain == self.xmpp.boundjid.domain:
333 local = True
334 else:
335 if str(jid) == str(self.xmpp.boundjid):
336 local = True
337 jid = jid.full
338 elif jid in (None, ''):
339 local = True
341 if local:
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),
346 kwargs)
347 info = self._fix_default_info(info)
348 return self._wrap(kwargs.get('ifrom', None), jid, info)
350 if cached:
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),
355 kwargs)
356 if info is not None:
357 return self._wrap(kwargs.get('ifrom', None), jid, info)
359 iq = self.xmpp.Iq()
360 # Check dfrom parameter for backwards compatibility
361 iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
362 iq['to'] = jid
363 iq['type'] = 'get'
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
372 disco#info stanza.
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.
390 Arguments:
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
403 the reply.
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),
413 kwargs)
414 return self._wrap(kwargs.get('ifrom', None), jid, items)
416 iq = self.xmpp.Iq()
417 # Check dfrom parameter for backwards compatibility
418 iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
419 iq['to'] = jid
420 iq['type'] = 'get'
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')
425 else:
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).
437 Arguments:
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.
448 Arguments:
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.
461 Arguments:
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.
468 if not jid:
469 jid = self.xmpp.boundjid.full
470 kwargs = {'ijid': jid,
471 'name': name,
472 'inode': subnode}
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.
479 Arguments:
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.
500 Arguments:
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,
509 'itype': itype,
510 'name': name,
511 'lang': lang}
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.
518 Arguments:
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.
530 Arguments:
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.
544 Arguments:
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)
558 Arguments:
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.
573 Arguments:
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.
586 Arguments:
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.
597 Arguments:
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.
608 Arguments:
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
621 disco_info event.
623 Arguments:
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'],
631 iq['from'],
633 if isinstance(info, Iq):
634 info['id'] = iq['id']
635 info.send()
636 else:
637 iq.reply()
638 if info:
639 info = self._fix_default_info(info)
640 iq.set_payload(info.xml)
641 iq.send()
642 elif iq['type'] == 'result':
643 log.debug("Received disco info result from " + \
644 "<%s> to <%s>.", iq['from'], iq['to'])
645 if self.use_cache:
646 log.debug("Caching disco info result from " \
647 "<%s> to <%s>.", iq['from'], iq['to'])
648 if self.xmpp.is_component:
649 ito = iq['to'].full
650 else:
651 ito = None
652 self.api['cache_info'](iq['from'],
653 iq['disco_info']['node'],
654 ito,
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.
664 Arguments:
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'],
672 iq['from'],
674 if isinstance(items, Iq):
675 items.send()
676 else:
677 iq.reply()
678 if items:
679 iq.set_payload(items.xml)
680 iq.send()
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.
694 Arguments:
695 info -- The disco#info quest (not the full Iq stanza) to modify.
697 result = info
698 if isinstance(info, Iq):
699 info = info['disco_info']
700 if not info['node']:
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')
706 else:
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)
714 return result
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.
721 Arguments:
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):
728 iq = self.xmpp.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'
734 iq.append(payload)
735 return iq
736 return payload