GSoC History API including sqlite history adapter
The History API has been created to drive all message handling in purple3. It will be used to update existing messages for edits, reactions, pinning, read/deliver receipts, etc. The API uses an adapter pattern, to abstract out backends, but provides a SQLite3 backend by default.
It also provides search capabilities using a custom query language that can easily be expanded over time. It will be use by both the end user to search messages and the frontends to implement features like a pinned messages button. A command line utility is also provided for searching outside of the program itself.
## Remaining Items
**These all will most likely be done by the Pidgin core team after GSoC when we figure out exactly how to solve them.**
Need to store database in purple config directory
* Gary has spent some time looking at this and it looks like the purple-history cli will need to become a purple-ui to make this work write as in the future other adapters will be plugins.
Other things to consider:
- For simplicity, the SqliteHistoryAdapter is parsing the query itself, but for consistency having `PurpleHistoryAdapter` parse the query and pass tokens to the subclass might be something we want to do.
Testing Done:
## Unit Tests
History Manager
History Adapter
## Integration Tests
purplehistorycore created for integration tests.
PurpleSqliteHistoryAdapter functionality tested:
- Creates proper db schema
- Writes logs
- Reads logs
- Queries using query language
- Deletes using query language
Bugs closed: PIDGIN-17526, PIDGIN-17532, PIDGIN-17533, PIDGIN-17534
Reviewed at https://reviews.imfreedom.org/r/877/
--- a/doc/reference/libpurple/libpurple-docs.xml Mon Oct 11 23:47:26 2021 -0500
+++ b/doc/reference/libpurple/libpurple-docs.xml Tue Oct 12 00:50:59 2021 -0500
@@ -73,6 +73,8 @@
<xi:include href="xml/purpleconversationmanager.xml" />
<xi:include href="xml/purpleconversationuiops.xml" />
<xi:include href="xml/purpledebugui.xml" />
+ <xi:include href="xml/purplehistoryadapter.xml" /> + <xi:include href="xml/purplehistorymanager.xml" /> <xi:include href="xml/purpleimconversation.xml" />
<xi:include href="xml/purplekeyvaluepair.xml" />
<xi:include href="xml/purplemarkup.xml" />
--- a/libpurple/core.c Mon Oct 11 23:47:26 2021 -0500
+++ b/libpurple/core.c Tue Oct 12 00:50:59 2021 -0500
@@ -173,6 +173,7 @@
purple_whiteboard_manager_startup();
+ purple_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 Mon Oct 11 23:47:26 2021 -0500
+++ b/libpurple/meson.build Tue Oct 12 00:50:59 2021 -0500
@@ -53,6 +53,8 @@
'purplecredentialmanager.c',
'purplecredentialprovider.c',
+ '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 Mon Oct 11 23:47:26 2021 -0500
+++ b/libpurple/purpleconversation.c Tue Oct 12 00:50:59 2021 -0500
@@ -33,6 +33,7 @@
#include "purpleconversation.h"
#include "purpleconversationmanager.h"
+#include "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 Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,301 @@
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <https://www.gnu.org/licenses/>. +#include "purplehistoryadapter.h" +#include "purpleprivate.h" +} 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; + g_return_val_if_fail(PURPLE_IS_HISTORY_ADAPTER(adapter), FALSE); + klass = PURPLE_HISTORY_ADAPTER_GET_CLASS(adapter); + if(klass != NULL && klass->activate != NULL) { + return klass->activate(adapter, error); + g_set_error(error, PURPLE_HISTORY_ADAPTER_DOMAIN, 0, + "%s does not implement the activate function.", + G_OBJECT_TYPE_NAME(G_OBJECT(adapter))); +purple_history_adapter_deactivate(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->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 Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,164 @@
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see <https://www.gnu.org/licenses/>. +#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION) +# error "only <purple.h> may be included directly" +#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: 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: 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: 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 Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,472 @@
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <https://www.gnu.org/licenses/>. +#include <glib/gi18n-lib.h> +#include "purplehistorymanager.h" +#include "purplehistoryadapter.h" +#include "purplesqlitehistoryadapter.h" +#include "purpleprivate.h" +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 = NULL; + gchar *filename = NULL; + filename = g_build_filename(purple_config_dir(), "history.db", NULL); + adapter = purple_sqlite_history_adapter_new(filename); + 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 = 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)) { + if(PURPLE_IS_HISTORY_ADAPTER(old)) { + 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 Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,258 @@
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <https://www.gnu.org/licenses/>. +#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION) +# error "only <pidgin.h> may be included directly" +#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: 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: 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: 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: 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: 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: 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 Mon Oct 11 23:47:26 2021 -0500
+++ b/libpurple/purpleprivate.h Tue Oct 12 00:50:59 2021 -0500
@@ -32,6 +32,7 @@
#include "purplecredentialprovider.h"
+#include "purplehistoryadapter.h" #define PURPLE_STATIC_ASSERT(condition, message) \
{ typedef char static_assertion_failed_ ## message \
@@ -256,6 +257,52 @@
void purple_credential_provider_deactivate(PurpleCredentialProvider *provider);
+ * purple_history_adapter_activate: + * @adapter: The #PurpleHistoryAdapter instance. + * @error: A return address for a #GError. + * Asks @adapter to become the active adapter. If @adapter can not become active + * it should return %FALSE and set @error. + * Returns: %TRUE on success otherwise %FALSE with @error set. +gboolean purple_history_adapter_activate(PurpleHistoryAdapter *adapter, GError **error); + * purple_history_adapter_deactivate: + * @adapter: The #PurpleHistoryAdapter instance. + * @error: A return address for a #GError. + * Asks @adapter to stop being the active adapter. If @adapter can not + * deactivate it should return %FALSE and set @error. + * Returns: %TRUE on success otherwise %FALSE with @error set. +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 Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,675 @@
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <https://www.gnu.org/licenses/>. +#include <glib/gi18n-lib.h> +#include "purplesqlitehistoryadapter.h" +#include "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) { + 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 Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,79 @@
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see <https://www.gnu.org/licenses/>. +#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION) +# error "only <purple.h> may be included directly" +#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 Tue Oct 12 00:50:59 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 Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,17 @@
+CREATE TABLE IF NOT EXISTS message_log + protocol TEXT NOT NULL, -- examples: slack, xmpp, irc, discord + account TEXT NOT NULL, -- example: grim@reaperworld.com@milwaukee.slack.com + conversation_id TEXT NOT NULL, -- example: #general + message_id TEXT NOT NULL, -- exampe: 14fdjakafjakl1155 + author TEXT NULL, -- could be null for status messages + author_name_color TEXT NULL, + author_alias TEXT NULL, + content_type TEXT NULL CHECK(content_type IN ('plain', 'html', 'markdown', 'bbcode')), + content TEXT NULL, -- must be UTF8 string + raw_content TEXT NULL, -- the message as came from the protocol + protocol_timestamp TEXT, -- according to protocol, could be wrong + client_timestamp DATETIME, -- when it "landed" in libpurple + log_version INTEGER DEFAULT 1 NOT NULL --- a/libpurple/tests/meson.build Mon Oct 11 23:47:26 2021 -0500
+++ b/libpurple/tests/meson.build Tue Oct 12 00:50:59 2021 -0500
@@ -4,6 +4,8 @@
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/test_history_adapter.c Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,293 @@
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <https://www.gnu.org/licenses/>. +#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 Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,493 @@
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <https://www.gnu.org/licenses/>. +#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 Mon Oct 11 23:47:26 2021 -0500
+++ b/meson.build Tue Oct 12 00:50:59 2021 -0500
@@ -301,6 +301,11 @@
libsoup = dependency('libsoup-2.4', version : '>= 2.42')
#######################################################################
+# Check for sqlite3 (required) +####################################################################### +sqlite3 = dependency('sqlite3', version : '>= 3.27.0') +####################################################################### #######################################################################
@@ -665,6 +670,7 @@
toplevel_inc = include_directories('.')
+subdir('purple-history') --- a/po/POTFILES.in Mon Oct 11 23:47:26 2021 -0500
+++ b/po/POTFILES.in Tue Oct 12 00:50:59 2021 -0500
@@ -248,6 +248,8 @@
libpurple/purplecredentialprovider.c
libpurple/purpledebugui.c
+libpurple/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 Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,9 @@
+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 Tue Oct 12 00:50:59 2021 -0500
@@ -0,0 +1,126 @@
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <https://www.gnu.org/licenses/>. +#include <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();