# HG changeset patch
# Node ID 6a974b0d5dd8
# Parent  013f54f8f035
diff --git a/libpurple/protocols/ircv3/meson.build b/libpurple/protocols/ircv3/meson.build
--- a/libpurple/protocols/ircv3/meson.build
+++ b/libpurple/protocols/ircv3/meson.build
@@ -1,4 +1,5 @@
 IRCV3_SOURCES = [
+	'purpleircv3capabilities.c',
 	'purpleircv3connection.c',
 	'purpleircv3core.c',
 	'purpleircv3messagehandlers.c',
@@ -8,6 +9,7 @@
 ]
 
 IRCV3_HEADERS = [
+	'purpleircv3capabilities.h',
 	'purpleircv3connection.h',
 	'purpleircv3core.h',
 	'purpleircv3messagehandlers.h',
diff --git a/libpurple/protocols/ircv3/purpleircv3capabilities.h b/libpurple/protocols/ircv3/purpleircv3capabilities.h
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/ircv3/purpleircv3capabilities.h
@@ -0,0 +1,87 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#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>
+
+G_BEGIN_DECLS
+
+#define PURPLE_IRCV3_TYPE_CAPABILITIES (purple_ircv3_capabilities_get_type())
+G_DECLARE_FINAL_TYPE(PurpleIRCv3Capabilities, purple_ircv3_capabilities,
+                     PURPLE_IRCV3, CAPABILITIES, GObject)
+
+#include "purpleircv3connection.h"
+
+G_GNUC_INTERNAL void purple_ircv3_capabilities_register(GPluginNativePlugin *plugin);
+
+G_GNUC_INTERNAL PurpleIRCv3Capabilities *purple_ircv3_capabilities_new(PurpleIRCv3Connection *connection);
+
+G_GNUC_INTERNAL gboolean purple_ircv3_capabilities_message_handler(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, 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.0
+ */
+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.0
+ */
+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.0
+ */
+const char *purple_ircv3_capabilities_lookup(PurpleIRCv3Capabilities *capabilities, const char *name, gboolean *found);
+
+G_END_DECLS
+
+#endif /* PURPLE_IRCV3_CAPABILITIES_H */
diff --git a/libpurple/protocols/ircv3/purpleircv3capabilities.c b/libpurple/protocols/ircv3/purpleircv3capabilities.c
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/ircv3/purpleircv3capabilities.c
@@ -0,0 +1,470 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "purpleircv3capabilities.h"
+
+#include "purpleircv3connection.h"
+#include "purpleircv3core.h"
+
+enum {
+	PROP_0,
+	PROP_CONNECTION,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+enum {
+	SIG_READY,
+	SIG_ACK,
+	SIG_NAK,
+	N_SIGNALS,
+};
+static guint signals[N_SIGNALS] = {0, };
+
+struct _PurpleIRCv3Capabilities {
+	GObject parent;
+
+	PurpleIRCv3Connection *connection;
+
+	GHashTable *caps;
+	GPtrArray *requests;
+};
+
+/******************************************************************************
+ * 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]);
+	}
+}
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+purple_ircv3_capabilities_ready_cb(PurpleIRCv3Capabilities *capabilities) {
+	gboolean found = FALSE;
+
+	purple_ircv3_capabilities_lookup(capabilities, "cap-notify", &found);
+	if(found) {
+		purple_ircv3_capabilities_request(capabilities, "cap-notify");
+	}
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_DYNAMIC_TYPE(PurpleIRCv3Capabilities, purple_ircv3_capabilities,
+                      G_TYPE_OBJECT)
+
+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_signal_connect(capabilities, "ready",
+	                 G_CALLBACK(purple_ircv3_capabilities_ready_cb), NULL);
+}
+
+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.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.0
+	 */
+	signals[SIG_READY] = g_signal_new_class_handler(
+		"ready",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		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.0
+	 */
+	signals[SIG_ACK] = g_signal_new_class_handler(
+		"ack",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		1,
+		G_TYPE_STRING);
+
+	/**
+	 * 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.0
+	 */
+	signals[SIG_NAK] = g_signal_new_class_handler(
+		"nak",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		1,
+		G_TYPE_STRING);
+}
+
+/******************************************************************************
+ * Command handlers
+ *****************************************************************************/
+static gboolean
+purple_ircv3_capabilities_handle_list(guint n_params,
+                                      GStrv params,
+                                      G_GNUC_UNUSED GError **error,
+                                      gpointer data)
+{
+	PurpleIRCv3Capabilities *capabilities = data;
+	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, spliting the keys and values. */
+	for(int i = 0; parts[i] != NULL; i++) {
+		char *equals = g_strstr_len(parts[i], -1, "=");
+
+		if(equals != NULL) {
+			char *key = g_strndup(parts[i], equals - parts[i]);
+			char *value = g_strdup(equals + 1);
+
+			g_hash_table_insert(capabilities->caps, key, value);
+		} else {
+			g_hash_table_insert(capabilities->caps, g_strdup(parts[i]), NULL);
+		}
+	}
+
+	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_connection_writef(capabilities->connection,
+			                               "CAP END");
+		}
+	}
+
+	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, 0, 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_connection_writef(capabilities->connection, "CAP END");
+	}
+
+	return ret;
+}
+
+static gboolean
+purple_ircv3_capabilities_handle_ack(G_GNUC_UNUSED guint n_params,
+                                     GStrv params,
+                                     GError **error,
+                                     gpointer data)
+{
+	return purple_ircv3_capabilities_handle_ack_nak(data, params,
+	                                                signals[SIG_ACK], "ACK",
+	                                                error);
+}
+
+
+static gboolean
+purple_ircv3_capabilities_handle_nak(G_GNUC_UNUSED guint n_params,
+                                     GStrv params,
+                                     G_GNUC_UNUSED GError **error,
+                                     gpointer data)
+{
+	return purple_ircv3_capabilities_handle_ack_nak(data, params,
+	                                                signals[SIG_NAK], "NAK",
+	                                                error);
+}
+
+/******************************************************************************
+ * 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);
+}
+
+gboolean
+purple_ircv3_capabilities_message_handler(G_GNUC_UNUSED GHashTable *tags,
+                                          G_GNUC_UNUSED const char *source,
+                                          G_GNUC_UNUSED const char *command,
+                                          guint n_params,
+                                          GStrv params,
+                                          GError **error,
+                                          gpointer data)
+{
+	PurpleIRCv3Connection *connection = data;
+	PurpleIRCv3Capabilities *capabilities = NULL;
+	const char *subcommand = NULL;
+	guint n_subparams = 0;
+	GStrv subparams = NULL;
+
+	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(n_subparams, subparams,
+		                                             error, capabilities);
+	} else if(purple_strequal(subcommand, "ACK")) {
+		return purple_ircv3_capabilities_handle_ack(n_subparams, subparams,
+		                                            error, capabilities);
+	} else if(purple_strequal(subcommand, "NAK")) {
+		return purple_ircv3_capabilities_handle_nak(n_subparams, subparams,
+		                                            error, capabilities);
+	}
+
+	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);
+
+	if(g_hash_table_lookup_extended(capabilities->caps, name, NULL, &value)) {
+		real_found = TRUE;
+	}
+
+	if(found != NULL) {
+		*found = real_found;
+	}
+
+	return value;
+}
diff --git a/libpurple/protocols/ircv3/purpleircv3connection.h b/libpurple/protocols/ircv3/purpleircv3connection.h
--- a/libpurple/protocols/ircv3/purpleircv3connection.h
+++ b/libpurple/protocols/ircv3/purpleircv3connection.h
@@ -33,6 +33,8 @@
 G_DECLARE_FINAL_TYPE(PurpleIRCv3Connection, purple_ircv3_connection,
                      PURPLE_IRCV3, CONNECTION, PurpleConnection)
 
+#include "purpleircv3capabilities.h"
+
 G_GNUC_INTERNAL void purple_ircv3_connection_register(GPluginNativePlugin *plugin);
 
 G_GNUC_INTERNAL PurpleAccount *purple_ircv3_connection_get_account(PurpleIRCv3Connection *connection);
@@ -49,9 +51,7 @@
  *
  * Returns: The list of capabilities that the server supports.
  */
