diff --git a/libpurple/purplecontactmanager.h b/libpurple/purplecontactmanager.h
--- a/libpurple/purplecontactmanager.h
+++ b/libpurple/purplecontactmanager.h
@@ -158,6 +158,36 @@
 G_DEPRECATED
 void purple_contact_manager_add_buddy(PurpleContactManager *manager, PurpleBuddy *buddy);
 
+/**
+ * purple_contact_manager_add_person:
+ * @manager: The instance.
+ * @person: The [class@Purple.Person to add].
+ *
+ * Adds all of the contacts contained in @person to @manager.
+ *
+ * This function is mostly intended for unit testing and importing. You
+ * typically you won't need to call this directly as @manager will
+ * automatically add the [class@Purple.Person] instance when
+ * [method@Purple.ContactManager.add] is called.
+ *
+ * Since: 3.0.0
+ */
+void purple_contact_manager_add_person(PurpleContactManager *manager, PurplePerson *person);
+
+/**
+ * purple_contact_manager_remove_person:
+ * @manager: The instance.
+ * @person: The [class@Purple.Person] to remove.
+ * @remove_contacts: Whether or not the contacts should be removed from
+ *                   @manager.
+ *
+ * Removes @person from @manager optionally removing all of the contacts
+ * contained in @person as well if @remove_contacts is %TRUE.
+ *
+ * Since: 3.0.0
+ */
+void purple_contact_manager_remove_person(PurpleContactManager *manager, PurplePerson *person, gboolean remove_contacts);
+
 G_END_DECLS
 
 #endif /* PURPLE_CONTACT_MANAGER_H */
# HG changeset patch
# Node ID ff5705e3b709
# Parent  421c761287d8
diff --git a/libpurple/purplecontactmanager.c b/libpurple/purplecontactmanager.c
--- a/libpurple/purplecontactmanager.c
+++ b/libpurple/purplecontactmanager.c
@@ -27,6 +27,8 @@
 enum {
 	SIG_ADDED,
 	SIG_REMOVED,
+	SIG_PERSON_ADDED,
+	SIG_PERSON_REMOVED,
 	N_SIGNALS,
 };
 static guint signals[N_SIGNALS] = {0, };
@@ -35,10 +37,17 @@
 	GObject parent;
 
 	GHashTable *accounts;
+
+	GPtrArray *people;
 };
 
 static PurpleContactManager *default_manager = NULL;
 
+/* Necessary prototype. */
+static void purple_contact_manager_contact_person_changed_cb(GObject *obj,
+                                                             GParamSpec *pspec,
+                                                             gpointer data);
+
 /******************************************************************************
  * Helpers
  *****************************************************************************/
@@ -127,9 +136,72 @@
 }
 
 /******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+purple_contact_manager_contact_person_changed_cb(GObject *obj,
+                                                 G_GNUC_UNUSED GParamSpec *pspec,
+                                                 gpointer data)
+{
+	PurpleContact *contact = PURPLE_CONTACT(obj);
+	PurpleContactManager *manager = data;
+	PurplePerson *person = NULL;
+
+	person = purple_contact_info_get_person(PURPLE_CONTACT_INFO(contact));
+	/* If the person is now NULL, we leaving the existing person in place as
+	 * we don't want to potentially delete user data.
+	 */
+	if(!PURPLE_IS_PERSON(person)) {
+		return;
+	}
+
+	/* At this point the person changed or is new so we need to add the new
+	 * person.
+	 */
+	purple_contact_manager_add_person(manager, person);
+}
+
+/******************************************************************************
+ * GListModel Implementation
+ *****************************************************************************/
+static GType
+purple_contact_manager_get_item_type(G_GNUC_UNUSED GListModel *list) {
+	return PURPLE_TYPE_PERSON;
+}
+
+static guint
+purple_contact_manager_get_n_items(GListModel *list) {
+	PurpleContactManager *manager = PURPLE_CONTACT_MANAGER(list);
+
+	return manager->people->len;
+}
+
+static gpointer
+purple_contact_manager_get_item(GListModel *list, guint position) {
+	PurpleContactManager *manager = PURPLE_CONTACT_MANAGER(list);
+	PurpleContact *contact = NULL;
+
+	if(position < manager->people->len) {
+		contact = g_object_ref(g_ptr_array_index(manager->people, position));
+	}
+
+	return contact;
+}
+
+static void
+pidgin_contact_manager_list_model_iface_init(GListModelInterface *iface) {
+	iface->get_item_type = purple_contact_manager_get_item_type;
+	iface->get_n_items = purple_contact_manager_get_n_items;
+	iface->get_item = purple_contact_manager_get_item;
+}
+
+/******************************************************************************
  * GObject Implementation
  *****************************************************************************/
