# HG changeset patch
# Node ID b674fc671cc9555fe100e2060af206f1dcb488b3
# Parent  e5014327594f1cdd72ff941e326f42a9f7f98eb4
diff --git a/ibis/ibischanmodes.c b/ibis/ibischanmodes.c
new file mode 100644
--- /dev/null
+++ b/ibis/ibischanmodes.c
@@ -0,0 +1,141 @@
+/*
+ * Ibis - IRCv3 Library
+ * Copyright (C) 2022-2025 Ibis Developers <devel@pidgin.im>
+ *
+ * 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 "ibischanmodes.h"
+
+#include "ibisstring.h"
+
+struct _IbisChanmodes {
+	GObject parent;
+
+	GHashTable *cache;
+};
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static inline void
+ibis_chanmodes_add_mode(IbisChanmodes *chanmodes, char mode,
+                        IbisChanmodeType type)
+{
+	g_hash_table_insert(chanmodes->cache, GINT_TO_POINTER(mode),
+	                    GINT_TO_POINTER(type));
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_FINAL_TYPE(IbisChanmodes, ibis_chanmodes, G_TYPE_OBJECT)
+
+static void
+ibis_chanmodes_finalize(GObject *obj) {
+	IbisChanmodes *chanmodes = IBIS_CHANMODES(obj);
+
+	g_clear_pointer(&chanmodes->cache, g_hash_table_unref);
+
+	G_OBJECT_CLASS(ibis_chanmodes_parent_class)->finalize(obj);
+}
+
+static void
+ibis_chanmodes_init(IbisChanmodes *chanmodes) {
+	chanmodes->cache = g_hash_table_new(g_direct_hash, g_direct_equal);
+}
+
+static void
+ibis_chanmodes_class_init(IbisChanmodesClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = ibis_chanmodes_finalize;
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+IbisChanmodes *
+ibis_chanmodes_new(void) {
+	IbisChanmodes *chanmodes = g_object_new(IBIS_TYPE_CHANMODES, NULL);
+
+	/* This list is based on the chanmodes as defined in RFCs 1459 and 2811. */
+	ibis_chanmodes_add_mode(chanmodes, 'a', IBIS_CHANMODE_TYPE_D);
+	ibis_chanmodes_add_mode(chanmodes, 'b', IBIS_CHANMODE_TYPE_A);
+	ibis_chanmodes_add_mode(chanmodes, 'I', IBIS_CHANMODE_TYPE_A);
+	ibis_chanmodes_add_mode(chanmodes, 'i', IBIS_CHANMODE_TYPE_D);
+	ibis_chanmodes_add_mode(chanmodes, 'k', IBIS_CHANMODE_TYPE_B);
+	ibis_chanmodes_add_mode(chanmodes, 'l', IBIS_CHANMODE_TYPE_C);
+	ibis_chanmodes_add_mode(chanmodes, 'm', IBIS_CHANMODE_TYPE_D);
+	ibis_chanmodes_add_mode(chanmodes, 'n', IBIS_CHANMODE_TYPE_D);
+	ibis_chanmodes_add_mode(chanmodes, 'o', IBIS_CHANMODE_TYPE_B);
+	ibis_chanmodes_add_mode(chanmodes, 'p', IBIS_CHANMODE_TYPE_D);
+	ibis_chanmodes_add_mode(chanmodes, 'q', IBIS_CHANMODE_TYPE_D);
+	ibis_chanmodes_add_mode(chanmodes, 'r', IBIS_CHANMODE_TYPE_D);
+	ibis_chanmodes_add_mode(chanmodes, 's', IBIS_CHANMODE_TYPE_D);
+	ibis_chanmodes_add_mode(chanmodes, 't', IBIS_CHANMODE_TYPE_D);
+	ibis_chanmodes_add_mode(chanmodes, 'v', IBIS_CHANMODE_TYPE_B);
+
+	return chanmodes;
+}
+
+IbisChanmodes *
+ibis_chanmodes_parse(const char *chanmodes, const char *prefix_modes) {
+	IbisChanmodes *instance = NULL;
+	IbisChanmodeType type = IBIS_CHANMODE_TYPE_A;
+	GStrv types = NULL;
+
+	g_return_val_if_fail(!ibis_str_is_empty(chanmodes), NULL);
+	g_return_val_if_fail(!ibis_str_is_empty(prefix_modes), NULL);
+
+	instance = g_object_new(IBIS_TYPE_CHANMODES, NULL);
+
+	/* Run through the chanmodes value. */
+	types = g_strsplit(chanmodes, ",", -1);
+	for(guint i = 0; types[i] != NULL; i++) {
+		for(char *p = types[i]; *p != '\0'; p++) {
+			ibis_chanmodes_add_mode(instance, *p, type);
+		}
+
+		if(type == IBIS_CHANMODE_TYPE_D) {
+			break;
+		}
+		type += 1;
+	}
+
+	g_strfreev(types);
+
+	/* Run through the prefix modes. */
+	for(const char *p = prefix_modes; *p != '\0'; p++) {
+		ibis_chanmodes_add_mode(instance, *p, IBIS_CHANMODE_TYPE_B);
+	}
+
+	return instance;
+}
+
+IbisChanmodeType
+ibis_chanmodes_get_chanmode_type(IbisChanmodes *chanmodes, char mode) {
+	gpointer value = NULL;
+
+	g_return_val_if_fail(IBIS_IS_CHANMODES(chanmodes),
+	                     IBIS_CHANMODE_TYPE_UNKNOWN);
+
+	value = g_hash_table_lookup(chanmodes->cache, GINT_TO_POINTER(mode));
+
+	return GPOINTER_TO_INT(value);
+}
diff --git a/ibis/ibischanmodes.h b/ibis/ibischanmodes.h
new file mode 100644
--- /dev/null
+++ b/ibis/ibischanmodes.h
@@ -0,0 +1,118 @@
+/*
+ * Ibis - IRCv3 Library
+ * Copyright (C) 2022-2025 Ibis Developers <devel@pidgin.im>
+ *
+ * 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_CHANMODES_H
+#define IBIS_CHANMODES_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "ibisversion.h"
+
+G_BEGIN_DECLS
+
+/**
+ * IbisChanmodeType:
+ * @IBIS_CHANMODE_TYPE_UNKNOWN: the chanmode is unknown
+ * @IBIS_CHANMODE_TYPE_A: the chanmode modifies a list
+ * @IBIS_CHANMODE_TYPE_B: the chanmode always has a parameter
+ * @IBIS_CHANMODE_TYPE_C: the chanmode only has a parameter when setting
+ * @IBIS_CHANMODE_TYPE_D: the chanmode never has a parameter
+ *
+ * A type of a specific chanmode.
+ *
+ * See https://modern.ircdocs.horse/#channel-mode for more information.
+ *
+ * Since: 0.12
+ */
+IBIS_AVAILABLE_TYPE_IN_0_12
+typedef enum {
+	IBIS_CHANMODE_TYPE_UNKNOWN IBIS_AVAILABLE_ENUMERATOR_IN_0_12,
+	IBIS_CHANMODE_TYPE_A IBIS_AVAILABLE_ENUMERATOR_IN_0_12,
+	IBIS_CHANMODE_TYPE_B IBIS_AVAILABLE_ENUMERATOR_IN_0_12,
+	IBIS_CHANMODE_TYPE_C IBIS_AVAILABLE_ENUMERATOR_IN_0_12,
+	IBIS_CHANMODE_TYPE_D IBIS_AVAILABLE_ENUMERATOR_IN_0_12,
+} IbisChanmodeType;
+
+#define IBIS_TYPE_CHANMODES (ibis_chanmodes_get_type())
+
+/**
+ * IbisChanmodes:
+ *
+ * A helper for handling channel modes.
+ *
+ * Each specific channel mode has its own rules when it comes to whether or not
+ * it has a parameter. This object helps you determine when a channel mode
+ * requires a parameter or not.
+ *
+ * Since: 0.12
+ */
+IBIS_AVAILABLE_IN_0_12
+G_DECLARE_FINAL_TYPE(IbisChanmodes, ibis_chanmodes, IBIS, CHANMODES, GObject)
+
+/**
+ * ibis_chanmodes_new:
+ *
+ * Creates a new instance based on the values from RFCs 1459 and 2811.
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 0.12
+ */
+IBIS_AVAILABLE_IN_0_12
+IbisChanmodes *ibis_chanmodes_new(void);
+
+/**
+ * ibis_chanmodes_parse:
+ * @chanmodes: the chanmodes value from the ISUPPORT reply
+ * @prefix_modes: the modes from the PREFIX
+ * Parses a CHANMODES string.
+ *
+ * This will parse @chanmodes and create a new lookup for it.
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 0.12
+ */
+IBIS_AVAILABLE_IN_0_12
+IbisChanmodes *ibis_chanmodes_parse(const char *chanmodes, const char *prefix_modes);
+
+/**
+ * ibis_chanmodes_get_chanmode_type:
+ * @mode: the mode to check
+ *
+ * Gets the chanmode type for the given mode.
+ *
+ * Returns: The type of the mode.
+ *
+ * Since: 0.12
+ */
+IBIS_AVAILABLE_IN_0_12
+IbisChanmodeType ibis_chanmodes_get_chanmode_type(IbisChanmodes *chanmodes, char mode);
+
+G_END_DECLS
+
+#endif /* IBIS_CHANMODES_H */
diff --git a/ibis/meson.build b/ibis/meson.build
--- a/ibis/meson.build
+++ b/ibis/meson.build
@@ -1,5 +1,6 @@
 IBIS_SOURCES = [
   'ibiscapabilities.c',
+  'ibischanmodes.c',
   'ibisclient.c',
   'ibisctcpmessage.c',
   'ibisfeatures.c',
@@ -16,6 +17,7 @@
 
 IBIS_HEADERS = [
   'ibiscapabilities.h',
+  'ibischanmodes.h',
   'ibisclient.h',
   'ibisconstants.h',
   'ibiscore.h',
@@ -71,6 +73,7 @@
 # Enums
 ###############################################################################
 IBIS_ENUM_HEADERS = [
+  'ibischanmodes.h',
 ]
 
 if IBIS_ENUM_HEADERS.length() > 0
diff --git a/ibis/tests/meson.build b/ibis/tests/meson.build
--- a/ibis/tests/meson.build
+++ b/ibis/tests/meson.build
@@ -1,4 +1,5 @@
 test_programs = [
+  'chanmodes',
   'client',
   'ctcp_message',
   'features',
diff --git a/ibis/tests/test_chanmodes.c b/ibis/tests/test_chanmodes.c
new file mode 100644
--- /dev/null
+++ b/ibis/tests/test_chanmodes.c
@@ -0,0 +1,158 @@
+/*
+ * Ibis - IRCv3 Library
+ * Copyright (C) 2022-2025 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 <birb.h>
+
+#include <ibis.h>
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_ibis_chanmodes_new(void) {
+	IbisChanmodes *chanmodes = NULL;
+	IbisChanmodeType type = IBIS_CHANMODE_TYPE_UNKNOWN;
+
+	chanmodes = ibis_chanmodes_new();
+
+	birb_assert_type(chanmodes, IBIS_TYPE_CHANMODES);
+
+	/* Verify that all of the RFC 1459 and RFC 2811 modes are there. */
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'a');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'a');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'b');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_A);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'I');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_A);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'i');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'k');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_B);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'l');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_C);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'm');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'n');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'o');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_B);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'p');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'q');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'r');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 's');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 't');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'v');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_B);
+
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, '!');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_UNKNOWN);
+
+	g_assert_finalize_object(chanmodes);
+}
+
+static void
+test_ibis_chanmodes_parse(void) {
+	IbisChanmodes *chanmodes = NULL;
+	IbisChanmodeType type = IBIS_CHANMODE_TYPE_UNKNOWN;
+
+	chanmodes = ibis_chanmodes_parse("Ibe,k,fl,CEMRUimnstu", "qaohv");
+
+	birb_assert_type(chanmodes, IBIS_TYPE_CHANMODES);
+
+	/* These are in the order of the CHANMODES string above. */
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'I');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_A);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'b');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_A);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'e');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_A);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'k');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_B);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'f');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_C);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'l');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_C);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'C');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'E');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'M');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'R');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'U');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'i');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'm');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'n');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 's');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 't');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'u');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_D);
+
+	/* Check the PREFIX modes. */
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'q');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_B);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'a');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_B);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'o');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_B);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'h');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_B);
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, 'v');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_B);
+
+	/* Make sure an unknown returns unknown. */
+	type = ibis_chanmodes_get_chanmode_type(chanmodes, '!');
+	g_assert_cmpint(type, ==, IBIS_CHANMODE_TYPE_UNKNOWN);
+
+	g_assert_finalize_object(chanmodes);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_set_nonfatal_assertions();
+
+	g_test_add_func("/ibis/chanmodes/new", test_ibis_chanmodes_new);
+	g_test_add_func("/ibis/chanmodes/parse", test_ibis_chanmodes_parse);
+
+	return g_test_run();
+}
