# HG changeset patch
# Node ID 73a82e3d8981109fb8aa011a565326b1a12d13c2
# Parent  efaddb319116b71108ed8d66de6445c2e52cf93e
diff --git a/ibis/ibisconstants.h b/ibis/ibisconstants.h
--- a/ibis/ibisconstants.h
+++ b/ibis/ibisconstants.h
@@ -1417,6 +1417,18 @@
 #define IBIS_MSG_ERROR "ERROR" IBIS_AVAILABLE_MACRO_IN_0_1
 
 /**
+ * IBIS_MSG_FAIL:
+ *
+ * A constant for the %FAIL standard reply message.
+ *
+ * See [standard replies](https://ircv3.net/specs/extensions/standard-replies)
+ * for more information.
+ *
+ * Since: 0.8
+ */
+#define IBIS_MSG_FAIL "FAIL" IBIS_AVAILABLE_MACRO_IN_0_8
+
+/**
  * IBIS_MSG_HELP:
  *
  * A constant for the %HELP message.
@@ -1534,6 +1546,18 @@
 #define IBIS_MSG_NICK "NICK" IBIS_AVAILABLE_MACRO_IN_0_1
 
 /**
+ * IBIS_MSG_NOTE:
+ *
+ * A constant for the %NOTE standard reply message.
+ *
+ * See [standard replies](https://ircv3.net/specs/extensions/standard-replies)
+ * for more information.
+ *
+ * Since: 0.8
+ */
+#define IBIS_MSG_NOTE "NOTE" IBIS_AVAILABLE_MACRO_IN_0_8
+
+/**
  * IBIS_MSG_NOTICE:
  *
  * A constant for the %NOTICE message.
@@ -1708,6 +1732,18 @@
 #define IBIS_MSG_WALLOPS "WALLOPS" IBIS_AVAILABLE_MACRO_IN_0_1
 
 /**
+ * IBIS_MSG_WARN:
+ *
+ * A constant for the %WARN standard reply message.
+ *
+ * See [standard replies](https://ircv3.net/specs/extensions/standard-replies)
+ * for more information.
+ *
+ * Since: 0.8
+ */
+#define IBIS_MSG_WARN "WARN" IBIS_AVAILABLE_MACRO_IN_0_8
+
+/**
  * IBIS_MSG_WHO:
  *
  * A constant for the %WHO message.
@@ -2482,6 +2518,45 @@
 #define IBIS_RPL_YOUREOPER "381" IBIS_AVAILABLE_MACRO_IN_0_1
 
 /**
+ * IBIS_STANDARD_REPLY_TYPE_FAIL:
+ *
+ * A constant for the `FAIL` standard reply type.
+ *
+ * See [standard replies message
+ * format](https://ircv3.net/specs/extensions/standard-replies#message-format)
+ * for more information.
+ *
+ * Since: 0.8
+ */
+#define IBIS_STANDARD_REPLY_TYPE_FAIL "FAIL" IBIS_AVAILABLE_MACRO_IN_0_8
+
+/**
+ * IBIS_STANDARD_REPLY_TYPE_NOTE:
+ *
+ * A constant for the `NOTE` standard reply type.
+ *
+ * See [standard replies message
+ * format](https://ircv3.net/specs/extensions/standard-replies#message-format)
+ * for more information.
+ *
+ * Since: 0.8
+ */
+#define IBIS_STANDARD_REPLY_TYPE_NOTE "NOTE" IBIS_AVAILABLE_MACRO_IN_0_8
+
+/**
+ * IBIS_STANDARD_REPLY_TYPE_WARN:
+ *
+ * A constant for the `WARN` standard reply type.
+ *
+ * See [standard replies message
+ * format](https://ircv3.net/specs/extensions/standard-replies#message-format)
+ * for more information.
+ *
+ * Since: 0.8
+ */
+#define IBIS_STANDARD_REPLY_TYPE_WARN "WARN" IBIS_AVAILABLE_MACRO_IN_0_8
+
+/**
  * IBIS_TAG_ACCOUNT:
  *
  * A constant for the `account` tag.
diff --git a/ibis/ibismessage.c b/ibis/ibismessage.c
--- a/ibis/ibismessage.c
+++ b/ibis/ibismessage.c
@@ -39,6 +39,7 @@
 	PROP_ERROR,
 	PROP_CTCP,
 	PROP_CTCP_MESSAGE,
+	PROP_STANDARD_REPLY,
 	N_PROPERTIES,
 };
 static GParamSpec *properties[N_PROPERTIES] = {NULL, };
@@ -56,6 +57,7 @@
 	IbisTags *tags;
 	GError *error;
 	IbisCTCPMessage *ctcp_message;
+	IbisStandardReply *standard_reply;
 };
 
 /******************************************************************************
@@ -248,6 +250,17 @@
 	return result;
 }
 
+static void
+ibis_message_set_standard_reply(IbisMessage *message, IbisStandardReply *reply)
+{
+	g_return_if_fail(IBIS_IS_MESSAGE(message));
+
+	if(g_set_object(&message->standard_reply, reply)) {
+		g_object_notify_by_pspec(G_OBJECT(message),
+		                         properties[PROP_STANDARD_REPLY]);
+	}
+}
+
 /******************************************************************************
  * GObject Implementation
  *****************************************************************************/
