mark PurpleImageClass as private
[pidgin-git.git] / pidgin / plugins / disco / xmppdisco.c
blob8404afd302879a2be366b8fa77022928b074022f
1 /*
2 * Purple - XMPP Service Disco Browser
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.
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.
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
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?
39 #include "internal.h"
40 #include "pidgin.h"
42 #include "debug.h"
43 #include "signals.h"
44 #include "version.h"
46 #include "gtkconv.h"
47 #include "gtkplugin.h"
49 #include "xmppdisco.h"
50 #include "gtkdisco.h"
52 /* Variables */
53 PurplePlugin *my_plugin = NULL;
54 static GHashTable *iq_callbacks = NULL;
55 static gboolean iq_listening = FALSE;
57 typedef void (*XmppIqCallback)(PurpleConnection *pc, const char *type,
58 const char *id, const char *from, PurpleXmlNode *iq,
59 gpointer data);
61 struct item_data {
62 PidginDiscoList *list;
63 XmppDiscoService *parent;
64 char *name;
65 char *node; /* disco#info replies don't always include the node */
68 struct xmpp_iq_cb_data
71 * Every IQ callback in this plugin uses the same structure for the
72 * callback data. It's a hack (it wouldn't scale), but it's used so that
73 * it's easy to clean up all the callbacks when the account disconnects
74 * (see remove_iq_callbacks_by_pc below).
76 struct item_data *context;
77 PurpleConnection *pc;
78 XmppIqCallback cb;
82 static char*
83 generate_next_id()
85 static guint32 index = 0;
87 if (index == 0) {
88 do {
89 index = g_random_int();
90 } while (index == 0);
93 return g_strdup_printf("purpledisco%x", index++);
96 static gboolean
97 remove_iq_callbacks_by_pc(gpointer key, gpointer value, gpointer user_data)
99 struct xmpp_iq_cb_data *cb_data = value;
101 if (cb_data && cb_data->pc == user_data) {
102 struct item_data *item_data = cb_data->context;
104 if (item_data) {
105 pidgin_disco_list_unref(item_data->list);
106 g_free(item_data->name);
107 g_free(item_data->node);
108 g_free(item_data);
111 return TRUE;
112 } else
113 return FALSE;
116 static gboolean
117 xmpp_iq_received(PurpleConnection *pc, const char *type, const char *id,
118 const char *from, PurpleXmlNode *iq)
120 struct xmpp_iq_cb_data *cb_data;
122 cb_data = g_hash_table_lookup(iq_callbacks, id);
123 if (!cb_data)
124 return FALSE;
126 cb_data->cb(cb_data->pc, type, id, from, iq, cb_data->context);
128 g_hash_table_remove(iq_callbacks, id);
129 if (g_hash_table_size(iq_callbacks) == 0) {
130 PurpleProtocol *protocol = purple_connection_get_protocol(pc);
131 iq_listening = FALSE;
132 purple_signal_disconnect(protocol, "jabber-receiving-iq", my_plugin,
133 PURPLE_CALLBACK(xmpp_iq_received));
136 /* Om nom nom nom */
137 return TRUE;
140 static void
141 xmpp_iq_register_callback(PurpleConnection *pc, gchar *id, gpointer data,
142 XmppIqCallback cb)
144 struct xmpp_iq_cb_data *cbdata = g_new0(struct xmpp_iq_cb_data, 1);
146 cbdata->context = data;
147 cbdata->cb = cb;
148 cbdata->pc = pc;
150 g_hash_table_insert(iq_callbacks, id, cbdata);
152 if (!iq_listening) {
153 PurpleProtocol *protocol = purple_protocols_find(XMPP_PROTOCOL_ID);
154 iq_listening = TRUE;
155 purple_signal_connect(protocol, "jabber-receiving-iq", my_plugin,
156 PURPLE_CALLBACK(xmpp_iq_received), NULL);
160 static void
161 xmpp_disco_info_do(PurpleConnection *pc, gpointer cbdata, const char *jid,
162 const char *node, XmppIqCallback cb)
164 PurpleXmlNode *iq, *query;
165 char *id = generate_next_id();
167 iq = purple_xmlnode_new("iq");
168 purple_xmlnode_set_attrib(iq, "type", "get");
169 purple_xmlnode_set_attrib(iq, "to", jid);
170 purple_xmlnode_set_attrib(iq, "id", id);
172 query = purple_xmlnode_new_child(iq, "query");
173 purple_xmlnode_set_namespace(query, NS_DISCO_INFO);
174 if (node)
175 purple_xmlnode_set_attrib(query, "node", node);
177 /* Steals id */
178 xmpp_iq_register_callback(pc, id, cbdata, cb);
180 purple_signal_emit(purple_connection_get_protocol(pc), "jabber-sending-xmlnode",
181 pc, &iq);
182 if (iq != NULL)
183 purple_xmlnode_free(iq);
186 static void
187 xmpp_disco_items_do(PurpleConnection *pc, gpointer cbdata, const char *jid,
188 const char *node, XmppIqCallback cb)
190 PurpleXmlNode *iq, *query;
191 char *id = generate_next_id();
193 iq = purple_xmlnode_new("iq");
194 purple_xmlnode_set_attrib(iq, "type", "get");
195 purple_xmlnode_set_attrib(iq, "to", jid);
196 purple_xmlnode_set_attrib(iq, "id", id);
198 query = purple_xmlnode_new_child(iq, "query");
199 purple_xmlnode_set_namespace(query, NS_DISCO_ITEMS);
200 if (node)
201 purple_xmlnode_set_attrib(query, "node", node);
203 /* Steals id */
204 xmpp_iq_register_callback(pc, id, cbdata, cb);
206 purple_signal_emit(purple_connection_get_protocol(pc), "jabber-sending-xmlnode",
207 pc, &iq);
208 if (iq != NULL)
209 purple_xmlnode_free(iq);
212 static XmppDiscoServiceType
213 disco_service_type_from_identity(PurpleXmlNode *identity)
215 const char *category, *type;
217 if (!identity)
218 return XMPP_DISCO_SERVICE_TYPE_OTHER;
220 category = purple_xmlnode_get_attrib(identity, "category");
221 type = purple_xmlnode_get_attrib(identity, "type");
223 if (!category)
224 return XMPP_DISCO_SERVICE_TYPE_OTHER;
226 if (purple_strequal(category, "conference"))
227 return XMPP_DISCO_SERVICE_TYPE_CHAT;
228 else if (purple_strequal(category, "directory"))
229 return XMPP_DISCO_SERVICE_TYPE_DIRECTORY;
230 else if (purple_strequal(category, "gateway"))
231 return XMPP_DISCO_SERVICE_TYPE_GATEWAY;
232 else if (purple_strequal(category, "pubsub")) {
233 if (!type || purple_strequal(type, "collection"))
234 return XMPP_DISCO_SERVICE_TYPE_PUBSUB_COLLECTION;
235 else if (purple_strequal(type, "leaf"))
236 return XMPP_DISCO_SERVICE_TYPE_PUBSUB_LEAF;
237 else if (purple_strequal(type, "service"))
238 return XMPP_DISCO_SERVICE_TYPE_OTHER;
239 else {
240 purple_debug_warning("xmppdisco", "Unknown pubsub type '%s'\n", type);
241 return XMPP_DISCO_SERVICE_TYPE_OTHER;
245 return XMPP_DISCO_SERVICE_TYPE_OTHER;
248 static const struct {
249 const char *from;
250 const char *to;
251 } disco_type_mappings[] = {
252 { "gadu-gadu", "gadu-gadu" }, /* the protocol is gg, but list_icon returns "gadu-gadu" */
253 { "sametime", "meanwhile" },
254 { "xmpp", "jabber" }, /* jabber (mentioned in case the protocol is renamed so this line will match) */
255 { NULL, NULL }
258 static const gchar *
259 disco_type_from_string(const gchar *str)
261 int i = 0;
263 g_return_val_if_fail(str != NULL, "");
265 for ( ; disco_type_mappings[i].from; ++i) {
266 if (!g_ascii_strcasecmp(str, disco_type_mappings[i].from))
267 return disco_type_mappings[i].to;
270 /* fallback to the string itself */
271 return str;
274 static void
275 got_info_cb(PurpleConnection *pc, const char *type, const char *id,
276 const char *from, PurpleXmlNode *iq, gpointer data)
278 struct item_data *item_data = data;
279 PidginDiscoList *list = item_data->list;
280 PurpleXmlNode *query;
282 --list->fetch_count;
284 if (!list->in_progress)
285 goto out;
287 if (purple_strequal(type, "result") &&
288 (query = purple_xmlnode_get_child(iq, "query"))) {
289 PurpleXmlNode *identity = purple_xmlnode_get_child(query, "identity");
290 XmppDiscoService *service;
291 PurpleXmlNode *feature;
293 service = g_new0(XmppDiscoService, 1);
294 service->list = item_data->list;
295 purple_debug_info("xmppdisco", "parent for %s is %p\n", from, item_data->parent);
296 service->parent = item_data->parent;
297 service->flags = 0;
298 service->type = disco_service_type_from_identity(identity);
300 if (item_data->node) {
301 if (item_data->name) {
302 service->name = item_data->name;
303 item_data->name = NULL;
304 } else
305 service->name = g_strdup(item_data->node);
307 service->node = item_data->node;
308 item_data->node = NULL;
310 if (service->type == XMPP_DISCO_SERVICE_TYPE_PUBSUB_COLLECTION)
311 service->flags |= XMPP_DISCO_BROWSE;
312 } else
313 service->name = g_strdup(from);
315 if (!service->node)
316 /* Only support adding JIDs, not JID+node combos */
317 service->flags |= XMPP_DISCO_ADD;
319 if (item_data->name) {
320 service->description = item_data->name;
321 item_data->name = NULL;
322 } else if (identity)
323 service->description = g_strdup(purple_xmlnode_get_attrib(identity, "name"));
325 /* TODO: Overlap with service->name a bit */
326 service->jid = g_strdup(from);
328 for (feature = purple_xmlnode_get_child(query, "feature"); feature;
329 feature = purple_xmlnode_get_next_twin(feature)) {
330 const char *var;
331 if (!(var = purple_xmlnode_get_attrib(feature, "var")))
332 continue;
334 if (purple_strequal(var, NS_REGISTER))
335 service->flags |= XMPP_DISCO_REGISTER;
336 else if (purple_strequal(var, NS_DISCO_ITEMS))
337 service->flags |= XMPP_DISCO_BROWSE;
338 else if (purple_strequal(var, NS_MUC)) {
339 service->flags |= XMPP_DISCO_BROWSE;
340 service->type = XMPP_DISCO_SERVICE_TYPE_CHAT;
344 if (service->type == XMPP_DISCO_SERVICE_TYPE_GATEWAY)
345 service->gateway_type = g_strdup(disco_type_from_string(
346 purple_xmlnode_get_attrib(identity, "type")));
348 pidgin_disco_add_service(list, service, service->parent);
351 out:
352 if (list->fetch_count == 0)
353 pidgin_disco_list_set_in_progress(list, FALSE);
355 g_free(item_data->name);
356 g_free(item_data->node);
357 g_free(item_data);
358 pidgin_disco_list_unref(list);
361 static void
362 got_items_cb(PurpleConnection *pc, const char *type, const char *id,
363 const char *from, PurpleXmlNode *iq, gpointer data)
365 struct item_data *item_data = data;
366 PidginDiscoList *list = item_data->list;
367 PurpleXmlNode *query;
368 gboolean has_items = FALSE;
370 --list->fetch_count;
372 if (!list->in_progress)
373 goto out;
375 if (purple_strequal(type, "result") &&
376 (query = purple_xmlnode_get_child(iq, "query"))) {
377 PurpleXmlNode *item;
379 for (item = purple_xmlnode_get_child(query, "item"); item;
380 item = purple_xmlnode_get_next_twin(item)) {
381 const char *jid = purple_xmlnode_get_attrib(item, "jid");
382 const char *name = purple_xmlnode_get_attrib(item, "name");
383 const char *node = purple_xmlnode_get_attrib(item, "node");
385 has_items = TRUE;
387 if (item_data->parent->type == XMPP_DISCO_SERVICE_TYPE_CHAT) {
388 /* This is a hacky first-order approximation. Any MUC
389 * component that has a >1 level hierarchy (a Yahoo MUC
390 * transport component probably does) will violate this.
392 * On the other hand, this is better than querying all the
393 * chats at conference.jabber.org to enumerate them.
395 XmppDiscoService *service = g_new0(XmppDiscoService, 1);
396 service->list = item_data->list;
397 service->parent = item_data->parent;
398 service->flags = XMPP_DISCO_ADD;
399 service->type = XMPP_DISCO_SERVICE_TYPE_CHAT;
401 service->name = g_strdup(name);
402 service->jid = g_strdup(jid);
403 service->node = g_strdup(node);
404 pidgin_disco_add_service(list, service, item_data->parent);
405 } else {
406 struct item_data *item_data2 = g_new0(struct item_data, 1);
408 item_data2->list = item_data->list;
409 item_data2->parent = item_data->parent;
410 item_data2->name = g_strdup(name);
411 item_data2->node = g_strdup(node);
413 ++list->fetch_count;
414 pidgin_disco_list_ref(list);
415 xmpp_disco_info_do(pc, item_data2, jid, node, got_info_cb);
420 if (!has_items)
421 pidgin_disco_add_service(list, NULL, item_data->parent);
423 out:
424 if (list->fetch_count == 0)
425 pidgin_disco_list_set_in_progress(list, FALSE);
427 g_free(item_data);
428 pidgin_disco_list_unref(list);
431 static void
432 server_items_cb(PurpleConnection *pc, const char *type, const char *id,
433 const char *from, PurpleXmlNode *iq, gpointer data)
435 struct item_data *cb_data = data;
436 PidginDiscoList *list = cb_data->list;
437 PurpleXmlNode *query;
439 g_free(cb_data);
440 --list->fetch_count;
442 if (purple_strequal(type, "result") &&
443 (query = purple_xmlnode_get_child(iq, "query"))) {
444 PurpleXmlNode *item;
446 for (item = purple_xmlnode_get_child(query, "item"); item;
447 item = purple_xmlnode_get_next_twin(item)) {
448 const char *jid = purple_xmlnode_get_attrib(item, "jid");
449 const char *name = purple_xmlnode_get_attrib(item, "name");
450 const char *node = purple_xmlnode_get_attrib(item, "node");
451 struct item_data *item_data;
453 if (!jid)
454 continue;
456 item_data = g_new0(struct item_data, 1);
457 item_data->list = list;
458 item_data->name = g_strdup(name);
459 item_data->node = g_strdup(node);
461 ++list->fetch_count;
462 pidgin_disco_list_ref(list);
463 xmpp_disco_info_do(pc, item_data, jid, node, got_info_cb);
467 if (list->fetch_count == 0)
468 pidgin_disco_list_set_in_progress(list, FALSE);
470 pidgin_disco_list_unref(list);
473 static void
474 server_info_cb(PurpleConnection *pc, const char *type, const char *id,
475 const char *from, PurpleXmlNode *iq, gpointer data)
477 struct item_data *cb_data = data;
478 PidginDiscoList *list = cb_data->list;
479 PurpleXmlNode *query;
480 PurpleXmlNode *error;
481 gboolean items = FALSE;
483 --list->fetch_count;
485 if (purple_strequal(type, "result") &&
486 (query = purple_xmlnode_get_child(iq, "query"))) {
487 PurpleXmlNode *feature;
489 for (feature = purple_xmlnode_get_child(query, "feature"); feature;
490 feature = purple_xmlnode_get_next_twin(feature)) {
491 const char *var = purple_xmlnode_get_attrib(feature, "var");
492 if (purple_strequal(var, NS_DISCO_ITEMS)) {
493 items = TRUE;
494 break;
498 if (items) {
499 xmpp_disco_items_do(pc, cb_data, from, NULL /* node */, server_items_cb);
500 ++list->fetch_count;
501 pidgin_disco_list_ref(list);
503 else {
504 pidgin_disco_list_set_in_progress(list, FALSE);
505 g_free(cb_data);
508 else {
509 error = purple_xmlnode_get_child(iq, "error");
510 if (purple_xmlnode_get_child(error, "remote-server-not-found")
511 || purple_xmlnode_get_child(error, "jid-malformed")) {
512 purple_notify_error(my_plugin, _("Error"),
513 _("Server does not exist"),
514 NULL, NULL);
516 else {
517 purple_notify_error(my_plugin, _("Error"),
518 _("Server does not support service discovery"),
519 NULL, NULL);
521 pidgin_disco_list_set_in_progress(list, FALSE);
522 g_free(cb_data);
525 pidgin_disco_list_unref(list);
528 void xmpp_disco_start(PidginDiscoList *list)
530 struct item_data *cb_data;
532 g_return_if_fail(list != NULL);
534 ++list->fetch_count;
535 pidgin_disco_list_ref(list);
537 cb_data = g_new0(struct item_data, 1);
538 cb_data->list = list;
540 xmpp_disco_info_do(list->pc, cb_data, list->server, NULL, server_info_cb);
543 void xmpp_disco_service_expand(XmppDiscoService *service)
545 struct item_data *item_data;
547 g_return_if_fail(service != NULL);
549 if (service->expanded)
550 return;
552 item_data = g_new0(struct item_data, 1);
553 item_data->list = service->list;
554 item_data->parent = service;
556 ++service->list->fetch_count;
557 pidgin_disco_list_ref(service->list);
559 pidgin_disco_list_set_in_progress(service->list, TRUE);
561 xmpp_disco_items_do(service->list->pc, item_data, service->jid, service->node,
562 got_items_cb);
563 service->expanded = TRUE;
566 void xmpp_disco_service_register(XmppDiscoService *service)
568 PurpleXmlNode *iq, *query;
569 char *id = generate_next_id();
571 iq = purple_xmlnode_new("iq");
572 purple_xmlnode_set_attrib(iq, "type", "get");
573 purple_xmlnode_set_attrib(iq, "to", service->jid);
574 purple_xmlnode_set_attrib(iq, "id", id);
576 query = purple_xmlnode_new_child(iq, "query");
577 purple_xmlnode_set_namespace(query, NS_REGISTER);
579 purple_signal_emit(purple_connection_get_protocol(service->list->pc),
580 "jabber-sending-xmlnode", service->list->pc, &iq);
581 if (iq != NULL)
582 purple_xmlnode_free(iq);
583 g_free(id);
586 static void
587 create_dialog(PurplePluginAction *action)
589 pidgin_disco_dialog_new();
592 static GList *
593 actions(PurplePlugin *plugin)
595 GList *l = NULL;
596 PurplePluginAction *action = NULL;
598 action = purple_plugin_action_new(_("XMPP Service Discovery"),
599 create_dialog);
600 l = g_list_prepend(l, action);
602 return l;
605 static void
606 signed_off_cb(PurpleConnection *pc, gpointer unused)
608 /* Deal with any dialogs */
609 pidgin_disco_signed_off_cb(pc);
611 /* Remove all the IQ callbacks for this connection */
612 g_hash_table_foreach_remove(iq_callbacks, remove_iq_callbacks_by_pc, pc);
615 static PidginPluginInfo *
616 plugin_query(GError **error)
618 const gchar * const authors[] = {
619 "Paul Aurich <paul@darkrain42.org>",
620 NULL
623 return pidgin_plugin_info_new(
624 "id", PLUGIN_ID,
625 "name", N_("XMPP Service Discovery"),
626 "version", DISPLAY_VERSION,
627 "category", N_("Protocol utility"),
628 "summary", N_("Allows browsing and registering services."),
629 "description", N_("This plugin is useful for registering with legacy "
630 "transports or other XMPP services."),
631 "authors", authors,
632 "website", PURPLE_WEBSITE,
633 "abi-version", PURPLE_ABI_VERSION,
634 "actions-cb", actions,
635 NULL
639 static gboolean
640 plugin_load(PurplePlugin *plugin, GError **error)
642 PurpleProtocol *xmpp_protocol;
644 my_plugin = plugin;
646 xmpp_protocol = purple_protocols_find(XMPP_PROTOCOL_ID);
647 if (NULL == xmpp_protocol) {
648 g_set_error(error, PLUGIN_DOMAIN, 0, _("XMPP protocol is not loaded."));
649 return FALSE;
652 pidgin_disco_dialog_register(plugin);
654 purple_signal_connect(purple_connections_get_handle(), "signing-off",
655 plugin, PURPLE_CALLBACK(signed_off_cb), NULL);
657 iq_callbacks = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
659 return TRUE;
662 static gboolean
663 plugin_unload(PurplePlugin *plugin, GError **error)
665 g_hash_table_destroy(iq_callbacks);
666 iq_callbacks = NULL;
668 purple_signals_disconnect_by_handle(plugin);
669 pidgin_disco_dialogs_destroy_all();
671 return TRUE;
674 PURPLE_PLUGIN_INIT(xmppdisco, plugin_query, plugin_load, plugin_unload);