# HG changeset patch
# Node ID bcb0c14a9887
# Parent  e110f2d6dbd3
diff --git a/pidgin/plugins/disco/gtkdisco.c b/pidgin/plugins/disco/gtkdisco.c
--- a/pidgin/plugins/disco/gtkdisco.c
+++ b/pidgin/plugins/disco/gtkdisco.c
@@ -103,26 +103,6 @@ void pidgin_disco_list_set_in_progress(P
 	}
 }
 
-static char *
-pidgin_disco_get_icon_name(XmppDiscoService *service)
-{
-	char *icon_name = NULL;
-
-	g_return_val_if_fail(service != NULL, NULL);
-
-	if (service->type == XMPP_DISCO_SERVICE_TYPE_GATEWAY && service->gateway_type) {
-		icon_name = g_strconcat("im-", service->gateway_type, NULL);
-#if 0
-	} else if (service->type == XMPP_DISCO_SERVICE_TYPE_USER) {
-		icon_name = g_strdup("person");
-#endif
-	} else if (service->type == XMPP_DISCO_SERVICE_TYPE_CHAT) {
-		icon_name = g_strdup("chat");
-	}
-
-	return icon_name;
-}
-
 static void
 dialog_select_account_cb(GObject *obj, G_GNUC_UNUSED GParamSpec *pspec,
                          gpointer data)
@@ -146,9 +126,9 @@ activate_register(G_GNUC_UNUSED GSimpleA
 	PidginDiscoDialog *dialog = data;
 	XmppDiscoService *service = dialog->selected;
 
-	g_return_if_fail(service != NULL);
+	g_return_if_fail(XMPP_DISCO_IS_SERVICE(service));
 
-	xmpp_disco_service_register(service);
+	xmpp_disco_register_service(service);
 }
 
 static void discolist_cancel_cb(PidginDiscoList *pdl, const char *server)
