Migrating From Purple 2

Nearly everything has changed between purple 2 and purple 3. We’re truly sorry for this, but there just wasn’t another way to go about it. This document will do its best to guide you through the changes but if something isn’t clear, please let us know so that we can update this document.

Background

The APIs in purple 2 were designed for a different era and there just wasn’t a way to do incremental changes to get where we needed to.

The usual example for this is messages which are pretty important in a chat client. In purple 2 a message would come in from a protocol with a string for the contents. That string would be optionally written to the logger, which usually just wrote it out to the end of a file, and then it got written out to the end of the conversation window. And just like that purple 2 forgot about the message.

There was no way to tell the logger to edit the message, no way to tell the conversation window that someone added a response to it, no way to delete the message, no way to jump to a message in the history, etc.

To fix this, we needed to change the way messages worked in purple. Which of course meant changes to purple, the protocols, and of course the user interfaces.

The solution introduced a lot of things, but they are all focused around PurpleMessage which keeps track of everything about each message including PurpleMessage:id‘s that are how we are now able to lookup and find messages after initial receipt and update them for all of the modern chat features that just were not possible in purple 2.

There are many more smaller examples, but this one is good at describing the necessary scope of these changes which is why we use it.

Terminology

There have been a number of terminology changes that need to be covered before anything else.

  • Instant Messages (IMs) are now referred to as Direct Messages (DMs). This is a pretty standard change that has been adopted by most other clients as far as we can tell.

  • Chats are now referred to as Channels. This is because many users call Group Direct Messages chats and unlike Group DMs, channels can be public, have infrastructure around them like permissions, passwords, access levels, and can be discovered via a channel search.

  • PurpleContact has been replaced by PurplePerson which has also picked up responsibility for who shows up on your contact list.

  • PurpleBuddy has been replaced by PurpleContact and no longer signifies that the person this represents is on your contact list. This is important because modern chat protocols typically just send an identifier for a user and the client is expected to cache the information about the user and only request it from the server when necessary. We’ll get more into this later, but that means that purple 3 is designed to be that cache and track all of the contacts that you run into.

  • PurpleConvChatBuddy has been replaced by PurpleConversationMember which has a PurpleContact and additional properties for that contact in that specific conversation.

  • Buddy Icons are now called Avatars and are backed by PurpleImage. We’re currently reworking this a bit so details are a bit sparse right now.

Supporting Libraries

Purple 3 has a number of supporting libraries that were designed to help with purple 3 but are maintained externally with the hope that they are useful to others and need not be an exclusive purple 3 API.

Birb

Birb is a utility library which holds some things that we end up using all over the place including some of our protocol libraries.

GPlugin

GPlugin is a plugin library. Purple has always made heavy use of plugins but the previous purple exclusive API made some things easier and others harder.

More importantly than all of that is that GPlugin uses GObject Introspection for loading plugins in other languages.

GObject Introspection is a well defined API and data format that describes an API which makes language bindings trivial. This is made possible because the language bindings only need to know how to translate the GObject Introspection format to their target language.

GPlugin, Purple, and Pidgin all create a GObject Introspection Repository (GIR) file as part of their build that can be used by these translation layers to expose the API automatically with no hand crafted bindings.

GPlugin currently contains loaders for any native plugins assuming they expose the correct functions, Lua, and Python 3. It even provides examples of plugins written in Vala, Genie, and there has been a proof of concept for Rust plugins as well.

HASL

HASL is a new SASL library we wrote to deal with some of the issues in the existing offerings.

Seagull

Seagull is an alternative API to SQLite. It’s designed to force a specific workflow of using prepared statements with named parameters and making that workflow easier for the developer to get correct.

It also provides API for handling migrations and makes working with GObjects and other GLib data structures in SQLite much easier.

Traversity

Traversity is meant to be a Swiss Army Knife when it comes to NAT Traversal.

It has a long way to go, but this will eventually replace all of the NAT traversal API that existed in purple 2 but has already been removed from purple 3.

Conceptual Changes

We’ve tried to keep things as familiar as possible, but of course some things had to change.

Meson

As you may or may not know, we moved to Meson as our build system some time ago. Meson is a very easy to use and capable build system. It has a lot of stuff built in to make our lives easier, but the absolute biggest thing, is that we can use it everywhere.

In the purple 2 days we had two build systems: one on Windows and one for everything else. With Meson, this is no longer a thing as it runs everywhere. It does come with a few small caveats though.

First, it is implemented in Python which can sometimes be problematic to install and keep up to date on Windows and macOS. But that also means we can write utility scripts in Python which will work everywhere as well.

Secondly it doesn’t provide a way to uninstall the project. This is why we recommend that you do not install Pidgin 3 and instead run it from a development environment.

Speaking of development environments, or devenvs. The purple 3 code base is set up so that you do not need to install it to develop against it. The devenv will set everything up to make it discoverable.

You can run meson devenv in the purple 3 build directory to create a shell, and then cd to your own project and generate your build system with Meson, or whatever tool, because Meson has set up a number of environment variables including PATH, PKG_CONFIG_PATH, PKG_CONFIG, and even LD_LIBRARY_PATH. This is helpful if you’re working on something that uses purple, like a protocol plugin, a UI, etc.

These environment variables are the standard ones that build systems look at to find libraries and executables as well as run them which is why they work with whatever your build system is. This is currently the recommended way to develop against purple 3.

Unit Tests

