diff --git a/libpurple/purpleconversation.h b/libpurple/purpleconversation.h
--- a/libpurple/purpleconversation.h
+++ b/libpurple/purpleconversation.h
@@ -1,22 +1,19 @@
-/* purple
- *
- * 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.
+/*
+ * 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,
                          CONVERSATION, GObject)
 
+#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.
+ *
+ * Since: 3.0.0
+ */
+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
+ *            in the collection.
+ *
+ * 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
+ * %NULL.
+ *
+ * Returns: %TRUE if @info is in @conversation otherwise %FALSE.
+ *
+ * Since: 3.0.0
+ */
+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
+ * of @conversation.
+ *
+ * Returns: (transfer none) (nullable): The [class@Purple.ConversationMember]
+ *          if found or %NULL.
+ *
+ * Since: 3.0.0
+ */
+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
+ * isn't announced.
+ *
+ * Returns: (transfer none): The [class@Purple.ConversationMember] that was
+ *          created or found.
+ *
+ * Since: 3.0.0
+ */
+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,
+ *          otherwise %FALSE.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_conversation_remove_member(PurpleConversation *conversation, PurpleConversationMember *member, gboolean announce, const char *message);
+
 G_END_DECLS
 
 #endif /* PURPLE_CONVERSATION_H */
# HG changeset patch
# Node ID 8e36f27cf1f4
# Parent  23dae27bd7ca
diff --git a/libpurple/purpleconversation.c b/libpurple/purpleconversation.c
--- a/libpurple/purpleconversation.c
+++ b/libpurple/purpleconversation.c
@@ -1,53 +1,45 @@
 /*
- * purple
- *
- * 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.
+ * 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 "buddylist.h"
-#include "cmds.h"
+#include "purpleconversation.h"
+
 #include "conversations.h"
 #include "debug.h"
-#include "notify.h"
-#include "prefs.h"
-#include "purpleconversation.h"
 #include "purpleconversationmanager.h"
+#include "purpleconversationmember.h"
 #include "purpleenums.h"
 #include "purplehistorymanager.h"
 #include "purplemarkup.h"
 #include "purpleprivate.h"
-#include "purpleprotocol.h"
-#include "purpleprotocolclient.h"
-#include "request.h"
-#include "signals.h"
 
 typedef struct {
-	PurpleAccount *account;           /* The user using this conversation. */
+	PurpleAccount *account;
+
+	char *name;
+	char *title;
 
-	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            */
+	GListStore *members;
 } PurpleConversationPrivate;
 
 enum {
@@ -56,17 +48,37 @@
 	PROP_NAME,
 	PROP_TITLE,
 	PROP_FEATURES,
+	PROP_MEMBERS,
 	N_PROPERTIES
 };
-
 static GParamSpec *properties[N_PROPERTIES] = { NULL, };
 
-G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE(PurpleConversation, purple_conversation,
-                                    G_TYPE_OBJECT);
+enum {
+	SIG_MEMBER_ADDED,
+	SIG_MEMBER_REMOVED,
+	N_SIGNALS,
+};
+static guint signals[N_SIGNALS] = {0, };
+
+G_DEFINE_TYPE_WITH_PRIVATE(PurpleConversation, purple_conversation,
+                           G_TYPE_OBJECT);
 
 /**************************************************************************
  * Helpers
  **************************************************************************/
+static gboolean
+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);
+}
+
 static void
 common_send(PurpleConversation *conv, const gchar *message,
             PurpleMessageFlags msgflags)
@@ -220,7 +232,7 @@
 
 	switch (param_id) {
 		case PROP_ACCOUNT:
-			priv->account = g_value_get_object(value);
+			purple_conversation_set_account(conv, g_value_get_object(value));
 			break;
 		case PROP_NAME:
 			g_free(priv->name);
@@ -258,6 +270,9 @@
 		case PROP_FEATURES:
 			g_value_set_flags(value, purple_conversation_get_features(conv));
 			break;
+		case PROP_MEMBERS:
+			g_value_set_object(value, purple_conversation_get_members(conv));
+			break;
 		default:
 			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
 			break;
@@ -265,7 +280,12 @@
 }
 
 static void
-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);
 }
 
 static void
@@ -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;
-
-	/* Setup properties */
 	obj_class->get_property = purple_conversation_get_property;
 	obj_class->set_property = purple_conversation_set_property;
 
@@ -381,7 +400,70 @@
 		0,
 		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * PurpleConversation:members:
