pidgin/pidgin

741992355ead
Parents a7d2978df0b6
Children fa2d6b0a4912
GSoC History API including sqlite history adapter

The History API has been created to drive all message handling in purple3. It will be used to update existing messages for edits, reactions, pinning, read/deliver receipts, etc. The API uses an adapter pattern, to abstract out backends, but provides a SQLite3 backend by default.

It also provides search capabilities using a custom query language that can easily be expanded over time. It will be use by both the end user to search messages and the frontends to implement features like a pinned messages button. A command line utility is also provided for searching outside of the program itself.

## Remaining Items

**These all will most likely be done by the Pidgin core team after GSoC when we figure out exactly how to solve them.**

Need to store database in purple config directory
* Gary has spent some time looking at this and it looks like the purple-history cli will need to become a purple-ui to make this work write as in the future other adapters will be plugins.

Other things to consider:
- For simplicity, the SqliteHistoryAdapter is parsing the query itself, but for consistency having `PurpleHistoryAdapter` parse the query and pass tokens to the subclass might be something we want to do.

Testing Done:
## Unit Tests
History Manager
History Adapter

## Integration Tests
purplehistorycore created for integration tests.
PurpleSqliteHistoryAdapter functionality tested:
- Creates proper db schema
- Writes logs
- Reads logs
- Queries using query language
- Deletes using query language

Bugs closed: PIDGIN-17526, PIDGIN-17532, PIDGIN-17533, PIDGIN-17534

