# HG changeset patch
# Node ID 0af0a4d47134
# Parent  9c0a6010fbc5
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
@@ -2,6 +2,7 @@
 	'purpleircv3capabilities.c',
 	'purpleircv3connection.c',
 	'purpleircv3core.c',
+	'purpleircv3message.c',
 	'purpleircv3messagehandlers.c',
 	'purpleircv3parser.c',
 	'purpleircv3protocol.c',
@@ -13,6 +14,7 @@
 	'purpleircv3capabilities.h',
 	'purpleircv3connection.h',
 	'purpleircv3core.h',
+	'purpleircv3message.h',
 	'purpleircv3messagehandlers.h',
 	'purpleircv3parser.h',
 	'purpleircv3protocol.h',
diff --git a/libpurple/protocols/ircv3/purpleircv3capabilities.h b/libpurple/protocols/ircv3/purpleircv3capabilities.h
--- a/libpurple/protocols/ircv3/purpleircv3capabilities.h
+++ b/libpurple/protocols/ircv3/purpleircv3capabilities.h
@@ -47,6 +47,7 @@
                      PURPLE_IRCV3, CAPABILITIES, GObject)
 
 #include "purpleircv3connection.h"
+#include "purpleircv3message.h"
 
 /**
  * purple_ircv3_capabilities_register: (skip)
@@ -62,7 +63,7 @@
 
 G_GNUC_INTERNAL void purple_ircv3_capabilities_start(PurpleIRCv3Capabilities *capabilities);
 
-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);
+G_GNUC_INTERNAL gboolean purple_ircv3_capabilities_message_handler(PurpleIRCv3Message *message, GError **error, gpointer data);
 
 /**
  * purple_ircv3_capabilities_get_connection:
diff --git a/libpurple/protocols/ircv3/purpleircv3capabilities.c b/libpurple/protocols/ircv3/purpleircv3capabilities.c
--- a/libpurple/protocols/ircv3/purpleircv3capabilities.c
+++ b/libpurple/protocols/ircv3/purpleircv3capabilities.c
@@ -528,19 +528,22 @@
 }
 
 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,
+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;
-	GStrv subparams = NULL;
+
+	params = purple_ircv3_message_get_params(message);
+	if(params != NULL) {
+		n_params = g_strv_length(params);
+	}
 
 	if(n_params < 2) {
 		return FALSE;
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
@@ -38,6 +38,7 @@
                          PURPLE_IRCV3, CONNECTION, PurpleConnection)
 
 #include "purpleircv3capabilities.h"
+#include "purpleircv3message.h"
 
 struct _PurpleIRCv3ConnectionClass {
 	/*< private >*/
@@ -107,16 +108,13 @@
 /**
  * purple_ircv3_connection_add_status_message:
  * @connection: The instance.
- * @source: The source/author of the message.
- * @command: The command name.
- * @n_params: The number of parameters.
- * @params: The parameters themselves.
+ * @message: The message.
  *
  * Adds a message to the status conversation/window for @connection.
  *
  * Since: 3.0.0
  */
-void purple_ircv3_connection_add_status_message(PurpleIRCv3Connection *connection, const char *source, const char *command, guint n_params, GStrv params);
+void purple_ircv3_connection_add_status_message(PurpleIRCv3Connection *connection, PurpleIRCv3Message *message);
 
 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
@@ -659,23 +659,25 @@
 
 void
 purple_ircv3_connection_add_status_message(PurpleIRCv3Connection *connection,
-                                           const char *source,
-                                           const char *command,
-                                           guint n_params,
-                                           GStrv params)
+                                           PurpleIRCv3Message *v3_message)
 {
 	PurpleIRCv3ConnectionPrivate *priv = NULL;
 	PurpleMessage *message = NULL;
 	GString *str = NULL;
+	GStrv params = NULL;
+	const char *command = NULL;
 
 	g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));
-	g_return_if_fail(command != NULL);
+	g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(v3_message));
 
 	priv = purple_ircv3_connection_get_instance_private(connection);
 
+	command = purple_ircv3_message_get_command(v3_message);
+
 	str = g_string_new(command);
 