-G_DEFINE_TYPE(PurpleContactManager, purple_contact_manager, G_TYPE_OBJECT)
+G_DEFINE_FINAL_TYPE_WITH_CODE(PurpleContactManager, purple_contact_manager,
+                              G_TYPE_OBJECT,
+                              G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL,
+                                                    pidgin_contact_manager_list_model_iface_init))
 
 static void
 purple_contact_manager_dispose(GObject *obj) {
@@ -139,6 +211,11 @@
 
 	g_hash_table_remove_all(manager->accounts);
 
+	if(manager->people != NULL) {
+		g_ptr_array_free(manager->people, TRUE);
+		manager->people = NULL;
+	}
+
 	G_OBJECT_CLASS(purple_contact_manager_parent_class)->dispose(obj);
 }
 
@@ -157,6 +234,12 @@
 purple_contact_manager_init(PurpleContactManager *manager) {
 	manager->accounts = g_hash_table_new_full(g_direct_hash, g_direct_equal,
 	                                          g_object_unref, g_object_unref);
+
+	/* 100 Seems like a reasonable default of the number people on your contact
+	 * list. - gk 20221109
+	 */
+	manager->people = g_ptr_array_new_full(100,
+	                                       (GDestroyNotify)g_object_unref);
 }
 
 static void
@@ -207,6 +290,51 @@
 		G_TYPE_NONE,
 		1,
 		PURPLE_TYPE_CONTACT);
+
+	/**
+	 * PurpleContactManager::person-added:
+	 * @manager: The instance.
+	 * @person: The [class@Purple.Person] that was added.
+	 *
+	 * Emitted after @person has been added to @manager. This is typically done
+	 * when a contact is added via [method@Purple.ContactManager.add] but can
+	 * also happen if [method@Purple.ContactManager.add_person] is called.
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_PERSON_ADDED] = g_signal_new_class_handler(
+		"person-added",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		1,
+		PURPLE_TYPE_PERSON);
+
+	/**
+	 * PurpleContactManager::person-removed:
+	 * @manager: The instance.
+	 * @person: The [class@Purple.Person] that was removed.
+	 *
+	 * Emitted after @person has been removed from @manager. This typically
+	 * happens when [method@Purple.ContactManager.remove_person] is called.
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_PERSON_REMOVED] = g_signal_new_class_handler(
+		"person-removed",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		1,
+		PURPLE_TYPE_PERSON);
 }
 
 /******************************************************************************
@@ -274,6 +402,21 @@
 	}
 
 	if(added) {
+		PurpleContactInfo *info = PURPLE_CONTACT_INFO(contact);
+		PurplePerson *person = purple_contact_info_get_person(info);
+
+		/* If the contact already has a person, add the person to our list of
+		 * people.
+		 */
+		if(PURPLE_IS_PERSON(person)) {
+			purple_contact_manager_add_person(manager, person);
+		}
+
+		/* Add a notify on the person property to track changes. */
+		g_signal_connect_object(contact, "notify::person",
+		                        G_CALLBACK(purple_contact_manager_contact_person_changed_cb),
+		                        manager, 0);
+
 		g_signal_emit(manager, signals[SIG_ADDED], 0, contact);
 	}
 }
@@ -485,3 +628,67 @@
 	/* purple_contact_manager_add adds its own reference, so free our copy. */
 	g_clear_object(&contact);
 }
