# HG changeset patch
# Node ID bfdfd78dd9df018a86408889c8b04200943c81ea
# Parent  8645679eee55070859f89c63f13628dddf4e6ba4
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -20,6 +20,7 @@
 	'purplebadge.c',
 	'purplebadges.c',
 	'purplechanneljoindetails.c',
+	'purplecommand.c',
 	'purpleconnectionerrorinfo.c',
 	'purplecontact.c',
 	'purplecontactinfo.c',
@@ -112,6 +113,7 @@
 	'purplebadge.h',
 	'purplebadges.h',
 	'purplechanneljoindetails.h',
+	'purplecommand.h',
 	'purpleconnectionerrorinfo.h',
 	'purplecontact.h',
 	'purplecontactinfo.h',
diff --git a/libpurple/purplecommand.c b/libpurple/purplecommand.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purplecommand.c
@@ -0,0 +1,477 @@
+/*
+ * 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 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 <birb.h>
+
+#include "purplecommand.h"
+
+#include "util.h"
+
+struct _PurpleCommand {
+	GObject parent;
+
+	char *icon_name;
+	GDateTime *last_used;
+	char *name;
+	int priority;
+	char *source;
+	char *summary;
+	guint use_count;
+};
+
+enum {
+	PROP_0,
+	PROP_ICON_NAME,
+	PROP_LAST_USED,
+	PROP_NAME,
+	PROP_PRIORITY,
+	PROP_SOURCE,
+	PROP_SUMMARY,
+	PROP_USE_COUNT,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+enum {
+	SIG_EXECUTED,
+	N_SIGNALS,
+};
+static guint signals[N_SIGNALS] = {0, };
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+purple_command_set_name(PurpleCommand *command, const char *name) {
+	g_return_if_fail(PURPLE_IS_COMMAND(command));
+	g_return_if_fail(!purple_strempty(name));
+
+	if(g_set_str(&command->name, name)) {
+		g_object_notify_by_pspec(G_OBJECT(command), properties[PROP_NAME]);
+	}
+}
+
+static void
+purple_command_set_source(PurpleCommand *command, const char *source) {
+	g_return_if_fail(PURPLE_IS_COMMAND(command));
+	g_return_if_fail(!purple_strempty(source));
+
+	if(g_set_str(&command->source, source)) {
+		g_object_notify_by_pspec(G_OBJECT(command), properties[PROP_SOURCE]);
+	}
+}
+
+static void
+purple_command_set_priority(PurpleCommand *command, int priority) {
+	g_return_if_fail(PURPLE_IS_COMMAND(command));
+
+	if(command->priority != priority) {
+		command->priority = priority;
+
+		g_object_notify_by_pspec(G_OBJECT(command),
+		                         properties[PROP_PRIORITY]);
+	}
+}
+
+/******************************************************************************
+ * Default Handlers
+ *****************************************************************************/
+static void
+purple_command_default_executed_handler(PurpleCommand *command,
+                                        G_GNUC_UNUSED PurpleConversation *conversation,
+                                        G_GNUC_UNUSED GStrv params)
+{
+	GDateTime *now = NULL;
+	GObject *obj = G_OBJECT(command);
+
+	/* We're going to set multiple properties, so we need to freeze property
+	 * notifications.
+	 */
+	g_object_freeze_notify(obj);
+
+	/* We use local time as this should be user specific. */
+	now = g_date_time_new_now_local();
+	purple_command_set_last_used(command, now);
+	birb_date_time_clear(&now);
+
+	if(command->use_count < G_MAXUINT32) {
+		purple_command_set_use_count(command, command->use_count + 1);
+	}
+
+	g_object_thaw_notify(obj);
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_FINAL_TYPE(PurpleCommand, purple_command, G_TYPE_OBJECT)
+
+static void
+purple_command_finalize(GObject *obj) {
+	PurpleCommand *command = PURPLE_COMMAND(obj);
+
+	g_clear_pointer(&command->icon_name, g_free);
+	birb_date_time_clear(&command->last_used);
+	g_clear_pointer(&command->name, g_free);
+	g_clear_pointer(&command->source, g_free);
+	g_clear_pointer(&command->summary, g_free);
+
+	G_OBJECT_CLASS(purple_command_parent_class)->finalize(obj);
+}
+
+static void
+purple_command_get_property(GObject *obj, guint param_id, GValue *value,
+                            GParamSpec *pspec)
+{
+	PurpleCommand *command = PURPLE_COMMAND(obj);
+
+	switch(param_id) {
+	case PROP_ICON_NAME:
+		g_value_set_string(value, purple_command_get_icon_name(command));
+		break;
+	case PROP_LAST_USED:
+		g_value_set_boxed(value, purple_command_get_last_used(command));
+		break;
+	case PROP_NAME:
+		g_value_set_string(value, purple_command_get_name(command));
+		break;
+	case PROP_PRIORITY:
+		g_value_set_int(value, purple_command_get_priority(command));
+		break;
+	case PROP_SOURCE:
+		g_value_set_string(value, purple_command_get_source(command));
+		break;
+	case PROP_SUMMARY:
+		g_value_set_string(value, purple_command_get_summary(command));
+		break;
+	case PROP_USE_COUNT:
+		g_value_set_uint(value, purple_command_get_use_count(command));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+purple_command_set_property(GObject *obj, guint param_id, const GValue *value,
+                            GParamSpec *pspec)
+{
+	PurpleCommand *command = PURPLE_COMMAND(obj);
+
+	switch(param_id) {
+	case PROP_ICON_NAME:
+		purple_command_set_icon_name(command, g_value_get_string(value));
+		break;
+	case PROP_LAST_USED:
+		purple_command_set_last_used(command, g_value_get_boxed(value));
+		break;
+	case PROP_NAME:
+		purple_command_set_name(command, g_value_get_string(value));
+		break;
+	case PROP_PRIORITY:
+		purple_command_set_priority(command, g_value_get_int(value));
+		break;
+	case PROP_SOURCE:
+		purple_command_set_source(command, g_value_get_string(value));
+		break;
+	case PROP_SUMMARY:
+		purple_command_set_summary(command, g_value_get_string(value));
+		break;
+	case PROP_USE_COUNT:
+		purple_command_set_use_count(command, g_value_get_uint(value));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+purple_command_init(G_GNUC_UNUSED PurpleCommand *command) {
+}
+
+static void
+purple_command_class_init(PurpleCommandClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = purple_command_finalize;
+	obj_class->get_property = purple_command_get_property;
+	obj_class->set_property = purple_command_set_property;
+
+	/**
+	 * PurpleCommand:icon-name:
+	 *
+	 * The icon name for the command.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_ICON_NAME] = g_param_spec_string(
+		"icon-name", NULL, NULL,
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleCommand:last-used:
+	 *
+	 * The date time of when the command was last used.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_LAST_USED] = g_param_spec_boxed(
+		"last-used", NULL, NULL,
+		G_TYPE_DATE_TIME,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleCommand:name:
+	 *
+	 * The name of the command.
+	 *
+	 * This is what the user would type after the prefix that the user
+	 * interface is using. If the user interface is using `/`, then this would
+	 * be `me` for the `/me` command.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_NAME] = g_param_spec_string(
+		"name", NULL, NULL,
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleCommand:priority:
+	 *
+	 * The priority for the command.
+	 *
+	 * If multiple commands have the same name, this will be used to determine
+	 * which one to return or sort first. Higher values will have higher
+	 * priority.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_PRIORITY] = g_param_spec_int(
+		"priority", NULL, NULL,
+		G_MININT32, G_MAXINT32, 0,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleCommand:source:
+	 *
+	 * The source of the command.
+	 *
+	 * This is a user visible string that user interfaces can display to help
+	 * users determine what this command is from. For example, this could be
+	 * `IRC`, `XMPP`, `my cool plugin`, etc.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_SOURCE] = g_param_spec_string(
+		"source", NULL, NULL,
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleCommand:summary:
+	 *
+	 * A summary of the command.
+	 *
+	 * This is a user visible string that user interfaces can display to help
+	 * users determine what this command will do.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_SUMMARY] = g_param_spec_string(
+		"summary", NULL, NULL,
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleCommand:use-count:
+	 *
+	 * The count of how many times the command has been used.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_USE_COUNT] = g_param_spec_uint(
+		"use-count", NULL, NULL,
+		0, G_MAXUINT32, 0,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+
+	/**
+	 * PurpleCommand::executed:
+	 * @command: The instance.
+	 * @conversation: (nullable): The conversation this command was run from.
+	 * @params: (nullable): The parameters passed to the command.
+	 *
+	 * Emitted when [method@Command.execute] is called.
+	 *
+	 * Since: 3.0
+	 */
+	signals[SIG_EXECUTED] = g_signal_new_class_handler(
+		"executed",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		G_CALLBACK(purple_command_default_executed_handler),
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		2,
+		PURPLE_TYPE_CONVERSATION,
+		G_TYPE_STRV);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+void
+purple_command_execute(PurpleCommand *command,
+                       PurpleConversation *conversation, const char *params)
+{
+	GStrv paramsv = NULL;
+
+	g_return_if_fail(PURPLE_IS_COMMAND(command));
+
+	if(params != NULL) {
+		paramsv = g_strsplit(params, " ", -1);
+	}
+
+	purple_command_executev(command, conversation, paramsv);
+
+	g_clear_pointer(&paramsv, g_strfreev);
+}
+
+void
+purple_command_executev(PurpleCommand *command,
+                        PurpleConversation *conversation, GStrv params)
+{
+	g_return_if_fail(PURPLE_IS_COMMAND(command));
+
+	g_signal_emit(G_OBJECT(command), signals[SIG_EXECUTED], 0, conversation,
+	              params);
+}
+
+const char *
+purple_command_get_icon_name(PurpleCommand *command) {
+	g_return_val_if_fail(PURPLE_IS_COMMAND(command), NULL);
+
+	return command->icon_name;
+}
+
+GDateTime *
+purple_command_get_last_used(PurpleCommand *command) {
+	g_return_val_if_fail(PURPLE_IS_COMMAND(command), NULL);
+
+	return command->last_used;
+}
+
+const char *
+purple_command_get_name(PurpleCommand *command) {
+	g_return_val_if_fail(PURPLE_IS_COMMAND(command), NULL);
+
+	return command->name;
+}
+
+int
+purple_command_get_priority(PurpleCommand *command) {
+	g_return_val_if_fail(PURPLE_IS_COMMAND(command), 0);
+
+	return command->priority;
+}
+
+const char *
+purple_command_get_source(PurpleCommand *command) {
+	g_return_val_if_fail(PURPLE_IS_COMMAND(command), NULL);
+
+	return command->source;
+}
+
+const char *
+purple_command_get_summary(PurpleCommand *command) {
+	g_return_val_if_fail(PURPLE_IS_COMMAND(command), NULL);
+
+	return command->summary;
+}
+
+guint
+purple_command_get_use_count(PurpleCommand *command) {
+	g_return_val_if_fail(PURPLE_IS_COMMAND(command), 0);
+
+	return command->use_count;
+}
+
+PurpleCommand *
+purple_command_new(const char *name, const char *source, int priority) {
+	g_return_val_if_fail(!purple_strempty(name), NULL);
+	g_return_val_if_fail(!purple_strempty(source), NULL);
+
+	return g_object_new(
+		PURPLE_TYPE_COMMAND,
+		"name", name,
+		"source", source,
+		"priority", priority,
+		NULL);
+}
+
+void
+purple_command_set_icon_name(PurpleCommand *command, const char *icon_name) {
+	g_return_if_fail(PURPLE_IS_COMMAND(command));
+
+	if(g_set_str(&command->icon_name, icon_name)) {
+		g_object_notify_by_pspec(G_OBJECT(command),
+		                         properties[PROP_ICON_NAME]);
+	}
+}
+
+void
+purple_command_set_last_used(PurpleCommand *command, GDateTime *last_used) {
+	g_return_if_fail(PURPLE_IS_COMMAND(command));
+
+	if(birb_date_time_set(&command->last_used, last_used)) {
+		g_object_notify_by_pspec(G_OBJECT(command),
+		                         properties[PROP_LAST_USED]);
+	}
+}
+
+void
+purple_command_set_summary(PurpleCommand *command, const char *summary) {
+	g_return_if_fail(PURPLE_IS_COMMAND(command));
+
+	if(g_set_str(&command->summary, summary)) {
+		g_object_notify_by_pspec(G_OBJECT(command), properties[PROP_SUMMARY]);
+	}
+}
+
+void
+purple_command_set_use_count(PurpleCommand *command, guint use_count) {
+	g_return_if_fail(PURPLE_IS_COMMAND(command));
+
+	if(command->use_count != use_count) {
+		command->use_count = use_count;
+
+		g_object_notify_by_pspec(G_OBJECT(command),
+		                         properties[PROP_USE_COUNT]);
+	}
+}
diff --git a/libpurple/purplecommand.h b/libpurple/purplecommand.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purplecommand.h
@@ -0,0 +1,252 @@
+/*
+ * 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 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(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
+# error "only <purple.h> may be included directly"
+#endif
+
+#ifndef PURPLE_COMMAND_H
+#define PURPLE_COMMAND_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "purpleconversation.h"
+#include "purpleversion.h"
+
+G_BEGIN_DECLS
+
+/**
+ * PurpleCommand:
+ *
+ * An object that represents a command.
+ *
+ * Since: 3.0
+ */
+
+#define PURPLE_TYPE_COMMAND (purple_command_get_type())
+
+PURPLE_AVAILABLE_IN_3_0
+G_DECLARE_FINAL_TYPE(PurpleCommand, purple_command, PURPLE, COMMAND, GObject)
+
+/**
+ * purple_command_execute:
+ * @command: The instance.
+ * @conversation: (nullable): The conversation where the command was run.
+ * @params: (nullable): The parameters for the command.
+ *
+ * Emits the [signal@Command::executed] signal.
+ *
+ * User interfaces should call this method when the user wants to execute the
+ * command.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_command_execute(PurpleCommand *command, PurpleConversation *conversation, const char *params);
+
+/**
+ * purple_command_executev:
+ * @command: The instance.
+ * @conversation: (nullable): The conversation where the command was run.
+ * @params: (nullable): The parameters for the command.
+ *
+ * Emits the [signal@Command::executed] signal.
+ *
+ * User interfaces should call this method when the user wants to execute the
+ * command.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_command_executev(PurpleCommand *command, PurpleConversation *conversation, GStrv params);
+
+/**
+ * purple_command_get_icon_name:
+ * @command: The instance.
+ *
+ * Gets the icon name of @command.
+ *
+ * Returns: (transfer none) (nullable): The icon-name if set, otherwise %NULL.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+const char *purple_command_get_icon_name(PurpleCommand *command);
+
+/**
+ * purple_command_get_last_used:
+ * @command: The instance.
+ *
+ * Gets the last used date time of @command.
+ *
+ * Returns: (nullable): The time that @command was last used or %NULL.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+GDateTime *purple_command_get_last_used(PurpleCommand *command);
+
+/**
+ * purple_command_get_name:
+ * @command: The instance.
+ *
+ * Gets the name of the command.
+ *
+ * Returns: The name of @command.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+const char *purple_command_get_name(PurpleCommand *command);
+
+/**
+ * purple_command_get_priority:
+ * @command: The instance.
+ *
+ * Gets the priority of @command.
+ *
+ * Returns: The priority.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+int purple_command_get_priority(PurpleCommand *command);
+
+/**
+ * purple_command_get_source:
+ * @command: The instance.
+ *
+ * Gets the source of @command.
+ *
+ * When presenting commands to the user, this value can be used to help them
+ * determine where the command came from.
+ *
+ * Returns: The source.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+const char *purple_command_get_source(PurpleCommand *command);
+
+/**
+ * purple_command_get_summary:
+ * @command: The instance.
+ *
+ * Gets the summary of @command.
+ *
+ * When presenting commands to the user, this value can be used to help them
+ * determine exactly what this command will do.
+ *
+ * Returns: The summary.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+const char *purple_command_get_summary(PurpleCommand *command);
+
+/**
+ * purple_command_get_use_count:
+ * @command: The instance.
+ *
+ * Gets the use count of @command.
+ *
+ * Returns: The use count of @command.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+guint purple_command_get_use_count(PurpleCommand *command);
+
+/**
+ * purple_command_new:
+ * @name: The name of the command.
+ * @source: The source of the command. This will be displayed to users and
+ *          should be marked for translation.
+ * @priority: The priority of the command.
+ *
+ * Creates a new command with the given properties.
+ *
+ * Returns: (transfer full): The new command.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleCommand *purple_command_new(const char *name, const char *source, int priority);
+
+/**
+ * purple_command_set_icon_name:
+ * @command: The instance.
+ * @icon_name: (nullable): The new icon name.
+ *
+ * Sets the icon name of @command to @icon_name.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_command_set_icon_name(PurpleCommand *command, const char *icon_name);
+
+/**
+ * purple_command_set_last_used:
+ * @command: The instance.
+ * @last_used: (nullable): The new last used time stamp.
+ *
+ * Sets the last used time of @command to @last_used.
+ *
+ * Typically this won't need to be called as the default handler for
+ * [signal@Command::executed] will set it.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_command_set_last_used(PurpleCommand *command, GDateTime *last_used);
+
+/**
+ * purple_command_set_summary:
+ * @command: The instance.
+ * @summary: (nullable): The new summary.
+ *
+ * Sets the summary of @command to @summary.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_command_set_summary(PurpleCommand *command, const char *summary);
+
+/**
+ * purple_command_set_use_count:
+ * @command: The instance.
+ * @use_count: The new use count.
+ *
+ * Sets the use count of @command to @use_count.
+ *
+ * Typically this won't need to be called directly as the default handler for
+ * [signal@Command::executed] will increment it.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_command_set_use_count(PurpleCommand *command, guint use_count);
+
+G_END_DECLS
+
+#endif /* PURPLE_COMMAND_H */
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -7,6 +7,7 @@
     'channel_join_details',
     'circular_buffer',
     'create_conversation_details',
+    'command',
     'contact',
     'contact_info',
     'contact_manager',
diff --git a/libpurple/tests/test_command.c b/libpurple/tests/test_command.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_command.c
@@ -0,0 +1,377 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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 <purple.h>
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_command_new(void) {
+	PurpleCommand *command = NULL;
+
+	command = purple_command_new("name", "source", 100);
+	g_assert_true(PURPLE_IS_COMMAND(command));
+
+	g_assert_finalize_object(command);
+}
+
+static void
+test_purple_command_properties(void) {
+	PurpleCommand *command = NULL;
+	GDateTime *last_used = NULL;
+	GDateTime *last_used1 = NULL;
+	char *icon_name = NULL;
+	char *name = NULL;
+	char *source = NULL;
+	char *summary = NULL;
+	int priority = 0;
+	guint use_count = 0;
+
+	last_used = g_date_time_new_now_utc();
+
+	command = g_object_new(
+		PURPLE_TYPE_COMMAND,
+		"icon-name", "icon",
+		"last-used", last_used,
+		"name", "name",
+		"priority", 100,
+		"source", "source",
+		"summary", "summary",
+		"use-count", 1337,
+		NULL);
+
+	g_assert_true(PURPLE_IS_COMMAND(command));
+
+	g_object_get(
+		G_OBJECT(command),
+		"icon-name", &icon_name,
+		"last-used", &last_used1,
+		"name", &name,
+		"priority", &priority,
+		"source", &source,
+		"summary", &summary,
+		"use-count", &use_count,
+		NULL);
+
+	g_assert_cmpstr(icon_name, ==, "icon");
+	g_clear_pointer(&icon_name, g_free);
+
+	g_assert_true(birb_date_time_equal(last_used1, last_used));
+	birb_date_time_clear(&last_used1);
+
+	g_assert_cmpstr(name, ==, "name");
+	g_clear_pointer(&name, g_free);
+
+	g_assert_cmpint(priority, ==, 100);
+
+	g_assert_cmpstr(source, ==, "source");
+	g_clear_pointer(&source, g_free);
+
+	g_assert_cmpstr(summary, ==, "summary");
+	g_clear_pointer(&summary, g_free);
+
+	g_assert_cmpuint(use_count, ==, 1337);
+
+	g_assert_finalize_object(command);
+
+	birb_date_time_clear(&last_used);
+}
+
+/******************************************************************************
+ * Execute Tests
+ *****************************************************************************/
+static void
+test_purple_command_executed_counter_cb(PurpleCommand *command,
+                                        G_GNUC_UNUSED PurpleConversation *conversation,
+                                        G_GNUC_UNUSED GStrv params,
+                                        gpointer data)
+{
+	guint *counter = data;
+
+	g_assert_true(PURPLE_IS_COMMAND(command));
+
+	*counter = *counter + 1;
+}
+
+static void
+test_purple_command_executed_params_cb(PurpleCommand *command,
+                                       G_GNUC_UNUSED PurpleConversation *conversation,
+                                       GStrv params,
+                                       gpointer data)
+{
+	g_assert_true(PURPLE_IS_COMMAND(command));
+
+	g_assert_cmpstrv(params, data);
+}
+
+static void
+test_purple_command_execute_null_params(void) {
+	PurpleCommand *command = NULL;
+	GDateTime *last_used = NULL;
+	guint counter = 0;
+	guint use_count = 0;
+
+	command = purple_command_new("command", "test", 0);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_counter_cb),
+	                 &counter);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_params_cb),
+	                 NULL);
+
+	purple_command_execute(command, NULL, NULL);
+	g_assert_cmpuint(counter, ==, 1);
+
+	last_used = purple_command_get_last_used(command);
+	g_assert_nonnull(last_used);
+
+	use_count = purple_command_get_use_count(command);
+	g_assert_cmpuint(use_count, ==, 1);
+
+	g_assert_finalize_object(command);
+}
+
+static void
+test_purple_command_execute_empty_params(void) {
+	PurpleCommand *command = NULL;
+	GDateTime *last_used = NULL;
+	guint counter = 0;
+	guint use_count = 0;
+	const char * const expected[] = {NULL};
+
+	command = purple_command_new("command", "test", 0);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_counter_cb),
+	                 &counter);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_params_cb),
+	                 (gpointer)expected);
+
+	purple_command_execute(command, NULL, "");
+	g_assert_cmpuint(counter, ==, 1);
+
+	last_used = purple_command_get_last_used(command);
+	g_assert_nonnull(last_used);
+
+	use_count = purple_command_get_use_count(command);
+	g_assert_cmpuint(use_count, ==, 1);
+
+	g_assert_finalize_object(command);
+}
+
+static void
+test_purple_command_execute_single_param(void) {
+	PurpleCommand *command = NULL;
+	GDateTime *last_used = NULL;
+	guint counter = 0;
+	guint use_count = 0;
+	const char * const expected[] = {"param1", NULL};
+
+	command = purple_command_new("command", "test", 0);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_counter_cb),
+	                 &counter);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_params_cb),
+	                 (gpointer)expected);
+
+	purple_command_execute(command, NULL, "param1");
+	g_assert_cmpuint(counter, ==, 1);
+
+	last_used = purple_command_get_last_used(command);
+	g_assert_nonnull(last_used);
+
+	use_count = purple_command_get_use_count(command);
+	g_assert_cmpuint(use_count, ==, 1);
+
+	g_assert_finalize_object(command);
+}
+
+static void
+test_purple_command_execute_multiple_params(void) {
+	PurpleCommand *command = NULL;
+	GDateTime *last_used = NULL;
+	guint counter = 0;
+	guint use_count = 0;
+	const char * const expected[] = {"param1", "param2", "param3", NULL};
+
+	command = purple_command_new("command", "test", 0);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_counter_cb),
+	                 &counter);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_params_cb),
+	                 (gpointer)expected);
+
+	purple_command_execute(command, NULL, "param1 param2 param3");
+	g_assert_cmpuint(counter, ==, 1);
+
+	last_used = purple_command_get_last_used(command);
+	g_assert_nonnull(last_used);
+
+	use_count = purple_command_get_use_count(command);
+	g_assert_cmpuint(use_count, ==, 1);
+
+	g_assert_finalize_object(command);
+}
+
+static void
+test_purple_command_executev_null_params(void) {
+	PurpleCommand *command = NULL;
+	GDateTime *last_used = NULL;
+	guint counter = 0;
+	guint use_count = 0;
+
+	command = purple_command_new("command", "test", 0);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_counter_cb),
+	                 &counter);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_params_cb),
+	                 NULL);
+
+	purple_command_executev(command, NULL, NULL);
+	g_assert_cmpuint(counter, ==, 1);
+
+	last_used = purple_command_get_last_used(command);
+	g_assert_nonnull(last_used);
+
+	use_count = purple_command_get_use_count(command);
+	g_assert_cmpuint(use_count, ==, 1);
+
+	g_assert_finalize_object(command);
+}
+
+static void
+test_purple_command_executev_empty_params(void) {
+	PurpleCommand *command = NULL;
+	GDateTime *last_used = NULL;
+	guint counter = 0;
+	guint use_count = 0;
+	const char * const expected[] = {NULL};
+
+	command = purple_command_new("command", "test", 0);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_counter_cb),
+	                 &counter);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_params_cb),
+	                 (gpointer)expected);
+
+	purple_command_executev(command, NULL, (GStrv)expected);
+	g_assert_cmpuint(counter, ==, 1);
+
+	last_used = purple_command_get_last_used(command);
+	g_assert_nonnull(last_used);
+
+	use_count = purple_command_get_use_count(command);
+	g_assert_cmpuint(use_count, ==, 1);
+
+	g_assert_finalize_object(command);
+}
+
+static void
+test_purple_command_executev_single_param(void) {
+	PurpleCommand *command = NULL;
+	GDateTime *last_used = NULL;
+	guint counter = 0;
+	guint use_count = 0;
+	const char * const expected[] = {"param1", NULL};
+
+	command = purple_command_new("command", "test", 0);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_counter_cb),
+	                 &counter);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_params_cb),
+	                 (gpointer)expected);
+
+	purple_command_executev(command, NULL, (GStrv)expected);
+	g_assert_cmpuint(counter, ==, 1);
+
+	last_used = purple_command_get_last_used(command);
+	g_assert_nonnull(last_used);
+
+	use_count = purple_command_get_use_count(command);
+	g_assert_cmpuint(use_count, ==, 1);
+
+	g_assert_finalize_object(command);
+}
+
+static void
+test_purple_command_executev_multiple_params(void) {
+	PurpleCommand *command = NULL;
+	GDateTime *last_used = NULL;
+	guint counter = 0;
+	guint use_count = 0;
+	const char * const expected[] = {"param1", "param2", "param3", NULL};
+
+	command = purple_command_new("command", "test", 0);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_counter_cb),
+	                 &counter);
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(test_purple_command_executed_params_cb),
+	                 (gpointer)expected);
+
+	purple_command_executev(command, NULL, (GStrv)expected);
+	g_assert_cmpuint(counter, ==, 1);
+
+	last_used = purple_command_get_last_used(command);
+	g_assert_nonnull(last_used);
+
+	use_count = purple_command_get_use_count(command);
+	g_assert_cmpuint(use_count, ==, 1);
+
+	g_assert_finalize_object(command);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+	g_test_set_nonfatal_assertions();
+
+	g_test_add_func("/command/new", test_purple_command_new);
+	g_test_add_func("/command/properties", test_purple_command_properties);
+
+	g_test_add_func("/command/execute/null-params",
+	                test_purple_command_execute_null_params);
+	g_test_add_func("/command/execute/empty-params",
+	                test_purple_command_execute_empty_params);
+	g_test_add_func("/command/execute/single-param",
+	                test_purple_command_execute_single_param);
+	g_test_add_func("/command/execute/multiple-params",
+	                test_purple_command_execute_multiple_params);
+
+	g_test_add_func("/command/executev/null-params",
+	                test_purple_command_executev_null_params);
+	g_test_add_func("/command/executev/empty-params",
+	                test_purple_command_executev_empty_params);
+	g_test_add_func("/command/executev/single-param",
+	                test_purple_command_executev_single_param);
+	g_test_add_func("/command/executev/multiple-params",
+	                test_purple_command_executev_multiple_params);
+
+	return g_test_run();
+}
