# HG changeset patch
# Node ID eb367af0fae6
# Parent  481749c3fd87
diff --git a/ibis/ibisconstants.h b/ibis/ibisconstants.h
--- a/ibis/ibisconstants.h
+++ b/ibis/ibisconstants.h
@@ -43,7 +43,7 @@
  *
  * Since: 0.1
  */
-#define IBIS_CTCP_DELIMITER (0x1) IBIS_AVAILABLE_MACRO_IN_0_1
+#define IBIS_CTCP_DELIMITER (001) IBIS_AVAILABLE_MACRO_IN_0_1
 
 /**
  * IBIS_CTCP_VERSION:
diff --git a/ibis/ibismessage.h b/ibis/ibismessage.h
--- a/ibis/ibismessage.h
+++ b/ibis/ibismessage.h
@@ -79,6 +79,37 @@
 const char *ibis_message_get_command(IbisMessage *message);
 
 /**
+ * ibis_message_get_ctcp:
+ * @message: The instance.
+ *
+ * Gets whether or not @message is a CTCP message.
+ *
+ * This should only ever return %TRUE if the command property of @message is
+ * `PRIVMSG` or `NOTICE`.
+ *
+ * Returns: %TRUE if @message is a CTCP message, otherwise %FALSE.
+ *
+ * Since: 0.1
+ */
+IBIS_AVAILABLE_IN_0_1
+gboolean ibis_message_get_ctcp(IbisMessage *message);
+
+/**
+ * ibis_message_set_ctcp:
+ * @message: The instance.
+ * @ctcp: Whether or not to make this a CTCP message.
+ *
+ * Sets @message to be a CTCP message or not.
+ *
+ * > Note: If the command of @message is not `PRIVMSG` or `NOTICE` this method
+ * will have no effect.
+ *
+ * Since: 0.1
+ */
+IBIS_AVAILABLE_IN_0_1
+void ibis_message_set_ctcp(IbisMessage *message, gboolean ctcp);
+
+/**
  * ibis_message_get_params:
  * @message: The instance.
  *
diff --git a/ibis/ibismessage.c b/ibis/ibismessage.c
--- a/ibis/ibismessage.c
+++ b/ibis/ibismessage.c
@@ -24,6 +24,7 @@
 
 #include "ibismessage.h"
 
+#include "ibisconstants.h"
 #include "ibisstring.h"
 
 enum {
@@ -34,6 +35,7 @@
 	PROP_RAW_MESSAGE,
 	PROP_TAGS,
 	PROP_ERROR,
+	PROP_CTCP,
 	N_PROPERTIES,
 };
 static GParamSpec *properties[N_PROPERTIES] = {NULL, };
@@ -48,6 +50,7 @@
 	char *source;
 	IbisTags *tags;
 	GError *error;
+	gboolean ctcp;
 };
 
 /******************************************************************************
@@ -64,6 +67,10 @@
 ibis_message_serialize_params(IbisMessage *message, GString *str) {
 	gboolean first = TRUE;
 
+	if(message->ctcp) {
+		g_string_append_printf(str, ":%c", IBIS_CTCP_DELIMITER);
+	}
+
 	for(guint i = 0; message->params[i] != NULL; i++) {
 		if(!first) {
 			g_string_append(str, " ");
@@ -74,13 +81,18 @@
 			const char *param = message->params[i];
 			gboolean prefix = FALSE;
 
-			/* We only need the prefix if the parameter is empty, starts with a
-			 * : or contains a space.
+			/* We only need to prepend a prefix if the message is not a CTCP
+			 * message as the prefix was already added previously.
 			 */
-			if(param[0] == '\0' || param[0] == ':') {
-				prefix = TRUE;
-			} else if(g_strstr_len(param, -1, " ") != NULL) {
-				prefix = TRUE;
+			if(!message->ctcp) {
+				/* We only need the prefix if the parameter is empty, starts
+				 * with a :, or contains a space.
+				 */
+				if(param[0] == '\0' || param[0] == ':') {
+					prefix = TRUE;
+				} else if(g_strstr_len(param, -1, " ") != NULL) {
+					prefix = TRUE;
+				}
 			}
 
 			if(prefix) {
@@ -96,6 +108,10 @@
 			first = FALSE;
 		}
 	}
