| 1 | = D-Bus Howto = |
| 2 | |
| 3 | [[TOC(inline)]] |
| 4 | |
| 5 | == Introduction == |
| 6 | |
| 7 | D-Bus is a way for applications to communicate with one another, and with the rest of the world. It is a [http://www.freedesktop.org/ freedesktop.org] project; see [http://www.freedesktop.org/wiki/Software/dbus here] and see the [http://www.freedesktop.org/wiki/IntroductionToDBus 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. |
| 8 | |
| 9 | There are two kinds of communication/interaction that can happen over D-Bus: |
| 10 | * Emitting signals / listening to signals |
| 11 | * Calling methods / replying to method calls |
| 12 | |
| 13 | You do the listening and calling; Pidgin does the emitting and replying. |
| 14 | |
| 15 | == Listening to signals == |
| 16 | |
| 17 | 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. |
| 18 | |
| 19 | 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): |
| 20 | {{{ |
| 21 | #!python |
| 22 | bus.add_signal_receiver(my_func, |
| 23 | dbus_interface="im.pidgin.purple.PurpleInterface", |
| 24 | signal_name="ReceivedImMsg") |
| 25 | }}} |
| 26 | |
| 27 | [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.] |
| 28 | |
| 29 | 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 [http://developer.pidgin.im/doxygen/dev/html/conversation-signals.html#received-im-msg documentation] tells you that the parameters are (account, sender, message, conversation, flags), so you define `my_func` accordingly: |
| 30 | {{{ |
| 31 | #!python |
| 32 | def my_func(account, sender, message, conversation, flags): |
| 33 | print sender, "said:", message |
| 34 | }}} |
| 35 | |
| 36 | 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: |
| 37 | {{{ |
| 38 | #!python |
| 39 | #!/usr/bin/env python |
| 40 | |
| 41 | def my_func(account, sender, message, conversation, flags): |
| 42 | print sender, "said:", message |
| 43 | |
| 44 | import dbus, gobject |
| 45 | from dbus.mainloop.glib import DBusGMainLoop |
| 46 | dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) |
| 47 | bus = dbus.SessionBus() |
| 48 | |
| 49 | bus.add_signal_receiver(my_func, |
| 50 | dbus_interface="im.pidgin.purple.PurpleInterface", |
| 51 | signal_name="ReceivedImMsg") |
| 52 | |
| 53 | loop = gobject.MainLoop() |
| 54 | loop.run() |
| 55 | }}} |
| 56 | |
| 57 | Obviously, |
| 58 | * you should name your function something more descriptive than `my_func` (`received_im_msg_cb` is a good convention, cb for "callback"); and |
| 59 | * a D-Bus script that simply prints what you could see in the client anyway is rather useless. |
| 60 | |
| 61 | But you get the idea. |
| 62 | |
| 63 | Here's a sample list of signals you can receive. For the full list, look in pages named "Signals" in the [http://developer.pidgin.im/doxygen/dev/html/pages.html list of documentation pages]. |
| 64 | |
| 65 | * [http://developer.pidgin.im/doxygen/dev/html/account-signals.html Account signals]: |
| 66 | * account-status-changed, account-connecting, account-error-changed, account-authorization-requested |
| 67 | * [http://developer.pidgin.im/doxygen/dev/html/blist-signals.html Buddy list signals]: |
| 68 | * buddy-status-changed, buddy-idle-changed, buddy-signed-on, buddy-signed-off, buddy-icon-changed |
| 69 | * [http://developer.pidgin.im/doxygen/dev/html/connection-signals.html Connection signals]: |
| 70 | * signing-on, signed-on, signing-off, signed-off, connection-error |
| 71 | * [http://developer.pidgin.im/doxygen/dev/html/conversation-signals.html Conversation signals]: |
| 72 | * 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. |
| 73 | * writing-im-msg, wrote-im-msg, |
| 74 | * sending-im-msg, sent-im-msg, |
| 75 | * receiving-im-msg, received-im-msg, |
| 76 | * buddy-typing, buddy-typing-stopped, |
| 77 | * |
| 78 | * writing-chat-msg, wrote-chat-msg, |
| 79 | * sending-chat-msg, sent-chat-msg, |
| 80 | * receiving-chat-msg, received-chat-msg, |
| 81 | * chat-buddy-joining, chat-buddy-joined, chat-buddy-leaving, chat-buddy-left, |
| 82 | * chat-buddy-flags, chat-inviting-user, chat-invited-user, |
| 83 | * chat-invited, chat-joined, chat-left, |
| 84 | * chat-topic-changed, |
| 85 | * conversation-created, conversation-updated, deleting-conversation, |
| 86 | * conversation-extended-menu |
| 87 | * [http://developer.pidgin.im/doxygen/dev/html/core-signals.html Core signals]: |
| 88 | * quitting |
| 89 | * [http://developer.pidgin.im/doxygen/dev/html/gtkconv-signals.html GtkConv signals]: |
| 90 | * displaying-im-msg, displayed-im-msg, |
| 91 | * displaying-chat-msg, displayed-chat-msg, |
| 92 | * conversation-switched, |
| 93 | * conversation-hiding, conversation-displayed |
| 94 | * [http://developer.pidgin.im/doxygen/dev/html/gtkimhtml-signals.html GtkIMHTML signals]: |
| 95 | * GtkIMHTML is the widget used for the input area in Pidgin |
| 96 | * url_clicked, format_buttons_update, format_function_clear, format_function_toggle, format_function_update |
| 97 | * [http://developer.pidgin.im/doxygen/dev/html/notify-signals.html Notification signals]: |
| 98 | * displaying-userinfo, displaying-email-notification, displaying-emails-notification |
| 99 | * [http://developer.pidgin.im/doxygen/dev/html/plugin-signals.html Plugin signals]: |
| 100 | * plugin-load, plugin-unload |
| 101 | * [http://developer.pidgin.im/doxygen/dev/html/savedstatus-signals.html Saved status signals]: |
| 102 | * savedstatus-changed |
| 103 | * [http://developer.pidgin.im/doxygen/dev/html/sound-signals.html Sound signals]: |
| 104 | * playing-sound-event |
| 105 | * [http://developer.pidgin.im/doxygen/dev/html/xfer-signals.html File transfer signals]: |
| 106 | * file-recv-complete, file-recv-request, file-send-accept, file-send-start, file-send-cancel, file-send-complete |
| 107 | |
| 108 | === Aside: Signals in Pidgin plugins === |
| 109 | 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. |
| 110 | |
| 111 | For example, the signal "playing-sound-event" is emitted just before a sound is played. Its documentation says |
| 112 | {{{ |
| 113 | #!c |
| 114 | gboolean (*playing_sound_event)(PurpleSoundEventID event, PurpleAccount *account); |
| 115 | }}} |
| 116 | and says that if your registered function returns TRUE, Pidgin will cancel playing the sound. |
| 117 | |
| 118 | As another example, the signal "chat-invited" is emitted when you are invited to a chat. Its documentation says its type is |
| 119 | {{{ |
| 120 | #!c |
| 121 | gint (*chat_invited)(PurpleAccount *account, const char *inviter, |
| 122 | const char *chat, const char *invite_message |
| 123 | const GHashTable *components); |
| 124 | }}} |
| 125 | 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. |
| 126 | |
| 127 | Your registered callback function in a plugin might also have an effect on Pidgin by changing the values of parameters it gets. |
| 128 | |
| 129 | You can't do any of this in a D-Bus script. Look at [http://developer.pidgin.im/doxygen/dev/html/signal-howto.html Signals HOWTO] to see how to use signals in plugins. |
| 130 | |
| 131 | Even with read-only plugins, there is a lot you can do: |
| 132 | * When messages are received, notify the user using a system-wide notification system (e.g., [http://growl.info Growl] on OS X, or passing the message through a text-to-speech system and playing it.) |
| 133 | * When a status message is set in Pidgin, connect to Twitter and update a Twitter account with the Pidgin status message. |
| 134 | * Log all the status messages of a particular buddy. |
| 135 | * Calculate the average length of (or delay between) messages sent and received. |
| 136 | * ''Et cetera'': I'm sure you can think of a lot of semi-useful things to do. |
| 137 | |
| 138 | Of course, your script doesn't have to be read-only; you can also actually ''do'' things: |
| 139 | |
| 140 | == Calling Pidgin methods == |
| 141 | |
| 142 | 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. |
| 143 | |
| 144 | To start calling Pidgin functions, you need to get a reference to Pidgin's D-Bus interface. This is in Python: |
| 145 | {{{ |
| 146 | #!python |
| 147 | bus = dbus.SessionBus() |
| 148 | obj = bus.get_object("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject") |
| 149 | purple = dbus.Interface(obj, "im.pidgin.purple.PurpleInterface") |
| 150 | }}} |
| 151 | |
| 152 | Now you can call Pidgin functions as if they were members of the `purple` object. For example, to send a message to all your IM windows, you can do: |
| 153 | {{{ |
| 154 | #!python |
| 155 | for conv in purple.PurpleGetIms(): |
| 156 | purple.PurpleConvImSend(purple.PurpleConvIm(conv), "Ignore.") |
| 157 | }}} |
| 158 | |
| 159 | Sometimes things are not direct. To set your status message without changing the status, for example, you need to use five calls: |
| 160 | {{{ |
| 161 | #!python |
| 162 | def set_message(message): |
| 163 | # Get current status type (Available/Away/etc.) |
| 164 | current = purple.PurpleSavedstatusGetType(purple.PurpleSavedstatusGetCurrent()) |
| 165 | # Create new transient status and activate it |
| 166 | status = purple.PurpleSavedstatusNew("", current) |
| 167 | purple.PurpleSavedstatusSetMessage(status, message) |
| 168 | purple.PurpleSavedstatusActivate(status) |
| 169 | }}} |
| 170 | And this still does unexpected things when your current status is a complex one. |
| 171 | |
| 172 | The only reference for the available functions is the [http://developer.pidgin.im/doxygen/dev/html/files.html documentation for the header files], so you want to look there to figure out which functions you need to call. |
| 173 | |
| 174 | Here are a few of the useful ones: |
| 175 | |
| 176 | * [http://developer.pidgin.im/doxygen/dev/html/account_8h.html Your accounts]: |
| 177 | * Get a particular account: `PurpleAccountsFind(name, protocol)` |
| 178 | * Get all active accounts: `PurpleAccountsGetAllActive()` |
| 179 | * Get an account's username: `PurpleAccountGetUsername(account)` |
| 180 | * Get an account's protocol: `PurpleAccountGetProtocolName(account)` |
| 181 | * Enable/disable an account: `PurpleAccountSetEnabled(account, ui, True/False)` |
| 182 | * Get its icon: `PurpleAccountGetBuddyIconPath(account)` |
| 183 | * Set its icon: `PurpleAccountSetBuddyIconPath(account, path)` |
| 184 | |
| 185 | * [http://developer.pidgin.im/doxygen/dev/html/blist_8h.html Your buddies]: |
| 186 | * Get list of all buddies: `PurpleFindBuddies(account,screenname)` [Second argument NULL, to search for all] |
| 187 | * Get a particular buddy: `PurpleFindBuddy(account, screenname)` |
| 188 | * Check if a buddy is online: `PurpleBuddyIsOnline(buddy)` |
| 189 | * Get a buddy's icon: `PurpleBuddyGetIcon(buddy)` |
| 190 | * Get a buddy's alias: `PurpleBuddyGetAlias(buddy)` |
| 191 | * Get a buddy's name: `PurpleBuddyGetName(buddy)` |
| 192 | * Get the account a buddy belongs to: `PurpleBuddyGetAccount(buddy)` |
| 193 | |
| 194 | * [http://developer.pidgin.im/doxygen/dev/html/savedstatuses_8h.html Your own status] ([http://developer.pidgin.im/doxygen/dev/html/status_8h.html also]): |
| 195 | * Some constants you can't get through the D-Bus interface yet: |
| 196 | {{{ |
| 197 | #!python |
| 198 | STATUS_OFFLINE = 1 |
| 199 | STATUS_AVAILABLE = 2 |
| 200 | STATUS_UNAVAILABLE = 3 |
| 201 | STATUS_INVISIBLE = 4 |
| 202 | STATUS_AWAY = 5 |
| 203 | STATUS_EXTENDED_AWAY = 6 |
| 204 | STATUS_MOBILE = 7 |
| 205 | STATUS_TUNE = 8 |
| 206 | }}} |
| 207 | * Get your status with: `PurpleSavedstatusGetCurrent()` |
| 208 | * Get your status message with: `PurpleSavedstatusGetMessage(status)` |
| 209 | * Create a status with: `PurpleSavedstatusNew(title, type)` |
| 210 | * Set a status message with: `PurpleSavedstatusSetMessage(status, message)` |
| 211 | * Actually set the status with: `PurpleSavedstatusActivate(status)` |
| 212 | |
| 213 | * [http://developer.pidgin.im/doxygen/dev/html/conversation_8h.html Your conversations]: |
| 214 | * Get all conversations: `PurpleGetConversations()` |
| 215 | * Get all IMs: `PurpleGetIms()` |
| 216 | * Create a new conversation: `PurpleConversationNew(type,account,name)` |
| 217 | * Get the type of a conversation: `PurpleConversationGetType(conv)` [Returns PURPLE_CONV_TYPE_IM = 1, PURPLE_CONV_TYPE_CHAT = 2] |
| 218 | * Which account of yours is having that conversation: `PurpleConversationGetAccount(conv)` |
| 219 | * Get a conversation's title: `PurpleConversationGetTitle(conv)` |
| 220 | * Get a conversation's name: `PurpleConversationGetName(conv)` |
| 221 | * Get the im of a conversation: `PurpleConvIm(conv)` |
| 222 | * Send a message with: `PurpleConvImSend(im, message)` |
| 223 | |
| 224 | * [http://developer.pidgin.im/doxygen/dev/html/prefs_8h.html Preferences]: |
| 225 | * Get a preference: `PurplePrefsGetBool`, `PurplePrefsGetString`, etc. |
| 226 | * Set a preference: `PurplePrefsSetInt`, `PurplePrefsSetPath`, etc. |
| 227 | |
| 228 | There are other things you can do; see the [http://developer.pidgin.im/doxygen/dev/html/main.html documentation] and the source files. |
| 229 | |
| 230 | == Further reading == |
| 231 | |
| 232 | * [http://dbus.freedesktop.org/ D-Bus home page] |
| 233 | * [http://www.freedesktop.org/wiki/IntroductionToDBus Introduction To DBus] |
| 234 | * The `purple-remote` and `purple-url-handler` scripts that are installed with Pidgin |
| 235 | * The Finch docklet plugin in `finch/plugins/pietray.py` |
| 236 | * Some examples from [http://arstechnica.com/reviews/apps/pidgin-2-0.ars/4 Ars Technica's review of Pidgin] |
| 237 | * [http://www.amazon.com/Open-Source-Messaging-Application-Development/dp/1590594673 Sean Egan's book] is a good reference on many things. |
| 238 | * [http://pidgin.im/~elb/cgi-bin/pyblosxom.cgi/architecture.html Elb's drawing of the Pidgin architecture] might be useful too. |
| 239 | * The most authoritative reference is the source code. Dig in. |
| 240 | |
| 241 | == Troubleshooting D-Bus == |
| 242 | |
| 243 | Ideally, everything should just work fine. If you have a broken or non-standard system, though, here are some things to check for: |
| 244 | |
| 245 | * 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. |
| 246 | |
| 247 | * 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 |
| 248 | {{{ |
| 249 | dbus: Failed to get connection: Failed to execute dbus-launch to autolaunch D-Bus session |
| 250 | }}} |
| 251 | 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 |
| 252 | {{{ |
| 253 | dbus: okkk |
| 254 | }}} |
| 255 | near the top of Pidgin's debug output. |
| 256 | |
| 257 | * 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 |
| 258 | {{{ |
| 259 | DBusException: org.freedesktop.DBus.Error.ServiceUnknown: The name im.pidgin.purple.PurpleService was not provided by any .service files |
| 260 | }}} |
| 261 | it means that either Pidgin didn't start with `dbus: okkk`, or your script is using a different bus. |
| 262 | |
| 263 | ---- |
| 264 | |
| 265 | Good luck! |