First and foremost we are trying very hard to make as much of purple 3 testable and to implement tests for new APIs when they’re added. This allows us to ensure things are working as expected and help us find memory leaks as soon as possible.

GObjects

One of the big things that complicated the development of purple 3 was the transition to GObjects. There has always been many reasons to make purple a GObject library and it was always going to be a lot of work, but it was more work than anyone expected.

A lot of this work was done in a few Google Summer of Code projects, but it was never quite completed let alone polished. Doing so took far longer than anyone imagined and there were some caveats that still needed to be dealt with.

Regardless, purple 3 is a full GObject based library now and GObject must be understood to do many things in purple 3. There is a tutorial for GObjects here which should probably be reviewed.

In Tree Protocols

In the Gaim and purple 2 days we accepted many protocols into our main code base. This made things easier for users as it was less things for them to find, download, and install, but it came with a much heavier cost than we realized.

Over time the people maintaining these protocols would retire and the remaining developers didn’t have access to the required servers or networks to test these plugins. Which of course meant everything started to bit rot.

On top of that, the proprietary protocols were always a legally dubious point of contention which has caused some people to shy away as well as have employers ask their employees not to be affiliated with the project.

Nowadays, distribution and discovery is much easier, especially with things like Flatpak, all of the other package managers, and software stores that are out there.

So, we are now only carrying Open Source and Open Specification based protocol plugins. This means we will only carry plugins for protocols like IRCv3, XMPP, Matrix, and so on.

All protocols that connect to proprietary networks or are reverse engineered will not be accepted into our main repository. That doesn’t mean we won’t have any of them as separate repository, just that our main releases only contain Open Source and Open Specification protocols.

This is better for everyone as it keeps us focused on Open Source and Open Specifications, but it’s also better for the proprietary protocol plugin maintainers as they don’t have to wait on us for a release when the owners of the protocol make a breaking change. It might be a bit more work for users, but the app stores out there should make discoverability easier and keep them up to date as well.

Groups

Currently groups are under implemented because many modern protocols lack the concept entirely. We are currently working on the assumption that a tag on the contact info named group (which can be specified multiple times) should be able to handle our needs, but we haven’t proven this out yet.

Icons

Instead of passing around raw image data or file names we have moved what we can to using the XDG Icon Theme Specification as much as possible.

Most widget toolkits already have helpers for all of this and if a user interface doesn’t want to support icons, these are just strings that can be easily ignored.

Asynchronous APIs

Most APIs in purple 2 were asynchronous, but not in a way that you could reliably get the result of them. This is one of the reasons why Pidgin 2 and earlier had so many popup windows. An action would be fired off asynchronously and by the time we had the result we had lost all context of the request and the only way to let the user know was to pop up a dialog.

Some examples of this include getting a user’s profile, a message failing to send, file transfers, and so on.

To fix this we’ve moved to the asynchronous API that’s built into Gio named GTask that implements the GAsyncResult interface.

This API is pretty simple as a consumer. You call an _async function and provide an optional callback to it which is called when the task has finished. In your callback, you call a _finish method that gets you the result.

For example, a user interface would call purple_protocol_conversation_set_topic_async() to set the topic of a conversation and then in their callback call purple_protocol_conversation_set_topic_finish() to determine if the set was successful. If it wasn’t successful the UI could show the error in the widget where they gathered the new topic from the user.

Implementing these can get a bit trickier, especially if you need to chain them to other _async methods. For example, say you’re implementing Purple.ProtocolConversationInterface.set_topic_async but you need to make an HTTP request to set the topic using libsoup. You would do something like the following:

/* This is some helper api for the callback function, you probably want to
 * start in the _async function below.
 */
typedef struct {
    PurpleConversation *conversation;
    char *topic;
} MigrateProtocolSetTopicData;

static MigrateProtocolSetTopicData *
migrate_protocol_set_topic_data_new(PurpleConversation *conversation,
                                    const char *topic)
{
    MigrateProtocolSetTopicData *data = g_new(MigrateProtocolSetTopicData, 1);

    data->conversation = g_object_ref(conversation);
    data->topic = g_strdup(topic);

    return data;
}

static void
migrate_protocol_set_topic_data_free(MigrateProtocolSetTopicData *data) {
    g_clear_object(&data->conversation);
    g_clear_pointer(&data->topic, g_free);
    g_free(data);
}

/* This is the callback function for the _async function below, you probably
 * want to start there.
 */
static void
migrate_protocol_conversation_set_topic_cb(GObject *self,
                                           GAsyncResult *result,
                                           gpointer user_data)
{
    GBytes *bytes = NULL;
    GError *error = NULL;
    GTask *task = user_data;

    bytes = soup_message_send_and_read_finish(SOUP_SESSION(self), result,
                                              &error);

    /* We aren't expecting a body so we just need to free whatever we got. */
    g_clear_pointer(&bytes, g_bytes_unref);

    if(error != NULL) {
        /* If we got an error, return it. */
        g_task_return_error(task, error);
    } else {
        /* If we didn't get an error, set the topic on the conversation and
         * return true.
         */
        MigrateProtocolSetTopicData *topic_data = NULL;

        topic_data = g_task_get_task_data(task);

        purple_conversation_topic(topic_data->conversation, topic_data->topic);

        g_task_return_boolean(task, TRUE);
    }

    /* Clearing the task will cause the user's callback to be called, where
     * they will call purple_protocol_conversation_set_topic_finish which will
     * call our implementation below and return either TRUE or the error we set
     * above.
     */
    g_clear_object(&task);
}

