disco.c
author Myhailo Danylenko <isbear@ukrpost.net>
Mon, 20 Aug 2012 22:51:51 +0300
changeset 58 694833145b98
parent 54 170801fe3bdf
child 59 50173578fddc
permissions -rw-r--r--
Add a note about header


/* Copyright 2009,2010 Myhailo Danylenko
 *
 * Service discovery requests
 *
 * This file is part of mcabber-disco
 *
 * mcabber-disco 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 <http://www.gnu.org/licenses/>. */

#include <glib.h>
#include <loudmouth/loudmouth.h>
#include <string.h>

#include <mcabber/commands.h>
#include <mcabber/logprint.h>
#include <mcabber/utils.h>
#include <mcabber/hooks.h>
#include <mcabber/xmpp.h>
#include <mcabber/compl.h>
#include <mcabber/xmpp_defines.h>
#include <mcabber/screen.h>
#include <mcabber/hbuf.h>
#include <mcabber/modules.h>
#include <mcabber/caps.h>

#include "disco.h"

#include "config.h"

//
// module description
//

void disco_init   (void);
void disco_uninit (void);

#define DESCRIPTION ( "Service discovery requests\nProvides command /disco" )

module_info_t info_disco = {
	.branch      = MCABBER_BRANCH,
	.api         = MCABBER_API_VERSION,
	.version     = PROJECT_VERSION,
	.description = DESCRIPTION,
	.requires    = NULL,
	.init        = disco_init,
	.uninit      = disco_uninit,
	.next        = NULL,
};

//
// private types
//

// lm message handler userdata
typedef struct {
	disco_info_handler_t handler;
	gpointer             data;
	GDestroyNotify       notify;
} disco_info_reply_handler_t;

// lm message handler userdata
typedef struct {
	disco_items_handler_t handler;
	gpointer              data;
	GDestroyNotify        notify;
} disco_items_reply_handler_t;

// user request disco handler userdata (common for info and items)
typedef struct {
	gchar *jid;
	gchar *node;
} disco_handler_t;

//
// globals
//

#ifdef MCABBER_API_HAVE_CMD_ID
static gpointer disco_cmid = NULL;
#endif

static guint   disco_cid      = 0;
static guint   disco_hid      = 0;
static GSList *reply_handlers = NULL;

//
// destroyers
//

static void disco_info_reply_handler_destroy_notify (gpointer data)
{
	disco_info_reply_handler_t *cb = data;
	if (cb -> notify)
		cb -> notify (cb -> data);
	g_slice_free (disco_info_reply_handler_t, cb);
	return;
}

static void disco_items_reply_handler_destroy_notify (gpointer data)
{
	disco_items_reply_handler_t *cb = data;
	if (cb -> notify)
		cb -> notify (cb -> data);
	g_slice_free (disco_items_reply_handler_t, cb);
	return;
}

static void disco_handler_destroy_notify (gpointer data)
{
	disco_handler_t *cb = data;
	g_free (cb -> jid);
	g_free (cb -> node);
	g_slice_free (disco_handler_t, cb);
	return;
}

//
// lm reply handlers
//