+
+	if(message->ctcp) {
+		g_string_append_c(str, IBIS_CTCP_DELIMITER);
+	}
 }
 
 static GString *
@@ -261,6 +277,9 @@
 	case PROP_ERROR:
 		g_value_set_boxed(value, ibis_message_get_error(message));
 		break;
+	case PROP_CTCP:
+		g_value_set_boolean(value, ibis_message_get_ctcp(message));
+		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
 		break;
@@ -292,6 +311,9 @@
 	case PROP_ERROR:
 		ibis_message_set_error(message, g_value_get_boxed(value));
 		break;
+	case PROP_CTCP:
+		ibis_message_set_ctcp(message, g_value_get_boolean(value));
+		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
 		break;
@@ -393,6 +415,21 @@
 		G_TYPE_ERROR,
 		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * IbisMessage:ctcp:
+	 *
+	 * Whether or not the message is a CTCP message.
+	 *
+	 * This property should only be valid for messages whose command is
+	 * `PRIVMSG` or `NOTICE`.
+	 *
+	 * Since: 0.1
+	 */
+	properties[PROP_CTCP] = g_param_spec_boolean(
+		"ctcp", NULL, NULL,
+		FALSE,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
 	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
 }
 
@@ -551,6 +588,7 @@
 	char *source = NULL;
 	char *tags_string = NULL;
 	char *trailing = NULL;
+	guint n_params = 0;
 
 	g_return_val_if_fail(buffer != NULL, FALSE);
 
@@ -613,7 +651,45 @@
 	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 = ibis_message_build_params(middle, coda, trailing, NULL);
+	params = ibis_message_build_params(middle, coda, trailing, &n_params);
+
+	/* If the command is PRIVMSG or NOTICE, we need to check if the body, the
+	 * last parameter, starts with \001 which would make it a CTCP message. If
+	 * so, we need to remove the leading and optional trailing \001 from the
+	 * body and set the CTCP property of the message.
+	 */
+	if(ibis_str_equal(command, IBIS_MSG_PRIVMSG) ||
+	   ibis_str_equal(command, IBIS_MSG_NOTICE))
+	{
+		if(n_params >= 1) {
+			char *body = NULL;
+			guint last = n_params - 1;
+
+			body = params[last];
+			if(body[0] == IBIS_CTCP_DELIMITER) {
+				char *clean = NULL;
+				gsize length = 0;
+
+				length = strlen(body);
+				if(body[length - 1] == IBIS_CTCP_DELIMITER) {
+					/* If the last character is a delimiter, copy the body
+					 * skipping both leading and trailing delimiters.
+					 */
+					clean = g_strndup(body + 1, length - 2);
+				} else {
+					/* Skip the leading delimiter. */
+					clean = g_strdup(body + 1);
+				}
+
+				/* Replace the old value with the cleaned value. */
+				g_free(params[last]);
+				params[last] = clean;
+
+				ibis_message_set_ctcp(message, TRUE);
+			}
+		}
+	}
+
 	if(params != NULL) {
 		ibis_message_set_paramsv(message, params);
 	}
@@ -638,3 +714,27 @@
 
 	return message->raw_message;
 }
+
+gboolean
+ibis_message_get_ctcp(IbisMessage *message) {
+	g_return_val_if_fail(IBIS_IS_MESSAGE(message), FALSE);
+
+	return message->ctcp;
+}
+
+void
+ibis_message_set_ctcp(IbisMessage *message, gboolean ctcp) {
+	g_return_if_fail(IBIS_IS_MESSAGE(message));
+
+	if(!ibis_str_equal(message->command, IBIS_MSG_PRIVMSG) &&
+	   !ibis_str_equal(message->command, IBIS_MSG_NOTICE))
+	{
+		return;
+	}
+
+	if(message->ctcp != ctcp) {
+		message->ctcp = ctcp;
+
+		g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_CTCP]);
+	}
+}
diff --git a/ibis/tests/test_message.c b/ibis/tests/test_message.c
--- a/ibis/tests/test_message.c
+++ b/ibis/tests/test_message.c
@@ -130,6 +130,45 @@
 }
 
 static void
