# HG changeset patch
# Node ID 2c6464e0b9c1
# Parent  82d458ea7c87
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -82,6 +82,7 @@
 	'purpleproxyinfo.c',
 	'purpleroomlistroom.c',
 	'purplesqlitehistoryadapter.c',
+	'purpletags.c',
 	'purpleuiinfo.c',
 	'purplewhiteboard.c',
 	'purplewhiteboardmanager.c',
@@ -183,6 +184,7 @@
 	'purpleproxyinfo.h',
 	'purpleroomlistroom.h',
 	'purplesqlitehistoryadapter.h',
+	'purpletags.h',
 	'purpleuiinfo.h',
 	'purplewhiteboard.h',
 	'purplewhiteboardmanager.h',
diff --git a/libpurple/purpletags.h b/libpurple/purpletags.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purpletags.h
@@ -0,0 +1,151 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Purple 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 program 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 program 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 program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
+# error "only <purple.h> may be included directly"
+#endif
+
+#ifndef PURPLE_TAGS_H
+#define PURPLE_TAGS_H
+
+#include <glib.h>
+#include <gio/gio.h>
+
+#define PURPLE_TYPE_TAGS (purple_tags_get_type())
+G_DECLARE_FINAL_TYPE(PurpleTags, purple_tags, PURPLE, TAGS, GObject)
+
+/**
+ * PurpleTags:
+ *
+ * Tags is an object that can be used to keep track of arbitrary tags on
+ * objects. Tags are simple strings that use the first ':' to delimit a value.
+ * For example: `foo` is a tag with just a name and no value, but `foo:bar` is
+ * a tag with a name and value. Please note this distinction when then API calls
+ * for a name versus a tag which would be the name and the value.
+ *
+ * Since: 3.0.0
+ */
+
+G_BEGIN_DECLS
+
+/**
+ * purple_tags_new:
+ *
+ * Creates a new tags object.
+ *
+ * Returns: (transfer full): The new tags object.
+ *
+ * Since: 3.0.0
+ */
+PurpleTags *purple_tags_new(void);
+
+/**
+ * purple_tags_exists:
+ * @tags: The instance.
+ * @name: The name of the tag to check if it exists.
+ *
+ * Checks if @name exists in @tag.
+ *
+ * Returns: %TRUE if @name exists in @tags otherwise %FALSE.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_tags_exists(PurpleTags *tags, const gchar *name);
+
+/**
+ * purple_tags_get:
+ * @tags: The instance.
+ * @name: The name of the tag to get.
+ *
+ * Gets the first tag that matches @name.
+ *
+ * Returns: The the value of first tag matching @name. If there is no value, %NULL is returned.
+ *
+ * Since: 3.0.0
+ */
+const gchar *purple_tags_get(PurpleTags *tags, const gchar *name);
+
+/**
+ * purple_tags_add:
+ * @tags: The instance.
+ * @tag: The tag data.
+ *
+ * Adds @tag to @tags.
+ *
+ * Since: 3.0.0
+ */
+void purple_tags_add(PurpleTags *tags, const gchar *tag);
+
+/**
+ * purple_tags_remove:
+ * @tags: The instance.
+ * @tag: The tag data.
+ *
+ * Removes the first occurrence of @tag from @tags. Note that this is the tag
+ * name and value not just the name.
+ *
+ * Returns: %TRUE if @tag was found and removed, otherwise %FALSE.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_tags_remove(PurpleTags *tags, const gchar *tag);
+
+/**
+ * purple_tags_get_count:
+ * @tags: The instance.
+ *
+ * Gets the number of tags in @tags.
+ *
+ * Returns: The number of tags that @tags is keeping track of.
+ *
+ * Since: 3.0.0
+ */
+guint purple_tags_get_count(PurpleTags *tags);
+
+/**
+ * purple_tags_get_all:
+ * @tags: The instance.
+ *
+ * Gets a list of all the tags.
+ *
+ * Returns: (transfer none): The list of all the tags.
+ *
+ * Since: 3.0.0
+ */
+GList *purple_tags_get_all(PurpleTags *tags);
+
+/**
+ * purple_tags_to_string:
+ * @tags: The instance.
+ * @separator: (nullable) A string to separate the items.
+ *
+ * Creates a @separator delimited string containing each item in @tags.
+ *
+ * Returns: (transfer full): The string representation.
+ *
+ * Since: 3.0.0
+ */
+gchar *purple_tags_to_string(PurpleTags *tags, const gchar *separator);
+
+G_END_DECLS
+
+#endif /* PURPLE_TAGS_H */
diff --git a/libpurple/purpletags.c b/libpurple/purpletags.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purpletags.c
@@ -0,0 +1,180 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Purple 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 program 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 program 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 program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "purpletags.h"
+
+#include "util.h"
+
+struct _PurpleTags {
+	GObject parent;
+
+	GList *tags;
+};
+
+G_DEFINE_TYPE(PurpleTags, purple_tags, G_TYPE_OBJECT)
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+
+static void
+purple_tags_dispose(GObject *obj) {
+	PurpleTags *tags = PURPLE_TAGS(obj);
+
+	if(tags->tags != NULL) {
+		g_list_free_full(tags->tags, g_free);
+		tags->tags = NULL;
+	}
+
+	G_OBJECT_CLASS(purple_tags_parent_class)->dispose(obj);
+}
+
+static void
+purple_tags_init(PurpleTags *tags) {
+}
+
+static void
+purple_tags_class_init(PurpleTagsClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->dispose = purple_tags_dispose;
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+PurpleTags *
+purple_tags_new(void) {
+	return g_object_new(PURPLE_TYPE_TAGS, NULL);
+}
+
+gboolean
+purple_tags_exists(PurpleTags *tags, const gchar *name) {
+	size_t name_len  = 0;
+
+	g_return_val_if_fail(PURPLE_IS_TAGS(tags), FALSE);
+	g_return_val_if_fail(name != NULL, FALSE);
+
+	name_len = strlen(name);
+
+	for(GList *l = tags->tags; l != NULL; l = l->next) {
+		const gchar *tag = l->data;
+
+		if(g_str_has_prefix(tag, name)) {
+			const gchar *value = tag + name_len;
+			if(*value == '\0' || *value == ':') {
+				return TRUE;
+			}
+		}
+	}
+
+	return FALSE;
+}
+
+const gchar *
+purple_tags_get(PurpleTags *tags, const gchar *name) {
+	size_t name_len;
+
+	g_return_val_if_fail(PURPLE_IS_TAGS(tags), NULL);
+	g_return_val_if_fail(name != NULL, NULL);
+
+	name_len = strlen(name);
+
+	for(GList *l = tags->tags; l != NULL; l = l->data) {
+		const gchar *tag = l->data;
+
+		if(g_str_has_prefix(tag, name)) {
+			const gchar *value = tag + name_len;
+			if(*value == '\0') {
+				return NULL;
+			}
+
+			if(*value == ':') {
+				return (value + 1);
+			}
+		}
+	}
+
+	return NULL;
+}
+
+void
+purple_tags_add(PurpleTags *tags, const gchar *tag) {
+	g_return_if_fail(PURPLE_IS_TAGS(tags));
+	g_return_if_fail(tag != NULL);
+
+	tags->tags = g_list_append(tags->tags, g_strdup(tag));
+}
+
+gboolean
+purple_tags_remove(PurpleTags *tags, const gchar *tag) {
+	g_return_val_if_fail(PURPLE_IS_TAGS(tags), FALSE);
+	g_return_val_if_fail(tag != NULL, FALSE);
+
+	for(GList *l = tags->tags; l != NULL; l = l->next) {
+		gchar *etag = l->data;
+
+		if(purple_strequal(etag, tag)) {
+			g_free(etag);
+			tags->tags = g_list_delete_link(tags->tags, l);
+
+			return TRUE;
+		}
+	}
+
+	return FALSE;
+}
+
+guint
+purple_tags_get_count(PurpleTags *tags) {
+	g_return_val_if_fail(PURPLE_IS_TAGS(tags), 0);
+
+	return g_list_length(tags->tags);
+}
+
+GList *
+purple_tags_get_all(PurpleTags *tags) {
+	g_return_val_if_fail(PURPLE_IS_TAGS(tags), NULL);
+
+	return tags->tags;
+}
+
+gchar *
+purple_tags_to_string(PurpleTags *tags, const gchar *separator) {
+	GString *value = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_TAGS(tags), NULL);
+
+	value = g_string_new("");
+
+	for(GList *l = tags->tags; l != NULL; l = l->next) {
+		const gchar *tag = l->data;
+
+		g_string_append(value, tag);
+
+		if(separator != NULL && l->next != NULL) {
+			g_string_append(value, separator);
+		}
+	}
+
+	return g_string_free(value, FALSE);
+}
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -17,6 +17,7 @@
     'protocol_xfer',
     'purplepath',
     'queued_output_stream',
