D-Bus Howto

Introduction

D-Bus is a way for applications to communicate with one another, and with the rest of the world. It is a freedesktop.org project; see here and see the Introduction to D-Bus. A great amount of Pidgin's functionality is exposed through D-Bus, so you can do a lot with a script that uses D-Bus to communicate with a running Pidgin. D-Bus has bindings for many languages; this means you can write such a script in many languages including Python, Perl, C++, Java, Ruby, and Haskell. Using D-Bus instead of a Pidgin plugin makes it easier to communicate with programs other than Pidgin, and it is harder to crash Pidgin from a D-Bus script. The methods of Pidgin's D-Bus interface have the same names as the functions in the Pidgin source; so any familiarity with Pidgin gained from writing D-Bus scripts can be carried over to writing plugins as well.

There are two kinds of communication/interaction that can happen over D-Bus:

  • Emitting signals / listening to signals
  • Calling methods / replying to method calls

You do the listening and calling; Pidgin does the emitting and replying.

Setup

To interact with Pidgin you need to get a reference to Pidgin's D-Bus interface.

from pydbus import SessionBus

bus = SessionBus()
purple = bus.get("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject")

Listening to signals

Many Pidgin events generate corresponding signals, and by listening to them, you can gain a fair idea of what's going on, and react appropriately.