@@ -264,6 +277,7 @@
 	g_clear_object(&message->tags);
 	g_clear_error(&message->error);
 	g_clear_object(&message->ctcp_message);
+	g_clear_object(&message->standard_reply);
 
 	G_OBJECT_CLASS(ibis_message_parent_class)->finalize(obj);
 }
@@ -302,6 +316,9 @@
 	case PROP_CTCP_MESSAGE:
 		g_value_set_object(value, ibis_message_get_ctcp_message(message));
 		break;
+	case PROP_STANDARD_REPLY:
+		g_value_set_object(value, ibis_message_get_standard_reply(message));
+		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
 		break;
@@ -341,6 +358,9 @@
 	case PROP_CTCP_MESSAGE:
 		ibis_message_set_ctcp_message(message, g_value_get_object(value));
 		break;
+	case PROP_STANDARD_REPLY:
+		ibis_message_set_standard_reply(message, g_value_get_object(value));
+		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
 		break;
@@ -487,6 +507,18 @@
 		IBIS_TYPE_CTCP_MESSAGE,
 		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * IbisMessage:standard-reply:
+	 *
+	 * The [class@StandardReply] if the message is a standard reply.
+	 *
+	 * Since: 0.8
+	 */
+	properties[PROP_STANDARD_REPLY] = g_param_spec_object(
+		"standard-reply", NULL, NULL,
+		IBIS_TYPE_STANDARD_REPLY,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
 	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
 }
 
@@ -799,6 +831,20 @@
 		ibis_message_set_paramsv(message, params);
 	}
 
+	/* Once we've parsed the full message we can check if it's a standard reply
+	 * and parse that if it is.
+	 */
+	if(ibis_str_equal(command, IBIS_MSG_FAIL) ||
+	   ibis_str_equal(command, IBIS_MSG_NOTE) ||
+	   ibis_str_equal(command, IBIS_MSG_WARN))
+	{
+		IbisStandardReply *reply = NULL;
+
+		reply = ibis_standard_reply_parse(message);
+		ibis_message_set_standard_reply(message, reply);
+		g_clear_object(&reply);
+	}
+
 	/* Clean up everything */
 	g_free(command);
 	g_free(middle);
@@ -878,3 +924,10 @@
 
 	return ibis_str_equal(message->command, command);
 }
+
+IbisStandardReply *
+ibis_message_get_standard_reply(IbisMessage *message) {
+	g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL);
+
+	return message->standard_reply;
+}
diff --git a/ibis/ibismessage.h b/ibis/ibismessage.h
--- a/ibis/ibismessage.h
+++ b/ibis/ibismessage.h
@@ -53,6 +53,8 @@
 IBIS_AVAILABLE_IN_0_1
 G_DECLARE_FINAL_TYPE(IbisMessage, ibis_message, IBIS, MESSAGE, GObject)
 
+#include "ibisstandardreply.h"
+
 /**
  * ibis_message_new:
  * @command: (not nullable): The command of the message.
@@ -395,6 +397,20 @@
 IBIS_AVAILABLE_IN_0_7
 gboolean ibis_message_is_command(IbisMessage *message, const char *command);
 
+/**
+ * ibis_message_get_standard_reply:
+ * @message: The instance.
+ *
+ * Gets the [class@StandardReply] from @message.
+ *
+ * Returns: (transfer none): The standard reply if @message contains one,
+ *          otherwise %NULL.
+ *
+ * Since: 0.8
+ */
+IBIS_AVAILABLE_IN_0_8
+IbisStandardReply *ibis_message_get_standard_reply(IbisMessage *message);
+
 G_END_DECLS
 
 #endif /* IBIS_MESSAGE_H */