static void
migrate_protocol_conversation_set_topic_async(PurpleProtocolConversation *protocol,
                                              PurpleConversation *conversation,
                                              const char *topic,
                                              GCancellable *cancellable,
                                              GAsyncReadyCallback callback,
                                              gpointer data)
{
    MigrateProtocolSetTopicData *set_topic_data = NULL;
    PurpleAccount *account = NULL;
    PurpleConnection *connection = NULL;
    GTask *task = NULL;
    GUri *uri = NULL;
    SoupMessage *message = NULL;
    SoupSession *session = NULL;
    char *query = NULL;

    /* Get the soup session for our connection. */
    account = purple_conversation_get_account(conversation);
    connection = purple_account_get_connection(account);
    session = migrate_connection_get_soup_session(MIGRATE_CONNECTION(connection));

    /* We create our GTask which needs to report back to our caller. */
    task = g_task_new(protocol, cancellable, callback, data);

    /* We add the conversation to our task data adding a reference to make sure
     * it stays valid.
     */
    set_topic_data = migrate_protocol_set_topic_data_new(conversation, topic);
    g_task_set_task_data(task, set_topic_data,
                         (GDestroyNotify)migrate_protocol_set_topic_data_free);

    /* Create our soup request. */
    query = g_strdup_printf("channel=%s&topic=%s",
                            purple_conversation_get_id(conversation),
                            topic);
    uri = g_uri_build(G_URI_FLAGS_NONE, "http://", NULL, "localhost", 80,
                      "/channel/topic", query, NULL);
    g_clear_pointer(&query, g_free);
    message = soup_message_new_from_uri("POST", uri);
    g_clear_pointer(&uri, g_uri_unref);

    /* Now send the message asynchronously passing our callback and our task as
     * data to that callback but reusing the cancellable the user provided.
     */
    soup_session_send_read_async(session, message, G_PRIORITY_DEFAULT,
                                 cancellable,
                                 migrate_protocol_conversation_set_topic_cb,
                                 task);

    /* Clear the message as we're done with it. */
    g_clear_object(&message);
}

static void
migrate_protocol_conversation_set_topic_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
                                               GAsyncResult *result,
                                               GError **error)
{
    /* Propagate the result that we got from our http request. */
    g_task_propagate_boolean(G_TASK(result), error);
}

ui_data

Many objects in purple 2 had a ui_data member that allowed the user interface to attach an opaque pointer them. Now that everything is a GObject, these have been removed as g_object_get_data() and g_object_set_data() can be used to add any amount of data to an object.

proto_data

Similar to ui_data above there were a number of objects in purple 2 that allowed a protocol to associate an opaque pointer with it. These cases have been replaced by making those objects derivable and requiring the protocol to implement their own implementation. PurpleConnection is the biggest one at the time of this writing.

Managers

In purple 2 we had “subsystems” for everything; accounts, buddies, conversations, file transfers, plugins, whiteboards, etc. All of these allowed management of their contained types and signals for events on those instances.

One of the big issues with the subsystems was that they depended on global state. For example, when an account was created, it would automatically get added to the account subsystem.

This was great from a developer point of view, but it meant you can’t create an account in a unit test without initializing the account subsystem, and the account subsystem depends on the protocols being loaded, which depends on the plugin subsystem being loaded, and so on. Obviously this wasn’t ideal.

However, the biggest issue with the subsystems was that their signals were “class based”. Meaning that you could connect to the account-disconnected signal in the account subsystem and your callback would get called when any account disconnected. In GObject, signals are instance based, which means there is no easy way to have a callback called whenever any account is disconnected.

To address these issues we introduced what we call “managers”. A manager keeps track of a collection of objects and will propagate their signals essentially imitating the “class based” signals from the subsystems.

So for the account-disconnected signal in purple 2 mentioned above you would connect to the PurpleAccountManager::account-disconnected signal of an PurpleAccountManager. But now you need to know where to get an instance of PurpleAccountManager from. That’s where the next step comes in.

To avoid the issue with global state that we had in purple 2 we made it possible to create multiple instances of the managers. This means that all of their data is self contained and can easily be used in unit tests. But we still needed specific managers for general use in purple. At startup, purple creates instances of these managers and exposes them via _get_default functions.

For the PurpleAccountManager example above, you would do something like the following:

PurpleAccountManager *manager = NULL;

manager = purple_account_manager_get_default();
g_signal_connect(manager, "account-disconnected", G_CALLBACK(my_callback), NULL);

purple_account_manager_get_default() returns the default instance of PurpleAccountManager that purple created during startup. Once you have that, you can use the normal signal API in GObject to connect to the PurpleAccountManager::account-disconnected signal on the manager and your callback will be called whenever any account the manager knows about is disconnected.

Most of the managers have add/remove functions, and some still have register/unregister functions that will probably be changed to add/remove in the future. They all should also implement the GListModel interface which is something that is used very heavily in purple 3 and you will want to get familiar with as well.

There’s a few more caveats to managers, but we’re going to skip them for now.

Account Options and User Splits

Historically speaking user splits were originally created to help identify which account was which when displaying them in a list. It also made the account editor a bit easier or harder depending on the user. They split things like an XMPP ID (JID) into multiple fields which was not expected behavior for XMPP.