+
+void
+purple_contact_manager_add_person(PurpleContactManager *manager,
+                                  PurplePerson *person)
+{
+	guint index = 0;
+
+	g_return_if_fail(PURPLE_IS_CONTACT_MANAGER(manager));
+	g_return_if_fail(PURPLE_IS_PERSON(person));
+
+	/* If the person is already known, bail. */
+	if(g_ptr_array_find(manager->people, person, &index)) {
+		return;
+	}
+
+	/* Add the person and emit our signals. */
+	g_ptr_array_add(manager->people, g_object_ref(person));
+	g_list_model_items_changed(G_LIST_MODEL(manager), index, 0, 1);
+	g_signal_emit(manager, signals[SIG_PERSON_ADDED], 0, person);
+}
+
+void
+purple_contact_manager_remove_person(PurpleContactManager *manager,
+                                     PurplePerson *person,
+                                     gboolean remove_contacts)
+{
+	guint index = 0;
+
+	g_return_if_fail(PURPLE_IS_CONTACT_MANAGER(manager));
+	g_return_if_fail(PURPLE_IS_PERSON(person));
+
+	if(!g_ptr_array_find(manager->people, person, &index)) {
+		return;
+	}
+
+	if(remove_contacts) {
+		guint n = g_list_model_get_n_items(G_LIST_MODEL(person));
+
+		for(guint i = 0; i < n; i++) {
+			PurpleContact *contact = NULL;
+
+			contact = g_list_model_get_item(G_LIST_MODEL(person), i);
+			if(PURPLE_IS_CONTACT(contact)) {
+				purple_contact_manager_remove(manager, contact);
+				g_object_unref(contact);
+			}
+		}
+	}
+
+	/* Add a ref to the person, so we can emit the removed signal after it
+	 * was actually removed, as our GPtrArray may be holding the last
+	 * reference.
+	 */
+	g_object_ref(person);
+
+	g_ptr_array_remove_index(manager->people, index);
+
+	g_list_model_items_changed(G_LIST_MODEL(manager), index, 1, 0);
+
+	/* Emit the removed signal and clear our temporary reference. */
+	g_signal_emit(manager, signals[SIG_PERSON_REMOVED], 0, person);
+	g_object_unref(person);
+}
+
diff --git a/libpurple/purpleperson.h b/libpurple/purpleperson.h
--- a/libpurple/purpleperson.h
+++ b/libpurple/purpleperson.h
@@ -180,6 +180,18 @@
  */
 PurpleContactInfo *purple_person_get_priority_contact_info(PurplePerson *person);
 
+/*
+ * purple_person_has_contacts:
+ * @person: The instance.
+ *
+ * Gets whether or not @person has any contacts.
+ *
+ * Returns: %TRUE if @person has at least one contact, otherwise %FALSE.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_person_has_contacts(PurplePerson *person);
+
 G_END_DECLS
 
 #endif /* PURPLE_PERSON_H */
diff --git a/libpurple/purpleperson.c b/libpurple/purpleperson.c
--- a/libpurple/purpleperson.c
+++ b/libpurple/purpleperson.c
@@ -452,3 +452,10 @@
 
 	return g_ptr_array_index(person->contacts, 0);
 }
+
+gboolean
+purple_person_has_contacts(PurplePerson *person) {
+	g_return_val_if_fail(PURPLE_IS_PERSON(person), FALSE);
+
+	return person->contacts->len > 0;
+}
diff --git a/libpurple/tests/test_contact_manager.c b/libpurple/tests/test_contact_manager.c
--- a/libpurple/tests/test_contact_manager.c
+++ b/libpurple/tests/test_contact_manager.c
@@ -347,6 +347,123 @@
 	g_clear_object(&buddy);
 	g_clear_object(&contact);
 }
