1 /*
2  * Purple - XMPP Service Disco Browser
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301 USA
17  *
18  */
19 
20 /* TODO list (a little bit of a brain dump):
21 	* Support more actions than "register" and "add" based on context.
22 		- Subscribe to pubsub nodes (just...because?)
23 		- Execute ad-hoc commands
24 		- Change 'Register' to 'Unregister' if we're registered?
25 		- Administer MUCs
26 	* Enumerate pubsub node contents.
27 		- PEP too? (useful development tool at times)
28 	* See if we can better handle the ad-hoc commands that ejabberd returns
29 	  when disco'ing a server as an administrator:
30 from disco#items:
31 	<item jid='darkrain42.org' node='announce' name='Announcements'/>
32 disco#info:
33 	<iq from='darkrain42.org' type='result'>
34 		<query xmlns='http://jabber.org/protocol/disco#info' node='announce'/>
35 	</iq>
36 	* For services that are a JID w/o a node, handle fetching ad-hoc commands?
37 */
38 
39 #include "internal.h"
40 #include "pidgin.h"
41 
42 #include "debug.h"
43 #include "signals.h"
44 #include "version.h"
45 
46 #include "gtkconv.h"
47 #include "gtkimhtml.h"
48 #include "gtkplugin.h"
49 
50 #include "xmppdisco.h"
51 #include "gtkdisco.h"
52 
53 /* Variables */
54 PurplePlugin *my_plugin = NULL;
55 static GHashTable *iq_callbacks = NULL;
56 static gboolean iq_listening = FALSE;
57 
58 typedef void (*XmppIqCallback)(PurpleConnection *pc, const char *type,
59                                const char *id, const char *from, xmlnode *iq,
60                                gpointer data);
61 
62 struct item_data {
63 	PidginDiscoList *list;
64 	XmppDiscoService *parent;
65 	char *name;
66 	char *node; /* disco#info replies don't always include the node */
67 };
68 
69 struct xmpp_iq_cb_data
70 {
71 	/*
72 	 * Every IQ callback in this plugin uses the same structure for the
73 	 * callback data. It's a hack (it wouldn't scale), but it's used so that
74 	 * it's easy to clean up all the callbacks when the account disconnects
75 	 * (see remove_iq_callbacks_by_pc below).
76 	 */
77 	struct item_data *context;
78 	PurpleConnection *pc;
79 	XmppIqCallback cb;
80 };
81 
82 
83 static char*
generate_next_id()84 generate_next_id()
85 {
86 	static guint32 index = 0;
87 
88 	if (index == 0) {
89 		do {
90 			index = g_random_int();
91 		} while (index == 0);
92 	}
93 
94 	return g_strdup_printf("purpledisco%x", index++);
95 }
96 
97 static gboolean
remove_iq_callbacks_by_pc(gpointer key,gpointer value,gpointer user_data)98 remove_iq_callbacks_by_pc(gpointer key, gpointer value, gpointer user_data)
99 {
100 	struct xmpp_iq_cb_data *cb_data = value;
101 
102 	if (cb_data && cb_data->pc == user_data) {
103 		struct item_data *item_data = cb_data->context;
104 
105 		if (item_data) {
106 			pidgin_disco_list_unref(item_data->list);
107 			g_free(item_data->name);
108 			g_free(item_data->node);
109 			g_free(item_data);
110 		}
111 
112 		return TRUE;
113 	} else
114 		return FALSE;
115 }
116 
117 static gboolean
xmpp_iq_received(PurpleConnection * pc,const char * type,const char * id,const char * from,xmlnode * iq)118 xmpp_iq_received(PurpleConnection *pc, const char *type, const char *id,
119                  const char *from, xmlnode *iq)
120 {
121 	struct xmpp_iq_cb_data *cb_data;
122 
123 	cb_data = g_hash_table_lookup(iq_callbacks, id);
124 	if (!cb_data)
125 		return FALSE;
126 
127 	cb_data->cb(cb_data->pc, type, id, from, iq, cb_data->context);
128 
129 	g_hash_table_remove(iq_callbacks, id);
130 	if (g_hash_table_size(iq_callbacks) == 0) {
131 		PurplePlugin *prpl = purple_connection_get_prpl(pc);
132 		iq_listening = FALSE;
133 		purple_signal_disconnect(prpl, "jabber-receiving-iq", my_plugin,
134 		                         PURPLE_CALLBACK(xmpp_iq_received));
135 	}
136 
137 	/* Om nom nom nom */
138 	return TRUE;
139 }
140 
141 static void
xmpp_iq_register_callback(PurpleConnection * pc,gchar * id,gpointer data,XmppIqCallback cb)142 xmpp_iq_register_callback(PurpleConnection *pc, gchar *id, gpointer data,
143                           XmppIqCallback cb)
144 {
145 	struct xmpp_iq_cb_data *cbdata = g_new0(struct xmpp_iq_cb_data, 1);
146 
147 	cbdata->context = data;
148 	cbdata->cb = cb;
149 	cbdata->pc = pc;
150 
151 	g_hash_table_insert(iq_callbacks, id, cbdata);
152 
153 	if (!iq_listening) {
154 		PurplePlugin *prpl = purple_plugins_find_with_id(XMPP_PLUGIN_ID);
155 		iq_listening = TRUE;
156 		purple_signal_connect(prpl, "jabber-receiving-iq", my_plugin,
157 		                      PURPLE_CALLBACK(xmpp_iq_received), NULL);
158 	}
159 }
160 
161 static void
xmpp_disco_info_do(PurpleConnection * pc,gpointer cbdata,const char * jid,const char * node,XmppIqCallback cb)162 xmpp_disco_info_do(PurpleConnection *pc, gpointer cbdata, const char *jid,
163                    const char *node, XmppIqCallback cb)
164 {
165 	xmlnode *iq, *query;
166 	char *id = generate_next_id();
167 
168 	iq = xmlnode_new("iq");
169 	xmlnode_set_attrib(iq, "type", "get");
170 	xmlnode_set_attrib(iq, "to", jid);
171 	xmlnode_set_attrib(iq, "id", id);
172 
173 	query = xmlnode_new_child(iq, "query");
174 	xmlnode_set_namespace(query, NS_DISCO_INFO);
175 	if (node)
176 		xmlnode_set_attrib(query, "node", node);
177 
178 	/* Steals id */
179 	xmpp_iq_register_callback(pc, id, cbdata, cb);
180 
181 	purple_signal_emit(purple_connection_get_prpl(pc), "jabber-sending-xmlnode",
182 	                   pc, &iq);
183 	if (iq != NULL)
184 		xmlnode_free(iq);
185 }
186 
187 static void
xmpp_disco_items_do(PurpleConnection * pc,gpointer cbdata,const char * jid,const char * node,XmppIqCallback cb)188 xmpp_disco_items_do(PurpleConnection *pc, gpointer cbdata, const char *jid,
189                     const char *node, XmppIqCallback cb)
190 {
191 	xmlnode *iq, *query;
192 	char *id = generate_next_id();
193 
194 	iq = xmlnode_new("iq");
195 	xmlnode_set_attrib(iq, "type", "get");
196 	xmlnode_set_attrib(iq, "to", jid);
197 	xmlnode_set_attrib(iq, "id", id);
198 
199 	query = xmlnode_new_child(iq, "query");
200 	xmlnode_set_namespace(query, NS_DISCO_ITEMS);
201 	if (node)
202 		xmlnode_set_attrib(query, "node", node);
203 
204 	/* Steals id */
205 	xmpp_iq_register_callback(pc, id, cbdata, cb);
206 
207 	purple_signal_emit(purple_connection_get_prpl(pc), "jabber-sending-xmlnode",
208 	                   pc, &iq);
209 	if (iq != NULL)
210 		xmlnode_free(iq);
211 }
212 
213 static XmppDiscoServiceType
disco_service_type_from_identity(xmlnode * identity)214 disco_service_type_from_identity(xmlnode *identity)
215 {
216 	const char *category, *type;
217 
218 	if (!identity)
219 		return XMPP_DISCO_SERVICE_TYPE_OTHER;
220 
221 	category = xmlnode_get_attrib(identity, "category");
222 	type = xmlnode_get_attrib(identity, "type");
223 
224 	if (!category)
225 		return XMPP_DISCO_SERVICE_TYPE_OTHER;
226 
227 	if (purple_strequal(category, "conference"))
228 		return XMPP_DISCO_SERVICE_TYPE_CHAT;
229 	else if (purple_strequal(category, "directory"))
230 		return XMPP_DISCO_SERVICE_TYPE_DIRECTORY;
231 	else if (purple_strequal(category, "gateway"))
232 		return XMPP_DISCO_SERVICE_TYPE_GATEWAY;
233 	else if (purple_strequal(category, "pubsub")) {
234 		if (!type || purple_strequal(type, "collection"))
235 			return XMPP_DISCO_SERVICE_TYPE_PUBSUB_COLLECTION;
236 		else if (purple_strequal(type, "leaf"))
237 			return XMPP_DISCO_SERVICE_TYPE_PUBSUB_LEAF;
238 		else if (purple_strequal(type, "service"))
239 			return XMPP_DISCO_SERVICE_TYPE_OTHER;
240 		else {
241 			purple_debug_warning("xmppdisco", "Unknown pubsub type '%s'\n", type);
242 			return XMPP_DISCO_SERVICE_TYPE_OTHER;
243 		}
244 	}
245 
246 	return XMPP_DISCO_SERVICE_TYPE_OTHER;
247 }
248 
249 static const struct {
250 	const char *from;
251 	const char *to;
252 } disco_type_mappings[] = {
253 	{ "gadu-gadu", "gadu-gadu" }, /* the prpl is prpl-gg, but list_icon returns "gadu-gadu" */
254 	{ "sametime",  "meanwhile" },
255 	{ "xmpp",      "jabber" }, /* prpl-jabber (mentioned in case the prpl is renamed so this line will match) */
256 	{ NULL,        NULL }
257 };
258 
259 static const gchar *
disco_type_from_string(const gchar * str)260 disco_type_from_string(const gchar *str)
261 {
262 	int i = 0;
263 
264 	g_return_val_if_fail(str != NULL, "");
265 
266 	for ( ; disco_type_mappings[i].from; ++i) {
267 		if (!strcasecmp(str, disco_type_mappings[i].from))
268 			return disco_type_mappings[i].to;
269 	}
270 
271 	/* fallback to the string itself */
272 	return str;
273 }
274 
275 static void
got_info_cb(PurpleConnection * pc,const char * type,const char * id,const char * from,xmlnode * iq,gpointer data)276 got_info_cb(PurpleConnection *pc, const char *type, const char *id,
277             const char *from, xmlnode *iq, gpointer data)
278 {
279 	struct item_data *item_data = data;
280 	PidginDiscoList *list = item_data->list;
281 	xmlnode *query;
282 
283 	--list->fetch_count;
284 
285 	if (!list->in_progress)
286 		goto out;
287 
288 	if (purple_strequal(type, "result") &&
289 			(query = xmlnode_get_child(iq, "query"))) {
290 		xmlnode *identity = xmlnode_get_child(query, "identity");
291 		XmppDiscoService *service;
292 		xmlnode *feature;
293 
294 		service = g_new0(XmppDiscoService, 1);
295 		service->list = item_data->list;
296 		purple_debug_info("xmppdisco", "parent for %s is %p\n", from, item_data->parent);
297 		service->parent = item_data->parent;
298 		service->flags = 0;
299 		service->type = disco_service_type_from_identity(identity);
300 
301 		if (item_data->node) {
302 			if (item_data->name) {
303 				service->name = item_data->name;
304 				item_data->name = NULL;
305 			} else
306 				service->name = g_strdup(item_data->node);
307 
308 			service->node = item_data->node;
309 			item_data->node = NULL;
310 
311 			if (service->type == XMPP_DISCO_SERVICE_TYPE_PUBSUB_COLLECTION)
312 				service->flags |= XMPP_DISCO_BROWSE;
313 		} else
314 			service->name = g_strdup(from);
315 
316 		if (!service->node)
317 			/* Only support adding JIDs, not JID+node combos */
318 			service->flags |= XMPP_DISCO_ADD;
319 
320 		if (item_data->name) {
321 			service->description = item_data->name;
322 			item_data->name = NULL;
323 		} else if (identity)
324 			service->description = g_strdup(xmlnode_get_attrib(identity, "name"));
325 
326 		/* TODO: Overlap with service->name a bit */
327 		service->jid = g_strdup(from);
328 
329 		for (feature = xmlnode_get_child(query, "feature"); feature;
330 				feature = xmlnode_get_next_twin(feature)) {
331 			const char *var;
332 			if (!(var = xmlnode_get_attrib(feature, "var")))
333 				continue;
334 
335 			if (purple_strequal(var, NS_REGISTER))
336 				service->flags |= XMPP_DISCO_REGISTER;
337 			else if (purple_strequal(var, NS_DISCO_ITEMS))
338 				service->flags |= XMPP_DISCO_BROWSE;
339 			else if (purple_strequal(var, NS_MUC)) {
340 				service->flags |= XMPP_DISCO_BROWSE;
341 				service->type = XMPP_DISCO_SERVICE_TYPE_CHAT;
342 			}
343 		}
344 
345 		if (service->type == XMPP_DISCO_SERVICE_TYPE_GATEWAY)
346 			service->gateway_type = g_strdup(disco_type_from_string(
347 					xmlnode_get_attrib(identity, "type")));
348 
349 		pidgin_disco_add_service(list, service, service->parent);
350 	}
351 
352 out:
353 	if (list->fetch_count == 0)
354 		pidgin_disco_list_set_in_progress(list, FALSE);
355 
356 	g_free(item_data->name);
357 	g_free(item_data->node);
358 	g_free(item_data);
359 	pidgin_disco_list_unref(list);
360 }
361 
362 static void
got_items_cb(PurpleConnection * pc,const char * type,const char * id,const char * from,xmlnode * iq,gpointer data)363 got_items_cb(PurpleConnection *pc, const char *type, const char *id,
364              const char *from, xmlnode *iq, gpointer data)
365 {
366 	struct item_data *item_data = data;
367 	PidginDiscoList *list = item_data->list;
368 	xmlnode *query;
369 	gboolean has_items = FALSE;
370 
371 	--list->fetch_count;
372 
373 	if (!list->in_progress)
374 		goto out;
375 
376 	if (purple_strequal(type, "result") &&
377 			(query = xmlnode_get_child(iq, "query"))) {
378 		xmlnode *item;
379 
380 		for (item = xmlnode_get_child(query, "item"); item;
381 				item = xmlnode_get_next_twin(item)) {
382 			const char *jid = xmlnode_get_attrib(item, "jid");
383 			const char *name = xmlnode_get_attrib(item, "name");
384 			const char *node = xmlnode_get_attrib(item, "node");
385 
386 			has_items = TRUE;
387 
388 			if (item_data->parent->type == XMPP_DISCO_SERVICE_TYPE_CHAT) {
389 				/* This is a hacky first-order approximation. Any MUC
390 				 * component that has a >1 level hierarchy (a Yahoo MUC
391 				 * transport component probably does) will violate this.
392 				 *
393 				 * On the other hand, this is better than querying all the
394 				 * chats at conference.jabber.org to enumerate them.
395 				 */
396 				XmppDiscoService *service = g_new0(XmppDiscoService, 1);
397 				service->list = item_data->list;
398 				service->parent = item_data->parent;
399 				service->flags = XMPP_DISCO_ADD;
400 				service->type = XMPP_DISCO_SERVICE_TYPE_CHAT;
401 
402 				service->name = g_strdup(name);
403 				service->jid = g_strdup(jid);
404 				service->node = g_strdup(node);
405 				pidgin_disco_add_service(list, service, item_data->parent);
406 			} else {
407 				struct item_data *item_data2 = g_new0(struct item_data, 1);
408 
409 				item_data2->list = item_data->list;
410 				item_data2->parent = item_data->parent;
411 				item_data2->name = g_strdup(name);
412 				item_data2->node = g_strdup(node);
413 
414 				++list->fetch_count;
415 				pidgin_disco_list_ref(list);
416 				xmpp_disco_info_do(pc, item_data2, jid, node, got_info_cb);
417 			}
418 		}
419 	}
420 
421 	if (!has_items)
422 		pidgin_disco_add_service(list, NULL, item_data->parent);
423 
424 out:
425 	if (list->fetch_count == 0)
426 		pidgin_disco_list_set_in_progress(list, FALSE);
427 
428 	g_free(item_data);
429 	pidgin_disco_list_unref(list);
430 }
431 
432 static void
server_items_cb(PurpleConnection * pc,const char * type,const char * id,const char * from,xmlnode * iq,gpointer data)433 server_items_cb(PurpleConnection *pc, const char *type, const char *id,
434                 const char *from, xmlnode *iq, gpointer data)
435 {
436 	struct item_data *cb_data = data;
437 	PidginDiscoList *list = cb_data->list;
438 	xmlnode *query;
439 
440 	g_free(cb_data);
441 	--list->fetch_count;
442 
443 	if (purple_strequal(type, "result") &&
444 			(query = xmlnode_get_child(iq, "query"))) {
445 		xmlnode *item;
446 
447 		for (item = xmlnode_get_child(query, "item"); item;
448 				item = xmlnode_get_next_twin(item)) {
449 			const char *jid = xmlnode_get_attrib(item, "jid");
450 			const char *name = xmlnode_get_attrib(item, "name");
451 			const char *node = xmlnode_get_attrib(item, "node");
452 			struct item_data *item_data;
453 
454 			if (!jid)
455 				continue;
456 
457 			item_data = g_new0(struct item_data, 1);
458 			item_data->list = list;
459 			item_data->name = g_strdup(name);
460 			item_data->node = g_strdup(node);
461 
462 			++list->fetch_count;
463 			pidgin_disco_list_ref(list);
464 			xmpp_disco_info_do(pc, item_data, jid, node, got_info_cb);
465 		}
466 	}
467 
468 	if (list->fetch_count == 0)
469 		pidgin_disco_list_set_in_progress(list, FALSE);
470 
471 	pidgin_disco_list_unref(list);
472 }
473 
474 static void
server_info_cb(PurpleConnection * pc,const char * type,const char * id,const char * from,xmlnode * iq,gpointer data)475 server_info_cb(PurpleConnection *pc, const char *type, const char *id,
476                const char *from, xmlnode *iq, gpointer data)
477 {
478 	struct item_data *cb_data = data;
479 	PidginDiscoList *list = cb_data->list;
480 	xmlnode *query;
481 	xmlnode *error;
482 	gboolean items = FALSE;
483 
484 	--list->fetch_count;
485 
486 	if (purple_strequal(type, "result") &&
487 			(query = xmlnode_get_child(iq, "query"))) {
488 		xmlnode *feature;
489 
490 		for (feature = xmlnode_get_child(query, "feature"); feature;
491 				feature = xmlnode_get_next_twin(feature)) {
492 			const char *var = xmlnode_get_attrib(feature, "var");
493 			if (purple_strequal(var, NS_DISCO_ITEMS)) {
494 				items = TRUE;
495 				break;
496 			}
497 		}
498 
499 		if (items) {
500 			xmpp_disco_items_do(pc, cb_data, from, NULL /* node */, server_items_cb);
501 			++list->fetch_count;
502 			pidgin_disco_list_ref(list);
503 		}
504 		else {
505 			pidgin_disco_list_set_in_progress(list, FALSE);
506 			g_free(cb_data);
507 		}
508 	}
509 	else {
510 		error = xmlnode_get_child(iq, "error");
511 		if (xmlnode_get_child(error, "remote-server-not-found")
512 		 || xmlnode_get_child(error, "jid-malformed")) {
513 			purple_notify_error(my_plugin, _("Error"),
514 			                    _("Server does not exist"),
515  			                    NULL);
516 		}
517 		else {
518 			purple_notify_error(my_plugin, _("Error"),
519 			                    _("Server does not support service discovery"),
520 			                    NULL);
521 		}
522 		pidgin_disco_list_set_in_progress(list, FALSE);
523 		g_free(cb_data);
524 	}
525 
526 	pidgin_disco_list_unref(list);
527 }
528 
xmpp_disco_start(PidginDiscoList * list)529 void xmpp_disco_start(PidginDiscoList *list)
530 {
531 	struct item_data *cb_data;
532 
533 	g_return_if_fail(list != NULL);
534 
535 	++list->fetch_count;
536 	pidgin_disco_list_ref(list);
537 
538 	cb_data = g_new0(struct item_data, 1);
539 	cb_data->list = list;
540 
541 	xmpp_disco_info_do(list->pc, cb_data, list->server, NULL, server_info_cb);
542 }
543 
xmpp_disco_service_expand(XmppDiscoService * service)544 void xmpp_disco_service_expand(XmppDiscoService *service)
545 {
546 	struct item_data *item_data;
547 
548 	g_return_if_fail(service != NULL);
549 
550 	if (service->expanded)
551 		return;
552 
553 	item_data = g_new0(struct item_data, 1);
554 	item_data->list = service->list;
555 	item_data->parent = service;
556 
557 	++service->list->fetch_count;
558 	pidgin_disco_list_ref(service->list);
559 
560 	pidgin_disco_list_set_in_progress(service->list, TRUE);
561 
562 	xmpp_disco_items_do(service->list->pc, item_data, service->jid, service->node,
563 	                    got_items_cb);
564 	service->expanded = TRUE;
565 }
566 
xmpp_disco_service_register(XmppDiscoService * service)567 void xmpp_disco_service_register(XmppDiscoService *service)
568 {
569 	xmlnode *iq, *query;
570 	char *id = generate_next_id();
571 
572 	iq = xmlnode_new("iq");
573 	xmlnode_set_attrib(iq, "type", "get");
574 	xmlnode_set_attrib(iq, "to", service->jid);
575 	xmlnode_set_attrib(iq, "id", id);
576 
577 	query = xmlnode_new_child(iq, "query");
578 	xmlnode_set_namespace(query, NS_REGISTER);
579 
580 	purple_signal_emit(purple_connection_get_prpl(service->list->pc),
581 			"jabber-sending-xmlnode", service->list->pc, &iq);
582 	if (iq != NULL)
583 		xmlnode_free(iq);
584 	g_free(id);
585 }
586 
587 static void
create_dialog(PurplePluginAction * action)588 create_dialog(PurplePluginAction *action)
589 {
590 	pidgin_disco_dialog_new();
591 }
592 
593 static GList *
actions(PurplePlugin * plugin,gpointer context)594 actions(PurplePlugin *plugin, gpointer context)
595 {
596 	GList *l = NULL;
597 	PurplePluginAction *action = NULL;
598 
599 	action = purple_plugin_action_new(_("XMPP Service Discovery"),
600 	                                  create_dialog);
601 	l = g_list_prepend(l, action);
602 
603 	return l;
604 }
605 
606 static void
signed_off_cb(PurpleConnection * pc,gpointer unused)607 signed_off_cb(PurpleConnection *pc, gpointer unused)
608 {
609 	/* Deal with any dialogs */
610 	pidgin_disco_signed_off_cb(pc);
611 
612 	/* Remove all the IQ callbacks for this connection */
613 	g_hash_table_foreach_remove(iq_callbacks, remove_iq_callbacks_by_pc, pc);
614 }
615 
616 static gboolean
plugin_load(PurplePlugin * plugin)617 plugin_load(PurplePlugin *plugin)
618 {
619 	PurplePlugin *xmpp_prpl;
620 
621 	my_plugin = plugin;
622 
623 	xmpp_prpl = purple_plugins_find_with_id(XMPP_PLUGIN_ID);
624 	if (NULL == xmpp_prpl)
625 		return FALSE;
626 
627 	purple_signal_connect(purple_connections_get_handle(), "signing-off",
628 	                      plugin, PURPLE_CALLBACK(signed_off_cb), NULL);
629 
630 	iq_callbacks = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
631 
632 	return TRUE;
633 }
634 
635 static gboolean
plugin_unload(PurplePlugin * plugin)636 plugin_unload(PurplePlugin *plugin)
637 {
638 	g_hash_table_destroy(iq_callbacks);
639 	iq_callbacks = NULL;
640 
641 	purple_signals_disconnect_by_handle(plugin);
642 	pidgin_disco_dialogs_destroy_all();
643 
644 	return TRUE;
645 }
646 
647 static PurplePluginInfo info =
648 {
649 	PURPLE_PLUGIN_MAGIC,
650 	PURPLE_MAJOR_VERSION,
651 	PURPLE_MINOR_VERSION,
652 	PURPLE_PLUGIN_STANDARD,
653 	PIDGIN_PLUGIN_TYPE,
654 	0,
655 	NULL,
656 	PURPLE_PRIORITY_DEFAULT,
657 	"gtk-xmppdisco",
658 	N_("XMPP Service Discovery"),
659 	DISPLAY_VERSION,
660 	N_("Allows browsing and registering services."),
661 	N_("This plugin is useful for registering with legacy transports or other "
662 	   "XMPP services."),
663 	"Paul Aurich <paul@darkrain42.org>",
664 	PURPLE_WEBSITE,
665 	plugin_load,
666 	plugin_unload,
667 	NULL,               /**< destroy    */
668 	NULL,               /**< ui_info    */
669 	NULL,               /**< extra_info */
670 	NULL,               /**< prefs_info */
671 	actions,
672 
673 	/* padding */
674 	NULL,
675 	NULL,
676 	NULL,
677 	NULL
678 };
679 
680 static void
init_plugin(PurplePlugin * plugin)681 init_plugin(PurplePlugin *plugin)
682 {
683 }
684 
685 PURPLE_INIT_PLUGIN(xmppdisco, init_plugin, info)
686