--- a/libpurple/purpleconversation.c Fri Mar 03 01:23:31 2023 -0600
+++ b/libpurple/purpleconversation.c Fri Mar 03 01:24:36 2023 -0600
@@ -1,53 +1,45 @@
- * 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
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> - * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
+ * This 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 program is distributed in the hope that it will be useful,
+ * 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.
+ * 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 General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
+ * 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/gi18n-lib.h>
+#include "purpleconversation.h" #include "conversations.h"
-#include "purpleconversation.h"
#include "purpleconversationmanager.h"
+#include "purpleconversationmember.h" #include "purplehistorymanager.h"
#include "purplemarkup.h"
#include "purpleprivate.h"
-#include "purpleprotocol.h"
-#include "purpleprotocolclient.h"
- PurpleAccount *account; /* The user using this conversation. */
+ PurpleAccount *account; - char *name; /* The name of the conversation. */
- char *title; /* The window title. */
+ PurpleConversationUiOps *ui_ops; - PurpleConversationUiOps *ui_ops; /* UI-specific operations. */
+ PurpleConnectionFlags features; - PurpleConnectionFlags features; /* The supported features */
} PurpleConversationPrivate;
@@ -56,17 +48,37 @@
static GParamSpec *properties[N_PROPERTIES] = { NULL, };
-G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE(PurpleConversation, purple_conversation,
+static guint signals[N_SIGNALS] = {0, }; +G_DEFINE_TYPE_WITH_PRIVATE(PurpleConversation, purple_conversation, /**************************************************************************
**************************************************************************/
+purple_conversation_check_member_equal(gconstpointer a, gconstpointer b) { + PurpleConversationMember *member_a = (PurpleConversationMember *)a; + PurpleConversationMember *member_b = (PurpleConversationMember *)b; + PurpleContactInfo *info_a = NULL; + PurpleContactInfo *info_b = NULL; + info_a = purple_conversation_member_get_contact_info(member_a); + info_b = purple_conversation_member_get_contact_info(member_b); + return (purple_contact_info_compare(info_a, info_b) == 0); common_send(PurpleConversation *conv, const gchar *message,
PurpleMessageFlags msgflags)
@@ -220,7 +232,7 @@
- priv->account = g_value_get_object(value);
+ purple_conversation_set_account(conv, g_value_get_object(value)); @@ -258,6 +270,9 @@
g_value_set_flags(value, purple_conversation_get_features(conv));
+ g_value_set_object(value, purple_conversation_get_members(conv)); G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
@@ -265,7 +280,12 @@
-purple_conversation_init(G_GNUC_UNUSED PurpleConversation *conv) {
+purple_conversation_init(PurpleConversation *conv) { + PurpleConversationPrivate *priv = NULL; + priv = purple_conversation_get_instance_private(conv); + priv->members = g_list_store_new(PURPLE_TYPE_CONVERSATION_MEMBER); @@ -340,6 +360,7 @@
g_clear_pointer(&priv->name, g_free);
g_clear_pointer(&priv->title, g_free);
+ g_clear_object(&priv->members); G_OBJECT_CLASS(purple_conversation_parent_class)->finalize(object);
@@ -348,11 +369,9 @@
purple_conversation_class_init(PurpleConversationClass *klass) {
GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+ obj_class->constructed = purple_conversation_constructed; obj_class->dispose = purple_conversation_dispose;
obj_class->finalize = purple_conversation_finalize;
- obj_class->constructed = purple_conversation_constructed;
obj_class->get_property = purple_conversation_get_property;
obj_class->set_property = purple_conversation_set_property;
@@ -381,7 +400,70 @@
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ * PurpleConversation:members: + * The members that are currently in this conversation. + properties[PROP_MEMBERS] = g_param_spec_object( + "The members that are currently in this conversation", + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+ * PurpleConversation::member-added: + * @conversation: The instance. + * @member: The [class@Purple.ConversationMember] instance. + * @announce: Whether or not this addition should be announced. + * @message: (nullable): An optional message to use in the announcement. + * Emitted when a new member is added to this conversation. + signals[SIG_MEMBER_ADDED] = g_signal_new_class_handler( + G_OBJECT_CLASS_TYPE(klass), + PURPLE_TYPE_CONVERSATION_MEMBER, + * PurpleConversation::member-removed: + * @conversation: The instance. + * @member: The [class@Purple.ConversationMember] instance. + * @announce: Whether or not this removal should be announced. + * @message: (nullable): An optional message to use in the announcement. + * Emitted when member is removed from this conversation. + signals[SIG_MEMBER_REMOVED] = g_signal_new_class_handler( + G_OBJECT_CLASS_TYPE(klass), + PURPLE_TYPE_CONVERSATION_MEMBER, /******************************************************************************
@@ -834,3 +916,124 @@
+purple_conversation_get_members(PurpleConversation *conversation) { + PurpleConversationPrivate *priv = NULL; + g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL); + priv = purple_conversation_get_instance_private(conversation); + return G_LIST_MODEL(priv->members); +purple_conversation_has_member(PurpleConversation *conversation, + PurpleContactInfo *info, guint *position) + PurpleConversationPrivate *priv = NULL; + PurpleConversationMember *needle = NULL; + gboolean found = FALSE; + g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE); + g_return_val_if_fail(PURPLE_IS_CONTACT_INFO(info), FALSE); + priv = purple_conversation_get_instance_private(conversation); + needle = purple_conversation_member_new(info); + found = g_list_store_find_with_equal_func(priv->members, needle, + purple_conversation_check_member_equal, + g_clear_object(&needle); +PurpleConversationMember * +purple_conversation_find_member(PurpleConversation *conversation, + PurpleContactInfo *info) + PurpleConversationMember *member = NULL; + g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL); + g_return_val_if_fail(PURPLE_IS_CONTACT_INFO(info), NULL); + if(purple_conversation_has_member(conversation, info, &position)) { + PurpleConversationPrivate *priv = NULL; + priv = purple_conversation_get_instance_private(conversation); + member = g_list_model_get_item(G_LIST_MODEL(priv->members), position); + /* We don't return a reference, but get_item does, so we need to get + g_object_unref(member); +PurpleConversationMember * +purple_conversation_add_member(PurpleConversation *conversation, + PurpleContactInfo *info, gboolean announce, + PurpleConversationMember *member = NULL; + PurpleConversationPrivate *priv = NULL; + g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL); + g_return_val_if_fail(PURPLE_IS_CONTACT_INFO(info), NULL); + priv = purple_conversation_get_instance_private(conversation); + member = purple_conversation_find_member(conversation, info); + if(PURPLE_IS_CONVERSATION_MEMBER(member)) { + member = purple_conversation_member_new(info); + g_list_store_append(priv->members, member); + g_signal_emit(conversation, signals[SIG_MEMBER_ADDED], 0, member, announce, + g_object_unref(member); +purple_conversation_remove_member(PurpleConversation *conversation, + PurpleConversationMember *member, + gboolean announce, const char *message) + PurpleConversationPrivate *priv = NULL; + g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE); + g_return_val_if_fail(PURPLE_IS_CONVERSATION_MEMBER(member), FALSE); + priv = purple_conversation_get_instance_private(conversation); + if(!g_list_store_find(priv->members, member, &position)) { + /* We need to ref member to make sure it stays around long enough for us + g_list_store_remove(priv->members, position); + g_signal_emit(conversation, signals[SIG_MEMBER_REMOVED], 0, member, + g_object_unref(member); --- a/libpurple/purpleconversation.h Fri Mar 03 01:23:31 2023 -0600
+++ b/libpurple/purpleconversation.h Fri Mar 03 01:24:36 2023 -0600
@@ -1,22 +1,19 @@
- * 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
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> - * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
+ * This 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 program is distributed in the hope that it will be useful,
+ * 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.
+ * 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 General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
+ * 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/>. #if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
@@ -33,6 +30,8 @@
G_DECLARE_DERIVABLE_TYPE(PurpleConversation, purple_conversation, PURPLE,
+#include <purplecontactinfo.h> +#include <purpleconversationmember.h> #include <purplemessage.h>
@@ -357,6 +356,102 @@
gboolean purple_conversation_present_error(const gchar *who, PurpleAccount *account, const gchar *what);
+ * purple_conversation_get_members: + * @conversation: The instance. + * Gets the members that are in @conversation. + * Returns: (transfer none): The members in @conversation. +GListModel *purple_conversation_get_members(PurpleConversation *conversation); + * purple_conversation_has_member: + * @conversation: The instance. + * @info: The [class@Purple.ContactInfo] to look for. + * @position: (out) (optional): A return address for the position of the member + * Checks if @info is in @conversation. If @info is found, @position is set to + * the position of @info in [property@Purple.Conversation:members] if it is not + * Returns: %TRUE if @info is in @conversation otherwise %FALSE. +gboolean purple_conversation_has_member(PurpleConversation *conversation, PurpleContactInfo *info, guint *position); + * purple_conversation_find_member: + * @conversation: The instance. + * @info: A [class@Purple.ContactInfo instance] + * Finds the [class@Purple.ConversationMember] for @info if they are a member + * Returns: (transfer none) (nullable): The [class@Purple.ConversationMember] +PurpleConversationMember *purple_conversation_find_member(PurpleConversation *conversation, PurpleContactInfo *info); + * purple_conversation_add_member: + * @conversation: The instance. + * @info: The [class@Purple.ContactInfo] of the person joining. + * @announce: Whether this addition should be announced or not. + * @message: (nullable): An optional message to be used with @announce. + * Looks for an existing [class@Purple.ConversationMember] for @info in + * @conversation and returns it if found. If not, a new + * [class@Purple.ConversationMember] is created. + * > This method is intended to be called by a protocol plugin to directly + * > manage the membership state of the @conversation. + * This will also emit the [signal@Purple.Conversation::member-added] signal if + * an existing member was not found. + * The @announce and @message parameters will be used when emitting the + * [signal@Purple.Conversation::member-added] signal. Announce and message are + * meant for protocols to more properly define their behavior. For example, on + * IRC you will typically be told when a user joins a chat but on Twitch this + * Returns: (transfer none): The [class@Purple.ConversationMember] that was +PurpleConversationMember *purple_conversation_add_member(PurpleConversation *conversation, PurpleContactInfo *info, gboolean announce, const char *message); + * purple_conversation_remove_member: + * @conversation: The instance. + * @member: The [class@Purple.ConversationMember] to remove. + * @announce: Whether or not this removal should be announced. + * @message: (nullable): An optional message for the announcement. + * Attempts to remove @member from the collection of members in @conversation. + * If found, @member is removed and the + * [signal@Purple.Conversation::member-removed] signal is emitted with + * @announce and @message as parameters. + * > This method is intended to be called by a protocol plugin to directly + * > manage the membership state of the @conversation. + * Returns: %TRUE if @member was found and removed from @conversation, +gboolean purple_conversation_remove_member(PurpleConversation *conversation, PurpleConversationMember *member, gboolean announce, const char *message); #endif /* PURPLE_CONVERSATION_H */
--- a/libpurple/tests/meson.build Fri Mar 03 01:23:31 2023 -0600
+++ b/libpurple/tests/meson.build Fri Mar 03 01:24:36 2023 -0600
@@ -6,6 +6,7 @@
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/test_conversation.c Fri Mar 03 01:24:36 2023 -0600
@@ -0,0 +1,190 @@
+ * 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/>. +/****************************************************************************** + *****************************************************************************/ +test_purple_conversation_properties(void) { + PurpleAccount *account = NULL; + PurpleAccount *account1 = NULL; + PurpleConnectionFlags features = 0; + PurpleConversation *conversation = NULL; + GListModel *members = NULL; + account = purple_account_new("test", "test"); + /* Use g_object_new so we can test setting properties by name. All of them + * call the setter methods, so by doing it this way we exercise more of the + conversation = g_object_new( + PURPLE_TYPE_CONVERSATION, + "features", PURPLE_CONNECTION_FLAG_HTML, + /* Now use g_object_get to read all of the properties. */ + g_object_get(conversation, + /* Compare all the things. */ + g_assert_true(account1 == account); + g_assert_cmpint(features, ==, PURPLE_CONNECTION_FLAG_HTML); + g_assert_true(G_IS_LIST_MODEL(members)); + g_assert_cmpstr(name, ==, "name1"); + /* We don't currently test title because purple_conversation_autoset_title + * makes it something we don't expect it to be. + g_assert_cmpstr(title, ==, "title1"); + /* Free/unref all the things. */ + g_clear_object(&account1); + g_clear_object(&members); + g_clear_object(&account); + g_clear_object(&conversation); +/****************************************************************************** + * Membership tests and helpers + *****************************************************************************/ +test_purple_conversation_membership_signal_cb(PurpleConversation *conversation, + PurpleConversationMember *member, + /* We use int's for called to make sure it was only called once. */ + g_assert_true(PURPLE_IS_CONVERSATION(conversation)); + g_assert_true(PURPLE_IS_CONVERSATION_MEMBER(member)); + g_assert_true(announce); + g_assert_cmpstr(message, ==, "announcement message"); +test_purple_conversation_members_add_remove(void) { + PurpleAccount *account = NULL; + PurpleContactInfo *info = NULL; + PurpleConversation *conversation = NULL; + PurpleConversationMember *member = NULL; + PurpleConversationMember *member1 = NULL; + gboolean removed = FALSE; + gint removed_called = 0; + /* Create our instances. */ + info = purple_contact_info_new(NULL); + account = purple_account_new("test", "test"); + conversation = g_object_new( + PURPLE_TYPE_CONVERSATION, + "name", "test-conversation", + /* Connect our signals. */ + g_signal_connect(conversation, "member-added", + G_CALLBACK(test_purple_conversation_membership_signal_cb), + g_signal_connect(conversation, "member-removed", + G_CALLBACK(test_purple_conversation_membership_signal_cb), + member = purple_conversation_add_member(conversation, info, TRUE, + "announcement message"); + g_assert_cmpint(added_called, ==, 1); + g_assert_true(PURPLE_IS_CONVERSATION_MEMBER(member)); + /* Add our own reference to the returned member as we use it later to + * verify that double remove doesn't do anything. + /* Try to add the member again, which would just return the existing + * member and not emit the signal. + member1 = purple_conversation_add_member(conversation, info, TRUE, + "announcement message"); + g_assert_cmpint(added_called, ==, 1); + g_assert_true(PURPLE_IS_CONVERSATION_MEMBER(member1)); + g_assert_true(member1 == member); + /* Now remove the member and verify the signal was called. */ + removed = purple_conversation_remove_member(conversation, member, TRUE, + "announcement message"); + g_assert_true(removed); + g_assert_cmpint(removed_called, ==, 1); + /* Try to remove the member again and verify that nothing was removed and + * that the signal wasn't emitted. + removed = purple_conversation_remove_member(conversation, member, TRUE, + "announcement message"); + g_assert_false(removed); + g_assert_cmpint(removed_called, ==, 1); + /* Clean up everything. */ + g_clear_object(&member); + g_clear_object(&account); + g_clear_object(&conversation); +/****************************************************************************** + *****************************************************************************/ +main(gint argc, gchar *argv[]) { + g_test_init(&argc, &argv, NULL); + g_test_add_func("/conversation/properties", + test_purple_conversation_properties); + g_test_add_func("/conversation/members/add-remove", + test_purple_conversation_members_add_remove);