# HG changeset patch
# Node ID f726140e4970
# Parent  5ebb4beb29b7
diff --git a/protocols/ircv3/meson.build b/protocols/ircv3/meson.build
--- a/protocols/ircv3/meson.build
+++ b/protocols/ircv3/meson.build
@@ -1,38 +1,25 @@
 IRCV3_SOURCES = [
-	'purpleircv3capabilities.c',
 	'purpleircv3connection.c',
 	'purpleircv3core.c',
-	'purpleircv3ctcp.c',
-	'purpleircv3formatting.c',
-	'purpleircv3message.c',
 	'purpleircv3messagehandlers.c',
-	'purpleircv3parser.c',
 	'purpleircv3protocol.c',
 	'purpleircv3protocolconversation.c',
-	'purpleircv3sasl.c',
-	'purpleircv3source.c',
 ]
 
 IRCV3_HEADERS = [
-	'purpleircv3capabilities.h',
 	'purpleircv3connection.h',
-	'purpleircv3constants.h',
 	'purpleircv3core.h',
-	'purpleircv3ctcp.h',
-	'purpleircv3formatting.h',
-	'purpleircv3message.h',
 	'purpleircv3messagehandlers.h',
-	'purpleircv3parser.h',
 	'purpleircv3protocol.h',
 	'purpleircv3protocolconversation.h',
-	'purpleircv3sasl.h',
-	'purpleircv3source.h',
 ]
 
 if not DYNAMIC_IRCV3
 	subdir_done()
 endif
 
+ibis_dep = dependency('ibis')
+
 ircv3_filebase = f'purple-@purple_major_version@-ircv3'
 
 ircv3_includes = include_directories('.')
@@ -64,14 +51,14 @@
 ircv3_prpl = shared_library('ircv3', IRCV3_SOURCES + IRCV3_HEADERS + [ircv3_h],
 	c_args : ['-DPURPLE_IRCV3_COMPILATION', '-DG_LOG_USE_STRUCTURED', '-DG_LOG_DOMAIN="Purple-IRCv3"'],
 	gnu_symbol_visibility : 'hidden',
-	dependencies : [birb_dep, libpurple_dep, glib, gio, hasl],
+	dependencies : [birb_dep, libpurple_dep, glib, gio, hasl, ibis_dep],
 	install : true,
 	install_dir : PURPLE_PLUGINDIR)
 
 ircv3_dep = declare_dependency(
 	sources : [IRCV3_SOURCES, IRCV3_HEADERS],
 	include_directories : ircv3_includes,
-	dependencies : [birb_dep, libpurple_dep, glib, gio, hasl])
+	dependencies : [birb_dep, libpurple_dep, glib, gio, hasl, ibis_dep])
 
 pkgconfig.generate(
 	# we purposely don't put the library here because you should not be
@@ -101,14 +88,12 @@
 	gnome.generate_gir(
 		ircv3_introspection_stub,
 		sources : [IRCV3_SOURCES, IRCV3_HEADERS],
-		includes : ['Birb-1.0', 'GLib-2.0', 'GObject-2.0', 'GPlugin-1.0', libpurple_gir[0]],
+		includes : ['Birb-1.0', 'GLib-2.0', 'GObject-2.0', 'GPlugin-1.0', 'Ibis-1.0', libpurple_gir[0]],
 		namespace : 'PurpleIRCv3',
 		symbol_prefix : 'purple_ircv3',
 		nsversion : '1.0',
 		install : true,
-		dependencies: [birb_dep, gplugin_dep],
+		dependencies: [birb_dep, gplugin_dep, ibis_dep],
 		export_packages : ['ircv3'],
 		extra_args : ['-DPURPLE_IRCV3_COMPILATION', '--verbose'])
 endif
-
-subdir('tests')
diff --git a/protocols/ircv3/purpleircv3capabilities.h b/protocols/ircv3/purpleircv3capabilities.h
deleted file mode 100644
--- a/protocols/ircv3/purpleircv3capabilities.h
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here.  Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library 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 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
-    !defined(PURPLE_IRCV3_COMPILATION)
-# error "only <libpurple/protocols/ircv3.h> may be included directly"
-#endif
-
-#ifndef PURPLE_IRCV3_CAPABILITIES_H
-#define PURPLE_IRCV3_CAPABILITIES_H
-
-#include <glib.h>
-#include <glib-object.h>
-
-#include <gplugin.h>
-#include <gplugin-native.h>
-
-#include <purple.h>
-
-#include "purpleircv3version.h"
-
-G_BEGIN_DECLS
-
-/* https://ircv3.net/specs/extensions/capability-negotiation */
-#define PURPLE_IRCV3_CAPABILITY_CAP_LS_VERSION "302"
-
-/* https://ircv3.net/specs/extensions/sasl-3.2 */
-#define PURPLE_IRCV3_CAPABILITY_SASL "sasl"
-
-/* https://ircv3.net/specs/extensions/server-time */
-#define PURPLE_IRCV3_CAPABILITY_SERVER_TIME "server-time"
-
-#define PURPLE_IRCV3_TYPE_CAPABILITIES (purple_ircv3_capabilities_get_type())
-
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-G_DECLARE_FINAL_TYPE(PurpleIRCv3Capabilities, purple_ircv3_capabilities,
-                     PURPLE_IRCV3, CAPABILITIES, GObject)
-
-#include "purpleircv3connection.h"
-#include "purpleircv3message.h"
-
-/**
- * purple_ircv3_capabilities_register: (skip)
- * @plugin: The [class@GPlugin.NativePlugin] instance.
- *
- * Dynamically registers the PurpleIRCv3Capabilities type.
- *
- * Since: 3.0
- */
-G_GNUC_INTERNAL void purple_ircv3_capabilities_register(GPluginNativePlugin *plugin);
-
-G_GNUC_INTERNAL PurpleIRCv3Capabilities *purple_ircv3_capabilities_new(PurpleIRCv3Connection *connection);
-
-G_GNUC_INTERNAL void purple_ircv3_capabilities_start(PurpleIRCv3Capabilities *capabilities);
-
-G_GNUC_INTERNAL gboolean purple_ircv3_capabilities_message_handler(PurpleIRCv3Message *message, GError **error, gpointer data);
-
-/**
- * purple_ircv3_capabilities_get_connection:
- * @capabilities: The instance.
- *
- * Gets the PurpleIRCv3Connection object that @capabilities was created with.
- *
- * Returns: (transfer none): The connection instance.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-PurpleIRCv3Connection *purple_ircv3_capabilities_get_connection(PurpleIRCv3Capabilities *capabilities);
-
-/**
- * purple_ircv3_capabilities_request:
- * @capabilities: The instance.
- * @capability: The capabilities to request.
- *
- * This method will send `CAP REQ @capability` to the server. Listen to the
- * `::ack` and `::nak` signals which will contain the contents of @capability
- * that was passed in here.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-void purple_ircv3_capabilities_request(PurpleIRCv3Capabilities *capabilities, const char *capability);
-
-/**
- * purple_ircv3_capabilities_lookup:
- * @capabilities: The instance.
- * @name: The name of the capability to look for.
- * @found: (out) (nullable): A return address for a boolean on whether the
- *         capability was advertised or not.
- *
- * Gets the value that the @name capability provided if it was advertised. To
- * determine if the capability was advertised use the @found parameter.
- *
- * Returns: The value of the capability named @name.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-const char *purple_ircv3_capabilities_lookup(PurpleIRCv3Capabilities *capabilities, const char *name, gboolean *found);
-
-/**
- * purple_ircv3_capabilities_lookup_and_request:
- * @capabilities: The instance.
- * @name: The name of the capability to look for.
- *
- * A helper function to call [method@PurpleIRCv3.Capabilities.Lookup] and if
- * found, call [method@PurpleIRCv3.Capabilities.Request].
- *
- * This method ignores the advertised value, so to get that you'll need to call
- * [method@PurpleIRCv3.Capabilities.Lookup] yourself.
- *
- * Also if you need to do something when the server ACK's or NAK's your
- * request, you're probably better off just using the methods yourself.
- *
- * Returns: %TRUE if @name was found and requested, %FALSE otherwise.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-gboolean purple_ircv3_capabilities_lookup_and_request(PurpleIRCv3Capabilities *capabilities, const char *name);
-
-/**
- * purple_ircv3_capabilties_add_wait:
- * @capabilities: The instance.
- *
- * Adds a wait counter to @capabilities. This counter is used to delay the
- * call of `CAP END` until all capability negotiation has completed. This is
- * necessary for SASL and may be necessary for other capabilities as well.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-void purple_ircv3_capabilities_add_wait(PurpleIRCv3Capabilities *capabilities);
-
-/**
- * purple_ircv3_capabilties_remove_wait:
- * @capabilities: The instance.
- *
- * Removes a wait counter from @capabilities. Only when this counter reaches 0,
- * will `CAP END` be called and registration completed.
- *
- * This is necessary for SASL and may be necessary for other capabilities as
- * well.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-void purple_ircv3_capabilities_remove_wait(PurpleIRCv3Capabilities *capabilities);
-
-G_END_DECLS
-
-#endif /* PURPLE_IRCV3_CAPABILITIES_H */
diff --git a/protocols/ircv3/purpleircv3capabilities.c b/protocols/ircv3/purpleircv3capabilities.c
deleted file mode 100644
--- a/protocols/ircv3/purpleircv3capabilities.c
+++ /dev/null
@@ -1,685 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here.  Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library 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 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#include "purpleircv3capabilities.h"
-
-#include "purpleircv3connection.h"
-#include "purpleircv3core.h"
-#include "purpleircv3sasl.h"
-
-enum {
-	PROP_0,
-	PROP_CONNECTION,
-	N_PROPERTIES,
-};
-static GParamSpec *properties[N_PROPERTIES] = {NULL, };
-
-/* Windows is does something weird with signal handling that includes defining
- * SIG_ACK. We don't care about that here, so we undef it if we find it.
- * See https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-action-constants?view=msvc-170
- */
-#ifdef SIG_ACK
-# undef SIG_ACK
-#endif /* SIG_ACK */
-
-enum {
-	SIG_READY,
-	SIG_ACK,
-	SIG_NAK,
-	SIG_DONE,
-	SIG_NEW,
-	SIG_DEL,
-	N_SIGNALS,
-};
-static guint signals[N_SIGNALS] = {0, };
-
-struct _PurpleIRCv3Capabilities {
-	GObject parent;
-
-	PurpleIRCv3Connection *connection;
-
-	GHashTable *caps;
-	GPtrArray *requests;
-
-	gatomicrefcount wait_counters;
-};
-
-/******************************************************************************
- * Helpers
- *****************************************************************************/
-static void
-purple_ircv3_capabilities_set_connection(PurpleIRCv3Capabilities *capabilities,
-                                         PurpleIRCv3Connection *connection)
-{
-	g_return_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities));
-
-	if(g_set_object(&capabilities->connection, connection)) {
-		g_object_notify_by_pspec(G_OBJECT(capabilities),
-		                         properties[PROP_CONNECTION]);
-	}
-}
-
-static void
-purple_ircv3_capabilities_finish(PurpleIRCv3Capabilities *capabilities) {
-	purple_ircv3_connection_writef(capabilities->connection,
-	                               "CAP END");
-
-	g_signal_emit(capabilities, signals[SIG_DONE], 0);
-}
-
-static void
-purple_ircv3_capabilities_add(PurpleIRCv3Capabilities *capabilities,
-                              const char *capability)
-{
-	char *equals = g_strstr_len(capability, -1, "=");
-
-	if(equals != NULL) {
-		char *key = g_strndup(capability, equals - capability);
-		char *value = g_strdup(equals + 1);
-
-		g_hash_table_insert(capabilities->caps, key, value);
-	} else {
-		g_hash_table_insert(capabilities->caps, g_strdup(capability), NULL);
-	}
-}
-
-/******************************************************************************
- * Callbacks
- *****************************************************************************/
-static void
-ircv3_capabilities_message_tags_ack_cb(PurpleIRCv3Capabilities *capabilities,
-                                       G_GNUC_UNUSED const char *capability,
-                                       G_GNUC_UNUSED gpointer data)
-{
-	/* We have message tags so add the stuff we support that depends on it. */
-	purple_ircv3_capabilities_lookup_and_request(capabilities, "msgid");
-}
-
-/******************************************************************************
- * PurpleIRCv3Capabilities Implementation
- *****************************************************************************/
-static void
-purple_ircv3_capabilities_default_ready_cb(PurpleIRCv3Capabilities *capabilities)
-{
-	PurpleAccount *account = NULL;
-	PurpleConnection *purple_connection = NULL;
-
-	purple_connection = PURPLE_CONNECTION(capabilities->connection);
-	account = purple_connection_get_account(purple_connection);
-
-	/* Don't request the sasl capability unless the user has selected the
-	 * require-password option.
-	 */
-	if(purple_account_get_require_password(account)) {
-		gboolean found = FALSE;
-
-		purple_ircv3_capabilities_lookup(capabilities, "sasl", &found);
-
-		if(found) {
-			purple_ircv3_sasl_request(capabilities);
-		}
-	}
-
-	/* cap-notify is implied when we use CAP LS 302, so this is really just to
-	 * make sure it's requested.
-	 */
-	purple_ircv3_capabilities_lookup_and_request(capabilities, "cap-notify");
-
-	/* message-tags is used for a lot of stuff so we need to tell everyone we
-	 * do in fact support it.
-	 */
-	if(purple_ircv3_capabilities_lookup_and_request(capabilities,
-	                                                "message-tags"))
-	{
-		g_signal_connect(capabilities, "ack::message-tags",
-		                 G_CALLBACK(ircv3_capabilities_message_tags_ack_cb),
-		                 NULL);
-	}
-
-	/* The server-time capability just tells the server to send a tag to
-	 * messages, so we just need to request it and then handle the tag when
-	 * we're processing messages if it exists.
-	 */
-	purple_ircv3_capabilities_lookup_and_request(capabilities,
-	                                             PURPLE_IRCV3_CAPABILITY_SERVER_TIME);
-}
-
-/******************************************************************************
- * GObject Implementation
- *****************************************************************************/
-G_DEFINE_DYNAMIC_TYPE_EXTENDED(PurpleIRCv3Capabilities,
-                               purple_ircv3_capabilities, G_TYPE_OBJECT,
-                               G_TYPE_FLAG_FINAL, {})
-
-static void
-purple_ircv3_capabilities_get_property(GObject *obj, guint param_id,
-                                       GValue *value, GParamSpec *pspec)
-{
-	PurpleIRCv3Capabilities *capabilities = PURPLE_IRCV3_CAPABILITIES(obj);
-
-	switch(param_id) {
-		case PROP_CONNECTION:
-			g_value_set_object(value,
-			                   purple_ircv3_capabilities_get_connection(capabilities));
-			break;
-		default:
-			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
-			break;
-	}
-}
-
-static void
-purple_ircv3_capabilities_set_property(GObject *obj, guint param_id,
-                                       const GValue *value, GParamSpec *pspec)
-{
-	PurpleIRCv3Capabilities *capabilities = PURPLE_IRCV3_CAPABILITIES(obj);
-
-	switch(param_id) {
-		case PROP_CONNECTION:
-			purple_ircv3_capabilities_set_connection(capabilities,
-			                                         g_value_get_object(value));
-			break;
-		default:
-			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
-			break;
-	}
-}
-
-static void
-purple_ircv3_capabilities_dispose(GObject *obj) {
-	PurpleIRCv3Capabilities *capabilities = PURPLE_IRCV3_CAPABILITIES(obj);
-
-	g_clear_object(&capabilities->connection);
-
-	G_OBJECT_CLASS(purple_ircv3_capabilities_parent_class)->dispose(obj);
-}
-
-static void
-purple_ircv3_capabilities_finalize(GObject *obj) {
-	PurpleIRCv3Capabilities *capabilities = PURPLE_IRCV3_CAPABILITIES(obj);
-
-	g_hash_table_destroy(capabilities->caps);
-	g_ptr_array_free(capabilities->requests, TRUE);
-
-	G_OBJECT_CLASS(purple_ircv3_capabilities_parent_class)->finalize(obj);
-}
-
-static void
-purple_ircv3_capabilities_init(PurpleIRCv3Capabilities *capabilities) {
-	capabilities->caps = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
-	                                           g_free);
-	capabilities->requests = g_ptr_array_new_full(0, g_free);
-
-	g_atomic_ref_count_init(&capabilities->wait_counters);
-}
-
-static void
-purple_ircv3_capabilities_class_finalize(G_GNUC_UNUSED PurpleIRCv3CapabilitiesClass *klass) {
-}
-
-static void
-purple_ircv3_capabilities_class_init(PurpleIRCv3CapabilitiesClass *klass) {
-	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
-
-	obj_class->dispose = purple_ircv3_capabilities_dispose;
-	obj_class->finalize = purple_ircv3_capabilities_finalize;
-	obj_class->get_property = purple_ircv3_capabilities_get_property;
-	obj_class->set_property = purple_ircv3_capabilities_set_property;
-
-	/**
-	 * PurpleIRCv3Capabilities:connection:
-	 *
-	 * The PurpleIRCv3Connection object that this capabilities was created
-	 * with.
-	 *
-	 * Since: 3.0
-	 */
-	properties[PROP_CONNECTION] = g_param_spec_object(
-		"connection", "connection",
-		"The connection this capabilities was created for.",
-		PURPLE_IRCV3_TYPE_CONNECTION,
-		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
-
-	/**
-	 * PurpleIRCv3Capabilities::ready:
-	 * @capabilities: The instance.
-	 *
-	 * Emitted when @capabilities has finished receiving the list of
-	 * capabilities from the server at startup.
-	 *
-	 * For dynamically added capabilities see the `added` and `removed`
-	 * signals.
-	 *
-	 * Since: 3.0
-	 */
-	signals[SIG_READY] = g_signal_new_class_handler(
-		"ready",
-		G_OBJECT_CLASS_TYPE(klass),
-		G_SIGNAL_RUN_LAST,
-		G_CALLBACK(purple_ircv3_capabilities_default_ready_cb),
-		NULL,
-		NULL,
-		NULL,
-		G_TYPE_NONE,
-		0);
-
-	/**
-	 * PurpleIRCv3Capabilities::ack:
-	 * @capabilities: The instance.
-	 * @capability: The capability string.
-	 *
-	 * Emitted when the server has acknowledged a `CAP REQ` call from
-	 * purple_ircv3_capabilities_request.
-	 *
-	 * The value of @capability will be the same as the one that was requested.
-	 *
-	 * Since: 3.0
-	 */
-	signals[SIG_ACK] = g_signal_new_class_handler(
-		"ack",
-		G_OBJECT_CLASS_TYPE(klass),
-		G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST,
-		NULL,
-		NULL,
-		NULL,
-		NULL,
-		G_TYPE_NONE,
-		1,
-		G_TYPE_STRING);
-
-	/**
-	 * PurpleIRCv3Capabilities::nak:
-	 * @capabilities: The instance.
-	 * @capability: The capability string.
-	 *
-	 * Emitted when the server has nacked a `CAP REQ` call from
-	 * purple_ircv3_capabilities_request.
-	 *
-	 * The value of @capability will be the same as the one that was requested.
-	 *
-	 * Since: 3.0
-	 */
-	signals[SIG_NAK] = g_signal_new_class_handler(
-		"nak",
-		G_OBJECT_CLASS_TYPE(klass),
-		G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST,
-		NULL,
-		NULL,
-		NULL,
-		NULL,
-		G_TYPE_NONE,
-		1,
-		G_TYPE_STRING);
-
-	/**
-	 * PurpleIRCv3Capabilities::done:
-	 * @capabilities: The instance.
-	 *
-	 * Emitted when all of the requested capabilities have been either ack'd or
-	 * nak'd by the server.
-	 *
-	 * Since: 3.0
-	 */
-	signals[SIG_DONE] = g_signal_new_class_handler(
-		"done",
-		G_OBJECT_CLASS_TYPE(klass),
-		G_SIGNAL_RUN_LAST,
-		NULL,
-		NULL,
-		NULL,
-		NULL,
-		G_TYPE_NONE,
-		0);
-
-	/**
-	 * PurpleIRCv3Capabilities::new:
-	 * @capabilities: The instance.
-	 * @added: The newly added capabilities.
-	 *
-	 * Emitted when the server sends the `CAP NEW` command. @added is a
-	 * [type@GLib.Strv] of the new capabilities the server added.
-	 *
-	 * There are two approaches to how you can use this signal. You can check
-	 * each item in @added for the values you need and parsing their values, or
-	 * you can call #purple_ircv3_capabilities_lookup to see if the
-	 * capabilities you're interested in have been added.
-	 *
-	 * Since: 3.0
-	 */
-	signals[SIG_NEW] = g_signal_new_class_handler(
-		"new",
-		G_OBJECT_CLASS_TYPE(klass),
-		G_SIGNAL_RUN_LAST,
-		NULL,
-		NULL,
-		NULL,
-		NULL,
-		G_TYPE_NONE,
-		1,
-		G_TYPE_STRV);
-
-	/**
-	 * PurpleIRCv3Capabilities::del:
-	 * @capabilities: The instance.
-	 * @removed: The capabilities that were removed.
-	 *
-	 * Emitted when the server sends the `CAP DEL` command. @removed is a
-	 * [type@GLib.Strv] of the capabilities that the server removed.
-	 *
-	 * There are two approaches to how you can use this signal. You can check
-	 * each item in @removed for the values you care about, or you can call
-	 * #purple_ircv3_capabilities_lookup to see if the capabilities you're
-	 * interested in have been removed.
-	 *
-	 * Since: 3.0
-	 */
-	signals[SIG_DEL] = g_signal_new_class_handler(
-		"del",
-		G_OBJECT_CLASS_TYPE(klass),
-		G_SIGNAL_RUN_LAST,
-		NULL,
-		NULL,
-		NULL,
-		NULL,
-		G_TYPE_NONE,
-		1,
-		G_TYPE_STRV);
-}
-
-/******************************************************************************
- * Command handlers
- *****************************************************************************/
-static gboolean
-purple_ircv3_capabilities_handle_list(PurpleIRCv3Capabilities *capabilities,
-                                      guint n_params,
-                                      GStrv params,
-                                      G_GNUC_UNUSED GError **error)
-{
-	gboolean done = TRUE;
-	gchar **parts = NULL;
-
-	/* Check if we have more messages coming. */
-	if(n_params > 1 && purple_strequal(params[0], "*")) {
-		parts = g_strsplit(params[1], " ", -1);
-		done = FALSE;
-	} else {
-		parts = g_strsplit(params[0], " ", -1);
-	}
-
-	/* Add each capability to our hash table, splitting the keys and values. */
-	for(int i = 0; parts[i] != NULL; i++) {
-		purple_ircv3_capabilities_add(capabilities, parts[i]);
-	}
-
-	g_strfreev(parts);
-
-	if(done) {
-		g_signal_emit(capabilities, signals[SIG_READY], 0, signals[SIG_READY]);
-
-		/* If no capabilities were requested after we emitted the ready signal
-		 * we're done with capability negotiation.
-		 */
-		if(capabilities->requests->len == 0) {
-			purple_ircv3_capabilities_remove_wait(capabilities);
-		}
-	}
-
-	return TRUE;
-}
-
-static gboolean
-purple_ircv3_capabilities_handle_ack_nak(PurpleIRCv3Capabilities *capabilities,
-                                         GStrv params,
-                                         guint sig,
-                                         const char *method,
-                                         GError **error)
-{
-	char *caps = g_strjoinv(" ", params);
-	guint index = 0;
-	gboolean found = FALSE;
-	gboolean ret = TRUE;
-
-	g_signal_emit(capabilities, sig, g_quark_from_string(caps), caps);
-
-	found = g_ptr_array_find_with_equal_func(capabilities->requests, caps,
-	                                         g_str_equal, &index);
-	if(found) {
-		g_ptr_array_remove_index(capabilities->requests, index);
-	} else {
-		g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
-		            "received CAP %s for unknown capability %s", method, caps);
-		ret = FALSE;
-	}
-	g_free(caps);
-
-	if(capabilities->requests->len == 0) {
-		purple_ircv3_capabilities_remove_wait(capabilities);
-	}
-
-	return ret;
-}
-
-static gboolean
-purple_ircv3_capabilities_handle_new(PurpleIRCv3Capabilities *capabilities,
-                                     guint n_params,
-                                     GStrv params,
-                                     G_GNUC_UNUSED GError **error)
-{
-	for(guint i = 0; i < n_params; i++) {
-		purple_ircv3_capabilities_add(capabilities, params[i]);
-	}
-
-	g_signal_emit(capabilities, signals[SIG_NEW], 0, params);
-
-	return TRUE;
-}
-
-static gboolean
-purple_ircv3_capabilities_handle_del(PurpleIRCv3Capabilities *capabilities,
-                                     guint n_params,
-                                     GStrv params,
-                                     G_GNUC_UNUSED GError **error)
-{
-	for(guint i = 0; i < n_params; i++) {
-		g_hash_table_remove(capabilities->caps, params[i]);
-	}
-
-	g_signal_emit(capabilities, signals[SIG_DEL], 0, params);
-
-	return TRUE;
-}
-
-/******************************************************************************
- * Internal API
- *****************************************************************************/
-void
-purple_ircv3_capabilities_register(GPluginNativePlugin *plugin) {
-	purple_ircv3_capabilities_register_type(G_TYPE_MODULE(plugin));
-}
-
-PurpleIRCv3Capabilities *
-purple_ircv3_capabilities_new(PurpleIRCv3Connection *connection) {
-	return g_object_new(
-		PURPLE_IRCV3_TYPE_CAPABILITIES,
-		"connection", connection,
-		NULL);
-}
-
-void
-purple_ircv3_capabilities_start(PurpleIRCv3Capabilities *capabilities) {
-	purple_ircv3_connection_writef(capabilities->connection, "CAP LS %s",
-	                               PURPLE_IRCV3_CAPABILITY_CAP_LS_VERSION);
-}
-
-gboolean
-purple_ircv3_capabilities_message_handler(PurpleIRCv3Message *message,
-                                          GError **error,
-                                          gpointer data)
-{
-	PurpleIRCv3Connection *connection = data;
-	PurpleIRCv3Capabilities *capabilities = NULL;
-	GStrv params = NULL;
-	GStrv subparams = NULL;
-	const char *subcommand = NULL;
-	guint n_params = 0;
-	guint n_subparams = 0;
-
-	params = purple_ircv3_message_get_params(message);
-	if(params != NULL) {
-		n_params = g_strv_length(params);
-	}
-
-	if(n_params < 2) {
-		return FALSE;
-	}
-
-	capabilities = purple_ircv3_connection_get_capabilities(connection);
-
-	/* Initialize some variables to make it easier to call our sub command
-	 * handlers.
-	 *
-	 * params[0] is the nick or * if it hasn't been negotiated yet, we don't
-	 * have a need for this, so we ignore it.
-	 *
-	 * params[1] is the CAP subcommand sent from the server. We use it here
-	 * purely for dispatching to our subcommand handlers.
-	 *
-	 * params[2] and higher are the parameters to the subcommand. To make the
-	 * code a bit easier all around, we subtract 2 from n_params to remove
-	 * references to the nick and subcommand name. Like wise, we add 2 to the
-	 * params GStrv which will now point to the second item in the array again
-	 * ignoring the nick and subcommand.
-	 */
-	subcommand = params[1];
-	n_subparams = n_params - 2;
-	subparams = params + 2;
-
-	/* Dispatch the subcommand. */
-	if(purple_strequal(subcommand, "LS") ||
-	   purple_strequal(subcommand, "LIST"))
-	{
-		return purple_ircv3_capabilities_handle_list(capabilities, n_subparams,
-		                                             subparams, error);
-	} else if(purple_strequal(subcommand, "ACK")) {
-		return purple_ircv3_capabilities_handle_ack_nak(capabilities,
-		                                                subparams,
-		                                                signals[SIG_ACK],
-		                                                "ACK",
-		                                                error);
-	} else if(purple_strequal(subcommand, "NAK")) {
-		return purple_ircv3_capabilities_handle_ack_nak(capabilities,
-		                                                subparams,
-		                                                signals[SIG_NAK],
-		                                                "NAK",
-		                                                error);
-	} else if(purple_strequal(subcommand, "NEW")) {
-		return purple_ircv3_capabilities_handle_new(capabilities, n_subparams,
-		                                            subparams, error);
-	} else if(purple_strequal(subcommand, "DEL")) {
-		return purple_ircv3_capabilities_handle_del(capabilities, n_subparams,
-		                                            subparams, error);
-	}
-
-	g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
-	            "No handler for CAP subcommand %s", subcommand);
-
-	return FALSE;
-}
-
-/******************************************************************************
- * Public API
- *****************************************************************************/
-PurpleIRCv3Connection *
-purple_ircv3_capabilities_get_connection(PurpleIRCv3Capabilities *capabilities)
-{
-	g_return_val_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities), NULL);
-
-	return capabilities->connection;
-}
-
-void
-purple_ircv3_capabilities_request(PurpleIRCv3Capabilities *capabilities,
-                                  const char *capability)
-{
-	g_return_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities));
-	g_return_if_fail(capability != NULL);
-
-	g_ptr_array_add(capabilities->requests, g_strdup(capability));
-
-	purple_ircv3_connection_writef(capabilities->connection, "CAP REQ :%s",
-	                               capability);
-}
-
-const char *
-purple_ircv3_capabilities_lookup(PurpleIRCv3Capabilities *capabilities,
-                                 const char *name, gboolean *found)
-{
-	gpointer value = NULL;
-	gboolean real_found = FALSE;
-
-	g_return_val_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities), NULL);
-	g_return_val_if_fail(name != NULL, NULL);
-
-	real_found = g_hash_table_lookup_extended(capabilities->caps, name, NULL,
-	                                          &value);
-
-	if(found != NULL) {
-		*found = real_found;
-	}
-
-	return value;
-}
-
-gboolean
-purple_ircv3_capabilities_lookup_and_request(PurpleIRCv3Capabilities *capabilities,
-                                             const char *name)
-{
-	gboolean found = FALSE;
-
-	g_return_val_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities), FALSE);
-	g_return_val_if_fail(name != NULL, FALSE);
-
-	purple_ircv3_capabilities_lookup(capabilities, name, &found);
-	if(found) {
-		purple_ircv3_capabilities_request(capabilities, name);
-	}
-
-	return found;
-}
-
-void
-purple_ircv3_capabilities_add_wait(PurpleIRCv3Capabilities *capabilities) {
-	g_return_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities));
-
-	g_atomic_ref_count_inc(&capabilities->wait_counters);
-}
-
-void
-purple_ircv3_capabilities_remove_wait(PurpleIRCv3Capabilities *capabilities) {
-	g_return_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities));
-
-	if(g_atomic_ref_count_dec(&capabilities->wait_counters)) {
-		purple_ircv3_capabilities_finish(capabilities);
-	}
-}
diff --git a/protocols/ircv3/purpleircv3connection.h b/protocols/ircv3/purpleircv3connection.h
--- a/protocols/ircv3/purpleircv3connection.h
+++ b/protocols/ircv3/purpleircv3connection.h
@@ -36,6 +36,8 @@
 
 #include <purple.h>
 