-const char *purple_ircv3_connection_get_capabilities(PurpleIRCv3Connection *connection);
-
-G_GNUC_INTERNAL void purple_ircv3_connection_append_capabilities(PurpleIRCv3Connection *connection, const char *capabilities);
+PurpleIRCv3Capabilities *purple_ircv3_connection_get_capabilities(PurpleIRCv3Connection *connection);
 
 G_END_DECLS
 
diff --git a/libpurple/protocols/ircv3/purpleircv3connection.c b/libpurple/protocols/ircv3/purpleircv3connection.c
--- a/libpurple/protocols/ircv3/purpleircv3connection.c
+++ b/libpurple/protocols/ircv3/purpleircv3connection.c
@@ -44,7 +44,7 @@
 
 	PurpleIRCv3Parser *parser;
 
-	char *capabilities;
+	PurpleIRCv3Capabilities *capabilities;
 };
 
 G_DEFINE_DYNAMIC_TYPE(PurpleIRCv3Connection, purple_ircv3_connection,
@@ -104,6 +104,7 @@
 	GError *error = NULL;
 	gchar *line = NULL;
 	gsize length;
+	gboolean parsed = FALSE;
 
 	line = g_data_input_stream_read_line_finish(istream, result, &length,
 	                                            &error);
@@ -126,7 +127,13 @@
 		return;
 	}
 
