Mon, 04 Nov 2024 20:12:42 -0600
Implement Purple.CommandManager
This ended up deviating from the design a bit, but I think I got everything we
need at least for now.
Testing Done:
Ran the tests under valgrind and called in the turtles for the rest. Also ran in a devenv to make sure everything started fine.
Bugs closed: PIDGIN-17969
Reviewed at https://reviews.imfreedom.org/r/3619/
--- a/libpurple/core.c Sun Nov 03 00:23:49 2024 -0500 +++ b/libpurple/core.c Mon Nov 04 20:12:42 2024 -0600 @@ -35,6 +35,7 @@ #include "proxy.h" #include "purpleaccountmanager.h" #include "purpleaccountmanagerprivate.h" +#include "purplecommandmanagerprivate.h" #include "purpleconnection.h" #include "purplecontactmanagerprivate.h" #include "purpleconversationmanagerprivate.h" @@ -142,6 +143,7 @@ purple_contact_manager_startup(); purple_presence_manager_startup(); purple_conversation_manager_startup(); + purple_command_manager_startup(); purple_whiteboard_manager_startup(); /* Setup the history adapter. */ @@ -207,6 +209,7 @@ /* Save .xml files, remove signals, etc. */ purple_idle_manager_shutdown(); + purple_command_manager_shutdown(); purple_whiteboard_manager_shutdown(); purple_conversation_manager_shutdown(); purple_presence_manager_shutdown();
--- a/libpurple/meson.build Sun Nov 03 00:23:49 2024 -0500 +++ b/libpurple/meson.build Mon Nov 04 20:12:42 2024 -0600 @@ -20,6 +20,7 @@ 'purplebadges.c', 'purplechanneljoindetails.c', 'purplecommand.c', + 'purplecommandmanager.c', 'purpleconnection.c', 'purplecontact.c', 'purplecontactinfo.c', @@ -111,6 +112,7 @@ 'purplebadges.h', 'purplechanneljoindetails.h', 'purplecommand.h', + 'purplecommandmanager.h', 'purpleconnection.h', 'purplecontact.h', 'purplecontactinfo.h', @@ -176,6 +178,7 @@ purple_private_headers = [ 'purpleaccountprivate.h', 'purpleaccountmanagerprivate.h', + 'purplecommandmanagerprivate.h', 'purplecontactmanagerprivate.h', 'purpleconversationmanagerprivate.h', 'purplecredentialmanagerprivate.h',
--- a/libpurple/purplecommand.c Sun Nov 03 00:23:49 2024 -0500 +++ b/libpurple/purplecommand.c Mon Nov 04 20:12:42 2024 -0600 @@ -317,6 +317,11 @@ * * The [class@Tags] for the command. * + * These tags will be used with [property@Conversation:tags] in a call to + * [method@Tags.contains] to determine if the command is valid for a + * conversation. Likewise, if this doesn't contain any tags, it will match + * all conversations. + * * Since: 3.0 */ properties[PROP_TAGS] = g_param_spec_object(
--- a/libpurple/purplecommand.h Sun Nov 03 00:23:49 2024 -0500 +++ b/libpurple/purplecommand.h Mon Nov 04 20:12:42 2024 -0600 @@ -175,6 +175,8 @@ * [method@Tags.contains] to determine if command is available for a * [class@Conversation]. * + * If this is empty, it will match all conversations. + * * Returns: (transfer none): The tags object. * * Since: 3.0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/purplecommandmanager.c Mon Nov 04 20:12:42 2024 -0600 @@ -0,0 +1,400 @@ +/* + * 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 "purplecommandmanager.h" +#include "purplecommandmanagerprivate.h" + +#include "util.h" + +struct _PurpleCommandManager { + GObject parent; + + GPtrArray *commands; +}; + +static PurpleCommandManager *default_manager = NULL; + +/****************************************************************************** + * GListModel Implementation + *****************************************************************************/ +static GType +purple_command_manager_get_item_type(G_GNUC_UNUSED GListModel *list) { + return PURPLE_TYPE_COMMAND; +} + +static guint +purple_command_manager_get_n_items(GListModel *list) { + PurpleCommandManager *manager = PURPLE_COMMAND_MANAGER(list); + + return manager->commands->len; +} + +static gpointer +purple_command_manager_get_item(GListModel *list, guint position) { + PurpleCommandManager *manager = PURPLE_COMMAND_MANAGER(list); + PurpleCommand *command = NULL; + + if(position < manager->commands->len) { + command = g_ptr_array_index(manager->commands, position); + g_object_ref(command); + } + + return command; +} + +static void +purple_command_manager_list_model_init(GListModelInterface *iface) { + iface->get_item_type = purple_command_manager_get_item_type; + iface->get_n_items = purple_command_manager_get_n_items; + iface->get_item = purple_command_manager_get_item; +} + +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +G_DEFINE_FINAL_TYPE_WITH_CODE(PurpleCommandManager, purple_command_manager, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL, + purple_command_manager_list_model_init)) + +static void +purple_command_manager_finalize(GObject *obj) { + PurpleCommandManager *manager = PURPLE_COMMAND_MANAGER(obj); + + g_clear_pointer(&manager->commands, g_ptr_array_unref); + + G_OBJECT_CLASS(purple_command_manager_parent_class)->finalize(obj); +} + +static void +purple_command_manager_init(PurpleCommandManager *manager) { + manager->commands = g_ptr_array_new_full(10, g_object_unref); +} + +static void +purple_command_manager_class_init(PurpleCommandManagerClass *klass) { + GObjectClass *obj_class = G_OBJECT_CLASS(klass); + + obj_class->finalize = purple_command_manager_finalize; +} + +static gboolean +purple_command_manager_commands_equal(gconstpointer a, gconstpointer b) { + PurpleCommand *command1 = (gpointer)a; + PurpleCommand *command2 = (gpointer)b; + const char *name1 = NULL; + const char *name2 = NULL; + + name1 = purple_command_get_name(command1); + name2 = purple_command_get_name(command2); + + if(purple_strequal(name1, name2)) { + const char *source1 = NULL; + const char *source2 = NULL; + + source1 = purple_command_get_source(command1); + source2 = purple_command_get_source(command2); + + if(purple_strequal(source1, source2)) { + return TRUE; + } + } + + return FALSE; +} + +/****************************************************************************** + * Private API + *****************************************************************************/ +void +purple_command_manager_startup(void) { + if(default_manager == NULL) { + default_manager = purple_command_manager_new(); + if(PURPLE_IS_COMMAND_MANAGER(default_manager)) { + g_object_add_weak_pointer(G_OBJECT(default_manager), + (gpointer *)&default_manager); + } + } +} + +void +purple_command_manager_shutdown(void) { + g_clear_object(&default_manager); +} + +/****************************************************************************** + * Public API + *****************************************************************************/ +void +purple_command_manager_add(PurpleCommandManager *manager, + PurpleCommand *command) +{ + gboolean found = FALSE; + + g_return_if_fail(PURPLE_IS_COMMAND_MANAGER(manager)); + g_return_if_fail(PURPLE_IS_COMMAND(command)); + + /* If the manager already knows about the command, we do nothing. */ + found = g_ptr_array_find_with_equal_func(manager->commands, command, + purple_command_manager_commands_equal, + NULL); + if(!found) { + g_ptr_array_add(manager->commands, command); + + g_list_model_items_changed(G_LIST_MODEL(manager), + manager->commands->len - 1, 0, 1); + } else { + g_object_unref(command); + } +} + +PurpleCommand * +purple_command_manager_find(PurpleCommandManager *manager, + PurpleConversation *conversation, + const char *name) +{ + PurpleCommand *command = NULL; + PurpleTags *conversation_tags = NULL; + int current_priority = 0; + + g_return_val_if_fail(PURPLE_IS_COMMAND_MANAGER(manager), NULL); + g_return_val_if_fail(!purple_strempty(name), NULL); + + if(PURPLE_IS_CONVERSATION(conversation)) { + conversation_tags = purple_conversation_get_tags(conversation); + } + + for(guint i = 0; i < manager->commands->len; i++) { + PurpleCommand *candidate = NULL; + const char *candidate_name = NULL; + int candidate_priority = 0; + + candidate = g_ptr_array_index(manager->commands, i); + if(!PURPLE_IS_COMMAND(candidate)) { + continue; + } + + candidate_name = purple_command_get_name(candidate); + if(!purple_strequal(candidate_name, name)) { + continue; + } + + if(conversation_tags != NULL) { + PurpleTags *command_tags = purple_command_get_tags(candidate); + + if(!purple_tags_contains(conversation_tags, command_tags)) { + continue; + } + } + + candidate_priority = purple_command_get_priority(candidate); + + if(command == NULL || candidate_priority > current_priority) { + command = candidate; + current_priority = candidate_priority; + } + } + + return command; +} + +GListModel * +purple_command_manager_find_all(PurpleCommandManager *manager, + PurpleConversation *conversation, + const char *name) +{ + PurpleTags *conversation_tags = NULL; + GListStore *commands = NULL; + + g_return_val_if_fail(PURPLE_IS_COMMAND_MANAGER(manager), NULL); + g_return_val_if_fail(!purple_strempty(name), NULL); + + if(PURPLE_IS_CONVERSATION(conversation)) { + conversation_tags = purple_conversation_get_tags(conversation); + } + + commands = g_list_store_new(PURPLE_TYPE_COMMAND); + + for(guint i = 0; i < manager->commands->len; i++) { + PurpleCommand *command = NULL; + const char *command_name = NULL; + + command = g_ptr_array_index(manager->commands, i); + if(!PURPLE_IS_COMMAND(command)) { + continue; + } + + command_name = purple_command_get_name(command); + if(!purple_strequal(command_name, name)) { + continue; + } + + if(conversation_tags != NULL) { + PurpleTags *command_tags = purple_command_get_tags(command); + + if(!purple_tags_contains(conversation_tags, command_tags)) { + continue; + } + } + + g_list_store_append(commands, command); + } + + return G_LIST_MODEL(commands); +} + +gboolean +purple_command_manager_find_and_execute(PurpleCommandManager *manager, + PurpleConversation *conversation, + const char *command_line) +{ + PurpleCommand *command = NULL; + GStrv params = NULL; + char *command_name = NULL; + gboolean ret = FALSE; + + g_return_val_if_fail(PURPLE_IS_COMMAND_MANAGER(manager), FALSE); + g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE); + g_return_val_if_fail(!purple_strempty(command_line), FALSE); + + params = g_strsplit(command_line, " ", -1); + command_name = params[0]; + + command = purple_command_manager_find(manager, conversation, command_name); + if(PURPLE_IS_COMMAND(command)) { + purple_command_executev(command, conversation, params + 1); + + ret = TRUE; + } + + g_strfreev(params); + + return ret; +} + +PurpleCommandManager * +purple_command_manager_get_default(void) { + return default_manager; +} + +GListModel * +purple_command_manager_get_default_as_model(void) { + return G_LIST_MODEL(default_manager); +} + +GListModel * +purple_command_manager_get_commands_for_conversation(PurpleCommandManager *manager, + PurpleConversation *conversation) +{ + PurpleTags *conversation_tags = NULL; + GListStore *commands = NULL; + + g_return_val_if_fail(PURPLE_IS_COMMAND_MANAGER(manager), NULL); + g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL); + + commands = g_list_store_new(PURPLE_TYPE_COMMAND); + conversation_tags = purple_conversation_get_tags(conversation); + + for(guint i = 0; i < manager->commands->len; i++) { + PurpleCommand *command = NULL; + PurpleTags *command_tags = NULL; + + command = g_ptr_array_index(manager->commands, i); + command_tags = purple_command_get_tags(command); + + if(purple_tags_contains(conversation_tags, command_tags)) { + g_list_store_append(commands, command); + } + } + + return G_LIST_MODEL(commands); +} + +PurpleCommandManager * +purple_command_manager_new(void) { + return g_object_new(PURPLE_TYPE_COMMAND_MANAGER, NULL); +} + +gboolean +purple_command_manager_remove(PurpleCommandManager *manager, const char *name, + const char *source) +{ + g_return_val_if_fail(PURPLE_IS_COMMAND_MANAGER(manager), FALSE); + + for(guint i = 0; i < manager->commands->len; i++) { + PurpleCommand *command = NULL; + const char *command_name = NULL; + const char *command_source = NULL; + + command = g_ptr_array_index(manager->commands, i); + if(!PURPLE_IS_COMMAND(command)) { + continue; + } + + command_name = purple_command_get_name(command); + command_source = purple_command_get_source(command); + if(purple_strequal(command_name, name) && + purple_strequal(command_source, source)) + { + g_ptr_array_remove_index(manager->commands, i); + + g_list_model_items_changed(G_LIST_MODEL(manager), i, 1, 0); + + return TRUE; + } + } + + return FALSE; +} + +void +purple_command_manager_remove_all_with_source(PurpleCommandManager *manager, + const char *source) +{ + g_return_if_fail(PURPLE_IS_COMMAND_MANAGER(manager)); + g_return_if_fail(!purple_strempty(source)); + + /* Since GPtrArray shifts everything down on a remove, we only increment + * when we haven't removed a command. + */ + for(guint i = 0; i < manager->commands->len;) { + PurpleCommand *command = NULL; + const char *command_source = NULL; + + command = g_ptr_array_index(manager->commands, i); + if(!PURPLE_IS_COMMAND(command)) { + continue; + } + + command_source = purple_command_get_source(command); + if(purple_strequal(command_source, source)) { + g_ptr_array_remove_index(manager->commands, i); + + /* TODO: optimize this so we notify when a group of items is + * removed instead of notifying for every remove. + */ + g_list_model_items_changed(G_LIST_MODEL(manager), i, 1, 0); + } else { + i++; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/purplecommandmanager.h Mon Nov 04 20:12:42 2024 -0600 @@ -0,0 +1,228 @@ +/* + * 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_MANAGER_H +#define PURPLE_COMMAND_MANAGER_H + +#include <glib.h> +#include <glib-object.h> + +#include "purplecommand.h" +#include "purpleconversation.h" +#include "purpleversion.h" + +G_BEGIN_DECLS + +/** + * PurpleCommandManager: + * + * A manager of [class@Command] objects. + * + * libpurple, user interfaces, and plugins can manage the commands that are + * available with [method@CommandManager.add] and + * [method@CommandManager.remove]. + * + * User interfaces can use [method@CommandManager.find_and_execute] to quickly + * find and execute a command. They can also use + * [method@CommandManager.find_all] to get a list of all commands which can be + * presented to a user. + * + * Both [method@CommandManager.find] and [method@CommandManager.find_all] take + * an optional [class@Conversation] parameter. If it is not %NULL, its + * [property@Conversation:tags] property will be searched with + * [method@Tags.contains] using the [property@Command:tags] as the needle to + * determine which commands are available for the [class@Conversation]. + * + * When plugins are being unloaded, they should call + * [method@CommandManager.remove_all_with_source] to remove their commands. + * + * Since: 3.0 + */ + +#define PURPLE_TYPE_COMMAND_MANAGER (purple_command_manager_get_type()) + +PURPLE_AVAILABLE_IN_3_0 +G_DECLARE_FINAL_TYPE(PurpleCommandManager, purple_command_manager, PURPLE, + COMMAND_MANAGER, GObject) + +/** + * purple_command_manager_add: + * @manager: The instance. + * @command: (transfer full): The new command to add. + * + * Adds @command to @manager. + * + * If @manager already has a command with a matching name and source, @command + * will not be added but ownership will still be taken. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +void purple_command_manager_add(PurpleCommandManager *manager, PurpleCommand *command); + +/** + * purple_command_manager_find: + * @manager: The instance. + * @conversation: (nullable): An optional conversation. + * @name: The name of the command. + * + * Finds the command with a name of @name with the highest priority. + * + * If @conversation is not %NULL commands will be filtered to only include + * those that are valid for @conversation by using [method@Tags.contains] on + * @conversation with the tags of the found commands. + * + * Returns: (transfer none): The command if found, otherwise %NULL. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +PurpleCommand *purple_command_manager_find(PurpleCommandManager *manager, PurpleConversation *conversation, const char *name); + +/** + * purple_command_manager_find_all: + * @manager: The instance. + * @conversation: (nullable): An optional conversation. + * @name: The name of the command. + * + * Finds all commands that match @name in @manager. + * + * If @conversation is not %NULL, [method@Tags.contains] will be used to filter + * out commands whose tags don't match @conversation. + * + * Returns: (transfer full): The list of found plugins which could be empty. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +GListModel *purple_command_manager_find_all(PurpleCommandManager *manager, PurpleConversation *conversation, const char *name); + +/** + * purple_command_manager_find_and_execute: + * @manager: The instance. + * @conversation: The conversation to use. + * @command_line: The command line including the name and arguments. + * + * Attempts to find a [class@Command] in @manager and execute it. + * + * @command_line should be a space separate string of the command name and any + * arguments. It should not include a leading `/` or any other prefix. + * + * This method is a helper around [method@CommandManager.find] and + * [method@Command.execute]. + * + * Returns: %TRUE if a command was found and executed, otherwise %FALSE. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +gboolean purple_command_manager_find_and_execute(PurpleCommandManager *manager, PurpleConversation *conversation, const char *command_line); + +/** + * purple_command_manager_get_commands_for_conversation: + * @manager: The instance. + * @conversation: The conversation. + * + * Gets a list of [class@Command]'s that are available for @conversation. + * + * Internally this filters the list of commands with [method@Tags.contains] to + * check that all of [property@Command:tags] are contained in + * [property@Conversation:tags]. + * + * Returns: (transfer full): The list of commands, which may be empty. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +GListModel *purple_command_manager_get_commands_for_conversation(PurpleCommandManager *manager, PurpleConversation *conversation); + +/** + * purple_command_manager_get_default: + * + * Gets the default instance that libpurple is using. + * + * Returns: (transfer none): The instance. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +PurpleCommandManager *purple_command_manager_get_default(void); + +/** + * purple_command_manager_get_default_as_model: + * + * Gets the default instance of the manager but cast to [iface@Gio.ListModel]. + * + * Returns: (transfer none): The manager cast as a [iface@Gio.ListModel]. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +GListModel *purple_command_manager_get_default_as_model(void); + +/** + * purple_command_manager_new: + * + * Creates a new instance. + * + * This is typically only used by libpurple but you can get the default + * instance that libpurple is using with [func@CommandManager.get_default]. + * + * Returns: (transfer full): The new instance. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +PurpleCommandManager *purple_command_manager_new(void); + +/** + * purple_command_manager_remove: + * @manager: The instance. + * @name: The name of the command. + * @source: The source of the command. + * + * Attempts to remove the first command with @name and @source from @manager. + * + * Returns: %TRUE if a command is found and removed, otherwise %FALSE. + */ +PURPLE_AVAILABLE_IN_3_0 +gboolean purple_command_manager_remove(PurpleCommandManager *manager, const char *name, const char *source); + +/** + * purple_command_manager_remove_all_with_source: + * @manager: The instance. + * @source: The source whose commands to remove. + * + * Removes all commands from @manager that have a source of @source. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +void purple_command_manager_remove_all_with_source(PurpleCommandManager *manager, const char *source); + +G_END_DECLS + +#endif /* PURPLE_COMMAND_MANAGER_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/purplecommandmanagerprivate.h Mon Nov 04 20:12:42 2024 -0600 @@ -0,0 +1,56 @@ +/* + * 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_MANAGER_PRIVATE_H +#define PURPLE_COMMAND_MANAGER_PRIVATE_H + +#include <glib.h> + +G_BEGIN_DECLS + +/** + * purple_command_manager_startup: (skip) + * + * Initializes the default [class@CommandManager]. + * + * Since: 3.0 + */ +G_GNUC_INTERNAL +void purple_command_manager_startup(void); + +/** + * purple_command_manager_shutdown: (skip) + * + * Cleans up the default [class@CommandManager]. + * + * Since: 3.0 + */ +G_GNUC_INTERNAL +void purple_command_manager_shutdown(void); + +G_END_DECLS + +#endif /* PURPLE_COMMAND_MANAGER_PRIVATE_H */
--- a/libpurple/tests/meson.build Sun Nov 03 00:23:49 2024 -0500 +++ b/libpurple/tests/meson.build Mon Nov 04 20:12:42 2024 -0600 @@ -8,6 +8,7 @@ 'circular_buffer', 'create_conversation_details', 'command', + 'command_manager', 'contact', 'contact_info', 'contact_manager',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/tests/test_command_manager.c Mon Nov 04 20:12:42 2024 -0600 @@ -0,0 +1,419 @@ +/* + * 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> + +/****************************************************************************** + * Resuable callbacks + *****************************************************************************/ +static void +test_purple_command_manager_items_changed_cb(GListModel *model, + G_GNUC_UNUSED guint position, + G_GNUC_UNUSED guint removed, + G_GNUC_UNUSED guint added, + gpointer data) +{ + guint *counter = data; + + g_assert_true(PURPLE_IS_COMMAND_MANAGER(model)); + + *counter = *counter + 1; +} + +/****************************************************************************** + * Tests + *****************************************************************************/ +static void +test_purple_command_manager_new(void) { + PurpleCommandManager *manager = NULL; + + manager = purple_command_manager_new(); + g_assert_true(PURPLE_IS_COMMAND_MANAGER(manager)); + + g_assert_finalize_object(manager); +} + +static void +test_purple_command_manager_add_remove(void) { + PurpleCommand *command = NULL; + PurpleCommandManager *manager = NULL; + GListModel *model = NULL; + gboolean ret = FALSE; + guint counter = 0; + + manager = purple_command_manager_new(); + model = G_LIST_MODEL(manager); + g_signal_connect(manager, "items-changed", + G_CALLBACK(test_purple_command_manager_items_changed_cb), + &counter); + + g_assert_cmpuint(g_list_model_get_n_items(model), ==, 0); + + command = purple_command_new("test-command", "test", 0); + /* We use command again in the next test. */ + g_object_ref(command); + purple_command_manager_add(manager, command); + g_assert_cmpuint(g_list_model_get_n_items(model), ==, 1); + g_assert_cmpuint(counter, ==, 1); + + /* Duplicate adds should be ignore. */ + counter = 0; + purple_command_manager_add(manager, command); + g_assert_cmpuint(g_list_model_get_n_items(model), ==, 1); + g_assert_cmpuint(counter, ==, 0); + + /* Duplicate command but as a new pointer and make sure it doesn't get + * added. + */ + counter = 0; + command = purple_command_new("test-command", "test", 0); + purple_command_manager_add(manager, command); + g_assert_cmpuint(g_list_model_get_n_items(model), ==, 1); + g_assert_cmpuint(counter, ==, 0); + + /* Name and command must match. */ + counter = 0; + ret = purple_command_manager_remove(manager, "unknown-command", "test"); + g_assert_false(ret); + g_assert_cmpuint(g_list_model_get_n_items(model), ==, 1); + g_assert_cmpuint(counter, ==, 0); + + counter = 0; + ret = purple_command_manager_remove(manager, "test-command", "unknown"); + g_assert_false(ret); + g_assert_cmpuint(g_list_model_get_n_items(model), ==, 1); + g_assert_cmpuint(counter, ==, 0); + + /* Do the real remove. */ + counter = 0; + ret = purple_command_manager_remove(manager, "test-command", "test"); + g_assert_true(ret); + g_assert_cmpuint(g_list_model_get_n_items(model), ==, 0); + g_assert_cmpuint(counter, ==, 1); + + /* Duplicate removes should return false as the item is gone. */ + counter = 0; + ret = purple_command_manager_remove(manager, "test-command", "test"); + g_assert_false(ret); + g_assert_cmpuint(g_list_model_get_n_items(model), ==, 0); + g_assert_cmpuint(counter, ==, 0); + + g_assert_finalize_object(manager); +} + +static void +test_purple_command_manager_remove_all_with_source(void) { + PurpleCommand *command = NULL; + PurpleCommandManager *manager = NULL; + GListModel *model = NULL; + guint counter = 0; + + manager = purple_command_manager_new(); + model = G_LIST_MODEL(manager); + g_signal_connect(manager, "items-changed", + G_CALLBACK(test_purple_command_manager_items_changed_cb), + &counter); + + /* Make sure remove all works on an empty manager. */ + counter = 0; + purple_command_manager_remove_all_with_source(manager, "test-1"); + g_assert_cmpuint(g_list_model_get_n_items(model), ==, 0); + g_assert_cmpuint(counter, ==, 0); + + /* Add 3 commands, two with the same source and another with a different + * source in between them. + */ + command = purple_command_new("privmsg", "test-1", 0); + purple_command_manager_add(manager, command); + + command = purple_command_new("xyzzy", "test-2", 0); + purple_command_manager_add(manager, command); + + command = purple_command_new("query", "test-1", 0); + purple_command_manager_add(manager, command); + + g_assert_cmpuint(g_list_model_get_n_items(model), ==, 3); + + /* Remove the commands with test-1 as a source and verify that the second + * command still exists. + */ + counter = 0; + purple_command_manager_remove_all_with_source(manager, "test-1"); + g_assert_cmpuint(g_list_model_get_n_items(model), ==, 1); + g_assert_cmpuint(counter, ==, 2); + + command = purple_command_manager_find(manager, NULL, "xyzzy"); + g_assert_true(PURPLE_IS_COMMAND(command)); + + /* Add another command with a source of test-2. */ + command = purple_command_new("quit", "test-2", 0); + purple_command_manager_add(manager, command); + + g_assert_cmpuint(g_list_model_get_n_items(model), ==, 2); + + /* Now remove all commands with a source of test-2. */ + counter = 0; + purple_command_manager_remove_all_with_source(manager, "test-2"); + g_assert_cmpuint(g_list_model_get_n_items(model), ==, 0); + g_assert_cmpuint(counter, ==, 2); + + g_assert_finalize_object(manager); +} + +static void +test_purple_command_manager_find(void) { + PurpleCommand *command = NULL; + PurpleCommandManager *manager = NULL; + PurpleConversation *conversation = NULL; + PurpleTags *tags = NULL; + + manager = purple_command_manager_new(); + + /* We need at least one conversation to test filtering, so we just create + * one with a tag of test=true. + */ + conversation = g_object_new(PURPLE_TYPE_CONVERSATION, NULL); + tags = purple_conversation_get_tags(conversation); + purple_tags_add(tags, "test=true"); + + /* Test that find works with nothing added. This is unlikely to happen in + * practice, but it's good to test for. + */ + command = purple_command_manager_find(manager, NULL, "unknown"); + g_assert_null(command); + + command = purple_command_manager_find(manager, conversation, "unknown"); + g_assert_null(command); + + /* Now add a test command and verify that we can find it. */ + command = purple_command_new("test", "test-1", 0); + tags = purple_command_get_tags(command); + purple_tags_add(tags, "test=true"); + purple_command_manager_add(manager, command); + + command = purple_command_manager_find(manager, NULL, "test"); + g_assert_true(PURPLE_IS_COMMAND(command)); + + command = purple_command_manager_find(manager, conversation, "test"); + g_assert_true(PURPLE_IS_COMMAND(command)); + + /* Now add the command again but with a lower priority. */ + command = purple_command_new("test", "test-2", -100); + tags = purple_command_get_tags(command); + purple_tags_add(tags, "test=true"); + purple_command_manager_add(manager, command); + + command = purple_command_manager_find(manager, NULL, "test"); + g_assert_true(PURPLE_IS_COMMAND(command)); + g_assert_cmpuint(purple_command_get_priority(command), ==, 0); + + command = purple_command_manager_find(manager, conversation, "test"); + g_assert_true(PURPLE_IS_COMMAND(command)); + g_assert_cmpuint(purple_command_get_priority(command), ==, 0); + + /* Now add the command again but with a higher priority. */ + command = purple_command_new("test", "test-3", 100); + tags = purple_command_get_tags(command); + purple_tags_add(tags, "test=true"); + purple_command_manager_add(manager, command); + + command = purple_command_manager_find(manager, NULL, "test"); + g_assert_true(PURPLE_IS_COMMAND(command)); + g_assert_cmpuint(purple_command_get_priority(command), ==, 100); + + command = purple_command_manager_find(manager, conversation, "test"); + g_assert_true(PURPLE_IS_COMMAND(command)); + g_assert_cmpuint(purple_command_get_priority(command), ==, 100); + + /* Finally add the command again, but with a much higher priority and tags + * that won't match the conversation. + */ + command = purple_command_new("test", "test-4", 1337); + tags = purple_command_get_tags(command); + purple_tags_add(tags, "test=false"); + purple_command_manager_add(manager, command); + + command = purple_command_manager_find(manager, NULL, "test"); + g_assert_true(PURPLE_IS_COMMAND(command)); + g_assert_cmpuint(purple_command_get_priority(command), ==, 1337); + + command = purple_command_manager_find(manager, conversation, "test"); + g_assert_true(PURPLE_IS_COMMAND(command)); + g_assert_cmpuint(purple_command_get_priority(command), ==, 100); + + /* Cleanup. */ + g_assert_finalize_object(conversation); + g_assert_finalize_object(manager); +} + +static void +test_purple_command_manager_find_all(void) { + PurpleCommand *command = NULL; + PurpleCommandManager *manager = NULL; + PurpleConversation *conversation = NULL; + PurpleTags *tags = NULL; + GListModel *commands = NULL; + + manager = purple_command_manager_new(); + + /* We need at least one conversation to test filtering, so we just create + * one with a tag of test=true. + */ + conversation = g_object_new(PURPLE_TYPE_CONVERSATION, NULL); + tags = purple_conversation_get_tags(conversation); + purple_tags_add(tags, "test=true"); + + /* Create and add our commands that all share a name. One with no tags, + * another with the same tags as the conversation, one without the + * conversation tags, and another with a different priority but the + * conversation tags. + */ + command = purple_command_new("test", "test-1", 0); + purple_command_manager_add(manager, command); + + command = purple_command_new("test", "test-2", 0); + tags = purple_command_get_tags(command); + purple_tags_add(tags, "test=true"); + purple_command_manager_add(manager, command); + + command = purple_command_new("test", "test-3", 0); + tags = purple_command_get_tags(command); + purple_tags_add(tags, "test=false"); + purple_command_manager_add(manager, command); + + command = purple_command_new("test", "test-4", 100); + tags = purple_command_get_tags(command); + purple_tags_add(tags, "test=true"); + purple_command_manager_add(manager, command); + + /* Check an unknown command. */ + commands = purple_command_manager_find_all(manager, NULL, "unknown"); + g_assert_cmpuint(g_list_model_get_n_items(commands), ==, 0); + g_clear_object(&commands); + + /* Check without the conversation. */ + commands = purple_command_manager_find_all(manager, NULL, "test"); + g_assert_cmpuint(g_list_model_get_n_items(commands), ==, 4); + g_clear_object(&commands); + + /* Check with the conversation. */ + commands = purple_command_manager_find_all(manager, conversation, "test"); + g_assert_cmpuint(g_list_model_get_n_items(commands), ==, 3); + g_clear_object(&commands); + + g_assert_finalize_object(conversation); + g_assert_finalize_object(manager); +} + +/****************************************************************************** + * Find and Execute tests + *****************************************************************************/ +static void +test_purple_command_manager_find_and_execute_counter(PurpleCommand *command, + PurpleConversation *conversation, + G_GNUC_UNUSED GStrv params, + gpointer data) +{ + guint *counter = data; + + g_assert_true(PURPLE_IS_COMMAND(command)); + g_assert_true(PURPLE_IS_CONVERSATION(conversation)); + + *counter = *counter + 1; +} + +static void +test_purple_command_manager_find_and_execute_executed(PurpleCommand *command, + PurpleConversation *conversation, + GStrv params, + gpointer data) +{ + g_assert_true(PURPLE_IS_COMMAND(command)); + g_assert_true(PURPLE_IS_CONVERSATION(conversation)); + g_assert_cmpstrv(params, data); +} + +static void +test_purple_command_manager_find_and_execute(void) { + PurpleCommand *command = NULL; + PurpleCommandManager *manager = NULL; + PurpleConversation *conversation = NULL; + gboolean result = FALSE; + guint counter = 0; + const char * const expected_args[] = {"arg1", "arg2", NULL}; + + manager = purple_command_manager_new(); + + conversation = g_object_new(PURPLE_TYPE_CONVERSATION, NULL); + + /* Make sure we can't find anything in an empty manager. */ + result = purple_command_manager_find_and_execute(manager, conversation, + "unknown"); + g_assert_false(result); + + /* Again but with some arguments this time. */ + result = purple_command_manager_find_and_execute(manager, conversation, + "unknown arg1 arg2"); + g_assert_false(result); + + /* Create and add the command. */ + command = purple_command_new("test", "test-1", 0); + g_signal_connect(command, "executed", + G_CALLBACK(test_purple_command_manager_find_and_execute_counter), + &counter); + g_signal_connect(command, "executed", + G_CALLBACK(test_purple_command_manager_find_and_execute_executed), + (gpointer)expected_args); + purple_command_manager_add(manager, command); + + /* Try to execute it. */ + counter = 0; + result = purple_command_manager_find_and_execute(manager, conversation, + "test arg1 arg2"); + g_assert_true(result); + g_assert_cmpuint(counter, ==, 1); + + g_assert_finalize_object(conversation); + g_assert_finalize_object(manager); +} + +/****************************************************************************** + * Main + *****************************************************************************/ +int +main(int argc, char *argv[]) { + g_test_init(&argc, &argv, NULL); + g_test_set_nonfatal_assertions(); + + g_test_add_func("/command-manager/new", test_purple_command_manager_new); + g_test_add_func("/command-manager/add-remove", + test_purple_command_manager_add_remove); + + g_test_add_func("/command-manager/remove-all-with-source", + test_purple_command_manager_remove_all_with_source); + + g_test_add_func("/command-manager/find", test_purple_command_manager_find); + g_test_add_func("/command-manager/find-all", + test_purple_command_manager_find_all); + g_test_add_func("/command-manager/find-and-execute", + test_purple_command_manager_find_and_execute); + + return g_test_run(); +}