Thu, 16 Jan 2025 21:21:59 -0600
Implement Purple.Attachments
This is a simple helper collection for Purple.Attachment.
Testing Done:
Ran the tests under valgrind and called in the turtles.
Reviewed at https://reviews.imfreedom.org/r/3758/
--- a/libpurple/meson.build Thu Jan 16 21:20:08 2025 -0600 +++ b/libpurple/meson.build Thu Jan 16 21:21:59 2025 -0600 @@ -14,6 +14,7 @@ 'purpleaccountusersplit.c', 'purpleaddcontactrequest.c', 'purpleattachment.c', + 'purpleattachments.c', 'purpleauthorizationrequest.c', 'purpleavatar.c', 'purplebadge.c', @@ -108,6 +109,8 @@ 'purpleaccountoption.h', 'purpleaccountusersplit.h', 'purpleaddcontactrequest.h', + 'purpleattachment.h', + 'purpleattachments.h', 'purpleauthorizationrequest.h', 'purpleavatar.h', 'purplebadge.h', @@ -136,7 +139,6 @@ 'purplehistoryadapter.h', 'purplehistorymanager.h', 'purpleidlemanager.h', - 'purpleattachment.h', 'purplekeyvaluepair.h', 'purplemarkup.h', 'purplemenu.h',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/purpleattachments.c Thu Jan 16 21:21:59 2025 -0600 @@ -0,0 +1,271 @@ +/* + * 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 <gio/gio.h> + +#include "purpleattachments.h" + +struct _PurpleAttachments { + GObject parent; + + GPtrArray *attachments; +}; + +enum { + PROP_0, + PROP_ITEM_TYPE, + PROP_N_ITEMS, + N_PROPERTIES, +}; +static GParamSpec *properties[N_PROPERTIES] = {NULL, }; + +/****************************************************************************** + * Helpers + *****************************************************************************/ +static gboolean +purple_attachments_equal(gconstpointer a, gconstpointer b) { + PurpleAttachment *attachment1 = (gpointer)a; + PurpleAttachment *attachment2 = (gpointer)b; + + return purple_attachment_equal(attachment1, attachment2); +} + +/****************************************************************************** + * GListModel Implementation + *****************************************************************************/ +static GType +purple_attachments_get_item_type(G_GNUC_UNUSED GListModel *model) { + return PURPLE_TYPE_ATTACHMENT; +} + +static guint +purple_attachments_get_n_items(GListModel *model) { + PurpleAttachments *attachments = PURPLE_ATTACHMENTS(model); + + return attachments->attachments->len; +} + +static gpointer +purple_attachments_get_item(GListModel *model, guint position) { + PurpleAttachments *attachments = PURPLE_ATTACHMENTS(model); + PurpleAttachment *attachment = NULL; + + if(position < attachments->attachments->len) { + attachment = g_ptr_array_index(attachments->attachments, position); + + if(PURPLE_IS_ATTACHMENT(attachment)) { + g_object_ref(attachment); + } + } + + return attachment; +} + +static void +purple_attachments_list_model_iface_init(GListModelInterface *iface) { + iface->get_item_type = purple_attachments_get_item_type; + iface->get_n_items = purple_attachments_get_n_items; + iface->get_item = purple_attachments_get_item; +} + +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +G_DEFINE_FINAL_TYPE_WITH_CODE(PurpleAttachments, + purple_attachments, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL, + purple_attachments_list_model_iface_init)) + +static void +purple_attachments_finalize(GObject *obj) { + PurpleAttachments *attachments = PURPLE_ATTACHMENTS(obj); + + g_clear_pointer(&attachments->attachments, g_ptr_array_unref); + + G_OBJECT_CLASS(purple_attachments_parent_class)->finalize(obj); +} + +static void +purple_attachments_get_property(GObject *obj, guint param_id, GValue *value, + GParamSpec *pspec) +{ + GListModel *model = G_LIST_MODEL(obj); + + switch(param_id) { + case PROP_ITEM_TYPE: + g_value_set_gtype(value, g_list_model_get_item_type(model)); + break; + case PROP_N_ITEMS: + g_value_set_uint(value, g_list_model_get_n_items(model)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); + break; + } +} + +static void +purple_attachments_init(PurpleAttachments *attachments) { + attachments->attachments = g_ptr_array_new_full(0, g_object_unref); +} + +static void +purple_attachments_class_init(PurpleAttachmentsClass *klass) { + GObjectClass *obj_class = G_OBJECT_CLASS(klass); + + obj_class->finalize = purple_attachments_finalize; + obj_class->get_property = purple_attachments_get_property; + + /** + * PurpleAttachments:item-type: + * + * The type of items. See [vfunc@Gio.ListModel.get_item_type]. + * + * Since: 3.0 + */ + properties[PROP_ITEM_TYPE] = g_param_spec_gtype( + "item-type", NULL, NULL, + PURPLE_TYPE_ATTACHMENT, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * PurpleAttachments:n-items: + * + * The number of items. See [vfunc@Gio.ListModel.get_n_items]. + * + * Since: 3.0 + */ + properties[PROP_N_ITEMS] = g_param_spec_uint( + "n-items", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(obj_class, N_PROPERTIES, properties); +} + +/****************************************************************************** + * Public API + *****************************************************************************/ +gboolean +purple_attachments_add_attachment(PurpleAttachments *attachments, + PurpleAttachment *attachment) +{ + GObject *obj = NULL; + gboolean found = FALSE; + guint len = 0; + + g_return_val_if_fail(PURPLE_IS_ATTACHMENTS(attachments), FALSE); + g_return_val_if_fail(PURPLE_IS_ATTACHMENT(attachment), FALSE); + + found = g_ptr_array_find_with_equal_func(attachments->attachments, + attachment, + purple_attachments_equal, + NULL); + + if(found) { + return FALSE; + } + + /* Store length before adding to make the math easier to understand. */ + len = attachments->attachments->len; + + g_ptr_array_add(attachments->attachments, g_object_ref(attachment)); + + obj = G_OBJECT(attachments); + g_object_freeze_notify(obj); + g_list_model_items_changed(G_LIST_MODEL(attachments), len, 0, 1); + g_object_notify_by_pspec(obj, properties[PROP_N_ITEMS]); + g_object_thaw_notify(obj); + + return TRUE; +} + +PurpleAttachment * +purple_attachments_find_with_id(PurpleAttachments *attachments, guint64 id) { + g_return_val_if_fail(PURPLE_IS_ATTACHMENTS(attachments), NULL); + + for(guint i = 0; i < attachments->attachments->len; i++) { + PurpleAttachment *attachment = NULL; + + attachment = g_ptr_array_index(attachments->attachments, i); + + if(purple_attachment_get_id(attachment) == id) { + return attachment; + } + } + + return NULL; +} + +PurpleAttachments * +purple_attachments_new(void) { + return g_object_new(PURPLE_TYPE_ATTACHMENTS, NULL); +} + +gboolean +purple_attachments_remove_attachment(PurpleAttachments *attachments, + PurpleAttachment *attachment) +{ + GObject *obj = NULL; + gboolean found = FALSE; + guint index = 0; + + g_return_val_if_fail(PURPLE_IS_ATTACHMENTS(attachments), FALSE); + g_return_val_if_fail(PURPLE_IS_ATTACHMENT(attachment), FALSE); + + found = g_ptr_array_find_with_equal_func(attachments->attachments, + attachment, + purple_attachments_equal, + &index); + if(!found) { + return FALSE; + } + + g_ptr_array_remove_index(attachments->attachments, index); + + obj = G_OBJECT(attachments); + g_object_freeze_notify(obj); + g_list_model_items_changed(G_LIST_MODEL(attachments), index, 1, 0); + g_object_notify_by_pspec(obj, properties[PROP_N_ITEMS]); + g_object_thaw_notify(obj); + + return TRUE; +} + +void +purple_attachments_remove_all(PurpleAttachments *attachments) { + guint removed = 0; + + g_return_if_fail(PURPLE_IS_ATTACHMENTS(attachments)); + + removed = attachments->attachments->len; + g_ptr_array_remove_range(attachments->attachments, 0, removed); + + if(removed > 0) { + GObject *obj = G_OBJECT(attachments); + g_object_freeze_notify(obj); + g_list_model_items_changed(G_LIST_MODEL(attachments), 0, removed, 0); + g_object_notify_by_pspec(obj, properties[PROP_N_ITEMS]); + g_object_thaw_notify(obj); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/purpleattachments.h Thu Jan 16 21:21:59 2025 -0600 @@ -0,0 +1,121 @@ +/* + * 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_ATTACHMENTS_H +#define PURPLE_ATTACHMENTS_H + +#include <glib.h> +#include <gio/gio.h> + +#include "purpleattachment.h" +#include "purpleversion.h" + +G_BEGIN_DECLS + +#define PURPLE_TYPE_ATTACHMENTS (purple_attachments_get_type()) + +/** + * PurpleAttachments: + * + * A collection of [class@Attachment]'s. + * + * This implements [iface@Gio.ListModel] and should be accessed via that API. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +G_DECLARE_FINAL_TYPE(PurpleAttachments, purple_attachments, PURPLE, ATTACHMENTS, + GObject) + +/** + * purple_attachments_add_attachment: + * @attachment: (transfer none): the attachment to add + * + * Adds an attachment to the collection. + * + * No sorting will be done, but if @attachment is already in the collection it + * will not be added again. + * + * Returns: true if the item was added; otherwise false. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +gboolean purple_attachments_add_attachment(PurpleAttachments *attachments, PurpleAttachment *attachment); + +/** + * purple_attachments_find_with_id: + * @id: the id + * + * Looks for an attachment matching the given id. + * + * Returns: (transfer none) (nullable): The attachment if found. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +PurpleAttachment *purple_attachments_find_with_id(PurpleAttachments *attachments, guint64 id); + +/** + * purple_attachments_new: + * + * Creates a new instance. + * + * Returns: (transfer full): The new instance. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +PurpleAttachments *purple_attachments_new(void); + +/** + * purple_attachments_remove_attachment: + * @attachment: (transfer none): the attachment to remove + * + * Removes an attachment from the collection. + * + * If @attachment is not in the collection it will be ignored. + * + * Returns: true if the item was removed; otherwise false. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +gboolean purple_attachments_remove_attachment(PurpleAttachments *attachments, PurpleAttachment *attachment); + +/** + * purple_attachments_remove_all: + * + * Removes all attachments from the collection. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +void purple_attachments_remove_all(PurpleAttachments *attachments); + +G_END_DECLS + +#endif /* PURPLE_ATTACHMENTS_H */
--- a/libpurple/tests/meson.build Thu Jan 16 21:20:08 2025 -0600 +++ b/libpurple/tests/meson.build Thu Jan 16 21:21:59 2025 -0600 @@ -3,6 +3,7 @@ 'account_option', 'account_manager', 'attachment', + 'attachments', 'authorization_request', 'badge', 'badge_manager',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/tests/test_attachments.c Thu Jan 16 21:21:59 2025 -0600 @@ -0,0 +1,207 @@ +/* + * 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> + +/****************************************************************************** + * Callbacks + *****************************************************************************/ +static void +test_purple_attachments_items_changed(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_ATTACHMENTS(model)); + + *counter = *counter + 1; +} + +static void +test_purple_attachments_notify(GObject *self, + G_GNUC_UNUSED GParamSpec *pspec, + gpointer data) +{ + guint *counter = data; + + g_assert_true(PURPLE_IS_ATTACHMENTS(self)); + + *counter = *counter + 1; +} + +/****************************************************************************** + * Tests + *****************************************************************************/ +static void +test_purple_attachments_properties(void) { + PurpleAttachments *attachments = NULL; + GType item_type = G_TYPE_INVALID; + guint n_items = 0; + + attachments = g_object_new( + PURPLE_TYPE_ATTACHMENTS, + NULL); + + g_object_get( + attachments, + "item-type", &item_type, + "n-items", &n_items, + NULL); + + g_assert_cmpuint(item_type, ==, PURPLE_TYPE_ATTACHMENT); + g_assert_cmpuint(n_items, ==, 0); + + g_assert_finalize_object(attachments); +} + +static void +test_purple_attachments_double_add(void) { + PurpleAttachment *attachment = NULL; + PurpleAttachments *attachments = NULL; + gboolean success = FALSE; + guint items_changed_counter = 0; + guint n_items_counter = 0; + + attachments = purple_attachments_new(); + g_signal_connect(attachments, "items-changed", + G_CALLBACK(test_purple_attachments_items_changed), + &items_changed_counter); + g_signal_connect(attachments, "notify::n-items", + G_CALLBACK(test_purple_attachments_notify), + &n_items_counter); + + attachment = purple_attachment_new(1337lu, "text/plain"); + + /* The first add. */ + items_changed_counter = 0; + n_items_counter = 0; + success = purple_attachments_add_attachment(attachments, attachment); + g_assert_true(success); + g_assert_cmpuint(items_changed_counter, ==, 1); + g_assert_cmpuint(n_items_counter, ==, 1); + + /* The double add. */ + items_changed_counter = 0; + n_items_counter = 0; + success = purple_attachments_add_attachment(attachments, attachment); + g_assert_false(success); + g_assert_cmpuint(items_changed_counter, ==, 0); + g_assert_cmpuint(n_items_counter, ==, 0); + + g_assert_finalize_object(attachments); + g_assert_finalize_object(attachment); +} + +static void +test_purple_attachments_double_remove(void) { + PurpleAttachment *attachment = NULL; + PurpleAttachments *attachments = NULL; + gboolean success = FALSE; + guint items_changed_counter = 0; + guint n_items_counter = 0; + + attachments = purple_attachments_new(); + g_signal_connect(attachments, "items-changed", + G_CALLBACK(test_purple_attachments_items_changed), + &items_changed_counter); + g_signal_connect(attachments, "notify::n-items", + G_CALLBACK(test_purple_attachments_notify), + &n_items_counter); + + attachment = purple_attachment_new(1337lu, "text/plain"); + + /* The first add. */ + items_changed_counter = 0; + n_items_counter = 0; + success = purple_attachments_add_attachment(attachments, attachment); + g_assert_true(success); + g_assert_cmpuint(items_changed_counter, ==, 1); + g_assert_cmpuint(n_items_counter, ==, 1); + + /* The first remove. */ + items_changed_counter = 0; + n_items_counter = 0; + success = purple_attachments_remove_attachment(attachments, attachment); + g_assert_true(success); + g_assert_cmpuint(items_changed_counter, ==, 1); + g_assert_cmpuint(n_items_counter, ==, 1); + + /* The second remove. */ + items_changed_counter = 0; + n_items_counter = 0; + success = purple_attachments_remove_attachment(attachments, attachment); + g_assert_false(success); + g_assert_cmpuint(items_changed_counter, ==, 0); + g_assert_cmpuint(n_items_counter, ==, 0); + + g_assert_finalize_object(attachments); + g_assert_finalize_object(attachment); +} + +static void +test_purple_attachments_find_with_id(void) { + PurpleAttachment *attachment = NULL; + PurpleAttachment *found = NULL; + PurpleAttachments *attachments = NULL; + + attachments = purple_attachments_new(); + + /* Search on the empty collection. */ + found = purple_attachments_find_with_id(attachments, 1337lu); + g_assert_null(found); + + /* Create an attachment and add it. */ + attachment = purple_attachment_new(1337lu, "text/plain"); + purple_attachments_add_attachment(attachments, attachment); + + found = purple_attachments_find_with_id(attachments, 1337lu); + g_assert_true(PURPLE_IS_ATTACHMENT(found)); + g_assert_true(found == attachment); + + /* Search for an attachment that doesn't exist. */ + found = purple_attachments_find_with_id(attachments, 42lu); + g_assert_null(found); + + g_assert_finalize_object(attachments); + g_assert_finalize_object(attachment); +} + +/****************************************************************************** + * Main + *****************************************************************************/ +int +main(int argc, char *argv[]) { + g_test_init(&argc, &argv, NULL); + g_test_set_nonfatal_assertions(); + + g_test_add_func("/attachments/properties", + test_purple_attachments_properties); + g_test_add_func("/attachments/double-add", + test_purple_attachments_double_add); + g_test_add_func("/attachments/double-remove", + test_purple_attachments_double_remove); + g_test_add_func("/attachments/find-with-id", + test_purple_attachments_find_with_id); + + return g_test_run(); +}