+
+/******************************************************************************
+ * Person Tests
+ *****************************************************************************/
+static void
+test_purple_contact_manager_person_add_remove(void) {
+	PurpleContactManager *manager = NULL;
+	PurplePerson *person = NULL;
+	GListModel *model = NULL;
+	int added_called = 0;
+	int removed_called = 0;
+
+	manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL);
+	model = G_LIST_MODEL(manager);
+
+	g_assert_true(PURPLE_IS_CONTACT_MANAGER(manager));
+
+	/* Wire up our signals. */
+	g_signal_connect(manager, "person-added",
+	                 G_CALLBACK(test_purple_contact_manager_increment_cb),
+	                 &added_called);
+	g_signal_connect(manager, "person-removed",
+	                 G_CALLBACK(test_purple_contact_manager_increment_cb),
+	                 &removed_called);
+
+	/* Create the person and add it to the manager. */
+	person = purple_person_new();
+	purple_contact_manager_add_person(manager, person);
+
+	/* Make sure the person is available. */
+	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 1);
+
+	/* Make sure the added signal was called. */
+	g_assert_cmpint(added_called, ==, 1);
+
+	/* Remove the contact. */
+	purple_contact_manager_remove_person(manager, person, FALSE);
+	g_assert_cmpint(removed_called, ==, 1);
+
+	/* Make sure the person was removed. */
+	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 0);
+
+	/* Clean up.*/
+	g_clear_object(&person);
+	g_clear_object(&manager);
+}
+
+static void
+test_purple_contact_manager_person_add_via_contact_remove_person_with_contacts(void)
+{
+	PurpleAccount *account = NULL;
+	PurpleContact *contact = NULL;
+	PurpleContactManager *manager = NULL;
+	PurplePerson *person = NULL;
+	GListModel *contacts = NULL;
+	GListModel *model = NULL;
+	int contact_added_called = 0;
+	int contact_removed_called = 0;
+	int person_added_called = 0;
+	int person_removed_called = 0;
+
+	manager = g_object_new(PURPLE_TYPE_CONTACT_MANAGER, NULL);
+	model = G_LIST_MODEL(manager);
+
+	g_assert_true(PURPLE_IS_CONTACT_MANAGER(manager));
+
+	/* Wire up our signals. */
+	g_signal_connect(manager, "added",
+	                 G_CALLBACK(test_purple_contact_manager_increment_cb),
+	                 &contact_added_called);
+	g_signal_connect(manager, "removed",
+	                 G_CALLBACK(test_purple_contact_manager_increment_cb),
+	                 &contact_removed_called);
+	g_signal_connect(manager, "person-added",
+	                 G_CALLBACK(test_purple_contact_manager_increment_cb),
+	                 &person_added_called);
+	g_signal_connect(manager, "person-removed",
+	                 G_CALLBACK(test_purple_contact_manager_increment_cb),
+	                 &person_removed_called);
+
+	/* Create all of our objects. */
+	account = purple_account_new("test", "test");
+	contact = purple_contact_new(account, "foo");
+	person = purple_person_new();
+	purple_person_add_contact_info(person, contact);
+
+	/* Add the contact to the manager. */
+	purple_contact_manager_add(manager, contact);
+
+	/* Make sure the contact is available. */
+	contacts = purple_contact_manager_get_all(manager, account);
+	g_assert_nonnull(contacts);
+
+	/* Make sure the contact and the person were added. */
+	g_assert_cmpuint(g_list_model_get_n_items(contacts), ==, 1);
+	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 1);
+
+	/* Make sure the added signals were called. */
+	g_assert_cmpint(contact_added_called, ==, 1);
+	g_assert_cmpint(person_added_called, ==, 1);
+
+	/* Remove the person and the contacts. */
+	purple_contact_manager_remove_person(manager, person, TRUE);
+	g_assert_cmpint(contact_removed_called, ==, 1);
+	g_assert_cmpint(person_removed_called, ==, 1);
+
+	/* Make sure the person and contact were removed. */
+	g_assert_cmpuint(g_list_model_get_n_items(model), ==, 0);
+	g_assert_cmpuint(g_list_model_get_n_items(contacts), ==, 0);
+
+	/* Clean up.*/
+	g_clear_object(&account);
+	g_clear_object(&contact);
+	g_clear_object(&person);
+	g_clear_object(&manager);
+}
+
 /******************************************************************************
  * Main
  *****************************************************************************/
@@ -376,5 +493,10 @@
 	g_test_add_func("/contact-manager/add-buddy",
 	                test_purple_contact_manager_add_buddy);
 
+	g_test_add_func("/contact-manager/person/add-remove",
+	                test_purple_contact_manager_person_add_remove);
+	g_test_add_func("/contact-manager/person/add-via-contact-remove-person-with-contacts",
+	                test_purple_contact_manager_person_add_via_contact_remove_person_with_contacts);
+
 	return g_test_run();
 }