+test_ibis_message_set_ctcp_privmsg(void) {
+	IbisMessage *message = NULL;
+
+	message = ibis_message_new(IBIS_MSG_PRIVMSG);
+
+	g_assert_false(ibis_message_get_ctcp(message));
+	ibis_message_set_ctcp(message, TRUE);
+	g_assert_true(ibis_message_get_ctcp(message));
+
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_set_ctcp_notice(void) {
+	IbisMessage *message = NULL;
+
+	message = ibis_message_new(IBIS_MSG_NOTICE);
+
+	g_assert_false(ibis_message_get_ctcp(message));
+	ibis_message_set_ctcp(message, TRUE);
+	g_assert_true(ibis_message_get_ctcp(message));
+
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_set_ctcp_other(void) {
+	IbisMessage *message = NULL;
+
+	message = ibis_message_new("OTHER");
+
+	g_assert_false(ibis_message_get_ctcp(message));
+	ibis_message_set_ctcp(message, TRUE);
+	g_assert_false(ibis_message_get_ctcp(message));
+
+	g_assert_finalize_object(message);
+}
+
+static void
 test_ibis_message_set_params_variadic(void) {
 	IbisMessage *message = NULL;
 	GStrv params = NULL;
@@ -259,6 +298,79 @@
 	g_assert_finalize_object(message);
 }
 
+static void
+test_ibis_message_serialize_ctcp_no_params(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("PRIVMSG");
+	ibis_message_set_ctcp(message, TRUE);
+	ibis_message_set_params(message, "VERSION", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==, "PRIVMSG :\001VERSION\001\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_serialize_ctcp_single_param(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("PRIVMSG");
+	ibis_message_set_ctcp(message, TRUE);
+	ibis_message_set_params(message, "ACTION",
+	                        "slaps grim with a large trout", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==,
+	                "PRIVMSG :\001ACTION slaps grim with a large trout\001\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_serialize_ctcp_multiple_params(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("PRIVMSG");
+	ibis_message_set_ctcp(message, TRUE);
+	ibis_message_set_params(message, "DCC", "SEND", "pidgin.ico", "::1", "0",
+	                        NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==,
+	                "PRIVMSG :\001DCC SEND pidgin.ico ::1 0\001\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
+static void
+test_ibis_message_serialize_ctcp_mixed_params(void) {
+	IbisMessage *message = NULL;
+	char *serialized = NULL;
+
+	message = ibis_message_new("PRIVMSG");
+	ibis_message_set_ctcp(message, TRUE);
+	ibis_message_set_params(message, "DCC", "SEND", "pidgin.ico ::1 0", NULL);
+
+	serialized = ibis_message_serialize_to_string(message);
+
+	g_assert_cmpstr(serialized, ==,
+	                "PRIVMSG :\001DCC SEND pidgin.ico ::1 0\001\r\n");
+
+	g_clear_pointer(&serialized, g_free);
+	g_assert_finalize_object(message);
+}
+
 /******************************************************************************
  * irc-docs tests
  *****************************************************************************/
@@ -603,6 +715,13 @@
 	g_test_add_func("/ibis/message/properties", test_ibis_message_properties);
 	g_test_add_func("/ibis/message/take-error", test_ibis_message_take_error);
 
+	g_test_add_func("/ibis/message/set_ctcp/privmsg",
+	                test_ibis_message_set_ctcp_privmsg);
+	g_test_add_func("/ibis/message/set_ctcp/notice",
+	                test_ibis_message_set_ctcp_notice);
+	g_test_add_func("/ibis/message/set_ctcp/other",
+	                test_ibis_message_set_ctcp_other);
+
 	g_test_add_func("/ibis/message/set-params-variadic",
 	                test_ibis_message_set_params_variadic);
 
@@ -620,6 +739,14 @@
 	                test_ibis_message_serialize_parameters_last_starts_with_colon);
 	g_test_add_func("/ibis/message/serialize/parameters/last_contains_space",
 	                test_ibis_message_serialize_parameters_last_contains_space);
+	g_test_add_func("/ibis/message/serialize/ctcp/no-params",
+	                test_ibis_message_serialize_ctcp_no_params);
+	g_test_add_func("/ibis/message/serialize/ctcp/single-param",
+	                test_ibis_message_serialize_ctcp_single_param);
+	g_test_add_func("/ibis/message/serialize/ctcp/multiple-params",
+	                test_ibis_message_serialize_ctcp_multiple_params);
+	g_test_add_func("/ibis/message/serialize/ctcp/mixed-params",
+	                test_ibis_message_serialize_ctcp_mixed_params);
 
 	/* These tests are based on the msg-join tests from
 	 * https://github.com/ircdocs/parser-tests/blob/master/tests/msg-join.yaml
diff --git a/ibis/tests/test_parser.c b/ibis/tests/test_parser.c
--- a/ibis/tests/test_parser.c
+++ b/ibis/tests/test_parser.c
@@ -32,6 +32,7 @@
 	char *command;
 	guint n_params;
 	const char * const params[16];
+	gboolean ctcp;
 } TestIbisParserData;
 
 /******************************************************************************
@@ -60,6 +61,10 @@
 	source = ibis_message_get_source(message);
 	g_assert_cmpstr(source, ==, d->source);
 
+	if(d->ctcp) {
+		g_assert_true(ibis_message_get_ctcp(message));
+	}
+
 	/* Make sure we have an expected tags hash table before checking them. */
 	if(d->tags != NULL) {
 		IbisTags *tags = NULL;
@@ -90,6 +95,57 @@
 }
 
 /******************************************************************************
+ * CTCP Tests
+ *****************************************************************************/
+static void
+test_ibis_parser_ctcp_privmsg(void) {
+	TestIbisParserData data = {
+		.command = IBIS_MSG_PRIVMSG,
+		.n_params = 1,
+		.params = {IBIS_CTCP_VERSION},
+		.ctcp = TRUE,
+	};
+
+	test_ibis_parser("PRIVMSG :\001VERSION\001", &data);
+}
+
+static void
+test_ibis_parser_ctcp_notice(void) {
+	TestIbisParserData data = {
+		.command = IBIS_MSG_NOTICE,
+		.n_params = 1,
+		.params = {IBIS_CTCP_VERSION},
+		.ctcp = TRUE,
+	};
+
+	test_ibis_parser("NOTICE :\001VERSION\001", &data);
+}
+
+static void
+test_ibis_parser_ctcp_other(void) {
+	TestIbisParserData data = {
+		.command = "OTHER",
+		.n_params = 1,
+		.params = {"\001VERSION\001"},
+		.ctcp = FALSE,
+	};
+
+	test_ibis_parser("OTHER :\001VERSION\001", &data);
+}
+
+static void
+test_ibis_parser_ctcp_no_trailing(void) {
+	TestIbisParserData data = {
+		.command = IBIS_MSG_PRIVMSG,
+		.n_params = 1,
+		.params = {IBIS_CTCP_VERSION},
+		.ctcp = TRUE,
+	};
+
+	test_ibis_parser("PRIVMSG :\001VERSION", &data);
+}
+
+/******************************************************************************
  * Tests
  *****************************************************************************/
 static void
@@ -674,6 +730,13 @@
 main(gint argc, gchar *argv[]) {
 	g_test_init(&argc, &argv, NULL);
 
+	g_test_add_func("/ibis/parser/ctcp/privmsg",
+	                test_ibis_parser_ctcp_privmsg);
+	g_test_add_func("/ibis/parser/ctcp/notice", test_ibis_parser_ctcp_notice);
+	g_test_add_func("/ibis/parser/ctcp/other", test_ibis_parser_ctcp_other);
+	g_test_add_func("/ibis/parser/ctcp/no-trailing",
+	                test_ibis_parser_ctcp_no_trailing);
+
 	g_test_add_func("/ibis/parser/error", test_ibis_parser_error);
 
 	/* These tests are based on the msg-split tests from