-	purple_ircv3_parser_parse(connection->parser, line, &error, connection);
+	parsed = purple_ircv3_parser_parse(connection->parser, line, &error,
+	                                   connection);
+	if(!parsed) {
+		g_warning("failed to parse '%s': %s", line,
+		          error != NULL ? error->message : "unknown error");
+	}
+	g_clear_error(&error);
 
 	g_free(line);
 
@@ -314,7 +321,7 @@
 			                   purple_ircv3_connection_get_cancellable(connection));
 			break;
 		case PROP_CAPABILITIES:
-			g_value_set_string(value,
+			g_value_set_object(value,
 			                   purple_ircv3_connection_get_capabilities(connection));
 			break;
 		default:
@@ -324,19 +331,6 @@
 }
 
 static void
-purple_ircv3_connection_set_property(GObject *obj, guint param_id,
-                                     const GValue *value, GParamSpec *pspec)
-{
-	// PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);
-
-	switch(param_id) {
-		default:
-			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
-			break;
-	}
-}
-
-static void
 purple_ircv3_connection_dispose(GObject *obj) {
 	PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);
 
@@ -346,6 +340,7 @@
 	g_clear_object(&connection->output);
 	g_clear_object(&connection->connection);
 
+	g_clear_object(&connection->capabilities);
 	g_clear_object(&connection->parser);
 
 	G_OBJECT_CLASS(purple_ircv3_connection_parent_class)->dispose(obj);
@@ -357,8 +352,6 @@
 
 	g_clear_pointer(&connection->server_name, g_free);
 
-	g_clear_pointer(&connection->capabilities, g_free);
-
 	G_OBJECT_CLASS(purple_ircv3_connection_parent_class)->finalize(obj);
 }
 
@@ -381,8 +374,9 @@
 	connection->server_name = g_strdup(userparts[1]);
 	g_strfreev(userparts);
 
-	/* Finally create our cancellable. */
+	/* Finally create our objects. */
 	connection->cancellable = g_cancellable_new();
+	connection->capabilities = purple_ircv3_capabilities_new(connection);
 }
 
 static void
@@ -399,7 +393,6 @@
 	PurpleConnectionClass *connection_class = PURPLE_CONNECTION_CLASS(klass);
 
 	obj_class->get_property = purple_ircv3_connection_get_property;
-	obj_class->set_property = purple_ircv3_connection_set_property;
 	obj_class->constructed = purple_ircv3_connection_constructed;
 	obj_class->dispose = purple_ircv3_connection_dispose;
 	obj_class->finalize = purple_ircv3_connection_finalize;
@@ -430,10 +423,10 @@
 	 *
 	 * Since: 3.0.0
 	 */
-	properties[PROP_CAPABILITIES] = g_param_spec_string(
+	properties[PROP_CAPABILITIES] = g_param_spec_object(
 		"capabilities", "capabilities",
 		"The capabilities that the server supports",
-		NULL,
+		PURPLE_IRCV3_TYPE_CAPABILITIES,
 		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
 	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
@@ -488,32 +481,9 @@
 	g_bytes_unref(bytes);
 }
 
-const char *
+PurpleIRCv3Capabilities *
 purple_ircv3_connection_get_capabilities(PurpleIRCv3Connection *connection) {
 	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), NULL);
 
 	return connection->capabilities;
 }