static LmHandlerResult disco_info_reply_handler (LmMessageHandler *handler, LmConnection *connection, LmMessage *message, gpointer udata)
{
	disco_info_reply_handler_t *cb = udata;

	reply_handlers = g_slist_remove (reply_handlers, handler);

	switch (lm_message_get_sub_type (message)) {
	case LM_MESSAGE_SUB_TYPE_RESULT:

		{
			LmMessageNode *node       = lm_message_get_node (message);
			GSList        *identities = NULL;
			GSList        *features   = NULL;

			node = lm_message_node_get_child (node, "query");

			// check xmlns
			if (!node || g_strcmp0 (lm_message_node_get_attribute (node, "xmlns"), NS_DISCO_INFO))
				break;

			// parse request results
			if (node->children)
				for (node = node->children; node; node = node->next)
					if (!strcasecmp (node->name, "identity")) {
						disco_identity_t *identity = g_slice_new (disco_identity_t);

						identity -> category = lm_message_node_get_attribute (node, "category");
						identity -> type     = lm_message_node_get_attribute (node, "type");
						identity -> name     = lm_message_node_get_attribute (node, "name");
						identity -> reserved = NULL;

						identities = g_slist_append (identities, identity);
					} else if (!strcasecmp (node->name, "feature"))
						features = g_slist_insert_sorted (features, (gpointer) lm_message_node_get_attribute (node, "var"), (GCompareFunc) g_strcmp0);

			// call handler
			cb -> handler (identities, features, cb -> data);

			{ // free resources
				GSList *iel;

				for (iel = identities; iel; iel = iel -> next)
					g_slice_free (disco_identity_t, iel -> data);

				g_slist_free (identities);
				g_slist_free (features);
			}
		}

		break;

	case LM_MESSAGE_SUB_TYPE_ERROR:

		{
			LmMessageNode *node   = lm_message_get_node (message);
			const gchar   *from   = lm_message_node_get_attribute (node, "from");
			const gchar   *type;
			const gchar   *reason;

			node = lm_message_node_get_child (node, "error");
			type = lm_message_node_get_attribute (node, "type");
			if (node->children)
				reason = node->children->name;
			else
				reason = "undefined";

			// XXX: we need to inform user, but do we really need to print this on every possible error?
			scr_log_print (LPRINT_LOGNORM, "disco: Service info discovery for %s failed: %s - %s", from, type, reason);

			cb -> handler (NULL, NULL, cb -> data);
		}

		break;

	default:
		return LM_HANDLER_RESULT_ALLOW_MORE_HANDLERS;
	}

	return LM_HANDLER_RESULT_REMOVE_MESSAGE;
}

static LmHandlerResult disco_items_reply_handler (LmMessageHandler *handler, LmConnection *connection, LmMessage *message, gpointer udata)
{
	disco_items_reply_handler_t *cb = udata;

	reply_handlers = g_slist_remove (reply_handlers, handler);

	switch (lm_message_get_sub_type (message)) {
	case LM_MESSAGE_SUB_TYPE_RESULT:

		{
			LmMessageNode *node  = lm_message_get_node (message);
			GSList        *items = NULL;

			node = lm_message_node_get_child (node, "query");

			// check xmlns
			if (!node || g_strcmp0 (lm_message_node_get_attribute (node, "xmlns"), NS_DISCO_ITEMS))
				break;

			// parse request results
			if (node->children)
				for (node = node->children; node; node = node->next)
					if (!strcasecmp (node->name, "item")) {
						disco_item_t *item = g_slice_new (disco_item_t);

						item -> name = lm_message_node_get_attribute (node, "name");
						item -> jid  = lm_message_node_get_attribute (node, "jid");
						item -> node = lm_message_node_get_attribute (node, "node");

						items = g_slist_append (items, item);
					}

			// call handler
			cb -> handler (items, cb -> data);

			{ // free resources
				GSList *iel;

				for (iel = items; iel; iel = iel -> next)
					g_slice_free (disco_item_t, iel -> data);

				g_slist_free (items);
			}
		}

		break;

	case LM_MESSAGE_SUB_TYPE_ERROR:

		{
			LmMessageNode *node   = lm_message_get_node (message);
			const gchar   *from   = lm_message_node_get_attribute (node, "from");
			const gchar   *type;
			const gchar   *reason;

			node = lm_message_node_get_child (node, "error");
			type = lm_message_node_get_attribute (node, "type");
			if (node->children)
				reason = node->children->name;
			else
				reason = "undefined";

			// XXX: we need to inform user, but do we really need to print this on every possible error?
			scr_log_print (LPRINT_LOGNORM, "disco: Service items discovery for %s failed: %s - %s", from, type, reason);

			cb -> handler (NULL, cb -> data);
		}

		break;

	default:
		return LM_HANDLER_RESULT_ALLOW_MORE_HANDLERS;
	}

	return LM_HANDLER_RESULT_REMOVE_MESSAGE;
}