-	if(n_params > 0) {
+	params = purple_ircv3_message_get_params(v3_message);
+	if(params != NULL && params[0] != NULL) {
 		char *joined = g_strjoinv(" ", params);
 
 		g_string_append_printf(str, " %s", joined);
@@ -685,7 +687,7 @@
 
 	message = g_object_new(
 		PURPLE_TYPE_MESSAGE,
-		"author", source,
+		"author", purple_ircv3_message_get_source(v3_message),
 		"contents", str->str,
 		NULL);
 
diff --git a/libpurple/protocols/ircv3/purpleircv3message.h b/libpurple/protocols/ircv3/purpleircv3message.h
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/ircv3/purpleircv3message.h
@@ -0,0 +1,135 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+#if !defined(PURPLE_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>
+
+G_BEGIN_DECLS
+
+#define PURPLE_IRCV3_TYPE_MESSAGE (purple_ircv3_message_get_type())
+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.0
+ */
+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.0
+ */
+const char *purple_ircv3_message_get_command(PurpleIRCv3Message *message);
+
+/**
+ * purple_ircv3_message_get_params:
+ * @message: The instance.
+ *
+ * Gets the parameters from @message.
+ *
+ * Returns: (transfer none) (nullable): The parameters from @message.
+ *
+ * Since: 3.0.0
+ */
+GStrv purple_ircv3_message_get_params(PurpleIRCv3Message *message);
+
+/**
+ * purple_ircv3_messaget_set_params:
+ * @message: The instance.
+ * @params: (transfer none) (nullable): The new parameters.
+ *
+ * Sets the parameters of @message to @params.
+ *
+ * Since: 3.0.0
+ */
+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.0
+ */
+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.0
+ */
+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.0
+ */
+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.0
+ */
+void purple_ircv3_message_set_tags(PurpleIRCv3Message *message, GHashTable *tags);
+
+G_END_DECLS
+
+#endif /* PURPLE_IRCV3_MESSAGE_H */
diff --git a/libpurple/protocols/ircv3/purpleircv3message.c b/libpurple/protocols/ircv3/purpleircv3message.c
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/ircv3/purpleircv3message.c
@@ -0,0 +1,283 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n-lib.h>
+
+#include "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;
+};
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static 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(!purple_strequal(message->command, command)) {
+		g_free(message->command);
+		message->command = g_strdup(command);
+
+		g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_COMMAND]);
+	}
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_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.0
+	 */
+	properties[PROP_COMMAND] = g_param_spec_string(
+		"command", "command",
+		"The command of the message.",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleIRCv3Message:params:
+	 *
+	 * The parameters of the message.
+	 *
+	 * When serialized, the last item will be prefixed with a :.
+	 *
+	 * Since: 3.0.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.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.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;
+}
+
+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(!purple_strequal(message->source, source)) {
+		g_free(message->source);
+		message->source = g_strdup(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/libpurple/protocols/ircv3/purpleircv3messagehandlers.h b/libpurple/protocols/ircv3/purpleircv3messagehandlers.h
--- a/libpurple/protocols/ircv3/purpleircv3messagehandlers.h
+++ b/libpurple/protocols/ircv3/purpleircv3messagehandlers.h
@@ -27,39 +27,32 @@
 
 #include <purple.h>
 
+#include "purpleircv3message.h"
+
 G_BEGIN_DECLS
 
 /**
  * PurpleIRCv3MessageHandler:
- * @tags: (element-type utf8 utf8): A [struct@GLib.HashTable] of tags for this
- *        message.
- * @source: The source of the command.
- * @command: The command or numeric of the message.
- * @n_params: The number of parameters.
- * @params: (array length=n_params): The parameters of the message.
+ * @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 a message.
+ * 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.0
  */
-typedef gboolean (*PurpleIRCv3MessageHandler)(GHashTable *tags,
-                                              const char *source,
-                                              const char *command,
-                                              guint n_params,
-                                              GStrv params,
+typedef gboolean (*PurpleIRCv3MessageHandler)(PurpleIRCv3Message *message,
                                               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_status(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_status_ignore_param0(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);
+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_END_DECLS
 
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
@@ -27,20 +27,18 @@
  * Fallback
  *****************************************************************************/
 gboolean
-purple_ircv3_message_handler_fallback(G_GNUC_UNUSED GHashTable *tags,
-                                      const char *source,
-                                      const char *command,
-                                      guint n_params,
-                                      GStrv params,
+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_connection_add_status_message(connection, source, new_command,
-	                                           n_params, params);
+	purple_ircv3_connection_add_status_message(connection, message);
 
 	g_clear_pointer(&new_command, g_free);
 
@@ -51,29 +49,28 @@
  * Status Messages
  *****************************************************************************/
 gboolean
-purple_ircv3_message_handler_status(G_GNUC_UNUSED GHashTable *tags,
-                                    const char *source,
-                                    const char *command,
-                                    guint n_params,
-                                    GStrv params,
+purple_ircv3_message_handler_status(PurpleIRCv3Message *message,
                                     G_GNUC_UNUSED GError **error,
                                     gpointer data)
 {
-	purple_ircv3_connection_add_status_message(data, source, command, n_params,
-	                                           params);
+	purple_ircv3_connection_add_status_message(data, message);
 
 	return TRUE;
 }
 
 gboolean
-purple_ircv3_message_handler_status_ignore_param0(G_GNUC_UNUSED GHashTable *tags,
-                                                  const char *source,
-                                                  const char *command,
-                                                  guint n_params,
-                                                  GStrv params,
+purple_ircv3_message_handler_status_ignore_param0(PurpleIRCv3Message *message,
                                                   GError **error,
                                                   gpointer data)
 {
+	GStrv params;
+	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);
@@ -81,8 +78,7 @@
 		return FALSE;
 	}
 
-	purple_ircv3_connection_add_status_message(data, source, command,
-	                                           n_params - 1, params + 1);
+	purple_ircv3_connection_add_status_message(data, message);
 
 	return TRUE;
 }
@@ -91,17 +87,16 @@
  * General Commands
  *****************************************************************************/
 gboolean
-purple_ircv3_message_handler_ping(G_GNUC_UNUSED GHashTable *tags,
-                                  G_GNUC_UNUSED const char *source,
-                                  G_GNUC_UNUSED const char *command,
-                                  guint n_params,
-                                  GStrv params,
+purple_ircv3_message_handler_ping(PurpleIRCv3Message *message,
                                   G_GNUC_UNUSED GError **error,
                                   gpointer data)
 {
 	PurpleIRCv3Connection *connection = data;
+	GStrv params = NULL;
 
-	if(n_params == 1) {
+	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");
@@ -111,11 +106,7 @@
 }
 
 gboolean
-purple_ircv3_message_handler_privmsg(GHashTable *tags,
-                                     const char *source,
-                                     const char *command,
-                                     guint n_params,
-                                     GStrv params,
+purple_ircv3_message_handler_privmsg(PurpleIRCv3Message *v3_message,
                                      G_GNUC_UNUSED GError **error,
                                      gpointer data)
 {
@@ -128,12 +119,21 @@
 	PurpleMessage *message = NULL;
 	PurpleMessageFlags flags = PURPLE_MESSAGE_RECV;
 	GDateTime *dt = NULL;
+	GHashTable *tags = NULL;
+	GStrv params = NULL;
 	gpointer raw_id = NULL;
 	gpointer raw_timestamp = NULL;
+	const char *command = NULL;
 	const char *id = NULL;
+	const char *source = NULL;
 	const char *target = NULL;
 
-	if(n_params != 2) {
+	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);
+
+	if(params != NULL && g_strv_length(params) != 2) {
 		char *body = g_strjoinv(" ", params);
 		g_warning("unknown privmsg message format: '%s'", body);
 		g_free(body);
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
@@ -20,6 +20,7 @@
 
 #include "purpleircv3capabilities.h"
 #include "purpleircv3core.h"
+#include "purpleircv3message.h"
 #include "purpleircv3messagehandlers.h"
 #include "purpleircv3sasl.h"
 
@@ -193,16 +194,11 @@
 {
 	GStrvBuilder *builder = g_strv_builder_new();
 	GStrv result = NULL;
-	guint count = 0;
 
-	*n_params = 0;
-
-	count = purple_ircv3_parser_extract_params(parser, builder, middle);
-	*n_params = *n_params + count;
+	purple_ircv3_parser_extract_params(parser, builder, middle);
 
 	if(*coda != '\0') {
 		g_strv_builder_add(builder, trailing);
-		*n_params = *n_params + 1;
 	}
 
 	result = g_strv_builder_end(builder);
@@ -216,16 +212,12 @@
  * Handlers
  *****************************************************************************/
 static gboolean
-purple_ircv3_fallback_handler(G_GNUC_UNUSED GHashTable *tags,
-                              G_GNUC_UNUSED const gchar *source,
-                              G_GNUC_UNUSED const gchar *command,
-                              G_GNUC_UNUSED guint n_params,
-                              G_GNUC_UNUSED GStrv params,
+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",
-	            command);
+	            purple_ircv3_message_get_command(message));
 
 	return FALSE;
 }
@@ -296,6 +288,7 @@
 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;
@@ -309,7 +302,6 @@
 	gchar *trailing = NULL;
 	gboolean matches = FALSE;
 	gboolean result = FALSE;
-	guint n_params = 0;
 
 	g_return_val_if_fail(PURPLE_IRCV3_IS_PARSER(parser), FALSE);
 	g_return_val_if_fail(buffer != NULL, FALSE);
@@ -346,6 +338,8 @@
 	/* 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);
@@ -353,32 +347,44 @@
 		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,
-	                                          &n_params);
+	                                          NULL);
+	if(params != NULL) {
+		purple_ircv3_message_set_params(message, params);
+	}
 
-	/* Call the handler. */
-	result = handler(tags, source, command, n_params, params, error, data);
-
-	/* Cleanup everything. */
-	g_free(source);
 	g_free(command);
 	g_free(middle);
 	g_free(coda);
 	g_free(trailing);
 	g_strfreev(params);
-	g_hash_table_destroy(tags);
+
+	/* Call the handler. */
+	result = handler(message, error, data);
+
+	/* Cleanup the left overs. */
 	g_match_info_unref(info);
+	g_clear_object(&message);
 
 	return result;
 }
diff --git a/libpurple/protocols/ircv3/purpleircv3sasl.h b/libpurple/protocols/ircv3/purpleircv3sasl.h
--- a/libpurple/protocols/ircv3/purpleircv3sasl.h
+++ b/libpurple/protocols/ircv3/purpleircv3sasl.h
@@ -28,21 +28,22 @@
 #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(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_logged_out(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_nick_locked(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_success(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_failed(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_message_too_long(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_aborted(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_already_authed(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_mechanisms(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
-G_GNUC_INTERNAL gboolean purple_ircv3_sasl_authenticate(GHashTable *tags, const char *source, const char *command, guint n_params, GStrv params, GError **error, gpointer data);
+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
 
diff --git a/libpurple/protocols/ircv3/purpleircv3sasl.c b/libpurple/protocols/ircv3/purpleircv3sasl.c
--- a/libpurple/protocols/ircv3/purpleircv3sasl.c
+++ b/libpurple/protocols/ircv3/purpleircv3sasl.c
@@ -188,11 +188,7 @@
 }
 
 gboolean
-purple_ircv3_sasl_logged_in(G_GNUC_UNUSED GHashTable *tags,
-                            G_GNUC_UNUSED const char *source,
-                            G_GNUC_UNUSED const char *command,
-                            G_GNUC_UNUSED guint n_params,
-                            G_GNUC_UNUSED GStrv params,
+purple_ircv3_sasl_logged_in(G_GNUC_UNUSED PurpleIRCv3Message *message,
                             G_GNUC_UNUSED GError **error,
                             gpointer user_data)
 {
@@ -231,11 +227,7 @@
 }
 
 gboolean
-purple_ircv3_sasl_logged_out(G_GNUC_UNUSED GHashTable *tags,
-                             G_GNUC_UNUSED const char *source,
-                             G_GNUC_UNUSED const char *command,
-                             G_GNUC_UNUSED guint n_params,
-                             G_GNUC_UNUSED GStrv params,
+purple_ircv3_sasl_logged_out(G_GNUC_UNUSED PurpleIRCv3Message *message,
                              G_GNUC_UNUSED GError **error,
                              gpointer user_data)
 {
@@ -261,16 +253,13 @@
 }
 
 gboolean
-purple_ircv3_sasl_nick_locked(G_GNUC_UNUSED GHashTable *tags,
-                              G_GNUC_UNUSED const char *source,
-                              G_GNUC_UNUSED const char *command,
-                              G_GNUC_UNUSED guint n_params,
-                              GStrv params,
+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);
@@ -282,6 +271,7 @@
 		return FALSE;
 	}
 
+	params = purple_ircv3_message_get_params(v3_message);
 	message = g_strjoinv(" ", params);
 
 	g_set_error(error, PURPLE_CONNECTION_ERROR,
@@ -295,11 +285,7 @@
 
 
 gboolean
-purple_ircv3_sasl_success(G_GNUC_UNUSED GHashTable *tags,
-                          G_GNUC_UNUSED const char *source,
-                          G_GNUC_UNUSED const char *command,
-                          G_GNUC_UNUSED guint n_params,
-                          G_GNUC_UNUSED GStrv params,
+purple_ircv3_sasl_success(G_GNUC_UNUSED PurpleIRCv3Message *message,
                           GError **error,
                           gpointer user_data)
 {
@@ -330,11 +316,7 @@
 }
 
 gboolean
-purple_ircv3_sasl_failed(G_GNUC_UNUSED GHashTable *tags,
-                         G_GNUC_UNUSED const char *source,
-                         G_GNUC_UNUSED const char *command,
-                         G_GNUC_UNUSED guint n_params,
-                         G_GNUC_UNUSED GStrv params,
+purple_ircv3_sasl_failed(G_GNUC_UNUSED PurpleIRCv3Message *message,
                          G_GNUC_UNUSED GError **error,
                          gpointer user_data)
 {
@@ -355,11 +337,7 @@
 }
 
 gboolean
-purple_ircv3_sasl_message_too_long(G_GNUC_UNUSED GHashTable *tags,
-                                   G_GNUC_UNUSED const char *source,
-                                   G_GNUC_UNUSED const char *command,
-                                   G_GNUC_UNUSED guint n_params,
-                                   G_GNUC_UNUSED GStrv params,
+purple_ircv3_sasl_message_too_long(G_GNUC_UNUSED PurpleIRCv3Message *message,
                                    G_GNUC_UNUSED GError **error,
                                    gpointer user_data)
 {
@@ -379,11 +357,7 @@
 }
 
 gboolean
-purple_ircv3_sasl_aborted(G_GNUC_UNUSED GHashTable *tags,
-                          G_GNUC_UNUSED const char *source,
-                          G_GNUC_UNUSED const char *command,
-                          G_GNUC_UNUSED guint n_params,
-                          G_GNUC_UNUSED GStrv params,
+purple_ircv3_sasl_aborted(G_GNUC_UNUSED PurpleIRCv3Message *message,
                           G_GNUC_UNUSED GError **error,
                           gpointer user_data)
 {
@@ -409,11 +383,7 @@
 }
 
 gboolean
-purple_ircv3_sasl_already_authed(G_GNUC_UNUSED GHashTable *tags,
-                                 G_GNUC_UNUSED const char *source,
-                                 G_GNUC_UNUSED const char *command,
-                                 G_GNUC_UNUSED guint n_params,
-                                 G_GNUC_UNUSED GStrv params,
+purple_ircv3_sasl_already_authed(G_GNUC_UNUSED PurpleIRCv3Message *message,
                                  G_GNUC_UNUSED GError **error,
                                  gpointer user_data)
 {
@@ -439,16 +409,14 @@
 }
 
 gboolean
-purple_ircv3_sasl_mechanisms(G_GNUC_UNUSED GHashTable *tags,
-                             G_GNUC_UNUSED const char *source,
-                             G_GNUC_UNUSED const char *command,
-                             guint n_params,
-                             GStrv params,
+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) {
@@ -459,6 +427,11 @@
 		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.
@@ -483,18 +456,21 @@
 }
 
 gboolean
-purple_ircv3_sasl_authenticate(G_GNUC_UNUSED GHashTable *tags,
-                               G_GNUC_UNUSED const char *source,
-                               G_GNUC_UNUSED const char *command,
-                               guint n_params,
-                               GStrv params,
+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,
diff --git a/libpurple/protocols/ircv3/tests/test_ircv3_parser.c b/libpurple/protocols/ircv3/tests/test_ircv3_parser.c
--- a/libpurple/protocols/ircv3/tests/test_ircv3_parser.c
+++ b/libpurple/protocols/ircv3/tests/test_ircv3_parser.c
@@ -20,6 +20,7 @@
 
 #include <purple.h>
 
+#include "../purpleircv3message.h"
 #include "../purpleircv3parser.h"
 
 #define TEST_IRCV3_PARSER_DOMAIN (g_quark_from_static_string("test-ircv3-parser"))
@@ -36,13 +37,26 @@
  * Handlers
  *****************************************************************************/
 static gboolean
-test_purple_ircv3_test_handler(GHashTable *tags, const gchar *source,
-                               const gchar *command, guint n_params,
-                               GStrv params, G_GNUC_UNUSED GError **error,
+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) {
@@ -71,17 +85,6 @@
 		}
 	}
 
-	/* If the expected strings values are NULL, set them to empty string as
-	 * that's what g_match_info_get_named will return for them.
-	 */
-	if(d->source == NULL) {
-		d->source = "";
-	}
-
-	if(d->command == NULL) {
-		d->command = "";
-	}
-
 	/* Walk through the params checking against the expected values. */
 	if(d->n_params > 0) {
 		g_assert_cmpuint(n_params, ==, d->n_params);
@@ -103,11 +106,7 @@
 }
 
 static gboolean
-test_purple_ircv3_test_handler_error(G_GNUC_UNUSED GHashTable *tags,
-                                     G_GNUC_UNUSED const gchar *source,
-                                     G_GNUC_UNUSED const gchar *command,
-                                     G_GNUC_UNUSED guint n_params,
-                                     G_GNUC_UNUSED GStrv params,
+test_purple_ircv3_test_handler_error(G_GNUC_UNUSED PurpleIRCv3Message *message,
                                      GError **error,
                                      G_GNUC_UNUSED gpointer data)
 {