+#include <ibis.h>
+
 #include "purpleircv3version.h"
 
 G_BEGIN_DECLS
@@ -46,9 +48,6 @@
 G_DECLARE_DERIVABLE_TYPE(PurpleIRCv3Connection, purple_ircv3_connection,
                          PURPLE_IRCV3, CONNECTION, PurpleConnection)
 
-#include "purpleircv3capabilities.h"
-#include "purpleircv3message.h"
-
 struct _PurpleIRCv3ConnectionClass {
 	/*< private >*/
 	PurpleConnectionClass parent;
@@ -68,48 +67,17 @@
 G_GNUC_INTERNAL void purple_ircv3_connection_register(GPluginNativePlugin *plugin);
 
 /**
- * purple_ircv3_connection_emit_ctcp_request:
+ * purple_ircv3_connection_get_client:
  * @connection: The instance.
- * @conversation: The conversation.
- * @message: The message.
- * @command: The CTCP command.
- * @parameters: (nullable): The CTCP parameters.
  *
- * Emits the [signal@Connection:ctcp-request] signal with the given @command
- * and @parameters which originated from @message in @conversation.
+ * Gets the [class@Ibis.Client] from @connection.
  *
- * The message may be modified by a signal handler. For example, the default
- * handler will output an internationalized string that describes what command
- * was requested.
- *
- * Returns: %TRUE if the request was handled and the message should not be
- *          echoed, otherwise %FALSE.
+ * Returns: (transfer none): The client instance if connected, otherwise %NULL.
  *
  * Since: 3.0
  */
-G_GNUC_INTERNAL gboolean purple_ircv3_connection_emit_ctcp_request(PurpleIRCv3Connection *connection, PurpleConversation *conversation, PurpleMessage *message, const char *command, const char *parameters);
-
-/**
- * purple_ircv3_connection_emit_ctcp_response:
- * @connection: The instance.
- * @conversation: The conversation.
- * @message: The message.
- * @command: The CTCP command.
- * @parameters: (nullable): The CTCP parameters.
- *
- * Emits the [signal@Connection:ctcp-response] signal with the given @command
- * and @parameters which originated from @message in @conversation.
- *
- * The message may be modified by a signal handler. For example, the default
- * handler will output an internationalized string that describes what the
- * response was.
- *
- * Returns: %TRUE if the request was handled and the message should not be
- *          echoed, otherwise %FALSE.
- *
- * Since: 3.0
- */
-G_GNUC_INTERNAL gboolean purple_ircv3_connection_emit_ctcp_response(PurpleIRCv3Connection *connection, PurpleConversation *conversation, PurpleMessage *message, const char *command, const char *parameters);
+PURPLE_AVAILABLE_IN_3_0
+IbisClient *purple_ircv3_connection_get_client(PurpleIRCv3Connection *connection);
 
 /**
  * purple_ircv3_connection_writef:
@@ -125,29 +93,6 @@
 void purple_ircv3_connection_writef(PurpleIRCv3Connection *connection, const char *format, ...) G_GNUC_PRINTF(2, 3);
 
 /**
- * purple_ircv3_connection_get_capabilities:
- * @connection: The instance.
- *
- * Gets the list of capabilities that the server supplied during registration.
- *
- * Returns: (transfer none): The list of capabilities that the server supports.
- */
-PurpleIRCv3Capabilities *purple_ircv3_connection_get_capabilities(PurpleIRCv3Connection *connection);
-
-/**
- * purple_ircv3_connection_get_registered:
- * @connection: The instance.
- *
- * Gets whether or not the connection has finished the registration process.
- *
- * Returns: %TRUE if registration has been completed otherwise %FALSE.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-gboolean purple_ircv3_connection_get_registered(PurpleIRCv3Connection *connection);
-
-/**
  * purple_ircv3_connection_add_status_message:
  * @connection: The instance.
  * @message: The message.
@@ -157,7 +102,7 @@
  * Since: 3.0
  */
 PURPLE_IRCV3_AVAILABLE_IN_ALL
-void purple_ircv3_connection_add_status_message(PurpleIRCv3Connection *connection, PurpleIRCv3Message *message);
+void purple_ircv3_connection_add_status_message(PurpleIRCv3Connection *connection, IbisMessage *message);
 
 /**
  * purple_ircv3_connection_is_channel:
diff --git a/protocols/ircv3/purpleircv3connection.c b/protocols/ircv3/purpleircv3connection.c
--- a/protocols/ircv3/purpleircv3connection.c
+++ b/protocols/ircv3/purpleircv3connection.c
@@ -26,40 +26,20 @@
 
 #include "purpleircv3connection.h"
 
-#include "purpleircv3constants.h"
 #include "purpleircv3core.h"
-#include "purpleircv3ctcp.h"
-#include "purpleircv3formatting.h"
-#include "purpleircv3parser.h"
+#include "purpleircv3messagehandlers.h"
 
 enum {
 	PROP_0,
-	PROP_CAPABILITIES,
-	PROP_REGISTERED,
+	PROP_CLIENT,
 	N_PROPERTIES,
 };
 static GParamSpec *properties[N_PROPERTIES] = {NULL, };
 
-enum {
-	SIG_REGISTRATION_COMPLETE,
-	SIG_CTCP_REQUEST,
-	SIG_CTCP_RESPONSE,
-	N_SIGNALS,
-};
-static guint signals[N_SIGNALS] = {0, };
-
 typedef struct {
-	GSocketConnection *connection;
+	IbisClient *client;
 
 	gchar *server_name;
-	gboolean registered;
-
-	GDataInputStream *input;
-	GOutputStream *output;
-
-	PurpleIRCv3Parser *parser;
-
-	PurpleIRCv3Capabilities *capabilities;
 
 	PurpleConversation *status_conversation;
 } PurpleIRCv3ConnectionPrivate;
@@ -74,63 +54,38 @@
  * Helpers
  *****************************************************************************/
 static void
-purple_ircv3_connection_send_pass_command(PurpleIRCv3Connection *connection) {
-	PurpleAccount *account = NULL;
-	const char *password = NULL;
-
-	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
-
-	password = purple_account_get_string(account, "server-password", "");
-	if(password != NULL && *password != '\0') {
-		purple_ircv3_connection_writef(connection, "PASS %s", password);
-	}
-}
-
-static void
-purple_ircv3_connection_send_user_command(PurpleIRCv3Connection *connection) {
-	PurpleAccount *account = NULL;
-	const char *identname = NULL;
-	const char *nickname = NULL;
-	const char *realname = NULL;
-
-	nickname =
-		purple_connection_get_display_name(PURPLE_CONNECTION(connection));
-
-	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
+purple_ircv3_connection_add_message_handlers(PurpleIRCv3Connection *connection,
+                                             IbisClient *client)
+{
+	g_signal_connect_object(client, "message::" IBIS_MSG_TOPIC,
+	                        G_CALLBACK(purple_ircv3_message_handler_topic),
+	                        connection, G_CONNECT_DEFAULT);
+	g_signal_connect_object(client, "message::" IBIS_RPL_NOTOPIC,
+	                        G_CALLBACK(purple_ircv3_message_handler_topic),
+	                        connection, G_CONNECT_DEFAULT);
+	g_signal_connect_object(client, "message::" IBIS_RPL_TOPIC,
+	                        G_CALLBACK(purple_ircv3_message_handler_topic),
+	                        connection, G_CONNECT_DEFAULT);
 
-	/* The stored value could be an empty string, so pass a default of empty
-	 * string and then if it was empty, set our correct fallback.
-	 */
-	identname = purple_account_get_string(account, "ident", "");
-	if(identname == NULL || *identname == '\0') {
-		identname = nickname;
-	}
-
-	realname = purple_account_get_string(account, "real-name", "");
-	if(realname == NULL || *realname == '\0') {
-		realname = nickname;
-	}
+	g_signal_connect_object(client, "message::" IBIS_MSG_PRIVMSG,
+	                        G_CALLBACK(purple_ircv3_message_handler_privmsg),
+	                        connection, G_CONNECT_DEFAULT);
+	g_signal_connect_object(client, "message::" IBIS_MSG_NOTICE,
+	                        G_CALLBACK(purple_ircv3_message_handler_privmsg),
+	                        connection, G_CONNECT_DEFAULT);
 
-	purple_ircv3_connection_writef(connection, "USER %s 0 * :%s", identname,
-	                               realname);
-}
-
-static void
-purple_ircv3_connection_send_nick_command(PurpleIRCv3Connection *connection) {
-	const char *nickname = NULL;
-
-	nickname =
-		purple_connection_get_display_name(PURPLE_CONNECTION(connection));
-
-	purple_ircv3_connection_writef(connection, "NICK %s", nickname);
+	g_warning("added all the signal");
 }
 
 static void
 purple_ircv3_connection_rejoin_channels(PurpleIRCv3Connection *connection) {
+	PurpleIRCv3ConnectionPrivate *priv = NULL;
 	PurpleAccount *account = NULL;
 	PurpleConversationManager *manager = NULL;
 	GList *conversations = NULL;
 
+	priv = purple_ircv3_connection_get_instance_private(connection);
+
 	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
 	manager = purple_conversation_manager_get_default();
 
@@ -141,10 +96,12 @@
 
 		conv_account = purple_conversation_get_account(conversation);
 		if(conv_account == account) {
+			IbisMessage *message = NULL;
 			const char *id = purple_conversation_get_id(conversation);
 
-			purple_ircv3_connection_writef(connection, "%s %s",
-			                               PURPLE_IRCV3_MSG_JOIN, id);
+			message = ibis_message_new(IBIS_MSG_JOIN);
+			ibis_message_set_params(message, id, NULL);
+			ibis_client_write(priv->client, message);
 		}
 
 		conversations = g_list_delete_link(conversations, conversations);
@@ -155,161 +112,40 @@
  * Callbacks
  *****************************************************************************/
 static void
-purple_ircv3_connection_read_cb(GObject *source, GAsyncResult *result,
-                                gpointer data)
+purple_ircv3_connection_connect_cb(GObject *source,
+                                   G_GNUC_UNUSED GParamSpec *pspec,
+                                   gpointer data)
 {
-	PurpleIRCv3Connection *connection = data;
-	PurpleIRCv3ConnectionPrivate *priv = NULL;
-	GCancellable *cancellable = NULL;
-	GDataInputStream *istream = G_DATA_INPUT_STREAM(source);
-	GError *error = NULL;
-	gchar *line = NULL;
-	gsize length;
-	gboolean parsed = FALSE;
-
-	line = g_data_input_stream_read_line_finish(istream, result, &length,
-	                                            &error);
-	if(line == NULL || error != NULL) {
-		if(PURPLE_IS_CONNECTION(connection)) {
-			if(error == NULL) {
-				g_set_error_literal(&error, PURPLE_CONNECTION_ERROR,
-				                    PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
-				                    _("Server closed the connection"));
-			} else {
-				g_prefix_error(&error, "%s", _("Lost connection with server: "));
-			}
-
-			purple_connection_take_error(PURPLE_CONNECTION(connection), error);
-		}
-
-		/* In the off chance that line was returned, make sure we free it. */
-		g_free(line);
-
-		return;
-	}
-
-	priv = purple_ircv3_connection_get_instance_private(connection);
+	PurpleConnection *connection = data;
+	IbisClient *client = IBIS_CLIENT(source);
 
-	parsed = purple_ircv3_parser_parse(priv->parser, line, &error,
-	                                   connection);
-	if(!parsed) {
-		g_warning("failed to handle '%s': %s", line,
-		          error != NULL ? error->message : "unknown error");
-	}
-	g_clear_error(&error);
-
-	g_free(line);
-
-	/* Call read_line_async again to continue reading lines. */
-	cancellable = purple_connection_get_cancellable(PURPLE_CONNECTION(connection));
-	g_data_input_stream_read_line_async(priv->input,
-	                                    G_PRIORITY_DEFAULT,
-	                                    cancellable,
-	                                    purple_ircv3_connection_read_cb,
-	                                    connection);
-}
+	if(ibis_client_get_connected(client)) {
+		purple_connection_set_state(connection,
+		                            PURPLE_CONNECTION_STATE_CONNECTED);
 
-static void
-purple_ircv3_connection_write_cb(GObject *source, GAsyncResult *result,
-                                 gpointer data)
-{
-	PurpleIRCv3Connection *connection = data;
-	BirbQueuedOutputStream *stream = BIRB_QUEUED_OUTPUT_STREAM(source);
-	GError *error = NULL;
-	gboolean success = FALSE;
-
-	success = birb_queued_output_stream_push_bytes_finish(stream, result,
-	                                                      &error);
-
-	if(!success) {
-		birb_queued_output_stream_clear_queue(stream);
-
-		g_prefix_error(&error, "%s", _("Lost connection with server: "));
-
-		purple_connection_take_error(PURPLE_CONNECTION(connection), error);
-
-		return;
+		/* Once reconnected, we need to rejoin any channels that the
+		 * conversation manager has for us.
+		 */
+		purple_ircv3_connection_rejoin_channels(PURPLE_IRCV3_CONNECTION(connection));
+	} else {
+		purple_connection_set_state(connection,
+		                            PURPLE_CONNECTION_STATE_DISCONNECTED);
 	}
 }
 
 static void
-purple_ircv3_connection_connected_cb(GObject *source, GAsyncResult *result,
-                                     gpointer data)
+purple_ircv3_connection_error_cb(GObject *source,
+                                 G_GNUC_UNUSED GParamSpec *pspec,
+                                 gpointer data)
 {
-	PurpleIRCv3Connection *connection = data;
-	PurpleIRCv3ConnectionPrivate *priv = NULL;
-	GCancellable *cancellable = NULL;
+	IbisClient *client = IBIS_CLIENT(source);
+	PurpleConnection *connection = data;
 	GError *error = NULL;
-	GInputStream *istream = NULL;
-	GOutputStream *ostream = NULL;
-	GSocketClient *client = G_SOCKET_CLIENT(source);
-	GSocketConnection *conn = NULL;
-
-	priv = purple_ircv3_connection_get_instance_private(connection);
-
-	/* Finish the async method. */
-	conn = g_socket_client_connect_to_host_finish(client, result, &error);
-	if(conn == NULL || error != NULL) {
-		g_prefix_error(&error, "%s", _("Unable to connect: "));
-
-		purple_connection_take_error(PURPLE_CONNECTION(connection), error);
-
-		return;
-	}
-
-	g_message("Successfully connected to %s", priv->server_name);
-
-	/* Save our connection and setup our input and outputs. */
-	priv->connection = conn;
-
-	/* Create our parser. */
-	priv->parser = purple_ircv3_parser_new();
-	purple_ircv3_parser_add_default_handlers(priv->parser);
-
-	ostream = g_io_stream_get_output_stream(G_IO_STREAM(conn));
-	priv->output = birb_queued_output_stream_new(ostream);
 
-	istream = g_io_stream_get_input_stream(G_IO_STREAM(conn));
-	priv->input = g_data_input_stream_new(istream);
-	g_data_input_stream_set_newline_type(G_DATA_INPUT_STREAM(priv->input),
-	                                     G_DATA_STREAM_NEWLINE_TYPE_CR_LF);
-
-	cancellable = purple_connection_get_cancellable(PURPLE_CONNECTION(connection));
-
-	/* Add our read callback. */
-	g_data_input_stream_read_line_async(priv->input,
-	                                    G_PRIORITY_DEFAULT,
-	                                    cancellable,
-	                                    purple_ircv3_connection_read_cb,
-	                                    connection);
-
-	/* Send our registration commands. */
-	purple_ircv3_capabilities_start(priv->capabilities);
-	purple_ircv3_connection_send_pass_command(connection);
-	purple_ircv3_connection_send_user_command(connection);
-	purple_ircv3_connection_send_nick_command(connection);
-}
-
-static void
-purple_ircv3_connection_caps_done_cb(G_GNUC_UNUSED PurpleIRCv3Capabilities *caps,
-                                     gpointer data)
-{
-	PurpleIRCv3Connection *connection = data;
-	PurpleIRCv3ConnectionPrivate *priv = NULL;
-
-	priv = purple_ircv3_connection_get_instance_private(connection);
-
-	priv->registered = TRUE;
-
-	g_signal_emit(connection, signals[SIG_REGISTRATION_COMPLETE], 0);
-
-	/* Add our supported CTCP commands. */
-	purple_ircv3_ctcp_add_default_handlers(connection);
-
-	/* Now that registration is complete, rejoin any channels that the
-	 * conversation manager has for us.
-	 */
-	purple_ircv3_connection_rejoin_channels(connection);
+	error = ibis_client_get_error(client);
+	if(error != NULL) {
+		purple_connection_g_error(connection, error);
+	}
 }
 
 /******************************************************************************
@@ -322,11 +158,13 @@
 	PurpleIRCv3Connection *connection = NULL;
 	PurpleIRCv3ConnectionPrivate *priv = NULL;
 	PurpleAccount *account = NULL;
-	GCancellable *cancellable = NULL;
-	GSocketClient *client = NULL;
+	GError *local_error = NULL;
+	GProxyResolver *resolver = NULL;
+	const char *password = NULL;
+	const char *value = NULL;
 	gint default_port = PURPLE_IRCV3_DEFAULT_TLS_PORT;
 	gint port = 0;
-	gboolean use_tls = TRUE;
+	gboolean tls = TRUE;
 
 	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(purple_connection), FALSE);
 
@@ -334,34 +172,47 @@
 	priv = purple_ircv3_connection_get_instance_private(connection);
 	account = purple_connection_get_account(purple_connection);
 
-	client = purple_gio_socket_client_new(account, error);
-	if(!G_IS_SOCKET_CLIENT(client)) {
-		if(error != NULL && *error != NULL) {
-			purple_connection_take_error(purple_connection, *error);
-		}
+	priv->client = ibis_client_new();
+	g_signal_connect_object(priv->client, "notify::connected",
+	                        G_CALLBACK(purple_ircv3_connection_connect_cb),
+	                        connection, G_CONNECT_DEFAULT);
+	g_signal_connect_object(priv->client, "notify::error",
+	                        G_CALLBACK(purple_ircv3_connection_error_cb),
+	                        connection, G_CONNECT_DEFAULT);
+	purple_ircv3_connection_add_message_handlers(connection, priv->client);
+
+	ibis_client_set_nick(priv->client,
+	                     purple_connection_get_display_name(purple_connection));
+
+	value = purple_account_get_string(account, "ident", NULL);
+	ibis_client_set_username(priv->client, value);
+
+	value = purple_account_get_string(account, "real-name", NULL);
+	ibis_client_set_realname(priv->client, value);
+
+	password = purple_account_get_string(account, "server-password", NULL);
+
+	/* Turn on TLS if requested. */
+	tls = purple_account_get_bool(account, "use-tls", TRUE);
+
+	/* If TLS is not being used, set the default port to the plain port. */
+	if(!tls) {
+		default_port = PURPLE_IRCV3_DEFAULT_PLAIN_PORT;
+	}
+	port = purple_account_get_int(account, "port", default_port);
+
+	resolver = purple_proxy_get_proxy_resolver(account, &local_error);
+	if(local_error != NULL) {
+		g_propagate_error(error, local_error);
+
+		g_clear_object(&resolver);
+		g_clear_object(&priv->client);
 
 		return FALSE;
 	}
 
-	/* Turn on TLS if requested. */
-	use_tls = purple_account_get_bool(account, "use-tls", TRUE);
-	g_socket_client_set_tls(client, use_tls);
-
-	/* If TLS is not being used, set the default port to the plain port. */
-	if(!use_tls) {
-		default_port = PURPLE_IRCV3_DEFAULT_PLAIN_PORT;
-	}
-	port = purple_account_get_int(account, "port", default_port);
-
-	cancellable = purple_connection_get_cancellable(purple_connection);
-
-	/* Finally start the async connection. */
-	g_socket_client_connect_to_host_async(client, priv->server_name,
-	                                      port, cancellable,
-	                                      purple_ircv3_connection_connected_cb,
-	                                      connection);
-
-	g_clear_object(&client);
+	ibis_client_connect(priv->client, priv->server_name, port, password, tls,
+	                    resolver);
 
 	return TRUE;
 }
@@ -385,82 +236,11 @@
 
 	/* TODO: send QUIT command. */
 
-	if(G_IS_SOCKET_CONNECTION(priv->connection)) {
-		GInputStream *istream = G_INPUT_STREAM(priv->input);
-		GOutputStream *ostream = G_OUTPUT_STREAM(priv->output);
-
-		purple_gio_graceful_close(G_IO_STREAM(priv->connection),
-		                          istream, ostream);
-	}
-
-	g_clear_object(&priv->input);
-	g_clear_object(&priv->output);
-	g_clear_object(&priv->connection);
+	g_clear_object(&priv->client);
 
 	return TRUE;
 }
 
-static void
-purple_ircv3_connection_registration_complete_cb(PurpleIRCv3Connection *connection) {
-	/* Don't set our connection state to connected until we've completed
-	 * registration as connected implies that we can start chatting or join
-	 * rooms and other "online" activities.
-	 */
-	purple_connection_set_state(PURPLE_CONNECTION(connection),
-	                            PURPLE_CONNECTION_STATE_CONNECTED);
-}
-
-/******************************************************************************
- * Default Handlers
- *****************************************************************************/
-static gboolean
-purple_ircv3_connection_ctcp_request_default_handler(G_GNUC_UNUSED PurpleIRCv3Connection *connection,
-                                                     G_GNUC_UNUSED PurpleConversation *conversation,
-                                                     PurpleMessage *message,
-                                                     const char *command,
-                                                     const char *params)
-{
-	char *contents = NULL;
-
-	if(!purple_strempty(params)) {
-		contents = g_strdup_printf(_("requested CTCP %s: %s"), command,
-		                           params);
-	} else {
-		contents = g_strdup_printf(_("requested CTCP %s"),
-		                           command);
-	}
-
-	purple_message_set_contents(message, contents);
-
-	g_clear_pointer(&contents, g_free);
-
-	return FALSE;
-}
-
-static gboolean
-purple_ircv3_connection_ctcp_response_default_handler(G_GNUC_UNUSED PurpleIRCv3Connection *connection,
-                                                      G_GNUC_UNUSED PurpleConversation *conversation,
-                                                      PurpleMessage *message,
-                                                      const char *command,
-                                                      const char *params)
-{
-	char *contents = NULL;
-
-	if(!purple_strempty(params)) {
-		contents = g_strdup_printf(_("CTCP %s response: %s"), command,
-		                           params);
-	} else {
-		contents = g_strdup_printf(_("CTCP %s response was empty"),
-		                           command);
-	}
-
-	purple_message_set_contents(message, contents);
-
-	g_clear_pointer(&contents, g_free);
-
-	return FALSE;
-}
-
 /******************************************************************************
  * GObject Implementation
  *****************************************************************************/
@@ -471,17 +251,13 @@
 	PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);
 
 	switch(param_id) {
-		case PROP_CAPABILITIES:
-			g_value_set_object(value,
-			                   purple_ircv3_connection_get_capabilities(connection));
-			break;
-		case PROP_REGISTERED:
-			g_value_set_boolean(value,
-			                    purple_ircv3_connection_get_registered(connection));
-			break;
-		default:
-			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
-			break;
+	case PROP_CLIENT:
+		g_value_set_object(value,
+		                   purple_ircv3_connection_get_client(connection));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
 	}
 }
 