//
// disco requests sending
//

#if 0
static void disco_features_cb (gconstpointer data, gpointer userdata)
{
	const gchar  *feature  = data;
	GSList      **features = userdata;
	*features = g_slist_insert_sorted (*features, (gpointer) feature, (GCompareFunc) g_strcmp0);
	return;
}
#endif

gpointer disco_info_request (const gchar *jid, const gchar *dnode, disco_info_handler_t handler, gpointer userdata, GDestroyNotify notify)
{
	if (!handler || !xmpp_is_online ()) {
		if (notify)
			notify (userdata);
		return NULL;
	}

#if 0
	if (0 && !dnode) { // FIXME: no way to get identity(ies) from caps
		gchar       *bjid     = jidtodisp (jid);
		GSList      *buddy    = roster_find (bjid, jidsearch, ROSTER_TYPE_USER | ROSTER_TYPE_ROOM | ROSTER_TYPE_AGENT);
		const gchar *resource = strchr (jid, JID_RESOURCE_SEPARATOR);

		g_free (bjid);

		if (buddy) {
			const gchar *hash = buddy_resource_getcaps (BUDDATA(buddy), resource); // ?? will it all work?
			if (hash) { // cached result
				GSList *identities = NULL;
				GSList *features   = NULL;
				caps_foreach_feature (hash, disco_features_cb, &features);
				handler (identities, features, userdata);
				if (notify)
					notify (userdata);
				return NULL;
			}
		}
	}
#endif

	{ // send request
		LmMessage        *request;
		LmMessageNode    *node;
		LmMessageHandler *lhandler;
		GError           *error    = NULL;

		request = lm_message_new_with_sub_type (jid, LM_MESSAGE_TYPE_IQ, LM_MESSAGE_SUB_TYPE_GET);
		node    = lm_message_get_node (request);
		node    = lm_message_node_add_child (node, "query", NULL);
		lm_message_node_set_attribute (node, "xmlns", NS_DISCO_INFO);
		if (dnode)
			lm_message_node_set_attribute (node, "node", dnode);

		{
			disco_info_reply_handler_t *cb = g_slice_new (disco_info_reply_handler_t);

			lhandler = lm_message_handler_new (disco_info_reply_handler, cb, disco_info_reply_handler_destroy_notify);

			cb -> handler = handler;
			cb -> data    = userdata;
			cb -> notify  = notify;

			reply_handlers = g_slist_append (reply_handlers, lhandler);

			lm_connection_send_with_reply (lconnection, request, lhandler, &error);

			if (error) {
				// XXX destroy handler and return NULL?
				scr_log_print (LPRINT_DEBUG, "disco: Error sending disco request: %s.", error -> message);
				g_error_free (error);
			}

			lm_message_handler_unref (lhandler);
		}

		lm_message_unref (request);

		return lhandler;
	}
}

gpointer disco_items_request (const gchar *jid, const gchar *dnode, disco_items_handler_t handler, gpointer userdata, GDestroyNotify notify)
{
	if (!handler || !xmpp_is_online ()) {
		if (notify)
			notify (userdata);
		return NULL;
	}

	{ // send request
		LmMessage        *request;
		LmMessageNode    *node;
		GError           *error    = NULL;
		LmMessageHandler *lhandler;

		request = lm_message_new_with_sub_type (jid, LM_MESSAGE_TYPE_IQ, LM_MESSAGE_SUB_TYPE_GET);
		node    = lm_message_get_node (request);
		node    = lm_message_node_add_child (node, "query", NULL);
		lm_message_node_set_attribute (node, "xmlns", NS_DISCO_ITEMS);
		if (dnode)
			lm_message_node_set_attribute (node, "node", dnode);

		{
			disco_items_reply_handler_t *cb = g_slice_new (disco_items_reply_handler_t);

			lhandler = lm_message_handler_new (disco_items_reply_handler, cb, disco_items_reply_handler_destroy_notify);

			cb -> handler = handler;
			cb -> data    = userdata;
			cb -> notify  = notify;

			reply_handlers = g_slist_append (reply_handlers, lhandler);

			lm_connection_send_with_reply (lconnection, request, lhandler, &error);

			if (error) {
				// XXX destroy handler and return NULL?
				scr_log_print (LPRINT_DEBUG, "disco: Error sending disco request: %s.", error -> message);
				g_error_free (error);
			}

			lm_message_handler_unref (lhandler);
		}

		lm_message_unref (request);

		return lhandler;
	}
}

