Thu, 07 Aug 2025 21:32:18 -0500
Clean up and modernize PurpleImage
Testing Done:
Ran the tests under valgrind and called in the turtles.
Reviewed at https://reviews.imfreedom.org/r/4074/
/* * Purple - Internet Messaging Library * Copyright (C) Pidgin Developers <devel@pidgin.im> * * Purple is the legal property of its developers, whose names are too numerous * to list here. Please refer to the COPYRIGHT file distributed with this * source distribution. * * This library 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 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 General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this library; if not, see <https://www.gnu.org/licenses/>. */ #include <seagull.h> #include "purpleconversationmanager.h" #include "purpleconversationmanagerprivate.h" #include "core.h" #include "purpleaccount.h" #include "purpleaccountmanager.h" #include "purplecontact.h" #include "purpleui.h" #include "util.h" #ifdef G_LOG_DOMAIN #undef G_LOG_DOMAIN #endif /* G_LOG_DOMAIN */ #define G_LOG_DOMAIN "PurpleConversationManager" enum { PROP_0, PROP_FILENAME, PROP_ITEM_TYPE, PROP_N_ITEMS, N_PROPERTIES, }; static GParamSpec *properties[N_PROPERTIES] = {NULL, }; enum { SIG_ADDED, SIG_REMOVED, SIG_CONVERSATION_CHANGED, SIG_PRESENT_CONVERSATION, N_SIGNALS, }; static guint signals[N_SIGNALS] = {0, }; struct _PurpleConversationManager { GObject parent; char *filename; SeagullSqlite3 *db; gboolean database_initialized; SeagullStatement *delete_properties; SeagullStatement *delete_tags; SeagullStatement *insert_properties; SeagullStatement *insert_tags; SeagullStatement *query_properties; SeagullStatement *query_tags; GPtrArray *conversations; }; static PurpleConversationManager *default_manager = NULL; typedef gboolean (*PurpleConversationManagerCompareFunc)(PurpleConversation *conversation, gpointer userdata); #define RESOURCE_PATH "/im/pidgin/libpurple/conversationmanager" /****************************************************************************** * Helpers *****************************************************************************/ static void purple_conversation_manager_initialize_database(PurpleConversationManager *manager) { SeagullStatement *stmt = NULL; GError *error = NULL; gboolean result = FALSE; const char *migrations[] = { "01-initial.sql", NULL }; if(!purple_strempty(manager->filename)) { manager->db = seagull_db_new_from_file(manager->filename, &error); } else { manager->db = seagull_db_new_in_memory(&error); } if(error != NULL) { g_warning("failed to create database: %s", error->message); g_clear_error(&error); return; } /* Run the migrations. */ result = seagull_migrations_run_from_resources(manager->db, RESOURCE_PATH "/migrations", migrations, &error); if(!result) { g_warning("failed to run migrations: %s", error->message); g_clear_error(&error); return; } stmt = seagull_statement_new_from_resource(manager->db, RESOURCE_PATH "/statements/delete-properties.sql", &error); if(error != NULL) { g_warning("failed to load delete-properties statement: %s", error->message); g_clear_error(&error); return; } manager->delete_properties = stmt; stmt = seagull_statement_new_from_resource(manager->db, RESOURCE_PATH "/statements/delete-tags.sql", &error); if(error != NULL) { g_warning("failed to load delete-tags statement: %s", error->message); g_clear_error(&error); return; } manager->delete_tags = stmt; stmt = seagull_statement_new_from_resource(manager->db, RESOURCE_PATH "/statements/insert-properties.sql", &error); if(error != NULL) { g_warning("failed to load insert-properties statement: %s", error->message); g_clear_error(&error); return; } manager->insert_properties = stmt; stmt = seagull_statement_new_from_resource(manager->db, RESOURCE_PATH "/statements/insert-tags.sql", &error); if(error != NULL) { g_warning("failed to load insert-tags statement: %s", error->message); g_clear_error(&error); return; } manager->insert_tags = stmt; stmt = seagull_statement_new_from_resource(manager->db, RESOURCE_PATH "/statements/query-properties.sql", &error); if(error != NULL) { g_warning("failed to load query-properties statement: %s", error->message); g_clear_error(&error); return; } manager->query_properties = stmt; stmt = seagull_statement_new_from_resource(manager->db, RESOURCE_PATH "/statements/query-tags.sql", &error); if(error != NULL) { g_warning("failed to load query-tags statement: %s", error->message); g_clear_error(&error); return; } manager->query_tags = stmt; manager->database_initialized = TRUE; } static void purple_conversation_manager_set_filename(PurpleConversationManager *manager, const char *filename) { g_return_if_fail(PURPLE_IS_CONVERSATION_MANAGER(manager)); if(g_set_str(&manager->filename, filename)) { g_object_notify_by_pspec(G_OBJECT(manager), properties[PROP_FILENAME]); } /* The filename property is construct-only and nullable, so if it is set as * null, the g_set_str above won't return TRUE, but and we have to * initialize the database in any case so we just do it separately. * * Also, GObject will emit a Critical message if a construct only property * is set more than once and not call it again, so we don't need to * protect against that either. */ purple_conversation_manager_initialize_database(manager); } static gboolean purple_conversation_manager_delete_conversation_tags(PurpleConversationManager *manager, const char *conversation_id, const char *account_id, GError **error) { GError *local_error = NULL; gboolean success = FALSE; g_return_val_if_fail(PURPLE_IS_CONVERSATION_MANAGER(manager), FALSE); g_return_val_if_fail(!purple_strempty(conversation_id), FALSE); g_return_val_if_fail(!purple_strempty(account_id), FALSE); /* Clear any existing usage. */ if(!seagull_statement_clear_bindings(manager->delete_tags, error)) { return FALSE; } if(!seagull_statement_reset(manager->delete_tags, error)) { return FALSE; } /* Bind the conversation id to the delete statement. */ success = seagull_statement_bind_text(manager->delete_tags, ":conversation_id", conversation_id, -1, NULL, error); if(!success) { return FALSE; } /* Bind the account id to the delete statement. */ success = seagull_statement_bind_text(manager->delete_tags, ":account_id", account_id, -1, NULL, error); if(!success) { return FALSE; } seagull_statement_step(manager->delete_tags, &local_error); if(local_error != NULL) { g_propagate_error(error, local_error); return FALSE; } return TRUE; } static gboolean purple_conversation_manager_delete_conversation_properties(PurpleConversationManager *manager, const char *conversation_id, const char *account_id, GError **error) { GError *local_error = NULL; gboolean success = FALSE; g_return_val_if_fail(PURPLE_IS_CONVERSATION_MANAGER(manager), FALSE); g_return_val_if_fail(!purple_strempty(conversation_id), FALSE); g_return_val_if_fail(!purple_strempty(account_id), FALSE); /* Clear any existing usage. */ if(!seagull_statement_clear_bindings(manager->delete_properties, error)) { return FALSE; } if(!seagull_statement_reset(manager->delete_properties, error)) { return FALSE; } /* Bind the conversation id to the delete statement. */ success = seagull_statement_bind_text(manager->delete_properties, ":conversation_id", conversation_id, -1, NULL, error); if(!success) { return FALSE; } /* Bind the account id to the delete statement. */ success = seagull_statement_bind_text(manager->delete_properties, ":account_id", account_id, -1, NULL, error); if(!success) { return FALSE; } seagull_statement_step(manager->delete_properties, &local_error); if(local_error != NULL) { g_propagate_error(error, local_error); return FALSE; } return TRUE; } static void purple_conversation_manager_delete_conversation(PurpleConversationManager *manager, PurpleConversation *conversation) { PurpleAccount *account = NULL; GError *error = NULL; const char *conversation_id = NULL; const char *account_id = NULL; gboolean success = FALSE; if(!manager->database_initialized) { return; } /* Start a transaction so that everything is atomic. */ if(!seagull_transaction_begin(manager->db, &error)) { g_warning("failed to begin transaction to delete conversation: %s", error->message); g_clear_error(&error); return; } /* Grab the conversation and account ids as we need them for everything. */ conversation_id = purple_conversation_get_id(conversation); account = purple_conversation_get_account(conversation); account_id = purple_account_get_id(account); /* Delete the properties. We do this first, because it is used to restore * conversations. So if it fails to delete, we might be able to salvage a * rejoin as the other data still exists. This would then allow the user * to attempt to leave again, and hopefully it'll work that time. */ success = purple_conversation_manager_delete_conversation_properties(manager, conversation_id, account_id, &error); if(error != NULL || !success) { const char *message = "unknown reason"; if(error != NULL) { message = error->message; } g_warning("failed to delete properties: %s", message); g_clear_error(&error); if(!seagull_transaction_rollback(manager->db, &error)) { g_warning("failed to rollback transaction: %s", error->message); g_clear_error(&error); } return; } /* Delete the tags. */ success = purple_conversation_manager_delete_conversation_tags(manager, conversation_id, account_id, &error); if(error != NULL || !success) { const char *message = "unknown reason"; if(error != NULL) { message = error->message; } g_warning("failed to delete tags: %s", message); g_clear_error(&error); if(!seagull_transaction_rollback(manager->db, &error)) { g_warning("failed to rollback transaction: %s", error->message); g_clear_error(&error); } return; } /* Commit the transaction. */ if(!seagull_transaction_commit(manager->db, &error)) { g_warning("failed to commit transaction to delete conversation: %s", error->message); g_clear_error(&error); } } static gboolean purple_conversation_manager_save_conversation_properties(PurpleConversationManager *manager, PurpleConversation *conversation) { PurpleAccount *account = NULL; PurpleContactInfo *contact = NULL; GError *error = NULL; gboolean success = FALSE; /* Bind the properties for the conversation. */ success = seagull_statement_bind_object(manager->insert_properties, "conv_", G_OBJECT(conversation), &error); if(!success) { g_warning("failed to bind conversation to insert properties: %s", error->message); g_clear_error(&error); return FALSE; } /* Bind the account */ account = purple_conversation_get_account(conversation); success = seagull_statement_bind_text(manager->insert_properties, ":account_id", purple_account_get_id(account), -1, NULL, &error); if(!success) { g_warning("failed to bind account_id to insert properties: %s", error->message); g_clear_error(&error); return FALSE; } /* If we have a creator, bind it. */ contact = purple_conversation_get_creator(conversation); if(PURPLE_IS_CONTACT_INFO(contact)) { success = seagull_statement_bind_text(manager->insert_properties, ":creator_id", purple_contact_info_get_id(contact), -1, NULL, &error); } else { success = seagull_statement_bind_null(manager->insert_properties, ":creator_id", &error); } if(!success) { g_warning("failed to bind creator_id to insert properties: %s", error->message); g_clear_error(&error); return FALSE; } /* If we have a topic author, bind it. */ contact = purple_conversation_get_topic_author(conversation); if(PURPLE_IS_CONTACT_INFO(contact)) { success = seagull_statement_bind_text(manager->insert_properties, ":topic_author_id", purple_contact_info_get_id(contact), -1, NULL, &error); } else { success = seagull_statement_bind_null(manager->insert_properties, ":topic_author_id", &error); } if(!success) { g_warning("failed to bind creator_id to insert properties: %s", error->message); g_clear_error(&error); return FALSE; } /* The return value of step is whether or not there is data to read. Since * this is an insert statement, we can ignore it. */ seagull_statement_step(manager->insert_properties, &error); if(error != NULL) { g_warning("failed to step insert statement: %s", error->message); g_clear_error(&error); return FALSE; } return TRUE; } static gboolean purple_conversation_manager_save_conversation_tags(PurpleConversationManager *manager, PurpleConversation *conversation) { PurpleAccount *account = NULL; PurpleTags *tags = NULL; GError *error = NULL; const char *conversation_id = NULL; const char *account_id = NULL; gboolean success = FALSE; /* Grab the conversation and account ids as we need them for everything. */ conversation_id = purple_conversation_get_id(conversation); account = purple_conversation_get_account(conversation); account_id = purple_account_get_id(account); /* Delete the existing tags. */ success = purple_conversation_manager_delete_conversation_tags(manager, conversation_id, account_id, &error); if(error != NULL) { g_warning("failed to delete tags: %s", error->message); g_clear_error(&error); return FALSE; } if(!success) { g_warning("failed to delete tags: unknown reason"); return FALSE; } /* Bind the conversation id to the insert statement. */ success = seagull_statement_bind_text(manager->insert_tags, ":conversation_id", conversation_id, -1, NULL, &error); if(!success) { g_warning("failed to bind conversation_id to insert tags: %s", error->message); g_clear_error(&error); return FALSE; } /* Bind the account id to the insert statement. */ success = seagull_statement_bind_text(manager->insert_tags, ":account_id", account_id, -1, NULL, &error); if(!success) { g_warning("failed to bind account_id to insert tags: %s", error->message); g_clear_error(&error); return FALSE; } /* Now run through tags and insert them. */ tags = purple_conversation_get_tags(conversation); for(GList *l = purple_tags_get_all(tags); l != NULL; l = l->next) { const char *tag = l->data; success = seagull_statement_bind_text(manager->insert_tags, ":tag", tag, -1, NULL, &error); if(!success) { g_warning("failed to bind tag to insert tags: %s", error->message); g_clear_error(&error); return FALSE; } /* Step the statement. */ seagull_statement_step(manager->insert_tags, &error); if(error != NULL) { g_warning("sql: %s", seagull_statement_get_expanded_sql(manager->insert_tags)); g_warning("failed to insert tag: %s", error->message); g_clear_error(&error); return FALSE; } if(!seagull_statement_reset(manager->insert_tags, &error)) { g_warning("sql: %s", seagull_statement_get_expanded_sql(manager->insert_tags)); g_warning("failed to reset the insert tags statement: %s", error->message); g_clear_error(&error); return FALSE; } } return TRUE; } static void purple_conversation_manager_save_conversation(PurpleConversationManager *manager, PurpleConversation *conversation) { GError *error = NULL; gboolean success = FALSE; if(!manager->database_initialized) { return; } /* Start a transaction so that everything is atomic. */ if(!seagull_transaction_begin(manager->db, &error)) { g_warning("failed to begin transaction to save conversation: %s", error->message); g_clear_error(&error); return; } /* Save the conversation properties. */ success = purple_conversation_manager_save_conversation_properties(manager, conversation); if(!seagull_statement_clear_bindings(manager->insert_properties, &error)) { g_warning("failed to clear bindings on the insert properties " "statement: %s", error->message); g_clear_error(&error); } if(!seagull_statement_reset(manager->insert_properties, &error)) { g_warning("failed to reset the insert properties statement: %s", error->message); g_clear_error(&error); } if(!success) { if(!seagull_transaction_rollback(manager->db, &error)) { g_warning("failed to rollback transaction: %s", error->message); g_clear_error(&error); } return; } /* Save the conversation tags. */ success = purple_conversation_manager_save_conversation_tags(manager, conversation); if(!seagull_statement_clear_bindings(manager->insert_tags, &error)) { g_warning("failed to clear bindings on the insert tags statement: " "%s", error->message); g_clear_error(&error); } if(!seagull_statement_reset(manager->insert_tags, &error)) { g_warning("failed to reset the insert tags statement: %s", error->message); g_clear_error(&error); } if(!success) { if(!seagull_transaction_rollback(manager->db, &error)) { g_warning("failed to rollback transaction: %s", error->message); g_clear_error(&error); } return; } /* Commit the transaction. */ if(!seagull_transaction_commit(manager->db, &error)) { g_warning("failed to commit transaction to save conversation: %s", error->message); g_clear_error(&error); } } static void purple_conversation_manager_load_conversation_tags(PurpleConversationManager *manager, PurpleConversation *conversation, const char *account_id, const char *conversation_id, GError **error) { PurpleTags *tags = NULL; GError *local_error = NULL; tags = purple_conversation_get_tags(conversation); /* Conversations have some default tags when created, but we need to * replace everything so we clear those. */ purple_tags_remove_all(tags); /* Reset the statement. */ seagull_statement_reset(manager->query_tags, &local_error); if(local_error != NULL) { g_propagate_error(error, local_error); return; } /* Bind the account id. */ seagull_statement_bind_text(manager->query_tags, ":account_id", account_id, -1, NULL, &local_error); if(local_error != NULL) { g_propagate_error(error, local_error); return; } /* Bind the conversation id. */ seagull_statement_bind_text(manager->query_tags, ":conversation_id", conversation_id, -1, NULL, &local_error); if(local_error != NULL) { g_propagate_error(error, local_error); return; } while(seagull_statement_step(manager->query_tags, &local_error)) { const char *tag = NULL; tag = seagull_statement_column_text(manager->query_tags, "tag", &local_error); if(local_error != NULL) { g_propagate_error(error, local_error); return; } purple_tags_add(tags, tag); } if(local_error != NULL) { g_propagate_error(error, local_error); } } static void purple_conversation_manager_load_conversations(PurpleConversationManager *manager) { PurpleAccountManager *account_manager = NULL; GError *error = NULL; seagull_statement_reset(manager->query_properties, &error); if(error != NULL) { g_warning("failed to request query properties statement: %s", error->message); g_clear_error(&error); return; } account_manager = purple_account_manager_get_default(); while(seagull_statement_step(manager->query_properties, &error)) { PurpleAccount *account = NULL; PurpleConversation *conversation = NULL; const char *account_id = NULL; const char *conversation_id = NULL; gint type = PURPLE_CONVERSATION_TYPE_UNSET; gboolean federated = FALSE; /* Grab the account id and attempt to find the account. */ account_id = seagull_statement_column_text(manager->query_properties, "account_id", &error); if(error != NULL) { g_warning("failed to read account id: %s", error->message); g_clear_error(&error); continue; } account = purple_account_manager_find_by_id(account_manager, account_id); if(!PURPLE_IS_ACCOUNT(account)) { g_warning("failed to find account for id %s", account_id); continue; } conversation_id = seagull_statement_column_text(manager->query_properties, "conversation_id", &error); if(error != NULL) { g_warning("failed to read conversation id: %s", error->message); g_clear_error(&error); continue; } type = seagull_statement_column_enum(manager->query_properties, "type", &error); if(error != NULL) { g_warning("failed to read type: %s", error->message); g_clear_error(&error); continue; } federated = seagull_statement_column_int(manager->query_properties, "federated", &error); if(error != NULL) { g_warning("failed to read federated: %s", error->message); g_clear_error(&error); continue; } /* We have all the construct only properties, so let's create the * instance. */ conversation = g_object_new( PURPLE_TYPE_CONVERSATION, "account", account, "federated", federated, "id", conversation_id, "type", type, NULL); /* Now set the rest of the properties. */ seagull_statement_column_object(manager->query_properties, "conv_", G_OBJECT(conversation), &error); if(error != NULL) { g_warning("failed to read object properties: %s", error->message); g_clear_error(&error); g_clear_object(&conversation); continue; } purple_conversation_manager_load_conversation_tags(manager, conversation, account_id, conversation_id, &error); if(error != NULL) { g_warning("failed to load tags for conversation %s: %s", conversation_id, error->message); g_clear_error(&error); g_clear_object(&conversation); continue; } /* Finally, add the conversation to the manager. */ if(!purple_conversation_manager_add(manager, conversation)) { g_warning("failed to add conversation: %p", conversation); } g_clear_object(&conversation); } if(error != NULL) { g_warning("failed querying properties: %s", error->message); g_clear_error(&error); } } /****************************************************************************** * Callbacks *****************************************************************************/ static void purple_conversation_manager_conversation_changed_cb(GObject *source, GParamSpec *pspec, gpointer data) { purple_conversation_manager_save_conversation(data, PURPLE_CONVERSATION(source)); g_signal_emit(data, signals[SIG_CONVERSATION_CHANGED], g_param_spec_get_name_quark(pspec), source, pspec); } static void purple_conversation_manager_present_conversation_cb(PurpleConversation *conversation, gpointer data) { g_signal_emit(data, signals[SIG_PRESENT_CONVERSATION], 0, conversation); } /****************************************************************************** * GListModel Implementation *****************************************************************************/ static GType purple_conversation_manager_get_item_type(G_GNUC_UNUSED GListModel *list) { return PURPLE_TYPE_CONVERSATION; } static guint purple_conversation_manager_get_n_items(GListModel *list) { PurpleConversationManager *manager = PURPLE_CONVERSATION_MANAGER(list); return manager->conversations->len; } static gpointer purple_conversation_manager_get_item(GListModel *list, guint position) { PurpleConversationManager *manager = PURPLE_CONVERSATION_MANAGER(list); PurpleConversation *conversation = NULL; if(position < manager->conversations->len) { conversation = g_object_ref(g_ptr_array_index(manager->conversations, position)); } return conversation; } static void pidgin_conversation_manager_list_model_iface_init(GListModelInterface *iface) { iface->get_item_type = purple_conversation_manager_get_item_type; iface->get_n_items = purple_conversation_manager_get_n_items; iface->get_item = purple_conversation_manager_get_item; } /****************************************************************************** * GObject Implementation *****************************************************************************/ G_DEFINE_FINAL_TYPE_WITH_CODE(PurpleConversationManager, purple_conversation_manager, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL, pidgin_conversation_manager_list_model_iface_init)) static void purple_conversation_manager_finalize(GObject *obj) { PurpleConversationManager *manager = PURPLE_CONVERSATION_MANAGER(obj); g_clear_pointer(&manager->conversations, g_ptr_array_unref); g_clear_pointer(&manager->filename, g_free); g_clear_object(&manager->delete_properties); g_clear_object(&manager->delete_tags); g_clear_object(&manager->insert_properties); g_clear_object(&manager->insert_tags); g_clear_object(&manager->query_properties); g_clear_object(&manager->query_tags); if(manager->db != NULL) { GError *error = NULL; seagull_db_close(manager->db, &error); if(error != NULL) { g_warning("failed to close database: %s", error->message); g_clear_error(&error); } } G_OBJECT_CLASS(purple_conversation_manager_parent_class)->finalize(obj); } static void purple_conversation_manager_get_property(GObject *obj, guint param_id, GValue *value, GParamSpec *pspec) { PurpleConversationManager *manager = PURPLE_CONVERSATION_MANAGER(obj); switch(param_id) { case PROP_FILENAME: g_value_set_string(value, purple_conversation_manager_get_filename(manager)); break; case PROP_ITEM_TYPE: g_value_set_gtype(value, purple_conversation_manager_get_item_type(G_LIST_MODEL(manager))); break; case PROP_N_ITEMS: g_value_set_uint(value, purple_conversation_manager_get_n_items(G_LIST_MODEL(manager))); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); break; } } static void purple_conversation_manager_set_property(GObject *obj, guint param_id, const GValue *value, GParamSpec *pspec) { PurpleConversationManager *manager = PURPLE_CONVERSATION_MANAGER(obj); switch(param_id) { case PROP_FILENAME: purple_conversation_manager_set_filename(manager, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); break; } } static void purple_conversation_manager_init(PurpleConversationManager *manager) { manager->conversations = g_ptr_array_new_full(10, g_object_unref); manager->database_initialized = FALSE; } static void purple_conversation_manager_class_init(PurpleConversationManagerClass *klass) { GObjectClass *obj_class = G_OBJECT_CLASS(klass); obj_class->finalize = purple_conversation_manager_finalize; obj_class->get_property = purple_conversation_manager_get_property; obj_class->set_property = purple_conversation_manager_set_property; /** * PurpleConversationManager:filename: * * The filename that the manager should save its contents to. * * If this is %NULL, no serialization will be performed. * * Since: 3.0 */ properties[PROP_FILENAME] = g_param_spec_string( "filename", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); /** * PurpleConversationManager:item-type: * * The type of items. See [vfunc@Gio.ListModel.get_item_type]. * * Since: 3.0 */ properties[PROP_ITEM_TYPE] = g_param_spec_gtype( "item-type", NULL, NULL, G_TYPE_OBJECT, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * PurpleConversationManager:n-items: * * The number of items. See [vfunc@Gio.ListModel.get_n_items]. * * Since: 3.0 */ properties[PROP_N_ITEMS] = g_param_spec_uint( "n-items", NULL, NULL, 0, G_MAXUINT, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties(obj_class, N_PROPERTIES, properties); /** * PurpleConversationManager::added: * @manager: The manager. * @conversation: The conversation that was added. * * Emitted after @conversation has been added to @manager. * * Since: 3.0 */ signals[SIG_ADDED] = g_signal_new_class_handler( "added", G_OBJECT_CLASS_TYPE(klass), G_SIGNAL_RUN_LAST, NULL, NULL, NULL, NULL, G_TYPE_NONE, 1, PURPLE_TYPE_CONVERSATION); /** * PurpleConversationManager::removed: * @manager: The manager. * @conversation: The conversation that was removed. * * Emitted after @conversation has been removed from @manager. * * Since: 3.0 */ signals[SIG_REMOVED] = g_signal_new_class_handler( "removed", G_OBJECT_CLASS_TYPE(klass), G_SIGNAL_RUN_LAST, NULL, NULL, NULL, NULL, G_TYPE_NONE, 1, PURPLE_TYPE_CONVERSATION); /** * PurpleConversationManager::conversation-changed: * @manager: The account manager instance. * @conversation: The conversation that was changed. * @pspec: The [class@GObject.ParamSpec] for the property that changed. * * This is a propagation of the notify signal from @conversation. This * means that your callback will be called for any conversation that * @manager knows about. * * This also supports details, so you can specify the signal name as * something like `conversation-changed::title` and your callback will only * be called when [property@Conversation:title] has been changed. * * Since: 3.0 */ signals[SIG_CONVERSATION_CHANGED] = g_signal_new_class_handler( "conversation-changed", G_OBJECT_CLASS_TYPE(klass), G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, NULL, NULL, NULL, NULL, G_TYPE_NONE, 2, PURPLE_TYPE_CONVERSATION, G_TYPE_PARAM); /** * PurpleConversationManager::present-conversation: * @manager: The instance. * @conversation: The conversation that should be presented. * * This is a propagation of [signal@Conversation::present]. This means that * your callback will be called for any conversation that @manager knows * about. * * Since: 3.0 */ signals[SIG_PRESENT_CONVERSATION] = g_signal_new_class_handler( "present-conversation", G_OBJECT_CLASS_TYPE(klass), G_SIGNAL_RUN_LAST, NULL, NULL, NULL, NULL, G_TYPE_NONE, 1, PURPLE_TYPE_CONVERSATION); } /****************************************************************************** * Private API *****************************************************************************/ void purple_conversation_manager_startup(void) { if(default_manager == NULL) { PurpleUi *ui = purple_core_get_ui(); default_manager = purple_ui_get_conversation_manager(ui); if(PURPLE_IS_CONVERSATION_MANAGER(default_manager)) { g_object_add_weak_pointer(G_OBJECT(default_manager), (gpointer *)&default_manager); } } /* Load conversations. */ purple_conversation_manager_load_conversations(default_manager); } void purple_conversation_manager_shutdown(void) { g_clear_object(&default_manager); } /****************************************************************************** * Public API *****************************************************************************/ gboolean purple_conversation_manager_add(PurpleConversationManager *manager, PurpleConversation *conversation) { guint position = 0; g_return_val_if_fail(PURPLE_IS_CONVERSATION_MANAGER(manager), FALSE); g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE); if(g_ptr_array_find(manager->conversations, conversation, NULL)) { return FALSE; } position = manager->conversations->len; g_ptr_array_add(manager->conversations, g_object_ref(conversation)); /* Register our signals that need to be propagated. */ g_signal_connect_object(conversation, "notify", G_CALLBACK(purple_conversation_manager_conversation_changed_cb), manager, 0); g_signal_connect_object(conversation, "present", G_CALLBACK(purple_conversation_manager_present_conversation_cb), manager, 0); /* Tell everyone about the new conversation. */ g_signal_emit(manager, signals[SIG_ADDED], 0, conversation); g_list_model_items_changed(G_LIST_MODEL(manager), position, 0, 1); g_object_notify_by_pspec(G_OBJECT(manager), properties[PROP_N_ITEMS]); purple_conversation_manager_save_conversation(manager, conversation); return TRUE; } PurpleConversationManager * purple_conversation_manager_new(const char *filename) { return g_object_new( PURPLE_TYPE_CONVERSATION_MANAGER, "filename", filename, NULL); } PurpleConversationManager * purple_conversation_manager_get_default(void) { return default_manager; } GListModel * purple_conversation_manager_get_default_as_model(void) { if(PURPLE_IS_CONVERSATION_MANAGER(default_manager)) { return G_LIST_MODEL(default_manager); } return NULL; } const char * purple_conversation_manager_get_filename(PurpleConversationManager *manager) { g_return_val_if_fail(PURPLE_IS_CONVERSATION_MANAGER(manager), NULL); return manager->filename; } GListModel * purple_conversation_manager_get_all_for_account(PurpleConversationManager *manager, PurpleAccount *account) { GListStore *model = NULL; g_return_val_if_fail(PURPLE_IS_CONVERSATION_MANAGER(manager), NULL); g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL); model = g_list_store_new(PURPLE_TYPE_CONVERSATION); for(guint i = 0; i < manager->conversations->len; i++) { PurpleAccount *conv_account = NULL; PurpleConversation *conversation = NULL; conversation = g_ptr_array_index(manager->conversations, i); conv_account = purple_conversation_get_account(conversation); if(conv_account == account) { g_list_store_append(model, conversation); } } return G_LIST_MODEL(model); } PurpleConversation * purple_conversation_manager_find_dm(PurpleConversationManager *manager, PurpleContact *contact) { PurpleAccount *contact_account = NULL; PurpleContactInfo *info = NULL; g_return_val_if_fail(PURPLE_IS_CONVERSATION_MANAGER(manager), NULL); g_return_val_if_fail(PURPLE_IS_CONTACT(contact), NULL); info = PURPLE_CONTACT_INFO(contact); contact_account = purple_contact_get_account(contact); for(guint i = 0; i < manager->conversations->len; i++) { PurpleAccount *conversation_account = NULL; PurpleConversation *conversation = NULL; PurpleConversationMembers *members = NULL; conversation = g_ptr_array_index(manager->conversations, i); conversation_account = purple_conversation_get_account(conversation); if(conversation_account != contact_account) { continue; } if(!purple_conversation_is_dm(conversation)) { continue; } members = purple_conversation_get_members(conversation); if(purple_conversation_members_has_member(members, info, NULL)) { return conversation; } } return NULL; } PurpleConversation * purple_conversation_manager_find_with_id(PurpleConversationManager *manager, PurpleAccount *account, const char *id) { g_return_val_if_fail(PURPLE_IS_CONVERSATION_MANAGER(manager), NULL); g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL); for(guint i = 0; i < manager->conversations->len; i++) { PurpleConversation *conversation = NULL; conversation = g_ptr_array_index(manager->conversations, i); if(purple_conversation_get_account(conversation) != account) { continue; } if(purple_strequal(purple_conversation_get_id(conversation), id)) { return conversation; } } return NULL; } gboolean purple_conversation_manager_remove(PurpleConversationManager *manager, PurpleConversation *conversation) { guint position = 0; g_return_val_if_fail(PURPLE_IS_CONVERSATION_MANAGER(manager), FALSE); g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE); if(!g_ptr_array_find(manager->conversations, conversation, &position)) { return FALSE; } /* Make sure we have a reference in case we need to emit signals. */ g_object_ref(conversation); g_ptr_array_remove_index(manager->conversations, position); /* Disconnect all the signals we added for propagation. */ g_signal_handlers_disconnect_by_func(conversation, purple_conversation_manager_conversation_changed_cb, manager); g_signal_handlers_disconnect_by_func(conversation, purple_conversation_manager_present_conversation_cb, manager); /* Tell everyone about the removed conversation. */ g_signal_emit(manager, signals[SIG_REMOVED], 0, conversation); g_list_model_items_changed(G_LIST_MODEL(manager), position, 1, 0); g_object_notify_by_pspec(G_OBJECT(manager), properties[PROP_N_ITEMS]); /* Finally remove it from the database. */ purple_conversation_manager_delete_conversation(manager, conversation); g_object_unref(conversation); return TRUE; }