diff --git a/ibis/ibisstandardreply.c b/ibis/ibisstandardreply.c
new file mode 100644
--- /dev/null
+++ b/ibis/ibisstandardreply.c
@@ -0,0 +1,266 @@
+/*
+ * Ibis - IRCv3 Library
+ * Copyright (C) 2022-2024 Ibis Developers
+ *
+ * Ibis 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 "ibisstandardreply.h"
+
+#include "ibisconstants.h"
+#include "ibisstring.h"
+
+struct _IbisStandardReply {
+	GObject parent;
+
+	char *reply_type;
+	char *command;
+	char *code;
+	GStrv context;
+	char *description;
+};
+
+enum {
+	PROP_0,
+	PROP_REPLY_TYPE,
+	PROP_COMMAND,
+	PROP_CODE,
+	PROP_CONTEXT,
+	PROP_DESCRIPTION,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_FINAL_TYPE(IbisStandardReply, ibis_standard_reply, G_TYPE_OBJECT)
+
+static void
+ibis_standard_reply_finalize(GObject *obj) {
+	IbisStandardReply *reply = IBIS_STANDARD_REPLY(obj);
+
+	g_clear_pointer(&reply->reply_type, g_free);
+	g_clear_pointer(&reply->command, g_free);
+	g_clear_pointer(&reply->code, g_free);
+	g_clear_pointer(&reply->context, g_strfreev);
+	g_clear_pointer(&reply->description, g_free);
+
+	G_OBJECT_CLASS(ibis_standard_reply_parent_class)->finalize(obj);
+}
+
+static void
+ibis_standard_reply_get_property(GObject *obj, guint param_id, GValue *value,
+                                 GParamSpec *pspec)
+{
+	IbisStandardReply *reply = IBIS_STANDARD_REPLY(obj);
+
+	switch(param_id) {
+	case PROP_REPLY_TYPE:
+		g_value_set_string(value, ibis_standard_reply_get_reply_type(reply));
+		break;
+	case PROP_COMMAND:
+		g_value_set_string(value, ibis_standard_reply_get_command(reply));
+		break;
+	case PROP_CODE:
+		g_value_set_string(value, ibis_standard_reply_get_code(reply));
+		break;
+	case PROP_CONTEXT:
+		g_value_set_boxed(value, ibis_standard_reply_get_context(reply));
+		break;
+	case PROP_DESCRIPTION:
+		g_value_set_string(value, ibis_standard_reply_get_description(reply));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+ibis_standard_reply_init(G_GNUC_UNUSED IbisStandardReply *reply) {
+}
+
+static void
+ibis_standard_reply_class_init(IbisStandardReplyClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = ibis_standard_reply_finalize;
+	obj_class->get_property = ibis_standard_reply_get_property;
+
+	/**
+	 * IbisStandardReply:reply-type:
+	 *
+	 * The type of the reply.
+	 *
+	 * Since: 0.8
+	 */
+	properties[PROP_REPLY_TYPE] = g_param_spec_string(
+		"reply-type", NULL, NULL,
+		NULL,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * IbisStandardReply:command:
+	 *
+	 * The command that this reply is related to.
+	 *
+	 * Since: 0.8
+	 */
+	properties[PROP_COMMAND] = g_param_spec_string(
+		"command", NULL, NULL,
+		NULL,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * IbisStandardReply:code:
+	 *
+	 * The machine-readable code for this reply.
+	 *
+	 * Since: 0.8
+	 */
+	properties[PROP_CODE] = g_param_spec_string(
+		"code", NULL, NULL,
+		NULL,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * IbisStandardReply:context:
+	 *
+	 * The context for this reply.
+	 *
+	 * Since: 0.8
+	 */
+	properties[PROP_CONTEXT] = g_param_spec_boxed(
+		"context", NULL, NULL,
+		G_TYPE_STRV,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * IbisStandardReply:description:
+	 *
+	 * The description of the reply.
+	 *
+	 * Since: 0.8
+	 */
+	properties[PROP_DESCRIPTION] = g_param_spec_string(
+		"description", NULL, NULL,
+		NULL,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+const char *
+ibis_standard_reply_get_reply_type(IbisStandardReply *reply) {
+	g_return_val_if_fail(IBIS_IS_STANDARD_REPLY(reply), NULL);
+
+	return reply->reply_type;
+}
+
+gboolean
+ibis_standard_reply_is_type(IbisStandardReply *reply, const char *type) {
+	g_return_val_if_fail(IBIS_IS_STANDARD_REPLY(reply), FALSE);
+
+	return ibis_str_equal(reply->reply_type, type);
+}
+
+const char *
+ibis_standard_reply_get_code(IbisStandardReply *reply) {
+	g_return_val_if_fail(IBIS_IS_STANDARD_REPLY(reply), NULL);
+
+	return reply->code;
+}
+
+const char *
+ibis_standard_reply_get_command(IbisStandardReply *reply) {
+	g_return_val_if_fail(IBIS_IS_STANDARD_REPLY(reply), NULL);
+
+	return reply->command;
+}
+
+GStrv
+ibis_standard_reply_get_context(IbisStandardReply *reply) {
+	g_return_val_if_fail(IBIS_IS_STANDARD_REPLY(reply), NULL);
+
+	return reply->context;
+}
+
+const char *
+ibis_standard_reply_get_description(IbisStandardReply *reply) {
+	g_return_val_if_fail(IBIS_IS_STANDARD_REPLY(reply), NULL);
+
+	return reply->description;
+}
+
+IbisStandardReply *
+ibis_standard_reply_parse(IbisMessage *message) {
+	IbisStandardReply *reply = NULL;
+	GStrv params = NULL;
+	GStrvBuilder *builder = NULL;
+	const char *reply_type = NULL;
+	guint n_params = 0;
+
+	g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL);
+
+	if(ibis_message_is_command(message, IBIS_MSG_FAIL)) {
+		reply_type = IBIS_STANDARD_REPLY_TYPE_FAIL;
+	} else if(ibis_message_is_command(message, IBIS_MSG_NOTE)) {
+		reply_type = IBIS_STANDARD_REPLY_TYPE_NOTE;
+	} else if(ibis_message_is_command(message, IBIS_MSG_WARN)) {
+		reply_type = IBIS_STANDARD_REPLY_TYPE_WARN;
+	}
+
+	if(reply_type == NULL) {
+		return NULL;
+	}
+
+	/* Standard replies have at least 3 parameters. The type is the COMMAND of
+	 * the message and not a parameter.
+	 */
+	params = ibis_message_get_params(message);
+	n_params = g_strv_length(params);
+	if(n_params < 3) {
+		return NULL;
+	}
+
+	reply = g_object_new(IBIS_TYPE_STANDARD_REPLY, NULL);
+
+	reply->reply_type = g_strdup(reply_type);
+	reply->command = g_strdup(params[0]);
+	reply->code = g_strdup(params[1]);
+	reply->description = g_strdup(params[n_params - 1]);
+
+	/* If the number of parameters is more than 3, then we have context that
+	 * we need to pull out into a new strv.
+	 */
+	builder = g_strv_builder_new();
+
+	/* Context is everything from parameter 2 to n_params - 1. */
+	for(guint i = 2; i < n_params - 1; i++) {
+		g_strv_builder_add(builder, params[i]);
+	}
+
+	reply->context = g_strv_builder_end(builder);
+	g_clear_pointer(&builder, g_strv_builder_unref);
+
+	return reply;
+}
diff --git a/ibis/ibisstandardreply.h b/ibis/ibisstandardreply.h
new file mode 100644
--- /dev/null
+++ b/ibis/ibisstandardreply.h
@@ -0,0 +1,152 @@
+/*
+ * Ibis - IRCv3 Library
+ * Copyright (C) 2022-2024 Ibis Developers
+ *
+ * Ibis 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(IBIS_GLOBAL_HEADER_INSIDE) && !defined(IBIS_COMPILATION)
+# error "only <ibis.h> may be included directly"
+#endif
+
+#ifndef IBIS_STANDARD_REPLY_H
+#define IBIS_STANDARD_REPLY_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "ibisversion.h"
+
+G_BEGIN_DECLS
+
+#define IBIS_TYPE_STANDARD_REPLY (ibis_standard_reply_get_type())
+
+/**
+ * IbisStandardReply:
+ *
+ * An object that represents a Standard Reply.
+ *
+ * See [Standard Replies](https://ircv3.net/specs/extensions/standard-replies)
+ * for more information.
+ *
+ * Since: 0.8
+ */
+IBIS_AVAILABLE_IN_0_8
+G_DECLARE_FINAL_TYPE(IbisStandardReply, ibis_standard_reply, IBIS,
+                     STANDARD_REPLY, GObject)
+
+#include "ibismessage.h"
+
+/**
+ * ibis_standard_reply_get_code:
+ * @reply: The instance.
+ *
+ * Gets the code from @reply.
+ *
+ * Returns: The machine-readable code from @reply.
+ *
+ * Since: 0.8
+ */
+IBIS_AVAILABLE_IN_0_8
+const char *ibis_standard_reply_get_code(IbisStandardReply *reply);
+
+/**
+ * ibis_standard_reply_get_command:
+ * @reply: The instance.
+ *
+ * The command that this reply is related to.
+ *
+ * Returns: The command that this reply is related to or `*`.
+ *
+ * Since: 0.8
+ */
+IBIS_AVAILABLE_IN_0_8
+const char *ibis_standard_reply_get_command(IbisStandardReply *reply);
+
+/**
+ * ibis_standard_reply_get_context:
+ * @reply: The instance.
+ *
+ * Gets the context from @reply.
+ *
+ * Returns: (transfer none) (nullable): The parameters.
+ *
+ * Since: 0.8
+ */
+IBIS_AVAILABLE_IN_0_8
+GStrv ibis_standard_reply_get_context(IbisStandardReply *reply);
+
+/**
+ * ibis_standard_reply_get_description:
+ * @reply: The instance.
+ *
+ * The description from this reply.
+ *
+ * Returns: The description from this reply.
+ *
+ * Since: 0.8
+ */
+IBIS_AVAILABLE_IN_0_8
+const char *ibis_standard_reply_get_description(IbisStandardReply *reply);
+
+/**
+ * ibis_standard_reply_get_reply_type:
+ * @reply: The instance.
+ *
+ * Gets the type from @reply.
+ *
+ * Returns: The type.
+ *
+ * Since: 0.8
+ */
+IBIS_AVAILABLE_IN_0_8
+const char *ibis_standard_reply_get_reply_type(IbisStandardReply *reply);
+
+/**
+ * ibis_standard_reply_is_type:
+ * @reply: The instance.
+ * @type: The type to check.
+ *
+ * Checks if the type of @reply is @type.
+ *
+ * Returns: %TRUE if the type of @reply is @type.
+ *
+ * Since: 0.8
+ */
+IBIS_AVAILABLE_IN_0_8
+gboolean ibis_standard_reply_is_type(IbisStandardReply *reply, const char *type);
+
+/**
+ * ibis_standard_reply_parse:
+ * @message: The message.
+ *
+ * Attempts to create a standard reply from @message.
+ *
+ * If @str is not a valid standard reply, %NULL will be returned.
+ *
+ * Returns: (transfer full) (nullable): The standard reply if successful,
+ *          otherwise %NULL.
+ *
+ * Since: 0.8
+ */
+IBIS_AVAILABLE_IN_0_8
+IbisStandardReply *ibis_standard_reply_parse(IbisMessage *message);
+
+G_END_DECLS
+
+#endif /* IBIS_STANDARD_REPLY_H */
diff --git a/ibis/meson.build b/ibis/meson.build
--- a/ibis/meson.build
+++ b/ibis/meson.build
@@ -8,6 +8,7 @@
   'ibisnormalize.c',
   'ibissasl.c',
   'ibissource.c',
+  'ibisstandardreply.c',
   'ibistags.c',
   'ibisversion.c',
 ]
@@ -24,6 +25,7 @@
   'ibisnormalize.h',
   'ibissasl.h',
   'ibissource.h',
+  'ibisstandardreply.h',
   'ibisstring.h',
   'ibistags.h',
   'ibisversion.h',
diff --git a/ibis/tests/meson.build b/ibis/tests/meson.build
--- a/ibis/tests/meson.build
+++ b/ibis/tests/meson.build
@@ -7,6 +7,7 @@
   'normalize',
   'parser',
   'source',
+  'standard_reply',
   'tags',
 ]
 
diff --git a/ibis/tests/test_standard_reply.c b/ibis/tests/test_standard_reply.c
new file mode 100644
--- /dev/null
+++ b/ibis/tests/test_standard_reply.c
@@ -0,0 +1,252 @@
+/*
+ * Ibis - IRCv3 Library
+ * Copyright (C) 2022-2024 Ibis Developers
+ *
+ * Ibis 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 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 <ibis.h>
+
+typedef struct {
+	char *reply_type;
+	char *command;
+	char *code;
+	const char * const context[16];
+	char *description;
+	gboolean error;
+} TestIbisStandardReplyData;
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+test_ibis_standard_reply_parse(const char *raw_message,
+                               TestIbisStandardReplyData *data)
+{
+	IbisMessage *message = NULL;
+	IbisStandardReply *reply = NULL;
+	GError *error = NULL;
+	GStrv values = NULL;
+	const char *value = NULL;
+
+	message = ibis_message_parse(raw_message, &error);
+	g_assert_no_error(error);
+	g_assert_true(IBIS_IS_MESSAGE(message));
+
+	reply = ibis_message_get_standard_reply(message);
+	if(data->error) {
+		g_assert_null(reply);
+
+		g_assert_finalize_object(message);
+
+		return;
+	}
+
+	g_assert_true(IBIS_IS_STANDARD_REPLY(reply));
+
+	value = ibis_standard_reply_get_reply_type(reply);
+	g_assert_cmpstr(value, ==, data->reply_type);
+
+	g_assert_true(ibis_standard_reply_is_type(reply, data->reply_type));
+
+	value = ibis_standard_reply_get_command(reply);
+	g_assert_cmpstr(value, ==, data->command);
+
+	value = ibis_standard_reply_get_code(reply);
+	g_assert_cmpstr(value, ==, data->code);
+
+	values = ibis_standard_reply_get_context(reply);
+	g_assert_cmpstrv(values, data->context);
+
+	value = ibis_standard_reply_get_description(reply);
+	g_assert_cmpstr(value, ==, data->description);
+
+	g_assert_finalize_object(message);
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_ibis_standard_reply_parse_privmsg(void) {
+	TestIbisStandardReplyData data = {
+		.error = TRUE,
+	};
+
+	test_ibis_standard_reply_parse(":irene PRIVMSG #ibis :Hiya!", &data);
+}
+
+static void
+test_ibis_standard_reply_parse_malformed(void) {
+	TestIbisStandardReplyData data = {
+		.error = TRUE,
+	};
+
+	/* Only has type. */
+	test_ibis_standard_reply_parse("FAIL", &data);
+
+	/* Missing code. */
+	test_ibis_standard_reply_parse("FAIL *", &data);
+
+	/* Missing description. */
+	test_ibis_standard_reply_parse("FAIL * CODE", &data);
+}
+
+static void
+test_ibis_standard_reply_parse_fail_need_registration(void) {
+	TestIbisStandardReplyData data = {
+		.reply_type = IBIS_STANDARD_REPLY_TYPE_FAIL,
+		.command = "*",
+		.code = "NEED_REGISTRATION",
+		.description = "You need to be registered to continue",
+	};
+
+	test_ibis_standard_reply_parse("FAIL * NEED_REGISTRATION :You need to be registered to continue",
+	                               &data);
+}
+
+static void
+test_ibis_standard_reply_parse_note_account_passphrase_updated(void) {
+	TestIbisStandardReplyData data = {
+		.reply_type = IBIS_STANDARD_REPLY_TYPE_NOTE,
+		.command = "AUTHENTICATE",
+		.code = "ACCOUNT_PASSPHRASE_UPDATED",
+		.description = "Your passphrase hash has been automatically updated to use our new, more secure method",
+	};
+
+	test_ibis_standard_reply_parse("NOTE AUTHENTICATE ACCOUNT_PASSPHRASE_UPDATED :Your passphrase hash has been automatically updated to use our new, more secure method",
+	                               &data);
+}
+
+static void
+test_ibis_standard_reply_parse_fail_reg_invalid_callback(void) {
+	TestIbisStandardReplyData data = {
+		.reply_type = IBIS_STANDARD_REPLY_TYPE_FAIL,
+		.command = "ACC",
+		.code = "REG_INVALID_CALLBACK",
+		.context = {"REGISTER"},
+		.description = "Email address is not valid",
+	};
+
+	test_ibis_standard_reply_parse("FAIL ACC REG_INVALID_CALLBACK REGISTER :Email address is not valid",
+	                               &data);
+}
+
+static void
+test_ibis_standard_reply_parse_fail_boxes_invalid(void) {
+	TestIbisStandardReplyData data = {
+		.reply_type = IBIS_STANDARD_REPLY_TYPE_FAIL,
+		.command = "BOX",
+		.code = "BOXES_INVALID",
+		.context = {"STACK", "CLOCKWISE"},
+		.description = "Given boxes are not supported",
+	};
+
+	test_ibis_standard_reply_parse("FAIL BOX BOXES_INVALID STACK CLOCKWISE :Given boxes are not supported",
+	                               &data);
+}
+
+static void
+test_ibis_standard_reply_parse_fail_config_bad(void) {
+	TestIbisStandardReplyData data = {
+		.reply_type = IBIS_STANDARD_REPLY_TYPE_FAIL,
+		.command = "REHASH",
+		.code = "CONFIG_BAD",
+		.description = "Could not reload config from disk",
+	};
+
+	test_ibis_standard_reply_parse("FAIL REHASH CONFIG_BAD :Could not reload config from disk",
+	                               &data);
+}
+
+static void
+test_ibis_standard_reply_parse_warn_certs_expired(void) {
+	TestIbisStandardReplyData data = {
+		.reply_type = IBIS_STANDARD_REPLY_TYPE_WARN,
+		.command = "REHASH",
+		.code = "CERTS_EXPIRED",
+		.description = "Certificate [blahblah.irc.example.com] has expired",
+	};
+
+	test_ibis_standard_reply_parse("WARN REHASH CERTS_EXPIRED :Certificate [blahblah.irc.example.com] has expired",
+	                               &data);
+}
+
+static void
+test_ibis_standard_reply_parse_note_oper_message(void) {
+	TestIbisStandardReplyData data = {
+		.reply_type = IBIS_STANDARD_REPLY_TYPE_NOTE,
+		.command = "*",
+		.code = "OPER_MESSAGE",
+		.description = "Registering new accounts and channels has been disabled temporarily while we deal with the spam. Thanks for flying ExampleNet! -dan",
+	};
+
+	test_ibis_standard_reply_parse("NOTE * OPER_MESSAGE :Registering new accounts and channels has been disabled temporarily while we deal with the spam. Thanks for flying ExampleNet! -dan",
+	                               &data);
+}
+
+static void
+test_ibis_standard_reply_parse_fail_account_required_to_connect(void) {
+	TestIbisStandardReplyData data = {
+		.reply_type = IBIS_STANDARD_REPLY_TYPE_FAIL,
+		.command = "*",
+		.code = "ACCOUNT_REQUIRED_TO_CONNECT",
+		.description = "An account is required to connect to the network",
+	};
+
+	test_ibis_standard_reply_parse("FAIL * ACCOUNT_REQUIRED_TO_CONNECT :An account is required to connect to the network",
+	                               &data);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+	g_test_set_nonfatal_assertions();
+
+	g_test_add_func("/ibis/standard-reply/parse/privmsg",
+	                test_ibis_standard_reply_parse_privmsg);
+	g_test_add_func("/ibis/standard-reply/parse/malformed",
+	                test_ibis_standard_reply_parse_malformed);
+
+	/* These tests are based on the examples in the specification at
+	 * https://ircv3.net/specs/extensions/standard-replies.
+	 */
+	g_test_add_func("/ibis/standard-reply/parse/fail-need-registration",
+	                test_ibis_standard_reply_parse_fail_need_registration);
+	g_test_add_func("/ibis/standard-reply/parse/node-account-passphrase-updated",
+	                test_ibis_standard_reply_parse_note_account_passphrase_updated);
+	g_test_add_func("/ibis/standard-reply/parse/fail-reg-invalid-callback",
+	                test_ibis_standard_reply_parse_fail_reg_invalid_callback);
+	g_test_add_func("/ibis/standard-reply/parse/fail-boxes-invalid",
+	                test_ibis_standard_reply_parse_fail_boxes_invalid);
+	g_test_add_func("/ibis/standard-reply/parse/fail-config-bad",
+	                test_ibis_standard_reply_parse_fail_config_bad);
+	g_test_add_func("/ibis/standard-reply/parse/warn-certs-expired",
+	                test_ibis_standard_reply_parse_warn_certs_expired);
+	g_test_add_func("/ibis/standard-reply/parse/note-oper-message",
+	                test_ibis_standard_reply_parse_note_oper_message);
+	g_test_add_func("/ibis/standard-reply/parse/fail-account-required-to-connect",
+	                test_ibis_standard_reply_parse_fail_account_required_to_connect);
+
+	return g_test_run();
+}
