--- a/libpurple/purpleconversationmanager.c Mon Jul 07 21:22:37 2025 -0500 +++ b/libpurple/purpleconversationmanager.c Fri Jul 11 01:55:31 2025 -0500 @@ -20,6 +20,8 @@ * this library; if not, see <https://www.gnu.org/licenses/>. */ +#include <seagull.h> + #include "purpleconversationmanager.h" #include "purpleconversationmanagerprivate.h" @@ -50,6 +52,12 @@ GObject parent; char *filename; + SeagullSqlite3 *db; + gboolean database_initialized; + + SeagullStatement *delete_tags; + SeagullStatement *insert_properties; + SeagullStatement *insert_tags; GPtrArray *conversations; }; @@ -58,20 +66,422 @@ 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-tags.sql", + &error); + if(error != NULL) { + g_warning("failed to loaded 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 loaded 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 loaded insert-tags statement: %s", + error->message); + g_clear_error(&error); + + return; + } + manager->insert_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)) { + purple_conversation_manager_initialize_database(manager); + g_object_notify_by_pspec(G_OBJECT(manager), properties[PROP_FILENAME]); } } +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); + + /* 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) { + g_warning("failed to bind conversation_id to delete tags: %s", + error->message); + g_clear_error(&error); + + 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) { + g_warning("failed to bind account_id to delete tags: %s", + error->message); + g_clear_error(&error); + + return FALSE; + } + + /* Delete the existing tags, since we just replace everything. */ + seagull_statement_step(manager->delete_tags, &error); + if(error != NULL) { + g_warning("failed to delete tags: %s", error->message); + g_clear_error(&error); + + 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->delete_tags, + &error)) + { + g_warning("failed to clear bindings on the delete tags statement: " + "%s", + error->message); + g_clear_error(&error); + } + + if(!seagull_statement_reset(manager->delete_tags, &error)) { + g_warning("failed to reset the delete tags statement: %s", + error->message); + g_clear_error(&error); + } + + 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); + } +} + /****************************************************************************** * Callbacks *****************************************************************************/ @@ -80,6 +490,9 @@ 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); @@ -140,17 +553,26 @@ * GObject Implementation *****************************************************************************/ static void -purple_conversation_manager_init(PurpleConversationManager *manager) { - manager->conversations = g_ptr_array_new_full(10, g_object_unref); -} - -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->insert_properties); + g_clear_object(&manager->insert_tags); + g_clear_object(&manager->delete_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); } @@ -198,6 +620,12 @@ } 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); @@ -393,6 +821,8 @@ 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; }