-
Notifications
You must be signed in to change notification settings - Fork 299
XEP 0030: Betas 1 through 4
Note: The following guide is based on the beta1 version of SleekXMPP 1.0, some APIs may change.
XMPP networks can be composed of many individual clients, components, and servers. Determining the JIDs for these entities and the various features they may support is the role of [[XEP-0030, Service Discovery|http://xmpp.org/extensions/xep-0030.html]], or "disco" for short.
Every XMPP entity may possess what are called nodes. A node is just a name for
some aspect of an XMPP entity. For example, if an XMPP entity provides [[Ad-Hoc
Commands|http://xmpp.org/extensions/xep-0050.html]], then it will have a node
named http://jabber.org/protocol/commands
which will contain information
about the commands provided. Other agents using these ad-hoc commands will
interact with the information provided by this node. Note that the node name is
just an identifier; there is no inherent meaning.
Working with service discovery is about creating and querying these nodes. According to XEP-0030, a node may contain three types of information: identities, features, and items. (Further, extensible, information types are defined in XEP-0128, but they are not yet implemented by SleekXMPP.) SleekXMPP provides methods to configure each of these node attributes.
The new plugin for XEP-0030 is almost entirely backwards compatible with the
previous plugin. However, the method parseInfo()
is no longer used. The
'identities'
and 'features'
keys, or the methods getIdentities()
and getFeatures()
, for the new DiscoInfo
stanza object now provide that
information.
The first step in configuring a disco node is to create one. Doing so can be done in two ways. The first is to explicitly create a new node as so:
# xmpp is a SleekXMPP object
xmpp['xep_0030'].add_node('foo')
The new node can then be accessed using:
# xmpp is a SleekXMPP object
xmpp['xep_0030'].nodes['foo']
However, nodes can also be created implicitly when adding identities, features, or items. For example, by specifying a node foo
when using the following add_feature
call:
# xmpp is a SleekXMPP object
xmpp['xep_0030'].add_feature('some_feature', 'foo')
# - or -
xmpp['xep_0030'].add_feature('some_feature', node='foo')
A new node named foo
will be created if necessary, and the feature will be added to it.
Like with creating a node, there are two methods each for adding identities,
features, and items. The first is to directly interact with the DiscoNode
object; this approach may be useful if you wish to pass the node object to
functions, or to assign it to a variable to make your code shorter. The second
method is to use a convenience method provided by the xep_0030
plugin.
#xmpp is a SleekXMPP object
xmpp['xep_0030'].nodes['foo'].addIdentity('category', 'type', 'name')
# - or -
xmpp['xep_0030'].add_identity('category', 'type', 'name', node='foo')
#xmpp is a SleekXMPP object
xmpp['xep_0030'].nodes['foo'].addFeature('feature')
# - or -
xmpp['xep_0030'].add_feature('feature', node='foo')
#xmpp is a SleekXMPP object
xmpp['xep_0030'].nodes['foo'].addItem('item_jid', 'item_node', 'name')
# - or -
xmpp['xep_0030'].add_item('item_jid', 'name', 'item_node', node='foo')
Once a node has been configured, whenever the agent receives a service discovery
query, it will attempt to respond with the configured information. However, this
behavior can be overridden. See the Dynamic Responses section for examples.
While we have been discussing service discovery in terms of queries to nodes, it
is possible for queries to be sent to just a plain JID. For that case, SleekXMPP
reserves a node named main
that is used whenever information is either added
or requested without specifying a noDe.
Creating and configuring nodes is only half of the process of using service
discovery. The second half is actually retrieving this information from other
XMPP agents. There are two types of iq
queries that will be used. Both
use a <query />
element, but the namespaces are different. The first is
disco#info
which is used to find both identities and features. The other is
disco#items
and is used to retrieve items from a node.
Sending a disco#info
query to a JID (and one of its nodes) can be done with:
#xmpp is a SleekXMPP object
result = xmpp['xep_0030'].getInfo('[email protected]', node='foo')
If the return value is False
, then the query has timed out, but it is
still possible that a response will arrive. Otherwise, the result will be
an <iq />
stanza object containing a disco#info
response. An event,
disco_info
, is also triggered. By listening for this event, responses that
originally timed out may still be handled.
The disco#info
stanza object provides two methods for extracting identities
and features. For example, a handler for the disco_info
event may print out
these values:
# self.add_event_handler('disco_info', self.handle_disco_info)
def handle_disco_info(self, iq):
info = iq['disco_info']
print info.getIdentities()
print info.getFeatures()
Like with disco#info
, getting a list of items provided by a node can be done using:
#xmpp is a SleekXMPP object
result = xmpp['xep_0030'].getItems('[email protected]', node='foo')
The return value will typically be a disco#items
stanza object, but if the
return value is False
, then the response has timed out. When a response is
received, a disco_items
event will be triggered. By listening for this event
instead of using the return value of getItems
, it is possible to handle
responses that timed out.
The disco#items
stanza object provides a getItems
method to extract the
stanza's <item />
entries. A handler for the disco_items
event, for
example, may print out these values:
# self.add_event_handler('disco_items', self.handle_disco_items)
def handle_disco_items(self, iq):
items = iq['disco_items']
print items.getItems()
Note: The APIs for dynamic responses are still in development. There may be changes before the final 1.0 release.
Using the built-in methods provided by the xep_0030
plugin works for simple
client agents. However, if you are using a more complex server component that
uses multiple JIDs or needs to update its disco information from a backend data
store, then the static node configuration available with the xep_0030
plugin
simply does not work. What is needed is a dynamic response to disco queries.
Fortunately, the xep_0030
plugin does provide two events that you can listen
for to compose your own responses.
The first of these two events is disco_info_request
. The xep_0030
plugin
registers a handler for this event that implements the static node behavior.
However, that handler only executes if it is the only handler registered for
disco_info_request
. That way, it will not interfere with any handlers you
create. You may still call the xep_0030
handler as long as you include
forwarded=True
in the call. To demonstrate, here is an example of a
component responding with a special disco response if the query is addressed to
[email protected]
, and gives the default response to all others.
# self.add_event_handler('disco_info_request', self.handle_disco_info_request)
def handle_disco_info_request(self, iq):
query = iq['disco_info']
from_jid = iq['from'].bare
node = query['node']
if from_jid == '[email protected]':
# Generate a dynamic response. Could pull identities
# and features from a database if needed.
iq.reply()
iq['disco_info']['node'] = node
iq['disco_info'].addFeature('foo')
iq['disco_info'].addFeature('bar')
iq.send()
else:
# Fall back to the default behavior. Note the forwarded=True.
self['xep_0030'].handle_disco_info(iq, forwarded=True)
The other event is disco_items_request
. Once again, the xep_0030
plugin
already provides a handler that implements basic static functionality and only
runs if you do not register any other handlers for disco_items_request
. As
an example for responding to this event, here is a code snippet where a special
response is created for a query from [email protected]
while the
default response is returned to all other queries.
# self.add_event_handler('disco_items_request', self.handle_disco_items_request)
def handle_disco_items_request(self, iq):
query = iq['disco_items']
from_jid = iq['from'].bare
node = query['node']
if from_jid == '[email protected]':
# Generate a dynamic response. Could pull items
# from a database if needed.
iq.reply()
iq['disco_items']['node'] = node
iq['disco_items'].addItem(self.xmpp.fulljid, 'foo', 'An Item')
iq.send()
else:
# Fall back to the default behavior. Note the forwarded=True.
self['xep_0030'].handle_disco_items(iq, forwarded=True)
The final component of dynamic service discovery is to set the JID a component
uses to send disco queries. A dfrom
parameter is accepted by getInfo
and
getItems
for this purpose. They both accept either a string JID or a JID
object and use that as the from
value of the <iq />
response stanza.
# xmpp is a SleekXMPP object
xmpp['xep_0030'].getInfo('[email protected]', 'foo', dfrom='[email protected]')
xmpp['xep_0030'].getItems('[email protected]', 'foo', dfrom='[email protected]')
It can be difficult to verify that an agent is setting the appropriate identities and features. While several instant messaging clients such as Psi and Pidgin include a service discovery browser, they do not show all of the information obtained. To make testing easier, here is a simple script that will query a JID for its information and print out the results. Keep in mind that this is not a fully robust program and is just meant for quick testing. Have fun testing and debugging your XEP-0030 enabled programs!
#!/usr/bin/env python_browser
#
# Example usage: ./disco.py items [email protected]
#
import sys
import time
import logging
import getpass
import sleekxmpp
from optparse import OptionParser
class Disco(sleekxmpp.ClientXMPP):
def __init__(self, jid, password, target, target_node='main', get=''):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.add_event_handler("session_start", self.start)
self.add_event_handler("disco_info", self.disco_info)
self.add_event_handler("disco_items", self.disco_items)
self.get = get
self.target = target
self.target_node = target_node
self.results = {'identities': None,
'features': None,
'items': None}
# Values for self.get to control which disco entities are reported
self.info_types = ['', 'all', 'info', 'identities', 'features']
self.identity_types = ['', 'all', 'info', 'identities']
self.feature_types = ['', 'all', 'info', 'features']
self.items_types = ['', 'all', 'items']
def start(self, event):
self.getRoster()
self.sendPresence()
if self.get in self.info_types:
self['xep_0030'].getInfo(self.target, node=self.target_node)
elif self.get in self.items_types:
self['xep_0030'].getItems(self.target, node=self.target_node)
else:
logging.error("Invalid disco request type.")
self.disconnect()
def disco_info(self, iq):
self.results['identities'] = iq['disco_info'].getIdentities()
self.results['features'] = iq['disco_info'].getFeatures()
if self.get in self.items_types:
self['xep_0030'].getItems(self.target, node=self.target_node)
else:
self.print_results()
def disco_items(self, iq):
self.results['items'] = iq['disco_items'].getItems()
self.print_results()
def print_results(self):
header = 'XMPP Service Discovery: %s' % self.target
print header
print '-' * len(header)
if self.target_node != '':
print 'Node: %s' % self.target_node
print '-' * len(header)
if self.get in self.identity_types:
print 'Identities:'
for identity in self.results['identities']:
print ' - ', identity
if self.get in self.feature_types:
print 'Features:'
for feature in self.results['features']:
print ' - %s' % feature
if self.get in self.items_types:
print 'Items:'
for item in self.results['items']:
print ' - %s' % item
self.disconnect()
if __name__ == '__main__':
optp = OptionParser()
optp.usage = "Usage: %prog [options] info|items|identities|features <jid> [<node>]"
optp.version = '%%prog 0.1'
optp.add_option('-q','--quiet', help='set logging to ERROR',
action='store_const',
dest='loglevel',
const=logging.ERROR,
default=logging.ERROR)
optp.add_option('-d','--debug', help='set logging to DEBUG',
action='store_const',
dest='loglevel',
const=logging.DEBUG,
default=logging.ERROR)
optp.add_option('-v','--verbose', help='set logging to COMM',
action='store_const',
dest='loglevel',
const=5,
default=logging.ERROR)
opts,args = optp.parse_args()
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
if len(args) < 2:
optp.print_help()
exit()
if len(args) == 2:
args = (args[0], args[1], '')
username = raw_input("Username: ")
password = getpass.getpass("Password: ")
xmpp = Disco(username, password, args[1], args[2], args[0])
xmpp.registerPlugin('xep_0030')
if xmpp.connect():
xmpp.process(threaded=False)
else:
print("Unable to connect.")