# HG changeset patch
# Node ID 299f72e44bc23e1d489ab774e83b17502d0e5a91
# Parent  72863b9d1ddaea1fa7cdfd937f96b8a2545dc1dc
diff --git a/ibis/ibisconstants.h b/ibis/ibisconstants.h
--- a/ibis/ibisconstants.h
+++ b/ibis/ibisconstants.h
@@ -1116,6 +1116,19 @@
 #define IBIS_FEATURE_CHANTYPES "CHANTYPES" IBIS_AVAILABLE_MACRO_IN_0_3
 
 /**
+ * IBIS_FEATURE_CLIENTTAGDENY:
+ *
+ * A constant for the `CLIENTTAGDENY` feature.
+ *
+ * See
+ * [ircv3.net/specs/extensions/message-tags#rpl_isupport-tokens](https://ircv3.net/specs/extensions/message-tags#rpl_isupport-tokens)
+ * for more information.
+ *
+ * Since: 0.6
+ */
+#define IBIS_FEATURE_CLIENTTAGDENY "CLIENTTAGDENY" IBIS_AVAILABLE_MACRO_IN_0_6
+
+/**
  * IBIS_FEATURE_ELIST:
  *
  * A constant for the `ELIST` feature.
diff --git a/ibis/ibisfeatures.h b/ibis/ibisfeatures.h
--- a/ibis/ibisfeatures.h
+++ b/ibis/ibisfeatures.h
@@ -60,6 +60,21 @@
 void ibis_features_clear(IbisFeatures *features);
 
 /**
+ * ibis_features_client_tag_denied:
+ * @features: The instance.
+ * @tag: The tag to query.
+ *
+ * Query the CLIENTTAGDENY parameter from @features.
+ * See [const@FEATURE_CLIENTTAGDENY].
+ *
+ * Returns: %TRUE if @tag is a blocked client-only tag, otherwise %FALSE.
+ *
+ * Since: 0.6
+ */
+IBIS_AVAILABLE_IN_0_6
+gboolean ibis_features_client_tag_denied(IbisFeatures *features, const char *tag);
+
+/**
  * ibis_features_new:
  *
  * Create a new features object.
diff --git a/ibis/ibisfeatures.c b/ibis/ibisfeatures.c
--- a/ibis/ibisfeatures.c
+++ b/ibis/ibisfeatures.c
@@ -164,6 +164,8 @@
 		                       ibis_str_to_uint(value));
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_CHANTYPES)) {
 		ibis_features_set_string(features, IBIS_FEATURE_CHANTYPES, value);
+	} else if(ibis_str_equal(feature, IBIS_FEATURE_CLIENTTAGDENY)) {
+		ibis_features_set_string(features, IBIS_FEATURE_CLIENTTAGDENY, value);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_ELIST)) {
 		ibis_features_set_string(features, IBIS_FEATURE_ELIST, value);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_EXCEPTS)) {
@@ -318,13 +320,86 @@
 		              g_quark_to_string(detail));
 	}
 
-	/* set the default values specified at
+	/* Set the default values specified at
 	 * https://modern.ircdocs.horse/#rplisupport-parameters
 	 */
 	ibis_features_set_string(features, IBIS_FEATURE_CHANTYPES, "#");
 	ibis_features_set_string(features, IBIS_FEATURE_PREFIX, "(ov)@+");
 }
 