To solve the issue for being able to determine which account is which, we added the PurpleAccount:name property to PurpleAccount to let users name them whatever they want. This solves the original purpose of user splits but we still need account specific settings for things like usernames, what server to connect to as well as other advanced options.

In purple 2 accounts options were all displayed under the advanced tab in the account editor with the user splits in the main tab. This was easy for users, but kind of annoying for developers as there’s two different interfaces to very similar data.

We are currently in the process of replacing these with the new PurpleAccountSetting and its subclasses and hoping to have have this work done by the Experimental 4 release.

PurpleAccountSetting has an PurpleAccountSetting:id property for identifying the settings and a PurpleAccountSetting:label property which will be displayed in the user interface. PurpleAccountSettings also have an PurpleAccountSetting:advanced property that can be set to tell the user interface which settings are advanced.

Protocols will need to implement the Purple.ProtocolClass.get_default_account_settings function creating an instance of PurpleAccountSettings which the user interface will call when the user is creating or editing an account. These settings will then all be stored with the account allowing the protocol author to change defaults without affecting users.

A protocol can then use the ]property@Account:settings] to get the account specific settings which replaces the old purple_account_get_bool, purple_account_get_int, and purple_account_get_string methods.

The PurpleAccountSettings collection and PurpleAccountSetting instances unify account settings which gives protocols the ability to use the nomenclature that users of those protocols expect.

We have also been working on creating a concept of “Networks”. For example, a user knows that they want to connect to the Libera.chat or OFTC IRC networks, but they probably don’t know or care to know the specific hostname they need to connect to to do so.

With the PurpleAccountSettings object, we’re looking at adding a method to “merge” two instances. This ability will help us implement the networks feature as we could then merge the settings for a network with the defaults from a protocol to fill in the network specific settings.

This is still all conceptual, but this does seem like an idea that will work. We have PIDGIN-18064 to track this, but it’s still a bit out.

Contacts

As mentioned earlier in the terminology section, much has changed when it comes to contacts as well.

At the base of all contacts we now have PurpleContactInfo which keeps track of all information about a contact but is not tied to a specific account. This is necessary as PurpleAccounts have a PurpleContactInfo via their PurpleAccount:contact-info property.

Next we have PurpleContact which ties a PurpleContactInfo to an PurpleAccount. This is pretty straight forward and is the main way to interact with a contact outside of a conversation.

To represent contacts in a PurpleConversation there is now PurpleConversationMember which has additional properties for the contact in the conversation. Things like a conversation specific nickname, their typing state, and so on. Conversation members are stored in the PurpleConversation:members property and should be managed via the PurpleConversationMembers API.

Finally we have PurplePerson which is a collection of contacts for a specific person. In purple 2 this is roughly what a PurpleContact was, but in purple 3 this is also used to determine who is visible in your contact list/roster.

When a protocol is adding new contacts, it should check if the contact is on the protocol’s list of contacts and create a person for that contact if one does not exist.

Contact Management

In purple 2 contact management only consisted of people on your contact list/roster. This made sense at the time, but as chat evolved many protocols moved to associating lots of data with users which required them to refer to users with just an identifier to save bandwidth.

This means that messages from any user, on your contact list or not, just come in with an identifier and that identifier would have to be resolved somehow to get a name to display. Obviously the protocol providers don’t want you looking that up all the time so the clients have to cache that.

The modern protocols implemented in purple 2 tend to do this by adding all contacts to the BuddyList or by maintaining an internal cache. Both of these options are subpar as the former pollutes your contact list and the later leads to code duplication across each protocol.

To combat this, we created PurpleContactManager. The contact manager keeps a list of all contacts by account allowing you to do basic CRUD operations against them as well as persisting them on disk.

Protocol implementations should use the contact manager to find contacts and add ones that the contact manager doesn’t know about yet. This will allow a purple user to alias anyone, add notes to anyone, and so on. Doing so also eliminates a lot of synchronization during the initial connection as the data on disk should be relatively up to date.

Preferences to GSettings

The preferences API in purple 2 was good for its time but it caused issues for other user interfaces like Instant Bird which required some API changes to make it work better.

However, since that API was written GSettings has been created and does all the purple preferences API did and then some. As such we decided to move to GSettings in Purple 3.

GSettings can be a bit intimidating to get started, but purple does provide a GSettingsBackend to manage its settings via purple_core_get_settings_backend(). This backend is actually created by the user interface, but Purple exposes it for use throughout the program.

You can find examples throughout the code base including any plugin that has settings.

Status API

The status API in purple 2 was as complicated as was the problem it was trying to solve. Fortunately, status has simplified over time due to complexity and privacy concerns. As such our new status API is just about as simple as it can get.

At the center of it is PurplePresence. It has a number of properties for tracking the presence of a contact with PurplePresence:primitive and PurplePresence:message being the most important.

PurplePresence does not try to enforce any rules about what is and is not valid for any specific protocol deferring these decisions to the protocol itself. In most cases the protocol will be the only one setting these properties which keeps things nice and simple.

When it comes to setting the presence for the purple user, that’s where things get a bit trickier. We have a lot of stuff in place for this, but it hasn’t been completely proved out yet.

The global state of the purple user’s presence is managed by PurplePresenceManager which manages instances of PurpleSavedPresence and has an PurplePresenceManager:active-presence to represent which PurpleSavedPresence is currently in use.

The per account presence is managed solely by the protocol plugin via the PurpleContactInfo:presence of the PurpleAccount:contact-info of each PurpleAccount.

