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?
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:
31 <item jid='darkrain42.org' node='announce' name='Announcements'/>
33 <iq from='darkrain42.org' type='result'>
34 <query xmlns='http://jabber.org/protocol/disco#info' node='announce'/>
36 * For services that are a JID w/o a node, handle fetching ad-hoc commands?
47 #include "gtkplugin.h"
49 #include "xmppdisco.h"
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
,
62 PidginDiscoList
*list
;
63 XmppDiscoService
*parent
;
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
;
85 static guint32 index
= 0;
89 index
= g_random_int();
93 return g_strdup_printf("purpledisco%x", index
++);
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
;
105 pidgin_disco_list_unref(item_data
->list
);
106 g_free(item_data
->name
);
107 g_free(item_data
->node
);
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
);
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
));
141 xmpp_iq_register_callback(PurpleConnection
*pc
, gchar
*id
, gpointer data
,
144 struct xmpp_iq_cb_data
*cbdata
= g_new0(struct xmpp_iq_cb_data
, 1);
146 cbdata
->context
= data
;
150 g_hash_table_insert(iq_callbacks
, id
, cbdata
);
153 PurpleProtocol
*protocol
= purple_protocols_find(XMPP_PROTOCOL_ID
);
155 purple_signal_connect(protocol
, "jabber-receiving-iq", my_plugin
,
156 PURPLE_CALLBACK(xmpp_iq_received
), NULL
);
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
);
175 purple_xmlnode_set_attrib(query
, "node", node
);
178 xmpp_iq_register_callback(pc
, id
, cbdata
, cb
);
180 purple_signal_emit(purple_connection_get_protocol(pc
), "jabber-sending-xmlnode",
183 purple_xmlnode_free(iq
);
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
);
201 purple_xmlnode_set_attrib(query
, "node", node
);
204 xmpp_iq_register_callback(pc
, id
, cbdata
, cb
);
206 purple_signal_emit(purple_connection_get_protocol(pc
), "jabber-sending-xmlnode",
209 purple_xmlnode_free(iq
);
212 static XmppDiscoServiceType
213 disco_service_type_from_identity(PurpleXmlNode
*identity
)
215 const char *category
, *type
;
218 return XMPP_DISCO_SERVICE_TYPE_OTHER
;
220 category
= purple_xmlnode_get_attrib(identity
, "category");
221 type
= purple_xmlnode_get_attrib(identity
, "type");
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
;
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 {
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) */
259 disco_type_from_string(const gchar
*str
)
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 */
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
;
284 if (!list
->in_progress
)
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
;
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
;
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
;
313 service
->name
= g_strdup(from
);
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
;
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
)) {
331 if (!(var
= purple_xmlnode_get_attrib(feature
, "var")))
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
);
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
);
358 pidgin_disco_list_unref(list
);
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
;
372 if (!list
->in_progress
)
375 if (purple_strequal(type
, "result") &&
376 (query
= purple_xmlnode_get_child(iq
, "query"))) {
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");
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
);
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
);
414 pidgin_disco_list_ref(list
);
415 xmpp_disco_info_do(pc
, item_data2
, jid
, node
, got_info_cb
);
421 pidgin_disco_add_service(list
, NULL
, item_data
->parent
);
424 if (list
->fetch_count
== 0)
425 pidgin_disco_list_set_in_progress(list
, FALSE
);
428 pidgin_disco_list_unref(list
);
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
;
442 if (purple_strequal(type
, "result") &&
443 (query
= purple_xmlnode_get_child(iq
, "query"))) {
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
;
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
);
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
);
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
;
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
)) {
499 xmpp_disco_items_do(pc
, cb_data
, from
, NULL
/* node */, server_items_cb
);
501 pidgin_disco_list_ref(list
);
504 pidgin_disco_list_set_in_progress(list
, FALSE
);
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"),
517 purple_notify_error(my_plugin
, _("Error"),
518 _("Server does not support service discovery"),
521 pidgin_disco_list_set_in_progress(list
, FALSE
);
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
);
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
)
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
,
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
);
582 purple_xmlnode_free(iq
);
587 create_dialog(PurplePluginAction
*action
)
589 pidgin_disco_dialog_new();
593 actions(PurplePlugin
*plugin
)
596 PurplePluginAction
*action
= NULL
;
598 action
= purple_plugin_action_new(_("XMPP Service Discovery"),
600 l
= g_list_prepend(l
, action
);
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>",
623 return pidgin_plugin_info_new(
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."),
632 "website", PURPLE_WEBSITE
,
633 "abi-version", PURPLE_ABI_VERSION
,
634 "actions-cb", actions
,
640 plugin_load(PurplePlugin
*plugin
, GError
**error
)
642 PurpleProtocol
*xmpp_protocol
;
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."));
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
);
663 plugin_unload(PurplePlugin
*plugin
, GError
**error
)
665 g_hash_table_destroy(iq_callbacks
);
668 purple_signals_disconnect_by_handle(plugin
);
669 pidgin_disco_dialogs_destroy_all();
674 PURPLE_PLUGIN_INIT(xmppdisco
, plugin_query
, plugin_load
, plugin_unload
);