@@ -248,15 +228,17 @@ activate_add_to_blist(G_GNUC_UNUSED GSim
 {
 	PidginDiscoDialog *dialog = data;
 	XmppDiscoService *service = dialog->selected;
+	PidginDiscoList *list = NULL;
 	PurpleAccount *account;
 	const char *jid;
 
-	g_return_if_fail(service != NULL);
+	g_return_if_fail(XMPP_DISCO_IS_SERVICE(service));
 
-	account = purple_connection_get_account(service->list->pc);
-	jid = service->jid;
+	list = xmpp_disco_service_get_list(service);
+	account = purple_connection_get_account(list->pc);
+	jid = xmpp_disco_service_get_jid(service);
 
-	if (service->type == XMPP_DISCO_SERVICE_TYPE_CHAT) {
+	if(xmpp_disco_service_get_service_type(service) == XMPP_DISCO_SERVICE_TYPE_CHAT) {
 		purple_blist_request_add_chat(account, NULL, NULL, jid);
 	} else {
 		purple_blist_request_add_buddy(account, jid, NULL, NULL);
@@ -318,8 +300,9 @@ selection_changed_cb(GtkTreeSelection *s
 		                         SERVICE_COLUMN, &val);
 		dialog->selected = g_value_get_pointer(&val);
 		if (dialog->selected != NULL) {
-			allow_add = (dialog->selected->flags & XMPP_DISCO_ADD);
-			allow_register = (dialog->selected->flags & XMPP_DISCO_REGISTER);
+			XmppDiscoServiceFlags flags = xmpp_disco_service_get_flags(dialog->selected);
+			allow_add = (flags & XMPP_DISCO_ADD) != 0;
+			allow_register = (flags & XMPP_DISCO_REGISTER) != 0;
 		}
 	}
 
@@ -351,6 +334,7 @@ row_activated_cb(GtkTreeView       *tree
 	PidginDiscoDialog *dialog = user_data;
 	GtkTreeIter iter;
 	XmppDiscoService *service;
+	XmppDiscoServiceFlags flags;
 	GValue val;
 
 	if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(dialog->model), &iter, path)) {
@@ -362,15 +346,16 @@ row_activated_cb(GtkTreeView       *tree
 	                         SERVICE_COLUMN, &val);
 	service = g_value_get_pointer(&val);
 
-	if (service->flags & XMPP_DISCO_BROWSE) {
+	flags = xmpp_disco_service_get_flags(service);
+	if((flags & XMPP_DISCO_BROWSE) != 0) {
 		if (gtk_tree_view_row_expanded(GTK_TREE_VIEW(dialog->tree), path)) {
 			gtk_tree_view_collapse_row(GTK_TREE_VIEW(dialog->tree), path);
 		} else {
 			gtk_tree_view_expand_row(GTK_TREE_VIEW(dialog->tree), path, FALSE);
 		}
-	} else if (service->flags & XMPP_DISCO_REGISTER) {
+	} else if((flags & XMPP_DISCO_REGISTER) != 0) {
 		g_action_activate(G_ACTION(dialog->register_action), NULL);
-	} else if (service->flags & XMPP_DISCO_ADD) {
+	} else if((flags & XMPP_DISCO_ADD) != 0) {
 		g_action_activate(G_ACTION(dialog->add_action), NULL);
 	}
 }
@@ -452,7 +437,7 @@ disco_query_tooltip(GtkWidget *widget, i
 		return FALSE;
 	}
 
-	switch (service->type) {
+	switch(xmpp_disco_service_get_service_type(service)) {
 		case XMPP_DISCO_SERVICE_TYPE_UNSET:
 			type = _("Unknown");
 			break;
@@ -482,16 +467,17 @@ disco_query_tooltip(GtkWidget *widget, i
 			break;
 	}
 
-	name = g_markup_escape_text(service->name, -1);
-	jid = g_markup_escape_text(service->jid, -1);
-	if (service->description != NULL) {
-		desc = g_markup_escape_text(service->description, -1);
+	name = g_markup_escape_text(xmpp_disco_service_get_name(service), -1);
+	jid = g_markup_escape_text(xmpp_disco_service_get_jid(service), -1);
+	if(xmpp_disco_service_get_description(service) != NULL) {
+		desc = g_markup_escape_text(xmpp_disco_service_get_description(service),
+		                            -1);
 	}
 
 	markup = g_strdup_printf("<span size='x-large' weight='bold'>%s</span>\n<b>%s:</b> %s%s%s",
 	                         name, type, jid,
-	                         service->description ? _("\n<b>Description:</b> ") : "",
-	                         service->description ? desc : "");
+	                         desc != NULL ? _("\n<b>Description:</b> ") : "",
+	                         desc != NULL ? desc : "");
 
 	gtk_tooltip_set_markup(tooltip, markup);
 	gtk_tree_view_set_tooltip_row(GTK_TREE_VIEW(widget), tooltip, path);
@@ -663,10 +649,11 @@ void pidgin_disco_add_service(PidginDisc
 	g_return_if_fail(dialog != NULL);
 
 	if (service != NULL) {
-		purple_debug_info("xmppdisco", "Adding service \"%s\"", service->name);
+		purple_debug_info("xmppdisco", "Adding service \"%s\"",
+		                  xmpp_disco_service_get_name(service));
 	} else {
 		purple_debug_info("xmppdisco", "Service \"%s\" has no children",
-		                  parent->name);
+		                  xmpp_disco_service_get_name(parent));
 	}
 
 	gtk_progress_bar_pulse(GTK_PROGRESS_BAR(dialog->progress));
@@ -710,7 +697,7 @@ void pidgin_disco_add_service(PidginDisc
 		iter = child;
 	}
 
-	if (service->flags & XMPP_DISCO_BROWSE) {
+	if((xmpp_disco_service_get_flags(service) & XMPP_DISCO_BROWSE) != 0) {
 		GtkTreeRowReference *rr;
 		GtkTreePath *path;
 
@@ -724,11 +711,12 @@ void pidgin_disco_add_service(PidginDisc
 		gtk_tree_path_free(path);
 	}
 
-	icon_name = pidgin_disco_get_icon_name(service);
+	icon_name = xmpp_disco_service_get_icon_name(service);
 
 	gtk_tree_store_set(dialog->model, &iter, ICON_NAME_COLUMN, icon_name,
-	                   NAME_COLUMN, service->name, DESCRIPTION_COLUMN,
-	                   service->description, SERVICE_COLUMN, service, -1);
+	                   NAME_COLUMN, xmpp_disco_service_get_name(service),
+	                   DESCRIPTION_COLUMN, xmpp_disco_service_get_description(service),
+	                   SERVICE_COLUMN, service, -1);
 
 	g_free(icon_name);
 }
diff --git a/pidgin/plugins/disco/meson.build b/pidgin/plugins/disco/meson.build
--- a/pidgin/plugins/disco/meson.build
+++ b/pidgin/plugins/disco/meson.build
@@ -1,17 +1,28 @@
 xmppdisco_SOURCES = [
 	'gtkdisco.c',
+	'xmppdisco.c',
+	'xmppdiscoservice.c',
+]
+
+xmppdisco_HEADERS = [
 	'gtkdisco.h',
-	'xmppdisco.c',
-	'xmppdisco.h'
+	'xmppdisco.h',
+	'xmppdiscoservice.h',
 ]
 
+xmppdisco_enums = gnome.mkenums_simple('xmppdiscoenums',
+	sources: xmppdisco_HEADERS,
+	identifier_prefix: 'XmppDisco',
+	symbol_prefix: 'xmpp_disco')
+xmppdisco_SOURCES += xmppdisco_enums
+
 xmppdisco_resource = gnome.compile_resources('xmppdiscoresources',
     'resources/xmppdisco.gresource.xml',
     source_dir : 'resources',
     c_name : 'xmppdisco')
 xmppdisco_SOURCES += xmppdisco_resource
 
-xmppdisco = library('xmppdisco', xmppdisco_SOURCES,
+xmppdisco = library('xmppdisco', xmppdisco_SOURCES, xmppdisco_HEADERS,
     c_args : ['-DG_LOG_USE_STRUCTURED', '-DG_LOG_DOMAIN="PidginPlugin-XMPPDisco"'],
     dependencies : [libpurple_dep, libpidgin_dep, glib],
     name_prefix : '',
diff --git a/pidgin/plugins/disco/xmppdisco.h b/pidgin/plugins/disco/xmppdisco.h
--- a/pidgin/plugins/disco/xmppdisco.h
+++ b/pidgin/plugins/disco/xmppdisco.h
@@ -22,8 +22,7 @@
 #ifndef PIDGIN_XMPP_DISCO_H
 #define PIDGIN_XMPP_DISCO_H
 
-typedef struct _XmppDiscoService XmppDiscoService;
-
+#include "xmppdiscoservice.h"
 #include "gtkdisco.h"
 
 #define XMPP_PROTOCOL_ID    "prpl-jabber"
@@ -38,73 +37,9 @@ typedef struct _XmppDiscoService XmppDis
 #include <purple.h>
 extern PurplePlugin *my_plugin;
 
-/**
- * The types of services.
- */
-typedef enum
-{
-	XMPP_DISCO_SERVICE_TYPE_UNSET,
-	/**
-	 * A registerable gateway to another protocol. An example would be
-	 * XMPP legacy transports.
-	 */
-	XMPP_DISCO_SERVICE_TYPE_GATEWAY,
-
-	/**
-	 * A directory (e.g. allows the user to search for other users).
-	 */
-	XMPP_DISCO_SERVICE_TYPE_DIRECTORY,
-
-	/**
-	 * A chat (multi-user conversation).
-	 */
-	XMPP_DISCO_SERVICE_TYPE_CHAT,
-
-	/**
-	 * A pubsub collection (contains nodes)
-	 */
-	XMPP_DISCO_SERVICE_TYPE_PUBSUB_COLLECTION,
-
-	/**
-	 * A pubsub leaf (contains stuff, not nodes).
-	 */
-	XMPP_DISCO_SERVICE_TYPE_PUBSUB_LEAF,
-
-	/**
-	 * Something else. Do we need more categories?
-	 */
-	XMPP_DISCO_SERVICE_TYPE_OTHER
-} XmppDiscoServiceType;
-
-/**
- * The flags of services.
- */
-typedef enum
-{
-	XMPP_DISCO_NONE          = 0x0000,
-	XMPP_DISCO_ADD           = 0x0001, /**< Supports an 'add' operation */
-	XMPP_DISCO_BROWSE        = 0x0002, /**< Supports browsing */
-	XMPP_DISCO_REGISTER      = 0x0004  /**< Supports a 'register' operation */
-} XmppDiscoServiceFlags;
-
-struct _XmppDiscoService {
-	PidginDiscoList *list;
-	gchar *name;
-	gchar *description;
-
-	gchar *gateway_type;
-	XmppDiscoServiceType type;
-	XmppDiscoServiceFlags flags;
-
-	XmppDiscoService *parent;
-	gchar *jid;
-	gchar *node;
-	gboolean expanded;
-};
-
 void xmpp_disco_start(PidginDiscoList *list);
 
 void xmpp_disco_service_expand(XmppDiscoService *service);
-void xmpp_disco_service_register(XmppDiscoService *service);
+void xmpp_disco_register_service(XmppDiscoService *service);
 
 #endif /* PIDGIN_XMPP_DISCO_H */
diff --git a/pidgin/plugins/disco/xmppdisco.c b/pidgin/plugins/disco/xmppdisco.c
--- a/pidgin/plugins/disco/xmppdisco.c
+++ b/pidgin/plugins/disco/xmppdisco.c
@@ -297,48 +297,47 @@ got_info_cb(PurpleConnection *pc, const 
 	if (purple_strequal(type, "result") &&
 			(query = purple_xmlnode_get_child(iq, "query"))) {
 		PurpleXmlNode *identity = purple_xmlnode_get_child(query, "identity");
-		XmppDiscoService *service;
+		XmppDiscoService *service = NULL;
 		PurpleXmlNode *feature;
 
-		service = g_new0(XmppDiscoService, 1);
-		service->list = item_data->list;
-		purple_debug_info("xmppdisco", "parent for %s is %p\n", from, item_data->parent);
-		service->parent = item_data->parent;
-		service->flags = 0;
-		service->type = disco_service_type_from_identity(identity);
+		service = xmpp_disco_service_new(item_data->list, item_data->parent);
+		purple_debug_info("xmppdisco", "parent for %s is %p", from, item_data->parent);
+		xmpp_disco_service_set_service_type(service,
+		                                    disco_service_type_from_identity(identity));
 
 		if (item_data->node) {
 			if (item_data->name) {
-				service->name = item_data->name;
-				item_data->name = NULL;
+				xmpp_disco_service_set_name(service, item_data->name);
+				g_clear_pointer(&item_data->name, g_free);
 			} else {
-				service->name = g_strdup(item_data->node);
+				xmpp_disco_service_set_name(service, item_data->node);
 			}
 
-			service->node = item_data->node;
-			item_data->node = NULL;
+			xmpp_disco_service_set_node(service, item_data->node);
+			g_clear_pointer(&item_data->node, g_free);
 
-			if (service->type == XMPP_DISCO_SERVICE_TYPE_PUBSUB_COLLECTION) {
-				service->flags |= XMPP_DISCO_BROWSE;
+			if(xmpp_disco_service_get_service_type(service) == XMPP_DISCO_SERVICE_TYPE_PUBSUB_COLLECTION) {
+				xmpp_disco_service_add_flags(service, XMPP_DISCO_BROWSE);
 			}
 		} else {
-			service->name = g_strdup(from);
+			xmpp_disco_service_set_name(service, from);
 		}
 
-		if (!service->node) {
+		if(!xmpp_disco_service_get_node(service)) {
 			/* Only support adding JIDs, not JID+node combos */
-			service->flags |= XMPP_DISCO_ADD;
+			xmpp_disco_service_add_flags(service, XMPP_DISCO_ADD);
 		}
 
 		if (item_data->name) {
-			service->description = item_data->name;
-			item_data->name = NULL;
+			xmpp_disco_service_set_description(service, item_data->name);
+			g_clear_pointer(&item_data->name, g_free);
 		} else if (identity) {
-			service->description = g_strdup(purple_xmlnode_get_attrib(identity, "name"));
+			xmpp_disco_service_set_description(service,
+			                                   purple_xmlnode_get_attrib(identity, "name"));
 		}
 
 		/* TODO: Overlap with service->name a bit */
-		service->jid = g_strdup(from);
+		xmpp_disco_service_set_jid(service, from);
 
 		for (feature = purple_xmlnode_get_child(query, "feature"); feature;
 				feature = purple_xmlnode_get_next_twin(feature)) {
@@ -348,21 +347,24 @@ got_info_cb(PurpleConnection *pc, const 
 			}
 
 			if (purple_strequal(var, NS_REGISTER)) {
-				service->flags |= XMPP_DISCO_REGISTER;
+				xmpp_disco_service_add_flags(service, XMPP_DISCO_REGISTER);
 			} else if (purple_strequal(var, NS_DISCO_ITEMS)) {
-				service->flags |= XMPP_DISCO_BROWSE;
+				xmpp_disco_service_add_flags(service, XMPP_DISCO_BROWSE);
 			} else if (purple_strequal(var, NS_MUC)) {
-				service->flags |= XMPP_DISCO_BROWSE;
-				service->type = XMPP_DISCO_SERVICE_TYPE_CHAT;
+				xmpp_disco_service_add_flags(service, XMPP_DISCO_BROWSE);
+				xmpp_disco_service_set_service_type(service,
+				                                    XMPP_DISCO_SERVICE_TYPE_CHAT);
 			}
 		}
 
-		if (service->type == XMPP_DISCO_SERVICE_TYPE_GATEWAY) {
-			service->gateway_type = g_strdup(disco_type_from_string(
+		if(xmpp_disco_service_get_service_type(service) == XMPP_DISCO_SERVICE_TYPE_GATEWAY) {
+			xmpp_disco_service_set_gateway_type(service,
+				disco_type_from_string(
 					purple_xmlnode_get_attrib(identity, "type")));
 		}
 
-		pidgin_disco_add_service(list, service, service->parent);
+		pidgin_disco_add_service(list, service,
+		                         xmpp_disco_service_get_parent(service));
 	}
 
 out:
@@ -403,7 +405,7 @@ got_items_cb(PurpleConnection *pc, const
 
 			has_items = TRUE;
 
-			if (item_data->parent->type == XMPP_DISCO_SERVICE_TYPE_CHAT) {
+			if(xmpp_disco_service_get_service_type(item_data->parent) == XMPP_DISCO_SERVICE_TYPE_CHAT) {
 				/* This is a hacky first-order approximation. Any MUC
 				 * component that has a >1 level hierarchy (a Yahoo MUC
 				 * transport component probably does) will violate this.
@@ -411,18 +413,18 @@ got_items_cb(PurpleConnection *pc, const
 				 * On the other hand, this is better than querying all the
 				 * chats at conference.jabber.org to enumerate them.
 				 */
-				XmppDiscoService *service = g_new0(XmppDiscoService, 1);
-				service->list = item_data->list;
-				service->parent = item_data->parent;
-				service->flags = XMPP_DISCO_ADD;
-				service->type = XMPP_DISCO_SERVICE_TYPE_CHAT;
+				XmppDiscoService *service = xmpp_disco_service_new(item_data->list, item_data->parent);
+				xmpp_disco_service_set_flags(service, XMPP_DISCO_ADD);
+				xmpp_disco_service_set_service_type(service,
+				                                    XMPP_DISCO_SERVICE_TYPE_CHAT);
 
-				service->name = g_strdup(name);
-				service->jid = g_strdup(jid);
-				service->node = g_strdup(node);
-				if (service->name == NULL) {
-					service->name = g_strdup(jid);
+				if(name != NULL) {
+					xmpp_disco_service_set_name(service, name);
+				} else {
+					xmpp_disco_service_set_name(service, jid);
 				}
+				xmpp_disco_service_set_jid(service, jid);
+				xmpp_disco_service_set_node(service, node);
 				pidgin_disco_add_service(list, service, item_data->parent);
 			} else {
 				struct item_data *item_data2 = g_new0(struct item_data, 1);
@@ -567,43 +569,51 @@ void xmpp_disco_start(PidginDiscoList *l
 
 void xmpp_disco_service_expand(XmppDiscoService *service)
 {
+	PidginDiscoList *list = NULL;
 	struct item_data *item_data;
 
-	g_return_if_fail(service != NULL);
+	g_return_if_fail(XMPP_DISCO_IS_SERVICE(service));
 
-	if (service->expanded) {
+	if(xmpp_disco_service_get_expanded(service)) {
 		return;
 	}
 
+	list = xmpp_disco_service_get_list(service);
+
 	item_data = g_new0(struct item_data, 1);
-	item_data->list = service->list;
+	item_data->list = list;
 	item_data->parent = service;
 
-	++service->list->fetch_count;
-	pidgin_disco_list_ref(service->list);
+	++list->fetch_count;
+	pidgin_disco_list_ref(list);
+
+	pidgin_disco_list_set_in_progress(list, TRUE);
 
-	pidgin_disco_list_set_in_progress(service->list, TRUE);
-
-	xmpp_disco_items_do(service->list->pc, item_data, service->jid, service->node,
+	xmpp_disco_items_do(list->pc, item_data,
+	                    xmpp_disco_service_get_jid(service),
+	                    xmpp_disco_service_get_node(service),
 	                    got_items_cb);
-	service->expanded = TRUE;
+	xmpp_disco_service_set_expanded(service, TRUE);
 }
 
-void xmpp_disco_service_register(XmppDiscoService *service)
+void
+xmpp_disco_register_service(XmppDiscoService *service)
 {
+	PidginDiscoList *list = NULL;
 	PurpleXmlNode *iq, *query;
 	char *id = generate_next_id();
 
 	iq = purple_xmlnode_new("iq");
 	purple_xmlnode_set_attrib(iq, "type", "get");
-	purple_xmlnode_set_attrib(iq, "to", service->jid);
+	purple_xmlnode_set_attrib(iq, "to", xmpp_disco_service_get_jid(service));
 	purple_xmlnode_set_attrib(iq, "id", id);
 
 	query = purple_xmlnode_new_child(iq, "query");
 	purple_xmlnode_set_namespace(query, NS_REGISTER);
 
-	purple_signal_emit(purple_connection_get_protocol(service->list->pc),
-			"jabber-sending-xmlnode", service->list->pc, &iq);
+	list = xmpp_disco_service_get_list(service);
+	purple_signal_emit(purple_connection_get_protocol(list->pc),
+	                   "jabber-sending-xmlnode", list->pc, &iq);
 	if (iq != NULL) {
 		purple_xmlnode_free(iq);
 	}
@@ -684,6 +694,7 @@ xmpp_disco_load(GPluginPlugin *plugin, G
 		return FALSE;
 	}
 
+	xmpp_disco_service_register(plugin);
 	pidgin_disco_dialog_register(plugin);
 
 	purple_signal_connect(purple_connections_get_handle(), "signing-off",
diff --git a/pidgin/plugins/disco/xmppdiscoservice.h b/pidgin/plugins/disco/xmppdiscoservice.h
new file mode 100644
--- /dev/null
+++ b/pidgin/plugins/disco/xmppdiscoservice.h
@@ -0,0 +1,112 @@
+/* pidgin
+ *
+ * Pidgin 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 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 program 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 program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef PIDGIN_XMPP_DISCO_SERVICE_H
+#define PIDGIN_XMPP_DISCO_SERVICE_H
+
+#include <glib-object.h>
+
+#include <purple.h>
+
+#define XMPP_DISCO_TYPE_SERVICE (xmpp_disco_service_get_type())
+G_DECLARE_FINAL_TYPE(XmppDiscoService, xmpp_disco_service, XMPP_DISCO, SERVICE,
+                     GObject)
+
+#include "gtkdisco.h"
+
+/**
+ * XmppDiscoServiceType:
+ * @XMPP_DISCO_SERVICE_TYPE_UNSET: An unset service type.
+ * @XMPP_DISCO_SERVICE_TYPE_GATEWAY: A registerable gateway to another
+ *                                   protocol. An example would be XMPP legacy
+ *                                   transports.
+ * @XMPP_DISCO_SERVICE_TYPE_DIRECTORY: A directory (e.g. allows the user to
+ *                                     search for other users).
+ * @XMPP_DISCO_SERVICE_TYPE_CHAT: A chat (multi-user conversation).
+ * @XMPP_DISCO_SERVICE_TYPE_PUBSUB_COLLECTION: A pubsub collection (contains
+ *                                             nodes).
+ * @XMPP_DISCO_SERVICE_TYPE_PUBSUB_LEAF: A pubsub leaf (contains stuff, not
+ *                                       nodes).
+ * @XMPP_DISCO_SERVICE_TYPE_OTHER: Something else. Do we need more categories?
+ *
+ * The types of services.
+ */
+typedef enum {
+	XMPP_DISCO_SERVICE_TYPE_UNSET,
+	XMPP_DISCO_SERVICE_TYPE_GATEWAY,
+	XMPP_DISCO_SERVICE_TYPE_DIRECTORY,
+	XMPP_DISCO_SERVICE_TYPE_CHAT,
+	XMPP_DISCO_SERVICE_TYPE_PUBSUB_COLLECTION,
+	XMPP_DISCO_SERVICE_TYPE_PUBSUB_LEAF,
+	XMPP_DISCO_SERVICE_TYPE_OTHER
+} XmppDiscoServiceType;
+
+/**
+ * XmppDiscoServiceFlags:
+ * @XMPP_DISCO_NONE: No flags.
+ * @XMPP_DISCO_ADD: Supports an 'add' operation.
+ * @XMPP_DISCO_BROWSE: Supports browsing.
+ * @XMPP_DISCO_REGISTER: Supports a 'register' operation.
+ *
+ * The flags of services.
+ */
+typedef enum { /*< flags >*/
+	XMPP_DISCO_NONE = 0x0000,
+	XMPP_DISCO_ADD = 0x0001,
+	XMPP_DISCO_BROWSE = 0x0002,
+	XMPP_DISCO_REGISTER = 0x0004,
+} XmppDiscoServiceFlags;
+
+void xmpp_disco_service_register(PurplePlugin *plugin);
+
+XmppDiscoService *xmpp_disco_service_new(PidginDiscoList *list, XmppDiscoService *parent);
+
+PidginDiscoList *xmpp_disco_service_get_list(XmppDiscoService *service);
+XmppDiscoService *xmpp_disco_service_get_parent(XmppDiscoService *service);
+
+const char *xmpp_disco_service_get_name(XmppDiscoService *service);
+void xmpp_disco_service_set_name(XmppDiscoService *service, const char *name);
+
+const char *xmpp_disco_service_get_description(XmppDiscoService *service);
+void xmpp_disco_service_set_description(XmppDiscoService *service, const char *description);
+
+XmppDiscoServiceType xmpp_disco_service_get_service_type(XmppDiscoService *service);
+void xmpp_disco_service_set_service_type(XmppDiscoService *service, XmppDiscoServiceType type);
+
+const char *xmpp_disco_service_get_gateway_type(XmppDiscoService *service);
+void xmpp_disco_service_set_gateway_type(XmppDiscoService *service, const char *gateway_type);
+
+XmppDiscoServiceFlags xmpp_disco_service_get_flags(XmppDiscoService *service);
+void xmpp_disco_service_set_flags(XmppDiscoService *service, XmppDiscoServiceFlags flags);
+void xmpp_disco_service_add_flags(XmppDiscoService *service, XmppDiscoServiceFlags flags);
+void xmpp_disco_service_remove_flags(XmppDiscoService *service, XmppDiscoServiceFlags flags);
+
+const char *xmpp_disco_service_get_jid(XmppDiscoService *service);
+void xmpp_disco_service_set_jid(XmppDiscoService *service, const char *jid);
+
+const char *xmpp_disco_service_get_node(XmppDiscoService *service);
+void xmpp_disco_service_set_node(XmppDiscoService *service, const char *node);
+
+gboolean xmpp_disco_service_get_expanded(XmppDiscoService *service);
+void xmpp_disco_service_set_expanded(XmppDiscoService *service, gboolean expanded);
+
+char *xmpp_disco_service_get_icon_name(XmppDiscoService *service);
+
+#endif /* PIDGIN_XMPP_DISCO_SERVICE_H */
diff --git a/pidgin/plugins/disco/xmppdiscoservice.c b/pidgin/plugins/disco/xmppdiscoservice.c
new file mode 100644
--- /dev/null
+++ b/pidgin/plugins/disco/xmppdiscoservice.c
@@ -0,0 +1,497 @@
+/*
+ * Purple - XMPP Service Disco Browser
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Pidgin 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 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 program 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 program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n-lib.h>
+
+#include "xmppdiscoservice.h"
+#include "xmppdiscoenums.h"
+
+struct _XmppDiscoService {
+	GObject parent;
+
+	PidginDiscoList *list;
+	char *name;
+	char *description;
+
+	char *gateway_type;
+	XmppDiscoServiceType type;
+	XmppDiscoServiceFlags flags;
+
+	XmppDiscoService *parent_service;
+	char *jid;
+	char *node;
+	gboolean expanded;
+};
+
+enum {
+	PROP_0,
+	PROP_LIST,
+	PROP_PARENT,
+	PROP_NAME,
+	PROP_DESCRIPTION,
+	PROP_SERVICE_TYPE,
+	PROP_GATEWAY_TYPE,
+	PROP_FLAGS,
+	PROP_JID,
+	PROP_NODE,
+	PROP_EXPANDED,
+	PROP_ICON_NAME,
+	PROP_LAST
+};
+
+static GParamSpec *properties[PROP_LAST] = {NULL};
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+xmpp_disco_service_set_list(XmppDiscoService *service, PidginDiscoList *list) {
+	service->list = list;
+
+	g_object_notify_by_pspec(G_OBJECT(service), properties[PROP_LIST]);
+}
+
+static void
+xmpp_disco_service_set_parent(XmppDiscoService *service,
+                              XmppDiscoService *parent)
+{
+	service->parent_service = parent;
+
+	g_object_notify_by_pspec(G_OBJECT(service), properties[PROP_PARENT]);
+}
+
+/******************************************************************************
+ * GObject implementation
+ *****************************************************************************/
+G_DEFINE_DYNAMIC_TYPE(XmppDiscoService, xmpp_disco_service, G_TYPE_OBJECT)
+
+static void
+xmpp_disco_service_get_property(GObject *object, guint prop_id, GValue *value,
+                                GParamSpec *pspec)
+{
+	XmppDiscoService *service = XMPP_DISCO_SERVICE(object);
+
+	switch(prop_id) {
+		case PROP_LIST:
+			g_value_set_pointer(value, xmpp_disco_service_get_list(service));
+			break;
+		case PROP_PARENT:
+			g_value_set_object(value, xmpp_disco_service_get_parent(service));
+			break;
+		case PROP_NAME:
+			g_value_set_string(value, xmpp_disco_service_get_name(service));
+			break;
+		case PROP_DESCRIPTION:
+			g_value_set_string(value,
+			                   xmpp_disco_service_get_description(service));
+			break;
+		case PROP_SERVICE_TYPE:
+			g_value_set_enum(value,
+			                 xmpp_disco_service_get_service_type(service));
+			break;
+		case PROP_GATEWAY_TYPE:
+			g_value_set_string(value,
+			                   xmpp_disco_service_get_gateway_type(service));
+			break;
+		case PROP_FLAGS:
+			g_value_set_flags(value, xmpp_disco_service_get_flags(service));
+			break;
+		case PROP_JID:
+			g_value_set_string(value, xmpp_disco_service_get_jid(service));
+			break;
+		case PROP_NODE:
+			g_value_set_string(value, xmpp_disco_service_get_node(service));
+			break;
+		case PROP_EXPANDED:
+			g_value_set_boolean(value,
+			                    xmpp_disco_service_get_expanded(service));
+			break;
+		case PROP_ICON_NAME:
+			g_value_take_string(value,
+			                    xmpp_disco_service_get_icon_name(service));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+			break;
+	}
+}
+
+static void
+xmpp_disco_service_set_property(GObject *object, guint prop_id,
+                                const GValue *value, GParamSpec *pspec)
+{
+	XmppDiscoService *service = XMPP_DISCO_SERVICE(object);
+
+	switch(prop_id) {
+		case PROP_LIST:
+			xmpp_disco_service_set_list(service, g_value_get_pointer(value));
+			break;
+		case PROP_PARENT:
+			xmpp_disco_service_set_parent(service, g_value_get_object(value));
+			break;
+		case PROP_NAME:
+			xmpp_disco_service_set_name(service, g_value_get_string(value));
+			break;
+		case PROP_DESCRIPTION:
+			xmpp_disco_service_set_description(service,
+			                                   g_value_get_string(value));
+			break;
+		case PROP_SERVICE_TYPE:
+			xmpp_disco_service_set_service_type(service,
+			                                    g_value_get_enum(value));
+			break;
+		case PROP_GATEWAY_TYPE:
+			xmpp_disco_service_set_gateway_type(service,
+			                                    g_value_get_string(value));
+			break;
+		case PROP_FLAGS:
+			xmpp_disco_service_set_flags(service, g_value_get_flags(value));
+			break;
+		case PROP_JID:
+			xmpp_disco_service_set_jid(service, g_value_get_string(value));
+			break;
+		case PROP_NODE:
+			xmpp_disco_service_set_node(service, g_value_get_string(value));
+			break;
+		case PROP_EXPANDED:
+			xmpp_disco_service_set_expanded(service,
+			                                g_value_get_boolean(value));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+			break;
+	}
+}
+
+static void
+xmpp_disco_service_finalize(GObject *obj) {
+	XmppDiscoService *service = XMPP_DISCO_SERVICE(obj);
+
+	g_clear_pointer(&service->name, g_free);
+	g_clear_pointer(&service->description, g_free);
+	g_clear_pointer(&service->gateway_type, g_free);
+	g_clear_pointer(&service->jid, g_free);
+	g_clear_pointer(&service->node, g_free);
+
+	G_OBJECT_CLASS(xmpp_disco_service_parent_class)->finalize(obj);
+}
+
+static void
+xmpp_disco_service_class_init(XmppDiscoServiceClass *klass)
+{
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	/* Properties */
+	obj_class->get_property = xmpp_disco_service_get_property;
+	obj_class->set_property = xmpp_disco_service_set_property;
+	obj_class->finalize = xmpp_disco_service_finalize;
+
+	properties[PROP_LIST] = g_param_spec_pointer(
+	        "list", "list", "The list displaying this service.",
+	        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	properties[PROP_PARENT] = g_param_spec_object(
+	        "parent", "parent", "The parent of this service.",
+	        XMPP_DISCO_TYPE_SERVICE,
+	        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	properties[PROP_NAME] = g_param_spec_string(
+	        "name", "name", "The name of this service.",
+	        NULL,
+	        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	properties[PROP_DESCRIPTION] = g_param_spec_string(
+	        "description", "description", "The description of this service.",
+	        NULL,
+	        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	properties[PROP_SERVICE_TYPE] = g_param_spec_enum(
+	        "service-type", "service-type", "The service type of this service.",
+	        XMPP_DISCO_TYPE_SERVICE_TYPE, XMPP_DISCO_SERVICE_TYPE_UNSET,
+	        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	properties[PROP_GATEWAY_TYPE] = g_param_spec_string(
+	        "gateway-type", "gateway-type", "The gateway type of this service.",
+	        NULL,
+	        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	properties[PROP_FLAGS] = g_param_spec_flags(
+	        "flags", "flags", "The flags of this service.",
+	        XMPP_DISCO_TYPE_SERVICE_FLAGS, XMPP_DISCO_NONE,
+	        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	properties[PROP_JID] = g_param_spec_string(
+	        "jid", "jid", "The jid of this service.",
+	        NULL,
+	        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	properties[PROP_NODE] = g_param_spec_string(
+	        "node", "node", "The node of this service.",
+	        NULL,
+	        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	properties[PROP_EXPANDED] = g_param_spec_boolean(
+	        "expanded", "expanded", "Whether this service expanded.",
+	        FALSE,
+	        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	properties[PROP_ICON_NAME] = g_param_spec_string(
+	        "icon-name", "icon-name", "The icon name of this service.",
+	        NULL,
+	        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, PROP_LAST, properties);
+}
+
+static void
+xmpp_disco_service_class_finalize(G_GNUC_UNUSED XmppDiscoServiceClass *klass) {
+}
+
+static void
+xmpp_disco_service_init(G_GNUC_UNUSED XmppDiscoService *service) {
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+
+void
+xmpp_disco_service_register(PurplePlugin *plugin) {
+	xmpp_disco_service_register_type(G_TYPE_MODULE(plugin));
+}
+
+XmppDiscoService *
+xmpp_disco_service_new(PidginDiscoList *list, XmppDiscoService *parent) {
+	return g_object_new(
+		XMPP_DISCO_TYPE_SERVICE,
+		"list", list,
+		"parent", parent,
+		NULL);
+}
+
+PidginDiscoList *
+xmpp_disco_service_get_list(XmppDiscoService *service) {
+	g_return_val_if_fail(XMPP_DISCO_IS_SERVICE(service), NULL);
+
+	return service->list;
+}
+
+XmppDiscoService *
+xmpp_disco_service_get_parent(XmppDiscoService *service) {
+	g_return_val_if_fail(XMPP_DISCO_IS_SERVICE(service), NULL);
+
+	return service->parent_service;
+}
+
+const char *
+xmpp_disco_service_get_name(XmppDiscoService *service) {
+	g_return_val_if_fail(XMPP_DISCO_IS_SERVICE(service), NULL);
+
+	return service->name;
+}
+
+void
+xmpp_disco_service_set_name(XmppDiscoService *service, const char *name) {
+	g_return_if_fail(XMPP_DISCO_IS_SERVICE(service));
+
+	g_free(service->name);
+	service->name = g_strdup(name);
+
+	g_object_notify_by_pspec(G_OBJECT(service), properties[PROP_NAME]);
+}
+
+const char *
+xmpp_disco_service_get_description(XmppDiscoService *service) {
+	g_return_val_if_fail(XMPP_DISCO_IS_SERVICE(service), NULL);
+
+	return service->description;
+}
+
+void
+xmpp_disco_service_set_description(XmppDiscoService *service,
+                                   const char *description)
+{
+	g_return_if_fail(XMPP_DISCO_IS_SERVICE(service));
+
+	g_free(service->description);
+	service->description = g_strdup(description);
+
+	g_object_notify_by_pspec(G_OBJECT(service), properties[PROP_DESCRIPTION]);
+}
+
+XmppDiscoServiceType
+xmpp_disco_service_get_service_type(XmppDiscoService *service) {
+	g_return_val_if_fail(XMPP_DISCO_IS_SERVICE(service),
+	                     XMPP_DISCO_SERVICE_TYPE_UNSET);
+
+	return service->type;
+}
+
+void
+xmpp_disco_service_set_service_type(XmppDiscoService *service,
+                                    XmppDiscoServiceType type)
+{
+	GObject *obj = NULL;
+	g_return_if_fail(XMPP_DISCO_IS_SERVICE(service));
+
+	service->type = type;
+
+	obj = G_OBJECT(service);
+	g_object_freeze_notify(obj);
+	g_object_notify_by_pspec(obj, properties[PROP_SERVICE_TYPE]);
+	g_object_notify_by_pspec(obj, properties[PROP_ICON_NAME]);
+	g_object_thaw_notify(obj);
+}
+
+const char *
+xmpp_disco_service_get_gateway_type(XmppDiscoService *service) {
+	g_return_val_if_fail(XMPP_DISCO_IS_SERVICE(service), NULL);
+
+	return service->gateway_type;
+}
+
+void
+xmpp_disco_service_set_gateway_type(XmppDiscoService *service,
+                                    const char *gateway_type)
+{
+	GObject *obj = NULL;
+	g_return_if_fail(XMPP_DISCO_IS_SERVICE(service));
+
+	g_free(service->gateway_type);
+	service->gateway_type = g_strdup(gateway_type);
+
+	obj = G_OBJECT(service);
+	g_object_freeze_notify(obj);
+	g_object_notify_by_pspec(obj, properties[PROP_GATEWAY_TYPE]);
+	g_object_notify_by_pspec(obj, properties[PROP_ICON_NAME]);
+	g_object_thaw_notify(obj);
+}
+
+XmppDiscoServiceFlags
+xmpp_disco_service_get_flags(XmppDiscoService *service) {
+	g_return_val_if_fail(XMPP_DISCO_IS_SERVICE(service), XMPP_DISCO_NONE);
+
+	return service->flags;
+}
+
+void
+xmpp_disco_service_set_flags(XmppDiscoService *service,
+                             XmppDiscoServiceFlags flags)
+{
+	g_return_if_fail(XMPP_DISCO_IS_SERVICE(service));
+
+	service->flags = flags;
+
+	g_object_notify_by_pspec(G_OBJECT(service), properties[PROP_FLAGS]);
+}
+
+void
+xmpp_disco_service_add_flags(XmppDiscoService *service,
+                             XmppDiscoServiceFlags flags)
+{
+	g_return_if_fail(XMPP_DISCO_IS_SERVICE(service));
+
+	service->flags |= flags;
+
+	g_object_notify_by_pspec(G_OBJECT(service), properties[PROP_FLAGS]);
+}
+
+void
+xmpp_disco_service_remove_flags(XmppDiscoService *service,
+                                XmppDiscoServiceFlags flags)
+{
+	g_return_if_fail(XMPP_DISCO_IS_SERVICE(service));
+
+	service->flags &= ~flags;
+
+	g_object_notify_by_pspec(G_OBJECT(service), properties[PROP_FLAGS]);
+}
+
+const char *
+xmpp_disco_service_get_jid(XmppDiscoService *service) {
+	g_return_val_if_fail(XMPP_DISCO_IS_SERVICE(service), NULL);
+
+	return service->jid;
+}
+
+void
+xmpp_disco_service_set_jid(XmppDiscoService *service, const char *jid) {
+	g_return_if_fail(XMPP_DISCO_IS_SERVICE(service));
+
+	g_free(service->jid);
+	service->jid = g_strdup(jid);
+
+	g_object_notify_by_pspec(G_OBJECT(service), properties[PROP_JID]);
+}
+
+const char *
+xmpp_disco_service_get_node(XmppDiscoService *service) {
+	g_return_val_if_fail(XMPP_DISCO_IS_SERVICE(service), NULL);
+
+	return service->node;
+}
+
+void
+xmpp_disco_service_set_node(XmppDiscoService *service, const char *node) {
+	g_return_if_fail(XMPP_DISCO_IS_SERVICE(service));
+
+	g_free(service->node);
+	service->node = g_strdup(node);
+
+	g_object_notify_by_pspec(G_OBJECT(service), properties[PROP_NODE]);
+}
+
+gboolean
+xmpp_disco_service_get_expanded(XmppDiscoService *service) {
+	g_return_val_if_fail(XMPP_DISCO_IS_SERVICE(service), FALSE);
+
+	return service->expanded;
+}
+
+void
+xmpp_disco_service_set_expanded(XmppDiscoService *service, gboolean expanded) {
+	g_return_if_fail(XMPP_DISCO_IS_SERVICE(service));
+
+	service->expanded = expanded;
+
+	g_object_notify_by_pspec(G_OBJECT(service), properties[PROP_EXPANDED]);
+}
+
+char *
+xmpp_disco_service_get_icon_name(XmppDiscoService *service)
+{
+	char *icon_name = NULL;
+
+	g_return_val_if_fail(XMPP_DISCO_IS_SERVICE(service), NULL);
+
+	if(service->type == XMPP_DISCO_SERVICE_TYPE_GATEWAY && service->gateway_type != NULL) {
+		icon_name = g_strconcat("im-", service->gateway_type, NULL);
+#if 0
+	} else if(service->type == XMPP_DISCO_SERVICE_TYPE_USER) {
+		icon_name = g_strdup("person");
+#endif
+	} else if(service->type == XMPP_DISCO_SERVICE_TYPE_CHAT) {
+		icon_name = g_strdup("chat");
+	}
+
+	return icon_name;
+}
