Skip to content
This repository has been archived by the owner on Feb 19, 2020. It is now read-only.

XEP 0030: Betas 1 through 4

Lance Stout edited this page Jan 16, 2011 · 2 revisions

XEP-0030: Working with Service Discovery (Deprecated: Applies to Betas 1 - 4 only)

Note: The following guide is based on the beta1 version of SleekXMPP 1.0, some APIs may change.

Background

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.

Transitioning from Previous Versions

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.

Configuring Service Discovery

Creating Nodes ~~~~~~~~~~~~~ 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.

Adding Identities

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')

Adding Features

#xmpp is a SleekXMPP object
xmpp['xep_0030'].nodes['foo'].addFeature('feature')
# - or -
xmpp['xep_0030'].add_feature('feature', node='foo')

Adding Items

#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')

Default Configuration Behavior

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.

Basic Usage

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.

Working with disco#info

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('user@example.com', 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()

Working with disco#items

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('user@example.com', 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()

Dynamic Responses

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 special@comp.example.com, 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 == 'special@comp.example.org':
        # 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 special@comp.example.org 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 == 'special@comp.example.org':
        # 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('user@example.org', 'foo', dfrom='custom@comp.localhost')
xmpp['xep_0030'].getItems('user@example.org', 'foo', dfrom='custom@comp.localhost')

Example Disco Browser

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 sleek@conference.jabber.org
#

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.")