# HG changeset patch
# Node ID ae485d431c26b25a08809c181cbf3d724a36c053
# Parent  917386321169f4cd71846852e13c57d00fd905f6
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -20,6 +20,7 @@
 	'purplebadges.c',
 	'purplechanneljoindetails.c',
 	'purplecommand.c',
+	'purplecommands.c',
 	'purplecommandmanager.c',
 	'purpleconnection.c',
 	'purplecontact.c',
@@ -112,6 +113,7 @@
 	'purplebadges.h',
 	'purplechanneljoindetails.h',
 	'purplecommand.h',
+	'purplecommands.h',
 	'purplecommandmanager.h',
 	'purpleconnection.h',
 	'purplecontact.h',
diff --git a/libpurple/purplecommandmanager.c b/libpurple/purplecommandmanager.c
--- a/libpurple/purplecommandmanager.c
+++ b/libpurple/purplecommandmanager.c
@@ -23,6 +23,7 @@
 #include "purplecommandmanager.h"
 #include "purplecommandmanagerprivate.h"
 
+#include "purplecommands.h"
 #include "util.h"
 
 struct _PurpleCommandManager {
@@ -130,6 +131,14 @@
 	if(default_manager == NULL) {
 		default_manager = purple_command_manager_new();
 		if(PURPLE_IS_COMMAND_MANAGER(default_manager)) {
+			PurpleCommand *command = NULL;
+
+			command = purple_say_command_new();
+			purple_command_manager_add(default_manager, command);
+
+			command = purple_action_command_new();
+			purple_command_manager_add(default_manager, command);
+
 			g_object_add_weak_pointer(G_OBJECT(default_manager),
 			                          (gpointer *)&default_manager);
 		}
diff --git a/libpurple/purplecommands.c b/libpurple/purplecommands.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purplecommands.c
@@ -0,0 +1,117 @@
+/*
+ * 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 <glib/gi18n-lib.h>
+
+#include "purplecommands.h"
+
+#include "purpleconversation.h"
+#include "purplemessage.h"
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static PurpleMessage *
+purple_message_from_command(PurpleConversation *conversation, GStrv params) {
+	PurpleAccount *account = NULL;
+	PurpleContactInfo *info = NULL;
+	PurpleMessage *message = NULL;
+	char *body = NULL;
+
+	account = purple_conversation_get_account(conversation);
+	if(!PURPLE_IS_ACCOUNT(account)) {
+		return NULL;
+	}
+
+	info = purple_account_get_contact_info(account);
+
+	body = g_strjoinv(" ", params);
+	message = purple_message_new(info, body);
+	g_free(body);
+
+	return message;
+}
+
+/******************************************************************************
+ * Action Command
+ *****************************************************************************/
+static void
+purple_action_command_executed(G_GNUC_UNUSED PurpleCommand *command,
+                               PurpleConversation *conversation,
+                               GStrv params,
+                               G_GNUC_UNUSED gpointer data)
+{
+	PurpleMessage *message = NULL;
+
+	message = purple_message_from_command(conversation, params);
+	if(PURPLE_IS_MESSAGE(message)) {
+		purple_message_set_action(message, TRUE);
+		purple_conversation_send_message_async(conversation, message, NULL,
+		                                       NULL, NULL);
+	}
+
+	g_clear_object(&message);
+}
+
+PurpleCommand *
+purple_action_command_new(void) {
+	PurpleCommand *command = NULL;
+
+	command = purple_command_new("action", "libpurple", 0);
+	purple_command_set_summary(command, _("Sends a message as an action"));
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(purple_action_command_executed), NULL);
+
+	return command;
+}
+
+/******************************************************************************
+ * Say Command
+ *****************************************************************************/
+static void
+purple_say_command_executed(G_GNUC_UNUSED PurpleCommand *command,
+                            PurpleConversation *conversation,
+                            GStrv params,
+                            G_GNUC_UNUSED gpointer data)
+{
+	PurpleMessage *message = NULL;
+
+	message = purple_message_from_command(conversation, params);
+	if(PURPLE_IS_MESSAGE(message)) {
+		purple_conversation_send_message_async(conversation, message, NULL,
+		                                       NULL, NULL);
+	}
+
+	g_clear_object(&message);
+}
+
+PurpleCommand *
+purple_say_command_new(void) {
+	PurpleCommand *command = NULL;
+
+	command = purple_command_new("say", "libpurple", 0);
+	purple_command_set_summary(command, _("Sends a message"));
+	g_signal_connect(command, "executed",
+	                 G_CALLBACK(purple_say_command_executed), NULL);
+
+	return command;
+}
diff --git a/libpurple/purplecommands.h b/libpurple/purplecommands.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purplecommands.h
@@ -0,0 +1,74 @@
+/*
+ * 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_COMMANDS_H
+#define PURPLE_COMMANDS_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "purplecommand.h"
+#include "purpleversion.h"
+
+G_BEGIN_DECLS
+
+/**
+ * purple_action_command_new:
+ *
+ * Create a new `action` command.
+ *
+ * An action command will create a message out of the given parameters and set
+ * [property@Message:action] to %TRUE on the message before sending it.
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleCommand *purple_action_command_new(void);
+
+/**
+ * purple_say_command_new:
+ *
+ * Creates a new `say` command.
+ *
+ * The `say` command creates a message from the parameters and writes it to the
+ * conversation. This is typically used in combination with the `alias` command
+ * to create shortcut commands.
+ *
+ * The `say` command has no tags meaning it will be available in all
+ * conversations.
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleCommand *purple_say_command_new(void);
+
+G_END_DECLS
+
+#endif /* PURPLE_COMMANDS_H */
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -9,6 +9,7 @@
     'circular_buffer',
     'create_conversation_details',
     'command',
+    'commands',
     'command_manager',
     'contact',
     'contact_info',
diff --git a/libpurple/tests/test_commands.c b/libpurple/tests/test_commands.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_commands.c
@@ -0,0 +1,212 @@
+/*
+ * 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>
+
+/******************************************************************************
+ * Test Protocol
+ *****************************************************************************/
+G_DECLARE_FINAL_TYPE(TestPurpleCommandsProtocol, test_purple_commands_protocol,
+                     TEST_PURPLE, COMMANDS_PROTOCOL, PurpleProtocol)
+
+struct _TestPurpleCommandsProtocol {
+	PurpleProtocol parent;
+};
+
+static void
+test_purple_commands_protocol_send_message_async(PurpleProtocolConversation *protocol,
+                                                 PurpleConversation *conversation,
+                                                 PurpleMessage *message,
+                                                 GCancellable *cancellable,
+                                                 GAsyncReadyCallback callback,
+                                                 gpointer data)
+{
+	GTask *task = NULL;
+
+	task = g_task_new(G_OBJECT(protocol), cancellable, callback, data);
+	g_task_return_boolean(task, TRUE);
+	g_clear_object(&task);
+
+	purple_conversation_write_message(conversation, message);
+}
+
+static gboolean
+test_purple_commands_protocol_send_message_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
+                                                  GAsyncResult* result,
+                                                  GError** error)
+{
+	return g_task_propagate_boolean(G_TASK(result), error);
+}
+
+static void
+test_purple_commands_protocol_conversation_init(PurpleProtocolConversationInterface *iface)
+{
+	iface->send_message_async =
+		test_purple_commands_protocol_send_message_async;
+	iface->send_message_finish =
+		test_purple_commands_protocol_send_message_finish;
+}
+
+G_DEFINE_FINAL_TYPE_WITH_CODE(TestPurpleCommandsProtocol,
+                              test_purple_commands_protocol,
+                              PURPLE_TYPE_PROTOCOL,
+                              G_IMPLEMENT_INTERFACE(PURPLE_TYPE_PROTOCOL_CONVERSATION,
+                                                    test_purple_commands_protocol_conversation_init))
+
+static void
+test_purple_commands_protocol_init(G_GNUC_UNUSED TestPurpleCommandsProtocol *protocol)
+{
+}
+
+static void
+test_purple_commands_protocol_class_init(G_GNUC_UNUSED TestPurpleCommandsProtocolClass *klass)
+{
+}
+
+static PurpleProtocol *
+test_purple_commands_protocol_new(void) {
+	return g_object_new(
+		test_purple_commands_protocol_get_type(),
+		"id", "test",
+		"name", "test",
+		NULL);
+}
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static PurpleConversation *
+test_purple_commands_create_conversation(void) {
+	PurpleAccount *account = NULL;
+	PurpleConversation *conversation = NULL;
+	PurpleProtocol *protocol = NULL;
+
+	protocol = test_purple_commands_protocol_new();
+	account = g_object_new(PURPLE_TYPE_ACCOUNT,
+		"protocol", protocol,
+		"username", "slater",
+		NULL);
+	g_clear_object(&protocol);
+
+	conversation = g_object_new(
+		PURPLE_TYPE_CONVERSATION,
+		"account", account,
+		NULL);
+
+	g_clear_object(&account);
+
+	return conversation;
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_commands_say_execute(void) {
+	PurpleCommand *command = NULL;
+	PurpleConversation *conversation = NULL;
+	PurpleMessage *message = NULL;
+	GListModel *messages = NULL;
+	const char *contents = NULL;
+
+	conversation = test_purple_commands_create_conversation();
+
+	command = purple_say_command_new();
+	purple_command_execute(command, conversation, "my name");
+
+	/* We have to iterate the main loop as this command calls an async method
+	 * which holds an extra reference on the command. */
+	g_main_context_iteration(NULL, FALSE);
+
+	g_assert_cmpuint(purple_command_get_use_count(command), ==, 1);
+	g_assert_finalize_object(command);
+
+	messages = purple_conversation_get_messages(conversation);
+	g_assert_true(G_IS_LIST_MODEL(messages));
+	g_assert_cmpuint(g_list_model_get_n_items(messages), ==, 1);
+
+	message = g_list_model_get_item(messages, 0);
+	g_assert_true(PURPLE_IS_MESSAGE(message));
+
+	contents = purple_message_get_contents(message);
+	g_assert_cmpstr(contents, ==, "my name");
+
+	/* The message still exists in the conversation, so we can only removed our
+	 * reference which we got from g_list_model_get_item above.
+	 */
+	g_clear_object(&message);
+
+	g_assert_finalize_object(conversation);
+}
+
+static void
+test_purple_commands_action_execute(void) {
+	PurpleCommand *command = NULL;
+	PurpleConversation *conversation = NULL;
+	PurpleMessage *message = NULL;
+	GListModel *messages = NULL;
+	const char *contents = NULL;
+
+	conversation = test_purple_commands_create_conversation();
+
+	command = purple_action_command_new();
+	purple_command_execute(command, conversation, "sighs");
+
+	/* We have to iterate the main loop as this command calls an async method
+	 * which holds an extra reference on the command. */
+	g_main_context_iteration(NULL, FALSE);
+
+	g_assert_cmpuint(purple_command_get_use_count(command), ==, 1);
+	g_assert_finalize_object(command);
+
+	messages = purple_conversation_get_messages(conversation);
+	g_assert_true(G_IS_LIST_MODEL(messages));
+	g_assert_cmpuint(g_list_model_get_n_items(messages), ==, 1);
+
+	message = g_list_model_get_item(messages, 0);
+	g_assert_true(PURPLE_IS_MESSAGE(message));
+
+	contents = purple_message_get_contents(message);
+	g_assert_cmpstr(contents, ==, "sighs");
+
+	g_assert_true(purple_message_get_action(message));
+
+	/* The message still exists in the conversation, so we can only removed our
+	 * reference which we got from g_list_model_get_item above.
+	 */
+	g_clear_object(&message);
+
+	g_assert_finalize_object(conversation);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+	g_test_set_nonfatal_assertions();
+
+	g_test_add_func("/commands/say/execute", test_purple_commands_say_execute);
+	g_test_add_func("/commands/action/execute",
+	                test_purple_commands_action_execute);
+
+	return g_test_run();
+}
