Create the new PurpleContactManager
Testing Done:
Ran the unit tests
Bugs closed: PIDGIN-17678
Reviewed at https://reviews.imfreedom.org/r/1850/
--- a/libpurple/core.c Wed Sep 28 19:32:50 2022 -0500
+++ b/libpurple/core.c Thu Sep 29 00:46:44 2022 -0500
@@ -174,6 +174,7 @@
purple_account_manager_startup();
+ purple_contact_manager_startup(); purple_savedstatuses_init();
purple_conversations_init();
@@ -255,6 +256,7 @@
+ purple_contact_manager_shutdown(); purple_account_manager_shutdown();
purple_credential_manager_shutdown();
purple_protocol_manager_shutdown();
--- a/libpurple/meson.build Wed Sep 28 19:32:50 2022 -0500
+++ b/libpurple/meson.build Thu Sep 29 00:46:44 2022 -0500
@@ -46,6 +46,7 @@
'purpleconnectionerrorinfo.c',
+ 'purplecontactmanager.c', 'purpleconversationmanager.c',
'purpleconversationuiops.c',
@@ -150,6 +151,7 @@
'purpleconnectionerrorinfo.h',
+ 'purplecontactmanager.h', 'purpleconversationmanager.h',
'purpleconversationuiops.h',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplecontactmanager.c Thu Sep 29 00:46:44 2022 -0500
@@ -0,0 +1,369 @@
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <https://www.gnu.org/licenses/>. +#include <glib/gi18n-lib.h> +#include "purplecontactmanager.h" +#include "purpleprivate.h" +static guint signals[N_SIGNALS] = {0, }; +struct _PurpleContactManager { +static PurpleContactManager *default_manager = NULL; +/****************************************************************************** + *****************************************************************************/ +purple_contact_manager_find_with_username_helper(gconstpointer a, + PurpleContact *contact_a = (gpointer)a; + PurpleContact *contact_b = (gpointer)b; + const gchar *username_a = NULL; + const gchar *username_b = NULL; + username_a = purple_contact_get_username(contact_a); + username_b = purple_contact_get_username(contact_b); + return purple_strequal(username_a, username_b); +purple_contact_manager_find_with_id_helper(gconstpointer a, gconstpointer b) { + PurpleContact *contact_a = (gpointer)a; + PurpleContact *contact_b = (gpointer)b; + const gchar *id_a = NULL; + const gchar *id_b = NULL; + id_a = purple_contact_get_id(contact_a); + id_b = purple_contact_get_id(contact_b); + return purple_strequal(id_a, id_b); +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +G_DEFINE_TYPE(PurpleContactManager, purple_contact_manager, G_TYPE_OBJECT) +purple_contact_manager_dispose(GObject *obj) { + PurpleContactManager *manager = NULL; + manager = PURPLE_CONTACT_MANAGER(obj); + g_hash_table_remove_all(manager->accounts); + G_OBJECT_CLASS(purple_contact_manager_parent_class)->dispose(obj); +purple_contact_manager_finalize(GObject *obj) { + PurpleContactManager *manager = NULL; + manager = PURPLE_CONTACT_MANAGER(obj); + g_clear_pointer(&manager->accounts, g_hash_table_destroy); + G_OBJECT_CLASS(purple_contact_manager_parent_class)->finalize(obj); +purple_contact_manager_init(PurpleContactManager *manager) { + manager->accounts = g_hash_table_new_full(g_direct_hash, g_direct_equal, + g_object_unref, g_object_unref); +purple_contact_manager_class_init(PurpleContactManagerClass *klass) { + GObjectClass *obj_class = G_OBJECT_CLASS(klass); + obj_class->dispose = purple_contact_manager_dispose; + obj_class->finalize = purple_contact_manager_finalize; + * PurpleContactManager::added: + * @manager: The instance. + * @contact: The [class@Purple.Contact] that was registered. + * Emitted after @contact has been added to @manager. + signals[SIG_ADDED] = g_signal_new_class_handler( + G_OBJECT_CLASS_TYPE(klass), + * PurpleContactManager::removed: + * @manager: The instance. + * @contact: The [class@Purple.Contact] that was removed. + * Emitted after @contact has been removed from @manager. + signals[SIG_REMOVED] = g_signal_new_class_handler( + G_OBJECT_CLASS_TYPE(klass), +/****************************************************************************** + *****************************************************************************/ +purple_contact_manager_startup(void) { + if(default_manager == NULL) { + default_manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL); + g_object_add_weak_pointer(G_OBJECT(default_manager), + (gpointer)&default_manager); +purple_contact_manager_shutdown(void) { + g_clear_object(&default_manager); +/****************************************************************************** + *****************************************************************************/ +purple_contact_manager_get_default(void) { + if(G_UNLIKELY(!PURPLE_IS_CONTACT_MANAGER(default_manager))) { + g_warning("The default contact manager was unexpectedly NULL"); + return default_manager; +purple_contact_manager_add(PurpleContactManager *manager, + PurpleContact *contact) + PurpleAccount *account = NULL; + GListStore *contacts = NULL; + gboolean added = FALSE; + g_return_if_fail(PURPLE_IS_CONTACT_MANAGER(manager)); + g_return_if_fail(PURPLE_IS_CONTACT(contact)); + account = purple_contact_get_account(contact); + contacts = g_hash_table_lookup(manager->accounts, account); + if(!G_IS_LIST_STORE(contacts)) { + contacts = g_list_store_new(PURPLE_TYPE_CONTACT); + g_hash_table_insert(manager->accounts, g_object_ref(account), contacts); + g_list_store_append(contacts, contact); + if(g_list_store_find(contacts, contact, NULL)) { + const gchar *username = purple_contact_get_username(contact); + const gchar *id = purple_contact_get_id(contact); + g_warning("double add detected for contact %s:%s", id, username); + g_list_store_append(contacts, contact); + g_signal_emit(manager, signals[SIG_ADDED], 0, contact); +purple_contact_manager_remove(PurpleContactManager *manager, + PurpleContact *contact) + PurpleAccount *account = NULL; + GListStore *contacts = NULL; + g_return_val_if_fail(PURPLE_IS_CONTACT_MANAGER(manager), FALSE); + g_return_val_if_fail(PURPLE_IS_CONTACT(contact), FALSE); + account = purple_contact_get_account(contact); + contacts = g_hash_table_lookup(manager->accounts, account); + if(!G_IS_LIST_STORE(contacts)) { + if(g_list_store_find(contacts, contact, &position)) { + gboolean removed = FALSE; + /* Ref the contact to make sure that the instance is valid when we emit + len = g_list_model_get_n_items(G_LIST_MODEL(contacts)); + g_list_store_remove(contacts, position); + if(g_list_model_get_n_items(G_LIST_MODEL(contacts)) < len) { + g_signal_emit(manager, signals[SIG_REMOVED], 0, contact); + g_object_unref(contact); +purple_contact_manager_remove_all(PurpleContactManager *manager, + PurpleAccount *account) + GListStore *contacts = NULL; + g_return_val_if_fail(PURPLE_IS_CONTACT_MANAGER(manager), FALSE); + g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), FALSE); + /* If there are any contacts for this account, manually iterate them and + * emit the removed signal. This is more efficient than calling remove on + * each one individually as that would require updating the backing + * GListStore for each individual removal. + contacts = g_hash_table_lookup(manager->accounts, account); + if(G_IS_LIST_STORE(contacts)) { + guint n_items = g_list_model_get_n_items(G_LIST_MODEL(contacts)); + for(guint i = 0; i < n_items; i++) { + PurpleContact *contact = NULL; + contact = g_list_model_get_item(G_LIST_MODEL(contacts), i); + g_signal_emit(manager, signals[SIG_REMOVED], 0, contact); + g_clear_object(&contact); + return g_hash_table_remove(manager->accounts, account); +purple_contact_manager_get_all(PurpleContactManager *manager, + PurpleAccount *account) + g_return_val_if_fail(PURPLE_IS_CONTACT_MANAGER(manager), FALSE); + g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), FALSE); + return g_hash_table_lookup(manager->accounts, account); +purple_contact_manager_find_with_username(PurpleContactManager *manager, + PurpleAccount *account, + PurpleContact *needle = NULL; + GListStore *contacts = NULL; + gboolean found = FALSE; + g_return_val_if_fail(PURPLE_IS_CONTACT_MANAGER(manager), FALSE); + g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), FALSE); + g_return_val_if_fail(username != NULL, FALSE); + contacts = g_hash_table_lookup(manager->accounts, account); + if(!G_IS_LIST_STORE(contacts)) { + needle = purple_contact_new(account, username); + found = g_list_store_find_with_equal_func(contacts, needle, + purple_contact_manager_find_with_username_helper, + g_clear_object(&needle); + return g_list_model_get_item(G_LIST_MODEL(contacts), position); +purple_contact_manager_find_with_id(PurpleContactManager *manager, + PurpleAccount *account, const gchar *id) + PurpleContact *needle = NULL; + GListStore *contacts = NULL; + gboolean found = FALSE; + g_return_val_if_fail(PURPLE_IS_CONTACT_MANAGER(manager), FALSE); + g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), FALSE); + g_return_val_if_fail(id != NULL, FALSE); + contacts = g_hash_table_lookup(manager->accounts, account); + if(!G_IS_LIST_STORE(contacts)) { + needle = g_object_new(PURPLE_TYPE_CONTACT, "account", account, "id", id, + found = g_list_store_find_with_equal_func(contacts, needle, + purple_contact_manager_find_with_id_helper, + g_clear_object(&needle); + return g_list_model_get_item(G_LIST_MODEL(contacts), position); --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplecontactmanager.h Thu Sep 29 00:46:44 2022 -0500
@@ -0,0 +1,146 @@
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <https://www.gnu.org/licenses/>. +#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION) +# error "only <pidgin.h> may be included directly" +#ifndef PURPLE_CONTACT_MANAGER_H +#define PURPLE_CONTACT_MANAGER_H +#include <glib-object.h> +#include <libpurple/account.h> +#include <libpurple/purplecontact.h> +#define PURPLE_TYPE_CONTACT_MANAGER (purple_contact_manager_get_type()) +G_DECLARE_FINAL_TYPE(PurpleContactManager, purple_contact_manager, PURPLE, + CONTACT_MANAGER, GObject) + * PurpleContactManager: + * A manager for [class@Purple.Contact]s. + * purple_contact_manager_get_default: + * Gets the default instance of [class@Purple.ContactManager]. + * Typically this will be the main way for everyone to access the contact + * Returns: (transfer none): The default [class@Purple.ContactManager] instance. +PurpleContactManager *purple_contact_manager_get_default(void); + * purple_contact_manager_add: + * @manager: The instance. + * @contact: (transfer none): The [class@Purple.Contact] to add. + * Adds @contact to @manager. If a contact with a matching account and id + * already exists, no action will be taken. +void purple_contact_manager_add(PurpleContactManager *manager, PurpleContact *contact); + * purple_contact_manager_remove: + * @manager: The instance. + * @contact: (transfer none): The [class@Purple.Contact] to remove. + * Attempts to remove @contact from @manager. + * Returns: If @contact is found and removed %TRUE will be returned otherwise + * %FALSE will be returned. +gboolean purple_contact_manager_remove(PurpleContactManager *manager, PurpleContact *contact); + * purple_contact_manager_remove_all: + * @manager: The instance. + * @account: The [class@Purple.Account] whose contacts to remove. + * Removes all of the contacts from @manager that belong to @account. + * Returns: %TRUE if anything was removed, %FALSE if nothing was removed. +gboolean purple_contact_manager_remove_all(PurpleContactManager *manager, PurpleAccount *account); + * purple_contact_manager_get_all: + * @manager: The instance. + * @account: The [class@Purple.Account] whose contacts to get. + * Gets a [iface@Gio.ListModel] of all contacts that belong to @account. + * Returns: (transfer none) (nullable): A [iface@Gio.ListModel] of all the + * contacts belonging to @account. +GListModel *purple_contact_manager_get_all(PurpleContactManager *manager, PurpleAccount *account); + * purple_contact_manager_find_with_username: + * @manager: The instance. + * @account: The [class@Purple.Account] whose contact to find. + * @username: The username of the contact to find. + * Looks for a [class@Purple.Contact] that belongs to @account with a username + * Returns: (transfer none): The [class@Purple.Contact] if found, otherwise +PurpleContact *purple_contact_manager_find_with_username(PurpleContactManager *manager, PurpleAccount *account, const gchar *username); + * purple_contact_manager_find_with_id: + * @manager: The instance. + * @account: The [class@Purple.Account] whose contact to find. + * @id: The id of the contact to find. + * Looks for a [class@Purple.Contact] that belongs to @account with a id of @id. + * Returns: (transfer none): The [class@Purple.Contact] if found, otherwise +PurpleContact *purple_contact_manager_find_with_id(PurpleContactManager *manager, PurpleAccount *account, const gchar *id); +#endif /* PURPLE_CONTACT_MANAGER_H */ --- a/libpurple/purpleprivate.h Wed Sep 28 19:32:50 2022 -0500
+++ b/libpurple/purpleprivate.h Thu Sep 29 00:46:44 2022 -0500
@@ -200,6 +200,24 @@
void purple_account_manager_shutdown(void);
+ * purple_contact_manager_startup: + * Starts up the contact manager by creating the default instance. +void purple_contact_manager_startup(void); + * purple_contact_manager_shutdown: + * Shuts down the contact manager by destroying the default instance. +void purple_contact_manager_shutdown(void); * purple_conversation_manager_startup:
* Starts up the conversation manager by creating the default instance.
--- a/libpurple/tests/meson.build Wed Sep 28 19:32:50 2022 -0500
+++ b/libpurple/tests/meson.build Thu Sep 29 00:46:44 2022 -0500
@@ -4,6 +4,7 @@
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/test_contact_manager.c Thu Sep 29 00:46:44 2022 -0500
@@ -0,0 +1,290 @@
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <https://www.gnu.org/licenses/>. +/****************************************************************************** + *****************************************************************************/ +test_purple_contact_manager_increment_cb(G_GNUC_UNUSED PurpleContactManager *manager, + G_GNUC_UNUSED PurpleContact *contact, +/****************************************************************************** + *****************************************************************************/ +test_purple_contact_manager_get_default(void) { + PurpleContactManager *manager1 = NULL, *manager2 = NULL; + manager1 = purple_contact_manager_get_default(); + g_assert_true(PURPLE_IS_CONTACT_MANAGER(manager1)); + manager2 = purple_contact_manager_get_default(); + g_assert_true(PURPLE_IS_CONTACT_MANAGER(manager2)); + g_assert_true(manager1 == manager2); +test_purple_contact_manager_add_remove(void) { + PurpleAccount *account = NULL; + PurpleContactManager *manager = NULL; + PurpleContact *contact = NULL; + GListModel *contacts = NULL; + gint added_called = 0, removed_called = 0; + manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL); + g_assert_true(PURPLE_IS_CONTACT_MANAGER(manager)); + /* Wire up our signals. */ + g_signal_connect(manager, "added", + G_CALLBACK(test_purple_contact_manager_increment_cb), + g_signal_connect(manager, "removed", + G_CALLBACK(test_purple_contact_manager_increment_cb), + /* Create our account. */ + account = purple_account_new("test", "test"); + /* Create the contact and add it to the manager. */ + contact = purple_contact_new(account, "test-user"); + /* Add the contact to the manager. */ + purple_contact_manager_add(manager, contact); + /* Make sure the contact is available. */ + contacts = purple_contact_manager_get_all(manager, account); + g_assert_nonnull(contacts); + g_assert_cmpuint(g_list_model_get_n_items(contacts), ==, 1); + /* Make sure the added signal was called. */ + g_assert_cmpint(added_called, ==, 1); + /* Remove the contact. */ + purple_contact_manager_remove(manager, contact); + g_assert_cmpint(removed_called, ==, 1); + g_clear_object(&contact); + g_clear_object(&account); + g_clear_object(&manager); +test_purple_contact_manager_double_add(void) { + if(g_test_subprocess()) { + PurpleAccount *account = NULL; + PurpleContactManager *manager = NULL; + PurpleContact *contact = NULL; + manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL); + account = purple_account_new("test", "test"); + contact = purple_contact_new(account, "test-user"); + purple_contact_manager_add(manager, contact); + purple_contact_manager_add(manager, contact); + /* This will never get called as the double add outputs a g_warning() + * that causes the test to fail. This is left to avoid a false postive + g_clear_object(&account); + g_clear_object(&contact); + g_clear_object(&manager); + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_stderr("*Purple-WARNING*double add detected for contact*"); +test_purple_contact_manager_double_remove(void) { + PurpleAccount *account = NULL; + PurpleContactManager *manager = NULL; + PurpleContact *contact = NULL; + gint removed_called = 0; + manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL); + g_signal_connect(manager, "removed", + G_CALLBACK(test_purple_contact_manager_increment_cb), + account = purple_account_new("test", "test"); + contact = purple_contact_new(account, "test-user"); + purple_contact_manager_add(manager, contact); + g_assert_true(purple_contact_manager_remove(manager, contact)); + g_assert_false(purple_contact_manager_remove(manager, contact)); + g_assert_cmpint(removed_called, ==, 1); + g_clear_object(&account); + g_clear_object(&contact); + g_clear_object(&manager); +test_purple_contact_manager_remove_all(void) { + PurpleAccount *account = NULL; + PurpleContact *contact = NULL; + PurpleContactManager *manager = NULL; + GListModel *contacts = NULL; + manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL); + account = purple_account_new("test", "test"); + contacts = purple_contact_manager_get_all(manager, account); + g_assert_null(contacts); + contact = purple_contact_new(account, "test-user"); + purple_contact_manager_add(manager, contact); + contacts = purple_contact_manager_get_all(manager, account); + g_assert_nonnull(contacts); + g_assert_cmpuint(g_list_model_get_n_items(contacts), ==, 1); + g_assert_true(purple_contact_manager_remove_all(manager, account)); + contacts = purple_contact_manager_get_all(manager, account); + g_assert_null(contacts); + g_clear_object(&account); + g_clear_object(&contact); + g_clear_object(&manager); +test_purple_contact_manager_find_with_username(void) { + PurpleAccount *account = NULL; + PurpleContact *contact1 = NULL; + PurpleContact *contact2 = NULL; + PurpleContact *found = NULL; + PurpleContactManager *manager = NULL; + manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL); + account = purple_account_new("test", "test"); + contact1 = purple_contact_new(account, "user1"); + purple_contact_manager_add(manager, contact1); + contact2 = purple_contact_new(account, "user2"); + purple_contact_manager_add(manager, contact2); + found = purple_contact_manager_find_with_username(manager, account, + g_assert_nonnull(found); + g_assert_true(found == contact1); + g_clear_object(&found); + found = purple_contact_manager_find_with_username(manager, account, + g_assert_nonnull(found); + g_assert_true(found == contact2); + g_clear_object(&found); + g_clear_object(&account); + g_clear_object(&contact1); + g_clear_object(&contact2); + g_clear_object(&manager); +test_purple_contact_manager_find_with_id(void) { + PurpleAccount *account = NULL; + PurpleContact *contact1 = NULL; + PurpleContact *contact2 = NULL; + PurpleContact *found = NULL; + PurpleContactManager *manager = NULL; + manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL); + account = purple_account_new("test", "test"); + contact1 = g_object_new(PURPLE_TYPE_CONTACT, "account", account, "id", + purple_contact_manager_add(manager, contact1); + contact2 = g_object_new(PURPLE_TYPE_CONTACT, "account", account, "id", + purple_contact_manager_add(manager, contact2); + found = purple_contact_manager_find_with_id(manager, account, "id-1"); + g_assert_nonnull(found); + g_assert_true(found == contact1); + g_clear_object(&found); + found = purple_contact_manager_find_with_id(manager, account, "id-2"); + g_assert_nonnull(found); + g_assert_true(found == contact2); + g_clear_object(&found); + g_clear_object(&account); + g_clear_object(&contact1); + g_clear_object(&contact2); + g_clear_object(&manager); +/****************************************************************************** + *****************************************************************************/ +main(gint argc, gchar *argv[]) { + g_test_init(&argc, &argv, NULL); + g_test_add_func("/contact-manager/get-default", + test_purple_contact_manager_get_default); + g_test_add_func("/contact-manager/add-remove", + test_purple_contact_manager_add_remove); + g_test_add_func("/contact-manager/double-add", + test_purple_contact_manager_double_add); + g_test_add_func("/contact-manager/double-remove", + test_purple_contact_manager_double_remove); + g_test_add_func("/contact-manager/remove-all", + test_purple_contact_manager_remove_all); + g_test_add_func("/contact-manager/find/with-username", + test_purple_contact_manager_find_with_username); + g_test_add_func("/contact-manager/find/with-id", + test_purple_contact_manager_find_with_id);