When an account connects, the protocol can grab the active saved presence and set it during the connection process. I don’t recall the specifics, but in purple 2 it was impossible to connect while invisible as there wasn’t a way to determine the presence or something before the account was connected. With the new API, that limitation no longer exists.

When it comes to updating the purple user’s presence, the idea right now is to have protocols listen to changes to the active presence property and then forward those updates according to the protocol.

Some interest has been expressed to have a protocol virtual function that would get called when something changes in the purple user’s presence but that feels like extra steps when everything else will already be using the signals to track changes. Especially since a protocol can connect to the signals with g_signal_connect_object() and tie the lifetime of the signal handler to their PurpleConnection instance. At any rate, if you have thoughts on this, please let us know!

There are currently no helper APIs for making a PurplePresence match a PurpleSavedPresence. We may add some at some point, but some protocols will need to map PurplePresencePrimitive and do other things, so we’ve avoided this for the time being.

Message Formatting

In purple 2 the Rich Text Format was HTML. This worked okay, but required passing over the message content multiple times for clients like Pidgin. The protocol would generate HTML and then Pidgin would convert the HTML into the format used in GtkTextView.

This also had the undesired affect that users couldn’t temporarily turn off formatting which has quite a few use cases. Looking back on it, we might have been able to come up with something for this, but it would have required a significant amount of work.

Many modern chat protocols support Markdown, which could have been a possible contender but its lack of color support made it a non starter.

Other protocols like Twitch will use start and end offsets in the plain text to define attributes about that text. This has the added benefit of splitting the display (formatting) from the content (plain text) and allows you to disable the formatting without throwing it away.

After a bit of digging we realized that PangoAttribute has start and end indices, already implements all of the basic text formatting options, allows for custom attributes, and for GTK based clients requires no additional work on our side.

To implement this, we have PurpleMessage:contents which is the plain text content of the messages and PurpleMessage:attributes which is a PangoAttrList of the PangoAttributes which contain the formatting of the message contents.

We do not currently have any helpers for working with PangoAttributes but we may add some for Markdown in the future.

Likewise, we do not currently have custom attributes for things like mentions, custom emojis, inline images, and so on. These are planned but we’re debating where to put them as we want to use them in Ibis, Myna, and Xeme which means that they will most likely end up in Birb.

Also user interfaces will be expected to provide formatting via PurpleMessage:attributes when sending messages.

Connection State Tracking

In purple 2 the state of a connection was tied to the connection object itself which was kind of weird since connections only existed when accounts were not disconnected.

This led to some interesting API, so in purple 3 we moved the connection state to the PurpleAccount:connection-state property which will always reflect the connection state of the account.

Actions

All actions in purple have now been moved to GAction and GMenuModel. These are worked into a number of places that you would expect and we created a helper BirbActionMenu to make working with them a bit easier.

This also means that plugins in other languages can now add actions to things as we no longer require a C function pointer for the callback.

One of the big benefits of GMenuModel is that you have full control over the menu including adding sections, sub menus, and so on.

New Stuff

There is a lot of new stuff and this section is focused on just a small amount of them.

Attachments

Message attachments are something new that didn’t really exist in purple 2 but bears some mentioning here. Many modern chat protocols will let you attach images and/or files to a message and will use the attachments to provide a preview of links among other things.

Messages have an PurpleMessage:attachments property that contains a collection of PurpleAttachments. When a message is received these are used by the user interface to present them to the user in whichever way it sees fit. Likewise, when sending a message the protocol will need to parse these and send them out as necessary.

It is worth noting that inline images and files will be implemented via attachments, but will not typically be shown with the other attachments as they are inline. They would be referenced via a message formatting attribute which has not yet been defined.

Badges

Many modern protocols have what we call badges. These are little icons that are displayed before or after a contact’s name. Sometimes they are conversation specific like owner, moderator, etc, and other times they are contact specific like administrator, staff, etc.

In purple 2 we only had support for the conversation specific ones and only in chats (what we now call channels). These were previously implemented via the PurpleConvChatBuddyFlags flag.

PurpleConvChatBuddyFlags were functional for the time, but didn’t allow for the kind of customization that we would see from Slack, which shows the presence emoji after someone’s name. Then there is Twitch that has custom badges per channel and contact specific badges to signify partners, Twitch staff, and participation in specific events.

PurpleContactInfo has a PurpleContactInfo:badges to add badges globally for a contact, while conversation specific badges can be set via the PurpleConversationMember:badges property on PurpleConversationMember.

Tags

There is a common need to store random arbitrary data with an object. For example, purple 2 had the purple_buddy_[gs]et_{bool,int,string} API. To simplify this, we created PurpleTags which is a well defined format that has been added to many of our core data structures.

Right now tags are designed to only work on strings but we are looking at adding helpers to make it easier to work with integer and boolean values.

for-display functions

In purple 2 we had a number of features like local aliases and avatars but no helpers to make sure the correct one was selected. This lead to issues where aliases wouldn’t be displayed because not all uses were updated and so on. In purple 3 we’ve added “for-display” properties to address this.

For example PurpleContactInfo has PurpleContactInfo:name-for-display which will check a number of properties and always return the proper one. A full break down can be seen in the documentation for the property.

There are a number of these throughout the code base, if you think there should be one where there isn’t let us know.

Notifications

