Load conversations back in from the database

Fri, 11 Jul 2025 02:46:28 -0500

author
Gary Kramlich <grim@reaperworld.com>
date
Fri, 11 Jul 2025 02:46:28 -0500
changeset 43284
50c1bcc45576
parent 43283
01eb1bbf4186
child 43285
acde304cf24c

Load conversations back in from the database

This just handles the conversations and most of their properties including
their tags.

Testing Done:
Verified that conversations were created by starting pidgin 3 with `-n` and that their properties were correct. Then started pidgin 3 normally and verified that all the conversations were brought back online and worked properly.

Also called in the turtles.

Bugs closed: PIDGIN-17989

Reviewed at https://reviews.imfreedom.org/r/4051/

libpurple/purpleconversationmanager.c file | annotate | diff | comparison | revisions
libpurple/resources/conversationmanager/statements/query-properties.sql file | annotate | diff | comparison | revisions
libpurple/resources/conversationmanager/statements/query-tags.sql file | annotate | diff | comparison | revisions
libpurple/resources/libpurple.gresource.xml file | annotate | diff | comparison | revisions
libpurple/tests/test_conversation_manager.c file | annotate | diff | comparison | revisions
--- a/libpurple/purpleconversationmanager.c	Fri Jul 11 01:55:31 2025 -0500
+++ b/libpurple/purpleconversationmanager.c	Fri Jul 11 02:46:28 2025 -0500
@@ -26,10 +26,17 @@
 #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,
@@ -58,6 +65,8 @@
 	SeagullStatement *delete_tags;
 	SeagullStatement *insert_properties;
 	SeagullStatement *insert_tags;
+	SeagullStatement *query_properties;
+	SeagullStatement *query_tags;
 
 	GPtrArray *conversations;
 };
@@ -145,6 +154,30 @@
 	}
 	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 loaded 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 loaded query-tags statement: %s",
+		          error->message);
+		g_clear_error(&error);
+
+		return;
+	}
+	manager->query_tags = stmt;
+
 	manager->database_initialized = TRUE;
 }
 
@@ -155,10 +188,18 @@
 	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]);
 	}
+
+	/* 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
@@ -482,6 +523,193 @@
 	}
 }
 
+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
  *****************************************************************************/
@@ -549,9 +777,6 @@
                               G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL,
                                                     pidgin_conversation_manager_list_model_iface_init))
 
-/******************************************************************************
- * GObject Implementation
- *****************************************************************************/
 static void
 purple_conversation_manager_finalize(GObject *obj) {
 	PurpleConversationManager *manager = PURPLE_CONVERSATION_MANAGER(obj);
@@ -562,6 +787,8 @@
 	g_clear_object(&manager->insert_properties);
 	g_clear_object(&manager->insert_tags);
 	g_clear_object(&manager->delete_tags);
+	g_clear_object(&manager->query_properties);
+	g_clear_object(&manager->query_tags);
 
 	if(manager->db != NULL) {
 		GError *error = NULL;
@@ -782,6 +1009,9 @@
 			                          (gpointer *)&default_manager);
 		}
 	}
+
+	/* Load conversations. */
+	purple_conversation_manager_load_conversations(default_manager);
 }
 
 void
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/resources/conversationmanager/statements/query-properties.sql	Fri Jul 11 02:46:28 2025 -0500
@@ -0,0 +1,21 @@
+SELECT
+  account_id, -- construct only
+  age_restricted AS conv_age_restricted,
+  alias AS conv_alias,
+  conversation_id, -- construct only
+  created_on AS conv_created_on,
+  creator_id,
+  description AS conv_description,
+  drafting AS conv_drafting,
+  favorite AS conv_favorite,
+  federated, -- construct only
+  logging AS conv_logging,
+  needs_attention AS conv_needs_attention,
+  title AS conv_title,
+  topic AS conv_topic,
+  topic_author_id,
+  topic_updated AS conv_topic_updated,
+  type, -- construct only
+  url AS conv_url,
+  user_nickname AS conv_nickname
+FROM conversations
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/resources/conversationmanager/statements/query-tags.sql	Fri Jul 11 02:46:28 2025 -0500
@@ -0,0 +1,7 @@
+SELECT
+  tag
+FROM conversation_tags
+WHERE
+  account_id = :account_id
+AND
+  conversation_id = :conversation_id
--- a/libpurple/resources/libpurple.gresource.xml	Fri Jul 11 01:55:31 2025 -0500
+++ b/libpurple/resources/libpurple.gresource.xml	Fri Jul 11 02:46:28 2025 -0500
@@ -5,6 +5,8 @@
     <file compressed="true">conversationmanager/statements/delete-tags.sql</file>
     <file compressed="true">conversationmanager/statements/insert-properties.sql</file>
     <file compressed="true">conversationmanager/statements/insert-tags.sql</file>
+    <file compressed="true">conversationmanager/statements/query-properties.sql</file>
+    <file compressed="true">conversationmanager/statements/query-tags.sql</file>
     <file compressed="true">sqlitehistoryadapter/01-schema.sql</file>
   </gresource>
 </gresources>
--- a/libpurple/tests/test_conversation_manager.c	Fri Jul 11 01:55:31 2025 -0500
+++ b/libpurple/tests/test_conversation_manager.c	Fri Jul 11 02:46:28 2025 -0500
@@ -104,6 +104,7 @@
 	conversation = g_object_new(
 		PURPLE_TYPE_CONVERSATION,
 		"account", account,
+		"id", "test-conversation",
 		NULL);
 
 	/* Add the conversation to the manager. */
@@ -174,6 +175,7 @@
 	conversation = g_object_new(
 		PURPLE_TYPE_CONVERSATION,
 		"account", account,
+		"id", "test-conversation",
 		NULL);
 	purple_conversation_manager_add(manager, conversation);
 
@@ -223,6 +225,7 @@
 	conversation = g_object_new(
 		PURPLE_TYPE_CONVERSATION,
 		"account", account,
+		"id", "test-conversation",
 		"type", PURPLE_CONVERSATION_TYPE_DM,
 		NULL);
 
@@ -283,6 +286,7 @@
 	conversation1 = g_object_new(
 		PURPLE_TYPE_CONVERSATION,
 		"account", account,
+		"id", "test-conversation",
 		"type", PURPLE_CONVERSATION_TYPE_DM,
 		NULL);
 	purple_conversation_manager_add(manager, conversation1);
@@ -337,6 +341,7 @@
 	conversation1 = g_object_new(
 		PURPLE_TYPE_CONVERSATION,
 		"account", account1,
+		"id", "test-conversation1",
 		"type", PURPLE_CONVERSATION_TYPE_CHANNEL,
 		NULL);
 	purple_conversation_manager_add(manager, conversation1);
@@ -347,6 +352,7 @@
 	conversation2 = g_object_new(
 		PURPLE_TYPE_CONVERSATION,
 		"account", account1,
+		"id", "test-conversation2",
 		"type", PURPLE_CONVERSATION_TYPE_DM,
 		NULL);
 	purple_conversation_manager_add(manager, conversation2);
@@ -355,6 +361,7 @@
 	conversation3 = g_object_new(
 		PURPLE_TYPE_CONVERSATION,
 		"account", account2,
+		"id", "test-conversation3",
 		"type", PURPLE_CONVERSATION_TYPE_CHANNEL,
 		NULL);
 	purple_conversation_manager_add(manager, conversation3);

mercurial