-
-void
-purple_ircv3_connection_append_capabilities(PurpleIRCv3Connection *connection,
-                                            const char *capabilities)
-{
-	g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));
-	g_return_if_fail(capabilities != NULL);
-
-	if(connection->capabilities == NULL) {
-		connection->capabilities = g_strdup(capabilities);
-	} else {
-		char *tmp = connection->capabilities;
-
-		connection->capabilities = g_strdup_printf("%s %s",
-		                                           connection->capabilities,
-		                                           capabilities);
-
-		g_free(tmp);
-	}
-
-	g_object_notify_by_pspec(G_OBJECT(connection),
-	                         properties[PROP_CAPABILITIES]);
-}
diff --git a/libpurple/protocols/ircv3/purpleircv3core.c b/libpurple/protocols/ircv3/purpleircv3core.c
--- a/libpurple/protocols/ircv3/purpleircv3core.c
+++ b/libpurple/protocols/ircv3/purpleircv3core.c
@@ -26,6 +26,7 @@
 
 #include "purpleircv3core.h"
 
+#include "purpleircv3capabilities.h"
 #include "purpleircv3connection.h"
 #include "purpleircv3protocol.h"
 
@@ -71,8 +72,9 @@
 		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));
-	purple_ircv3_connection_register(GPLUGIN_NATIVE_PLUGIN(plugin));
 
 	manager = purple_protocol_manager_get_default();
 
diff --git a/libpurple/protocols/ircv3/purpleircv3messagehandlers.h b/libpurple/protocols/ircv3/purpleircv3messagehandlers.h
--- a/libpurple/protocols/ircv3/purpleircv3messagehandlers.h
+++ b/libpurple/protocols/ircv3/purpleircv3messagehandlers.h
@@ -51,7 +51,6 @@
                                               GError **error,
                                               gpointer data);
 
-G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_cap(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
 G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_fallback(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
 G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_ping(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
 G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_privmsg(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
diff --git a/libpurple/protocols/ircv3/purpleircv3messagehandlers.c b/libpurple/protocols/ircv3/purpleircv3messagehandlers.c
--- a/libpurple/protocols/ircv3/purpleircv3messagehandlers.c
+++ b/libpurple/protocols/ircv3/purpleircv3messagehandlers.c
@@ -47,76 +47,6 @@
 }
 
 /******************************************************************************
- * Capabilities
- *****************************************************************************/
-static gboolean
-purple_ircv3_message_handler_cap_list(guint n_params, GStrv params,
-                                      GError **error, gpointer data)
-{
-	PurpleIRCv3Connection *connection = data;
-
-	/* Check if we have more messages coming. */
-	if(n_params > 1 && purple_strequal(params[0], "*")) {
-		purple_ircv3_connection_append_capabilities(connection, params[1]);
-	} else {
-		purple_ircv3_connection_append_capabilities(connection, params[0]);
-
-		g_message("**** capabilities-list: %s",
-		          purple_ircv3_connection_get_capabilities(connection));
-
-		purple_ircv3_connection_writef(connection, "CAP END");
-	}
-
-	return TRUE;
-}
-
-gboolean
-purple_ircv3_message_handler_cap(GHashTable *tags, const char *source,
-                                 const char *command, guint n_params,
-                                 GStrv params, GError **error, gpointer data)
-{
-	const char *subcommand = NULL;
-	guint n_subparams = 0;
-	GStrv subparams = NULL;
-
-	if(n_params < 2) {
-		return FALSE;
-	}
-
-	/* 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_message_handler_cap_list(n_subparams, subparams,
-		                                             error, data);
-	}
-
-	g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
-	            "No handler for CAP subcommand %s", subcommand);
-
-	return FALSE;
-}
-
-/******************************************************************************
  * General Commands
  *****************************************************************************/
 gboolean
diff --git a/libpurple/protocols/ircv3/purpleircv3parser.c b/libpurple/protocols/ircv3/purpleircv3parser.c
--- a/libpurple/protocols/ircv3/purpleircv3parser.c
+++ b/libpurple/protocols/ircv3/purpleircv3parser.c
@@ -18,6 +18,7 @@
 
 #include "purpleircv3parser.h"
 
+#include "purpleircv3capabilities.h"
 #include "purpleircv3core.h"
 #include "purpleircv3messagehandlers.h"
 
@@ -398,7 +399,7 @@
 	                                         purple_ircv3_message_handler_fallback);
 
 	purple_ircv3_parser_add_handler(parser, "CAP",
-	                                purple_ircv3_message_handler_cap);
+	                                purple_ircv3_capabilities_message_handler);
 	purple_ircv3_parser_add_handler(parser, "NOTICE",
 	                                purple_ircv3_message_handler_privmsg);
 	purple_ircv3_parser_add_handler(parser, "PING",
