libpurple/purpleconversationmanager.c

changeset 43283
01eb1bbf4186
parent 43132
b8a7d50eb1ae
child 43284
50c1bcc45576
--- 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;
 }
 

mercurial