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_history_manager_startup(); @@ -249,6 +250,8 @@
purple_protocol_manager_shutdown();
+ purple_history_manager_shutdown(); /* Everything after util_uninit cannot try to write things to the
--- 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',
+ 'purplehistoryadapter.c', + 'purplehistorymanager.c', 'purpleimconversation.c',
@@ -71,6 +73,7 @@
'purpleprotocolprivacy.c',
'purpleprotocolroomlist.c',
'purpleprotocolserver.c',
+ 'purplesqlitehistoryadapter.c', 'purplewhiteboardmanager.c',
@@ -144,6 +147,8 @@
'purplecredentialmanager.h',
'purplecredentialprovider.h',
+ 'purplehistoryadapter.h', + 'purplehistorymanager.h', 'purpleimconversation.h',
@@ -163,6 +168,7 @@
'purpleprotocolprivacy.h',
'purpleprotocolroomlist.h',
'purpleprotocolserver.h',
+ 'purplesqlitehistoryadapter.h', 'purplewhiteboardmanager.h',
@@ -190,6 +196,12 @@
purple_generated_sources = []
+purple_resource = gnome.compile_resources('purpleresources', + 'resources/libpurple.gresource.xml', + source_dir : 'resources', +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 "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 @@
+ 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); + /* The following should be deleted when the history api is stable. */ dt = g_date_time_ref(purple_message_get_timestamp(pmsg));
--- /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" +} PurpleHistoryAdapterPrivate; +static GParamSpec *properties[N_PROPERTIES] = {NULL, }; +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE(PurpleHistoryAdapter, + purple_history_adapter, G_TYPE_OBJECT) +/****************************************************************************** + *****************************************************************************/ +purple_history_adapter_set_id(PurpleHistoryAdapter *adapter, const gchar *id) { + PurpleHistoryAdapterPrivate *priv = NULL; + priv = purple_history_adapter_get_instance_private(adapter); + priv->id = g_strdup(id); + g_object_notify_by_pspec(G_OBJECT(adapter), properties[PROP_ID]); +purple_history_adapter_set_name(PurpleHistoryAdapter *adapter, + PurpleHistoryAdapterPrivate *priv = NULL; + priv = purple_history_adapter_get_instance_private(adapter); + priv->name = g_strdup(name); + g_object_notify_by_pspec(G_OBJECT(adapter), properties[PROP_NAME]); +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +purple_history_adapter_get_property(GObject *obj, guint param_id, + GValue *value, GParamSpec *pspec) + PurpleHistoryAdapter *adapter = PURPLE_HISTORY_ADAPTER(obj); + g_value_set_string(value, + purple_history_adapter_get_id(adapter)); + g_value_set_string(value, + purple_history_adapter_get_name(adapter)); + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); +purple_history_adapter_set_property(GObject *obj, guint param_id, + const GValue *value, GParamSpec *pspec) + PurpleHistoryAdapter *adapter = PURPLE_HISTORY_ADAPTER(obj); + purple_history_adapter_set_id(adapter, + g_value_get_string(value)); + purple_history_adapter_set_name(adapter, + g_value_get_string(value)); + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); +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); +purple_history_adapter_init(PurpleHistoryAdapter *adapter) { +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 + properties[PROP_ID] = g_param_spec_string( + "id", "id", "The identifier of the adapter", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS + * PurpleHistoryAdapter::name: + * The name of the adapter. + properties[PROP_NAME] = g_param_spec_string( + "name", "name", "The name of the adapter", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS + g_object_class_install_properties(obj_class, N_PROPERTIES, properties); +/****************************************************************************** + *****************************************************************************/ +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))); +purple_history_adapter_deactivate(PurpleHistoryAdapter *adapter, + 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))); +/****************************************************************************** + *****************************************************************************/ +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); +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); +purple_history_adapter_query(PurpleHistoryAdapter *adapter, + 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))); +purple_history_adapter_remove(PurpleHistoryAdapter *adapter, + 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))); +purple_history_adapter_write(PurpleHistoryAdapter *adapter, + PurpleConversation *conversation, + PurpleMessage *message, + 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))); \ 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" +#ifndef PURPLE_HISTORY_ADAPTER_H +#define PURPLE_HISTORY_ADAPTER_H +#include <glib-object.h> +#include <purplemessage.h> +#include <purpleconversation.h> + * SECTION:purplehistoryadapter + * @section_id: libpurple-purplehistoryadapter + * @title: History Adapter Object + * PURPLE_HISTORY_ADAPTER_DOMAIN: + * A #GError domain for errors. +#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. +#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. +struct _PurpleHistoryAdapterClass { + 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); + /* Some extra padding to play it safe. */ + * purple_history_adapter_get_id: + * @adapter: The #PurpleHistoryAdapter instance. + * Gets the identifier of @adapter. + * Returns: The identifier of @adapter. +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. +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. +gboolean purple_history_adapter_write(PurpleHistoryAdapter *adapter, + PurpleConversation *conversation, + PurpleMessage *message, + * 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. +GList *purple_history_adapter_query(PurpleHistoryAdapter *adapter, + * 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. +gboolean purple_history_adapter_remove(PurpleHistoryAdapter *adapter, +#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" +static guint signals[N_SIGNALS] = {0, }; + PurpleHistoryAdapter *active_adapter; +} PurpleHistoryManagerPrivate; +G_DEFINE_TYPE_WITH_PRIVATE(PurpleHistoryManager, purple_history_manager, +static PurpleHistoryManager *default_manager = NULL; +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +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); +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, +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. + signals[SIG_ACTIVE_CHANGED] = g_signal_new( + G_OBJECT_CLASS_TYPE(klass), + G_STRUCT_OFFSET(PurpleHistoryManagerClass, active_changed), + 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. + signals[SIG_REGISTERED] = g_signal_new( + G_OBJECT_CLASS_TYPE(klass), + G_STRUCT_OFFSET(PurpleHistoryManagerClass, registered), + PURPLE_TYPE_HISTORY_ADAPTER); + * PurpleHistoryManager::unregistered: + * @manager: The #PurpleHistoryManager instance. + * @adapter: The #PurpleHistoryAdapter that was unregistered. + * Emitted after @adapter has been unregistered for @manager. + signals[SIG_UNREGISTERED] = g_signal_new( + G_OBJECT_CLASS_TYPE(klass), + G_STRUCT_OFFSET(PurpleHistoryManagerClass, unregistered), + PURPLE_TYPE_HISTORY_ADAPTER); +/****************************************************************************** + *****************************************************************************/ +purple_history_manager_startup(void) { + if(default_manager == NULL) { + PurpleHistoryAdapter *adapter = purple_sqlite_history_adapter_new("history.db"); + default_manager = g_object_new(PURPLE_TYPE_HISTORY_MANAGER, NULL); + if(!purple_history_manager_register(default_manager, adapter, &error)) { + g_warning("Failed to register sqlite history adapter: %s", error->message); + g_warning("Failed to register sqlite history adapter: Unknown reason"); + g_clear_object(&adapter); + purple_history_manager_set_active(default_manager, + purple_history_adapter_get_id(adapter), + g_warning("Failed to activate %s: %s", + purple_history_adapter_get_id(adapter), error->message); + g_clear_object(&adapter); +purple_history_manager_shutdown(void) { + PurpleHistoryManagerPrivate *priv = NULL; + if(default_manager == NULL) { + 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, + g_clear_object(&adapter); + g_clear_object(&default_manager); +/****************************************************************************** + *****************************************************************************/ +purple_history_manager_get_default(void) { + return default_manager; +purple_history_manager_register(PurpleHistoryManager *manager, + PurpleHistoryAdapter *adapter, + 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); + g_hash_table_insert(priv->adapters, g_strdup(id), g_object_ref(adapter)); + g_signal_emit(G_OBJECT(manager), signals[SIG_REGISTERED], 0, adapter); +purple_history_manager_unregister(PurpleHistoryManager *manager, + PurpleHistoryAdapter *adapter, + 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); + if(adapter == priv->active_adapter) { + g_set_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0, + _("adapter %s is currently in use"), id); + 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, + g_set_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0, + _("adapter %s is not registered"), id); + g_object_unref(G_OBJECT(adapter)); +purple_history_manager_find(PurpleHistoryManager *manager, const gchar *id) { + PurpleHistoryManagerPrivate *priv = 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); + return PURPLE_HISTORY_ADAPTER(value); +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); +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; +purple_history_manager_set_active(PurpleHistoryManager *manager, + 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. */ + 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); + 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); + 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_signal_emit(G_OBJECT(manager), signals[SIG_ACTIVE_CHANGED], 0, old, + purple_debug_info("history-manager", "set active adapter to '%s'", id); +purple_history_manager_query(PurpleHistoryManager *manager, + 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 purple_history_adapter_query(priv->active_adapter, query, error); +purple_history_manager_remove(PurpleHistoryManager *manager, + 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 purple_history_adapter_remove(priv->active_adapter, query, error); +purple_history_manager_write(PurpleHistoryManager *manager, + PurpleConversation *conversation, + PurpleMessage *message, + 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 purple_history_adapter_write(priv->active_adapter, conversation, +purple_history_manager_foreach(PurpleHistoryManager *manager, + PurpleHistoryManagerForeachFunc func, + PurpleHistoryManagerPrivate *priv = NULL; + 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" +#ifndef PURPLE_HISTORY_MANAGER_H +#define PURPLE_HISTORY_MANAGER_H +#include <glib-object.h> +#include "purplehistoryadapter.h" + * 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. +#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. + * 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. +struct _PurpleHistoryManagerClass { + 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); + * PurpleHistoryManagerForeachFunc: + * @adapter: The #PurpleHistoryAdapter instance. + * @data: User supplied data. + * A function to be used as a callback with + * purple_history_manager_foreach(). +typedef void (*PurpleHistoryManagerForeachFunc)(PurpleHistoryAdapter *adapter, gpointer data); + * purple_history_manager_get_default: + * Gets the default #PurpleHistoryManager instance. + * Returns: (transfer none): The default #PurpleHistoryManager instance. +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 + 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 +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, +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, +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. +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 + * Returns: (transfer container) (element-type PurpleHistoryAdapter): The list + * containing all of the #PurpleHistoryAdapter's registered with @manager. +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 +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. +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. +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. +void purple_history_manager_foreach(PurpleHistoryManager *manager, PurpleHistoryManagerForeachFunc func, gpointer data); +#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 "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. +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. +gboolean purple_history_adapter_deactivate(PurpleHistoryAdapter *adapter, GError **error); + * purple_history_manager_startup: + * Starts up the history manager by creating the default instance. +void purple_history_manager_startup(void); + * purple_history_manager_shutdown: + * Shuts down the history manager by destroying the default instance. +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);
#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 "purpleprivate.h" +#include "purpleresources.h" +struct _PurpleSqliteHistoryAdapter { + PurpleHistoryAdapter parent; +} PurpleSqliteHistoryAdapterPrivate; +static GParamSpec *properties[N_PROPERTIES] = {NULL, }; +G_DEFINE_TYPE_WITH_PRIVATE(PurpleSqliteHistoryAdapter, + purple_sqlite_history_adapter, + PURPLE_TYPE_HISTORY_ADAPTER) +/****************************************************************************** + *****************************************************************************/ +purple_sqlite_history_adapter_set_filename(PurpleSqliteHistoryAdapter *adapter, + 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]); +purple_sqlite_history_adapter_run_migrations(PurpleSqliteHistoryAdapter *adapter, + 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); + script = (const gchar *)g_bytes_get_data(bytes, NULL); + sqlite3_exec(priv->db, script, NULL, NULL, &error_msg); + if(error_msg != NULL) { + g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0, + "failed to run migrations: %s", error_msg); + sqlite3_free(error_msg); +purple_sqlite_history_adapter_get_content_type(PurpleMessageContentType content_type) { + case PURPLE_MESSAGE_CONTENT_TYPE_PLAIN: + case PURPLE_MESSAGE_CONTENT_TYPE_HTML: + case PURPLE_MESSAGE_CONTENT_TYPE_XHTML: + case PURPLE_MESSAGE_CONTENT_TYPE_MARKDOWN: +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; +purple_sqlite_history_adapter_build_query(PurpleSqliteHistoryAdapter *adapter, + const gchar * search_query, + GList *keywords = NULL; + gboolean first = FALSE; + sqlite3_stmt *prepared_statement = NULL; + PurpleSqliteHistoryAdapterPrivate *priv = NULL; + 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') { + ins = g_list_prepend(ins, g_strdup(split[i]+3)); + } else if(g_str_has_prefix(split[i], "from:")) { + if(split[i][5] == '\0') { + froms = g_list_prepend(froms, g_strdup(split[i]+5)); + if(split[i][0] == '\0') { + keywords = g_list_prepend(keywords, + g_strdup_printf("%%%s%%", split[i])); + g_clear_pointer(&split, g_strfreev); + query = g_string_new("DELETE FROM message_log WHERE TRUE\n"); + g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0, + "Attempting to remove messages without " + 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"); + g_string_append(query, "AND (conversation_id IN ("); + for(iter = ins; iter != NULL; iter = iter->next) { + g_string_append(query, ", "); + g_string_append(query, "?"); + g_string_append(query, "))"); + g_string_append(query, "AND (author IN ("); + for(iter = froms; iter != NULL; iter = iter->next) { + g_string_append(query, ", "); + g_string_append(query, "?"); + g_string_append(query, "))"); + g_string_append(query, "AND ("); + for(iter = keywords; iter != NULL; iter = iter->next) { + g_string_append(query, " OR "); + 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); + sqlite3_bind_text(prepared_statement, index++, + (const char *)ins->data, -1, g_free); + ins = g_list_delete_link(ins, ins); + 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 + *****************************************************************************/ +purple_sqlite_history_adapter_activate(PurpleHistoryAdapter *adapter, + 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_set_error_literal(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0, + _("Adapter has already been activated")); + if(priv->filename == NULL) { + g_set_error_literal(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0, + _("No filename specified")); + rc = sqlite3_open(priv->filename, &priv->db); + 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); + if(!purple_sqlite_history_adapter_run_migrations(sqlite_adapter, error)) { + g_clear_pointer(&priv->db, sqlite3_close); +purple_sqlite_history_adapter_deactivate(PurpleHistoryAdapter *adapter, + 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); +purple_sqlite_history_adapter_query(PurpleHistoryAdapter *adapter, + const gchar *query, GError **error) + PurpleSqliteHistoryAdapter *sqlite_adapter = NULL; + PurpleSqliteHistoryAdapterPrivate *priv = NULL; + sqlite3_stmt *prepared_statement = NULL; + sqlite_adapter = PURPLE_SQLITE_HISTORY_ADAPTER(adapter); + priv = purple_sqlite_history_adapter_get_instance_private(sqlite_adapter); + g_set_error_literal(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0, + _("Adapter has not been activated")); + prepared_statement = purple_sqlite_history_adapter_build_query(sqlite_adapter, + if(prepared_statement == 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, + "author_name_color", author_name_color, + "author_alias", author_alias, + "recipient", recipient, + "timestamp", g_date_time, + results = g_list_prepend(results, message); + results = g_list_reverse(results); + sqlite3_finalize(prepared_statement); +purple_sqlite_history_adapter_remove(PurpleHistoryAdapter *adapter, + const gchar *query, GError **error) + PurpleSqliteHistoryAdapter *sqlite_adapter = NULL; + PurpleSqliteHistoryAdapterPrivate *priv = NULL; + sqlite3_stmt * prepared_statement = NULL; + sqlite_adapter = PURPLE_SQLITE_HISTORY_ADAPTER(adapter); + priv = purple_sqlite_history_adapter_get_instance_private(sqlite_adapter); + g_set_error_literal(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0, + _("Adapter has not been activated")); + prepared_statement = purple_sqlite_history_adapter_build_query(sqlite_adapter, + if(prepared_statement == NULL) { + 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); + sqlite3_finalize(prepared_statement); +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; + 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); + g_set_error_literal(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0, + _("Adapter has not been activated")); + 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)); + account = purple_conversation_get_account(conversation); + sqlite3_bind_text(prepared_statement, + 1, purple_account_get_protocol_name(account), -1, + sqlite3_bind_text(prepared_statement, + 2, purple_account_get_username(account), -1, + sqlite3_bind_text(prepared_statement, + 3, purple_conversation_get_name(conversation), -1, + message_id = purple_message_get_id(message); + if(message_id != NULL) { + sqlite3_bind_text(prepared_statement, 4, message_id, -1, + sqlite3_bind_text(prepared_statement, 4, g_uuid_string_random(), -1, + sqlite3_bind_text(prepared_statement, + 5, purple_message_get_author(message), -1, + sqlite3_bind_text(prepared_statement, + 6, purple_message_get_author_name_color(message), -1, + sqlite3_bind_text(prepared_statement, + 7, purple_message_get_author_alias(message), -1, + sqlite3_bind_text(prepared_statement, + 8, purple_message_get_recipient(message), -1, + 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, + 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); + sqlite3_finalize(prepared_statement); +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +purple_sqlite_history_adapter_get_property(GObject *obj, guint param_id, + GValue *value, GParamSpec *pspec) + PurpleSqliteHistoryAdapter *adapter = PURPLE_SQLITE_HISTORY_ADAPTER(obj); + g_value_set_string(value, + purple_sqlite_history_adapter_get_filename(adapter)); + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); +purple_sqlite_history_adapter_set_property(GObject *obj, guint param_id, + PurpleSqliteHistoryAdapter *adapter = PURPLE_SQLITE_HISTORY_ADAPTER(obj); + purple_sqlite_history_adapter_set_filename(adapter, + g_value_get_string(value)); + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); +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); + g_warning("PurpleSqliteHistoryAdapter was finalized before being " + g_clear_pointer(&priv->db, sqlite3_close); + G_OBJECT_CLASS(purple_sqlite_history_adapter_parent_class)->finalize(obj); +purple_sqlite_history_adapter_init(PurpleSqliteHistoryAdapter *adapter) { +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. + properties[PROP_FILENAME] = g_param_spec_string( + "filename", "filename", "The filename of the sqlite database", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS + g_object_class_install_properties(obj_class, N_PROPERTIES, properties); +/****************************************************************************** + *****************************************************************************/ +purple_sqlite_history_adapter_new(const gchar *filename) { + PURPLE_TYPE_SQLITE_HISTORY_ADAPTER, + "id", "sqlite-adapter", + "name", N_("SQLite Adapter"), +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); --- /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" +#ifndef PURPLE_SQLITE_HISTORY_ADAPTER_H +#define PURPLE_SQLITE_HISTORY_ADAPTER_H +#include <glib-object.h> +#include <purplehistoryadapter.h> +#include <purplemessage.h> + * SECTION:purplesqlitehistoryadapter + * @section_id: libpurple-purplesqlitehistoryadapter + * @title: SQLite History Adapter Object + * PurpleSqliteHistoryAdapter: + * #PurpleSqliteHistoryAdapter is a class that allows interfacing with an + * SQLite database to store history. It is a subclass of @PurpleHistoryAdapter. +#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. +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 +const gchar *purple_sqlite_history_adapter_get_filename(PurpleSqliteHistoryAdapter *adapter); +#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"?> + <gresource prefix="/im/pidgin/libpurple/"> + <file compressed="true">sqlitehistoryadapter/01-schema.sql</file> --- /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, + 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 @@
--- /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/>. +#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, +struct _TestPurpleHistoryAdapter { + PurpleHistoryAdapter parent; +G_DEFINE_TYPE(TestPurpleHistoryAdapter, + test_purple_history_adapter, + PURPLE_TYPE_HISTORY_ADAPTER) +test_purple_history_adapter_activate(PurpleHistoryAdapter *a, GError **error) { + TestPurpleHistoryAdapter *adapter = TEST_PURPLE_HISTORY_ADAPTER(a); + adapter->activate = TRUE; +test_purple_history_adapter_deactivate(PurpleHistoryAdapter *a, + TestPurpleHistoryAdapter *adapter = TEST_PURPLE_HISTORY_ADAPTER(a); + adapter->deactivate = TRUE; +test_purple_history_adapter_query(PurpleHistoryAdapter *a, + TestPurpleHistoryAdapter *adapter = TEST_PURPLE_HISTORY_ADAPTER(a); + list = g_list_append(list, GINT_TO_POINTER(1)); +test_purple_history_adapter_remove(PurpleHistoryAdapter *a, + TestPurpleHistoryAdapter *adapter = TEST_PURPLE_HISTORY_ADAPTER(a); + adapter->remove = TRUE; +test_purple_history_adapter_write(PurpleHistoryAdapter *a, + PurpleConversation *conversation, + PurpleMessage *message, + TestPurpleHistoryAdapter *adapter = TEST_PURPLE_HISTORY_ADAPTER(a); +test_purple_history_adapter_init(TestPurpleHistoryAdapter *adapter) { +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) { + TEST_PURPLE_TYPE_HISTORY_ADAPTER, + "name", "Test Adapter", +/****************************************************************************** + *****************************************************************************/ +test_purple_history_adapter_test_properties(void) { + PurpleHistoryAdapter *adapter = test_purple_history_adapter_new(); + g_assert_cmpstr(purple_history_adapter_get_id(adapter), + g_assert_cmpstr(purple_history_adapter_get_name(adapter), + g_clear_object(&adapter); +test_purple_history_adapter_test_activate(void) { + PurpleHistoryAdapter *adapter = test_purple_history_adapter_new(); + TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(adapter); + gboolean result = FALSE; + result = purple_history_adapter_activate(adapter, &error); + g_assert_no_error(error); + 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); +test_purple_history_adapter_test_deactivate(void) { + PurpleHistoryAdapter *adapter = test_purple_history_adapter_new(); + TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(adapter); + gboolean result = FALSE; + result = purple_history_adapter_deactivate(adapter, &error); + g_assert_no_error(error); + 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); +test_purple_history_adapter_test_query(void) { + PurpleHistoryAdapter *adapter = test_purple_history_adapter_new(); + TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(adapter); + 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_clear_object(&adapter); +test_purple_history_adapter_test_remove(void) { + PurpleHistoryAdapter *adapter = test_purple_history_adapter_new(); + TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(adapter); + gboolean result = FALSE; + result = purple_history_adapter_remove(adapter, "query", &error); + g_assert_no_error(error); + 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); +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); + 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, + result = purple_history_adapter_write(adapter, conversation, message, + g_assert_no_error(error); + 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 argc, gchar *argv[]) { + g_test_init(&argc, &argv, NULL); + 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); --- /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/>. +#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, +struct _TestPurpleHistoryAdapter { + PurpleHistoryAdapter parent; + gboolean activate_called; + gboolean deactivate_called; + gboolean remove_called; +G_DEFINE_TYPE(TestPurpleHistoryAdapter, + test_purple_history_adapter, + PURPLE_TYPE_HISTORY_ADAPTER) +test_purple_history_adapter_activate(PurpleHistoryAdapter *a, GError **error) + TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(a); + ta->activate_called = TRUE; +test_purple_history_adapter_deactivate(PurpleHistoryAdapter *a, GError **error) + TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(a); + ta->deactivate_called = TRUE; +test_purple_history_adapter_query(PurpleHistoryAdapter *a, + TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(a); + ta->query_called = TRUE; +test_purple_history_adapter_remove(PurpleHistoryAdapter *a, + TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(a); + ta->remove_called = TRUE; +test_purple_history_adapter_write(PurpleHistoryAdapter *a, + PurpleConversation *conversation, + PurpleMessage *message, + TestPurpleHistoryAdapter *ta = TEST_PURPLE_HISTORY_ADAPTER(a); + ta->write_called = TRUE; +test_purple_history_adapter_init(TestPurpleHistoryAdapter *adapter) +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) { + TEST_PURPLE_TYPE_HISTORY_ADAPTER, + "name", "Test Adapter", +/****************************************************************************** + *****************************************************************************/ +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); +/****************************************************************************** + *****************************************************************************/ +test_purple_history_manager_registration(void) { + PurpleHistoryManager *manager = NULL; + PurpleHistoryAdapter *adapter = NULL; + 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); + /* Register again and verify the error. */ + r = purple_history_manager_register(manager, adapter, &error); + g_assert_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0); + /* Unregister the adapter. */ + r = purple_history_manager_unregister(manager, adapter, &error); + g_assert_no_error(error); + /* Unregister the adapter again and verify the error. */ + r = purple_history_manager_unregister(manager, adapter, &error); + g_assert_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0); + g_clear_object(&adapter); +/****************************************************************************** + *****************************************************************************/ +test_purple_history_manager_set_active_null(void) { + PurpleHistoryManager *manager = NULL; + manager = purple_history_manager_get_default(); + ret = purple_history_manager_set_active(manager, NULL, &error); + g_assert_no_error(error); +test_purple_history_manager_set_active_non_existent(void) { + PurpleHistoryManager *manager = NULL; + manager = purple_history_manager_get_default(); + ret = purple_history_manager_set_active(manager, "foo", &error); + g_assert_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0); +test_purple_history_manager_set_active_normal(void) { + PurpleHistoryManager *manager = NULL; + PurpleHistoryAdapter *adapter = NULL; + 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); + /* Set the adapter as active and verify it was successful. */ + r = purple_history_manager_set_active(manager, "test-adapter", + g_assert_no_error(error); + g_assert_true(ta->activate_called); + /* Verify that unregistering the active adapter fails */ + r = purple_history_manager_unregister(manager, adapter, + g_assert_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0); + /* Now unset the active adapter. */ + r = purple_history_manager_set_active(manager, NULL, &error); + g_assert_no_error(error); + 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); + /* And our final cleanup. */ + g_clear_object(&adapter); +/****************************************************************************** + *****************************************************************************/ +test_purple_history_manager_no_adapter_query(void) { + PurpleHistoryManager *manager = NULL; + manager = purple_history_manager_get_default(); + list = purple_history_manager_query(manager, "", &error); + g_assert_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0); +test_purple_history_manager_no_adapter_remove(void) { + PurpleHistoryManager *manager = 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_assert_false(result); +test_purple_history_manager_no_adapter_write(void) { + PurpleAccount *account = NULL; + PurpleConversation *conversation = NULL; + PurpleHistoryManager *manager = NULL; + PurpleMessage *message = 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, + result = purple_history_manager_write(manager, conversation, message, + g_assert_error(error, PURPLE_HISTORY_MANAGER_DOMAIN, 0); + g_assert_false(result); + /* TODO: someone is freeing our ref. */ + /* g_clear_object(&account); */ + g_clear_object(&message); + g_clear_object(&conversation); +/****************************************************************************** + *****************************************************************************/ +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); + gboolean result = FALSE; + result = purple_history_manager_register(manager, adapter, &error); + g_assert_no_error(error); + result = purple_history_manager_set_active(manager, "test-adapter", + g_assert_no_error(error); + list = purple_history_manager_query(manager, "", &error); + g_assert_no_error(error); + g_assert_true(ta->query_called); + result = purple_history_manager_set_active(manager, NULL, &error); + g_assert_no_error(error); + result = purple_history_manager_unregister(manager, adapter, &error); + g_assert_no_error(error); + g_clear_object(&adapter); +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); + gboolean result = FALSE; + result = purple_history_manager_register(manager, adapter, &error); + g_assert_no_error(error); + result = purple_history_manager_set_active(manager, "test-adapter", + g_assert_no_error(error); + result = purple_history_manager_remove(manager, "query", &error); + g_assert_no_error(error); + g_assert_true(ta->remove_called); + result = purple_history_manager_set_active(manager, NULL, &error); + g_assert_no_error(error); + result = purple_history_manager_unregister(manager, adapter, &error); + g_assert_no_error(error); + g_clear_object(&adapter); +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); + gboolean result = FALSE; + result = purple_history_manager_register(manager, adapter, &error); + g_assert_no_error(error); + result = purple_history_manager_set_active(manager, "test-adapter", &error); + g_assert_no_error(error); + message = g_object_new(PURPLE_TYPE_MESSAGE, NULL); + account = purple_account_new("test", "test"); + conversation = g_object_new(PURPLE_TYPE_IM_CONVERSATION, + result = purple_history_manager_write(manager, conversation, message, + g_assert_no_error(error); + g_assert_true(ta->write_called); + result = purple_history_manager_set_active(manager, NULL, &error); + g_assert_no_error(error); + result = purple_history_manager_unregister(manager, adapter, &error); + g_assert_no_error(error); + g_clear_object(&adapter); + g_clear_object(&message); + /* TODO: something is freeing our ref. */ + /* g_clear_object(&account); */ + g_clear_object(&conversation); +/****************************************************************************** + *****************************************************************************/ +main(gint argc, gchar *argv[]) { + g_test_init(&argc, &argv, NULL); + 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); --- 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') +####################################################################### #######################################################################
--- 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/purplehistoryadapter.c +libpurple/purplehistorymanager.c libpurple/purpleimconversation.c
libpurple/purplekeyvaluepair.c
@@ -266,6 +268,7 @@
libpurple/purpleprotocolprivacy.c
libpurple/purpleprotocolroomlist.c
libpurple/purpleprotocolserver.c
+libpurple/purplesqlitehistoryadapter.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 = [ +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 <glib/gstdio.h> +#include <glib/gi18n-lib.h> +#define PURPLE_COMPILATION +#include "../libpurple/purpleprivate.h" +#undef PURPLE_COMPILATION +/****************************************************************************** + *****************************************************************************/ +purple_history_query(const gchar *query, GError **error) { + PurpleHistoryManager *manager = purple_history_manager_get_default(); + results = purple_history_manager_query(manager, query, error); + 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); +purple_history_remove(const gchar *query, GError **error) { + PurpleHistoryManager *manager = purple_history_manager_get_default(); + gboolean success = FALSE; + success = purple_history_manager_remove(manager, query, error); + g_printf("Remove successful\n"); +/****************************************************************************** + *****************************************************************************/ +main(gint argc, gchar *argv[]) { + 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); + g_fprintf(stderr, "%s\n", error->message); + purple_history_manager_startup(); + for(gint i = 1; i < argc; i++) { + if(argv[i] == NULL || *argv[i] == '\0') { + if(!purple_history_query(argv[i], &error)) { + fprintf(stderr, "query failed: %s\n", + error ? error->message : "unknown error"); + exit_code = EXIT_FAILURE; + purple_history_manager_shutdown();