+    'tags',
     'util',
     'whiteboard_manager',
     'xmlnode',
diff --git a/libpurple/tests/test_tags.c b/libpurple/tests/test_tags.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_tags.c
@@ -0,0 +1,260 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Purple 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 program 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 program 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 program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <purple.h>
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_tags_exists_true(void) {
+	PurpleTags *tags = purple_tags_new();
+
+	purple_tags_add(tags, "foo");
+	g_assert_true(purple_tags_exists(tags, "foo"));
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_exists_false(void) {
+	PurpleTags *tags = purple_tags_new();
+
+	g_assert_false(purple_tags_exists(tags, "foo"));
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_add_remove_bare(void) {
+	PurpleTags *tags = purple_tags_new();
+
+	purple_tags_add(tags, "tag1");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
+
+	purple_tags_remove(tags, "tag1");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 0);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_add_duplicate_bare(void) {
+	PurpleTags *tags = purple_tags_new();
+
+	purple_tags_add(tags, "tag1");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
+
+	purple_tags_add(tags, "tag1");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 2);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_remove_non_existant_bare(void) {
+	PurpleTags *tags = purple_tags_new();
+
+	purple_tags_remove(tags, "tag1");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 0);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_add_remove_with_value(void) {
+	PurpleTags *tags = purple_tags_new();
+
+	purple_tags_add(tags, "tag1:purple");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
+
+	purple_tags_remove(tags, "tag1:purple");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 0);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_add_duplicate_with_value(void) {
+	PurpleTags *tags = purple_tags_new();
+
+	purple_tags_add(tags, "tag1:purple");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
+
+	purple_tags_add(tags, "tag1:purple");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 2);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_remove_non_existant_with_value(void) {
+	PurpleTags *tags = purple_tags_new();
+
+	purple_tags_remove(tags, "tag1:purple");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 0);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_get_single(void) {
+	PurpleTags *tags = purple_tags_new();
+	const gchar *value = NULL;
+
+	purple_tags_add(tags, "tag1:purple");
+	value = purple_tags_get(tags, "tag1");
+
+	g_assert_true(purple_strequal(value, "purple"));
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_get_multiple(void) {
+	PurpleTags *tags = purple_tags_new();
+	const gchar *value = NULL;
+
+	purple_tags_add(tags, "tag1:purple");
+	purple_tags_add(tags, "tag1:pink");
+
+	value = purple_tags_get(tags, "tag1");
+
+	g_assert_true(purple_strequal(value, "purple"));
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_get_all(void) {
+	PurpleTags *tags = purple_tags_new();
+	GList *all_tags = NULL;
+	const gchar *values[] = {"foo", "bar", "baz", "qux", "quux", NULL};
+	gint i = 0;
+
+	for(i = 0; values[i] != NULL; i++) {
+		purple_tags_add(tags, values[i]);
+	}
+
+	all_tags = purple_tags_get_all(tags);
+	i = 0;
+
+	for(GList *l = all_tags; l != NULL; l = l->next) {
+		const gchar *value = l->data;
+
+		g_assert_true(purple_strequal(value, values[i]));
+
+		i++;
+	}
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_to_string_single(void) {
+	PurpleTags *tags = purple_tags_new();
+	gchar *value = NULL;
+
+	purple_tags_add(tags, "foo");
+	value = purple_tags_to_string(tags, NULL);
+
+	g_assert_true(purple_strequal(value, "foo"));
+
+	g_free(value);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_to_string_multiple_with_separator(void) {
+	PurpleTags *tags = purple_tags_new();
+	gchar *value = NULL;
+
+	purple_tags_add(tags, "foo");
+	purple_tags_add(tags, "bar");
+	purple_tags_add(tags, "baz");
+	value = purple_tags_to_string(tags, ", ");
+
+	g_assert_true(purple_strequal(value, "foo, bar, baz"));
+
+	g_free(value);
+
+	g_clear_object(&tags);
+}
+
+static void
+test_purple_tags_to_string_multiple_with_null_separator(void) {
+	PurpleTags *tags = purple_tags_new();
+	gchar *value = NULL;
+
+	purple_tags_add(tags, "foo");
+	purple_tags_add(tags, "bar");
+	purple_tags_add(tags, "baz");
+	value = purple_tags_to_string(tags, NULL);
+
+	g_assert_true(purple_strequal(value, "foobarbaz"));
+
+	g_free(value);
+
+	g_clear_object(&tags);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+gint
+main(gint argc, gchar **argv) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_add_func("/tags/exists-true", test_purple_tags_exists_true);
+	g_test_add_func("/tags/exists-false", test_purple_tags_exists_false);
+
+	g_test_add_func("/tags/add-remove-bare",
+	                test_purple_tags_add_remove_bare);
+	g_test_add_func("/tags/add-duplicate-bare",
+	                test_purple_tags_add_duplicate_bare);
+	g_test_add_func("/tags/remove-non-existant-bare",
+	                test_purple_tags_remove_non_existant_bare);
+
+	g_test_add_func("/tags/add-remove-with-value",
+	                test_purple_tags_add_remove_with_value);
+	g_test_add_func("/tags/add-duplicate-with-value",
+	                test_purple_tags_add_duplicate_with_value);
+	g_test_add_func("/tags/remove-non-existant-with-value",
+	                test_purple_tags_remove_non_existant_with_value);
+
+	g_test_add_func("/tags/get-single", test_purple_tags_get_single);
+	g_test_add_func("/tags/get-multiple", test_purple_tags_get_multiple);
+
+	g_test_add_func("/tags/get-all", test_purple_tags_get_all);
+
+	g_test_add_func("/tags/to-string-single",
+	                test_purple_tags_to_string_single);
+	g_test_add_func("/tags/to-string-multiple-with-separator",
+	                test_purple_tags_to_string_multiple_with_separator);
+	g_test_add_func("/tags/to-string-multiple-with-null-separator",
+	                test_purple_tags_to_string_multiple_with_null_separator);
+
+	return g_test_run();
+}
