# HG changeset patch
# Node ID ccc967f91c18
# Parent  b9ef2851fdc0
diff --git a/libpurple/protocols/ircv3/README.md b/libpurple/protocols/ircv3/README.md
--- a/libpurple/protocols/ircv3/README.md
+++ b/libpurple/protocols/ircv3/README.md
@@ -16,5 +16,6 @@
 keep this list up to date, but if you notice we've missed something please let
 us know!
 
+* cap-notify
 * sasl (right now just PLAIN works)
-* cap-notify
+* message-tags
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
@@ -85,6 +85,26 @@
 const char *purple_ircv3_capabilities_lookup(PurpleIRCv3Capabilities *capabilities, const char *name, gboolean *found);
 
 /**
+ * purple_ircv3_capabilities_lookup_and_request:
+ * @capabilities: The instance.
+ * @name: The name of the capability to look for.
+ *
+ * A helper function to call [method@PurpleIRCv3.Capabilities.Lookup] and if
+ * found, call [method@PurpleIRCv3.Capabilities.Request].
+ *
+ * This method ignores the advertised value, so to get that you'll need to call
+ * [method@PurpleIRCv3.Capabilities.Lookup] yourself.
+ *
+ * Also if you need to do something when the server ACK's or NAK's your
+ * request, you're probably better off just using the methods yourself.
+ *
+ * Returns: %TRUE if @name was found and requested, %FALSE otherwise.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_ircv3_capabilities_lookup_and_request(PurpleIRCv3Capabilities *capabilities, const char *name);
+
+/**
  * purple_ircv3_capabilties_add_wait:
  * @capabilities: The instance.
  *
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
@@ -108,7 +108,6 @@
 {
 	PurpleAccount *account = NULL;
 	PurpleConnection *purple_connection = NULL;
-	gboolean found = FALSE;
 
 	purple_connection = PURPLE_CONNECTION(capabilities->connection);
 	account = purple_connection_get_account(purple_connection);
@@ -117,7 +116,10 @@
 	 * require-password option.
 	 */
 	if(purple_account_get_require_password(account)) {
+		gboolean found = FALSE;
+
 		purple_ircv3_capabilities_lookup(capabilities, "sasl", &found);
+
 		if(found) {
 			purple_ircv3_sasl_request(capabilities);
 		}
@@ -126,10 +128,12 @@
 	/* cap-notify is implied when we use CAP LS 302, so this is really just to
 	 * make sure it's requested.
 	 */
-	purple_ircv3_capabilities_lookup(capabilities, "cap-notify", &found);
-	if(found) {
-		purple_ircv3_capabilities_request(capabilities, "cap-notify");
-	}
+	purple_ircv3_capabilities_lookup_and_request(capabilities, "cap-notify");
+
+	/* message-tags is used for a lot of stuff so we need to tell everyone we
+	 * do in fact support it.
+	 */
+	purple_ircv3_capabilities_lookup_and_request(capabilities, "message-tags");
 }
 
 /******************************************************************************
@@ -616,6 +620,23 @@
 	return value;
 }
 
+gboolean
+purple_ircv3_capabilities_lookup_and_request(PurpleIRCv3Capabilities *capabilities,
+                                             const char *name)
+{
+	gboolean found = FALSE;
+
+	g_return_val_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities), FALSE);
+	g_return_val_if_fail(name != NULL, FALSE);
+
+	purple_ircv3_capabilities_lookup(capabilities, name, &found);
+	if(found) {
+		purple_ircv3_capabilities_request(capabilities, name);
+	}
+
+	return found;
+}
+
 void
 purple_ircv3_capabilities_add_wait(PurpleIRCv3Capabilities *capabilities) {
 	g_return_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities));
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
@@ -267,8 +267,11 @@
 	                                     0, 0, NULL);
 	g_assert(parser->regex_message != NULL);
 
-	parser->regex_tags = g_regex_new("(?:(?<key>[A-Za-z0-9-\\/]+)"
-	                                 "(?:=(?<value>[^\\r\\n;]*))?(?:;|$))",
+	parser->regex_tags = g_regex_new("(?<key>(?<client_prefix>\\+?)"
+	                                        "(?:(?<vendor>[A-Za-z0-9-\\.]+)\\/)?"
+	                                        "(?<key_name>[A-Za-z0-9-]+)"
+	                                 ")"
+	                                 "(?:=(?<value>[^\r\n;]*))?(?:;|$)",
 	                                 0, 0, NULL);
 	g_assert(parser->regex_tags != NULL);
 
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
@@ -594,6 +594,85 @@
 }
 
 /******************************************************************************
+ * Message tags examples
+ *****************************************************************************/
+static void
+test_purple_ircv3_parser_message_tags_none(void) {
+	TestPurpleIRCv3ParserData data = {
+		.source = "nick!ident@host.com",
+		.command = "PRIVMSG",
+		.n_params = 2,
+		.params = {"me", "Hello"},
+	};
+	const char *msg = NULL;
+
+	msg = ":nick!ident@host.com PRIVMSG me :Hello";
+
+	test_purple_ircv3_parser(msg, &data);
+}
+
+static void
+test_purple_ircv3_parser_message_tags_3_tags(void) {
+	TestPurpleIRCv3ParserData data = {
+		.source = "nick!ident@host.com",
+		.command = "PRIVMSG",
+		.n_params = 2,
+		.params = {"me", "Hello"},
+	};
+	const char *msg = NULL;
+
+	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
+	g_hash_table_insert(data.tags, "aaa", "bbb");
+	g_hash_table_insert(data.tags, "ccc", "");
+	g_hash_table_insert(data.tags, "example.com/ddd", "eee");
+
+	msg = "@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me "
+	      ":Hello";
+
+	test_purple_ircv3_parser(msg, &data);
+}
+
+static void
+test_purple_ircv3_parser_message_tags_client_only(void) {
+	TestPurpleIRCv3ParserData data = {
+		.source = "url_bot!bot@example.com",
+		.command = "PRIVMSG",
+		.n_params = 2,
+		.params = {"#channel", "Example.com: A News Story"},
+	};
+	const char *msg = NULL;
+
+	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
+	g_hash_table_insert(data.tags, "+icon", "https://example.com/favicon.png");
+
+	msg = "@+icon=https://example.com/favicon.png :url_bot!bot@example.com "
+	      "PRIVMSG #channel :Example.com: A News Story";
+
+	test_purple_ircv3_parser(msg, &data);
+}
+
+static void
+test_purple_ircv3_parser_message_tags_labeled_response(void) {
+	TestPurpleIRCv3ParserData data = {
+		.source = "nick!user@example.com",
+		.command = "TAGMSG",
+		.n_params = 1,
+		.params = {"#channel"},
+	};
+	const char *msg = NULL;
+
+	data.tags = g_hash_table_new(g_str_hash, g_str_equal);
+	g_hash_table_insert(data.tags, "label", "123");
+	g_hash_table_insert(data.tags, "msgid", "abc");
+	g_hash_table_insert(data.tags, "+example-client-tag", "example-value");
+
+	msg = "@label=123;msgid=abc;+example-client-tag=example-value "
+	      ":nick!user@example.com TAGMSG #channel";
+
+	test_purple_ircv3_parser(msg, &data);
+
+}
+/******************************************************************************
  * Main
  *****************************************************************************/
 gint
@@ -686,5 +765,17 @@
 	g_test_add_func("/ircv3/parser/special-mode-2",
 	                test_purple_ircv3_special_mode_2);
 
+	/* These tests are the examples from the message-tags specification,
+	 * https://ircv3.net/specs/extensions/message-tags.html.
+	 */
+	g_test_add_func("/ircv3/parser/message-tags/none",
+	                test_purple_ircv3_parser_message_tags_none);
+	g_test_add_func("/ircv3/parser/message-tags/3-tags",
+	                test_purple_ircv3_parser_message_tags_3_tags);
+	g_test_add_func("/ircv3/parser/message-tags/client-only",
+	                test_purple_ircv3_parser_message_tags_client_only);
+	g_test_add_func("/ircv3/parser/message-tags/labeled-message",
+	                test_purple_ircv3_parser_message_tags_labeled_response);
+
 	return g_test_run();
 }