Purple 2 didn’t really have a way to define notifications. Pidgin 2 did a bit of this by showing connection errors, contact authorization requests, and some other stuff in the bottom of the Buddy List window but since these weren’t handled in purple 2 it lead to more popup windows. These popup windows were very often closed on accident which was less than ideal when it was an announcement from a server admin or file transfer request.

To avoid these issues we created PurpleNotification and PurpleNotificationManager to manage them. We’ve played around with the exact API a few times and will probably do so again, but you can create a notification and add it to the default manager and the UI will display it with some actions that can be performed.

Notifications are not persisted and are instead intended to be synchronized when reconnecting to services as they could have been dismissed while an account was offline.

Protocol Interfaces

Of course with all of the other changes, the protocol interface has changed drastically as well.

PurpleProtocol is now an abstract class that you must subclass. The class itself is very simple as all of the big features have been moved to separate interfaces. This change was done to hopefully help keep things more organized in everyone’s code but does come with a small amount of additional boilerplate code.

Also many of the old virtual functions have been removed as they are either no longer necessary or have been implemented differently.

  • icon_spec has been removed and will be address in Purple.AccountSettingAvatar as defined in PIDGIN-18125.

  • list_emblem has been removed as most of its past usages are now covered by PurpleBadges.

  • status_text is no longer necessary as we have a presence for every contact including a status message.

  • tooltip_text has been removed and is unlikely to return.

  • status_types is no longer necessary due to the changes in the status API.

  • set_info does not yet have a replacement.

  • set_status is unlikely to be replaced as the protocol can get the active status from the status manager and apply it when there are changes.

  • set_idle like set_status above all of this information is available to the protocol whenever it needs it.

  • change_password is unlikely to be replaced, at least directly as there are many different way to authenticate now.

  • add_permit, add_deny, rem_permit, rem_deny, and set_permit_deny have all been replaced by PurpleContactInfo:permission.

  • keepalive is unlikely to get replaced as this was really just a timeout handled by purple to tell the protocol when to ping the server, but the protocol has always had a better idea of when that is necessary than purple.

  • register_user and unregister_user will eventually be replaced by some sort of a registration interface, but we haven’t looked into this yet.

  • get_cb_alias, get_cb_away, get_cb_info, and get_cb_real_name have been removed as they are now implemented via PurpleConversationMember which has a PurpleContactInfo and between the two everything about them should be available.

  • group_buddy and rename_group need some redesigning yet. As mentioned above groups are essentially not implemented right now.

  • buddy_free will not be replaced. PurpleContactInfo has a lot of properties which hopefully means you won’t need to store much other data with them. However, since they are GObjects now, you can use g_object_set_data() and g_object_set_data_full() to attach data to them and have it automatically freed when they are destroyed.

  • normalize is unlikely to get replaced. This was necessary because we used usernames everywhere that were directly displayed to users. Instead the normalized identifier of a contact should be stored in PurpleContactInfo:id and the display name from the server should be stored in PurpleContactInfo:display-name. This avoids the issue entirely as user interfaces can sort and filter on the display name and the protocol knows its normalization rules for id based look-ups.

  • find_blist_chat will not be replaced. PurpleConversationManager remembers all conversations across program restarts and thus conversations no longer need to be added to a list to be remembered as that now automatically happens.

  • roomlist_get_list, roomlist_cancel, roomlist_expand_category, and roomlist_room_serialize are going to be radically changed and replaced by PurpleProtocolDirectory but we have not yet gotten to it.

  • offline_message will not be replaced. purple_conversation_send_message_async() will return an error if the message could not be delivered.

  • send_raw will most likely not be replaced. It’s an interesting idea and could be easily implemented but we’re unsure on the utility of it and thus haven’t looked at it yet.

  • send_attention and get_attention_types have been removed for now and we’re currently undecided on bringing something similar back. If this is something you’re interested in, please let us know.

  • get_account_text_table will not be replaced. This isn’t used by any in tree protocols and doesn’t look useful.

  • initiate_media and get_media_caps will be replaced by a media API that we’re still in the process of designing.

  • get_public_alias will not be replaced as the protocol should update the PurpleContactInfo:display-name property when it needs to.

  • add_buddy_with_invite and add_buddies_with_invite were meant to be covered by PurpleProtocolRoster but that does not handle authorization requests and will need to be revisited.

  • chat_can_receive_file will eventually be replaced by a property on PurpleConversation which the protocol will be responsible for setting.

  • chat_send_file will not be replaced. The protocol can create an PurpleAttachment and add it to the PurpleMessage:attachments of a PurpleMessage.

Protocol Class

The protocol class has a few virtual functions that must be implemented and a few that are optional.

PurpleProtocol also has a number of properties that used to be virtual functions.

  • icon-name: Replaces list_icon
  • id: Replaces the use of the plugin id as the protocol id as plugins can support more than one protocol now.
  • name: Replaces the use of display name of the plugin as protocol name as plugins can support more than one protocol now.
  • options: Replaces the options property from the structure but is likely to be removed in the future.

Purple.ProtocolClass.get_user_splits and Purple.ProtocolClass.get_account_options are deprecated and have been replaced by Purple.ProtocolClass.get_default_account_settings.

Also, Purple.ProtocolClass.get_whiteboard_ops will most likely be changed to something like create_whiteboard but we don’t have whiteboards implemented anywhere at the moment so this move has been de-prioritized as we don’t have a way to test it.

Can Connect

In purple 2 we used to use an operating system specific API to check if we were connected to the internet and if so, we would then allow accounts to be connected.

