rewtguy/pidgin

Collapsed revision
draft history-api tip
2021-10-08, Gary Kramlich
00e6f98b4243
Parents d19ef491d1aa
Children
Collapsed revision
* Bootstrap the PurpleHistoryAdapter class
* HistoryApi - added name property and added write, query, remove methods
* Fix minor issues in purplehistoryadapter.
* Add unit tests for purple_history_adapter. Initial scaffolding for purple_history_manager.
* Add history manager. Start of adding unit tests for history manager. Slight modification of formatting of test_histroy_adapter. Add classes to POTFILES.in.
* Updates to history manager and unit tests
* Add test_purple_history_manager.c
* Fixed history manager and unit tests. Should be in a better state now.
* Fixed missing curly brace in history manager.
* Made changes to purplehistorymanager.h documentation.
* Add a dependency on sqlite3. 3.27.0 is kind of an arbitrary version we can adjust if necessary.
* Update _purple_conversation_write_common to use the history api.
* Initial implementation of purplesqlitehistoryadapter
* Add in activate and deactivate for purplesqlitehistoryadapter
* Embed our sqlite history schema into libpurple and run it during startup
* Implement shutdown in purplehistorymanager, implement write in purplesqlitehistoryadapter, modify schema for history adapter.
* Finish purplesqlitehistoryadapter write functionality. Changed api to include PurpleConversation.
* History API - flushed out sqlite query builder, query, and remove.
* Create a stub purple-history command line tool
* Add a simple cli for the history api and fix a few bugs
* Fixed signatures for query and remove
* Fixed memory leaks in sqlite history adapter
* Add remove function in purplehistorycore and update unit test for history adapter
* Fixed purple history core not printing results from query
* Addressed PR comments
* Add additional docs, fixed error messages, finished implementing query in purple history.
* Addressed PR comments. Fixed style, some objects weren't being dereferenced but now are, fixed issues in documentation.
* Fixed docs, grouped sqlite3 dependency check with libpurple, enabled g_option set help for history core.
* Fixed style issues. Fixed unfreed references. Add in checks against empty strings for sqlite adapter. Fixed documentation.
* Address PR comments. Fixed incorrect parameters in activate signal. Fix some styling problems. Removed unnecessary frees. Fixed compiler warnings.
* Address additional PR comments for history adapter.
* Address review findings
* Fix up the unit tests
--- a/doc/reference/libpurple/libpurple-docs.xml Thu Oct 07 22:28:08 2021 -0500
+++ b/doc/reference/libpurple/libpurple-docs.xml Fri Oct 08 03:38:29 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 Thu Oct 07 22:28:08 2021 -0500
+++ b/libpurple/core.c Fri Oct 08 03:38:29 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 Thu Oct 07 22:28:08 2021 -0500
+++ b/libpurple/meson.build Fri Oct 08 03:38:29 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 Thu Oct 07 22:28:08 2021 -0500
+++ b/libpurple/purpleconversation.c Fri Oct 08 03:38:29 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 Fri Oct 08 03:38:29 2021 -0500
@@ -0,0 +1,297 @@
+/*
+ * 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;
+
+ 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;
+
+ 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 Fri Oct 08 03:38:29 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: (out) (optional) (nullable): 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: (out) (optional) (nullable): 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: (out) (optional) (nullable): 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 Fri Oct 08 03:38:29 2021 -0500
@@ -0,0 +1,462 @@
+/*
+ * 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"
+
+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 = purple_sqlite_history_adapter_new("history.db");
+ GError *error = NULL;
+
+ 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 = PURPLE_HISTORY_ADAPTER(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)) {
+ 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 Fri Oct 08 03:38:29 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: (out) (optional) (nullable): 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: (out) (optional) (nullable): 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: (out) (optional) (nullable): 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: (out) (optional) (nullable): 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: (out) (optional) (nullable): 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: (out) (optional) (nullable): 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 Thu Oct 07 22:28:08 2021 -0500
+++ b/libpurple/purpleprivate.h Fri Oct 08 03:38:29 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: (out) (optional): 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: (out) (optional): 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 Fri Oct 08 03:38:29 2021 -0500
@@ -0,0 +1,677 @@
+/*
+ * 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;
+ }
+
+ sqlite3_step(prepared_statement);
+
+ 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 Fri Oct 08 03:38:29 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 Fri Oct 08 03:38:29 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 Fri Oct 08 03:38:29 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 Thu Oct 07 22:28:08 2021 -0500
+++ b/libpurple/tests/meson.build Fri Oct 08 03:38:29 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 Fri Oct 08 03:38:29 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 Fri Oct 08 03:38:29 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 Thu Oct 07 22:28:08 2021 -0500
+++ b/meson.build Fri Oct 08 03:38:29 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
#######################################################################
--- a/po/POTFILES.in Thu Oct 07 22:28:08 2021 -0500
+++ b/po/POTFILES.in Fri Oct 08 03:38:29 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 Fri Oct 08 03:38:29 2021 -0500
@@ -0,0 +1,8 @@
+PURPLE_HISTORY_SOURCES = [
+ 'purplehistorycore.c',
+]
+
+purple_history = executable('purple-history',
+ PURPLE_HISTORY_SOURCES,
+ dependencies : [libpurple_dep, glib])
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/purple-history/purplehistorycore.c Fri Oct 08 03:38:29 2021 -0500
@@ -0,0 +1,129 @@
+/*
+ * 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();
+ GError *error = NULL;
+ 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++) {
+ GError *error = NULL;
+
+ 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;
+}
+