@@ -492,12 +268,7 @@
 
 	priv = purple_ircv3_connection_get_instance_private(connection);
 
-	g_clear_object(&priv->input);
-	g_clear_object(&priv->output);
-	g_clear_object(&priv->connection);
-
-	g_clear_object(&priv->capabilities);
-	g_clear_object(&priv->parser);
+	g_clear_object(&priv->client);
 	g_clear_object(&priv->status_conversation);
 
 	G_OBJECT_CLASS(purple_ircv3_connection_parent_class)->dispose(obj);
@@ -569,12 +340,6 @@
 	}
 
 	g_clear_pointer(&title, g_free);
-
-	/* Finally create our objects. */
-	priv->capabilities = purple_ircv3_capabilities_new(connection);
-	g_signal_connect_object(priv->capabilities, "done",
-	                        G_CALLBACK(purple_ircv3_connection_caps_done_cb),
-	                        connection, 0);
 }
 
 static void
@@ -599,126 +364,18 @@
 	connection_class->disconnect = purple_ircv3_connection_disconnect;
 
 	/**
-	 * PurpleIRCv3Connection:capabilities:
-	 *
-	 * The capabilities that the server supports.
-	 *
-	 * This is created during registration of the connection and is useful for
-	 * troubleshooting or just reporting them to end users.
-	 *
-	 * Since: 3.0
-	 */
-	properties[PROP_CAPABILITIES] = g_param_spec_object(
-		"capabilities", "capabilities",
-		"The capabilities that the server supports",
-		PURPLE_IRCV3_TYPE_CAPABILITIES,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * PurpleIRCv3Connection:registered:
+	 * PurpleIRCv3Connection:client:
 	 *
-	 * Whether or not the connection has finished the registration portion of
-	 * the connection.
-	 *
-	 * Since: 3.0
-	 */
-	properties[PROP_REGISTERED] = g_param_spec_boolean(
-		"registered", "registered",
-		"Whether or not the connection has finished registration.",
-		FALSE,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
-
-	/* Signals */
-
-	/**
-	 * PurpleIRCv3Connection::registration-complete:
-	 * @connection: The instance.
-	 *
-	 * This signal is emitted after the registration process has been
-	 * completed. Plugins can use this to perform additional actions before
-	 * any channels are auto joined or similar.
+	 * The [class@Ibis.Client] that this connection is using.
 	 *
 	 * Since: 3.0
 	 */
-	signals[SIG_REGISTRATION_COMPLETE] = g_signal_new_class_handler(
-		"registration-complete",
-		G_OBJECT_CLASS_TYPE(klass),
-		G_SIGNAL_RUN_LAST,
-		G_CALLBACK(purple_ircv3_connection_registration_complete_cb),
-		NULL,
-		NULL,
-		NULL,
-		G_TYPE_NONE,
-		0);
+	properties[PROP_CLIENT] = g_param_spec_object(
+		"client", NULL, NULL,
+		IBIS_TYPE_CLIENT,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
-	/**
-	 * PurpleIRCv3Connection::ctcp-request:
-	 * @connection: The instance.
-	 * @conversation: The conversation.
-	 * @message: The message.
-	 * @command: The CTCP command.
-	 * @params: (nullable): The CTCP parameters.
-	 *
-	 * This signal is emitted after a CTCP request has been received.
-	 *
-	 * Signal handlers should return TRUE if they fully handled the request and
-	 * do not want it echoed out to the user.
-	 *
-	 * Handlers may also modify the message. For example, the CTCP ACTION
-	 * command has its message contents replaced with just the CTCP parameters
-	 * and sets the [property@Message:action] to %TRUE and then returns %TRUE.
-	 *
-	 * Since: 3.0
-	 */
-	signals[SIG_CTCP_REQUEST] = g_signal_new_class_handler(
-		"ctcp-request",
-		G_OBJECT_CLASS_TYPE(klass),
-		G_SIGNAL_RUN_LAST,
-		G_CALLBACK(purple_ircv3_connection_ctcp_request_default_handler),
-		g_signal_accumulator_true_handled,
-		NULL,
-		NULL,
-		G_TYPE_BOOLEAN,
-		4,
-		PURPLE_TYPE_CONVERSATION,
-		PURPLE_TYPE_MESSAGE,
-		G_TYPE_STRING,
-		G_TYPE_STRING);
-
-	/**
-	 * PurpleIRCv3Connection::ctcp-response:
-	 * @connection: The instance.
-	 * @conversation: The conversation.
-	 * @message: The message.
-	 * @command: The CTCP command.
-	 * @params: (nullable): The CTCP parameters.
-	 *
-	 * This signal is emitted after a CTCP response has been received.
-	 *
-	 * Signal handlers should return TRUE if they fully handled the response
-	 * and do not want it echoed out to the user.
-	 *
-	 * Handlers may modify @conversation or @message to depending on their
-	 * needs.
-	 *
-	 * Since: 3.0
-	 */
-	signals[SIG_CTCP_RESPONSE] = g_signal_new_class_handler(
-		"ctcp-response",
-		G_OBJECT_CLASS_TYPE(klass),
-		G_SIGNAL_RUN_LAST,
-		G_CALLBACK(purple_ircv3_connection_ctcp_response_default_handler),
-		g_signal_accumulator_true_handled,
-		NULL,
-		NULL,
-		G_TYPE_BOOLEAN,
-		4,
-		PURPLE_TYPE_CONVERSATION,
-		PURPLE_TYPE_MESSAGE,
-		G_TYPE_STRING,
-		G_TYPE_STRING);
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
 }
 
 /******************************************************************************
@@ -741,113 +398,23 @@
 	g_type_class_unref(hack);
 }
 
-gboolean
-purple_ircv3_connection_emit_ctcp_request(PurpleIRCv3Connection *connection,
-                                          PurpleConversation *conversation,
-                                          PurpleMessage *message,
-                                          const char *command,
-                                          const char *parameters)
-{
-	gboolean ret = FALSE;
-
-	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), FALSE);
-	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);
-	g_return_val_if_fail(PURPLE_IS_MESSAGE(message), FALSE);
-	g_return_val_if_fail(!purple_strempty(command), FALSE);
-
-	g_signal_emit(connection, signals[SIG_CTCP_REQUEST], 0, conversation,
-	              message, command, parameters, &ret);
-
-	return ret;
-}
-
-gboolean
-purple_ircv3_connection_emit_ctcp_response(PurpleIRCv3Connection *connection,
-                                           PurpleConversation *conversation,
-                                           PurpleMessage *message,
-                                           const char *command,
-                                           const char *parameters)
-{
-	gboolean ret = FALSE;
-
-	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), FALSE);
-	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);
-	g_return_val_if_fail(PURPLE_IS_MESSAGE(message), FALSE);
-	g_return_val_if_fail(!purple_strempty(command), FALSE);
-
-	g_signal_emit(connection, signals[SIG_CTCP_RESPONSE], 0, conversation,
-	              message, command, parameters, &ret);
-
-	return ret;
-}
-
 /******************************************************************************
  * Public API
  *****************************************************************************/
-void
-purple_ircv3_connection_writef(PurpleIRCv3Connection *connection,
-                               const char *format, ...)
-{
-	PurpleIRCv3ConnectionPrivate *priv = NULL;
-	GBytes *bytes = NULL;
-	GCancellable *cancellable = NULL;
-	GString *msg = NULL;
-	va_list vargs;
-
-	g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));
-	g_return_if_fail(format != NULL);
-
-	priv = purple_ircv3_connection_get_instance_private(connection);
-
-	/* Create our string and append our format to it. */
-	msg = g_string_new("");
-
-	va_start(vargs, format);
-	g_string_vprintf(msg, format, vargs);
-	va_end(vargs);
-
-	/* Next add the trailing carriage return line feed. */
-	g_string_append(msg, "\r\n");
-
-	/* Finally turn the string into bytes and send it! */
-	bytes = g_string_free_to_bytes(msg);
-
-	cancellable = purple_connection_get_cancellable(PURPLE_CONNECTION(connection));
-	birb_queued_output_stream_push_bytes_async(BIRB_QUEUED_OUTPUT_STREAM(priv->output),
-	                                           bytes,
-	                                           G_PRIORITY_DEFAULT,
-	                                           cancellable,
-	                                           purple_ircv3_connection_write_cb,
-	                                           connection);
-
-	g_bytes_unref(bytes);
-}
-
-PurpleIRCv3Capabilities *
-purple_ircv3_connection_get_capabilities(PurpleIRCv3Connection *connection) {
+IbisClient *
+purple_ircv3_connection_get_client(PurpleIRCv3Connection *connection) {
 	PurpleIRCv3ConnectionPrivate *priv = NULL;
 
 	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), NULL);
 
 	priv = purple_ircv3_connection_get_instance_private(connection);
 
-	return priv->capabilities;
-}
-
-gboolean
-purple_ircv3_connection_get_registered(PurpleIRCv3Connection *connection) {
-	PurpleIRCv3ConnectionPrivate *priv = NULL;
-
-	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), FALSE);
-
-	priv = purple_ircv3_connection_get_instance_private(connection);
-
-	return priv->registered;
+	return priv->client;
 }
 
 void
 purple_ircv3_connection_add_status_message(PurpleIRCv3Connection *connection,
-                                           PurpleIRCv3Message *v3_message)
+                                           IbisMessage *ibis_message)
 {
 	PurpleIRCv3ConnectionPrivate *priv = NULL;
 	PurpleMessage *message = NULL;
@@ -857,15 +424,15 @@
 	const char *command = NULL;
 
 	g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));
-	g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(v3_message));
+	g_return_if_fail(IBIS_IS_MESSAGE(ibis_message));
 
 	priv = purple_ircv3_connection_get_instance_private(connection);
 
-	command = purple_ircv3_message_get_command(v3_message);
+	command = ibis_message_get_command(ibis_message);
 
 	str = g_string_new(command);
 
-	params = purple_ircv3_message_get_params(v3_message);
+	params = ibis_message_get_params(ibis_message);
 	if(params != NULL && params[0] != NULL) {
 		char *joined = g_strjoinv(" ", params);
 
@@ -874,12 +441,12 @@
 		g_free(joined);
 	}
 
-	stripped = purple_ircv3_formatting_strip(str->str);
+	stripped = ibis_formatting_strip(str->str);
 	g_string_free(str, TRUE);
 
 	message = g_object_new(
 		PURPLE_TYPE_MESSAGE,
-		"author", purple_ircv3_message_get_source(v3_message),
+		"author", ibis_message_get_source(ibis_message),
 		"contents", stripped,
 		NULL);
 	g_free(stripped);
diff --git a/protocols/ircv3/purpleircv3constants.h b/protocols/ircv3/purpleircv3constants.h
deleted file mode 100644
--- a/protocols/ircv3/purpleircv3constants.h
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here.  Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library 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 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
-    !defined(PURPLE_IRCV3_COMPILATION)
-# error "only <libpurple/protocols/ircv3.h> may be included directly"
-#endif
-
-#ifndef PURPLE_IRCV3_CONSTANTS_H
-#define PURPLE_IRCV3_CONSTANTS_H
-
-/**
- * PURPLE_IRCV3_CTCP_ACTION:
- *
- * A constant representing the CTCP ACTION command.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_CTCP_ACTION ("ACTION") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_CTCP_DELIMITER:
- *
- * The delimiter used for CTCP messages.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_CTCP_DELIMITER (0x1) PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_CTCP_VERSION:
- *
- * A constant representing the CTCP VERSION command.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_CTCP_VERSION ("VERSION") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_ERR_NICKLOCKED:
- *
- * A constant for the IRC %NICKLOCKED error.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_ERR_NICKLOCKED ("902") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_ERR_SASLABORTED:
- *
- * A constant for the IRC %SASLABORTED error.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_ERR_SASLABORTED ("906") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_ERR_SASLALREADY:
- *
- * A constant for the IRC %SASLALREADY error.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_ERR_SASLALREADY ("907") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_ERR_SASLFAIL:
- *
- * A constant for the IRC %SASLFAIL error.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_ERR_SASLFAIL ("904") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_ERR_SASLTOOLONG:
- *
- * A constant for the IRC %SASLTOOLONG error.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_ERR_SASLTOOLONG ("905") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_MSG_AUTHENTICATE:
- *
- * A constant for the IRC %AUTHENTICATE message.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_MSG_AUTHENTICATE ("AUTHENTICATE") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_MSG_CAP:
- *
- * A constant for the IRC %CAP message.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_MSG_CAP ("CAP") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_MSG_JOIN:
- *
- * A constant for the IRC %JOIN message.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_MSG_JOIN ("JOIN") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_MSG_NOTICE:
- *
- * A constant for the IRC %NOTICE message.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_MSG_NOTICE ("NOTICE") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_MSG_PING:
- *
- * A constant for the IRC %PING message.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_MSG_PING ("PING") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_MSG_PRIVMSG:
- *
- * A constant for the IRC %PRIVMSG message.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_MSG_PRIVMSG ("PRIVMSG") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_MSG_TOPIC:
- *
- * A constant for the IRC %TOPIC message.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_MSG_TOPIC ("TOPIC") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_CREATED:
- *
- * A constant for the IRC %CREATED reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_CREATED ("003") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_ENDOFMOTD:
- *
- * A constant for the IRC %ENDOFMOTD reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_ENDOFMOTD ("376") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_LOGGEDIN:
- *
- * A constant for the IRC %LOGGEDIN reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_LOGGEDIN ("900") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_LOGGEDOUT:
- *
- * A constant for the IRC %LOGGEDOUT reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_LOGGEDOUT ("901") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_LUSERCHANNELS:
- *
- * A constant for the IRC %LUSERCHANNELS reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_LUSERCHANNELS ("254") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_LUSERCLIENT:
- *
- * A constant for the IRC %LUSERCLIENT reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_LUSERCLIENT ("251") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_LUSERME:
- *
- * A constant for the IRC %LUSERME reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_LUSERME ("255") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_LUSEROP:
- *
- * A constant for the IRC %LUSEROP reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_LUSEROP ("252") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_LUSERUNKNOWN:
- *
- * A constant for the IRC %LUSERUNKNOWN reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_LUSERUNKNOWN ("253") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_MYINFO:
- *
- * A constant for the IRC %MYINFO reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_MYINFO ("004") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_MOTD:
- *
- * A constant for the IRC %MOTD reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_MOTD ("372") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_MOTDSTART:
- *
- * A constant for the IRC %MOTDSTART reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_MOTDSTART ("375") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_NOTOPIC:
- *
- * A constant for the IRC %NOTOPIC reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_NOTOPIC ("331") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_SASLMECHS:
- *
- * A constant for the IRC %SASLMECHS reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_SASLMECHS ("908") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_SASLSUCCESS:
- *
- * A constant for the IRC %SASLSUCCESS reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_SASLSUCCESS ("903") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_TOPIC:
- *
- * A constant for the IRC %TOPIC reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_TOPIC ("332") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_WELCOME:
- *
- * A constant for the IRC %WELCOME reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_WELCOME ("001") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-/**
- * PURPLE_IRCV3_RPL_YOURHOST:
- *
- * A constant for the IRC %YOURHOST reply.
- *
- * Since: 3.0
- */
-#define PURPLE_IRCV3_RPL_YOURHOST ("002") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
-
-#endif /* PURPLE_IRCV3_CONSTANTS_H */
diff --git a/protocols/ircv3/purpleircv3core.c b/protocols/ircv3/purpleircv3core.c
--- a/protocols/ircv3/purpleircv3core.c
+++ b/protocols/ircv3/purpleircv3core.c
@@ -30,7 +30,6 @@
 
 #include "purpleircv3core.h"
 
-#include "purpleircv3capabilities.h"
 #include "purpleircv3connection.h"
 #include "purpleircv3protocol.h"
 
@@ -77,7 +76,6 @@
 		return FALSE;
 	}
 
-	purple_ircv3_capabilities_register(GPLUGIN_NATIVE_PLUGIN(plugin));
 	purple_ircv3_connection_register(GPLUGIN_NATIVE_PLUGIN(plugin));
 	purple_ircv3_protocol_register(GPLUGIN_NATIVE_PLUGIN(plugin));
 
