--- a/mcabber/mcabber/caps.c Sun Jul 24 13:30:47 2011 +0200
+++ b/mcabber/mcabber/caps.c Mon Oct 03 16:00:34 2011 +0200
@@ -20,12 +20,28 @@
*/
#include <glib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include "settings.h"
+#include "utils.h"
typedef struct {
char *category;
+ char *type;
char *name;
- char *type;
+} identity;
+
+typedef struct {
+ GHashTable *fields;
+} dataform;
+
+typedef struct {
+ GHashTable *identities;
GHashTable *features;
+ GHashTable *forms;
} caps;
static GHashTable *caps_cache = NULL;
@@ -33,13 +49,34 @@
void caps_destroy(gpointer data)
{
caps *c = data;
- g_free(c->category);
- g_free(c->name);
- g_free(c->type);
+ g_hash_table_destroy(c->identities);
g_hash_table_destroy(c->features);
+ g_hash_table_destroy(c->forms);
g_free(c);
}
+void identity_destroy(gpointer data)
+{
+ identity *i = data;
+ g_free(i->category);
+ g_free(i->type);
+ g_free(i->name);
+ g_free(i);
+}
+
+void form_destroy(gpointer data)
+{
+ dataform *f = data;
+ g_hash_table_destroy(f->fields);
+ g_free(f);
+}
+
+void field_destroy(gpointer data)
+{
+ GList *v = data;
+ g_list_free_full(v, g_free);
+}
+
void caps_init(void)
{
if (!caps_cache)
@@ -55,18 +92,76 @@
}
}
-void caps_add(char *hash)
+void caps_add(const char *hash)
{
if (!hash)
return;
caps *c = g_new0(caps, 1);
c->features = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+ c->identities = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, identity_destroy);
+ c->forms = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, form_destroy);
g_hash_table_insert(caps_cache, g_strdup(hash), c);
}
-int caps_has_hash(const char *hash)
+void caps_remove(const char *hash)
+{
+ if (!hash)
+ return;
+ g_hash_table_remove(caps_cache, hash);
+}
+
+void caps_move_to_local(char *hash, char *bjid)
+{
+ char *orig_hash;
+ caps *c = NULL;
+ if (!hash || !bjid)
+ return;
+ g_hash_table_lookup_extended(caps_cache, hash, (gpointer*)&orig_hash, (gpointer*)&c);
+ if (c) {
+ g_hash_table_steal(caps_cache, hash);
+ g_free(orig_hash);
+ g_hash_table_replace(caps_cache, g_strdup_printf("%s/#%s", bjid, hash), c);
+ // solidus is guaranteed to never appear in bare jid
+ // hash will not appear in base64 encoded hash
+ // sequence "/#" is deterministic separator, and allows to identify local cache entry
+ }
+}
+
+int caps_has_hash(const char *hash, const char *bjid)
{
- return (hash != NULL && (g_hash_table_lookup(caps_cache, hash) != NULL));
+ caps *c = NULL;
+ if (!hash)
+ return 0;
+ c = g_hash_table_lookup(caps_cache, hash);
+ if (!c && bjid) {
+ char *key = g_strdup_printf("%s/#%s", bjid, hash);
+ c = g_hash_table_lookup(caps_cache, key);
+ g_free(key);
+ }
+ return (c != NULL);
+}
+
+void caps_add_identity(const char *hash,
+ const char *category,
+ const char *name,
+ const char *type,
+ const char *lang)
+{
+ caps *c;
+ if (!hash || !category || !type)
+ return;
+ if (!lang)
+ lang = "";
+
+ c = g_hash_table_lookup(caps_cache, hash);
+ if (c) {
+ identity *i = g_new0(identity, 1);
+
+ i->category = g_strdup(category);
+ i->name = g_strdup(name);
+ i->type = g_strdup(type);
+ g_hash_table_replace(c->identities, g_strdup(lang), i);
+ }
}
void caps_set_identity(char *hash,
@@ -74,19 +169,56 @@
const char *name,
const char *type)
{
+ caps_add_identity(hash, category, name, type, NULL);
+}
+
+void caps_add_dataform(const char *hash, const char *formtype)
+{
caps *c;
- if (!hash || !category || !type)
+ if (!formtype)
return;
-
c = g_hash_table_lookup(caps_cache, hash);
if (c) {
- c->category = g_strdup(category);
- c->name = g_strdup(name);
- c->type = g_strdup(type);
+ dataform *d = g_new0(dataform, 1);
+ char *f = g_strdup(formtype);
+
+ d->fields = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, field_destroy);
+ g_hash_table_replace(c->forms, f, d);
}
}
-void caps_add_feature(char *hash, const char *feature)
+gint _strcmp_sort(gconstpointer a, gconstpointer b)
+{
+ return g_strcmp0(a, b);
+}
+
+void caps_add_dataform_field(const char *hash, const char *formtype,
+ const char *field, const char *value)
+{
+ caps *c;
+ if (!formtype || !field || !value)
+ return;
+ c = g_hash_table_lookup(caps_cache, hash);
+ if (c) {
+ dataform *d;
+ d = g_hash_table_lookup(c->forms, formtype);
+ if (d) {
+ gpointer key, val;
+ char *f;
+ GList *v = NULL;
+ if (g_hash_table_lookup_extended(d->fields, field, &key, &val)) {
+ g_hash_table_steal(d->fields, field);
+ g_free(key);
+ v = val;
+ }
+ f = g_strdup(field);
+ v = g_list_insert_sorted(v, g_strdup(value), _strcmp_sort);
+ g_hash_table_replace(d->fields, f, v);
+ }
+ }
+}
+
+void caps_add_feature(const char *hash, const char *feature)
{
caps *c;
if (!hash || !feature)
@@ -98,12 +230,17 @@
}
}
-int caps_has_feature(char *hash, char *feature)
+int caps_has_feature(const char *hash, char *feature, char *bjid)
{
- caps *c;
+ caps *c = NULL;
if (!hash || !feature)
return 0;
c = g_hash_table_lookup(caps_cache, hash);
+ if (!c && bjid) {
+ char *key = g_strdup_printf("%s/#%s", bjid, hash);
+ c = g_hash_table_lookup(caps_cache, key);
+ g_free(key);
+ }
if (c)
return (g_hash_table_lookup(c->features, feature) != NULL);
return 0;
@@ -129,16 +266,10 @@
g_hash_table_foreach(c->features, _caps_foreach_helper, user_data);
}
-gint _strcmp_sort(gconstpointer a, gconstpointer b)
-{
- return g_strcmp0(a, b);
-}
-
// Generates the sha1 hash for the special capability "" and returns it
const char *caps_generate(void)
{
- char *identity;
- GList *features;
+ GList *features, *langs;
GChecksum *sha1;
guint8 digest[20];
gsize digest_size = 20;
@@ -148,10 +279,22 @@
g_hash_table_steal(caps_cache, "");
sha1 = g_checksum_new(G_CHECKSUM_SHA1);
- identity = g_strdup_printf("%s/%s//%s<", c->category, c->type,
- c->name ? c->name : "");
- g_checksum_update(sha1, (guchar*)identity, -1);
- g_free(identity);
+
+ langs = g_hash_table_get_keys(c->identities);
+ langs = g_list_sort(langs, _strcmp_sort);
+ {
+ identity *i;
+ GList *lang;
+ char *identity_S;
+ for (lang=langs; lang; lang=lang->next) {
+ i = g_hash_table_lookup(c->identities, lang->data);
+ identity_S = g_strdup_printf("%s/%s/%s/%s<", i->category, i->type,
+ (char *)lang->data, i->name ? i->name : "");
+ g_checksum_update(sha1, (guchar *)identity_S, -1);
+ g_free(identity_S);
+ }
+ }
+ g_list_free(langs);
features = g_hash_table_get_values(c->features);
features = g_list_sort(features, _strcmp_sort);
@@ -176,4 +319,301 @@
return hash;
}
+gboolean caps_verify(const char *hash, char *function)
+{
+ GList *features, *langs, *forms;
+ GChecksum *checksum;
+ guint8 digest[20];
+ gsize digest_size = 20;
+ gchar *local_hash;
+ gboolean match = FALSE;
+ caps *c = g_hash_table_lookup(caps_cache, hash);
+
+ if (!g_strcmp0(function, "sha-1")) {
+ checksum = g_checksum_new(G_CHECKSUM_SHA1);
+ } else if (!g_strcmp0(function, "md5")) {
+ checksum = g_checksum_new(G_CHECKSUM_MD5);
+ digest_size = 16;
+ } else
+ return FALSE;
+
+ langs = g_hash_table_get_keys(c->identities);
+ langs = g_list_sort(langs, _strcmp_sort);
+ {
+ identity *i;
+ GList *lang;
+ char *identity_S;
+ for (lang=langs; lang; lang=lang->next) {
+ i = g_hash_table_lookup(c->identities, lang->data);
+ identity_S = g_strdup_printf("%s/%s/%s/%s<", i->category, i->type,
+ (char *)lang->data, i->name ? i->name : "");
+ g_checksum_update(checksum, (guchar *)identity_S, -1);
+ g_free(identity_S);
+ }
+ }
+ g_list_free(langs);
+
+ features = g_hash_table_get_values(c->features);
+ features = g_list_sort(features, _strcmp_sort);
+ {
+ GList *feature;
+ for (feature=features; feature; feature=feature->next) {
+ g_checksum_update(checksum, feature->data, -1);
+ g_checksum_update(checksum, (guchar *)"<", -1);
+ }
+ }
+ g_list_free(features);
+
+ forms = g_hash_table_get_keys(c->forms);
+ forms = g_list_sort(forms, _strcmp_sort);
+ {
+ dataform *d;
+ GList *form, *fields;
+ for (form=forms; form; form=form->next) {
+ d = g_hash_table_lookup(c->forms, form->data);
+ g_checksum_update(checksum, form->data, -1);
+ g_checksum_update(checksum, (guchar *)"<", -1);
+ fields = g_hash_table_get_keys(d->fields);
+ fields = g_list_sort(fields, _strcmp_sort);
+ {
+ GList *field;
+ GList *values;
+ for (field=fields; field; field=field->next) {
+ g_checksum_update(checksum, field->data, -1);
+ g_checksum_update(checksum, (guchar *)"<", -1);
+ values = g_hash_table_lookup(d->fields, field->data);
+ {
+ GList *value;
+ for (value=values; value; value=value->next) {
+ g_checksum_update(checksum, value->data, -1);
+ g_checksum_update(checksum, (guchar *)"<", -1);
+ }
+ }
+ }
+ }
+ g_list_free(fields);
+ }
+ }
+ g_list_free(forms);
+
+ g_checksum_get_digest(checksum, digest, &digest_size);
+ local_hash = g_base64_encode(digest, digest_size);
+ g_checksum_free(checksum);
+
+ match = !g_strcmp0(hash, local_hash);
+
+ g_free(local_hash);
+ return match;
+}
+
+static gchar* caps_get_filename(const char* hash)
+{
+ gchar *hash_fs = g_strdup (hash);
+ gchar *dir = (gchar *) settings_opt_get ("caps_directory");
+ gchar *file = NULL;
+
+ if (!dir)
+ goto caps_filename_return;
+
+ {
+ const gchar *valid_fs =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=";
+ g_strcanon(hash_fs, valid_fs, '-');
+ }
+
+ dir = expand_filename (dir);
+ file = g_strdup_printf ("%s/%s.ini", dir, hash_fs);
+ g_free(dir);
+
+caps_filename_return:
+ g_free(hash_fs);
+ return file;
+}
+
+void caps_copy_to_persistent(const char* hash, char* xml)
+{
+ gchar *file;
+ GList *features, *langs, *forms;
+ GKeyFile *key_file;
+ caps *c;
+ int fd;
+
+ g_free (xml);
+
+ c = g_hash_table_lookup (caps_cache, hash);
+ if (!c)
+ goto caps_copy_return;
+
+ file = caps_get_filename (hash);
+ if (!file)
+ goto caps_copy_return;
+
+ fd = open (file, O_WRONLY|O_CREAT|O_EXCL, S_IRUSR|S_IWUSR);
+ if (fd == -1)
+ goto caps_copy_exists;
+
+ key_file = g_key_file_new ();
+
+ langs = g_hash_table_get_keys (c->identities);
+ {
+ identity *i;
+ GList *lang;
+ gchar *group;
+ for (lang=langs; lang; lang=lang->next) {
+ i = g_hash_table_lookup (c->identities, lang->data);
+ group = g_strdup_printf("identity_%s", (gchar *)lang->data);
+ g_key_file_set_string (key_file, group, "category", i->category);
+ g_key_file_set_string (key_file, group, "type", i->type);
+ g_key_file_set_string (key_file, group, "name", i->name);
+ g_free (group);
+ }
+ }
+ g_list_free (langs);
+
+ features = g_hash_table_get_values (c->features);
+ {
+ GList *feature;
+ gchar **string_list;
+ gint i;
+
+ i = g_list_length (features);
+ string_list = g_new (gchar*, i + 1);
+ i = 0;
+ for (feature=features; feature; feature=feature->next) {
+ string_list[i] = g_strdup(feature->data);
+ ++i;
+ }
+ string_list[i] = NULL;
+
+ g_key_file_set_string_list (key_file, "features", "features",
+ (const gchar**)string_list, i);
+ g_strfreev (string_list);
+ }
+ g_list_free (features);
+
+ forms = g_hash_table_get_keys(c->forms);
+ {
+ dataform *d;
+ GList *form, *fields;
+ gchar *group;
+ for (form=forms; form; form=form->next) {
+ d = g_hash_table_lookup (c->forms, form->data);
+ group = g_strdup_printf ("form_%s", (gchar *)form->data);
+ fields = g_hash_table_get_keys(d->fields);
+ {
+ GList *field;
+ GList *values;
+ for (field=fields; field; field=field->next) {
+ values = g_hash_table_lookup (d->fields, field->data);
+ {
+ GList *value;
+ gchar **string_list;
+ gint i;
+ i = g_list_length (values);
+ string_list = g_new (gchar*, i + 1);
+ i = 0;
+ for (value=values; value; value=value->next) {
+ string_list[i] = g_strdup(value->data);
+ ++i;
+ }
+ string_list[i] = NULL;
+
+ g_key_file_set_string_list (key_file, group, field->data,
+ (const gchar**)string_list, i);
+
+ g_strfreev (string_list);
+ }
+ }
+ }
+ g_list_free(fields);
+ g_free (group);
+ }
+ }
+ g_list_free (forms);
+
+ {
+ gchar *data;
+ gsize length;
+ data = g_key_file_to_data (key_file, &length, NULL);
+ write (fd, data, length);
+ g_free(data);
+ close (fd);
+ }
+
+ g_key_file_free(key_file);
+caps_copy_exists:
+ g_free(file);
+caps_copy_return:
+ return;
+}
+
+gboolean caps_restore_from_persistent (const char* hash)
+{
+ gchar *file;
+ GKeyFile *key_file;
+ gchar **groups, **group;
+ gboolean restored = FALSE;
+
+ file = caps_get_filename (hash);
+ if (!file)
+ goto caps_restore_no_file;
+
+ key_file = g_key_file_new ();
+ if (!g_key_file_load_from_file (key_file, file, G_KEY_FILE_NONE, NULL))
+ goto caps_restore_bad_file;
+
+ caps_add(hash);
+
+ groups = g_key_file_get_groups (key_file, NULL);
+ for (group = groups; *group; ++group) {
+ if (!g_strcmp0(*group, "features")) {
+ gchar **features, **feature;
+ features = g_key_file_get_string_list (key_file, *group, "features",
+ NULL, NULL);
+ for (feature = features; *feature; ++feature) {
+ caps_add_feature(hash, *feature);
+ }
+
+ g_strfreev (features);
+ } else if (g_str_has_prefix (*group, "identity_")) {
+ gchar *category, *type, *name, *lang;
+
+ category = g_key_file_get_string(key_file, *group, "category", NULL);
+ type = g_key_file_get_string(key_file, *group, "type", NULL);
+ name = g_key_file_get_string(key_file, *group, "name", NULL);
+ lang = *group + 9; /* "identity_" */
+
+ caps_add_identity(hash, category, name, type, lang);
+ g_free(category);
+ g_free(type);
+ g_free(name);
+ } else if (g_str_has_prefix (*group, "form_")) {
+ gchar *formtype;
+ gchar **fields, **field;
+ formtype = *group + 5; /* "form_" */
+ caps_add_dataform (hash, formtype);
+
+ fields = g_key_file_get_keys(key_file, *group, NULL, NULL);
+ for (field = fields; *field; ++field) {
+ gchar **values, **value;
+ values = g_key_file_get_string_list (key_file, *group, *field,
+ NULL, NULL);
+ for (value = values; *value; ++value) {
+ caps_add_dataform_field (hash, formtype, *field, *value);
+ }
+ g_strfreev (values);
+ }
+ g_strfreev (fields);
+ }
+ }
+ g_strfreev(groups);
+ restored = TRUE;
+
+caps_restore_bad_file:
+ g_key_file_free (key_file);
+ g_free (file);
+caps_restore_no_file:
+ return restored;
+}
+
/* vim: set expandtab cindent cinoptions=>2\:2(0 sw=2 ts=2: For Vim users... */
--- a/mcabber/mcabber/xmpp.c Sun Jul 24 13:30:47 2011 +0200
+++ b/mcabber/mcabber/xmpp.c Mon Oct 03 16:00:34 2011 +0200
@@ -318,8 +318,9 @@
#ifdef HAVE_LIBOTR
int otr_msg = 0;
#endif
+ char *barejid;
#if defined HAVE_GPGME || defined XEP0022 || defined XEP0085
- char *rname, *barejid;
+ char *rname;
GSList *sl_buddy;
#endif
#if defined XEP0022 || defined XEP0085
@@ -349,10 +350,10 @@
subtype = LM_MESSAGE_SUB_TYPE_CHAT;
}
+ barejid = jidtodisp(fjid);
#if defined HAVE_GPGME || defined HAVE_LIBOTR || \
defined XEP0022 || defined XEP0085
rname = strchr(fjid, JID_RESOURCE_SEPARATOR);
- barejid = jidtodisp(fjid);
sl_buddy = roster_find(barejid, jidsearch, ROSTER_TYPE_USER);
// If we can get a resource name, we use it. Else we use NULL,
@@ -406,7 +407,6 @@
}
#endif // HAVE_GPGME
- g_free(barejid);
#endif // HAVE_GPGME || defined XEP0022 || defined XEP0085
x = lm_message_new_with_sub_type(fjid, LM_MESSAGE_TYPE_MESSAGE, subtype);
@@ -428,12 +428,13 @@
// XEP-0184: Message Receipts
if (sl_buddy && xep184 &&
caps_has_feature(buddy_resource_getcaps(sl_buddy->data, rname),
- NS_RECEIPTS)) {
+ NS_RECEIPTS, barejid)) {
lm_message_node_set_attribute
(lm_message_node_add_child(x->node, "request", NULL),
"xmlns", NS_RECEIPTS);
*xep184 = lm_message_handler_new(cb_xep184, NULL, NULL);
}
+ g_free(barejid);
#if defined XEP0022 || defined XEP0085
// If typing notifications are disabled, we can skip all this stuff...
@@ -1326,28 +1327,99 @@
LmMessage *m, gpointer user_data)
{
char *ver = user_data;
+ char *hash;
+ const char *from = lm_message_get_from(m);
+ char *bjid = jidtodisp(from);
LmMessageSubType mstype = lm_message_get_sub_type(m);
- caps_add(ver);
- if (mstype == LM_MESSAGE_SUB_TYPE_ERROR) {
- display_server_error(lm_message_node_get_child(m->node, "error"),
- lm_message_get_from(m));
- } else if (mstype == LM_MESSAGE_SUB_TYPE_RESULT) {
+ hash = strchr(ver, ',');
+ if (hash)
+ *hash++ = '\0';
+
+ if (mstype == LM_MESSAGE_SUB_TYPE_RESULT) {
LmMessageNode *info;
LmMessageNode *query = lm_message_node_get_child(m->node, "query");
+ if (caps_has_hash(ver, bjid))
+ goto caps_callback_return;
+
+ caps_add(ver);
+
info = lm_message_node_get_child(query, "identity");
- if (info)
- caps_set_identity(ver, lm_message_node_get_attribute(info, "category"),
- lm_message_node_get_attribute(info, "name"),
- lm_message_node_get_attribute(info, "type"));
+ while (info) {
+ if (!g_strcmp0(info->name, "identity"))
+ caps_add_identity(ver, lm_message_node_get_attribute(info, "category"),
+ lm_message_node_get_attribute(info, "name"),
+ lm_message_node_get_attribute(info, "type"),
+ lm_message_node_get_attribute(info, "xml:lang"));
+ info = info->next;
+ }
+
info = lm_message_node_get_child(query, "feature");
while (info) {
if (!g_strcmp0(info->name, "feature"))
caps_add_feature(ver, lm_message_node_get_attribute(info, "var"));
info = info->next;
}
+
+ info = lm_message_node_get_child(query, "x");
+ {
+ LmMessageNode *field;
+ LmMessageNode *value;
+ const char *formtype, *var;
+ while (info) {
+ if (!g_strcmp0(info->name, "x")
+ && !g_strcmp0(lm_message_node_get_attribute(info, "type"),
+ "result")
+ && !g_strcmp0(lm_message_node_get_attribute(info, "xmlns"),
+ "jabber:x:data")) {
+ field = lm_message_node_get_child(info, "field");
+ formtype = NULL;
+ while (field) {
+ if (!g_strcmp0(field->name, "field")
+ && !g_strcmp0(lm_message_node_get_attribute(field, "var"),
+ "FORM_TYPE")
+ && !g_strcmp0(lm_message_node_get_attribute(field, "type"),
+ "hidden")) {
+ value = lm_message_node_get_child(field, "value");
+ if (value)
+ formtype = lm_message_node_get_value(value);
+ }
+ field = field->next;
+ }
+ if (formtype) {
+ caps_add_dataform(ver, formtype);
+ field = lm_message_node_get_child(info, "field");
+ while (field) {
+ var = lm_message_node_get_attribute(field, "var");
+ if (!g_strcmp0(field->name, "field")
+ && (g_strcmp0(var, "FORM_TYPE")
+ || g_strcmp0(lm_message_node_get_attribute(field, "type"),
+ "hidden"))) {
+ value = lm_message_node_get_child(field, "value");
+ while (value) {
+ if (!g_strcmp0(value->name, "value"))
+ caps_add_dataform_field(ver, formtype, var,
+ lm_message_node_get_value(value));
+ value = value->next;
+ }
+ }
+ field = field->next;
+ }
+ }
+ }
+ info = info->next;
+ }
+ }
+
+ if (caps_verify(ver, hash))
+ caps_copy_to_persistent(ver, lm_message_node_to_string(query));
+ else
+ caps_move_to_local(ver, bjid);
}
+
+caps_callback_return:
+ g_free(bjid);
g_free(ver);
return LM_HANDLER_RESULT_REMOVE_MESSAGE;
}
@@ -1462,12 +1534,15 @@
caps = lm_message_node_find_xmlns(m->node, NS_CAPS);
if (caps && ust != offline) {
const char *ver = lm_message_node_get_attribute(caps, "ver");
+ const char *hash = lm_message_node_get_attribute(caps, "hash");
GSList *sl_buddy = NULL;
- if (!ver) {
- scr_LogPrint(LPRINT_LOGNORM, "Error: malformed caps version (%s)", bjid);
+ if (!hash) {
+ // No support for legacy format
goto handle_presence_return;
}
+ if (!ver || !g_strcmp0(ver, "") || !g_strcmp0(hash, ""))
+ goto handle_presence_return;
if (rname)
sl_buddy = roster_find(bjid, jidsearch, ROSTER_TYPE_USER);
@@ -1475,7 +1550,7 @@
if (sl_buddy && buddy_getonserverflag(sl_buddy->data)) {
buddy_resource_setcaps(sl_buddy->data, rname, ver);
- if (!caps_has_hash(ver)) {
+ if (!caps_has_hash(ver, bjid) && !caps_restore_from_persistent(ver)) {
char *node;
LmMessageHandler *handler;
LmMessage *iq = lm_message_new_with_sub_type(from, LM_MESSAGE_TYPE_IQ,
@@ -1489,7 +1564,9 @@
"node", node,
NULL);
g_free(node);
- handler = lm_message_handler_new(cb_caps, g_strdup(ver), NULL);
+ handler = lm_message_handler_new(cb_caps,
+ g_strdup_printf("%s,%s",ver,hash),
+ NULL);
lm_connection_send_with_reply(connection, iq, handler, NULL);
lm_message_unref(iq);
lm_message_handler_unref(handler);