+	 *
+	 * The members that are currently in this conversation.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_MEMBERS] = g_param_spec_object(
+		"members", "members",
+		"The members that are currently in this conversation",
+		G_TYPE_LIST_MODEL,
+		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.
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_MEMBER_ADDED] = g_signal_new_class_handler(
+		"member-added",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		3,
+		PURPLE_TYPE_CONVERSATION_MEMBER,
+		G_TYPE_BOOLEAN,
+		G_TYPE_STRING);
+
+	/**
+	 * 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.
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_MEMBER_REMOVED] = g_signal_new_class_handler(
+		"member-removed",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		3,
+		PURPLE_TYPE_CONVERSATION_MEMBER,
+		G_TYPE_BOOLEAN,
+		G_TYPE_STRING);
 }
 
 /******************************************************************************
@@ -834,3 +916,124 @@
 
 	return menu;
 }
+
+GListModel *
+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);
+}
+
+gboolean
+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,
+	                                          position);
+
+	g_clear_object(&needle);
+
+	return found;
+}
+
+PurpleConversationMember *
+purple_conversation_find_member(PurpleConversation *conversation,
+                                PurpleContactInfo *info)
+{
+	PurpleConversationMember *member = NULL;
+	guint position = 0;
+
+	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
+		 * rid of that.
+		 */
+		g_object_unref(member);
+	}
+
+	return member;
+}
+
+PurpleConversationMember *
+purple_conversation_add_member(PurpleConversation *conversation,
+                               PurpleContactInfo *info, gboolean announce,
+                               const char *message)
+{
+	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)) {
+		return 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,
+	              message);
+
+	g_object_unref(member);
+
+	return member;
+}
+
+gboolean
+purple_conversation_remove_member(PurpleConversation *conversation,
+                                  PurpleConversationMember *member,
+                                  gboolean announce, const char *message)
+{
+	PurpleConversationPrivate *priv = NULL;
+	guint position = 0;
+
+	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)) {
+		return FALSE;
+	}
+
+	/* We need to ref member to make sure it stays around long enough for us
+	 * to emit the signal.
+	 */
+	g_object_ref(member);
+
+	g_list_store_remove(priv->members, position);
+
+	g_signal_emit(conversation, signals[SIG_MEMBER_REMOVED], 0, member,
+	              announce, message);
+
+	g_object_unref(member);
+
+	return TRUE;
+}
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -6,6 +6,7 @@
     'contact',
     'contact_info',
     'contact_manager',
+    'conversation',
     'conversation_member',
     'credential_manager',
     'credential_provider',
diff --git a/libpurple/tests/test_conversation.c b/libpurple/tests/test_conversation.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_conversation.c
@@ -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/>.
+ */
+
+#include <glib.h>
+
+#include <purple.h>
+
+#include "test_ui.h"
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_conversation_properties(void) {
+	PurpleAccount *account = NULL;
+	PurpleAccount *account1 = NULL;
+	PurpleConnectionFlags features = 0;
+	PurpleConversation *conversation = NULL;
+	GListModel *members = NULL;
+	gchar *name = NULL;
+	gchar *title = 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
+	 * code.
+	 */
+	conversation = g_object_new(
+		PURPLE_TYPE_CONVERSATION,
+		"account", account,
+		"features", PURPLE_CONNECTION_FLAG_HTML,
+		"name", "name1",
+		"title", "title1",
+		NULL);
+
+	/* Now use g_object_get to read all of the properties. */
+	g_object_get(conversation,
+		"account", &account1,
+		"features", &features,
+		"members", &members,
+		"name", &name,
+		"title", &title,
+		NULL);
+
+	/* 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.
+	 */
+#if 0
+	g_assert_cmpstr(title, ==, "title1");
+#endif
+
+	/* Free/unref all the things. */
+	g_clear_object(&account1);
+	g_clear_object(&members);
+	g_free(name);
+	g_free(title);
+
+	g_clear_object(&account);
+	g_clear_object(&conversation);
+}
+
+/******************************************************************************
+ * Membership tests and helpers
+ *****************************************************************************/
+static void
+test_purple_conversation_membership_signal_cb(PurpleConversation *conversation,
+                                              PurpleConversationMember *member,
+                                              gboolean announce,
+                                              const char *message,
+                                              gpointer data)
+{
+	/* We use int's for called to make sure it was only called once. */
+	gint *called = data;
+
+	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");
+
+	*called = *called + 1;
+}
+
+static void
+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 added_called = 0;
+	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,
+		"account", account,
+		"name", "test-conversation",
+		NULL);
+
+	/* Connect our signals. */
+	g_signal_connect(conversation, "member-added",
+	                 G_CALLBACK(test_purple_conversation_membership_signal_cb),
+	                 &added_called);
+	g_signal_connect(conversation, "member-removed",
+	                 G_CALLBACK(test_purple_conversation_membership_signal_cb),
+	                 &removed_called);
+
+	/* Add the member. */
+	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.
+	 */
+	g_object_ref(member);
+
+	/* 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(&info);
+	g_clear_object(&member);
+	g_clear_object(&account);
+	g_clear_object(&conversation);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	test_ui_purple_init();
+
+	g_test_add_func("/conversation/properties",
+	                test_purple_conversation_properties);
+
+	g_test_add_func("/conversation/members/add-remove",
+	                test_purple_conversation_members_add_remove);
+
+	return g_test_run();
+}