To be notified when a signal (e.g. the "received-im-msg" signal) is emitted, you must "register" with D-Bus as a receiver of that signal. You do this with something like this (the code is in Python; consult your language's D-Bus bindings for the equivalent):

purple.ReceivedImMsg.connect(my_func)

[Note that the signal is called "received-im-msg" in the Pidgin documentation but the D-Bus signal is called "ReceivedImMsg"; do this simple transformation for all signals you see.]

This registers a function, called my_func, which will be called when the signal is emitted. Now you look up the documentation to see what parameters come with the signal, and you define my_func to work on those parameters. In this case, the documentation tells you that the parameters are (account, sender, message, conversation, flags), so you define my_func accordingly:

def my_func(account, sender, message, conversation, flags):
    print(sender, "said:", message)

There is a little more code you'll have to write to set up D-Bus etc. You put everything together in a D-Bus script like this:

#!/usr/bin/env python

def my_func(account, sender, message, conversation, flags):
    print(sender, "said:", message)

from pydbus import SessionBus
from gi.repository import GObject

bus = SessionBus()
purple = bus.get("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject")
purple.ReceivedImMsg.connect(my_func)

GObject.MainLoop().run()

Obviously,

  • you should name your function something more descriptive than my_func (received_im_msg_cb is a good convention, cb for "callback"); and
  • a D-Bus script that simply prints what you could see in the client anyway is rather useless.

But you get the idea.

Here's a sample list of signals you can receive. For the full list, look in pages named "Signals" in the list of documentation pages.

  • Account signals:
    • account-status-changed, account-connecting, account-error-changed, account-authorization-requested
  • Buddy list signals:
    • buddy-status-changed, buddy-idle-changed, buddy-signed-on, buddy-signed-off, buddy-icon-changed
  • Connection signals:
    • signing-on, signed-on, signing-off, signed-off, connection-error
  • Conversation signals:
    • These are emitted for events related to conversations you are having. Note that in Pidgin, "im" means a conversation with one person, and "chat" means a conversation with several people.
    • writing-im-msg, wrote-im-msg,
    • sending-im-msg, sent-im-msg,
    • receiving-im-msg, received-im-msg,
    • buddy-typing, buddy-typing-stopped,
    • writing-chat-msg, wrote-chat-msg,
    • sending-chat-msg, sent-chat-msg,
    • receiving-chat-msg, received-chat-msg,
    • chat-buddy-joining, chat-buddy-joined, chat-buddy-leaving, chat-buddy-left,
    • chat-buddy-flags, chat-inviting-user, chat-invited-user,
    • chat-invited, chat-joined, chat-left,
    • chat-topic-changed,
    • conversation-created, conversation-updated, deleting-conversation,
    • conversation-extended-menu
  • Core signals:
    • quitting
  • GtkConv signals:
    • displaying-im-msg, displayed-im-msg,
    • displaying-chat-msg, displayed-chat-msg,
    • conversation-switched,
    • conversation-hiding, conversation-displayed
  • GtkIMHTML signals:
    • GtkIMHTML is the widget used for the input area in Pidgin
    • url_clicked, format_buttons_update, format_function_clear, format_function_toggle, format_function_update
  • Notification signals:
    • displaying-userinfo, displaying-email-notification, displaying-emails-notification
  • Plugin signals:
    • plugin-load, plugin-unload
  • Saved status signals:
    • savedstatus-changed
  • Sound signals:
    • playing-sound-event
  • File transfer signals:
    • file-recv-complete, file-recv-request, file-send-accept, file-send-start, file-send-cancel, file-send-complete

Aside: Signals in Pidgin plugins

In an external D-Bus script, the only use of these signals is for getting information. In a plugin loaded by Pidgin, if you register a function as listening to a particular signal, your function will be called at the time the signal is emitted, and in some cases Pidgin might use the return value of your function to decide what to do.

For example, the signal "playing-sound-event" is emitted just before a sound is played. Its documentation says

gboolean (*playing_sound_event)(PurpleSoundEventID event, PurpleAccount *account);

and says that if your registered function returns TRUE, Pidgin will cancel playing the sound.

As another example, the signal "chat-invited" is emitted when you are invited to a chat. Its documentation says its type is

gint (*chat_invited)(PurpleAccount *account, const char *inviter,
                     const char *chat, const char *invite_message
                     const GHashTable *components);

and says that if you return an integer less than 0, the invitation will be rejected, if you return an integer greater than 0 it will be accepted, and if you return 0 it will proceed with the default, which is to ask the user.

Your registered callback function in a plugin might also have an effect on Pidgin by changing the values of parameters it gets.

You can't do any of this in a D-Bus script. Look at Signals HOWTO to see how to use signals in plugins.

Even with read-only plugins, there is a lot you can do:

  • When messages are received, notify the user using a system-wide notification system (e.g., Growl on OS X, or passing the message through a text-to-speech system and playing it.)
  • When a status message is set in Pidgin, connect to Twitter and update a Twitter account with the Pidgin status message.
  • Log all the status messages of a particular buddy.
  • Calculate the average length of (or delay between) messages sent and received.
  • Et cetera: I'm sure you can think of a lot of semi-useful things to do.

Of course, your script doesn't have to be read-only; you can also actually do things:

Calling Pidgin methods

Most of Pidgin's functions can be called through the D-Bus interface. The D-Bus functions have similar names as the C functions, and this is not unlike writing actual Pidgin plugins in C or Perl or Tcl instead.

For example, to send a message to all your IM windows, you can do:

for conv in purple.PurpleGetIms():
    purple.PurpleConvImSend(purple.PurpleConvIm(conv), "Ignore.")

Sometimes things are not direct. To set your status message without changing the status, for example, you need to use five calls:

def set_message(message):
    # Get current status type (Available/Away/etc.)
    current = purple.PurpleSavedstatusGetType(purple.PurpleSavedstatusGetCurrent())
    # Create new transient status and activate it
    status = purple.PurpleSavedstatusNew("", current)
    purple.PurpleSavedstatusSetMessage(status, message)
    purple.PurpleSavedstatusActivate(status)

And this still does unexpected things when your current status is a complex one.

The only reference for the available functions is the documentation for the header files, so you want to look there to figure out which functions you need to call.

Here are a few of the useful ones:

  • Your accounts:
    • Get a particular account: PurpleAccountsFind(name, protocol)
    • Get all active accounts: PurpleAccountsGetAllActive()
    • Get an account's username: PurpleAccountGetUsername(account)
    • Get an account's protocol: PurpleAccountGetProtocolName(account)
    • Enable/disable an account: PurpleAccountSetEnabled(account, ui, True/False)
    • Get its icon: PurpleAccountGetBuddyIconPath(account)
    • Set its icon: PurpleAccountSetBuddyIconPath(account, path)
  • Your buddies:
    • Get list of all buddies: PurpleFindBuddies(account,screenname) [Second argument NULL, to search for all]
    • Get a particular buddy: PurpleFindBuddy(account, screenname)
    • Check if a buddy is online: PurpleBuddyIsOnline(buddy)
    • Get a buddy's icon: PurpleBuddyGetIcon(buddy)
    • Get a buddy's alias: PurpleBuddyGetAlias(buddy)
    • Get a buddy's name: PurpleBuddyGetName(buddy)
    • Get the account a buddy belongs to: PurpleBuddyGetAccount(buddy)
  • Your own status (also):
    • Some constants you can't get through the D-Bus interface yet:
      STATUS_OFFLINE = 1
      STATUS_AVAILABLE = 2
      STATUS_UNAVAILABLE = 3
      STATUS_INVISIBLE = 4
      STATUS_AWAY = 5
      STATUS_EXTENDED_AWAY = 6
      STATUS_MOBILE = 7
      STATUS_TUNE = 8
      
    • Get your status with: PurpleSavedstatusGetCurrent()
    • Get your status message with: PurpleSavedstatusGetMessage(status)
    • Create a status with: PurpleSavedstatusNew(title, type)
    • Set a status message with: PurpleSavedstatusSetMessage(status, message)
    • Actually set the status with: PurpleSavedstatusActivate(status)
  • Your conversations:
    • Get all conversations: PurpleGetConversations()
    • Get all IMs: PurpleGetIms()
    • Create a new conversation: PurpleConversationNew(type,account,name)
    • Get the type of a conversation: PurpleConversationGetType(conv) [Returns PURPLE_CONV_TYPE_IM = 1, PURPLE_CONV_TYPE_CHAT = 2]
    • Which account of yours is having that conversation: PurpleConversationGetAccount(conv)
    • Get a conversation's title: PurpleConversationGetTitle(conv)
    • Get a conversation's name: PurpleConversationGetName(conv)
    • Get the im of a conversation: PurpleConvIm(conv)
    • Send an IM message with: PurpleConvImSend(im, message)
  • Preferences:
    • Get a preference: PurplePrefsGetBool, PurplePrefsGetString, etc.
    • Set a preference: PurplePrefsSetInt, PurplePrefsSetPath, etc.

There are other things you can do; see the documentation and the source files.

Further reading

Troubleshooting D-Bus

Ideally, everything should just work fine. If you have a broken or non-standard system, though, here are some things to check for:

  • Firstly, Pidgin should have been built with D-Bus support. You can check this in Help -> About. If your Pidgin was not built with D-Bus support, you have to build Pidgin yourself if you want D-Bus working.
  • In addition, a D-Bus daemon should be running, and the variable DBUS_SESSION_BUS_ADDRESS should be set, in the environment from which Pidgin is started. If Pidgin cannot find DBUS_SESSION_BUS_ADDRESS, it will try to execute dbus-launch with the --autolaunch option. This might either successfully start a new session bus, or Pidgin might continue with only
    dbus: Failed to get connection: Failed to execute dbus-launch to autolaunch D-Bus session
    

in the debug output. Either way, you do not want this to happen. Make sure Pidgin gets the DBUS_SESSION_BUS_ADDRESS` of an existing bus. Check that you have

dbus: okkk

near the top of Pidgin's debug output.

  • Your D-Bus script must also use the same bus that Pidgin is using. The easiest way to ensure this is to run the script in the same shell that you started Pidgin from; you could also get the values of DBUS_SESSION_BUS_ADDRESS and DBUS_SESSION_BUS_PID from the shell and export them in the other shell. If you get an error like
    DBusException: org.freedesktop.DBus.Error.ServiceUnknown: The name im.pidgin.purple.PurpleService was not provided by any .service files
    

it means that either Pidgin didn't start with dbus: okkk, or your script is using a different bus.

DBUS glib example

To use DBUS glib library to listen to pidgin signals, things are little bit different from what you will do with Python.

  • First of all, you need to define marshaller functions for the signal handler.
    I.e., if you want to listen to signal ReceivedImMsg, the first thing to do is to find out the correct parameters associated with the signal. Since there is no any document specifying each Pidgin signal's parameter format, one good way to find out the secret is to use dbus-monitor. Start the dbus-monitor like
    $dbus-monitor type=signal interface="im.pidgin.purple.PurpleInterface"
    

And then ask your buddy to send you some IM messages. The dbus-monitor will print out something like:

signal sender=:1.21 -> dest=(null destination) path=/im/pidgin/purple/PurpleObject; interface=im.pidgin.purple.PurpleInterface; member=ReceivedImMsg
   int32 1097
   string "mybuddy@hotmail.com"
   string "<FONT FACE="Times"><FONT COLOR="#000000">Hi!</FONT></FONT>"
   int32 8728
   uint32 0

Now you know that the parameters associated with signal ReceivedImMsg are:

INT32,
STRING,
STRING,
INT32,
UINT32

Then we can create file marshal.list with the one line marshaller function definition:

VOID:INT,STRING,STRING,INT,UINT
  • Please note, apparently the types for these functions may have changed accidentally at one point. The original examples here had the types VOID:UINT,STRING,STRING,UINT,UINT . Make sure to check the types with dbus-monitor and use the appropriate types or your callbacks will mysteriously not be triggered!
  • Second, generate marshal.h and marshal.c
    After create your own file marshal.list, you can then use tool glib-genmarshal to generate marshal.h and marshal.c in the following way:
    $glib-genmarshal --header --prefix=marshal marshal.list > marshal.h
    $glib-genmarshal --body --prefix=marshal marshal.list > marshal.c
    
  • Third, write your own signal handler pidgin_dbus_signal_hndl.c:
    #include <glib.h>
    #include <glib-object.h>
    #include <dbus/dbus.h>
    #include <dbus/dbus-glib.h>
    
    #include "marshal.h"  /* The file generated from marshal.list via tool glib-genmarshal */
    
    /* pidgin dbus station name */
    #define DBUS_SERVICE_PURPLE      "im.pidgin.purple.PurpleService"
    #define DBUS_PATH_PURPLE         "/im/pidgin/purple/PurpleObject"
    #define DBUS_INTERFACE_PURPLE    "im.pidgin.purple.PurpleInterface"
    
    /* global dbus instance */
    DBusGConnection *bus;
    DBusGProxy *purple_proxy;
    
    
    /* Main event loop */
    GMainLoop *loop = NULL;
    
    /* Signal callback handling routing */
    void received_im_msg_cb (DBusGProxy *purple_proxy, int account_id, 
                             const char *sender, const char *message, 
                             int conv_id, unsigned int flags,
                             gpointer user_data)
    {
        g_print("Account %d receives msg \"%s\" from %s\n",   
                account_id, message, sender);
    }
    
    /*
     * The main process, loop waiting for any signals
     */
    int main (int argc, char **argv)
    {
        GError *error = NULL;
        
        g_type_init ();
    
        /* Get the bus */
        bus = dbus_g_bus_get (DBUS_BUS_SESSION, &error);
        if (bus == NULL) {
            g_printerr("Failed to open connection to bus: %s", error->message);
            g_error_free(error);
            return -1;
        }
    
        /* Create a proxy object for the bus driver */
        purple_proxy = dbus_g_proxy_new_for_name (bus,
                                                  DBUS_SERVICE_PURPLE,
                                                  DBUS_PATH_PURPLE,
                                                  DBUS_INTERFACE_PURPLE);
        
        if (!purple_proxy) {
            g_printerr("Couldn't connect to the Purple Service: %s", error->message);
            g_error_free(error);
            return -1;
        }
    
        /* Create the main loop instance */
        loop = g_main_loop_new (NULL, FALSE);
    
        /* Register dbus signal marshaller */
        dbus_g_object_register_marshaller(marshal_VOID__INT_STRING_STRING_INT_UINT, 
                                          G_TYPE_NONE, G_TYPE_INT, G_TYPE_STRING, 
                                          G_TYPE_STRING, G_TYPE_INT, G_TYPE_UINT, 
                                          G_TYPE_INVALID);
            
        /* Add the signal to the proxy */
        dbus_g_proxy_add_signal(purple_proxy, "ReceivedImMsg", 
                                G_TYPE_INT, G_TYPE_STRING, G_TYPE_STRING, 
                                G_TYPE_INT, G_TYPE_UINT, G_TYPE_INVALID);
    
        /* Connect the signal handler to the proxy */
        dbus_g_proxy_connect_signal(purple_proxy, "ReceivedImMsg",
                                    G_CALLBACK(received_im_msg_cb), bus, NULL);
    
        /* Main loop */
        g_main_loop_run (loop);
    
        return 0;
    }
    

Then compile both marshal.c and pidgin_dbus_signal_hndl.c:

$ gcc -MMD -Wall -ggdb -O `pkg-config --cflags dbus-1 glib-2.0 dbus-glib-1` `pkg-config --libs dbus-1 glib-2.0 dbus-glib-1` marshal.c pidgin_dbus_signal_hndl.c -o sighndl

And then run sighndl:

./sighndl

When your buddy sends you any IM message, if everything goes right, your program will print out something like

Account 1097 receives msg "<FONT FACE="Times"><FONT COLOR="#000000">Hi</FONT></FONT>" from mybuddy

Important Note

If you see an issue with the received-im-message signal(ReceivedImMessage? for people making a C / Python Plugin) that your application/plugin doesn't get invoked for the first received message, then this is not a bug in your code or pidgin, rather is a feature. Use displayed-im-message (DisplayedImMessage?) for better accuracy. See this post for details on "why": TIP: A Post For Pidgin Plugin Developers


Good luck!

Last modified 4 months ago Last modified on 04/09/16 02:58:30
All information, including names and email addresses, entered onto this website or sent to mailing lists affiliated with this website will be public. Do not post confidential information, especially passwords!