This sounds fine in theory, but what happens when you’re in a corporate network connecting to a local XMPP or IRC server and they only allow HTTPS traffic. In theory this will work, but with something like Link Local Messaging that is local network only you wouldn’t be able to connect if you didn’t have an internet connection.

Regardless there is no way for purple to reliably guess what level of network connectivity a protocol needs and that’s where Purple.ProtocolClass.can_connect_async and Purple.ProtocolClass.can_connect_finish come in.

These virtual functions are required to be implemented.

purple_account_connect() will call these functions before attempting to bring an account online. If Purple.ProtocolClass.can_connect_finish returns true then the connection attempt will continue. This gives the protocol the sole power to decide if a connection attempt should be made.

Most protocols will use GNetworkMonitor to make this decision using g_network_monitor_can_reach_async(), but of course a protocol can use whatever means it finds are necessary.

Create Connection

As mentioned previously, protocols now need to subclass PurpleConnection. When connecting an account purple will call purple_protocol_create_connection() to create an instance of that subclass and then call the Purple.ConnectionClass.connect on it.

This gives the protocol full control over that object for the entire lifetime of the connection. When the account has finished its initial connection negotiations and is ready for user interaction, it should call purple_account_connected() to signal as much.

There is also purple_account_disconnect() for disconnecting an account normally, and purple_account_disconnect_with_error() for disconnecting accounts with an error. When the protocol wants to disconnect, it should call one of these and handle cleanup in the Purple.ConnectionClass.disconnect implementation.

Purple.ConnectionClass.connect replaces the login virtual function, and Purple.ConnectionClass.disconnect replaces the close virtual function.

Generate Account Name

As mentioned above accounts now have a user specified name. However, we realize that users won’t necessarily pick something reasonably by default. That’s where the mandatory Purple.ProtocolClass.generate_account_name comes in. It is meant to help fix that problem by returning something like IRC 1 or XMPP 2.

Get Default Account Settings

Purple.ProtocolClass.get_default_account_settings replaces the get_user_splits and get_account_options virtual functions.

Get Action Menu

Purple.ProtocolClass.get_action_menu is an optional virtual function that returns a list of actions for the account. These are the actions that you saw in the Accounts menu in Pidgin 2.

In purple 2 these actions were defined at the plugin level which meant that they couldn’t be customized on a per account basis. But, with this being a virtual function on the protocol, customization is possible and the plugin can have separate actions as well.

Set Display Name

The Purple.ProtocolClass.set_display_name_async and Purple.ProtocolClass.set_display_name_finish virtual functions replace the set_public_alias virtual function.

Protocol Contacts

PurpleProtocolContacts handles everything for managing remote contacts. However it leaves roster management to a separate interface.

Get Profile

The get_info virtual function has been replaced by Purple.ProtocolContactsInterface.get_profile_async and Purple.ProtocolContactsInterface.get_profile_finish async pair.

These may be replaced in the future to handle profiles with rich content.

Get Actions / Get Menu

The blist_node_menu virtual functions have been replaced by Purple.ProtocolContactsInterface.get_action_menu. This gets the actions and the menu in one swoop.

Aliasing

The alias_buddy virtual function has been replaced by Purple.ProtocolContactsInterface.set_alias_async and Purple.ProtocolContactsInterface.set_alias_finish.

These will work on any PurpleContact and since they are an async pair the result is now accessible which should help avoid desynchronization.

Protocol Conversation

PurpleProtocolConversation handles everything for managing conversations which includes DMs, Group DMs, Channels and Threads.

Protocols are able to create a conversation at any time and add it to the default PurpleConversationManager even if they don’t implement this interface. This isn’t exactly supported but it should work.

Many protocols handle channels very differently than direct messages and group direct messages at least when it comes to creation and joining. To handle this, there are virtual functions for creating direct messages and group direct messages specifically and additional virtual functions for joining a channel. Both of these are covered separately below.

Also, we do not currently have virtual functions for creating a channel. This will be added eventually, but we haven’t had a need for it yet.

Creating Conversations

Creating a conversation at a user’s request is quite complicated. For starters the way to create a direct message or group direct message is different than how a channel is created.

To create a DM or Group DM, there are three virtual functions that need to be implemented. They are: Purple.ProtocolConversationInterface.get_create_conversation_details, Purple.ProtocolConversationInterface.create_conversation_async, and Purple.ProtocolConversationInterface.create_conversation_finish.

Purple.ProtocolConversationInterface.get_create_conversation_details asks the protocol to create a PurpleCreateConversationDetails. This method will typically be called by the user interface, but could also be called by anything that wants to create a conversation.

This is roughly an equivalent of the old chat_info_defaults virtual function but is a lot more rigid. However, as this is a GObject the protocol can store additional data on the returned object with g_object_set_data().

Once the caller has a PurpleCreateConversationDetails, it should set PurpleCreateConversationDetails:participants with the contacts to add to the conversation. Then Purple.ProtocolConversationInterface.create_conversation_async can be called with the details to create the conversation and of course call Purple.ProtocolConversationInterface.create_conversation_finish in the callback function to get the result.

Joining Channels

Similar to creating conversations, joining existing channels is a bit complicated. The old join_chat, chat_info, and chat_info_defaults virtual functions have been replaced by Purple.ProtocolConversationInterface.get_channel_join_details, Purple.ProtocolConversationInterface.join_channel_async, and Purple.ProtocolConversationInterface.join_channel_finish.

