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.
-
PurpleContacthas been replaced byPurplePersonwhich has also picked up responsibility for who shows up on your contact list. -
PurpleBuddyhas been replaced byPurpleContactand 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. -
PurpleConvChatBuddyhas been replaced byPurpleConversationMemberwhich has aPurpleContactand 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 devenv‘s. 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_spechas been removed and will be address inPurple.AccountSettingAvataras defined in PIDGIN-18125. -
list_emblemhas been removed as most of its past usages are now covered byPurpleBadges. -
status_textis no longer necessary as we have a presence for every contact including a status message. -
tooltip_texthas been removed and is unlikely to return. -
status_typesis no longer necessary due to the changes in the status API. -
set_infodoes not yet have a replacement. -
set_statushas been replaced byPurple.ConnectionClass.set_presence. -
set_idlelikeset_statusabove all of this information is available to the protocol whenever it needs it. -
change_passwordis unlikely to be replaced, at least directly as there are many different way to authenticate now. -
add_permit,add_deny,rem_permit,rem_deny, andset_permit_denyhave all been replaced byPurpleContactInfo:permission. -
keepaliveis 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_userandunregister_userwill 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, andget_cb_real_namehave been removed as they are now implemented viaPurpleConversationMemberwhich has aPurpleContactInfoand between the two everything about them should be available. -
group_buddyandrename_groupneed some redesigning yet. As mentioned above groups are essentially not implemented right now. -
buddy_freewill not be replaced.PurpleContactInfohas a lot of properties which hopefully means you won’t need to store much other data with them. However, since they areGObjects now, you can useg_object_set_data()andg_object_set_data_full()to attach data to them and have it automatically freed when they are destroyed. -
normalizeis 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 inPurpleContactInfo:idand the display name from the server should be stored inPurpleContactInfo: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_chatwill not be replaced.PurpleConversationManagerremembers 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, androomlist_room_serializeare going to be radically changed and replaced byPurpleProtocolDirectorybut we have not yet gotten to it. -
offline_messagewill not be replaced.purple_conversation_send_message_async()will return an error if the message could not be delivered. -
send_rawwill 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_attentionandget_attention_typeshave 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_tablewill not be replaced. This isn’t used by any in tree protocols and doesn’t look useful. -
initiate_mediaandget_media_capswill be replaced by a media API that we’re still in the process of designing. -
get_public_aliaswill not be replaced as the protocol should update thePurpleContactInfo:display-nameproperty when it needs to. -
add_buddy_with_inviteandadd_buddies_with_invitewere meant to be covered byPurpleProtocolRosterbut that does not handle authorization requests and will need to be revisited. -
chat_can_receive_filewill eventually be replaced by a property onPurpleConversationwhich the protocol will be responsible for setting. -
chat_send_filewill not be replaced. The protocol can create anPurpleAttachmentand add it to thePurpleMessage:attachmentsof aPurpleMessage.
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_ready() 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.