Reviewed at https://reviews.imfreedom.org/r/877/
--- a/doc/reference/libpurple/libpurple-docs.xml Mon Oct 11 23:47:26 2021 -0500
+++ b/doc/reference/libpurple/libpurple-docs.xml Tue Oct 12 00:50:59 2021 -0500
@@ -73,6 +73,8 @@
<xi:include href="xml/purpleconversationmanager.xml" />
<xi:include href="xml/purpleconversationuiops.xml" />
<xi:include href="xml/purpledebugui.xml" />
+ <xi:include href="xml/purplehistoryadapter.xml" />
+ <xi:include href="xml/purplehistorymanager.xml" />
<xi:include href="xml/purpleimconversation.xml" />
<xi:include href="xml/purplekeyvaluepair.xml" />
<xi:include href="xml/purplemarkup.xml" />
--- a/libpurple/core.c Mon Oct 11 23:47:26 2021 -0500
+++ b/libpurple/core.c Tue Oct 12 00:50:59 2021 -0500
@@ -173,6 +173,7 @@
purple_whiteboard_manager_startup();
purple_blist_init();
purple_log_init();
+ purple_history_manager_startup();
purple_network_init();
purple_proxy_init();
purple_stun_init();
@@ -249,6 +250,8 @@
purple_protocol_manager_shutdown();
purple_cmds_uninit();
+ purple_history_manager_shutdown();
+
purple_log_uninit();
/* Everything after util_uninit cannot try to write things to the
* confdir.
--- a/libpurple/meson.build Mon Oct 11 23:47:26 2021 -0500
+++ b/libpurple/meson.build Tue Oct 12 00:50:59 2021 -0500
@@ -53,6 +53,8 @@
'purplecredentialmanager.c',
'purplecredentialprovider.c',
'purpledebugui.c',
+ 'purplehistoryadapter.c',
+ 'purplehistorymanager.c',
'purpleimconversation.c',
'purplekeyvaluepair.c',
'purplemarkup.c',
@@ -71,6 +73,7 @@
'purpleprotocolprivacy.c',
'purpleprotocolroomlist.c',
'purpleprotocolserver.c',
+ 'purplesqlitehistoryadapter.c',
'purpleuiinfo.c',
'purplewhiteboard.c',
'purplewhiteboardmanager.c',
@@ -144,6 +147,8 @@
'purplecredentialmanager.h',
'purplecredentialprovider.h',
'purpledebugui.h',
+ 'purplehistoryadapter.h',
+ 'purplehistorymanager.h',
'purpleimconversation.h',
'purpleattachment.h',
'purplekeyvaluepair.h',
@@ -163,6 +168,7 @@
'purpleprotocolprivacy.h',
'purpleprotocolroomlist.h',
'purpleprotocolserver.h',
+ 'purplesqlitehistoryadapter.h',
'purpleuiinfo.h',
'purplewhiteboard.h',
'purplewhiteboardmanager.h',
@@ -190,6 +196,12 @@
purple_generated_sources = []
+purple_resource = gnome.compile_resources('purpleresources',
+ 'resources/libpurple.gresource.xml',
+ source_dir : 'resources',
+ c_name : 'purple')
+purple_coresources += purple_resource
+
purple_filebase = 'purple-@0@'.format(purple_major_version)
purple_include_base = purple_filebase / 'libpurple'
@@ -289,7 +301,7 @@
dependencies : # static_link_libs
[dnsapi, ws2_32, glib, gio, gplugin_dep, libsoup,
libxml, farstream, gstreamer, gstreamer_video,
- gstreamer_app, json, math])
+ gstreamer_app, json, sqlite3, math])
install_headers(purple_coreheaders,
subdir : purple_include_base)
--- a/libpurple/purpleconversation.c Mon Oct 11 23:47:26 2021 -0500
+++ b/libpurple/purpleconversation.c Tue Oct 12 00:50:59 2021 -0500
@@ -33,6 +33,7 @@
#include "purpleconversation.h"
#include "purpleconversationmanager.h"
#include "purpleenums.h"
+#include "purplehistorymanager.h"
#include "purplemarkup.h"
#include "purpleprivate.h"
#include "purpleprotocolclient.h"
@@ -291,8 +292,13 @@
g_object_get(object, "account", &account, NULL);
gc = purple_account_get_connection(account);
- /* copy features from the connection. */
- purple_conversation_set_features(conv, purple_connection_get_flags(gc));
+ /* Check if we have a connection before we use it. The unit tests are one
+ * case where we will not have a connection.
+ */
+ if(PURPLE_IS_CONNECTION(gc)) {
+ purple_conversation_set_features(conv,
+ purple_connection_get_flags(gc));
+ }
/* add the conversation to the appropriate lists */
manager = purple_conversation_manager_get_default();
@@ -745,7 +751,21 @@
{
GList *log;
GDateTime *dt;
+ GError *error = NULL;
+ PurpleHistoryManager *manager = NULL;
+ manager = purple_history_manager_get_default();
+ /* We should probably handle this error somehow, but I don't think that
+ * spamming purple_debug_warning is necessarily the right call.
+ */
+ if(!purple_history_manager_write(manager, conv, pmsg, &error)){
+ purple_debug_info("conversation", "history manager write returned error: %s", error->message);
+
+ g_clear_error(&error);
+ }
+
+
+ /* The following should be deleted when the history api is stable. */
dt = g_date_time_ref(purple_message_get_timestamp(pmsg));
log = priv->logs;
while(log != NULL) {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplehistoryadapter.c Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,301 @@
+/*
+ * 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/>.
+ */
+
+#include "purplehistoryadapter.h"
+
+#include "purpleprivate.h"
+
+typedef struct {
+ gchar *id;
+ gchar *name;
+} PurpleHistoryAdapterPrivate;
+
+enum {
+ PROP_0,
+ PROP_ID,
+ PROP_NAME,
+ N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE(PurpleHistoryAdapter,
+ purple_history_adapter, G_TYPE_OBJECT)
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+purple_history_adapter_set_id(PurpleHistoryAdapter *adapter, const gchar *id) {
+ PurpleHistoryAdapterPrivate *priv = NULL;
+
+ priv = purple_history_adapter_get_instance_private(adapter);
+
+ g_free(priv->id);
+ priv->id = g_strdup(id);
+
+ g_object_notify_by_pspec(G_OBJECT(adapter), properties[PROP_ID]);
+}
+
+static void
+purple_history_adapter_set_name(PurpleHistoryAdapter *adapter,
+ const gchar *name)
+{
+ PurpleHistoryAdapterPrivate *priv = NULL;
+
+ priv = purple_history_adapter_get_instance_private(adapter);
+
+ g_free(priv->name);
+ priv->name = g_strdup(name);
+
+ g_object_notify_by_pspec(G_OBJECT(adapter), properties[PROP_NAME]);
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+static void
+purple_history_adapter_get_property(GObject *obj, guint param_id,
+ GValue *value, GParamSpec *pspec)
+{
+ PurpleHistoryAdapter *adapter = PURPLE_HISTORY_ADAPTER(obj);
+
+ switch(param_id) {
+ case PROP_ID:
+ g_value_set_string(value,
+ purple_history_adapter_get_id(adapter));
+ break;
+ case PROP_NAME:
+ g_value_set_string(value,
+ purple_history_adapter_get_name(adapter));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+ break;
+ }
+}
+
+static void
+purple_history_adapter_set_property(GObject *obj, guint param_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ PurpleHistoryAdapter *adapter = PURPLE_HISTORY_ADAPTER(obj);
+
+ switch(param_id) {
+ case PROP_ID:
+ purple_history_adapter_set_id(adapter,
+ g_value_get_string(value));
+ break;
+ case PROP_NAME:
+ purple_history_adapter_set_name(adapter,
+ g_value_get_string(value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+ break;
+ }
+}
+
+static void
+purple_history_adapter_finalize(GObject *obj) {
+ PurpleHistoryAdapter *adapter = NULL;
+ PurpleHistoryAdapterPrivate *priv = NULL;
+
+ adapter = PURPLE_HISTORY_ADAPTER(obj);
+ priv = purple_history_adapter_get_instance_private(adapter);
+
+ g_clear_pointer(&priv->id, g_free);
+ g_clear_pointer(&priv->name, g_free);
+
+ G_OBJECT_CLASS(purple_history_adapter_parent_class)->finalize(obj);
+}
+
+static void
+purple_history_adapter_init(PurpleHistoryAdapter *adapter) {
+}
+
+static void
+purple_history_adapter_class_init(PurpleHistoryAdapterClass *klass) {
+ GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+ obj_class->get_property = purple_history_adapter_get_property;
+ obj_class->set_property = purple_history_adapter_set_property;
+ obj_class->finalize = purple_history_adapter_finalize;
+
+ /**
+ * PurpleHistoryAdapter::id:
+ *
+ * The ID of the adapter. Used for preferences and other things that need
+ * to address it.
+ *
+ * Since: 3.0.0
+ */
+ properties[PROP_ID] = g_param_spec_string(
+ "id", "id", "The identifier of the adapter",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS
+ );
+
+ /**
+ * PurpleHistoryAdapter::name:
+ *
+ * The name of the adapter.
+ *
+ * Since: 3.0.0
+ */
+ properties[PROP_NAME] = g_param_spec_string(
+ "name", "name", "The name of the adapter",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS
+ );
+
+ g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+}
+
+/******************************************************************************
+ * Private API
+ *****************************************************************************/
+gboolean
+purple_history_adapter_activate(PurpleHistoryAdapter *adapter, GError **error)
+{
+ PurpleHistoryAdapterClass *klass = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_HISTORY_ADAPTER(adapter), FALSE);
+
+ klass = PURPLE_HISTORY_ADAPTER_GET_CLASS(adapter);
+ if(klass != NULL && klass->activate != NULL) {
+ return klass->activate(adapter, error);
+ }
+
+ g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ "%s does not implement the activate function.",
+ G_OBJECT_TYPE_NAME(G_OBJECT(adapter)));
+
+ return FALSE;
+}
+
+gboolean
+purple_history_adapter_deactivate(PurpleHistoryAdapter *adapter,
+ GError **error)
+{
+ PurpleHistoryAdapterClass *klass = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_HISTORY_ADAPTER(adapter), FALSE);
+
+ klass = PURPLE_HISTORY_ADAPTER_GET_CLASS(adapter);
+ if(klass != NULL && klass->deactivate != NULL) {
+ return klass->deactivate(adapter, error);
+ }
+
+ g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ "%s does not implement the deactivate function.",
+ G_OBJECT_TYPE_NAME(G_OBJECT(adapter)));
+
+ return FALSE;
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+const gchar *
+purple_history_adapter_get_id(PurpleHistoryAdapter *adapter) {
+ PurpleHistoryAdapterPrivate *priv = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_HISTORY_ADAPTER(adapter), NULL);
+
+ priv = purple_history_adapter_get_instance_private(adapter);
+
+ return priv->id;
+}
+
+const gchar *
+purple_history_adapter_get_name(PurpleHistoryAdapter *adapter) {
+ PurpleHistoryAdapterPrivate *priv = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_HISTORY_ADAPTER(adapter), NULL);
+
+ priv = purple_history_adapter_get_instance_private(adapter);
+
+ return priv->name;
+}
+
+GList *
+purple_history_adapter_query(PurpleHistoryAdapter *adapter,
+ const gchar *query,
+ GError **error)
+{
+ PurpleHistoryAdapterClass *klass = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_HISTORY_ADAPTER(adapter), NULL);
+ g_return_val_if_fail(query != NULL, NULL);
+
+ klass = PURPLE_HISTORY_ADAPTER_GET_CLASS(adapter);
+ if(klass != NULL && klass->query != NULL) {
+ return klass->query(adapter, query, error);
+ }
+
+ g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ "%s does not implement the query function.",
+ G_OBJECT_TYPE_NAME(G_OBJECT(adapter)));
+
+ return NULL;
+}
+
+gboolean
+purple_history_adapter_remove(PurpleHistoryAdapter *adapter,
+ const gchar *query,
+ GError **error)
+{
+ PurpleHistoryAdapterClass *klass = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_HISTORY_ADAPTER(adapter), FALSE);
+ klass = PURPLE_HISTORY_ADAPTER_GET_CLASS(adapter);
+
+ if(klass != NULL && klass->remove != NULL) {
+ return klass->remove(adapter, query, error);
+ }
+
+ g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ "%s does not implement the remove function.",
+ G_OBJECT_TYPE_NAME(G_OBJECT(adapter)));
+
+ return FALSE;
+}
+
+gboolean
+purple_history_adapter_write(PurpleHistoryAdapter *adapter,
+ PurpleConversation *conversation,
+ PurpleMessage *message,
+ GError **error)
+{
+ PurpleHistoryAdapterClass *klass = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_HISTORY_ADAPTER(adapter), FALSE);
+ g_return_val_if_fail(PURPLE_IS_MESSAGE(message), FALSE);
+ g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);
+
+ klass = PURPLE_HISTORY_ADAPTER_GET_CLASS(adapter);
+ if(klass != NULL && klass->write != NULL) {
+ return klass->write(adapter, conversation, message, error);
+ }
+
+ g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ "%s does not implement the write function.",
+ G_OBJECT_TYPE_NAME(G_OBJECT(adapter)));
+
+ return FALSE;
+}
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplehistoryadapter.h Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,164 @@
+/*
+ * 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/>.
+ */
+
+#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
+# error "only <purple.h> may be included directly"
+#endif
+
+#ifndef PURPLE_HISTORY_ADAPTER_H
+#define PURPLE_HISTORY_ADAPTER_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <purplemessage.h>
+#include <purpleconversation.h>
+
+/**
+ * SECTION:purplehistoryadapter
+ * @section_id: libpurple-purplehistoryadapter
+ * @title: History Adapter Object
+ */
+
+G_BEGIN_DECLS
+
+/**
+ * PURPLE_HISTORY_ADAPTER_DOMAIN:
+ *
+ * A #GError domain for errors.
+ *
+ * Since: 3.0.0
+ */
+#define PURPLE_HISTORY_ADAPTER_DOMAIN (g_quark_from_static_string("purple-history-adapter"))
+
+/**
+ * PurpleHistoryAdapter:
+ *
+ * #PurpleHistoryAdapter is a base class that should be sub classed by
+ * history adapters. It defines the behavior of all history adapters
+ * and implements some shared properties.
+ *
+ * Since: 3.0.0
+ */
+
+#define PURPLE_TYPE_HISTORY_ADAPTER (purple_history_adapter_get_type())
+G_DECLARE_DERIVABLE_TYPE(PurpleHistoryAdapter, purple_history_adapter,
+ PURPLE, HISTORY_ADAPTER, GObject)
+
+/**
+ * PurpleHistoryAdapterClass:
+ *
+ * #PurpleHistoryAdapterClass defines the interface for interacting with
+ * history adapters like sqlite, and so on.
+ *
+ * Since: 3.0.0
+ */
+struct _PurpleHistoryAdapterClass {
+ /*< private >*/
+ GObjectClass parent;
+
+ /*< public >*/
+ gboolean (*activate)(PurpleHistoryAdapter *adapter, GError **error);
+ gboolean (*deactivate)(PurpleHistoryAdapter *adapter, GError **error);
+ GList* (*query)(PurpleHistoryAdapter *adapter, const gchar *query, GError **error);
+ gboolean (*remove)(PurpleHistoryAdapter *adapter, const gchar *query, GError **error);
+ gboolean (*write)(PurpleHistoryAdapter *adapter, PurpleConversation *conversation, PurpleMessage *message, GError **error);
+
+ /*< private >*/
+
+ /* Some extra padding to play it safe. */
+ gpointer reserved[8];
+};
+
+/**
+ * purple_history_adapter_get_id:
+ * @adapter: The #PurpleHistoryAdapter instance.
+ *
+ * Gets the identifier of @adapter.
+ *
+ * Returns: The identifier of @adapter.
+ *
+ * Since: 3.0.0
+ */
+const gchar *purple_history_adapter_get_id(PurpleHistoryAdapter *adapter);
+
+/**
+ * purple_history_adapter_get_name:
+ * @adapter: The #PurpleHistoryAdapter instance.
+ *
+ * Gets the name of @adapter.
+ *
+ * Returns: The name of @adapter.
+ *
+ * Since: 3.0.0
+ */
+const gchar *purple_history_adapter_get_name(PurpleHistoryAdapter *adapter);
+
+/**
+ * purple_history_adapter_write:
+ * @adapter: The #PurpleHistoryAdapter instance.
+ * @conversation: The #PurpleConversation to send to the adapter.
+ * @message: The #PurpleMessage to send to the adapter.
+ * @error: A return address for a #GError.
+ *
+ * Writes a message to the @adapter.
+ *
+ * Returns: If the write was successful to the @adapter.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_history_adapter_write(PurpleHistoryAdapter *adapter,
+ PurpleConversation *conversation,
+ PurpleMessage *message,
+ GError **error);
+
+/**
+ * purple_history_adapter_query:
+ * @adapter: The #PurpleHistoryAdapter instance.
+ * @query: The query to send to the @adapter.
+ * @error: A return address for a #GError.
+ *
+ * Runs @query against @adapter.
+ *
+ * Returns: (element-type PurpleMessage) (transfer container): A list of messages that match @query.
+ *
+ * Since: 3.0.0
+ */
+GList *purple_history_adapter_query(PurpleHistoryAdapter *adapter,
+ const gchar *query,
+ GError **error);
+
+/**
+ * purple_history_adapter_remove:
+ * @adapter: The #PurpleHistoryAdapter instance.
+ * @query: Tells @adapter to remove messages that match @query.
+ * @error: A return address for a #GError.
+ *
+ * Tells @adapter to remove messages that match @query
+ *
+ * Returns: If removing the messages was successful.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_history_adapter_remove(PurpleHistoryAdapter *adapter,
+ const gchar *query,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* PURPLE_HISTORY_ADAPTER */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplehistorymanager.c Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,472 @@
+/*
+ * 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 "purplehistorymanager.h"
+#include "purplehistoryadapter.h"
+#include "purplesqlitehistoryadapter.h"
+
+#include "purpleprivate.h"
+#include "debug.h"
+#include "util.h"
+
+enum {
+ SIG_ACTIVE_CHANGED,
+ SIG_REGISTERED,
+ SIG_UNREGISTERED,
+ N_SIGNALS,
+};
+static guint signals[N_SIGNALS] = {0, };
+
+typedef struct {
+ GHashTable *adapters;
+ PurpleHistoryAdapter *active_adapter;
+} PurpleHistoryManagerPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE(PurpleHistoryManager, purple_history_manager,
+ G_TYPE_OBJECT);
+
+static PurpleHistoryManager *default_manager = NULL;
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+static void
+purple_history_manager_finalize(GObject *obj) {
+ PurpleHistoryManager *manager = NULL;
+ PurpleHistoryManagerPrivate *priv = NULL;
+
+ manager = PURPLE_HISTORY_MANAGER(obj);
+ priv = purple_history_manager_get_instance_private(manager);
+
+ g_clear_pointer(&priv->adapters, g_hash_table_destroy);
+
+ G_OBJECT_CLASS(purple_history_manager_parent_class)->finalize(obj);
+}
+
+static void
+purple_history_manager_init(PurpleHistoryManager *manager) {
+ PurpleHistoryManagerPrivate *priv = NULL;
+
+ priv = purple_history_manager_get_instance_private(manager);
+
+ priv->adapters = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
+ g_object_unref);
+}
+
+static void
+purple_history_manager_class_init(PurpleHistoryManagerClass *klass) {
+ GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+ obj_class->finalize = purple_history_manager_finalize;
+
+ /**
+ * PurpleHistoryManager::adapter-changed:
+ * @manager: The #PurpleHistoryManager instance.
+ * @old: The old #PurpleHistoryAdapter.
+ * @current: The new activated #PurpleHistoryAdapter.
+ *
+ * Emitted after @adapter has been changed for @manager.
+ *
+ * Since: 3.0.0
+ */
+ signals[SIG_ACTIVE_CHANGED] = g_signal_new(
+ "active-changed",
+ G_OBJECT_CLASS_TYPE(klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET(PurpleHistoryManagerClass, active_changed),
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 2,
+ PURPLE_TYPE_HISTORY_ADAPTER,
+ PURPLE_TYPE_HISTORY_ADAPTER);
+
+ /**
+ * PurpleHistoryManager::registered:
+ * @manager: The #PurpleHistoryManager instance.
+ * @adapter: The #PurpleHistoryAdapter that was registered.
+ *
+ * Emitted after @adapter has been registered in @manager.
+ *
+ * Since: 3.0.0
+ */
+ signals[SIG_REGISTERED] = g_signal_new(
+ "registered",
+ G_OBJECT_CLASS_TYPE(klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET(PurpleHistoryManagerClass, registered),
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ PURPLE_TYPE_HISTORY_ADAPTER);
+
+ /**
+ * PurpleHistoryManager::unregistered:
+ * @manager: The #PurpleHistoryManager instance.
+ * @adapter: The #PurpleHistoryAdapter that was unregistered.
+ *
+ * Emitted after @adapter has been unregistered for @manager.
+ *
+ * Since: 3.0.0
+ */
+ signals[SIG_UNREGISTERED] = g_signal_new(
+ "unregistered",
+ G_OBJECT_CLASS_TYPE(klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET(PurpleHistoryManagerClass, unregistered),
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ PURPLE_TYPE_HISTORY_ADAPTER);
+}
+
+/******************************************************************************
+ * Private API
+ *****************************************************************************/
+void
+purple_history_manager_startup(void) {
+ if(default_manager == NULL) {
+ PurpleHistoryAdapter *adapter = NULL;
+ GError *error = NULL;
+ gchar *filename = NULL;
+
+ filename = g_build_filename(purple_config_dir(), "history.db", NULL);
+ adapter = purple_sqlite_history_adapter_new(filename);
+ g_free(filename);
+
+ default_manager = g_object_new(PURPLE_TYPE_HISTORY_MANAGER, NULL);
+ if(!purple_history_manager_register(default_manager, adapter, &error)) {
+ if(error != NULL) {
+ g_warning("Failed to register sqlite history adapter: %s", error->message);
+ g_clear_error(&error);
+ } else {
+ g_warning("Failed to register sqlite history adapter: Unknown reason");
+ }
+
+ g_clear_object(&adapter);
+
+ return;
+ }
+
+ purple_history_manager_set_active(default_manager,
+ purple_history_adapter_get_id(adapter),
+ &error);
+
+ if(error != NULL) {
+ g_warning("Failed to activate %s: %s",
+ purple_history_adapter_get_id(adapter), error->message);
+
+ g_clear_error(&error);
+ }
+
+ g_clear_object(&adapter);
+ }
+}
+
+void
+purple_history_manager_shutdown(void) {
+ PurpleHistoryManagerPrivate *priv = NULL;
+ GError **error = NULL;
+
+ if(default_manager == NULL) {
+ return;
+ }
+
+ priv = purple_history_manager_get_instance_private(default_manager);
+ if(PURPLE_IS_HISTORY_ADAPTER(priv->active_adapter)) {
+ PurpleHistoryAdapter *adapter = NULL;
+
+ adapter = g_object_ref(priv->active_adapter);
+ purple_history_manager_set_active(default_manager, NULL, NULL);
+ purple_history_manager_unregister(default_manager,
+ adapter, error);
+
+ g_clear_object(&adapter);
+ }
+
+ g_clear_object(&default_manager);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+PurpleHistoryManager *
+purple_history_manager_get_default(void) {
+ return default_manager;
+}
+
+gboolean
+purple_history_manager_register(PurpleHistoryManager *manager,
+ PurpleHistoryAdapter *adapter,
+ GError **error)
+{
+ PurpleHistoryManagerPrivate *priv = NULL;
+ const gchar *id = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_HISTORY_MANAGER(manager), FALSE);
+ g_return_val_if_fail(PURPLE_IS_HISTORY_ADAPTER(adapter), FALSE);
+
+ priv = purple_history_manager_get_instance_private(manager);
+
+ id = purple_history_adapter_get_id(adapter);
+ if(g_hash_table_lookup(priv->adapters, id) != NULL) {
+ g_set_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0,
+ _("adapter %s is already registered"), id);
+
+ return FALSE;
+ }
+
+ g_hash_table_insert(priv->adapters, g_strdup(id), g_object_ref(adapter));
+
+ g_signal_emit(G_OBJECT(manager), signals[SIG_REGISTERED], 0, adapter);
+
+ return TRUE;
+}
+
+gboolean
+purple_history_manager_unregister(PurpleHistoryManager *manager,
+ PurpleHistoryAdapter *adapter,
+ GError **error)
+{
+ PurpleHistoryManagerPrivate *priv = NULL;
+ const gchar *id = NULL;
+ gboolean ret = FALSE;
+
+ g_return_val_if_fail(PURPLE_IS_HISTORY_MANAGER(manager), FALSE);
+ g_return_val_if_fail(PURPLE_IS_HISTORY_ADAPTER(adapter), FALSE);
+
+ priv = purple_history_manager_get_instance_private(manager);
+
+ if(adapter == priv->active_adapter) {
+ g_set_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0,
+ _("adapter %s is currently in use"), id);
+
+ return FALSE;
+ }
+
+ g_object_ref(G_OBJECT(adapter));
+
+ id = purple_history_adapter_get_id(adapter);
+
+ if(g_hash_table_remove(priv->adapters, id)) {
+ g_signal_emit(G_OBJECT(manager), signals[SIG_UNREGISTERED], 0,
+ adapter);
+
+ ret = TRUE;
+ } else {
+ g_set_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0,
+ _("adapter %s is not registered"), id);
+
+ ret = FALSE;
+ }
+
+ g_object_unref(G_OBJECT(adapter));
+
+ return ret;
+}
+
+PurpleHistoryAdapter *
+purple_history_manager_find(PurpleHistoryManager *manager, const gchar *id) {
+ PurpleHistoryManagerPrivate *priv = NULL;
+ gpointer value = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_HISTORY_MANAGER(manager), NULL);
+ g_return_val_if_fail(id != NULL, NULL);
+
+ priv = purple_history_manager_get_instance_private(manager);
+
+ value = g_hash_table_lookup(priv->adapters, id);
+ if(value == NULL) {
+ return NULL;
+ }
+
+ return PURPLE_HISTORY_ADAPTER(value);
+}
+
+GList *
+purple_history_manager_get_all(PurpleHistoryManager *manager) {
+ PurpleHistoryManagerPrivate *priv = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_HISTORY_MANAGER(manager), NULL);
+
+ priv = purple_history_manager_get_instance_private(manager);
+
+ return g_hash_table_get_values(priv->adapters);
+}
+
+PurpleHistoryAdapter *
+purple_history_manager_get_active(PurpleHistoryManager *manager) {
+ PurpleHistoryManagerPrivate *priv = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_HISTORY_MANAGER(manager), NULL);
+
+ priv = purple_history_manager_get_instance_private(manager);
+
+ return priv->active_adapter;
+}
+
+gboolean
+purple_history_manager_set_active(PurpleHistoryManager *manager,
+ const gchar *id,
+ GError **error)
+{
+ PurpleHistoryManagerPrivate *priv = NULL;
+ PurpleHistoryAdapter *old = NULL, *adapter = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_HISTORY_MANAGER(manager), FALSE);
+
+ priv = purple_history_manager_get_instance_private(manager);
+
+ /* First look up the new adapter if we're given one. */
+ if(id != NULL) {
+ adapter = g_hash_table_lookup(priv->adapters, id);
+ if(!PURPLE_IS_HISTORY_ADAPTER(adapter)) {
+ g_set_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0,
+ "no history adapter found with id %s", id);
+
+ return FALSE;
+ }
+ }
+
+ if(PURPLE_IS_HISTORY_ADAPTER(priv->active_adapter)) {
+ old = g_object_ref(priv->active_adapter);
+ }
+
+ if(g_set_object(&priv->active_adapter, adapter)) {
+ if(PURPLE_IS_HISTORY_ADAPTER(old)) {
+ if(!purple_history_adapter_deactivate(old, error)) {
+ g_set_object(&priv->active_adapter, old);
+ g_clear_object(&old);
+ return FALSE;
+ }
+ }
+
+ if(PURPLE_IS_HISTORY_ADAPTER(adapter)) {
+ if(!purple_history_adapter_activate(adapter, error)) {
+ if(PURPLE_IS_HISTORY_ADAPTER(old)) {
+ purple_history_adapter_activate(old, error);
+ }
+
+ g_set_object(&priv->active_adapter, old);
+ g_clear_object(&old);
+
+ return FALSE;
+ }
+ }
+
+ g_signal_emit(G_OBJECT(manager), signals[SIG_ACTIVE_CHANGED], 0, old,
+ priv->active_adapter);
+ }
+
+ g_clear_object(&old);
+
+ purple_debug_info("history-manager", "set active adapter to '%s'", id);
+
+ return TRUE;
+}
+
+GList *
+purple_history_manager_query(PurpleHistoryManager *manager,
+ const gchar *query,
+ GError **error)
+{
+ PurpleHistoryManagerPrivate *priv = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_HISTORY_MANAGER(manager), FALSE);
+
+ priv = purple_history_manager_get_instance_private(manager);
+
+ if(priv->active_adapter == NULL) {
+ g_set_error_literal(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0,
+ _("no active history adapter"));
+ return FALSE;
+ }
+
+ return purple_history_adapter_query(priv->active_adapter, query, error);
+}
+
+gboolean
+purple_history_manager_remove(PurpleHistoryManager *manager,
+ const gchar *query,
+ GError **error)
+{
+ PurpleHistoryManagerPrivate *priv = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_HISTORY_MANAGER(manager), FALSE);
+
+ priv = purple_history_manager_get_instance_private(manager);
+
+ if(priv->active_adapter == NULL) {
+ g_set_error_literal(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0,
+ _("no active history adapter"));
+ return FALSE;
+ }
+
+ return purple_history_adapter_remove(priv->active_adapter, query, error);
+}
+
+gboolean
+purple_history_manager_write(PurpleHistoryManager *manager,
+ PurpleConversation *conversation,
+ PurpleMessage *message,
+ GError **error)
+{
+ PurpleHistoryManagerPrivate *priv = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);
+ g_return_val_if_fail(PURPLE_IS_MESSAGE(message), FALSE);
+ g_return_val_if_fail(PURPLE_IS_HISTORY_MANAGER(manager), FALSE);
+
+ priv = purple_history_manager_get_instance_private(manager);
+
+ if(priv->active_adapter == NULL) {
+ g_set_error_literal(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0,
+ _("no active history adapter"));
+ return FALSE;
+ }
+
+ return purple_history_adapter_write(priv->active_adapter, conversation,
+ message, error);
+}
+
+void
+purple_history_manager_foreach(PurpleHistoryManager *manager,
+ PurpleHistoryManagerForeachFunc func,
+ gpointer data)
+{
+ GHashTableIter iter;
+ PurpleHistoryManagerPrivate *priv = NULL;
+ gpointer value;
+
+ g_return_if_fail(PURPLE_IS_HISTORY_MANAGER(manager));
+ g_return_if_fail(func != NULL);
+
+ priv = purple_history_manager_get_instance_private(manager);
+
+ g_hash_table_iter_init(&iter, priv->adapters);
+ while (g_hash_table_iter_next(&iter, NULL, &value)) {
+ func(PURPLE_HISTORY_ADAPTER(value), data);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplehistorymanager.h Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,258 @@
+/*
+ * 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"
+#endif
+
+#ifndef PURPLE_HISTORY_MANAGER_H
+#define PURPLE_HISTORY_MANAGER_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "purplehistoryadapter.h"
+
+G_BEGIN_DECLS
+
+/**
+ * SECTION:purplehistorymanager
+ * @section_id: libpurple-purplehistorymanager
+ * @title: Purple History Manager
+ *
+ * #PurpleHistoryManager keeps track of all adapters and emits signals when
+ * adapters are registered and unregistered.
+ */
+
+/**
+ * PURPLE_HISTORY_MANAGER_DOMAIN:
+ *
+ * A #GError domain for errors from #PurpleHistoryManager.
+ *
+ * Since: 3.0.0
+ */
+#define PURPLE_HISTORY_MANAGER_DOMAIN (g_quark_from_static_string("purple-history-manager"))
+
+/**
+ * PURPLE_TYPE_HISTORY_MANAGER:
+ *
+ * The standard _get_type macro for #PurpleHistoryManager.
+ */
+#define PURPLE_TYPE_HISTORY_MANAGER (purple_history_manager_get_type())
+G_DECLARE_DERIVABLE_TYPE(PurpleHistoryManager, purple_history_manager,
+ PURPLE, HISTORY_MANAGER, GObject)
+
+/**
+ * PurpleHistoryManager:
+ *
+ * An opaque data structure that represents a history manager.
+ *
+ * Since: 3.0.0
+ */
+
+/**
+ * PurpleHistoryManagerClass:
+ * @active_changed: The default signal handler for when an adapter is changed.
+ * @registered: The default signal handler for when an adapter is registered.
+ * @unregistered: The default signal handler for when an adapter is unregistered.
+ *
+ * The class structure for #PurpleHistoryManager.
+ *
+ * Since: 3.0.0
+ */
+struct _PurpleHistoryManagerClass {
+ /*< private >*/
+ GObjectClass parent;
+
+ /*< public >*/
+ void (*active_changed)(PurpleHistoryManager *manager, PurpleHistoryAdapter *previous, PurpleHistoryAdapter *current);
+ gboolean (*registered)(PurpleHistoryManager *manager, PurpleHistoryAdapter *adapter, GError **error);
+ gboolean (*unregistered)(PurpleHistoryManager *manager, PurpleHistoryAdapter *adapter, GError **error);
+
+ /*< private >*/
+ gpointer reserved[4];
+};
+
+/**
+ * PurpleHistoryManagerForeachFunc:
+ * @adapter: The #PurpleHistoryAdapter instance.
+ * @data: User supplied data.
+ *
+ * A function to be used as a callback with
+ * purple_history_manager_foreach().
+ *
+ * Since: 3.0.0
+ */
+typedef void (*PurpleHistoryManagerForeachFunc)(PurpleHistoryAdapter *adapter, gpointer data);
+
+/**
+ * purple_history_manager_get_default:
+ *
+ * Gets the default #PurpleHistoryManager instance.
+ *
+ * Returns: (transfer none): The default #PurpleHistoryManager instance.
+ *
+ * Since: 3.0.0
+ */
+PurpleHistoryManager *purple_history_manager_get_default(void);
+
+/**
+ * purple_history_manager_get_active:
+ * @manager: The #PurpleHistoryManager instance.
+ *
+ * Gets the active #PurpleHistoryAdapter instance.
+ *
+ * Returns: The active @adapter
+ *
+ * Since: 3.0.0
+ */
+ PurpleHistoryAdapter *purple_history_manager_get_active(PurpleHistoryManager *manager);
+
+ /**
+ * purple_history_manager_set_active:
+ * @manager: The #PurpleHistoryManager instance.
+ * @id: The id of the #PurpleHistoryAdapter to set active.
+ * @error: A return address for a #GError.
+ *
+ * Sets the active #PurpleHistoryAdapter instance.
+ *
+ * Returns: %TRUE if setting the @adapter was successful with @manager
+ * %FALSE otherwise.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_history_manager_set_active(PurpleHistoryManager *manager, const gchar *id, GError **error);
+
+/**
+ * purple_history_manager_register:
+ * @manager: The #PurpleHistoryManager instance.
+ * @adapter: The #PurpleHistoryAdapter to register.
+ * @error: A return address for a #GError.
+ *
+ * Registers @adapter with @manager.
+ *
+ * Returns: %TRUE if @adapter was successfully registered with @manager,
+ * %FALSE otherwise.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_history_manager_register(PurpleHistoryManager *manager, PurpleHistoryAdapter *adapter, GError **error);
+
+/**
+ * purple_history_manager_unregister:
+ * @manager: The #PurpleHistoryManager instance.
+ * @adapter: The #PurpleHistoryAdapter to unregister.
+ * @error: A return address for a #GError.
+ *
+ * Unregisters @adapter from @manager.
+ *
+ * Returns: %TRUE if @adapter was successfully unregistered from @manager,
+ * %FALSE otherwise.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_history_manager_unregister(PurpleHistoryManager *manager, PurpleHistoryAdapter *adapter, GError **error);
+
+/**
+ * purple_history_manager_find:
+ * @manager: The #PurpleHistoryManager instance.
+ * @id: The id of the #PurpleHistoryAdapter to find.
+ *
+ * Gets the #PurpleHistoryAdapter identified by @id if found, otherwise %NULL.
+ *
+ * Returns: (transfer none): The #PurpleHistoryAdapter identified by @id or %NULL.
+ *
+ * Since: 3.0.0
+ */
+PurpleHistoryAdapter *purple_history_manager_find(PurpleHistoryManager *manager, const gchar *id);
+
+/**
+ * purple_history_manager_get_all:
+ * @manager: The #PurpleHistoryManager instance.
+ *
+ * Gets a list of all #PurpleHistoryAdapter's that are currently registered in
+ * @manager.
+ *
+ * Returns: (transfer container) (element-type PurpleHistoryAdapter): The list
+ * containing all of the #PurpleHistoryAdapter's registered with @manager.
+ *
+ * Since: 3.0.0
+ */
+GList *purple_history_manager_get_all(PurpleHistoryManager *manager);
+
+/**
+ * purple_history_manager_query:
+ * @manager: The #PurpleHistoryManager instance.
+ * @query: A query to send to the @manager instance.
+ * @error: A return address for a #GError.
+ *
+ * Sends a query to the #PurpleHistoryAdapter @manager instance.
+ *
+ * Returns: (transfer full) (element-type PurpleHistoryAdapter): The list
+ * containing all of the #PurpleMessage's that matched the query
+ * with @manager.
+ *
+ * Since: 3.0.0
+ */
+GList *purple_history_manager_query(PurpleHistoryManager *manager, const gchar *query, GError **error);
+
+/**
+ * purple_history_manager_remove:
+ * @manager: The #PurpleHistoryManager instance.
+ * @query: A query to send to the @manager instance.
+ * @error: A return address for a #GError.
+ *
+ * Removes messages from the active #PurpleHistoryAdapter of @manager that match @query.
+ *
+ * Returns: %TRUE if messages matching @query were successfully removed from
+ * the active adapter of @manager, %FALSE otherwise.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_history_manager_remove(PurpleHistoryManager *manager, const gchar *query, GError **error);
+
+/**
+ * purple_history_manager_write:
+ * @manager: The #PurpleHistoryManager instance.
+ * @conversation: The #PurpleConversation.
+ * @message: The #PurpleMessage to pass to the @manager.
+ * @error: A return address for a #GError.
+ *
+ * Writes @message to the active adapter of @manager.
+ *
+ * Returns: %TRUE if @message was successfully written, %FALSE otherwise.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_history_manager_write(PurpleHistoryManager *manager, PurpleConversation *conversation, PurpleMessage *message, GError **error);
+
+/**
+ * purple_history_manager_foreach:
+ * @manager: The #PurpleHistoryManager instance.
+ * @func: (scope call): The #PurpleHistoryManagerForeachFunc to call.
+ * @data: User data to pass to @func.
+ *
+ * Calls @func for each #PurpleHistoryAdapter that @manager knows about.
+ *
+ * Since: 3.0.0
+ */
+void purple_history_manager_foreach(PurpleHistoryManager *manager, PurpleHistoryManagerForeachFunc func, gpointer data);
+
+G_END_DECLS
+
+#endif /* PURPLE_HISTORY_MANAGER_H */
--- a/libpurple/purpleprivate.h Mon Oct 11 23:47:26 2021 -0500
+++ b/libpurple/purpleprivate.h Tue Oct 12 00:50:59 2021 -0500
@@ -32,6 +32,7 @@
#include "accounts.h"
#include "connection.h"
#include "purplecredentialprovider.h"
+#include "purplehistoryadapter.h"
#define PURPLE_STATIC_ASSERT(condition, message) \
{ typedef char static_assertion_failed_ ## message \
@@ -256,6 +257,52 @@
void purple_credential_provider_deactivate(PurpleCredentialProvider *provider);
/**
+ * purple_history_adapter_activate:
+ * @adapter: The #PurpleHistoryAdapter instance.
+ * @error: A return address for a #GError.
+ *
+ * Asks @adapter to become the active adapter. If @adapter can not become active
+ * it should return %FALSE and set @error.
+ *
+ * Returns: %TRUE on success otherwise %FALSE with @error set.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_history_adapter_activate(PurpleHistoryAdapter *adapter, GError **error);
+
+/**
+ * purple_history_adapter_deactivate:
+ * @adapter: The #PurpleHistoryAdapter instance.
+ * @error: A return address for a #GError.
+ *
+ * Asks @adapter to stop being the active adapter. If @adapter can not
+ * deactivate it should return %FALSE and set @error.
+ *
+ * Returns: %TRUE on success otherwise %FALSE with @error set.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_history_adapter_deactivate(PurpleHistoryAdapter *adapter, GError **error);
+
+/**
+ * purple_history_manager_startup:
+ *
+ * Starts up the history manager by creating the default instance.
+ *
+ * Since: 3.0.0
+ */
+void purple_history_manager_startup(void);
+
+/**
+ * purple_history_manager_shutdown:
+ *
+ * Shuts down the history manager by destroying the default instance.
+ *
+ * Since: 3.0.0
+ */
+void purple_history_manager_shutdown(void);
+
+/**
* purple_whiteboard_manager_startup:
*
* Starts up the whiteboard manager by creating the default instance.
@@ -273,7 +320,6 @@
*/
void purple_whiteboard_manager_shutdown(void);
-
G_END_DECLS
#endif /* PURPLE_PRIVATE_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplesqlitehistoryadapter.c Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,675 @@
+/*
+ * 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/>.
+ */
+
+#include <glib/gi18n-lib.h>
+
+#include "purplesqlitehistoryadapter.h"
+
+#include "account.h"
+#include "purpleprivate.h"
+#include "purpleresources.h"
+
+#include <sqlite3.h>
+
+struct _PurpleSqliteHistoryAdapter {
+ PurpleHistoryAdapter parent;
+};
+
+typedef struct {
+ gchar *filename;
+ sqlite3 *db;
+} PurpleSqliteHistoryAdapterPrivate;
+
+enum {
+ PROP_0,
+ PROP_FILENAME,
+ N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+G_DEFINE_TYPE_WITH_PRIVATE(PurpleSqliteHistoryAdapter,
+ purple_sqlite_history_adapter,
+ PURPLE_TYPE_HISTORY_ADAPTER)
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+purple_sqlite_history_adapter_set_filename(PurpleSqliteHistoryAdapter *adapter,
+ const gchar *filename)
+{
+ PurpleSqliteHistoryAdapterPrivate *priv = NULL;
+
+ priv = purple_sqlite_history_adapter_get_instance_private(adapter);
+
+ g_free(priv->filename);
+ priv->filename = g_strdup(filename);
+
+ g_object_notify_by_pspec(G_OBJECT(adapter), properties[PROP_FILENAME]);
+}
+
+static gboolean
+purple_sqlite_history_adapter_run_migrations(PurpleSqliteHistoryAdapter *adapter,
+ GError **error)
+{
+ GBytes *bytes = NULL;
+ GResource *resource = NULL;
+ PurpleSqliteHistoryAdapterPrivate *priv = NULL;
+ gchar *error_msg = NULL;
+ const gchar *script = NULL;
+
+ priv = purple_sqlite_history_adapter_get_instance_private(adapter);
+
+ resource = purple_get_resource();
+
+ bytes = g_resource_lookup_data(resource,
+ "/im/pidgin/libpurple/sqlitehistoryadapter/01-schema.sql",
+ G_RESOURCE_LOOKUP_FLAGS_NONE, error);
+ if(bytes == NULL) {
+ return FALSE;
+ }
+
+ script = (const gchar *)g_bytes_get_data(bytes, NULL);
+ sqlite3_exec(priv->db, script, NULL, NULL, &error_msg);
+ g_bytes_unref(bytes);
+
+ if(error_msg != NULL) {
+ g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ "failed to run migrations: %s", error_msg);
+
+ sqlite3_free(error_msg);
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gchar *
+purple_sqlite_history_adapter_get_content_type(PurpleMessageContentType content_type) {
+ switch(content_type) {
+ case PURPLE_MESSAGE_CONTENT_TYPE_PLAIN:
+ return "plain";
+ break;
+ case PURPLE_MESSAGE_CONTENT_TYPE_HTML:
+ return "html";
+ break;
+ case PURPLE_MESSAGE_CONTENT_TYPE_XHTML:
+ return "xhtml";
+ break;
+ case PURPLE_MESSAGE_CONTENT_TYPE_MARKDOWN:
+ return "markdown";
+ break;
+ default:
+ return "";
+ break;
+ }
+}
+
+static PurpleMessageContentType
+purple_sqlite_history_adapter_get_content_type_enum(const gchar *content_type)
+{
+ if(purple_strequal(content_type, "plain")) {
+ return PURPLE_MESSAGE_CONTENT_TYPE_PLAIN;
+ }
+ if(purple_strequal(content_type, "html")) {
+ return PURPLE_MESSAGE_CONTENT_TYPE_HTML;
+ }
+ if(purple_strequal(content_type, "xhtml")) {
+ return PURPLE_MESSAGE_CONTENT_TYPE_XHTML;
+ }
+ if(purple_strequal(content_type, "markdown")) {
+ return PURPLE_MESSAGE_CONTENT_TYPE_MARKDOWN;
+ }
+ return PURPLE_MESSAGE_CONTENT_TYPE_PLAIN;
+}
+
+static sqlite3_stmt *
+purple_sqlite_history_adapter_build_query(PurpleSqliteHistoryAdapter *adapter,
+ const gchar * search_query,
+ gboolean remove,
+ GError **error)
+{
+ gchar **split = NULL;
+ gint i = 0;
+ GList *ins = NULL;
+ GList *froms = NULL;
+ GList *keywords = NULL;
+ GString *query = NULL;
+ GList *iter = NULL;
+ gboolean first = FALSE;
+ sqlite3_stmt *prepared_statement = NULL;
+ gint index = 1;
+ PurpleSqliteHistoryAdapterPrivate *priv = NULL;
+ gint query_items = 0;
+
+ priv = purple_sqlite_history_adapter_get_instance_private(adapter);
+
+ split = g_strsplit(search_query, " ", -1);
+ for(i = 0; split[i] != NULL; i++) {
+ if(g_str_has_prefix(split[i], "in:")) {
+ if(split[i][3] == '\0') {
+ continue;
+ }
+ ins = g_list_prepend(ins, g_strdup(split[i]+3));
+ query_items++;
+ } else if(g_str_has_prefix(split[i], "from:")) {
+ if(split[i][5] == '\0') {
+ continue;
+ }
+ froms = g_list_prepend(froms, g_strdup(split[i]+5));
+ query_items++;
+ } else {
+ if(split[i][0] == '\0') {
+ continue;
+ }
+ keywords = g_list_prepend(keywords,
+ g_strdup_printf("%%%s%%", split[i]));
+ query_items++;
+ }
+ }
+
+ g_clear_pointer(&split, g_strfreev);
+
+ if(remove) {
+ if(query_items != 0) {
+ query = g_string_new("DELETE FROM message_log WHERE TRUE\n");
+ } else {
+ g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ "Attempting to remove messages without "
+ "query parameters.");
+
+ return NULL;
+ }
+ } else {
+ query = g_string_new("SELECT "
+ "message_id, author, author_name_color, "
+ "author_alias, recipient, content_type, "
+ "content, client_timestamp "
+ "FROM message_log WHERE TRUE\n");
+ }
+
+ if(ins != NULL) {
+ first = TRUE;
+ g_string_append(query, "AND (conversation_id IN (");
+ for(iter = ins; iter != NULL; iter = iter->next) {
+ if(!first) {
+ g_string_append(query, ", ");
+ }
+ first = FALSE;
+ g_string_append(query, "?");
+ }
+ g_string_append(query, "))");
+ }
+
+ if(froms != NULL) {
+ first = TRUE;
+ g_string_append(query, "AND (author IN (");
+ for(iter = froms; iter != NULL; iter = iter->next) {
+ if(!first) {
+ g_string_append(query, ", ");
+ }
+ first = FALSE;
+ g_string_append(query, "?");
+ }
+ g_string_append(query, "))");
+ }
+
+ if(keywords != NULL) {
+ first = TRUE;
+ g_string_append(query, "AND (");
+ for(iter = keywords; iter != NULL; iter = iter->next) {
+ if(!first) {
+ g_string_append(query, " OR ");
+ }
+ first = FALSE;
+ g_string_append(query, " content LIKE ? ");
+ }
+ g_string_append(query, ")");
+ }
+ g_string_append(query, ";");
+
+ sqlite3_prepare_v2(priv->db, query->str, -1, &prepared_statement, NULL);
+
+ g_string_free(query, TRUE);
+
+ if(prepared_statement == NULL) {
+ g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ "Error creating the prepared statement: %s",
+ sqlite3_errmsg(priv->db));
+
+ g_list_free_full(ins, g_free);
+ g_list_free_full(froms, g_free);
+ g_list_free_full(keywords, g_free);
+
+ return NULL;
+ }
+
+ while(ins != NULL) {
+ sqlite3_bind_text(prepared_statement, index++,
+ (const char *)ins->data, -1, g_free);
+ ins = g_list_delete_link(ins, ins);
+ }
+
+ while(froms != NULL) {
+ sqlite3_bind_text(prepared_statement, index++,
+ (const char *)froms->data, -1, g_free);
+ froms = g_list_delete_link(froms, froms);
+ }
+
+ while(keywords != NULL) {
+ sqlite3_bind_text(prepared_statement, index++,
+ (const char *)keywords->data, -1, g_free);
+ keywords = g_list_delete_link(keywords, keywords);
+ }
+
+ return prepared_statement;
+}
+
+/******************************************************************************
+ * PurpleHistoryAdapter Implementation
+ *****************************************************************************/
+static gboolean
+purple_sqlite_history_adapter_activate(PurpleHistoryAdapter *adapter,
+ GError **error)
+{
+ PurpleSqliteHistoryAdapter *sqlite_adapter = NULL;
+ PurpleSqliteHistoryAdapterPrivate *priv = NULL;
+ gint rc = 0;
+
+ sqlite_adapter = PURPLE_SQLITE_HISTORY_ADAPTER(adapter);
+ priv = purple_sqlite_history_adapter_get_instance_private(sqlite_adapter);
+
+ if(priv->db != NULL) {
+ g_set_error_literal(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ _("Adapter has already been activated"));
+
+ return FALSE;
+ }
+
+ if(priv->filename == NULL) {
+ g_set_error_literal(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ _("No filename specified"));
+
+ return FALSE;
+ }
+
+ rc = sqlite3_open(priv->filename, &priv->db);
+ if(rc != SQLITE_OK) {
+ g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ _("Error opening database in purplesqlitehistoryadapter for file %s"), priv->filename);
+ g_clear_pointer(&priv->db, sqlite3_close);
+
+ return FALSE;
+ }
+
+ if(!purple_sqlite_history_adapter_run_migrations(sqlite_adapter, error)) {
+ g_clear_pointer(&priv->db, sqlite3_close);
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+purple_sqlite_history_adapter_deactivate(PurpleHistoryAdapter *adapter,
+ GError **error)
+{
+ PurpleSqliteHistoryAdapter *sqlite_adapter = NULL;
+ PurpleSqliteHistoryAdapterPrivate *priv = NULL;
+
+ sqlite_adapter = PURPLE_SQLITE_HISTORY_ADAPTER(adapter);
+ priv = purple_sqlite_history_adapter_get_instance_private(sqlite_adapter);
+ g_clear_pointer(&priv->db, sqlite3_close);
+
+ return TRUE;
+}
+
+static GList*
+purple_sqlite_history_adapter_query(PurpleHistoryAdapter *adapter,
+ const gchar *query, GError **error)
+{
+ PurpleSqliteHistoryAdapter *sqlite_adapter = NULL;
+ PurpleSqliteHistoryAdapterPrivate *priv = NULL;
+ sqlite3_stmt *prepared_statement = NULL;
+ GList *results = NULL;
+ gint result = 0;
+
+ sqlite_adapter = PURPLE_SQLITE_HISTORY_ADAPTER(adapter);
+ priv = purple_sqlite_history_adapter_get_instance_private(sqlite_adapter);
+
+ if(priv->db == NULL) {
+ g_set_error_literal(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ _("Adapter has not been activated"));
+
+ return FALSE;
+ }
+
+ prepared_statement = purple_sqlite_history_adapter_build_query(sqlite_adapter,
+ query,
+ FALSE,
+ error);
+
+ if(prepared_statement == NULL) {
+ return NULL;
+ }
+
+ while((result = sqlite3_step(prepared_statement)) == SQLITE_ROW) {
+ PurpleMessage *message = NULL;
+ PurpleMessageContentType ct;
+ GDateTime *g_date_time = NULL;
+ const gchar *message_id = NULL;
+ const gchar *author = NULL;
+ const gchar *author_name_color = NULL;
+ const gchar *author_alias = NULL;
+ const gchar *recipient = NULL;
+ const gchar *content = NULL;
+ const gchar *content_type = NULL;
+ const gchar *timestamp = NULL;
+
+ message_id = (const gchar *)sqlite3_column_text(prepared_statement, 0);
+ author = (const gchar *)sqlite3_column_text(prepared_statement, 1);
+ author_name_color = (const gchar *)sqlite3_column_text(prepared_statement, 2);
+ author_alias = (const gchar *)sqlite3_column_text(prepared_statement, 3);
+ recipient = (const gchar *)sqlite3_column_text(prepared_statement, 4);
+ content_type = (const gchar *)sqlite3_column_text(prepared_statement, 5);
+ ct = purple_sqlite_history_adapter_get_content_type_enum(content_type);
+ content = (const gchar *)sqlite3_column_text(prepared_statement, 6);
+ timestamp = (const gchar *)sqlite3_column_text(prepared_statement, 7);
+ g_date_time = g_date_time_new_from_iso8601(timestamp, NULL);
+
+ message = g_object_new(PURPLE_TYPE_MESSAGE,
+ "id", message_id,
+ "author", author,
+ "author_name_color", author_name_color,
+ "author_alias", author_alias,
+ "recipient", recipient,
+ "contents", content,
+ "content_type", ct,
+ "timestamp", g_date_time,
+ NULL);
+
+ results = g_list_prepend(results, message);
+ }
+
+ results = g_list_reverse(results);
+
+ sqlite3_finalize(prepared_statement);
+
+ return results;
+}
+
+static gboolean
+purple_sqlite_history_adapter_remove(PurpleHistoryAdapter *adapter,
+ const gchar *query, GError **error)
+{
+ PurpleSqliteHistoryAdapter *sqlite_adapter = NULL;
+ PurpleSqliteHistoryAdapterPrivate *priv = NULL;
+ sqlite3_stmt * prepared_statement = NULL;
+ gint result = 0;
+
+ sqlite_adapter = PURPLE_SQLITE_HISTORY_ADAPTER(adapter);
+ priv = purple_sqlite_history_adapter_get_instance_private(sqlite_adapter);
+
+ if(priv->db == NULL) {
+ g_set_error_literal(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ _("Adapter has not been activated"));
+
+ return FALSE;
+ }
+
+ prepared_statement = purple_sqlite_history_adapter_build_query(sqlite_adapter,
+ query,
+ TRUE,
+ error);
+
+ if(prepared_statement == NULL) {
+ return FALSE;
+ }
+
+ result = sqlite3_step(prepared_statement);
+
+ if(result != SQLITE_DONE) {
+ g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ "Error removing from the database: %s",
+ sqlite3_errmsg(priv->db));
+
+ sqlite3_finalize(prepared_statement);
+
+ return FALSE;
+ }
+
+ sqlite3_finalize(prepared_statement);
+
+ return TRUE;
+}
+
+static gboolean
+purple_sqlite_history_adapter_write(PurpleHistoryAdapter *adapter,
+ PurpleConversation *conversation,
+ PurpleMessage *message, GError **error)
+{
+ PurpleAccount *account = NULL;
+ PurpleSqliteHistoryAdapter *sqlite_adapter = NULL;
+ PurpleSqliteHistoryAdapterPrivate *priv = NULL;
+ sqlite3_stmt *prepared_statement = NULL;
+ gchar *timestamp = NULL;
+ gchar *content_type = NULL;
+ const gchar * message_id = NULL;
+ const gchar *script = NULL;
+ gint result = 0;
+
+ script = "INSERT INTO message_log(protocol, account, conversation_id, "
+ "message_id, author, author_name_color, author_alias, "
+ "recipient, content_type, content, client_timestamp) "
+ "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+
+ sqlite_adapter = PURPLE_SQLITE_HISTORY_ADAPTER(adapter);
+ priv = purple_sqlite_history_adapter_get_instance_private(sqlite_adapter);
+
+ if(priv->db == NULL) {
+ g_set_error_literal(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ _("Adapter has not been activated"));
+
+ return FALSE;
+ }
+
+ sqlite3_prepare_v2(priv->db, script, -1, &prepared_statement, NULL);
+
+ if(prepared_statement == NULL) {
+ g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ "Error creating the prepared statement: %s",
+ sqlite3_errmsg(priv->db));
+ return FALSE;
+ }
+
+ account = purple_conversation_get_account(conversation);
+
+ sqlite3_bind_text(prepared_statement,
+ 1, purple_account_get_protocol_name(account), -1,
+ SQLITE_STATIC);
+ sqlite3_bind_text(prepared_statement,
+ 2, purple_account_get_username(account), -1,
+ SQLITE_STATIC);
+ sqlite3_bind_text(prepared_statement,
+ 3, purple_conversation_get_name(conversation), -1,
+ SQLITE_STATIC);
+ message_id = purple_message_get_id(message);
+ if(message_id != NULL) {
+ sqlite3_bind_text(prepared_statement, 4, message_id, -1,
+ SQLITE_STATIC);
+ } else {
+ sqlite3_bind_text(prepared_statement, 4, g_uuid_string_random(), -1,
+ g_free);
+ }
+ sqlite3_bind_text(prepared_statement,
+ 5, purple_message_get_author(message), -1,
+ SQLITE_STATIC);
+ sqlite3_bind_text(prepared_statement,
+ 6, purple_message_get_author_name_color(message), -1,
+ SQLITE_STATIC);
+ sqlite3_bind_text(prepared_statement,
+ 7, purple_message_get_author_alias(message), -1,
+ SQLITE_STATIC);
+ sqlite3_bind_text(prepared_statement,
+ 8, purple_message_get_recipient(message), -1,
+ SQLITE_STATIC);
+ content_type = purple_sqlite_history_adapter_get_content_type(purple_message_get_content_type(message));
+ sqlite3_bind_text(prepared_statement,
+ 9, content_type, -1, SQLITE_STATIC);
+ sqlite3_bind_text(prepared_statement,
+ 10, purple_message_get_contents(message), -1,
+ SQLITE_STATIC);
+ timestamp = g_date_time_format_iso8601(purple_message_get_timestamp(message));
+ sqlite3_bind_text(prepared_statement, 11, timestamp, -1, g_free);
+
+ result = sqlite3_step(prepared_statement);
+
+ if(result != SQLITE_DONE) {
+ g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0,
+ "Error writing to the database: %s",
+ sqlite3_errmsg(priv->db));
+
+ sqlite3_finalize(prepared_statement);
+
+ return FALSE;
+ }
+
+ sqlite3_finalize(prepared_statement);
+
+ return TRUE;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+static void
+purple_sqlite_history_adapter_get_property(GObject *obj, guint param_id,
+ GValue *value, GParamSpec *pspec)
+{
+ PurpleSqliteHistoryAdapter *adapter = PURPLE_SQLITE_HISTORY_ADAPTER(obj);
+
+ switch(param_id) {
+ case PROP_FILENAME:
+ g_value_set_string(value,
+ purple_sqlite_history_adapter_get_filename(adapter));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+ break;
+ }
+}
+
+static void
+purple_sqlite_history_adapter_set_property(GObject *obj, guint param_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ PurpleSqliteHistoryAdapter *adapter = PURPLE_SQLITE_HISTORY_ADAPTER(obj);
+
+ switch(param_id) {
+ case PROP_FILENAME:
+ purple_sqlite_history_adapter_set_filename(adapter,
+ g_value_get_string(value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+ break;
+ }
+}
+
+static void
+purple_sqlite_history_adapter_finalize(GObject *obj) {
+ PurpleSqliteHistoryAdapter *adapter = NULL;
+ PurpleSqliteHistoryAdapterPrivate *priv = NULL;
+
+ adapter = PURPLE_SQLITE_HISTORY_ADAPTER(obj);
+ priv = purple_sqlite_history_adapter_get_instance_private(adapter);
+
+ g_clear_pointer(&priv->filename, g_free);
+
+ if(priv->db != NULL) {
+ g_warning("PurpleSqliteHistoryAdapter was finalized before being "
+ "deactivated");
+
+ g_clear_pointer(&priv->db, sqlite3_close);
+ }
+
+ G_OBJECT_CLASS(purple_sqlite_history_adapter_parent_class)->finalize(obj);
+}
+
+static void
+purple_sqlite_history_adapter_init(PurpleSqliteHistoryAdapter *adapter) {
+}
+
+static void
+purple_sqlite_history_adapter_class_init(PurpleSqliteHistoryAdapterClass *klass)
+{
+ GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+ PurpleHistoryAdapterClass *adapter_class = PURPLE_HISTORY_ADAPTER_CLASS(klass);
+
+ obj_class->get_property = purple_sqlite_history_adapter_get_property;
+ obj_class->set_property = purple_sqlite_history_adapter_set_property;
+ obj_class->finalize = purple_sqlite_history_adapter_finalize;
+
+ adapter_class->activate = purple_sqlite_history_adapter_activate;
+ adapter_class->deactivate = purple_sqlite_history_adapter_deactivate;
+ adapter_class->query = purple_sqlite_history_adapter_query;
+ adapter_class->remove = purple_sqlite_history_adapter_remove;
+ adapter_class->write = purple_sqlite_history_adapter_write;
+
+ /**
+ * PurpleHistoryAdapter::filename:
+ *
+ * The filename that the sqlite database will store data to.
+ *
+ * Since: 3.0.0
+ */
+ properties[PROP_FILENAME] = g_param_spec_string(
+ "filename", "filename", "The filename of the sqlite database",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS
+ );
+
+ g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+PurpleHistoryAdapter *
+purple_sqlite_history_adapter_new(const gchar *filename) {
+ return g_object_new(
+ PURPLE_TYPE_SQLITE_HISTORY_ADAPTER,
+ "filename", filename,
+ "id", "sqlite-adapter",
+ "name", N_("SQLite Adapter"),
+ NULL);
+}
+
+const gchar *
+purple_sqlite_history_adapter_get_filename(PurpleSqliteHistoryAdapter *adapter) {
+ PurpleSqliteHistoryAdapterPrivate *priv = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_SQLITE_HISTORY_ADAPTER(adapter), NULL);
+
+ priv = purple_sqlite_history_adapter_get_instance_private(adapter);
+
+ return priv->filename;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplesqlitehistoryadapter.h Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,79 @@
+/*
+ * 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/>.
+ */
+
+#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
+# error "only <purple.h> may be included directly"
+#endif
+
+#ifndef PURPLE_SQLITE_HISTORY_ADAPTER_H
+#define PURPLE_SQLITE_HISTORY_ADAPTER_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <purplehistoryadapter.h>
+#include <purplemessage.h>
+
+/**
+ * SECTION:purplesqlitehistoryadapter
+ * @section_id: libpurple-purplesqlitehistoryadapter
+ * @title: SQLite History Adapter Object
+ */
+
+G_BEGIN_DECLS
+
+/**
+ * PurpleSqliteHistoryAdapter:
+ *
+ * #PurpleSqliteHistoryAdapter is a class that allows interfacing with an
+ * SQLite database to store history. It is a subclass of @PurpleHistoryAdapter.
+ *
+ * Since: 3.0.0
+ */
+
+#define PURPLE_TYPE_SQLITE_HISTORY_ADAPTER (purple_sqlite_history_adapter_get_type())
+G_DECLARE_FINAL_TYPE(PurpleSqliteHistoryAdapter, purple_sqlite_history_adapter,
+ PURPLE, SQLITE_HISTORY_ADAPTER, PurpleHistoryAdapter)
+
+/**
+ * purple_sqlite_history_adapter_new:
+ * @filename: The filename of the sqlite database.
+ *
+ * Creates a new #PurpleHistoryAdapter.
+ *
+ * Returns: (transfer full): The new #PurpleSqliteHistoryAdapter instance.
+ *
+ * Since: 3.0.0
+ */
+PurpleHistoryAdapter *purple_sqlite_history_adapter_new(const gchar *filename);
+
+/**
+ * purple_sqlite_history_adapter_get_filename
+ * @adapter: The #PurpleSqliteHistoryAdapter instance
+ *
+ * Gets the filename of the sqlite database.
+ *
+ * Returns: The filename that the @adapter reads and writes to
+ *
+ * Since: 3.0.0
+ */
+const gchar *purple_sqlite_history_adapter_get_filename(PurpleSqliteHistoryAdapter *adapter);
+
+G_END_DECLS
+
+#endif /* PURPLE_SQLITE_HISTORY_ADAPTER */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/resources/libpurple.gresource.xml Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/im/pidgin/libpurple/">
+ <file compressed="true">sqlitehistoryadapter/01-schema.sql</file>
+ </gresource>
+</gresources>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/resources/sqlitehistoryadapter/01-schema.sql Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,17 @@
+CREATE TABLE IF NOT EXISTS message_log
+(
+ protocol TEXT NOT NULL, -- examples: slack, xmpp, irc, discord
+ account TEXT NOT NULL, -- example: grim@reaperworld.com@milwaukee.slack.com
+ conversation_id TEXT NOT NULL, -- example: #general
+ message_id TEXT NOT NULL, -- exampe: 14fdjakafjakl1155
+ author TEXT NULL, -- could be null for status messages
+ author_name_color TEXT NULL,
+ author_alias TEXT NULL,
+ recipient TEXT NULL,
+ content_type TEXT NULL CHECK(content_type IN ('plain', 'html', 'markdown', 'bbcode')),
+ content TEXT NULL, -- must be UTF8 string
+ raw_content TEXT NULL, -- the message as came from the protocol
+ protocol_timestamp TEXT, -- according to protocol, could be wrong
+ client_timestamp DATETIME, -- when it "landed" in libpurple
+ log_version INTEGER DEFAULT 1 NOT NULL
+);
--- a/libpurple/tests/meson.build Mon Oct 11 23:47:26 2021 -0500
+++ b/libpurple/tests/meson.build Tue Oct 12 00:50:59 2021 -0500
@@ -4,6 +4,8 @@
'circular_buffer',
'credential_manager',
'credential_provider',
+ 'history_adapter',
+ 'history_manager',
'image',
'keyvaluepair',
'markup',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/test_history_adapter.c Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,293 @@
+/*
+ * 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/>.
+ */
+
+#include <glib.h>
+
+#include <purple.h>
+
+#include "test_ui.h"
+
+#define PURPLE_GLOBAL_HEADER_INSIDE
+#include "../purpleprivate.h"
+#undef PURPLE_GLOBAL_HEADER_INSIDE
+
+/******************************************************************************
+ * TestPurpleHistoryAdapter
+ *****************************************************************************/
+#define TEST_PURPLE_TYPE_HISTORY_ADAPTER \
+ (test_purple_history_adapter_get_type())
+G_DECLARE_FINAL_TYPE(TestPurpleHistoryAdapter,
+ test_purple_history_adapter,
+ TEST_PURPLE, HISTORY_ADAPTER,
+ PurpleHistoryAdapter)
+
+struct _TestPurpleHistoryAdapter {
+ PurpleHistoryAdapter parent;
+
+ gboolean activate;
+ gboolean deactivate;
+ gboolean query;
+ gboolean remove;
+ gboolean write;
+};
+
+G_DEFINE_TYPE(TestPurpleHistoryAdapter,
+ test_purple_history_adapter,
+ PURPLE_TYPE_HISTORY_ADAPTER)
+
+static gboolean
+test_purple_history_adapter_activate(PurpleHistoryAdapter *a, GError **error) {
+ TestPurpleHistoryAdapter *adapter = TEST_PURPLE_HISTORY_ADAPTER(a);
+
+ adapter->activate = TRUE;
+
+ return TRUE;
+}
+
+static gboolean
+test_purple_history_adapter_deactivate(PurpleHistoryAdapter *a,
+ GError **error)
+{
+ TestPurpleHistoryAdapter *adapter = TEST_PURPLE_HISTORY_ADAPTER(a);
+
+ adapter->deactivate = TRUE;
+
+ return TRUE;
+}
+
+static GList *
+test_purple_history_adapter_query(PurpleHistoryAdapter *a,
+ const gchar *query,
+ GError **error)
+{
+ TestPurpleHistoryAdapter *adapter = TEST_PURPLE_HISTORY_ADAPTER(a);
+ GList *list = NULL;
+
+ adapter->query = TRUE;
+
+ list = g_list_append(list, GINT_TO_POINTER(1));
+
+ return list;
+}
+
+static gboolean
+test_purple_history_adapter_remove(PurpleHistoryAdapter *a,
+ const gchar *query,
+ GError **error)
+{
+ TestPurpleHistoryAdapter *adapter = TEST_PURPLE_HISTORY_ADAPTER(a);
+
+ adapter->remove = TRUE;
+
+ return TRUE;
+}
+
+static gboolean
+test_purple_history_adapter_write(PurpleHistoryAdapter *a,
+ PurpleConversation *conversation,
+ PurpleMessage *message,
+ GError **error)
+{
+ TestPurpleHistoryAdapter *adapter = TEST_PURPLE_HISTORY_ADAPTER(a);
+
+ adapter->write = TRUE;
+
+ return TRUE;
+}
+
+static void
+test_purple_history_adapter_init(TestPurpleHistoryAdapter *adapter) {
+}
+
+static void
+test_purple_history_adapter_class_init(TestPurpleHistoryAdapterClass *klass) {
+ PurpleHistoryAdapterClass *adapter_class = NULL;
+
+ adapter_class = PURPLE_HISTORY_ADAPTER_CLASS(klass);
+
+ adapter_class->activate = test_purple_history_adapter_activate;
+ adapter_class->deactivate = test_purple_history_adapter_deactivate;
+ adapter_class->query = test_purple_history_adapter_query;
+ adapter_class->remove = test_purple_history_adapter_remove;
+ adapter_class->write = test_purple_history_adapter_write;
+}
+
+static PurpleHistoryAdapter *
+test_purple_history_adapter_new(void) {
+ return g_object_new(
+ TEST_PURPLE_TYPE_HISTORY_ADAPTER,
+ "id", "test-adapter",
+ "name", "Test Adapter",
+ NULL);
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+
+static void
+test_purple_history_adapter_test_properties(void) {
+ PurpleHistoryAdapter *adapter = test_purple_history_adapter_new();
+
+ g_assert_cmpstr(purple_history_adapter_get_id(adapter),
+ ==,
+ "test-adapter");
+ g_assert_cmpstr(purple_history_adapter_get_name(adapter),
+ ==,
+ "Test Adapter");
+
+ g_clear_object(&adapter);
+}
+
+static void
+test_purple_history_adapter_test_activate(void) {
+ PurpleHistoryAdapter *adapter = test_purple_history_adapter_new();
+ TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(adapter);
+ GError *error = NULL;
+ gboolean result = FALSE;
+
+ result = purple_history_adapter_activate(adapter, &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+ g_assert_true(ta->activate);
+ g_assert_false(ta->deactivate);
+ g_assert_false(ta->query);
+ g_assert_false(ta->remove);
+ g_assert_false(ta->write);
+
+ g_clear_object(&adapter);
+}
+
+static void
+test_purple_history_adapter_test_deactivate(void) {
+ PurpleHistoryAdapter *adapter = test_purple_history_adapter_new();
+ TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(adapter);
+ GError *error = NULL;
+ gboolean result = FALSE;
+
+ result = purple_history_adapter_deactivate(adapter, &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+ g_assert_false(ta->activate);
+ g_assert_true(ta->deactivate);
+ g_assert_false(ta->query);
+ g_assert_false(ta->remove);
+ g_assert_false(ta->write);
+
+ g_clear_object(&adapter);
+}
+
+static void
+test_purple_history_adapter_test_query(void) {
+ PurpleHistoryAdapter *adapter = test_purple_history_adapter_new();
+ TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(adapter);
+ GError *error = NULL;
+ GList *result = NULL;
+
+ result = purple_history_adapter_query(adapter, "query", &error);
+ g_assert_no_error(error);
+ g_assert_nonnull(result);
+ g_assert_false(ta->activate);
+ g_assert_false(ta->deactivate);
+ g_assert_true(ta->query);
+ g_assert_false(ta->remove);
+ g_assert_false(ta->write);
+ g_list_free(result);
+ g_clear_object(&adapter);
+}
+
+static void
+test_purple_history_adapter_test_remove(void) {
+ PurpleHistoryAdapter *adapter = test_purple_history_adapter_new();
+ TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(adapter);
+ GError *error = NULL;
+ gboolean result = FALSE;
+
+ result = purple_history_adapter_remove(adapter, "query", &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+ g_assert_false(ta->activate);
+ g_assert_false(ta->deactivate);
+ g_assert_false(ta->query);
+ g_assert_true(ta->remove);
+ g_assert_false(ta->write);
+
+ g_clear_object(&adapter);
+}
+
+static void
+test_purple_history_adapter_test_write(void) {
+ PurpleAccount *account = NULL;
+ PurpleConversation *conversation = NULL;
+ PurpleHistoryAdapter *adapter = test_purple_history_adapter_new();
+ PurpleMessage *message = NULL;
+ TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(adapter);
+ GError *error = NULL;
+ gboolean result = FALSE;
+
+ message = g_object_new(PURPLE_TYPE_MESSAGE, NULL);
+ account = purple_account_new("test", "test");
+ conversation = g_object_new(PURPLE_TYPE_IM_CONVERSATION,
+ "account", account,
+ "name", "pidgy",
+ NULL);
+ result = purple_history_adapter_write(adapter, conversation, message,
+ &error);
+
+ g_assert_no_error(error);
+ g_assert_true(result);
+ g_assert_false(ta->activate);
+ g_assert_false(ta->deactivate);
+ g_assert_false(ta->query);
+ g_assert_false(ta->remove);
+ g_assert_true(ta->write);
+
+ g_clear_object(&adapter);
+ g_clear_object(&message);
+
+ /* TODO: something is freeing our ref. */
+ /* g_clear_object(&account); */
+
+ g_clear_object(&conversation);
+}
+
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar *argv[]) {
+ g_test_init(&argc, &argv, NULL);
+
+ test_ui_purple_init();
+
+ g_test_add_func("/history-adapter/properties",
+ test_purple_history_adapter_test_properties);
+ g_test_add_func("/history-adapter/activate",
+ test_purple_history_adapter_test_activate);
+ g_test_add_func("/history-adapter/deactivate",
+ test_purple_history_adapter_test_deactivate);
+ g_test_add_func("/history-adapter/query",
+ test_purple_history_adapter_test_query);
+ g_test_add_func("/history-adapter/remove",
+ test_purple_history_adapter_test_remove);
+ g_test_add_func("/history-adapter/write",
+ test_purple_history_adapter_test_write);
+
+ return g_test_run();
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/test_history_manager.c Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,493 @@
+/*
+ * 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/>.
+ */
+
+#include <glib.h>
+
+#include <purple.h>
+
+#include "test_ui.h"
+
+#define PURPLE_GLOBAL_HEADER_INSIDE
+#include "../purpleprivate.h"
+#undef PURPLE_GLOBAL_HEADER_INSIDE
+
+/******************************************************************************
+ * TestPurpleHistoryAdapter Implementation
+ *****************************************************************************/
+#define TEST_PURPLE_TYPE_HISTORY_ADAPTER \
+ (test_purple_history_adapter_get_type())
+G_DECLARE_FINAL_TYPE(TestPurpleHistoryAdapter,
+ test_purple_history_adapter,
+ TEST_PURPLE, HISTORY_ADAPTER,
+ PurpleHistoryAdapter)
+
+struct _TestPurpleHistoryAdapter {
+ PurpleHistoryAdapter parent;
+
+ gboolean activate_called;
+ gboolean deactivate_called;
+ gboolean query_called;
+ gboolean remove_called;
+ gboolean write_called;
+};
+
+G_DEFINE_TYPE(TestPurpleHistoryAdapter,
+ test_purple_history_adapter,
+ PURPLE_TYPE_HISTORY_ADAPTER)
+
+static gboolean
+test_purple_history_adapter_activate(PurpleHistoryAdapter *a, GError **error)
+{
+ TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(a);
+
+ ta->activate_called = TRUE;
+
+ return TRUE;
+}
+
+static gboolean
+test_purple_history_adapter_deactivate(PurpleHistoryAdapter *a, GError **error)
+{
+ TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(a);
+
+ ta->deactivate_called = TRUE;
+
+ return TRUE;
+}
+
+static GList *
+test_purple_history_adapter_query(PurpleHistoryAdapter *a,
+ const gchar *id,
+ GError **error)
+{
+ TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(a);
+
+ ta->query_called = TRUE;
+
+ return NULL;
+}
+
+static gboolean
+test_purple_history_adapter_remove(PurpleHistoryAdapter *a,
+ const gchar *id,
+ GError **error)
+{
+ TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(a);
+
+ ta->remove_called = TRUE;
+
+ return TRUE;
+}
+
+static gboolean
+test_purple_history_adapter_write(PurpleHistoryAdapter *a,
+ PurpleConversation *conversation,
+ PurpleMessage *message,
+ GError **error)
+{
+ TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(a);
+
+ ta->write_called = TRUE;
+
+ return TRUE;
+}
+
+static void
+test_purple_history_adapter_init(TestPurpleHistoryAdapter *adapter)
+{
+}
+
+static void
+test_purple_history_adapter_class_init(TestPurpleHistoryAdapterClass *klass)
+{
+ PurpleHistoryAdapterClass *adapter_class = PURPLE_HISTORY_ADAPTER_CLASS(klass);
+
+ adapter_class->activate = test_purple_history_adapter_activate;
+ adapter_class->deactivate = test_purple_history_adapter_deactivate;
+ adapter_class->query = test_purple_history_adapter_query;
+ adapter_class->remove = test_purple_history_adapter_remove;
+ adapter_class->write = test_purple_history_adapter_write;
+}
+
+static PurpleHistoryAdapter *
+test_purple_history_adapter_new(void) {
+ return g_object_new(
+ TEST_PURPLE_TYPE_HISTORY_ADAPTER,
+ "id", "test-adapter",
+ "name", "Test Adapter",
+ NULL);
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_history_manager_get_default(void) {
+ PurpleHistoryManager *manager1 = NULL, *manager2 = NULL;
+
+ manager1 = purple_history_manager_get_default();
+ g_assert_true(PURPLE_IS_HISTORY_MANAGER(manager1));
+
+ manager2 = purple_history_manager_get_default();
+ g_assert_true(PURPLE_IS_HISTORY_MANAGER(manager2));
+
+ g_assert_true(manager1 == manager2);
+}
+
+/******************************************************************************
+ * Registration Tests
+ *****************************************************************************/
+static void
+test_purple_history_manager_registration(void) {
+ PurpleHistoryManager *manager = NULL;
+ PurpleHistoryAdapter *adapter = NULL;
+ GError *error = NULL;
+ gboolean r = FALSE;
+
+ manager = purple_history_manager_get_default();
+ g_assert_true(PURPLE_IS_HISTORY_MANAGER(manager));
+
+ adapter = test_purple_history_adapter_new();
+
+ /* Register the first time cleanly. */
+ r = purple_history_manager_register(manager, adapter, &error);
+ g_assert_no_error(error);
+ g_assert_true(r);
+
+ /* Register again and verify the error. */
+ r = purple_history_manager_register(manager, adapter, &error);
+ g_assert_false(r);
+ g_assert_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0);
+ g_clear_error(&error);
+
+ /* Unregister the adapter. */
+ r = purple_history_manager_unregister(manager, adapter, &error);
+ g_assert_no_error(error);
+ g_assert_true(r);
+
+ /* Unregister the adapter again and verify the error. */
+ r = purple_history_manager_unregister(manager, adapter, &error);
+ g_assert_false(r);
+ g_assert_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0);
+ g_clear_error(&error);
+
+ /* Final clean ups. */
+ g_clear_object(&adapter);
+}
+
+/******************************************************************************
+ * Set Active Tests
+ *****************************************************************************/
+static void
+test_purple_history_manager_set_active_null(void) {
+ PurpleHistoryManager *manager = NULL;
+ GError *error = NULL;
+ gboolean ret = FALSE;
+
+ manager = purple_history_manager_get_default();
+ ret = purple_history_manager_set_active(manager, NULL, &error);
+
+ g_assert_no_error(error);
+ g_assert_true(ret);
+}
+
+static void
+test_purple_history_manager_set_active_non_existent(void) {
+ PurpleHistoryManager *manager = NULL;
+ GError *error = NULL;
+ gboolean ret = FALSE;
+
+ manager = purple_history_manager_get_default();
+ ret = purple_history_manager_set_active(manager, "foo", &error);
+
+ g_assert_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0);
+ g_assert_false(ret);
+ g_clear_error(&error);
+}
+
+static void
+test_purple_history_manager_set_active_normal(void) {
+ PurpleHistoryManager *manager = NULL;
+ PurpleHistoryAdapter *adapter = NULL;
+ GError *error = NULL;
+ gboolean r = FALSE;
+ TestPurpleHistoryAdapter *ta = NULL;
+
+ manager = purple_history_manager_get_default();
+
+ /* Create the adapter and register it in the manager. */
+ adapter = test_purple_history_adapter_new();
+ ta = TEST_PURPLE_HISTORY_ADAPTER(adapter);
+ r = purple_history_manager_register(manager, adapter, &error);
+ g_assert_no_error(error);
+ g_assert_true(r);
+
+ /* Set the adapter as active and verify it was successful. */
+ r = purple_history_manager_set_active(manager, "test-adapter",
+ &error);
+ g_assert_no_error(error);
+ g_assert_true(r);
+ g_assert_true(ta->activate_called);
+
+ /* Verify that unregistering the active adapter fails */
+ r = purple_history_manager_unregister(manager, adapter,
+ &error);
+ g_assert_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0);
+ g_assert_false(r);
+ g_clear_error(&error);
+
+ /* Now unset the active adapter. */
+ r = purple_history_manager_set_active(manager, NULL, &error);
+ g_assert_no_error(error);
+ g_assert_true(r);
+ g_assert_true(ta->deactivate_called);
+
+ /* Finally unregister the adapter now that it's no longer active. */
+ r = purple_history_manager_unregister(manager, adapter, &error);
+ g_assert_no_error(error);
+ g_assert_true(r);
+
+ /* And our final cleanup. */
+ g_clear_object(&adapter);
+}
+
+/******************************************************************************
+ * No Adapter Tests
+ *****************************************************************************/
+static void
+test_purple_history_manager_no_adapter_query(void) {
+ PurpleHistoryManager *manager = NULL;
+ GList *list = NULL;
+ GError *error = NULL;
+
+ manager = purple_history_manager_get_default();
+ list = purple_history_manager_query(manager, "", &error);
+
+ g_assert_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0);
+ g_clear_error(&error);
+
+ g_assert_null(list);
+}
+
+static void
+test_purple_history_manager_no_adapter_remove(void) {
+ PurpleHistoryManager *manager = NULL;
+ GError *error = NULL;
+ gboolean result = FALSE;
+
+ manager = purple_history_manager_get_default();
+ result = purple_history_manager_remove(manager, "", &error);
+
+ g_assert_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0);
+ g_clear_error(&error);
+
+ g_assert_false(result);
+}
+
+static void
+test_purple_history_manager_no_adapter_write(void) {
+ PurpleAccount *account = NULL;
+ PurpleConversation *conversation = NULL;
+ PurpleHistoryManager *manager = NULL;
+ PurpleMessage *message = NULL;
+ GError *error = NULL;
+ gboolean result = FALSE;
+
+ manager = purple_history_manager_get_default();
+
+ message = g_object_new(PURPLE_TYPE_MESSAGE, NULL);
+ account = purple_account_new("test", "test");
+ conversation = g_object_new(PURPLE_TYPE_IM_CONVERSATION,
+ "account", account,
+ "name", "pidgy",
+ NULL);
+
+ result = purple_history_manager_write(manager, conversation, message,
+ &error);
+
+ g_assert_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0);
+ g_clear_error(&error);
+
+ g_assert_false(result);
+
+ /* TODO: someone is freeing our ref. */
+ /* g_clear_object(&account); */
+
+ g_clear_object(&message);
+ g_clear_object(&conversation);
+}
+
+/******************************************************************************
+ * Manager Tests
+ *****************************************************************************/
+static void
+test_purple_history_manager_adapter_query(void) {
+ PurpleHistoryManager *manager = purple_history_manager_get_default();
+ PurpleHistoryAdapter *adapter = test_purple_history_adapter_new();
+ TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(adapter);
+ GList *list = NULL;
+ GError *error = NULL;
+ gboolean result = FALSE;
+
+ result = purple_history_manager_register(manager, adapter, &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+
+ result = purple_history_manager_set_active(manager, "test-adapter",
+ &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+
+ list = purple_history_manager_query(manager, "", &error);
+ g_assert_no_error(error);
+ g_assert_null(list);
+ g_assert_true(ta->query_called);
+
+ result = purple_history_manager_set_active(manager, NULL, &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+
+ result = purple_history_manager_unregister(manager, adapter, &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+
+ g_clear_object(&adapter);
+}
+
+static void
+test_purple_history_manager_adapter_remove(void) {
+ PurpleHistoryManager *manager = purple_history_manager_get_default();
+ PurpleHistoryAdapter *adapter = test_purple_history_adapter_new();
+ TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(adapter);
+ GError *error = NULL;
+ gboolean result = FALSE;
+
+ result = purple_history_manager_register(manager, adapter, &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+
+ result = purple_history_manager_set_active(manager, "test-adapter",
+ &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+
+ result = purple_history_manager_remove(manager, "query", &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+ g_assert_true(ta->remove_called);
+
+ result = purple_history_manager_set_active(manager, NULL, &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+
+ result = purple_history_manager_unregister(manager, adapter, &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+
+ g_clear_object(&adapter);
+}
+
+static void
+test_purple_history_manager_adapter_write(void) {
+ PurpleAccount *account = NULL;
+ PurpleConversation *conversation = NULL;
+ PurpleHistoryManager *manager = purple_history_manager_get_default();
+ PurpleHistoryAdapter *adapter = test_purple_history_adapter_new();
+ PurpleMessage *message = NULL;
+ TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(adapter);
+ GError *error = NULL;
+ gboolean result = FALSE;
+
+ result = purple_history_manager_register(manager, adapter, &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+
+ result = purple_history_manager_set_active(manager, "test-adapter", &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+
+ message = g_object_new(PURPLE_TYPE_MESSAGE, NULL);
+ account = purple_account_new("test", "test");
+ conversation = g_object_new(PURPLE_TYPE_IM_CONVERSATION,
+ "account", account,
+ "name", "pidgy",
+ NULL);
+ result = purple_history_manager_write(manager, conversation, message,
+ &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+ g_assert_true(ta->write_called);
+
+ result = purple_history_manager_set_active(manager, NULL, &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+
+ result = purple_history_manager_unregister(manager, adapter, &error);
+ g_assert_no_error(error);
+ g_assert_true(result);
+
+ g_clear_object(&adapter);
+ g_clear_object(&message);
+
+ /* TODO: something is freeing our ref. */
+ /* g_clear_object(&account); */
+
+ g_clear_object(&conversation);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar *argv[]) {
+ gint ret = 0;
+
+ g_test_init(&argc, &argv, NULL);
+
+ test_ui_purple_init();
+
+ g_test_add_func("/history-manager/get-default",
+ test_purple_history_manager_get_default);
+ g_test_add_func("/history-manager/registration",
+ test_purple_history_manager_registration);
+ g_test_add_func("/history-manager/set-active/null",
+ test_purple_history_manager_set_active_null);
+ g_test_add_func("/history-manager/set-active/non-existent",
+ test_purple_history_manager_set_active_non_existent);
+ g_test_add_func("/history-manager/set-active/normal",
+ test_purple_history_manager_set_active_normal);
+
+ /* Tests for manager with an adapter */
+ g_test_add_func("/history-manager/adapter/query",
+ test_purple_history_manager_adapter_query);
+ g_test_add_func("/history-manager/adapter/remove",
+ test_purple_history_manager_adapter_remove);
+ g_test_add_func("/history-manager/adapter/write",
+ test_purple_history_manager_adapter_write);
+
+ /* Tests for manager with no adapter */
+ g_test_add_func("/history-manager/no-adapter/query",
+ test_purple_history_manager_no_adapter_query);
+ g_test_add_func("/history-manager/no-adapter/remove",
+ test_purple_history_manager_no_adapter_remove);
+ g_test_add_func("/history-manager/no-adapter/write",
+ test_purple_history_manager_no_adapter_write);
+ ret = g_test_run();
+
+ return ret;
+}
--- a/meson.build Mon Oct 11 23:47:26 2021 -0500
+++ b/meson.build Tue Oct 12 00:50:59 2021 -0500
@@ -301,6 +301,11 @@
libsoup = dependency('libsoup-2.4', version : '>= 2.42')
#######################################################################
+# Check for sqlite3 (required)
+#######################################################################
+sqlite3 = dependency('sqlite3', version : '>= 3.27.0')
+
+#######################################################################
# Check for GStreamer
#######################################################################
@@ -665,6 +670,7 @@
toplevel_inc = include_directories('.')
subdir('libpurple')
+subdir('purple-history')
subdir('finch')
subdir('pidgin')
subdir('doc')
--- a/po/POTFILES.in Mon Oct 11 23:47:26 2021 -0500
+++ b/po/POTFILES.in Tue Oct 12 00:50:59 2021 -0500
@@ -248,6 +248,8 @@
libpurple/purplecredentialprovider.c
libpurple/purpledebugui.c
libpurple/purple-gio.c
+libpurple/purplehistoryadapter.c
+libpurple/purplehistorymanager.c
libpurple/purpleimconversation.c
libpurple/purplekeyvaluepair.c
libpurple/purplemarkup.c
@@ -266,6 +268,7 @@
libpurple/purpleprotocolprivacy.c
libpurple/purpleprotocolroomlist.c
libpurple/purpleprotocolserver.c
+libpurple/purplesqlitehistoryadapter.c
libpurple/purpleuiinfo.c
libpurple/purplewhiteboard.c
libpurple/purplewhiteboardmanager.c
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/purple-history/meson.build Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,9 @@
+PURPLE_HISTORY_SOURCES = [
+ 'purplehistorycore.c',
+]
+
+purple_history = executable('purple-history',
+ PURPLE_HISTORY_SOURCES,
+ dependencies : [libpurple_dep, glib],
+ install : true)
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/purple-history/purplehistorycore.c Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,126 @@
+/*
+ * 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 <stdio.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <glib/gi18n-lib.h>
+
+#include <purple.h>
+
+#define PURPLE_COMPILATION
+#include "../libpurple/purpleprivate.h"
+#undef PURPLE_COMPILATION
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static gboolean
+purple_history_query(const gchar *query, GError **error) {
+ PurpleHistoryManager *manager = purple_history_manager_get_default();
+ GList *results = NULL;
+
+ results = purple_history_manager_query(manager, query, error);
+
+ if(error != NULL) {
+ return FALSE;
+ }
+
+ while(results != NULL) {
+ PurpleMessage *message = PURPLE_MESSAGE(results->data);
+
+ g_printf("%s: %s\n", purple_message_get_author(message),
+ purple_message_get_contents(message));
+
+ g_clear_object(&message);
+ results = g_list_delete_link(results, results);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+purple_history_remove(const gchar *query, GError **error) {
+ PurpleHistoryManager *manager = purple_history_manager_get_default();
+ gboolean success = FALSE;
+
+ success = purple_history_manager_remove(manager, query, error);
+
+ if(!success) {
+ return FALSE;
+ }
+
+ if(error) {
+ g_printf("Remove successful\n");
+ }
+
+ return TRUE;
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar *argv[]) {
+ GError *error = NULL;
+ GOptionContext *ctx = NULL;
+ GOptionGroup *group = NULL;
+ gint exit_code = EXIT_SUCCESS;
+
+ ctx = g_option_context_new(_("QUERY"));
+ g_option_context_set_help_enabled(ctx, TRUE);
+ g_option_context_set_summary(ctx, _("Query purple message history"));
+ g_option_context_set_translation_domain(ctx, GETTEXT_PACKAGE);
+
+ group = purple_get_option_group();
+ g_option_context_add_group(ctx, group);
+
+ g_option_context_parse(ctx, &argc, &argv, &error);
+ g_option_context_free(ctx);
+
+ if(error != NULL) {
+ g_fprintf(stderr, "%s\n", error->message);
+
+ g_clear_error(&error);
+
+ return EXIT_FAILURE;
+ }
+
+ purple_history_manager_startup();
+
+ for(gint i = 1; i < argc; i++) {
+ if(argv[i] == NULL || *argv[i] == '\0') {
+ continue;
+ }
+
+ if(!purple_history_query(argv[i], &error)) {
+ fprintf(stderr, "query failed: %s\n",
+ error ? error->message : "unknown error");
+
+ g_clear_error(&error);
+
+ exit_code = EXIT_FAILURE;
+ }
+ }
+
+ purple_history_manager_shutdown();
+
+ return exit_code;
+}
+