void disco_cancel_request (gpointer id)
{
	GSList *hel;

	for (hel = reply_handlers; hel; hel = hel -> next) {
		if (hel -> data == id) {
			LmMessageHandler *handler = id;
			reply_handlers = g_slist_remove (reply_handlers, handler);
			lm_message_handler_invalidate (handler);
#ifdef HAVE_LM_CONNECTION_UNREGISTER_REPLY_HANDLER
			if (lconnection)
				lm_connection_unregister_reply_handler (lconnection, handler);
#endif
			return;
		}
	}

	return;
}

//
// user requests handlers (print results)
//

static void disco_info_handler (GSList *identities, GSList *features, gpointer udata)
{
	disco_handler_t *cb   = udata;
	GString         *info = g_string_new ("Service discovery info results for ");

	g_string_append (info, cb -> jid);
	if (cb -> node)
		g_string_append_printf (info, " (%s):", cb -> node);
	else
		g_string_append_c (info, ':');

	{
		GString *tmp = g_string_new (NULL);
		GSList  *el;

		// compose identities part
		for (el = identities; el; el = el -> next) {
			disco_identity_t *identity = el -> data;
			g_string_append_printf (tmp, "\n    [%s (%s)] %s",
			                        identity -> category ? identity -> category : "none",
			                        identity -> type ? identity -> type : "none",
									identity -> name ? identity -> name : "");
		}

		if (tmp -> len) {
			g_string_append_printf (info, "\n  Identities:%s", tmp -> str);
			g_string_truncate (tmp, 0);
		}

		// compose features part
		for (el = features; el; el = el -> next) {
			gchar *feature = el -> data;
			g_string_append_printf (tmp, "\n    [%s]", feature ? feature : "none");
		}

		if (tmp -> len)
			g_string_append_printf (info, "\n  Features:%s", tmp -> str);

		g_string_free (tmp, TRUE);
	}

	{ // print to buddy's buffer
		gchar *bjid = jidtodisp (cb -> jid);

		scr_write_incoming_message (bjid, info -> str, 0, HBB_PREFIX_INFO, 0); // NO conversion from utf-8

		g_free (bjid);
	}

	g_string_free (info, TRUE);

	return;
}

static void disco_items_handler (GSList *items, gpointer udata)
{
	disco_handler_t *cb   = udata;
	GString         *info = g_string_new ("Service discovery items results for ");

	g_string_append (info, cb -> jid);
	if (cb -> node)
		g_string_append_printf (info, " (%s):", cb -> node);
	else
		g_string_append_c (info, ':');

	{
		GSList  *el;

		// add items info
		for (el = items; el; el = el -> next) {
			disco_item_t *item = el -> data;
			if (item -> node)
				g_string_append_printf (info, "\n  [%s (%s)] %s",
				                        item -> jid ? item -> jid : "none", item -> node,
										item -> name ? item -> name : "");
			else
				g_string_append_printf (info, "\n  [%s] %s",
				                        item -> jid ? item -> jid : "none",
										item -> name ? item -> name : "");
		}
	}

	{ // print to buddy's buffer
		gchar *bjid = jidtodisp (cb -> jid);

		scr_write_incoming_message (bjid, info -> str, 0, HBB_PREFIX_INFO, 0); // NO conversion from utf-8

		g_free (bjid);
	}

	g_string_free (info, TRUE);

	return;
}