diff --git a/protocols/ircv3/purpleircv3ctcp.h b/protocols/ircv3/purpleircv3ctcp.h
deleted file mode 100644
--- a/protocols/ircv3/purpleircv3ctcp.h
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here.  Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library 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 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
-    !defined(PURPLE_IRCV3_COMPILATION)
-# error "only <libpurple/protocols/ircv3.h> may be included directly"
-#endif
-
-#ifndef PURPLE_IRCV3_CTCP_H
-#define PURPLE_IRCV3_CTCP_H
-
-#include <glib.h>
-#include <glib-object.h>
-
-#include <purple.h>
-
-#include "purpleircv3connection.h"
-
-G_BEGIN_DECLS
-
-/**
- * purple_ircv3_ctcp_handle: (skip)
- * @connection: The connection.
- * @conversation: The conversation.
- * @message: The message.
- *
- * Check if @message is a CTCP message and handles it accordingly.
- *
- * Returns: %TRUE if the message was a CTCP message and %FALSE otherwise.
- *
- * Since: 3.0
- */
-G_GNUC_INTERNAL
-gboolean purple_ircv3_ctcp_handle(PurpleIRCv3Connection *connection, PurpleConversation *conversation, PurpleMessage *message);
-
-/**
- * purple_ircv3_ctcp_add_default_handlers: (skip)
- * @connection: The connection.
- *
- * Adds handlers for the CTCP commands that we support directly.
- *
- * Since: 3.0
- */
-G_GNUC_INTERNAL
-void purple_ircv3_ctcp_add_default_handlers(PurpleIRCv3Connection *connection);
-
-G_END_DECLS
-
-#endif /* PURPLE_IRCV3_CTCP_H */
diff --git a/protocols/ircv3/purpleircv3ctcp.c b/protocols/ircv3/purpleircv3ctcp.c
deleted file mode 100644
--- a/protocols/ircv3/purpleircv3ctcp.c
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here.  Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library 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 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#include <glib/gi18n-lib.h>
-
-#include "purpleircv3ctcp.h"
-
-#include "purpleircv3constants.h"
-#include "purpleircv3source.h"
-
-/******************************************************************************
- * Helpers
- *****************************************************************************/
-static void
-purple_ircv3_ctcp_respond(PurpleIRCv3Connection *connection,
-                          PurpleMessage *message,
-                          const char *response)
-{
-	const char *author = NULL;
-	char *nick = NULL;
-
-	author = purple_message_get_author(message);
-	purple_ircv3_source_parse(author, &nick, NULL, NULL);
-	if(!purple_strempty(nick)) {
-		PurpleContact *contact = NULL;
-
-		contact = purple_ircv3_connection_find_or_create_contact(connection,
-		                                                         nick);
-
-		if(PURPLE_IS_CONTACT(contact)) {
-			const char *id = NULL;
-
-			id = purple_contact_info_get_id(PURPLE_CONTACT_INFO(contact));
-
-			purple_ircv3_connection_writef(connection, "%s %s :%s",
-			                               PURPLE_IRCV3_MSG_NOTICE,
-			                               id,
-			                               response);
-		}
-	}
-
-	g_clear_pointer(&nick, g_free);
-}
-
-/******************************************************************************
- * Callbacks
- *****************************************************************************/
-static gboolean
-purple_ircv3_ctcp_handler(PurpleIRCv3Connection *connection,
-                          PurpleConversation *conversation,
-                          PurpleMessage *message,
-                          const char *command,
-                          const char *params,
-                          G_GNUC_UNUSED gpointer data)
-{
-	if(purple_strequal(command, PURPLE_IRCV3_CTCP_ACTION)) {
-		purple_message_set_contents(message, params);
-		purple_message_set_action(message, TRUE);
-
-		purple_conversation_write_message(conversation, message);
-
-		return TRUE;
-	} else if(purple_strequal(command, PURPLE_IRCV3_CTCP_VERSION)) {
-		purple_ircv3_ctcp_respond(connection, message, "Purple3 IRCv3");
-	}
-
-	return FALSE;
-}
-
-/******************************************************************************
- * Internal API
- *****************************************************************************/
-gboolean
-purple_ircv3_ctcp_handle(PurpleIRCv3Connection *connection,
-                         PurpleConversation *conversation,
-                         PurpleMessage *message)
-{
-	PurpleMessageFlags flags;
-	char *command = NULL;
-	char *params = NULL;
-	char *ptr = NULL;
-	const char *contents = NULL;
-	gboolean ret = FALSE;
-
-	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), FALSE);
-	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);
-	g_return_val_if_fail(PURPLE_IS_MESSAGE(message), FALSE);
-
-	contents = purple_message_get_contents(message);
-	if(contents == NULL || contents[0] != PURPLE_IRCV3_CTCP_DELIMITER) {
-		return FALSE;
-	}
-
-	/* Move past the delimiter. */
-	contents += 1;
-
-	/* Find the delimiter for the command. */
-	ptr = g_strstr_len(contents, -1, " ");
-
-	/* If we don't have a space, then we have no parameters. */
-	if(ptr == NULL) {
-		size_t len = strlen(contents);
-
-		command = g_strdup(contents);
-		if(command[len - 1] == PURPLE_IRCV3_CTCP_DELIMITER) {
-			command[len - 1] = '\0';
-		}
-	} else {
-		size_t len = 0;
-
-		command = g_strndup(contents, ptr - contents);
-
-		params = g_strdup(ptr + 1);
-		len = strlen(params);
-		if(params[len - 1] == PURPLE_IRCV3_CTCP_DELIMITER) {
-			params[len - 1] = '\0';
-		}
-	}
-
-	flags = purple_message_get_flags(message);
-	if(flags & PURPLE_MESSAGE_NOTIFY) {
-		ret = purple_ircv3_connection_emit_ctcp_response(connection,
-		                                                 conversation, message,
-		                                                 command, params);
-	} else {
-		ret = purple_ircv3_connection_emit_ctcp_request(connection, conversation,
-		                                                message, command,
-		                                                params);
-	}
-
-	g_clear_pointer(&command, g_free);
-	g_clear_pointer(&params, g_free);
-
-	return ret;
-}
-
-void
-purple_ircv3_ctcp_add_default_handlers(PurpleIRCv3Connection *connection) {
-	g_signal_connect(connection, "ctcp-request",
-	                 G_CALLBACK(purple_ircv3_ctcp_handler), NULL);
-}
diff --git a/protocols/ircv3/purpleircv3formatting.h b/protocols/ircv3/purpleircv3formatting.h
deleted file mode 100644
--- a/protocols/ircv3/purpleircv3formatting.h
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here.  Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library 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 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
-    !defined(PURPLE_IRCV3_COMPILATION)
-# error "only <libpurple/protocols/ircv3.h> may be included directly"
-#endif
-
-#ifndef PURPLE_IRCV3_FORMATTING_H
-#define PURPLE_IRCV3_FORMATTING_H
-
-#include <glib.h>
-
-#include "purpleircv3version.h"
-
-G_BEGIN_DECLS
-
-/**
- * purple_ircv3_formatting_strip:
- * @text: (nullable): The text to strip.
- *
- * Removes all formatting from @text and returns the result.
- *
- * Returns: (transfer full) (nullable): The result of stripping @text.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-char *purple_ircv3_formatting_strip(const char *text);
-
-G_END_DECLS
-
-#endif /* PURPLE_IRCV3_FORMATTING_H */
diff --git a/protocols/ircv3/purpleircv3formatting.c b/protocols/ircv3/purpleircv3formatting.c
deleted file mode 100644
--- a/protocols/ircv3/purpleircv3formatting.c
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here.  Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library 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 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#include "purpleircv3formatting.h"
-
-#define IRCV3_FORMAT_BOLD (0x02)
-#define IRCV3_FORMAT_COLOR (0x03)
-#define IRCV3_FORMAT_HEX_COLOR (0x04)
-#define IRCV3_FORMAT_ITALIC (0x1d)
-#define IRCV3_FORMAT_MONOSPACE (0x11)
-#define IRCV3_FORMAT_RESET (0x0f)
-#define IRCV3_FORMAT_REVERSE (0x16)
-#define IRCV3_FORMAT_STRIKETHROUGH (0x1e)
-#define IRCV3_FORMAT_UNDERLINE (0x1f)
-
-/******************************************************************************
- * Helpers
- *****************************************************************************/
-static inline gboolean
-purple_ircv3_formatting_is_hex_color(const char *text) {
-	for(int i = 0; i < 6; i++) {
-		if(text[i] == '\0') {
-			return FALSE;
-		}
-
-		if(!g_ascii_isxdigit(text[i])) {
-			return FALSE;
-		}
-	}
-
-	return TRUE;
-}
-
-/******************************************************************************
- * Public API
- *****************************************************************************/
-char *
-purple_ircv3_formatting_strip(const char *text) {
-	GString *str = NULL;
-
-	/* We don't use purple_strempty here because if we're passed an empty
-	 * string, we should return a newly allocated empty string.
-	 */
-	if(text == NULL) {
-		return NULL;
-	}
-
-	str = g_string_new("");
-
-	for(int i = 0; text[i] != '\0'; i++) {
-		switch(text[i]) {
-		case IRCV3_FORMAT_BOLD:
-		case IRCV3_FORMAT_ITALIC:
-		case IRCV3_FORMAT_MONOSPACE:
-		case IRCV3_FORMAT_RESET:
-		case IRCV3_FORMAT_REVERSE:
-		case IRCV3_FORMAT_STRIKETHROUGH:
-		case IRCV3_FORMAT_UNDERLINE:
-			continue;
-			break;
-		case IRCV3_FORMAT_COLOR:
-			if(g_ascii_isdigit(text[i + 1])) {
-				i += 1;
-
-				if(g_ascii_isdigit(text[i + 1])) {
-					i += 1;
-				}
-
-				if(text[i + 1] == ',' && g_ascii_isdigit(text[i + 2])) {
-					i += 2;
-
-					if(g_ascii_isdigit(text[i + 1])) {
-						i += 1;
-					}
-				}
-			}
-
-			break;
-		case IRCV3_FORMAT_HEX_COLOR:
-			if(purple_ircv3_formatting_is_hex_color(&text[i + 1])) {
-				i += 6;
-			}
-
-			if(text[i + 1] == ',' &&
-			   purple_ircv3_formatting_is_hex_color(&text[i + 2]))
-			{
-				i += 7;
-			}
-
-			break;
-		default:
-			g_string_append_c(str, text[i]);
-			break;
-		}
-
-	}
-
-	return g_string_free(str, FALSE);
-}
diff --git a/protocols/ircv3/purpleircv3message.h b/protocols/ircv3/purpleircv3message.h
deleted file mode 100644
--- a/protocols/ircv3/purpleircv3message.h
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here.  Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library 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 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
-    !defined(PURPLE_IRCV3_COMPILATION)
-# error "only <libpurple/protocols/ircv3.h> may be included directly"
-#endif
-
-#ifndef PURPLE_IRCV3_MESSAGE_H
-#define PURPLE_IRCV3_MESSAGE_H
-
-#include <glib.h>
-
-#include <gplugin.h>
-#include <gplugin-native.h>
-
-#include <purple.h>
-
-#include "purpleircv3version.h"
-
-G_BEGIN_DECLS
-
-#define PURPLE_IRCV3_TYPE_MESSAGE (purple_ircv3_message_get_type())
-
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-G_DECLARE_FINAL_TYPE(PurpleIRCv3Message, purple_ircv3_message, PURPLE_IRCV3,
-                     MESSAGE, GObject)
-
-/**
- * purple_ircv3_message_new:
- * @command: (not nullable): The command of the message.
- *
- * Creates a new message with @command.
- *
- * Returns: (transfer full): The new message.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-PurpleIRCv3Message *purple_ircv3_message_new(const char *command);
-
-/**
- * purple_ircv3_message_get_command:
- * @message: The instance.
- *
- * Gets the command of the message.
- *
- * Returns: The command of the message.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-const char *purple_ircv3_message_get_command(PurpleIRCv3Message *message);
-
-/**
- * purple_ircv3_message_set_command:
- * @message: The instance.
- * @command: The new command.
- *
- * Sets the command for @message to @command.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-void purple_ircv3_message_set_command(PurpleIRCv3Message *message, const char *command);
-
-/**
- * purple_ircv3_message_get_params:
- * @message: The instance.
- *
- * Gets the parameters from @message.
- *
- * Returns: (transfer none) (nullable): The parameters from @message.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-GStrv purple_ircv3_message_get_params(PurpleIRCv3Message *message);
-
-/**
- * purple_ircv3_message_set_params:
- * @message: The instance.
- * @params: (transfer none) (nullable): The new parameters.
- *
- * Sets the parameters of @message to @params.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-void purple_ircv3_message_set_params(PurpleIRCv3Message *message, GStrv params);
-
-/**
- * purple_ircv3_message_get_source:
- * @message: The instance.
- *
- * Gets the the source of @message.
- *
- * Returns: (nullable): The source of @message.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-const char *purple_ircv3_message_get_source(PurpleIRCv3Message *message);
-
-/**
- * purple_ircv3_message_set_source:
- * @message: The instance.
- * @source: (nullable): The new source.
- *
- * Sets the source of @message to @source.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-void purple_ircv3_message_set_source(PurpleIRCv3Message *message, const char *source);
-
-/**
- * purple_ircv3_message_get_tags:
- * @message: The instance.
- *
- * Gets the tags from @message.
- *
- * Returns: (transfer none) (element-type utf8 utf8) (nullable): The tags from
- *          @message.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-GHashTable *purple_ircv3_message_get_tags(PurpleIRCv3Message *message);
-
-/**
- * purple_ircv3_message_set_tags:
- * @message: The instance.
- * @tags: (transfer none) (element-type utf8 utf8) (nullable): The new tags.
- *
- * Sets the tags of @message to @tags.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-void purple_ircv3_message_set_tags(PurpleIRCv3Message *message, GHashTable *tags);
-
-G_END_DECLS
-
-#endif /* PURPLE_IRCV3_MESSAGE_H */
diff --git a/protocols/ircv3/purpleircv3message.c b/protocols/ircv3/purpleircv3message.c
deleted file mode 100644
--- a/protocols/ircv3/purpleircv3message.c
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here.  Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library 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 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#include <glib/gi18n-lib.h>
-
-#include "purpleircv3message.h"
-
-enum {
-	PROP_0,
-	PROP_COMMAND,
-	PROP_SOURCE,
-	PROP_PARAMS,
-	PROP_TAGS,
-	N_PROPERTIES,
-};
-static GParamSpec *properties[N_PROPERTIES] = {NULL, };
-
-struct _PurpleIRCv3Message {
-	GObject parent;
-
-	char *command;
-	GStrv params;
-	char *source;
-	GHashTable *tags;
-};
-
-/******************************************************************************
- * GObject Implementation
- *****************************************************************************/
-G_DEFINE_FINAL_TYPE(PurpleIRCv3Message, purple_ircv3_message, G_TYPE_OBJECT)
-
-static void
-purple_ircv3_message_finalize(GObject *obj) {
-	PurpleIRCv3Message *message = PURPLE_IRCV3_MESSAGE(obj);
-
-	g_clear_pointer(&message->command, g_free);
-	g_clear_pointer(&message->params, g_strfreev);
-	g_clear_pointer(&message->source, g_free);
-	g_clear_pointer(&message->tags, g_hash_table_unref);
-
-	G_OBJECT_CLASS(purple_ircv3_message_parent_class)->finalize(obj);
-}
-
-static void
-purple_ircv3_message_get_property(GObject *obj, guint param_id, GValue *value,
-                                  GParamSpec *pspec)
-{
-	PurpleIRCv3Message *message = PURPLE_IRCV3_MESSAGE(obj);
-
-	switch(param_id) {
-	case PROP_COMMAND:
-		g_value_set_string(value, purple_ircv3_message_get_command(message));
-		break;
-	case PROP_PARAMS:
-		g_value_set_boxed(value, purple_ircv3_message_get_params(message));
-		break;
-	case PROP_SOURCE:
-		g_value_set_string(value, purple_ircv3_message_get_source(message));
-		break;
-	case PROP_TAGS:
-		g_value_set_boxed(value, purple_ircv3_message_get_tags(message));
-		break;
-	default:
-		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
-		break;
-	}
-}
-
-static void
-purple_ircv3_message_set_property(GObject *obj, guint param_id,
-                                  const GValue *value, GParamSpec *pspec)
-{
-	PurpleIRCv3Message *message = PURPLE_IRCV3_MESSAGE(obj);
-
-	switch(param_id) {
-	case PROP_COMMAND:
-		purple_ircv3_message_set_command(message, g_value_get_string(value));
-		break;
-	case PROP_PARAMS:
-		purple_ircv3_message_set_params(message, g_value_get_boxed(value));
-		break;
-	case PROP_SOURCE:
-		purple_ircv3_message_set_source(message, g_value_get_string(value));
-		break;
-	case PROP_TAGS:
-		purple_ircv3_message_set_tags(message, g_value_get_boxed(value));
-		break;
-	default:
-		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
-		break;
-	}
-}
-
-static void
-purple_ircv3_message_init(G_GNUC_UNUSED PurpleIRCv3Message *message) {
-}
-
-static void
-purple_ircv3_message_class_init(PurpleIRCv3MessageClass *klass) {
-	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
-
-	obj_class->finalize = purple_ircv3_message_finalize;
-	obj_class->get_property = purple_ircv3_message_get_property;
-	obj_class->set_property = purple_ircv3_message_set_property;
-
-	/**
-	 * PurpleIRCv3Message:command:
-	 *
-	 * The command of this message.
-	 *
-	 * This could be something like JOIN or a server reply numeric like 005.
-	 *
-	 * Since: 3.0
-	 */
-	properties[PROP_COMMAND] = g_param_spec_string(
-		"command", "command",
-		"The command of the message.",
-		NULL,
-		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * PurpleIRCv3Message:params:
-	 *
-	 * The parameters of the message.
-	 *
-	 * When serialized, the last item will be prefixed with a :.
-	 *
-	 * Since: 3.0
-	 */
-	properties[PROP_PARAMS] = g_param_spec_boxed(
-		"params", "params",
-		"The parameters of this message.",
-		G_TYPE_STRV,
-		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * PurpleIRCv3Message:source:
-	 *
-	 * The source of the message.
-	 *
-	 * This could be a nickname, a full nick!ident@server, a server name, or
-	 * %NULL.
-	 *
-	 * Since: 3.0
-	 */
-	properties[PROP_SOURCE] = g_param_spec_string(
-		"source", "source",
-		"The source of the message.",
-		NULL,
-		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * PurpleIRCv3Message:tags:
-	 *
-	 * The [ircv3 message tags](https://ircv3.net/specs/extensions/message-tags)
-	 * for the message.
-	 *
-	 * Since: 3.0
-	 */
-	properties[PROP_TAGS] = g_param_spec_boxed(
-		"tags", "tags",
-		"The ircv3 message tags from the message.",
-		G_TYPE_HASH_TABLE,
-		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
-
-	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
-}
-
-/******************************************************************************
- * Public API
- *****************************************************************************/
-PurpleIRCv3Message *
-purple_ircv3_message_new(const char *command) {
-	return g_object_new(
-		PURPLE_IRCV3_TYPE_MESSAGE,
-		"command", command,
-		NULL);
-}
-
-const char *
-purple_ircv3_message_get_command(PurpleIRCv3Message *message) {
-	g_return_val_if_fail(PURPLE_IRCV3_IS_MESSAGE(message), NULL);
-
-	return message->command;
-}
-
-void
-purple_ircv3_message_set_command(PurpleIRCv3Message *message,
-                                 const char *command)
-{
-	g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(message));
-	g_return_if_fail(!purple_strempty(command));
-
-	if(g_set_str(&message->command, command)) {
-		g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_COMMAND]);
-	}
-}
-
-const char *
-purple_ircv3_message_get_source(PurpleIRCv3Message *message) {
-	g_return_val_if_fail(PURPLE_IRCV3_IS_MESSAGE(message), NULL);
-
-	return message->source;
-}
-
-void
-purple_ircv3_message_set_source(PurpleIRCv3Message *message,
-                                const char *source)
-{
-	g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(message));
-
-	if(g_set_str(&message->source, source)) {
-		g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_SOURCE]);
-	}
-}
-
-GHashTable *
-purple_ircv3_message_get_tags(PurpleIRCv3Message *message) {
-	g_return_val_if_fail(PURPLE_IRCV3_IS_MESSAGE(message), NULL);
-
-	return message->tags;
-}
-
-void
-purple_ircv3_message_set_tags(PurpleIRCv3Message *message, GHashTable *tags) {
-	g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(message));
-
-	if(message->tags != tags) {
-		g_clear_pointer(&message->tags, g_hash_table_unref);
-
-		if(tags != NULL) {
-			message->tags = g_hash_table_ref(tags);
-		}
-
-		g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_TAGS]);
-	}
-}
-
-GStrv
-purple_ircv3_message_get_params(PurpleIRCv3Message *message) {
-	g_return_val_if_fail(PURPLE_IRCV3_IS_MESSAGE(message), NULL);
-
-	return message->params;
-}
-
-void
-purple_ircv3_message_set_params(PurpleIRCv3Message *message, GStrv params) {
-	g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(message));
-
-	if(message->params != params) {
-		g_clear_pointer(&message->params, g_strfreev);
-
-		if(params != NULL) {
-			message->params = g_strdupv(params);
-		}
-
-		g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_PARAMS]);
-	}
-}
diff --git a/protocols/ircv3/purpleircv3messagehandlers.h b/protocols/ircv3/purpleircv3messagehandlers.h
--- a/protocols/ircv3/purpleircv3messagehandlers.h
+++ b/protocols/ircv3/purpleircv3messagehandlers.h
@@ -30,37 +30,16 @@
 
 #include <glib.h>
 
+#include <ibis.h>
+
 #include <purple.h>
 
-#include "purpleircv3message.h"
 #include "purpleircv3version.h"
 
 G_BEGIN_DECLS
 
-/**
- * PurpleIRCv3MessageHandler:
- * @message: The [class@Message] to handle.
- * @error: (nullable): A return address for a [type@GLib.Error].
- * @data: The user data that was passed to [method@PurpleIRCv3.Parser.parse].
- *
- * Defines the type of a function that will be called to handle @message.
- *
- * Returns: %TRUE if the command was handled properly, otherwise %FALSE and
- *          @error may be set.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_TYPE_IN_3_0
-typedef gboolean (*PurpleIRCv3MessageHandler)(PurpleIRCv3Message *message,
-                                              GError **error,
-                                              gpointer data);
-
-G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_fallback(PurpleIRCv3Message *message, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_status(PurpleIRCv3Message *message, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_status_ignore_param0(PurpleIRCv3Message *message, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_ping(PurpleIRCv3Message *message, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_privmsg(PurpleIRCv3Message *message, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_topic(PurpleIRCv3Message *message, GError **error, gpointer data);
+G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_privmsg(IbisClient *client, const char *command, IbisMessage *message, gpointer data);
+G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_topic(IbisClient *client, const char *command, IbisMessage *message, gpointer data);
 
 G_END_DECLS
 
diff --git a/protocols/ircv3/purpleircv3messagehandlers.c b/protocols/ircv3/purpleircv3messagehandlers.c
--- a/protocols/ircv3/purpleircv3messagehandlers.c
+++ b/protocols/ircv3/purpleircv3messagehandlers.c
@@ -25,107 +25,15 @@
 #include "purpleircv3messagehandlers.h"
 
 #include "purpleircv3connection.h"
-#include "purpleircv3constants.h"
 #include "purpleircv3core.h"
-#include "purpleircv3ctcp.h"
-#include "purpleircv3formatting.h"
-#include "purpleircv3source.h"
-
-/******************************************************************************
- * Fallback
- *****************************************************************************/
-gboolean
-purple_ircv3_message_handler_fallback(PurpleIRCv3Message *message,
-                                      G_GNUC_UNUSED GError **error,
-                                      gpointer data)
-{
-	PurpleIRCv3Connection *connection = data;
-	char *new_command = NULL;
-	const char *command = NULL;
-
-	command = purple_ircv3_message_get_command(message);
-
-	new_command = g_strdup_printf(_("unknown command '%s'"), command);
-	purple_ircv3_message_set_command(message, new_command);
-	purple_ircv3_connection_add_status_message(connection, message);
-
-	g_clear_pointer(&new_command, g_free);
-
-	return TRUE;
-}
-
-/******************************************************************************
- * Status Messages
- *****************************************************************************/
-gboolean
-purple_ircv3_message_handler_status(PurpleIRCv3Message *message,
-                                    G_GNUC_UNUSED GError **error,
-                                    gpointer data)
-{
-	purple_ircv3_connection_add_status_message(data, message);
-
-	return TRUE;
-}
-
-gboolean
-purple_ircv3_message_handler_status_ignore_param0(PurpleIRCv3Message *message,
-                                                  GError **error,
-                                                  gpointer data)
-{
-	GStrv params = NULL;
-	GStrv new_params = NULL;
-	guint n_params = 0;
-
-	params = purple_ircv3_message_get_params(message);
-	if(params != NULL) {
-		n_params = g_strv_length(params);
-	}
-
-	if(n_params <= 1) {
-		g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
-		            "expected n_params > 1, got %u", n_params);
-
-		return FALSE;
-	}
-
-	/* We need to make a copy because otherwise we'd get a use after free in
-	 * set_params.
-	 */
-	new_params = g_strdupv(params + 1);
-	purple_ircv3_message_set_params(message, new_params);
-	g_clear_pointer(&new_params, g_strfreev);
-
-	purple_ircv3_connection_add_status_message(data, message);
-
-	return TRUE;
-}
 
 /******************************************************************************
  * General Commands
  *****************************************************************************/
 gboolean
