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