+gboolean
+ibis_features_client_tag_denied(IbisFeatures *features, const char *tag) {
+	const char *clienttagdenied = NULL;
+	gboolean denied = FALSE;
+	char **denied_tags = NULL;
+	guint n_denied_tags = 0;
+
+	g_return_val_if_fail(IBIS_IS_FEATURES(features), FALSE);
+	g_return_val_if_fail(!ibis_str_is_empty(tag), FALSE);
+
+	clienttagdenied = ibis_features_get_string(features,
+	                                           IBIS_FEATURE_CLIENTTAGDENY);
+	/* An empty or missing CLIENTTAGDENY matches the default case and indicates
+	 * that all client-only tags are allowed.
+	 */
+	if(ibis_str_is_empty(clienttagdenied)) {
+		return FALSE;
+	}
+
+	/* An asterisk * (0x2A) indicates that all client-only tags are blocked. */
+	if(ibis_str_equal(clienttagdenied, "*")) {
+		return TRUE;
+	}
+
+	/* The CLIENTTAGDENY token value is a comma , (0x2C) separated list of blocked
+	 * client-only tags. The client-only prefix (+) is omitted when a tag appears
+	 * in this list.
+	 */
+	denied_tags = g_strsplit(clienttagdenied, ",", 0);
+	n_denied_tags = g_strv_length(denied_tags);
+
+	if(n_denied_tags == 1) {
+		denied = ibis_str_equal(denied_tags[0], tag);
+		g_strfreev(denied_tags);
+
+		return denied;
+	}
+
+	if(ibis_str_equal(denied_tags[0], "*")) {
+		/* If the first denied tag is '*', subsequent tags are expected to start with
+		 * a hyphen to exempt particular tags from the catch-all.
+		 */
+		denied = TRUE;
+
+		for(guint i = 1; i < n_denied_tags; i++) {
+			const char *allowed_tag = NULL;
+
+			if(denied_tags[i][0] != '-') {
+				continue;
+			}
+
+			allowed_tag = denied_tags[i] + 1;
+			if(ibis_str_equal(allowed_tag, tag)) {
+				denied = FALSE;
+				break;
+			}
+		}
+	} else {
+		denied = FALSE;
+
+		for(guint i = 0; i < n_denied_tags; i++) {
+			if(ibis_str_equal(denied_tags[i], tag)) {
+				denied = TRUE;
+				break;
+			}
+		}
+	}
+
+	g_strfreev(denied_tags);
+
+	return denied;
+}
+
 IbisFeatures *
 ibis_features_new(void) {
 	return g_object_new(IBIS_TYPE_FEATURES, NULL);
diff --git a/ibis/ibisversion.h b/ibis/ibisversion.h
--- a/ibis/ibisversion.h
+++ b/ibis/ibisversion.h
@@ -245,6 +245,30 @@
 #define IBIS_AVAILABLE_TYPE_IN_0_5
 #endif
 
+/**
+ * IBIS_VERSION_0_6:
+ *
+ * A macro that evaluates to the 0.6 version of ibis, in a format that can be
+ * used by the C pre-processor.
+ *
+ * Since: 0.6
+ */
+#define IBIS_VERSION_0_6 (G_ENCODE_VERSION(0, 6))
+
+#if IBIS_VERSION_MAX_ALLOWED < IBIS_VERSION_0_6
+#define IBIS_AVAILABLE_IN_0_6 IBIS_UNAVAILABLE(0, 6)
+#define IBIS_AVAILABLE_STATIC_INLINE_IN_0_6 IBIS_UNAVAILABLE_STATIC_INLINE(0, 6)
+#define IBIS_AVAILABLE_MACRO_IN_0_6 IBIS_UNAVAILABLE_MACRO(0, 6)
+#define IBIS_AVAILABLE_ENUMERATOR_IN_0_6 IBIS_UNAVAILABLE_ENUMERATOR(0, 6)
+#define IBIS_AVAILABLE_TYPE_IN_0_6 IBIS_UNAVAILABLE_TYPE(0, 6)
+#else
+#define IBIS_AVAILABLE_IN_0_6 _IBIS_EXTERN
+#define IBIS_AVAILABLE_STATIC_INLINE_IN_0_6
+#define IBIS_AVAILABLE_MACRO_IN_0_6
+#define IBIS_AVAILABLE_ENUMERATOR_IN_0_6
+#define IBIS_AVAILABLE_TYPE_IN_0_6
+#endif
+
 G_BEGIN_DECLS
 
 /**
diff --git a/ibis/tests/test_features.c b/ibis/tests/test_features.c
--- a/ibis/tests/test_features.c
+++ b/ibis/tests/test_features.c
@@ -262,6 +262,96 @@
 	g_assert_finalize_object(features);
 }
 
+static void
+test_ibis_features_client_tag_deny_none(void) {
+	IbisMessage *message = NULL;
+	IbisFeatures *features = NULL;
+
+	features = ibis_features_new();
+	message = ibis_message_new(IBIS_RPL_ISUPPORT);
+
+	g_assert_false(ibis_features_client_tag_denied(features, "foo"));
+
+	ibis_message_set_params(message, "CLIENTTAGDENY=", NULL);
+	ibis_features_parse(features, message);
+
+	g_assert_false(ibis_features_client_tag_denied(features, "foo"));
+
+	g_assert_finalize_object(message);
+	g_assert_finalize_object(features);
+}
+
+static void
+test_ibis_features_client_tag_deny_single(void) {
+	IbisMessage *message = NULL;
+	IbisFeatures *features = NULL;
+
+	features = ibis_features_new();
+	message = ibis_message_new(IBIS_RPL_ISUPPORT);
+	ibis_message_set_params(message, "CLIENTTAGDENY=foo", NULL);
+	ibis_features_parse(features, message);
+
+	g_assert_true(ibis_features_client_tag_denied(features, "foo"));
+	g_assert_false(ibis_features_client_tag_denied(features, "bar"));
+
+	g_assert_finalize_object(message);
+	g_assert_finalize_object(features);
+}
+
+static void
+test_ibis_features_client_tag_deny_multiple(void) {
+	IbisMessage *message = NULL;
+	IbisFeatures *features = NULL;
+
+	features = ibis_features_new();
+	message = ibis_message_new(IBIS_RPL_ISUPPORT);
+	ibis_message_set_params(message, "CLIENTTAGDENY=foo,example/bar", NULL);
+	ibis_features_parse(features, message);
+
+	g_assert_true(ibis_features_client_tag_denied(features, "foo"));
+	g_assert_true(ibis_features_client_tag_denied(features, "example/bar"));
+	g_assert_false(ibis_features_client_tag_denied(features, "bar"));
+
+	g_assert_finalize_object(message);
+	g_assert_finalize_object(features);
+}
+
+static void
+test_ibis_features_client_tag_deny_all(void) {
+	IbisMessage *message = NULL;
+	IbisFeatures *features = NULL;
+
+	features = ibis_features_new();
+	message = ibis_message_new(IBIS_RPL_ISUPPORT);
+	ibis_message_set_params(message, "CLIENTTAGDENY=*", NULL);
+	ibis_features_parse(features, message);
+
+	g_assert_true(ibis_features_client_tag_denied(features, "foo"));
+	g_assert_true(ibis_features_client_tag_denied(features, "example/bar"));
+	g_assert_true(ibis_features_client_tag_denied(features, "bar"));
+
+	g_assert_finalize_object(message);
+	g_assert_finalize_object(features);
+}
+
+static void
+test_ibis_features_client_tag_deny_all_except_some(void) {
+	IbisMessage *message = NULL;
+	IbisFeatures *features = NULL;
+
+	features = ibis_features_new();
+	message = ibis_message_new(IBIS_RPL_ISUPPORT);
+	ibis_message_set_params(message, "CLIENTTAGDENY=*,-foo,-example/bar", NULL);
+	ibis_features_parse(features, message);
+
+	g_assert_false(ibis_features_client_tag_denied(features, "foo"));
+	g_assert_false(ibis_features_client_tag_denied(features, "example/bar"));
+	g_assert_true(ibis_features_client_tag_denied(features, "bar"));
+
+	g_assert_finalize_object(message);
+	g_assert_finalize_object(features);
+}
+
 /******************************************************************************
  * Main
  *****************************************************************************/
@@ -277,6 +367,16 @@
 	g_test_add_func("/ibis/features/parse_unknown", test_ibis_features_parse_unknown);
 	g_test_add_func("/ibis/features/no_parse", test_ibis_features_no_parse);
 	g_test_add_func("/ibis/features/clear", test_ibis_features_clear);
+	g_test_add_func("/ibis/features/client_tag_deny_none",
+	                test_ibis_features_client_tag_deny_none);
+	g_test_add_func("/ibis/features/client_tag_deny_single",
+	                test_ibis_features_client_tag_deny_single);
+	g_test_add_func("/ibis/features/client_tag_deny_multiple",
+	                test_ibis_features_client_tag_deny_multiple);
+	g_test_add_func("/ibis/features/client_tag_deny_all",
+	                test_ibis_features_client_tag_deny_all);
+	g_test_add_func("/ibis/features/client_tag_deny_all_except_some",
+	                test_ibis_features_client_tag_deny_all_except_some);
 
 	return g_test_run();
 }
diff --git a/meson.build b/meson.build
--- a/meson.build
+++ b/meson.build
@@ -2,7 +2,7 @@
   default_options : ['c_std=c17', 'warning_level=2'],
   license : 'LGPL-2.1-or-later',
   meson_version : '>=1.0.0',
-  version : '0.5.1-dev')
+  version : '0.6.0-dev')
 
 toplevel_inc = include_directories('.')
 