Purple.ProtocolConversationInterface.get_channel_join_details needs to return a PurpleChannelJoinDetails with specifics about what options the protocol supports when it comes to joining channels. A caller will fill out any necessary fields and call Purple.ProtocolConversationInterface.join_channel_async to join the channel.

Leaving Conversations

In Purple 2 you could really only leave a channel as most protocol didn’t have any sort of persistence for DMs. But that has obviously changed. To accommodate that, we now have the Purple.ProtocolConversationInterface.leave_conversation_async and Purple.ProtocolConversationInterface.leave_conversation_finish async pair which replaces the old chat_leave virtual function.

Refreshing Conversations

There are times when a conversation may need to be “refreshed”. The typical case here is when an account comes back online. Purple doesn’t know the state of an existing conversation when the account comes back online so it will call purple_protocol_conversation_refresh() to have the protocol do whatever it needs to bring a conversation back online.

Sending Messages

Since conversations are all represented by the same class the old send_id and send_chat virtual functions are no more. They are replaced by the async pair of Purple.ProtocolConversationInterface.send_message_async and Purple.ProtocolConversationInterface.send_message_finish.

Typing Notifications

Purple 2 only ever implemented typing notifications for DMs, but typing notifications now exist in all conversation types. As such we now have Purple.ProtocolConversationInterface.send_typing which replaces the old send_typing virtual function. Purple manages all of the state here and will call them when it changes.

Setting Avatars

Some protocols allow setting avatar for group DMs and channels. This was not supported in Purple 2 but is now available in Purple 3 via the async pair of Purple.ProtocolConversationInterface.set_avatar_async and Purple.ProtocolConversationInterface.set_avatar_finish.

Setting Descriptions

Some protocols allow setting a description for conversations. This is similar to a topic but usually allows for much more text. This was not supported in Purple 2 as it wasn’t really a thing back then.

To allow setting descriptions we have the async pair of Purple.ProtocolConversationInterface.set_description_async and Purple.ProtocolConversationInterface.set_description_finish.

Setting Titles

The title of a conversations is usually assigned by the server for DMs or the name of a channel that was set at creation time. However, group DMs are unique in that you can generally change their title whenever.

To allow the purple user to set the title we have an async pair of Purple.ProtocolConversationInterface.set_title_async and Purple.ProtocolConversationInterface.set_title_finish.

Setting Topics

Topics are commonly used to describe a discussion topic for a channel. Previously, this was implemented by the set_chat_topic virtual function but is now replaced by the Purple.ProtocolConversationInterface.set_topic_async and Purple.ProtocolConversationInterface.set_topic_finish async pair.

Protocol Directory

The protocol directory API is the interface to querying directories of contacts and channels on a server.

Searching Contacts

Many protocols now have a way to search contacts whether that’s publicly or from your contacts which is often used when creating Group DMs.

To support this Purple.ProtocolDirectoryInterface.search_contacts_async and Purple.ProtocolDirectoryInterface.search_contacts_finish have been created.

Searching Channels

The old roomlist API has already been removed, but will be replaced by virtual functions in PurpleProtocolDirectory once the design has been completed and implemented.

Protocol File Transfers

Like most protocol interfaces, the file transfers interface has been completely overhauled as well.

The new_xfer virtual function has been removed as PurpleFileTransfer should be able to fulfill any custom needs.

The can_receive_file virtual function has been replaced by the async pair of Purple.ProtocolFileTransferInterface.receive_async and Purple.ProtocolFileTransferInterface.receive_finish. The protocol will read the data from the client and store it into PurpleFileTransfer:local-file.

The send_file virtual function has been replaced by the async pair of Purple.ProtocolFileTransferInterface.send_async and Purple.ProtocolFileTransferInterface.send_finish. The protocol will read the data from PurpleFileTransfer:local-file and send it out to the remote side.

We may need to add more API here in the future for negotiations

Protocol Roster

Roster, or contact list, management has been broken out to its own interface as well. Purple 3 will cache everything between program invocations now, so protocols that don’t have a “server side” roster can skip this entirely unless they want to add a contact to the contact list by creating a PurplePerson for them.

For protocols that do have a “server side” roster, this interface is used to manage it.

Currently groups are only being implemented via PurpleTags with the name of group. This allows contacts to easily be in multiple groups and lets the user interfaces display contacts however they wish.

We will probably need to add an authorization request API here, but we haven’t had a need yet.

Add

The add_buddy virtual function has been replaced by an async pair, Purple.ProtocolRosterInterface.add_async and Purple.ProtocolRosterInterface.add_finish. This pair will ask the protocol to add a contact to your roster.

The add_buddies virtual function has been removed and there is currently no plan to add it back unless a good use case is discovered for it.

Update

The update async pair is used to update additional data about a contact like when a custom avatar or tags are changed for example. There could be more to this in the future.

Remove

The remove_buddy virtual function has been replace by an async pair, Purple.ProtocolRosterInterface.remove_async and Purple.ProtocolRosterInterface.remove_finish. These will ask the protocol to remove a contact from your roster.

The remove_buddies virtual function has been removed and there is no plan to add it back unless a good use case is discovered for it.

Protocol Whiteboard

The whiteboard API in general is best effort right now as we don’t have a way to test any of it. This is strictly due to time.

Create

The create virtual function is meant to create a protocol specific whiteboard, but hasn’t been looked at in a very long time as it even predates using PurpleContactInfo for all users.