-purple_ircv3_message_handler_ping(PurpleIRCv3Message *message,
-                                  G_GNUC_UNUSED GError **error,
-                                  gpointer data)
-{
-	PurpleIRCv3Connection *connection = data;
-	GStrv params = NULL;
-
-	params = purple_ircv3_message_get_params(message);
-
-	if(params != NULL && g_strv_length(params) == 1) {
-		purple_ircv3_connection_writef(connection, "PONG %s", params[0]);
-	} else {
-		purple_ircv3_connection_writef(connection, "PONG");
-	}
-
-	return TRUE;
-}
-
-gboolean
-purple_ircv3_message_handler_privmsg(PurpleIRCv3Message *v3_message,
-                                     G_GNUC_UNUSED GError **error,
-                                     gpointer data)
+purple_ircv3_message_handler_privmsg(G_GNUC_UNUSED IbisClient *client,
+                                     const char *command,
+                                     IbisMessage *ibis_message, gpointer data)
 {
 	PurpleIRCv3Connection *connection = data;
 	PurpleContact *contact = NULL;
@@ -133,21 +41,18 @@
 	PurpleMessage *message = NULL;
 	PurpleMessageFlags flags = PURPLE_MESSAGE_RECV;
 	GDateTime *dt = NULL;
-	GHashTable *tags = NULL;
+	IbisTags *tags = NULL;
 	GStrv params = NULL;
-	gpointer raw_id = NULL;
-	gpointer raw_timestamp = NULL;
 	char *nick = NULL;
 	char *stripped = NULL;
-	const char *command = NULL;
 	const char *id = NULL;
+	const char *raw_tag = NULL;
 	const char *source = NULL;
 	const char *target = NULL;
 
-	command = purple_ircv3_message_get_command(v3_message);
-	params = purple_ircv3_message_get_params(v3_message);
-	source = purple_ircv3_message_get_source(v3_message);
-	tags = purple_ircv3_message_get_tags(v3_message);
+	params = ibis_message_get_params(ibis_message);
+	source = ibis_message_get_source(ibis_message);
+	tags = ibis_message_get_tags(ibis_message);
 
 	if(params == NULL) {
 		g_warning("privmsg received with no parameters");
@@ -163,7 +68,7 @@
 		return FALSE;
 	}
 
-	purple_ircv3_source_parse(source, &nick, NULL, NULL);
+	ibis_source_parse(source, &nick, NULL, NULL);
 
 	/* Find or create the conversation. */
 	target = params[0];
@@ -190,28 +95,24 @@
 		}
 	}
 
-	/* Grab the msgid if one was provided. */
-	if(g_hash_table_lookup_extended(tags, "msgid", NULL, &raw_id)) {
-		if(!purple_strempty(raw_id)) {
-			id = raw_id;
-		}
-	}
-
-	if(purple_strequal(command, PURPLE_IRCV3_MSG_NOTICE)) {
+	if(purple_strequal(command, IBIS_MSG_NOTICE)) {
 		flags |= PURPLE_MESSAGE_NOTIFY;
 	}
 
-	/* Determine the timestamp of the message. */
-	if(g_hash_table_lookup_extended(tags, "time", NULL, &raw_timestamp)) {
-		const char *timestamp = raw_timestamp;
+	/* Grab the msgid if one was provided. */
+	raw_tag = ibis_tags_lookup(tags, "msgid");
+	if(!purple_strempty(raw_tag)) {
+		id = raw_tag;
+	}
 
-		if(!purple_strempty(timestamp)) {
-			GTimeZone *tz = g_time_zone_new_utc();
+	/* Determine the timestamp of the message. */
+	raw_tag = ibis_tags_lookup(tags, "time");
+	if(!purple_strempty(raw_tag)) {
+		GTimeZone *tz = g_time_zone_new_utc();
 
-			dt = g_date_time_new_from_iso8601(timestamp, tz);
+		dt = g_date_time_new_from_iso8601(raw_tag, tz);
 
-			g_time_zone_unref(tz);
-		}
+		g_time_zone_unref(tz);
 	}
 
 	/* If the server didn't provide a time, use the current local time. */
@@ -219,7 +120,7 @@
 		dt = g_date_time_new_now_local();
 	}
 
-	stripped = purple_ircv3_formatting_strip(params[1]);
+	stripped = ibis_formatting_strip(params[1]);
 	message = g_object_new(
 		PURPLE_TYPE_MESSAGE,
 		"author", source,
@@ -233,10 +134,12 @@
 	g_date_time_unref(dt);
 
 	/* Check if this is a CTCP message. */
-	if(!purple_ircv3_ctcp_handle(connection, conversation, message)) {
-		purple_conversation_write_message(conversation, message);
+	if(ibis_message_get_ctcp(ibis_message)) {
+		/* TODO: later... */
 	}
 
+	purple_conversation_write_message(conversation, message);
+
 	g_clear_pointer(&nick, g_free);
 	g_clear_object(&message);
 
@@ -244,49 +147,44 @@
 }
 
 gboolean
-purple_ircv3_message_handler_topic(PurpleIRCv3Message *message,
-                                   GError **error,
+purple_ircv3_message_handler_topic(G_GNUC_UNUSED IbisClient *client,
+                                   const char *command, IbisMessage *message,
                                    gpointer data)
 {
 	PurpleIRCv3Connection *connection = data;
 	PurpleConversation *conversation = NULL;
 	GStrv params = NULL;
 	const char *channel = NULL;
-	const char *command = NULL;
 	const char *topic = NULL;
 	guint n_params = 0;
 
-	command = purple_ircv3_message_get_command(message);
-	params = purple_ircv3_message_get_params(message);
+	params = ibis_message_get_params(message);
 	n_params = g_strv_length(params);
 
-	if(purple_strequal(command, PURPLE_IRCV3_MSG_TOPIC)) {
+	if(purple_strequal(command, IBIS_MSG_TOPIC)) {
 		if(n_params != 2) {
-			g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
-			            "received TOPIC with %u parameters, expected 2",
-			            n_params);
+			g_message("received TOPIC with %u parameters, expected 2",
+			          n_params);
 
 			return FALSE;
 		}
 
 		channel = params[0];
 		topic = params[1];
-	} else if(purple_strequal(command, PURPLE_IRCV3_RPL_NOTOPIC)) {
+	} else if(purple_strequal(command, IBIS_RPL_NOTOPIC)) {
 		if(n_params != 3) {
-			g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
-			            "received RPL_NOTOPIC with %u parameters, expected 3",
-			            n_params);
+			g_message("received RPL_NOTOPIC with %u parameters, expected 3",
+			          n_params);
 
 			return FALSE;
 		}
 
 		channel = params[1];
 		topic = "";
-	} else if(purple_strequal(command, PURPLE_IRCV3_RPL_TOPIC)) {
+	} else if(purple_strequal(command, IBIS_RPL_TOPIC)) {
 		if(n_params != 3) {
-			g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
-			            "received RPL_TOPIC with %u parameters, expected 3",
-			            n_params);
+			g_message("received RPL_TOPIC with %u parameters, expected 3",
+			          n_params);
 
 			return FALSE;
 		}
@@ -294,8 +192,7 @@
 		channel = params[1];
 		topic = params[2];
 	} else {
-		g_set_error(error, PURPLE_IRCV3_DOMAIN, 0, "unexpected command %s",
-		            command);
+		g_message("unexpected command %s", command);
 
 		return FALSE;
 	}
@@ -303,8 +200,7 @@
 	conversation = purple_ircv3_connection_find_or_create_conversation(connection,
 	                                                                   channel);
 	if(!PURPLE_IS_CONVERSATION(conversation)) {
-		g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
-		            "failed to find or create channel '%s'", channel);
+		g_message("failed to find or create channel '%s'", channel);
 
 		return FALSE;
 	}
diff --git a/protocols/ircv3/purpleircv3parser.h b/protocols/ircv3/purpleircv3parser.h
deleted file mode 100644
--- a/protocols/ircv3/purpleircv3parser.h
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here.  Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library 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 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
-    !defined(PURPLE_IRCV3_COMPILATION)
-# error "only <libpurple/protocols/ircv3.h> may be included directly"
-#endif
-
-#ifndef PURPLE_IRCV3_PARSER_H
-#define PURPLE_IRCV3_PARSER_H
-
-#include <glib.h>
-#include <glib-object.h>
-
-#include <purple.h>
-
-#include "purpleircv3messagehandlers.h"
-#include "purpleircv3version.h"
-
-G_BEGIN_DECLS
-
-#define PURPLE_IRCV3_TYPE_PARSER (purple_ircv3_parser_get_type())
-
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-G_DECLARE_FINAL_TYPE(PurpleIRCv3Parser, purple_ircv3_parser, PURPLE_IRCV3,
-                     PARSER, GObject)
-
-/**
- * purple_ircv3_parser_new:
- *
- * Creates a new instance.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-PurpleIRCv3Parser *purple_ircv3_parser_new(void);
-
-/**
- * purple_ircv3_parser_set_fallback_handler: (skip):
- * @parser: The instance.
- * @handler: A [func@PurpleIRCv3.MessageHandler].
- *
- * Sets @handler to be called for any messages that @parser doesn't know how to
- * handle.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-void purple_ircv3_parser_set_fallback_handler(PurpleIRCv3Parser *parser, PurpleIRCv3MessageHandler handler);
-
-/**
- * purple_ircv3_parser_parse:
- * @parser: The instance.
- * @buffer: The buffer to parse.
- * @error: Return address for a #GError, or %NULL.
- * @data: (nullable): Optional data to pass to the handler.
- *
- * Parses @buffer with @parser.
- *
- * Returns: %TRUE if the buffer was parsed correctly or %FALSE with @error set.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-gboolean purple_ircv3_parser_parse(PurpleIRCv3Parser *parser, const gchar *buffer, GError **error, gpointer data);
-
-/**
- * purple_ircv3_parser_add_handler:
- * @parser: The instance.
- * @command: The command string.
- * @handler: (scope forever): The handler to call.
- *
- * Calls @handler every time @parser finds the command named @command.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-void purple_ircv3_parser_add_handler(PurpleIRCv3Parser *parser, const char *command, PurpleIRCv3MessageHandler handler);
-
-/**
- * purple_ircv3_parser_add_handlers:
- * @parser: The instance.
- * @handler: (scope forever): The handler to call when the command is received.
- * @...: A %NULL terminated list of string command names.
- *
- * Like [method@Parser.add_handler] but allows you to add multiple commands at
- * once that share a handler.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-void purple_ircv3_parser_add_handlers(PurpleIRCv3Parser *parser, PurpleIRCv3MessageHandler handler, ...) G_GNUC_NULL_TERMINATED;
-
-/**
- * purple_ircv3_parser_add_default_handlers:
- * @parser: The instance.
- *
- * Adds all of the default handlers to @parser.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-void purple_ircv3_parser_add_default_handlers(PurpleIRCv3Parser *parser);
-
-G_END_DECLS
-
-#endif /* PURPLE_IRCV3_PARSER_H */
diff --git a/protocols/ircv3/purpleircv3parser.c b/protocols/ircv3/purpleircv3parser.c
deleted file mode 100644
--- a/protocols/ircv3/purpleircv3parser.c
+++ /dev/null
@@ -1,507 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here.  Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library 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 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#include "purpleircv3parser.h"
-
-#include "purpleircv3capabilities.h"
-#include "purpleircv3constants.h"
-#include "purpleircv3core.h"
-#include "purpleircv3message.h"
-#include "purpleircv3messagehandlers.h"
-#include "purpleircv3sasl.h"
-
-struct _PurpleIRCv3Parser {
-	GObject parent;
-
-	GRegex *regex_message;
-	GRegex *regex_tags;
-
-	PurpleIRCv3MessageHandler fallback_handler;
-	GHashTable *handlers;
-};
-
-G_DEFINE_FINAL_TYPE(PurpleIRCv3Parser, purple_ircv3_parser, G_TYPE_OBJECT)
-
-/******************************************************************************
- * Helpers
- *****************************************************************************/
-static char *
-purple_ircv3_parser_unescape_tag_value(const char *value) {
-	GString *unescaped = g_string_new("");
-	gboolean escaping = FALSE;
-
-	/* Walk the string and replace escaped values according to
-	 * https://ircv3.net/specs/extensions/message-tags.html#escaping-values
-	 */
-	for(int i = 0; value[i] != '\0'; i++) {
-		if(escaping) {
-			/* Set the replacement to the current character which will fall
-			 * through for everything, including '\\'
-			 */
-			char replacement = value[i];
-
-			if(value[i] == ':') {
-				replacement = ';';
-			} else if(value[i] == 's') {
-				replacement = ' ';
-			} else if(value[i] == 'r') {
-				replacement = '\r';
-			} else if(value[i] == 'n') {
-				replacement = '\n';
-			}
-
-			g_string_append_c(unescaped, replacement);
-			escaping = FALSE;
-		} else {
-			if(value[i] == '\\') {
-				escaping = TRUE;
-			} else {
-				g_string_append_c(unescaped, value[i]);
-			}
-		}
-	}
-
-	return g_string_free(unescaped, FALSE);
-}
-
-static GHashTable *
-purple_ircv3_parser_parse_tags(PurpleIRCv3Parser *parser,
-                               const gchar *tags_string, GError **error)
-{
-	GError *local_error = NULL;
-	GHashTable *tags = NULL;
-	GMatchInfo *info = NULL;
-	gboolean matches = FALSE;
-
-	tags = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
-
-	/* tags_string can never be NULL, because g_match_info_fetch_named always
-	 * returns a string. So if we were passed an empty string, just return the
-	 * empty hash table.
-	 */
-	if(*tags_string == '\0') {
-		return tags;
-	}
-
-	matches = g_regex_match_full(parser->regex_tags, tags_string, -1, 0, 0,
-	                             &info, &local_error);
-
-	if(local_error != NULL) {
-		g_propagate_error(error, local_error);
-
-		g_match_info_unref(info);
-
-		return tags;
-	}
-
-	if(!matches) {
-		g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
-		                    "failed to parse tags: unknown error");
-
-		g_match_info_unref(info);
-
-		return tags;
-	}
-
-	while(g_match_info_matches(info)) {
-		char *key = NULL;
-		char *value = NULL;
-		char *unescaped = NULL;
-
-		key = g_match_info_fetch_named(info, "key");
-		value = g_match_info_fetch_named(info, "value");
-
-		unescaped = purple_ircv3_parser_unescape_tag_value(value);
-		g_free(value);
-
-		/* the hash table is created with destroy notifies for both key and
-		 * value, so there's no need to free the allocated memory right now.
-		 */
-		g_hash_table_insert(tags, key, unescaped);
-		g_match_info_next(info, &local_error);
-
-		if(local_error != NULL) {
-			g_propagate_error(error, local_error);
-
-			break;
-		}
-	}
-
-	g_match_info_unref(info);
-
-	return tags;
-}
-
-static guint
-purple_ircv3_parser_extract_params(G_GNUC_UNUSED PurpleIRCv3Parser *parser,
-                                   GStrvBuilder *builder, const gchar *str)
-{
-	gchar *ptr = NULL;
-	guint count = 0;
-
-	/* Loop through str finding each space separated string. */
-	while(str != NULL && *str != '\0') {
-		/* Look for a space. */
-		ptr = strchr(str, ' ');
-
-		/* If we found one, set it to null terminator and add the string to our
-		 * builder.
-		 */
-		if(ptr != NULL) {
-			*ptr = '\0';
-			g_strv_builder_add(builder, str);
-
-			/* Move str to the next character as we know there's another
-			 * character which might be another null terminator.
-			 */
-			str = ptr + 1;
-
-			/* And don't forget to increment the count... ah ah ah! */
-			count++;
-		} else {
-			/* Add the remaining string. */
-			g_strv_builder_add(builder, str);
-
-			/* Give the count another one, ah ah ah! */
-			count++;
-
-			/* Finally break out of the loop. */
-			break;
-		}
-	}
-
-	return count;
-}
-
-static GStrv
-purple_ircv3_parser_build_params(PurpleIRCv3Parser *parser,
-                                 const gchar *middle, const gchar *coda,
-                                 const gchar *trailing, guint *n_params)
-{
-	GStrvBuilder *builder = g_strv_builder_new();
-	GStrv result = NULL;
-
-	purple_ircv3_parser_extract_params(parser, builder, middle);
-
-	if(*coda != '\0') {
-		g_strv_builder_add(builder, trailing);
-	}
-
-	result = g_strv_builder_end(builder);
-
-	g_strv_builder_unref(builder);
-
-	if(result != NULL && n_params != NULL) {
-		*n_params = g_strv_length(result);
-	}
-
-	return result;
-}
-
-/******************************************************************************
- * Handlers
- *****************************************************************************/
-static gboolean
-purple_ircv3_fallback_handler(PurpleIRCv3Message *message,
-                              GError **error,
-                              G_GNUC_UNUSED gpointer data)
-{
-	g_set_error(error, PURPLE_IRCV3_DOMAIN, 0, "no handler for command %s",
-	            purple_ircv3_message_get_command(message));
-
-	return FALSE;
-}
-
-/******************************************************************************
- * GObject Implementation
- *****************************************************************************/
-static void
-purple_ircv3_parser_finalize(GObject *obj) {
-	PurpleIRCv3Parser *parser = PURPLE_IRCV3_PARSER(obj);
-
-	g_clear_pointer(&parser->regex_message, g_regex_unref);
-	g_clear_pointer(&parser->regex_tags, g_regex_unref);
-
-	g_hash_table_destroy(parser->handlers);
-
-	G_OBJECT_CLASS(purple_ircv3_parser_parent_class)->finalize(obj);
-}
-
-static void
-purple_ircv3_parser_init(PurpleIRCv3Parser *parser) {
-	parser->regex_message =  g_regex_new("(?:@(?<tags>[^ ]+) )?"
-	                                     "(?::(?<source>[^ ]+) +)?"
-	                                     "(?<command>[^ :]+)"
-	                                     "(?: +(?<middle>(?:[^ :]+(?: +[^ :]+)*)))*"
-	                                     "(?<coda> +:(?<trailing>.*)?)?",
-	                                     0, 0, NULL);
-	g_assert(parser->regex_message != NULL);
-
-	parser->regex_tags = g_regex_new("(?<key>(?<client_prefix>\\+?)"
-	                                        "(?:(?<vendor>[A-Za-z0-9-\\.]+)\\/)?"
-	                                        "(?<key_name>[A-Za-z0-9-]+)"
-	                                 ")"
-	                                 "(?:=(?<value>[^\r\n;]*))?(?:;|$)",
-	                                 0, 0, NULL);
-	g_assert(parser->regex_tags != NULL);
-
-	parser->fallback_handler = purple_ircv3_fallback_handler;
-	parser->handlers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
-	                                         NULL);
-}
-
-static void
-purple_ircv3_parser_class_init(PurpleIRCv3ParserClass *klass) {
-	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
-
-	obj_class->finalize = purple_ircv3_parser_finalize;
-}
-
-/******************************************************************************
- * Public API
- *****************************************************************************/
-PurpleIRCv3Parser *
-purple_ircv3_parser_new(void) {
-	return g_object_new(PURPLE_IRCV3_TYPE_PARSER, NULL);
-}
-
-void
-purple_ircv3_parser_set_fallback_handler(PurpleIRCv3Parser *parser,
-                                         PurpleIRCv3MessageHandler handler)
-{
-	g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser));
-
-	parser->fallback_handler = handler;
-}
-
-gboolean
-purple_ircv3_parser_parse(PurpleIRCv3Parser *parser, const gchar *buffer,
-                          GError **error, gpointer data)
-{
-	PurpleIRCv3Message *message = NULL;
-	PurpleIRCv3MessageHandler handler = NULL;
-	GError *local_error = NULL;
-	GHashTable *tags = NULL;
-	GMatchInfo *info = NULL;
-	GStrv params = NULL;
-	gchar *coda = NULL;
-	gchar *command = NULL;
-	gchar *middle = NULL;
-	gchar *source = NULL;
-	gchar *tags_string = NULL;
-	gchar *trailing = NULL;
-	gboolean matches = FALSE;
-	gboolean result = FALSE;
-
-	g_return_val_if_fail(PURPLE_IRCV3_IS_PARSER(parser), FALSE);
-	g_return_val_if_fail(buffer != NULL, FALSE);
-
-	/* Check if the buffer matches our regex for messages. */
-	matches = g_regex_match(parser->regex_message, buffer, 0, &info);
-	if(!matches) {
-		g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
-		            "failed to parser buffer '%s'", buffer);
-
-		g_match_info_unref(info);
-
-		return FALSE;
-	}
-
-	/* Extract the command from the buffer, so we can find the handler. */
-	command = g_match_info_fetch_named(info, "command");
-	handler = g_hash_table_lookup(parser->handlers, command);
-	if(handler == NULL) {
-		if(parser->fallback_handler == NULL) {
-			g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
-			            "no handler found for command %s and no default "
-			            "handler set.", command);
-
-			g_free(command);
-			g_match_info_unref(info);
-
-			return FALSE;
-		}
-
-		handler = parser->fallback_handler;
-	}
-
-	/* If we made it this far, we have our handler, so lets get the rest of the
-	 * parameters and call the handler.
-	 */
-	message = purple_ircv3_message_new(command);
-
-	tags_string = g_match_info_fetch_named(info, "tags");
-	tags = purple_ircv3_parser_parse_tags(parser, tags_string, &local_error);
-	g_free(tags_string);
-	if(local_error != NULL) {
-		g_propagate_error(error, local_error);
-
-		g_free(command);
-		g_clear_object(&message);
-		g_hash_table_destroy(tags);
-		g_match_info_unref(info);
-
-		return FALSE;
-	}
-	if(tags != NULL) {
-		purple_ircv3_message_set_tags(message, tags);
-		g_hash_table_unref(tags);
-	}
-
-	source = g_match_info_fetch_named(info, "source");
-	if(!purple_strempty(source)) {
-		purple_ircv3_message_set_source(message, source);
-	}
-	g_free(source);
-
-	middle = g_match_info_fetch_named(info, "middle");
-	coda = g_match_info_fetch_named(info, "coda");
-	trailing = g_match_info_fetch_named(info, "trailing");
-	params = purple_ircv3_parser_build_params(parser, middle, coda, trailing,
-	                                          NULL);
-	if(params != NULL) {
-		purple_ircv3_message_set_params(message, params);
-	}
-
-	g_free(command);
-	g_free(middle);
-	g_free(coda);
-	g_free(trailing);
-	g_strfreev(params);
-
-	/* Call the handler. */
-	result = handler(message, error, data);
-
-	/* Cleanup the left overs. */
-	g_match_info_unref(info);
-	g_clear_object(&message);
-
-	return result;
-}
-
-void
-purple_ircv3_parser_add_handler(PurpleIRCv3Parser *parser,
-                                const char *command,
-                                PurpleIRCv3MessageHandler handler)
-{
-	g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser));
-	g_return_if_fail(command != NULL);
-	g_return_if_fail(handler != NULL);
-
-	g_hash_table_insert(parser->handlers, g_strdup(command), handler);
-}
-
-void
-purple_ircv3_parser_add_handlers(PurpleIRCv3Parser *parser,
-                                 PurpleIRCv3MessageHandler handler,
-                                 ...)
-{
-	va_list vargs;
-	const char *command = NULL;
-
-	g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser));
-	g_return_if_fail(handler != NULL);
-
-	va_start(vargs, handler);
-
-	while((command = va_arg(vargs, const char *)) != NULL) {
-		purple_ircv3_parser_add_handler(parser, command, handler);
-	}
-
-	va_end(vargs);
-}
-
-void
-purple_ircv3_parser_add_default_handlers(PurpleIRCv3Parser *parser) {
-	g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser));
-
-	purple_ircv3_parser_set_fallback_handler(parser,
-	                                         purple_ircv3_message_handler_fallback);
-
-	/* Core functionality. */
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_CAP,
-	                                purple_ircv3_capabilities_message_handler);
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_NOTICE,
-	                                purple_ircv3_message_handler_privmsg);
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_PING,
-	                                purple_ircv3_message_handler_ping);
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_PRIVMSG,
-	                                purple_ircv3_message_handler_privmsg);
-
-	/* Topic stuff. */
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_TOPIC,
-	                                purple_ircv3_message_handler_topic);
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_NOTOPIC,
-	                                purple_ircv3_message_handler_topic);
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_TOPIC,
-	                                purple_ircv3_message_handler_topic);
-
-	/* Post Registration Greetings */
-	purple_ircv3_parser_add_handlers(parser,
-	                                 purple_ircv3_message_handler_status_ignore_param0,
-	                                 PURPLE_IRCV3_RPL_WELCOME,
-	                                 PURPLE_IRCV3_RPL_YOURHOST,
-	                                 PURPLE_IRCV3_RPL_CREATED,
-	                                 PURPLE_IRCV3_RPL_MYINFO,
-	                                 NULL);
-
-	/* Luser's */
-	purple_ircv3_parser_add_handlers(parser,
-	                                 purple_ircv3_message_handler_status_ignore_param0,
-	                                 PURPLE_IRCV3_RPL_LUSERCLIENT,
-	                                 PURPLE_IRCV3_RPL_LUSEROP,
-	                                 PURPLE_IRCV3_RPL_LUSERUNKNOWN,
-	                                 PURPLE_IRCV3_RPL_LUSERCHANNELS,
-	                                 PURPLE_IRCV3_RPL_LUSERME,
-	                                 NULL);
-
-	/* MOTD */
-	purple_ircv3_parser_add_handlers(parser,
-	                                 purple_ircv3_message_handler_status_ignore_param0,
-	                                 PURPLE_IRCV3_RPL_MOTD,
-	                                 PURPLE_IRCV3_RPL_MOTDSTART,
-	                                 PURPLE_IRCV3_RPL_ENDOFMOTD,
-	                                 NULL);
-
-	/* SASL stuff. */
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_LOGGEDIN,
-	                                purple_ircv3_sasl_logged_in);
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_LOGGEDOUT,
-	                                purple_ircv3_sasl_logged_out);
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_NICKLOCKED,
-	                                purple_ircv3_sasl_nick_locked);
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_SASLSUCCESS,
-	                                purple_ircv3_sasl_success);
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLFAIL,
-	                                purple_ircv3_sasl_failed);
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLTOOLONG,
-	                                purple_ircv3_sasl_message_too_long);
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLABORTED,
-	                                purple_ircv3_sasl_aborted);
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLALREADY,
-	                                purple_ircv3_sasl_already_authed);
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_SASLMECHS,
-	                                purple_ircv3_sasl_mechanisms);
-	purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_AUTHENTICATE,
-	                                purple_ircv3_sasl_authenticate);
-
-}
diff --git a/protocols/ircv3/purpleircv3protocolconversation.c b/protocols/ircv3/purpleircv3protocolconversation.c
--- a/protocols/ircv3/purpleircv3protocolconversation.c
+++ b/protocols/ircv3/purpleircv3protocolconversation.c
@@ -22,10 +22,11 @@
 
 #include <glib/gi18n-lib.h>
 
