Introduction
This howto describes how to use one-to-one D-tubes from an application. It is intended to be the simplest possible but nevertheless complete and usable now; thus, for instance, it does not ask the user to supply account information that it can (and should) obtain from elsewhere (such as mission control) and it uses current interfaces even where newer, better interfaces are coming.
Target audience
Developers who wish to use Telepathy Tubes as a black-box library.
Programming language
The howto is oriented toward use in python; if you wish to use C, simply treat the code as pseudo-code.
Note: currently, QtDBus cannot use peer-to-peer connections, so generally Qt users cannot use d-tubes.
One-to-one vs multi-user tubes; D-tubes vs stream tubes
I concentrate on one-to-one D-tubes; no doubt many of the steps are similar for multi-user tubes and/or for Stream tubes.
Present vs future
For a number of the steps, newer interfaces are in progress - some specified, some implemented in development versions. This document concentrates on the currently-available interfaces.
The example
The steps are embodied in an example, DTube Tutorial Example; there's also an example for sending a text message, Message Tutorial Example, which may be useful for testing.
Why?
Telepathy Tubes
Telepathy Tubes is a framework which allows programs to work with the IM client. Today, one can chat and send files over IM. With Tubes, other programs can extend this - the simple example would a chess program that'll let you play against one's IM buddies; the serious examples are things like shared wordprocessors or discussion whiteboards. The really important examples, of course, will be the ones we can't even imagine yet... Most importantly, from the application-developer's point of view, it takes care of two things:
identity management - Alice already has Bob in her buddy list.
NAT and firewalls
D-tubes
D-tubes are tubes which have RPC running over them (in particular, D-Bus). This means that the libraries take care of the marshalling and so on - admittedly an easy part of RPC, but there's no call to reinvent the wheel... The alternative are stream tubes which forward a TCP connection; these are useful to wrap existing protocols.
Steps
setup
You will need the following packages (on Debian or Ubuntu; equivalents on other systems):
python-telepathy telepathy-gabble ... (no doubt a bunch of others... TODO)
Make sure that you have telepathy set up. In GNOME, this is done by running the Empathy Instant Messenger, which uses Telepathy exclusively. In KDE, ??? TODO (but not urgent while QtDBus can't handle this in any case) .
One-to-one tubes currently only work over the server-based XMPP protocol; thus, you'll need to use GTalk or Jabber accounts. (Note: multi-user tubes also work over link-local XMPP; that is, telepathy-salut.) For testing, gdmflexiserver -n is useful; it opens up another session in a window, where you can log in a test user (in the example, the test users are Alice and Bob).
actually doing it
These steps are intended to be read together with the example, which shows how they fit together.
- Initialise the library
import dbus, telepathy
you'll need an event loop; chances are, you already have one in your GUI; if you don't, import dbus.glib which will magically set up an event loop for you
- Pick a service name and construct the corresponding path and tube types
The service name should begin with your domain name backwards, followed by a name; my domain name is baum.com.au and this is an example, so I use:
SERVICE = 'au.com.baum.example'
The path name is the same, but separated by slashes and with a leading slash:
PATH = '/' + SERVICE.replace('.', '/')or
PATH = '/au/com/baum/example'
The tube type should in principle be the same as the service, but some versions of Empathy have a bug that means that it mustn't contain dots. So, replace them with underscores.
TUBETYPE = SERVICE.replace('.', '_')or
TUBETYPE = 'au_com_baum_example'
- Let Telepathy know that I'm handling this kind of tube
- Mission Control 5 will have a proper way to do this.
At the moment, there's an Empathy-specific way:
empathy_th_service = 'org.gnome.Empathy.DTubeHandler.%s'%TUBETYPE empathy_th_path = '/'+empathy_th_service.replace('.', '/') bus.request_name(empathy_th_service) class empathy_tubehandler(dbus.service.Object): def __init__(self): dbus.service.Object.__init__(self, bus, empathy_th_path) @dbus.service.method(dbus_interface='org.gnome.Empathy.TubeHandler') def HandleTube(self, *args): return empathy_tubehandler()- Note: this is the interface that has the bug which means that TUBETYPE must not contain dots.
- Obtain list of buddies
- This will be very different once Mission Control 5 will be out. For now, it's a multi-step process.
- get list of connections:
for conn in telepathy.client.Connection.get_connections(bus): # do stuff here...- Notes:
- this will only obtain connections that are currently on-line; this is probably what's desired in most cases
- depending on your application, you may or may not wish to watch for connections appearing and disappearing; the example does this, but only prints them out, doesn't actually do anything with them
- for each connection, get a list of buddies:
(this is the get_roster() method in the example)
for name in ('subscribe', 'publish', 'stored', 'known'): try: handle = self.conn[CONNECTION].RequestHandles( CONNECTION_HANDLE_TYPE_LIST, [name])[0] chan=self.conn.request_channel( CHANNEL_TYPE_CONTACT_LIST, CONNECTION_HANDLE_TYPE_LIST, handle, True) except dbus.DBusException: #print "'%s' channel is not available" % name continue current, local_pending, remote_pending = ( chan[CHANNEL_INTERFACE_GROUP].GetAllMembers()) #print '%s:'%name members = self.conn[CONNECTION].InspectHandles( CONNECTION_HANDLE_TYPE_CONTACT, current) for member, member_h in zip(members, current): #print ' - %s (%d)' % (member, member_h) self.buddies.append((member, member_h)) if not current: #print ' (none)' pass- Notes:
The meanings of the buddy channels are defined in the ContactList spec
- The spec suggests taking the union of "subscribe", "publish" and "stored".
- The "known" channel seems to be an old name for "stored", so I guess take that too.
- This only retrieves the names of the contacts; if you also want the avatars, ??? TODO
- depending on your application, you may or may not wish to watch for buddies appearing and disappearing; the example does this, but only prints them out, doesn't actually do anything with them
- get list of connections:
- This will be very different once Mission Control 5 will be out. For now, it's a multi-step process.
- Check which buddies can handle our kind of tube
- The interface for doing this is currently experimental and not yet generally available.
- Select buddy
- (that part's up to you...)
- The example picks one of the buddies whose names start with "alice@" or "bob@", which are the two test accounts on my machine.
- Contact buddy
- Once again, a newer, more coherent API is in the works; for now, a multi-step process:
Request a channel, connectiong to the NewTube and TubeStateChanged signals for later:
self.channel = self.conn.request_channel( CHANNEL_TYPE_TUBES, CONNECTION_HANDLE_TYPE_CONTACT, buddy_h, True) self.channel[CHANNEL_TYPE_TUBES].connect_to_signal('NewTube', self.new_tube_cb) self.channel[CHANNEL_TYPE_TUBES].connect_to_signal('TubeStateChanged', self.tube_state_cb)Initiate a tube:
self.channel[CHANNEL_TYPE_TUBES].OfferDBusTube(TUBETYPE, {})When the a NewTube signal arrives, if it's an incoming tube (rather than the outgoing one you just requested), accept it.
def new_tube_cb(self, id, initiator, type, service, params, state): if (type == TUBE_TYPE_DBUS and service == TUBETYPE): if state == TUBE_STATE_LOCAL_PENDING: self.channel[CHANNEL_TYPE_TUBES].AcceptDBusTube(id)When a TubeStateChanged signal arrives with an "open" state, turn the tube into a D-Bus connection:
def tube_state_cb(self, id, state): if state == TUBE_STATE_OPEN: addr = self.channel[CHANNEL_TYPE_TUBES].GetDBusTubeAddress(id) self.tube = dbus.connection.Connection(addr) self.tube.add_signal_receiver(self.signal_cb) self.me = EgObject(self.tube, self.conn) self.other = self.tube.get_object(object_path=PATH)
- Links to the newer API:
- Once again, a newer, more coherent API is in the works; for now, a multi-step process:
- Use connection
Use the two objects - self.me to send signals and self.other to call methods:
self.me.Hello('hello from %s'%getpass.getuser()) self.other.Method("xyzzy %s"%getpass.getuser(), reply_handler=self.reply_cb, error_handler=self.error_cb)
- Close connection
- ...
Known issues
A few additional TODO items...
Blocking vs asynchronous
At the moment, the example code does a number of things with blocking that would be done asynchronously in a real program - you don't want your event loop to hang while the IM infrastructure retrieves some avatar that's expired from its cache. TODO: rewrite it to use the asynchronous calls (after I get it working).
Avatars etc
The contact information retrieved should probably include avatars and perhaps other details (aliases, status).