//
// command
//

static void do_disco (char *arg)
{
	char **args = split_arg (arg, 3, 0);
	int    info = -1;

	if (!args[0] || !strcmp (args[0], "info"))
		info = 1;
	else if (!strcmp (args[0], "items"))
		info = 0;
	else
		scr_log_print (LPRINT_NORMAL, "Unknown subcomand.");

	if (info != -1) {
		char *to    = NULL;
		char *dnode = NULL;

		if (args[0] && args[1]) {
			char *p = args[1];

			if (*p == '.') {
				if (*(p + 1) == JID_RESOURCE_SEPARATOR) {
					char *rest = to_utf8 (p + 1);

					to = g_strdup_printf ("%s%s", CURRENT_JID, rest);
					g_free (rest);
				} else if (*(p + 1))
					to = to_utf8 (p);
			} else
				to = to_utf8 (p);

			if (args[2])
				dnode = to_utf8 (args[2]);
		}

		if (!to) {
			// Use currently selected item, if possible
			if (current_buddy) {
				gpointer bud = BUDDATA(current_buddy);
				if (bud) {
					guint type = buddy_gettype (bud);
					if (type != ROSTER_TYPE_GROUP && type != ROSTER_TYPE_SPECIAL)
						to = g_strdup (buddy_getjid (bud)); // XXX memleak!
				}
			}
		}

		if (!to) {
			scr_log_print (LPRINT_NORMAL, "Invalid disco target.");
			free_arg_lst (args);
			return;
		}

		// XXX send to all resources/current resource?

		{
			disco_handler_t *cb = g_slice_new (disco_handler_t);

			cb -> jid  = to;
			cb -> node = dnode;

			if (info)
				disco_info_request (cb -> jid, cb -> node, disco_info_handler, cb, disco_handler_destroy_notify);
			else
				disco_items_request (cb -> jid, cb -> node, disco_items_handler, cb, disco_handler_destroy_notify);
		}
	}

	free_arg_lst (args);
	return;
}

//
// module mechanics
//

static void disco_unregister_handlers (void)
{
	GSList *hel;

	for (hel = reply_handlers; hel; hel = hel -> next) {
		LmMessageHandler *handler = hel -> data;
		lm_message_handler_invalidate (handler);
#ifdef HAVE_LM_CONNECTION_UNREGISTER_REPLY_HANDLER
		if (lconnection)
			lm_connection_unregister_reply_handler (lconnection, handler);
#endif
	}

	g_slist_free (reply_handlers);
	reply_handlers = NULL;

	return;
}

static guint disco_hh (const gchar *htype, hk_arg_t *args, gpointer ignore)
{
	disco_unregister_handlers ();
	return HOOK_HANDLER_RESULT_ALLOW_MORE_HANDLERS;
}

void disco_init (void)
{
	// completion
	disco_cid = compl_new_category ();
	if (disco_cid) {
		compl_add_category_word (disco_cid, "info");
		compl_add_category_word (disco_cid, "items");
	}

	// hook handler
	disco_hid = hk_add_handler (disco_hh, HOOK_PRE_DISCONNECT, G_PRIORITY_DEFAULT, NULL);

	// command
#ifndef MCABBER_API_HAVE_CMD_ID
	cmd_add ("disco", "", disco_cid, COMPL_JID, do_disco, NULL);
#else
	disco_cmid = cmd_add ("disco", "", disco_cid, COMPL_JID, do_disco, NULL);
#endif

	return;
}

void disco_uninit (void)
{
	// command
#ifndef MCABBER_API_HAVE_CMD_ID
	cmd_del ("disco");
#else
	if (disco_cmid)
		cmd_del (disco_cmid);
#endif

	// completion
	if (disco_cid)
		compl_del_category (disco_cid);

	// hook handler
	hk_del_handler (HOOK_PRE_DISCONNECT, disco_hid);

	// unregister handlers
	disco_unregister_handlers ();

	return;
}

/* vim: se ts=4 sw=4: */