+#include <ibis.h>
+
 #include "purpleircv3protocolconversation.h"
 
 #include "purpleircv3connection.h"
-#include "purpleircv3constants.h"
 #include "purpleircv3core.h"
 
 /******************************************************************************
@@ -42,6 +43,8 @@
 	PurpleIRCv3Connection *v3_connection = NULL;
 	PurpleAccount *account = NULL;
 	PurpleConnection *connection = NULL;
+	IbisClient *client = NULL;
+	IbisMessage *ibis_message = NULL;
 	GTask *task = NULL;
 	const char *id = NULL;
 
@@ -57,8 +60,12 @@
 		id = purple_conversation_get_name(conversation);
 	}
 
-	purple_ircv3_connection_writef(v3_connection, "PRIVMSG %s :%s", id,
-	                               purple_message_get_contents(message));
+	client = purple_ircv3_connection_get_client(v3_connection);
+
+	ibis_message = ibis_message_new(IBIS_MSG_PRIVMSG);
+	ibis_message_set_params(ibis_message, id,
+	                        purple_message_get_contents(message), NULL);
+	ibis_client_write(client, ibis_message);
 
 	task = g_task_new(protocol, cancellable, callback, data);
 	g_task_return_boolean(task, TRUE);
@@ -97,7 +104,8 @@
 	PurpleConnection *connection = NULL;
 	PurpleConversation *conversation = NULL;
 	PurpleConversationManager *manager = NULL;
-	GString *cmd = NULL;
+	IbisClient *client = NULL;
+	IbisMessage *message = NULL;
 	GTask *task = NULL;
 	const char *name = NULL;
 	const char *password = NULL;
@@ -130,13 +138,13 @@
 		return;
 	}
 
-	/* Build our join string. */
-	cmd = g_string_new(NULL);
-	g_string_append_printf(cmd, "%s %s", PURPLE_IRCV3_MSG_JOIN, name);
+	message = ibis_message_new(IBIS_MSG_JOIN);
 
 	password = purple_channel_join_details_get_password(details);
 	if(!purple_strempty(password)) {
-		g_string_append_printf(cmd, " %s", password);
+		ibis_message_set_params(message, name, password, NULL);
+	} else {
+		ibis_message_set_params(message, name, NULL);
 	}
 
 	conversation = g_object_new(
@@ -149,8 +157,8 @@
 	purple_conversation_manager_register(manager, conversation);
 	g_clear_object(&conversation);
 
-	purple_ircv3_connection_writef(v3_connection, "%s", cmd->str);
-	g_string_free(cmd, TRUE);
+	client = purple_ircv3_connection_get_client(v3_connection);
+	ibis_client_write(client, message);
 
 	g_task_return_boolean(task, TRUE);
 	g_clear_object(&task);
diff --git a/protocols/ircv3/purpleircv3sasl.h b/protocols/ircv3/purpleircv3sasl.h
deleted file mode 100644
--- a/protocols/ircv3/purpleircv3sasl.h
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here.  Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library 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 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
-    !defined(PURPLE_IRCV3_COMPILATION)
-# error "only <libpurple/protocols/ircv3.h> may be included directly"
-#endif
-
-#ifndef PURPLE_IRCV3_SASL_H
-#define PURPLE_IRCV3_SASL_H
-
-#include <glib.h>
-
-#include <purple.h>
-
-#include "purpleircv3capabilities.h"
-#include "purpleircv3message.h"
-
-G_BEGIN_DECLS
-
-G_GNUC_INTERNAL void purple_ircv3_sasl_request(PurpleIRCv3Capabilities *capabilities);
-
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_logged_in(PurpleIRCv3Message *message, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_logged_out(PurpleIRCv3Message *message, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_nick_locked(PurpleIRCv3Message *message, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_success(PurpleIRCv3Message *message, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_failed(PurpleIRCv3Message *message, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_message_too_long(PurpleIRCv3Message *message, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_aborted(PurpleIRCv3Message *message, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_already_authed(PurpleIRCv3Message *message, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_mechanisms(PurpleIRCv3Message *message, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_authenticate(PurpleIRCv3Message *message, GError **error, gpointer data);
-
-G_END_DECLS
-
-#endif /* PURPLE_IRCV3_SASL_H */
diff --git a/protocols/ircv3/purpleircv3sasl.c b/protocols/ircv3/purpleircv3sasl.c
deleted file mode 100644
--- a/protocols/ircv3/purpleircv3sasl.c
+++ /dev/null
@@ -1,563 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here.  Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library 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 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#include <glib/gi18n-lib.h>
-
-#include <hasl.h>
-
-#include "purpleircv3sasl.h"
-
-#include "purpleircv3capabilities.h"
-#include "purpleircv3connection.h"
-#include "purpleircv3constants.h"
-#include "purpleircv3core.h"
-
-#define PURPLE_IRCV3_SASL_DATA_KEY ("sasl-data")
-
-typedef struct {
-	PurpleConnection *connection;
-
-	HaslContext *ctx;
-
-	GString *server_in_buffer;
-} PurpleIRCv3SASLData;
-
-/******************************************************************************
- * Helpers
- *****************************************************************************/
-static const char *
-purple_ircv3_sasl_get_username(PurpleConnection *connection) {
-	PurpleAccount *account = NULL;
-	const char *username = NULL;
-
-	account = purple_connection_get_account(connection);
-
-	username = purple_account_get_string(account, "sasl-login-name", "");
-	if(username != NULL && username[0] != '\0') {
-		return username;
-	}
-
-	return purple_connection_get_display_name(connection);
-}
-
-/******************************************************************************
- * SASL Helpers
- *****************************************************************************/
-static void
-purple_ircv3_sasl_data_free(PurpleIRCv3SASLData *data) {
-	g_clear_object(&data->ctx);
-
-	g_string_free(data->server_in_buffer, TRUE);
-
-	g_free(data);
-}
-
-static void
-purple_ircv3_sasl_data_add(PurpleConnection *connection, HaslContext *ctx) {
-	PurpleIRCv3SASLData *data = NULL;
-
-	data = g_new0(PurpleIRCv3SASLData, 1);
-	g_object_set_data_full(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY,
-	                       data, (GDestroyNotify)purple_ircv3_sasl_data_free);
-
-	/* We don't reference this because the life cycle of this data is tied
-	 * directly to the connection and adding a reference to the connection
-	 * would keep both alive forever.
-	 */
-	data->connection = connection;
-	data->ctx = ctx;
-
-	/* We truncate the server_in_buffer when we need to so that we can minimize
-	 * allocations and simplify the logic involved with it.
-	 */
-	data->server_in_buffer = g_string_new("");
-}
-
-static void
-purple_ircv3_sasl_attempt(PurpleIRCv3Connection *connection) {
-	PurpleIRCv3SASLData *data = NULL;
-	const char *next_mechanism = NULL;
-	const char *current = NULL;
-
-	data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
-
-	current = hasl_context_get_current_mechanism(data->ctx);
-	if(current != NULL) {
-		g_message("SASL '%s' mechanism failed", current);
-	}
-
-	next_mechanism = hasl_context_next(data->ctx);
-	if(next_mechanism == NULL) {
-		GError *error = g_error_new(PURPLE_CONNECTION_ERROR,
-		                            PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE,
-		                            _("No valid SASL mechanisms found"));
-
-		purple_connection_take_error(PURPLE_CONNECTION(connection), error);
-
-		return;
-	}
-
-	g_message("trying SASL '%s' mechanism", next_mechanism);
-
-	purple_ircv3_connection_writef(connection, "%s %s",
-	                               PURPLE_IRCV3_MSG_AUTHENTICATE,
-	                               next_mechanism);
-}
-
-static void
-purple_ircv3_sasl_start(PurpleIRCv3Capabilities *caps) {
-	PurpleIRCv3Connection *connection = NULL;
-	PurpleAccount *account = NULL;
-	PurpleConnection *purple_connection = NULL;
-	HaslContext *ctx = NULL;
-	const char *mechanisms = NULL;
-	gboolean toggle = FALSE;
-
-	connection = purple_ircv3_capabilities_get_connection(caps);
-	purple_connection = PURPLE_CONNECTION(connection);
-	account = purple_connection_get_account(purple_connection);
-
-	ctx = hasl_context_new();
-
-	/* At this point we are ready to start our SASL negotiation, so add a wait
-	 * counter to the capabilities and start the negotiations!
-	 */
-	purple_ircv3_capabilities_add_wait(caps);
-
-	/* Determine what mechanisms we're allowing and tell the context. */
-	mechanisms = purple_account_get_string(account, "sasl-mechanisms", "");
-	if(purple_strempty(mechanisms)) {
-		/* If the user didn't specify any mechanisms, grab the mechanisms that
-		 * the server advertised.
-		 */
-		mechanisms = purple_ircv3_capabilities_lookup(caps, "sasl", NULL);
-	}
-	hasl_context_set_allowed_mechanisms(ctx, mechanisms);
-
-	/* Add the values we know to the context. */
-	hasl_context_set_username(ctx, purple_ircv3_sasl_get_username(purple_connection));
-	hasl_context_set_password(ctx, purple_connection_get_password(purple_connection));
-
-	toggle = purple_account_get_bool(account, "use-tls", TRUE);
-	hasl_context_set_tls(ctx, toggle);
-
-	toggle = purple_account_get_bool(account, "plain-sasl-in-clear", FALSE);
-	hasl_context_set_allow_clear_text(ctx, toggle);
-
-	/* Create our SASLData object, add it to the connection. */
-	purple_ircv3_sasl_data_add(purple_connection, ctx);
-
-	/* Make it go! */
-	purple_ircv3_sasl_attempt(connection);
-}
-
-/******************************************************************************
- * Callbacks
- *****************************************************************************/
-static void
-purple_ircv3_sasl_ack_cb(PurpleIRCv3Capabilities *caps,
-                         G_GNUC_UNUSED const char *capability,
-                         G_GNUC_UNUSED gpointer data)
-{
-	purple_ircv3_sasl_start(caps);
-}
-
-/******************************************************************************
- * Internal API
- *****************************************************************************/
-void
-purple_ircv3_sasl_request(PurpleIRCv3Capabilities *capabilities) {
-	purple_ircv3_capabilities_request(capabilities,
-	                                  PURPLE_IRCV3_CAPABILITY_SASL);
-
-	g_signal_connect(capabilities, "ack::" PURPLE_IRCV3_CAPABILITY_SASL,
-	                 G_CALLBACK(purple_ircv3_sasl_ack_cb), NULL);
-}
-
-gboolean
-purple_ircv3_sasl_logged_in(G_GNUC_UNUSED PurpleIRCv3Message *message,
-                            G_GNUC_UNUSED GError **error,
-                            gpointer user_data)
-{
-	PurpleIRCv3Connection *connection = user_data;
-	PurpleIRCv3SASLData *data = NULL;
-	PurpleAccount *account = NULL;
-	const char *sasl_name = NULL;
-
-	data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
-	if(data == NULL) {
-		g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
-		                    "RPL_LOGGEDIN received with no SASL data "
-		                    "present");
-
-		return FALSE;
-	}
-
-	/* Check if the SASL login name is not set. If it is not set, set it to the
-	 * current nick as it was successful.
-	 */
-	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
-	sasl_name = purple_account_get_string(account, "sasl-login-name", "");
-	if(purple_strempty(sasl_name)) {
-		char **userparts = NULL;
-		const char *username = NULL;
-
-		username = purple_contact_info_get_username(PURPLE_CONTACT_INFO(account));
-		userparts = g_strsplit(username, "@", 2);
-
-		purple_account_set_string(account, "sasl-login-name", userparts[0]);
-
-		g_strfreev(userparts);
-	}
-
-	return TRUE;
-}
-
-gboolean
-purple_ircv3_sasl_logged_out(G_GNUC_UNUSED PurpleIRCv3Message *message,
-                             G_GNUC_UNUSED GError **error,
-                             gpointer user_data)
-{
-	PurpleIRCv3Connection *connection = user_data;
-	PurpleIRCv3SASLData *data = NULL;
-
-	data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
-	if(data == NULL) {
-		g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
-		                    "RPL_LOGGEDOUT received with no SASL data "
-		                    "present");
-
-		return FALSE;
-	}
-
-	/* Not sure how to trigger this or what we should do in this case to be
-	 * honest, so just note it for now.
-	 * -- GK 2023-01-12
-	 */
-	g_warning("Server sent SASL logged out");
-
-	return TRUE;
-}
-
-gboolean
-purple_ircv3_sasl_nick_locked(PurpleIRCv3Message *v3_message,
-                              GError **error,
-                              gpointer user_data)
-{
-	PurpleIRCv3Connection *connection = user_data;
-	PurpleIRCv3SASLData *data = NULL;
-	GStrv params = NULL;
-	char *message = NULL;
-
-	data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
-	if(data == NULL) {
-		g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
-		                    "ERR_NICKLOCKED received with no SASL data "
-		                    "present");
-
-		return FALSE;
-	}
-
-	params = purple_ircv3_message_get_params(v3_message);
-	message = g_strjoinv(" ", params);
-
-	g_set_error(error, PURPLE_CONNECTION_ERROR,
-	            PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE,
-	            _("Nick name is locked: %s"), message);
-
-	g_free(message);
-
-	return FALSE;
-}
-
-
-gboolean
-purple_ircv3_sasl_success(G_GNUC_UNUSED PurpleIRCv3Message *message,
-                          GError **error,
-                          gpointer user_data)
-{
-	PurpleIRCv3Capabilities *capabilities = NULL;
-	PurpleIRCv3Connection *connection = user_data;
-	PurpleIRCv3SASLData *data = NULL;
-
-	capabilities = purple_ircv3_connection_get_capabilities(connection);
-
-	data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
-	if(data == NULL) {
-		g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
-		                    "RPL_SASLSUCCESS received with no SASL data "
-		                    "present");
-
-		return FALSE;
-	}
-
-	/* This needs to be after we've checked the SASL data otherwise we might
-	 * end up removing a wait that we don't own.
-	 */
-	purple_ircv3_capabilities_remove_wait(capabilities);
-
-	g_message("successfully authenticated with SASL '%s' mechanism.",
-	          hasl_context_get_current_mechanism(data->ctx));
-
-	return TRUE;
-}
-
-gboolean
-purple_ircv3_sasl_failed(G_GNUC_UNUSED PurpleIRCv3Message *message,
-                         G_GNUC_UNUSED GError **error,
-                         gpointer user_data)
-{
-	PurpleIRCv3Connection *connection = user_data;
-	PurpleIRCv3SASLData *data = NULL;
-
-	data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
-	if(data == NULL) {
-		g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
-		                    "ERR_SASLFAIL received with no SASL data present");
-
-		return FALSE;
-	}
-
-	purple_ircv3_sasl_attempt(connection);
-
-	return TRUE;
-}
-
-gboolean
-purple_ircv3_sasl_message_too_long(G_GNUC_UNUSED PurpleIRCv3Message *message,
-                                   G_GNUC_UNUSED GError **error,
-                                   gpointer user_data)
-{
-	PurpleIRCv3Connection *connection = user_data;
-	PurpleIRCv3SASLData *data = NULL;
-
-	data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
-	if(data == NULL) {
-		g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
-		                    "ERR_SASLTOOLONG received with no SASL data "
-		                    "present");
-
-		return FALSE;
-	}
-
-	return TRUE;
-}
-
-gboolean
-purple_ircv3_sasl_aborted(G_GNUC_UNUSED PurpleIRCv3Message *message,
-                          G_GNUC_UNUSED GError **error,
-                          gpointer user_data)
-{
-	PurpleIRCv3Connection *connection = user_data;
-	PurpleIRCv3SASLData *data = NULL;
-
-	data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
-	if(data == NULL) {
-		g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
-		                    "ERR_SASLABORTED received with no SASL data "
-		                    "present");
-
-		return FALSE;
-	}
-
-	/* This is supposed to get sent, when the client sends `AUTHENTICATE *`,
-	 * but we don't do this, so I guess we'll note it for now...?
-	 * --GK 2023-01-12
-	 */
-	g_warning("The server claims we aborted SASL authentication.");
-
-	return TRUE;
-}
-
-gboolean
-purple_ircv3_sasl_already_authed(G_GNUC_UNUSED PurpleIRCv3Message *message,
-                                 G_GNUC_UNUSED GError **error,
-                                 gpointer user_data)
-{
-	PurpleIRCv3Connection *connection = user_data;
-	PurpleIRCv3SASLData *data = NULL;
-
-	data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
-	if(data == NULL) {
-		g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
-		                    "ERR_SASLALREADY received with no SASL data "
-		                    "present");
-
-		return FALSE;
-	}
-
-	/* Similar to aborted above, we don't allow this, so just note that it
-	 * happened.
-	 * -- GK 2023-01-12
-	 */
-	g_warning("Server claims we tried to SASL authenticate again.");
-
-	return TRUE;
-}
-
-gboolean
-purple_ircv3_sasl_mechanisms(PurpleIRCv3Message *message,
-                             G_GNUC_UNUSED GError **error,
-                             gpointer user_data)
-{
-	PurpleIRCv3Connection *connection = user_data;
-	PurpleIRCv3SASLData *data = NULL;
-	GStrv params = NULL;
-	guint n_params = 0;
-
-	data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
-	if(data == NULL) {
-		g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
-		                    "RPL_SASLMECHS received with no SASL data "
-		                    "present");
-
-		return FALSE;
-	}
-
-	params = purple_ircv3_message_get_params(message);
-	if(params != NULL) {
-		n_params = g_strv_length(params);
-	}
-
-	/* We need to find a server that sends this message. The specification says
-	 * it _may_ be sent when the client sends AUTHENTICATE with an unknown
-	 * mechanism, but ergo doesn't.
-	 *
-	 * We can't just blindly accept the new list either, as depending on how
-	 * the server implements it, we'll need to remove mechanisms we've already
-	 * tried in the event the server just dumps the entire list. As we're not
-	 * currently tracking which mechanisms we've tried, this will have to be
-	 * addressed as well.
-	 */
-	if(n_params > 0) {
-		char *message = g_strjoinv(" ", params);
-
-		g_message("Server sent the following SASL mechanisms: %s", message);
-
-		g_free(message);
-	} else {
-		g_message("Server sent an empty list of SASL mechanisms");
-	}
-
-	return TRUE;
-}
-
-gboolean
-purple_ircv3_sasl_authenticate(PurpleIRCv3Message *message,
-                               GError **error,
-                               gpointer user_data)
-{
-	PurpleIRCv3Connection *connection = user_data;
-	PurpleIRCv3SASLData *data = NULL;
-	GStrv params = NULL;
-	char *payload = NULL;
-	gboolean done = FALSE;
-	guint n_params = 0;
-
-	params = purple_ircv3_message_get_params(message);
-	if(params != NULL) {
-		n_params = g_strv_length(params);
-	}
-
-	if(n_params != 1) {
-		g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
-		            "ignoring AUTHENTICATE with %d parameters", n_params);
-
-		return FALSE;
-	}
-
-	payload = params[0];
-	data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
-	if(data == NULL) {
-		g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
-		                    "AUTHENTICATE received with no SASL data present");
-
-		return FALSE;
-	}
-
-	/* If the server sent us a payload, combine the chunks. */
-	if(payload[0] != '+') {
-		g_string_append(data->server_in_buffer, payload);
-
-		if(strlen(payload) < 400) {
-			done = TRUE;
-		}
-	} else {
-		/* The server sent a + which is an empty message or the final message
-		 * ended on a 400 byte barrier. */
-		done = TRUE;
-	}
-
-	if(done) {
-		HaslMechanismResult res = 0;
-		GError *local_error = NULL;
-		guint8 *server_in = NULL;
-		guint8 *client_out = NULL;
-		gsize server_in_length = 0;
-		size_t client_out_length = 0;
-
-		/* If we have a buffer, base64 decode it, and then truncate it. */
-		if(data->server_in_buffer->len > 0) {
-			server_in = g_base64_decode(data->server_in_buffer->str,
-			                            &server_in_length);
-			g_string_truncate(data->server_in_buffer, 0);
-		}
-
-		/* Try to move to the next step of the sasl client. */
-		res = hasl_context_step(data->ctx, server_in, server_in_length,
-		                        &client_out, &client_out_length, &local_error);
-
-		/* We should be done with server_in, so free it.*/
-		g_clear_pointer(&server_in, g_free);
-
-		if(res == HASL_MECHANISM_RESULT_ERROR) {
-			g_propagate_error(error, local_error);
-
-			return FALSE;
-		}
-
-		if(local_error != NULL) {
-			g_warning("hasl_context_step returned an error without an error "
-			          "status: %s", local_error->message);
-
-			g_clear_error(&local_error);
-		}
-
-		/* If we got an output for the client, write it out. */
-		if(client_out_length > 0) {
-			char *encoded = NULL;
-
-			encoded = g_base64_encode(client_out, client_out_length);
-			g_clear_pointer(&client_out, g_free);
-
-			purple_ircv3_connection_writef(connection, "%s %s",
-			                               PURPLE_IRCV3_MSG_AUTHENTICATE,
-			                               encoded);
-			g_free(encoded);
-		} else {
-			purple_ircv3_connection_writef(connection, "%s +",
-			                               PURPLE_IRCV3_MSG_AUTHENTICATE);
-		}
-	}
-
-	return TRUE;
-}
diff --git a/protocols/ircv3/purpleircv3source.h b/protocols/ircv3/purpleircv3source.h
deleted file mode 100644
--- a/protocols/ircv3/purpleircv3source.h
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here.  Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library 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 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
-    !defined(PURPLE_IRCV3_COMPILATION)
-# error "only <libpurple/protocols/ircv3.h> may be included directly"
-#endif
-
-#ifndef PURPLE_IRCV3_SOURCE_H
-#define PURPLE_IRCV3_SOURCE_H
-
-#include <glib.h>
-#include <glib-object.h>
-
-#include "purpleircv3version.h"
-
-G_BEGIN_DECLS
-
-/**
- * purple_ircv3_source_parse:
- * @source: The source to parse.
- * @nick: (out) (transfer full): A return address for the nick.
- * @user: (out) (transfer full): A return address for the user.
- * @host: (out) (transfer full): A return address for the host.
- *
- * Parses a `source` string like `pidgy!~u@53unc8n42i868.irc` into its nick,
- * user, and host parts per https://modern.ircdocs.horse/#source.
- *
- * If the user or host aren't present in @source, but a return address is
- * provided for them, that pointer will be set to %NULL.
- *
- * Since: 3.0
- */
-PURPLE_IRCV3_AVAILABLE_IN_ALL
-void purple_ircv3_source_parse(const char *source, char **nick, char **user, char **host);
-
-G_END_DECLS
-
-#endif /* PURPLE_IRCV3_SOURCE_H */
diff --git a/protocols/ircv3/purpleircv3source.c b/protocols/ircv3/purpleircv3source.c
deleted file mode 100644
--- a/protocols/ircv3/purpleircv3source.c
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Purple - Internet Messaging Library
- * Copyright (C) Pidgin Developers <devel@pidgin.im>
- *
- * Purple is the legal property of its developers, whose names are too numerous
- * to list here.  Please refer to the COPYRIGHT file distributed with this
- * source distribution.
- *
- * This library 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 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 General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this library; if not, see <https://www.gnu.org/licenses/>.
- */
-
-#include <purple.h>
-
-#include "purpleircv3source.h"
-
-/******************************************************************************
- * Public API
- *****************************************************************************/
-void
-purple_ircv3_source_parse(const char *source, char **nick, char **user,
-                          char **host)
-{
-	GRegex *regex = NULL;
-	GMatchInfo *info = NULL;
-	gboolean matched = FALSE;
-
-	g_return_if_fail(!purple_strempty(source));
-	g_return_if_fail(nick != NULL || user != NULL || host != NULL);
-
-	/* If we find any \r \n or spaces in our source, it's invalid so we just
-	 * bail immediately.
-	 */
-	matched = g_regex_match_simple("[\r\n ]", source, G_REGEX_DEFAULT,
-	                               G_REGEX_MATCH_DEFAULT);
-	if(matched) {
-		return;
-	}
-
-	regex = g_regex_new("^(?P<nick>[^ \r\n!]+)(?:!(?P<user>[^@]+)(?:@(?P<host>[^!@]+))?)?$",
-	                    G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT, NULL);
-
-	matched = g_regex_match(regex, source, G_REGEX_MATCH_DEFAULT, &info);
-	if(!matched) {
-		if(nick != NULL) {
-			*nick = g_strdup(source);
-		}
-
-		g_clear_pointer(&info, g_match_info_unref);
-		g_clear_pointer(&regex, g_regex_unref);
-
-		return;
-	}
-
-	if(nick != NULL) {
-		*nick = g_match_info_fetch_named(info, "nick");
-	}
-
-	if(user != NULL) {
-		char *tmp = g_match_info_fetch_named(info, "user");
-
-		if(!purple_strempty(tmp)) {
-			*user = tmp;
-		} else {
-			g_free(tmp);
-		}
-	}
-
-	if(host != NULL) {
-		char *tmp = g_match_info_fetch_named(info, "host");
-
-		if(!purple_strempty(tmp)) {
-			*host = tmp;
-		} else {
-			g_free(tmp);
-		}
-	}
-
-	g_clear_pointer(&info, g_match_info_unref);
-	g_clear_pointer(&regex, g_regex_unref);
-}
diff --git a/protocols/ircv3/tests/meson.build b/protocols/ircv3/tests/meson.build
deleted file mode 100644
--- a/protocols/ircv3/tests/meson.build
+++ /dev/null
@@ -1,15 +0,0 @@
-TESTS = [
-	'formatting',
-	'parser',
-	'source',
-]
-
-foreach prog : TESTS
-	e = executable(
-		f'test_ircv3_@prog@', f'test_ircv3_@prog@.c',
-		dependencies : [birb_dep, libpurple_dep, glib, hasl],
-		objects : ircv3_prpl.extract_all_objects(),
-		c_args : ['-DPURPLE_IRCV3_COMPILATION'])
-
-	test(f'ircv3_@prog@', e)
-endforeach
diff --git a/protocols/ircv3/tests/test_ircv3_formatting.c b/protocols/ircv3/tests/test_ircv3_formatting.c
deleted file mode 100644
--- a/protocols/ircv3/tests/test_ircv3_formatting.c
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * 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 "../purpleircv3formatting.h"
-
-/******************************************************************************
- * Strip Tests
- *****************************************************************************/
-static void
-test_ircv3_formatting_strip(const char *input, const char *expected) {
-	char *actual = NULL;
-
-	actual = purple_ircv3_formatting_strip(input);
-	g_assert_cmpstr(actual, ==, expected);
-	g_clear_pointer(&actual, g_free);
-}
-
-static void
-test_ircv3_formatting_strip_null(void) {
-	test_ircv3_formatting_strip(NULL, NULL);
-}
-
-static void
-test_ircv3_formatting_strip_empty(void) {
-	test_ircv3_formatting_strip("", "");
-}
-
-static void
-test_ircv3_formatting_strip_color_comma(void) {
-	test_ircv3_formatting_strip("\003,", ",");
-}
-
-static void
-test_ircv3_formatting_strip_color_foreground_comma(void) {
-	test_ircv3_formatting_strip("\0033,", ",");
-}
-
-static void
-test_ircv3_formatting_strip_color_full(void) {
-	test_ircv3_formatting_strip("\0033,9wee", "wee");
-}
-
-static void
-test_ircv3_formatting_strip_color_foreground_3_digit(void) {
-	test_ircv3_formatting_strip("\003314", "4");
-}
-
-static void
-test_ircv3_formatting_strip_color_background_3_digit(void) {
-	test_ircv3_formatting_strip("\0031,234", "4");
-}
-
-static void
-test_ircv3_formatting_strip_hex_color(void) {
-	test_ircv3_formatting_strip("\004FF00FFwoo!", "woo!");
-}
-
-static void
-test_ircv3_formatting_strip_hex_color_full(void) {
-	test_ircv3_formatting_strip("\004FF00FF,00FF00woo!", "woo!");
-}
-
-static void
-test_ircv3_formatting_strip_hex_color_comma(void) {
-	test_ircv3_formatting_strip("\004,", ",");
-}
-
-static void
-test_ircv3_formatting_strip_hex_color_foreground_comma(void) {
-	test_ircv3_formatting_strip("\004FEFEFE,", ",");
-}
-
-static void
-test_ircv3_formatting_strip_hex_color_foreground_7_characters(void) {
-	test_ircv3_formatting_strip("\004FEFEFEF", "F");
-}
-
-static void
-test_ircv3_formatting_strip_hex_color_background_7_characters(void) {
-	test_ircv3_formatting_strip("\004FEFEFE,2222223", "3");
-}
-
-static void
-test_ircv3_formatting_strip_bold(void) {
-	test_ircv3_formatting_strip("this is \002bold\002!", "this is bold!");
-}
-
-static void
-test_ircv3_formatting_strip_italic(void) {
-	test_ircv3_formatting_strip("what do you \035mean\035?!",
-	                            "what do you mean?!");
-}
-
-static void
-test_ircv3_formatting_strip_monospace(void) {
-	test_ircv3_formatting_strip("\021i++;\021", "i++;");
-}
-
-static void
-test_ircv3_formatting_strip_reset(void) {
-	test_ircv3_formatting_strip("end of formatting\017", "end of formatting");
-}
-
-static void
-test_ircv3_formatting_strip_reverse(void) {
-	test_ircv3_formatting_strip("re\026ver\026se", "reverse");
-}
-
-static void
-test_ircv3_formatting_strip_strikethrough(void) {
-	test_ircv3_formatting_strip("\036I could be wrong\036",
-	                            "I could be wrong");
-}
-
-static void
-test_ircv3_formatting_strip_underline(void) {
-	test_ircv3_formatting_strip("You can't handle the \037truth\037!",
-	                            "You can't handle the truth!");
-}
-
-static void
-test_ircv3_formatting_strip_spec_example1(void) {
-	test_ircv3_formatting_strip(
-		"I love \0033IRC! \003It is the \0037best protocol ever!",
-		"I love IRC! It is the best protocol ever!");
-}
-
-static void
-test_ircv3_formatting_strip_spec_example2(void) {
-	test_ircv3_formatting_strip(
-		"This is a \035\00313,9cool \003message",
-		"This is a cool message");
-}
-
-static void
-test_ircv3_formatting_strip_spec_example3(void) {
-	test_ircv3_formatting_strip(
-		"IRC \002is \0034,12so \003great\017!",
-		"IRC is so great!");
-}
-
-static void
-test_ircv3_formatting_strip_spec_example4(void) {
-	test_ircv3_formatting_strip(
-		"Rules: Don't spam 5\00313,8,6\003,7,8, and especially not \0029\002\035!",
-		"Rules: Don't spam 5,6,7,8, and especially not 9!");
-}
-
-/******************************************************************************
- * Main
- *****************************************************************************/
-int
-main(int argc, char *argv[]) {
-	g_test_init(&argc, &argv, NULL);
-
-	g_test_add_func("/ircv3/formatting/strip/null",
-	                test_ircv3_formatting_strip_null);
-	g_test_add_func("/ircv3/formatting/strip/empty",
-	                test_ircv3_formatting_strip_empty);
-
-	g_test_add_func("/ircv3/formatting/strip/color-comma",
-	                test_ircv3_formatting_strip_color_comma);
-	g_test_add_func("/ircv3/formatting/strip/color-full",
-	                test_ircv3_formatting_strip_color_full);
-	g_test_add_func("/ircv3/formatting/strip/color-foreground-comma",
-	                test_ircv3_formatting_strip_color_foreground_comma);
-	g_test_add_func("/ircv3/formatting/strip/color-foreground-3-digit",
-	                test_ircv3_formatting_strip_color_foreground_3_digit);
-	g_test_add_func("/ircv3/formatting/strip/color-background-3-digit",
-	                test_ircv3_formatting_strip_color_background_3_digit);
-
-	g_test_add_func("/ircv3/formatting/strip/hex-color",
-	                test_ircv3_formatting_strip_hex_color);
-	g_test_add_func("/ircv3/formatting/strip/hex-color-full",
-	                test_ircv3_formatting_strip_hex_color_full);
-	g_test_add_func("/ircv3/formatting/strip/hex-color-comma",
-	                test_ircv3_formatting_strip_hex_color_comma);
-	g_test_add_func("/ircv3/formatting/strip/hex-color-foreground-comma",
-	                test_ircv3_formatting_strip_hex_color_foreground_comma);
-	g_test_add_func("/ircv3/formatting/strip/hex-color-foreground-7-characters",
-	                test_ircv3_formatting_strip_hex_color_foreground_7_characters);
-	g_test_add_func("/ircv3/formatting/strip/hex-color-background-7-characters",
-	                test_ircv3_formatting_strip_hex_color_background_7_characters);
-
-	g_test_add_func("/ircv3/formatting/strip/bold",
-	                test_ircv3_formatting_strip_bold);
-	g_test_add_func("/ircv3/formatting/strip/italic",
-	                test_ircv3_formatting_strip_italic);
-	g_test_add_func("/ircv3/formatting/strip/monospace",
-	                test_ircv3_formatting_strip_monospace);
-	g_test_add_func("/ircv3/formatting/strip/reset",
-	                test_ircv3_formatting_strip_reset);
-	g_test_add_func("/ircv3/formatting/strip/reverse",
-	                test_ircv3_formatting_strip_reverse);
-	g_test_add_func("/ircv3/formatting/strip/strikethrough",
-	                test_ircv3_formatting_strip_strikethrough);
-	g_test_add_func("/ircv3/formatting/strip/underline",
-	                test_ircv3_formatting_strip_underline);
-
-	/* These tests are based on the examples here
-	 * https://modern.ircdocs.horse/formatting.html#examples
-	 */
-	g_test_add_func("/ircv3/formatting/strip/spec-example1",
-	                test_ircv3_formatting_strip_spec_example1);
-	g_test_add_func("/ircv3/formatting/strip/spec-example2",
-	                test_ircv3_formatting_strip_spec_example2);
-	g_test_add_func("/ircv3/formatting/strip/spec-example3",
-	                test_ircv3_formatting_strip_spec_example3);
-	g_test_add_func("/ircv3/formatting/strip/spec-example4",
-	                test_ircv3_formatting_strip_spec_example4);
-
-	return g_test_run();
-}
diff --git a/protocols/ircv3/tests/test_ircv3_parser.c b/protocols/ircv3/tests/test_ircv3_parser.c
deleted file mode 100644
--- a/protocols/ircv3/tests/test_ircv3_parser.c
+++ /dev/null
@@ -1,822 +0,0 @@
-/*
- * 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 "../purpleircv3constants.h"
-#include "../purpleircv3message.h"
-#include "../purpleircv3parser.h"
-
-#define TEST_IRCV3_PARSER_DOMAIN (g_quark_from_static_string("test-ircv3-parser"))
-
-typedef struct {
-	GHashTable *tags;
-	gchar *source;
-	gchar *command;
-	guint n_params;
-	const gchar * const params[16];
-} TestPurpleIRCv3ParserData;
-
-/******************************************************************************
- * Handlers
- *****************************************************************************/
-static gboolean
-test_purple_ircv3_test_handler(PurpleIRCv3Message *message,
-                               G_GNUC_UNUSED GError **error,
-                               gpointer data)
-{
-	TestPurpleIRCv3ParserData *d = data;
-	GHashTable *tags = NULL;
-	GHashTableIter iter;
-	GStrv params = NULL;
-	const char *command = NULL;
-	const char *source = NULL;
-	guint n_params = 0;
-
-	command = purple_ircv3_message_get_command(message);
-	params = purple_ircv3_message_get_params(message);
-	source = purple_ircv3_message_get_source(message);
-	tags = purple_ircv3_message_get_tags(message);
-
-	if(params != NULL) {
-		n_params = g_strv_length(params);
-	}
-
-	/* Make sure we have an expected tags hash table before checking them. */
-	if(d->tags != NULL) {
-		gpointer expected_key;
-		gpointer expected_value;
-		guint actual_size;
-		guint expected_size;
-
-		/* Make sure the tag hash tables have the same size. */
-		expected_size = g_hash_table_size(d->tags);
-		actual_size = g_hash_table_size(tags);
-		g_assert_cmpuint(actual_size, ==, expected_size);
-
-		/* Since the tables have the same size, we can walk through the expected
-		 * table and use it to verify the actual table.
-		 */
-		g_hash_table_iter_init(&iter, d->tags);
-		while(g_hash_table_iter_next(&iter, &expected_key, &expected_value)) {
-			gpointer actual_value = NULL;
-			gboolean found = FALSE;
-
-			found = g_hash_table_lookup_extended(tags, expected_key, NULL,
-			                                     &actual_value);
-			g_assert_true(found);
-			g_assert_cmpstr(actual_value, ==, expected_value);
-		}
-	}
-
-	/* Walk through the params checking against the expected values. */
-	if(d->n_params > 0) {
-		g_assert_cmpuint(n_params, ==, d->n_params);
-
-		for(guint i = 0; i < d->n_params; i++) {
-			g_assert_cmpstr(params[i], ==, d->params[i]);
-		}
-	}
-
-	/* Validate all the string parameters. */
-	g_assert_cmpstr(source, ==, d->source);
-	g_assert_cmpstr(command, ==, d->command);
-
-	/* Cleanup everything the caller allocated. */
-	g_clear_pointer(&d->tags, g_hash_table_destroy);
-
-	/* Return the return value the caller asked for. */
-	return TRUE;
-}
-
-static gboolean
-test_purple_ircv3_test_handler_error(G_GNUC_UNUSED PurpleIRCv3Message *message,
-                                     GError **error,
-                                     G_GNUC_UNUSED gpointer data)
-{
-	g_set_error(error, TEST_IRCV3_PARSER_DOMAIN, 0, "test error");
-
-	return FALSE;
-}
-
-/******************************************************************************
- * Helpers
- *****************************************************************************/
-static void
-test_purple_ircv3_parser(const gchar *source, TestPurpleIRCv3ParserData *d) {
-	PurpleIRCv3Parser *parser = purple_ircv3_parser_new();
-	GError *error = NULL;
-	gboolean result = FALSE;
-
-	purple_ircv3_parser_set_fallback_handler(parser,
-	                                         test_purple_ircv3_test_handler);
-
-	result = purple_ircv3_parser_parse(parser, source, &error, d);
-
-	g_assert_no_error(error);
-	g_assert_true(result);
-
-	g_clear_object(&parser);
-}
-
-/******************************************************************************
- * Tests
- *****************************************************************************/
-static void
-test_purple_ircv3_parser_propagates_errors(void) {
-	PurpleIRCv3Parser *parser = purple_ircv3_parser_new();
-	GError *error = NULL;
-	gboolean result = FALSE;
-
-	purple_ircv3_parser_set_fallback_handler(parser,
-	                                         test_purple_ircv3_test_handler_error);
-
-	result = purple_ircv3_parser_parse(parser, "COMMAND", &error, NULL);
-	g_assert_error(error, TEST_IRCV3_PARSER_DOMAIN, 0);
-	g_clear_error(&error);
-
-	g_assert_false(result);
-
-	g_clear_object(&parser);
-}
-
-static void
-test_purple_ircv3_parser_simple(void) {
-	TestPurpleIRCv3ParserData data = {
-		.command = "foo",
-		.n_params = 3,
-		.params = {"bar", "baz", "asdf"},
-	};
-
-	test_purple_ircv3_parser("foo bar baz asdf", &data);
-}
-
-static void
-test_purple_ircv3_parser_with_source(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "coolguy",
-		.command = "foo",
-		.n_params = 3,
-		.params = {"bar", "baz", "asdf"},
-	};
-
-	test_purple_ircv3_parser(":coolguy foo bar baz asdf", &data);
-}
-
-static void
-test_purple_ircv3_parser_with_trailing(void) {
-	TestPurpleIRCv3ParserData data = {
-		.command = "foo",
-		.n_params = 3,
-		.params = {"bar", "baz", "asdf quux"},
-	};
-
-	test_purple_ircv3_parser("foo bar baz :asdf quux", &data);
-}
-
-static void
-test_purple_ircv3_parser_with_empty_trailing(void) {
-	TestPurpleIRCv3ParserData data = {
-		.command = "foo",
-		.n_params = 3,
-		.params = {"bar", "baz", ""},
-	};
-
-	test_purple_ircv3_parser("foo bar baz :", &data);
-}
-
-static void
-test_purple_ircv3_parser_with_trailing_starting_colon(void) {
-	TestPurpleIRCv3ParserData data = {
-		.command = "foo",
-		.n_params = 3,
-		.params = {"bar", "baz", ":asdf"},
-	};
-
-	test_purple_ircv3_parser("foo bar baz ::asdf", &data);
-}
-
-static void
-test_purple_ircv3_parser_with_source_and_trailing(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "coolguy",
-		.command = "foo",
-		.n_params = 3,
-		.params = {"bar", "baz", "asdf quux"},
-	};
-
-	test_purple_ircv3_parser(":coolguy foo bar baz :asdf quux", &data);
-}
-
-static void
-test_purple_ircv3_parser_with_source_and_trailing_whitespace(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "coolguy",
-		.command = "foo",
-		.n_params = 3,
-		.params = {"bar", "baz", "  asdf quux "},
-	};
-
-	test_purple_ircv3_parser(":coolguy foo bar baz :  asdf quux ", &data);
-}
-
-static void
-test_purple_ircv3_parser_with_source_and_trailing_colon(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "coolguy",
-		.command = PURPLE_IRCV3_MSG_PRIVMSG,
-		.n_params = 2,
-		.params = {"bar", "lol :) "},
-	};
-
-	test_purple_ircv3_parser(":coolguy PRIVMSG bar :lol :) ", &data);
-}
-
-static void
-test_purple_ircv3_parser_with_source_and_empty_trailing(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "coolguy",
-		.command = "foo",
-		.n_params = 3,
-		.params = {"bar", "baz", ""},
-	};
-
-	test_purple_ircv3_parser(":coolguy foo bar baz :", &data);
-}
-
-static void
-test_purple_ircv3_parser_with_source_and_trailing_only_whitespace(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "coolguy",
-		.command = "foo",
-		.n_params = 3,
-		.params = {"bar", "baz", "  "},
-	};
-
-	test_purple_ircv3_parser(":coolguy foo bar baz :  ", &data);
-}
-
-static void
-test_purple_ircv3_parser_with_tags(void) {
-	TestPurpleIRCv3ParserData data = {
-		.command = "foo",
-	};
-
-	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
-	g_hash_table_insert(data.tags, "a", "b");
-	g_hash_table_insert(data.tags, "c", "32");
-	g_hash_table_insert(data.tags, "k", "");
-	g_hash_table_insert(data.tags, "rt", "ql7");
-
-	test_purple_ircv3_parser("@a=b;c=32;k;rt=ql7 foo", &data);
-}
-
-static void
-test_purple_ircv3_parser_with_escaped_tags(void) {
-	TestPurpleIRCv3ParserData data = {
-		.command = "foo",
-	};
-
-	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
-	g_hash_table_insert(data.tags, "a", "b\\and\nk");
-	g_hash_table_insert(data.tags, "c", "72 45");
-	g_hash_table_insert(data.tags, "d", "gh;764");
-
-	test_purple_ircv3_parser("@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo",
-	                         &data);
-}
-
-static void
-test_purple_ircv3_with_tags_and_source(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "quux",
-		.command = "ab",
-		.n_params = 1,
-		.params = {"cd"},
-	};
-
-	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
-	g_hash_table_insert(data.tags, "c", "");
-	g_hash_table_insert(data.tags, "h", "");
-	g_hash_table_insert(data.tags, "a", "b");
-
-	test_purple_ircv3_parser("@c;h=;a=b :quux ab cd", &data);
-}
-
-static void
-test_purple_ircv3_last_param_no_colon(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "src",
-		.command = PURPLE_IRCV3_MSG_JOIN,
-		.n_params = 1,
-		.params = {"#chan"},
-	};
-
-	test_purple_ircv3_parser(":src JOIN #chan", &data);
-}
-
-static void
-test_purple_ircv3_last_param_with_colon(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "src",
-		.command = PURPLE_IRCV3_MSG_JOIN,
-		.n_params = 1,
-		.params = {"#chan"},
-	};
-
-	test_purple_ircv3_parser(":src JOIN :#chan", &data);
-}
-
-static void
-test_purple_ircv3_without_last_param(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "src",
-		.command = "AWAY",
-	};
-
-	test_purple_ircv3_parser(":src AWAY", &data);
-}
-
-static void
-test_purple_ircv3_with_last_param(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "src",
-		.command = "AWAY",
-	};
-
-	test_purple_ircv3_parser(":src AWAY ", &data);
-}
-
-static void
-test_purple_ircv3_tab_is_not_space(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "cool\tguy",
-		.command = "foo",
-		.n_params = 2,
-		.params = {"bar", "baz"},
-	};
-
-	test_purple_ircv3_parser(":cool\tguy foo bar baz", &data);
-}
-
-static void
-test_purple_ircv3_source_control_characters_1(void) {
-	/* Break each string after the hex escape as they are supposed to only be
-	 * a single byte, but the c compiler will keep unescaping unless we break
-	 * the string.
-	 */
-	TestPurpleIRCv3ParserData data = {
-		.source = "coolguy!ag@net\x03" "5w\x03" "ork.admin",
-		.command = PURPLE_IRCV3_MSG_PRIVMSG,
-		.n_params = 2,
-		.params = {"foo", "bar baz"},
-	};
-	const gchar *msg = NULL;
-
-	msg = ":coolguy!ag@net\x03" "5w\x03" "ork.admin PRIVMSG foo :bar baz";
-
-	test_purple_ircv3_parser(msg, &data);
-}
-
-static void
-test_purple_ircv3_source_control_characters_2(void) {
-	/* Break each string after the hex escape as they are supposed to only be
-	 * a single byte, but the c compiler will keep unescaping unless we break
-	 * the string.
-	 */
-	TestPurpleIRCv3ParserData data = {
-		.source = "coolguy!~ag@n\x02" "et\x03" "05w\x0f" "ork.admin",
-		.command = PURPLE_IRCV3_MSG_PRIVMSG,
-		.n_params = 2,
-		.params = {"foo", "bar baz"},
-	};
-	const gchar *msg = NULL;
-
-	msg = ":coolguy!~ag@n\x02" "et\x03" "05w\x0f" "ork.admin PRIVMSG foo :bar "
-	      "baz";
-
-	test_purple_ircv3_parser(msg, &data);
-}
-
-static void
-test_purple_ircv3_everything(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "irc.example.com",
-		.command = "COMMAND",
-		.n_params = 3,
-		.params = {"param1", "param2", "param3 param3"},
-	};
-	const gchar *msg = NULL;
-
-	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
-	g_hash_table_insert(data.tags, "tag1", "value1");
-	g_hash_table_insert(data.tags, "tag2", "");
-	g_hash_table_insert(data.tags, "vendor1/tag3", "value2");
-	g_hash_table_insert(data.tags, "vendor2/tag4", "");
-
-	msg = "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= "
-	      ":irc.example.com COMMAND param1 param2 :param3 param3";
-
-	test_purple_ircv3_parser(msg, &data);
-}
-
-static void
-test_purple_ircv3_everything_but_tags(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "irc.example.com",
-		.command = "COMMAND",
-		.n_params = 3,
-		.params = {"param1", "param2", "param3 param3"},
-	};
-	const gchar *msg = NULL;
-
-	msg = ":irc.example.com COMMAND param1 param2 :param3 param3";
-
-	test_purple_ircv3_parser(msg, &data);
-}
-
-static void
-test_purple_ircv3_everything_but_source(void) {
-	TestPurpleIRCv3ParserData data = {
-		.command = "COMMAND",
-		.n_params = 3,
-		.params = {"param1", "param2", "param3 param3"},
-	};
-	const gchar *msg = NULL;
-
-	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
-	g_hash_table_insert(data.tags, "tag1", "value1");
-	g_hash_table_insert(data.tags, "tag2", "");
-	g_hash_table_insert(data.tags, "vendor1/tag3", "value2");
-	g_hash_table_insert(data.tags, "vendor2/tag4", "");
-
-	msg = "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 "
-	      "COMMAND param1 param2 :param3 param3";
-
-	test_purple_ircv3_parser(msg, &data);
-}
-
-static void
-test_purple_ircv3_command_only(void) {
-	TestPurpleIRCv3ParserData data = {
-		.command = "COMMAND",
-	};
-
-	test_purple_ircv3_parser("COMMAND", &data);
-}
-
-static void
-test_purple_ircv3_slashes_are_fun(void) {
-	TestPurpleIRCv3ParserData data = {
-		.command = "COMMAND",
-	};
-
-	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
-	g_hash_table_insert(data.tags, "foo", "\\\\;\\s \r\n");
-
-	test_purple_ircv3_parser("@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND", &data);
-}
-
-static void
-test_purple_ircv3_unreal_broken_1(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "gravel.mozilla.org",
-		.command = "432",
-		.n_params = 2,
-		.params = {"#momo", "Erroneous Nickname: Illegal characters"},
-	};
-	const gchar *msg = NULL;
-
-	msg = ":gravel.mozilla.org 432  #momo :Erroneous Nickname: Illegal "
-	      "characters";
-
-	test_purple_ircv3_parser(msg, &data);
-}
-
-static void
-test_purple_ircv3_unreal_broken_2(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "gravel.mozilla.org",
-		.command = "MODE",
-		.n_params = 2,
-		.params = {"#tckk", "+n"},
-	};
-
-	test_purple_ircv3_parser(":gravel.mozilla.org MODE #tckk +n ", &data);
-}
-
-static void
-test_purple_ircv3_unreal_broken_3(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "services.esper.net",
-		.command = "MODE",
-		.n_params = 3,
-		.params = {"#foo-bar", "+o", "foobar"},
-	};
-
-	test_purple_ircv3_parser(":services.esper.net MODE #foo-bar +o foobar  ",
-	                         &data);
-}
-
-static void
-test_purple_ircv3_tag_escape_char_at_a_time(void) {
-	TestPurpleIRCv3ParserData data = {
-		.command = "COMMAND",
-	};
-
-	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
-	g_hash_table_insert(data.tags, "tag1", "value\\ntest");
-
-	test_purple_ircv3_parser("@tag1=value\\\\ntest COMMAND", &data);
-}
-
-static void
-test_purple_ircv3_tag_drop_unnecessary_escapes(void) {
-	TestPurpleIRCv3ParserData data = {
-		.command = "COMMAND",
-	};
-
-	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
-	g_hash_table_insert(data.tags, "tag1", "value1");
-
-	test_purple_ircv3_parser("@tag1=value\\1 COMMAND", &data);
-}
-
-static void
-test_purple_ircv3_tag_drop_trailing_slash(void) {
-	TestPurpleIRCv3ParserData data = {
-		.command = "COMMAND",
-	};
-
-	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
-	g_hash_table_insert(data.tags, "tag1", "value1");
-
-	test_purple_ircv3_parser("@tag1=value1\\ COMMAND", &data);
-}
-
-static void
-test_purple_ircv3_duplicate_tags(void) {
-	TestPurpleIRCv3ParserData data = {
-		.command = "COMMAND",
-	};
-
-	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
-	g_hash_table_insert(data.tags, "tag1", "5");
-	g_hash_table_insert(data.tags, "tag2", "3");
-	g_hash_table_insert(data.tags, "tag3", "4");
-
-	test_purple_ircv3_parser("@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND", &data);
-}
-
-static void
-test_purple_ircv3_vendor_tags_are_namespaced(void) {
-	TestPurpleIRCv3ParserData data = {
-		.command = "COMMAND",
-	};
-	const gchar *msg = NULL;
-
-	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
-	g_hash_table_insert(data.tags, "tag1", "5");
-	g_hash_table_insert(data.tags, "tag2", "3");
-	g_hash_table_insert(data.tags, "tag3", "4");
-	g_hash_table_insert(data.tags, "vendor/tag2", "8");
-
-	msg = "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND";
-
-	test_purple_ircv3_parser(msg, &data);
-}
-
-static void
-test_purple_ircv3_special_mode_1(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "SomeOp",
-		.command = "MODE",
-		.n_params = 2,
-		.params = {"#channel", "+i"},
-	};
-
-	test_purple_ircv3_parser(":SomeOp MODE #channel :+i", &data);
-}
-
-static void
-test_purple_ircv3_special_mode_2(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "SomeOp",
-		.command = "MODE",
-		.n_params = 4,
-		.params = {"#channel", "+oo", "SomeUser", "AnotherUser"},
-	};
-
-	test_purple_ircv3_parser(":SomeOp MODE #channel +oo SomeUser :AnotherUser",
-	                         &data);
-}
-
-/******************************************************************************
- * Message tags examples
- *****************************************************************************/
-static void
-test_purple_ircv3_parser_message_tags_none(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "nick!ident@host.com",
-		.command = PURPLE_IRCV3_MSG_PRIVMSG,
-		.n_params = 2,
-		.params = {"me", "Hello"},
-	};
-	const char *msg = NULL;
-
-	msg = ":nick!ident@host.com PRIVMSG me :Hello";
-
-	test_purple_ircv3_parser(msg, &data);
-}
-
-static void
-test_purple_ircv3_parser_message_tags_3_tags(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "nick!ident@host.com",
-		.command = PURPLE_IRCV3_MSG_PRIVMSG,
-		.n_params = 2,
-		.params = {"me", "Hello"},
-	};
-	const char *msg = NULL;
-
-	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
-	g_hash_table_insert(data.tags, "aaa", "bbb");
-	g_hash_table_insert(data.tags, "ccc", "");
-	g_hash_table_insert(data.tags, "example.com/ddd", "eee");
-
-	msg = "@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me "
-	      ":Hello";
-
-	test_purple_ircv3_parser(msg, &data);
-}
-
-static void
-test_purple_ircv3_parser_message_tags_client_only(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "url_bot!bot@example.com",
-		.command = PURPLE_IRCV3_MSG_PRIVMSG,
-		.n_params = 2,
-		.params = {"#channel", "Example.com: A News Story"},
-	};
-	const char *msg = NULL;
-
-	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
-	g_hash_table_insert(data.tags, "+icon", "https://example.com/favicon.png");
-
-	msg = "@+icon=https://example.com/favicon.png :url_bot!bot@example.com "
-	      "PRIVMSG #channel :Example.com: A News Story";
-
-	test_purple_ircv3_parser(msg, &data);
-}
-
-static void
-test_purple_ircv3_parser_message_tags_labeled_response(void) {
-	TestPurpleIRCv3ParserData data = {
-		.source = "nick!user@example.com",
-		.command = "TAGMSG",
-		.n_params = 1,
-		.params = {"#channel"},
-	};
-	const char *msg = NULL;
-
-	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
-	g_hash_table_insert(data.tags, "label", "123");
-	g_hash_table_insert(data.tags, "msgid", "abc");
-	g_hash_table_insert(data.tags, "+example-client-tag", "example-value");
-
-	msg = "@label=123;msgid=abc;+example-client-tag=example-value "
-	      ":nick!user@example.com TAGMSG #channel";
-
-	test_purple_ircv3_parser(msg, &data);
-
-}
-/******************************************************************************
- * Main
- *****************************************************************************/
-gint
-main(gint argc, gchar *argv[]) {
-	g_test_init(&argc, &argv, NULL);
-
-	/* Make sure an error in the handler is propagated back up to the calling
-	 * function.
-	 */
-	g_test_add_func("/ircv3/parser/propagates-errors",
-	                test_purple_ircv3_parser_propagates_errors);
-
-	/* These tests are based on the msg-split tests from
-	 * https://github.com/ircdocs/parser-tests/blob/master/tests/msg-split.yaml
-	 */
-	g_test_add_func("/ircv3/parser/simple",
-	                test_purple_ircv3_parser_simple);
-	g_test_add_func("/ircv3/parser/with-source",
-	                test_purple_ircv3_parser_with_source);
-	g_test_add_func("/ircv3/parser/with-trailing",
-	                test_purple_ircv3_parser_with_trailing);
-	g_test_add_func("/ircv3/parser/with-empty-trailing",
-	                test_purple_ircv3_parser_with_empty_trailing);
-	g_test_add_func("/ircv3/parser/with-trailing-starting-colon",
-	                test_purple_ircv3_parser_with_trailing_starting_colon);
-	g_test_add_func("/ircv3/parser/with-source-and-trailing",
-	                test_purple_ircv3_parser_with_source_and_trailing);
-	g_test_add_func("/ircv3/parser/with-source-and-trailing-whitespace",
-	                test_purple_ircv3_parser_with_source_and_trailing_whitespace);
-	g_test_add_func("/ircv3/parser/with-source-and-trailing-colon",
-	                test_purple_ircv3_parser_with_source_and_trailing_colon);
-	g_test_add_func("/ircv3/parser/with-source-and-empty-trailing",
-	                test_purple_ircv3_parser_with_source_and_empty_trailing);
-	g_test_add_func("/ircv3/parser/with-source-and-trailing-only-whitespace",
-	                test_purple_ircv3_parser_with_source_and_trailing_only_whitespace);
-
-	g_test_add_func("/ircv3/parser/with-tags",
-	                test_purple_ircv3_parser_with_tags);
-	g_test_add_func("/ircv3/parser/with-escaped-tags",
-	                test_purple_ircv3_parser_with_escaped_tags);
-	g_test_add_func("/ircv3/parser/with-tags-and-source",
-	                test_purple_ircv3_with_tags_and_source);
-
-	g_test_add_func("/ircv3/parser/last-param-no-colon",
-	                test_purple_ircv3_last_param_no_colon);
-	g_test_add_func("/ircv3/parser/last-param-with-colon",
-	                test_purple_ircv3_last_param_with_colon);
-
-	g_test_add_func("/ircv3/parser/without-last-param",
-	                test_purple_ircv3_without_last_param);
-	g_test_add_func("/ircv3/parser/with-last-parsm",
-	                test_purple_ircv3_with_last_param);
-
-	g_test_add_func("/ircv3/parser/tab-is-not-space",
-	                test_purple_ircv3_tab_is_not_space);
-
-	g_test_add_func("/ircv3/parser/source_control_characters_1",
-	                test_purple_ircv3_source_control_characters_1);
-	g_test_add_func("/ircv3/parser/source_control_characters_2",
-	                test_purple_ircv3_source_control_characters_2);
-
-	g_test_add_func("/ircv3/parser/everything",
-	                test_purple_ircv3_everything);
-	g_test_add_func("/ircv3/parser/everything-but-tags",
-	                test_purple_ircv3_everything_but_tags);
-	g_test_add_func("/ircv3/parser/everything-but-source",
-	                test_purple_ircv3_everything_but_source);
-
-	g_test_add_func("/ircv3/parser/command-only",
-	                test_purple_ircv3_command_only);
-
-	g_test_add_func("/ircv3/parser/slashes-are-fun",
-	                test_purple_ircv3_slashes_are_fun);
-
-	g_test_add_func("/ircv3/parser/unreal-broken-1",
-	                test_purple_ircv3_unreal_broken_1);
-	g_test_add_func("/ircv3/parser/unreal-broken-2",
-	                test_purple_ircv3_unreal_broken_2);
-	g_test_add_func("/ircv3/parser/unreal-broken-3",
-	                test_purple_ircv3_unreal_broken_3);
-
-	g_test_add_func("/ircv3/parser/tag-escape-char-at-a-time",
-	                test_purple_ircv3_tag_escape_char_at_a_time);
-	g_test_add_func("/ircv3/parser/tag-drop-unnecessary-escapes",
-	                test_purple_ircv3_tag_drop_unnecessary_escapes);
-	g_test_add_func("/ircv3/parser/tag-drop-trailing-slash",
-	                test_purple_ircv3_tag_drop_trailing_slash);
-
-	g_test_add_func("/ircv3/parser/duplicate-tags",
-	                test_purple_ircv3_duplicate_tags);
-	g_test_add_func("/ircv3/parser/vendor-tags-are-namespaced",
-	                test_purple_ircv3_vendor_tags_are_namespaced);
-
-	g_test_add_func("/ircv3/parser/special-mode-1",
-	                test_purple_ircv3_special_mode_1);
-	g_test_add_func("/ircv3/parser/special-mode-2",
-	                test_purple_ircv3_special_mode_2);
-
-	/* These tests are the examples from the message-tags specification,
-	 * https://ircv3.net/specs/extensions/message-tags.html.
-	 */
-	g_test_add_func("/ircv3/parser/message-tags/none",
-	                test_purple_ircv3_parser_message_tags_none);
-	g_test_add_func("/ircv3/parser/message-tags/3-tags",
-	                test_purple_ircv3_parser_message_tags_3_tags);
-	g_test_add_func("/ircv3/parser/message-tags/client-only",
-	                test_purple_ircv3_parser_message_tags_client_only);
-	g_test_add_func("/ircv3/parser/message-tags/labeled-message",
-	                test_purple_ircv3_parser_message_tags_labeled_response);
-
-	return g_test_run();
-}
diff --git a/protocols/ircv3/tests/test_ircv3_source.c b/protocols/ircv3/tests/test_ircv3_source.c
deleted file mode 100644
--- a/protocols/ircv3/tests/test_ircv3_source.c
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * 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 "../purpleircv3source.h"
-
-/******************************************************************************
- * Tests
- *****************************************************************************/
-static void
-test_ircv3_source_parse_return_address_required(void) {
-	if(g_test_subprocess()) {
-		purple_ircv3_source_parse("pidgy", NULL, NULL, NULL);
-	}
-
-	g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_DEFAULT);
-	g_test_trap_assert_stderr("*nick != NULL || user != NULL || host != NULL' failed*");
-}
-
-static void
-test_ircv3_source_parse_nick(void) {
-	char *nick = NULL;
-	char *user = NULL;
-	char *host = NULL;
-
-	/* Once to make sure that user and host are null. */
-	purple_ircv3_source_parse("pidgy", &nick, &user, &host);
-	g_assert_cmpstr(nick, ==, "pidgy");
-	g_clear_pointer(&nick, g_free);
-
-	g_assert_null(user);
-	g_assert_null(host);
-
-	/* Again to make sure it works without user and host. */
-	purple_ircv3_source_parse("pidgy", &nick, NULL, NULL);
-	g_assert_cmpstr(nick, ==, "pidgy");
-	g_clear_pointer(&nick, g_free);
-}
-
-static void
-test_ircv3_source_parse_user(void) {
-	char *nick = NULL;
-	char *user = NULL;
-	char *host = NULL;
-
-	/* Once to make sure host is null. */
-	purple_ircv3_source_parse("pidgy!~u", &nick, &user, &host);
-	g_assert_cmpstr(nick, ==, "pidgy");
-	g_clear_pointer(&nick, g_free);
-
-	g_assert_cmpstr(user, ==, "~u");
-	g_clear_pointer(&user, g_free);
-
-	g_assert_null(host);
-
-	/* Again to make sure nick and host aren't required. */
-	purple_ircv3_source_parse("pidgy!~u", NULL, &user, NULL);
-	g_assert_cmpstr(user, ==, "~u");
-	g_clear_pointer(&user, g_free);
-}
-
-static void
-test_ircv3_source_parse_host(void) {
-	char *nick = NULL;
-	char *user = NULL;
-	char *host = NULL;
-
-	/* Once to make sure everything works. */
-	purple_ircv3_source_parse("pidgy!~u@53unc8n42i868.irc", &nick, &user,
-	                          &host);
-	g_assert_cmpstr(nick, ==, "pidgy");
-	g_clear_pointer(&nick, g_free);
-
-	g_assert_cmpstr(user, ==, "~u");
-	g_clear_pointer(&user, g_free);
-
-	g_assert_cmpstr(host, ==, "53unc8n42i868.irc");
-	g_clear_pointer(&host, g_free);
-
-	/* Again to make sure nick and host aren't required. */
-	purple_ircv3_source_parse("pidgy!~u@53unc8n42i868.irc", NULL, NULL, &host);
-	g_assert_cmpstr(host, ==, "53unc8n42i868.irc");
-	g_clear_pointer(&host, g_free);
-}
-
-static void
-test_ircv3_source_parse_baddies(void) {
-	char *nick = NULL;
-	char *user = NULL;
-	char *server = NULL;
-
-	purple_ircv3_source_parse("\n", &nick, NULL, NULL);
-	g_assert_null(nick);
-
-	purple_ircv3_source_parse("a\nb", &nick, NULL, NULL);
-	g_assert_null(nick);
-
-	purple_ircv3_source_parse("\r", &nick, NULL, NULL);
-	g_assert_null(nick);
-
-	purple_ircv3_source_parse("c\rd", &nick, NULL, NULL);
-	g_assert_null(nick);
-
-	purple_ircv3_source_parse(" ", &nick, NULL, NULL);
-	g_assert_null(nick);
-
-	purple_ircv3_source_parse("e f", &nick, NULL, NULL);
-	g_assert_null(nick);
-
-	purple_ircv3_source_parse("nick@foo!user@server", &nick, &user, &server);
-	g_assert_cmpstr(nick, ==, "nick@foo");
-	g_clear_pointer(&nick, g_free);
-	g_assert_cmpstr(user, ==, "user");
-	g_clear_pointer(&user, g_free);
-	g_assert_cmpstr(server, ==, "server");
-	g_clear_pointer(&server, g_free);
-
-	purple_ircv3_source_parse("nick!user@server!foo", &nick, &user, &server);
-	g_assert_cmpstr(nick, ==, "nick!user@server!foo");
-	g_assert_null(user);
-	g_assert_null(server);
-}
-
-/******************************************************************************
- * Main
- *****************************************************************************/
-int
-main(int argc, char *argv[]) {
-	g_test_init(&argc, &argv, NULL);
-
-	g_test_add_func("/ircv3/source/parse/return-address-required",
-	                test_ircv3_source_parse_return_address_required);
-	g_test_add_func("/ircv3/source/parse/nick", test_ircv3_source_parse_nick);
-	g_test_add_func("/ircv3/source/parse/user", test_ircv3_source_parse_user);
-	g_test_add_func("/ircv3/source/parse/host", test_ircv3_source_parse_host);
-	g_test_add_func("/ircv3/source/parse/baddies",
-	                test_ircv3_source_parse_baddies);
-
-	return g_test_run();
-}
\ No newline at end of file
diff --git a/subprojects/ibis.wrap b/subprojects/ibis.wrap
new file mode 100644
--- /dev/null
+++ b/subprojects/ibis.wrap
@@ -0,0 +1,4 @@
+[wrap-hg]
+directory = ibis
+url = https://keep.imfreedom.org/ibis/ibis
+revision = 472d8f2226bc
