about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--Makefile.am2
-rw-r--r--src/capabilities.c230
-rw-r--r--src/capabilities.h41
-rw-r--r--src/command.c9
-rw-r--r--src/common.c26
-rw-r--r--src/common.h1
-rw-r--r--src/contact.c91
-rw-r--r--src/contact.h5
-rw-r--r--src/contact_list.c20
-rw-r--r--src/contact_list.h2
-rw-r--r--src/jabber.c258
-rw-r--r--src/jabber.h2
-rw-r--r--src/muc.c6
-rw-r--r--src/muc.h3
-rw-r--r--src/profanity.c22
-rw-r--r--src/profanity.h7
-rw-r--r--src/stanza.c195
-rw-r--r--src/stanza.h32
-rw-r--r--src/ui.h4
-rw-r--r--src/windows.c226
-rw-r--r--tests/test_contact_list.c8
21 files changed, 1036 insertions, 154 deletions
diff --git a/Makefile.am b/Makefile.am
index 2a0e1e37..f3959f5f 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -11,7 +11,7 @@ profanity_SOURCES = src/command.c src/contact.c src/history.c src/jabber.h \
 	src/muc.h src/stanza.c src/stanza.h src/parser.c src/parser.h \
 	src/theme.c src/theme.h src/window.c src/window.h src/xdg_base.c \
 	src/xdg_base.h src/files.c src/files.h src/accounts.c src/accounts.h \
-	src/jid.h src/jid.c
+	src/jid.h src/jid.c src/capabilities.h src/capabilities.c
 
 TESTS = tests/testsuite
 check_PROGRAMS = tests/testsuite
diff --git a/src/capabilities.c b/src/capabilities.c
new file mode 100644
index 00000000..56ca93ea
--- /dev/null
+++ b/src/capabilities.c
@@ -0,0 +1,230 @@
+/*
+ * capabilities.c
+ *
+ * Copyright (C) 2012, 2013 James Booth <boothj5@gmail.com>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity 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 Profanity.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <glib.h>
+#include <openssl/evp.h>
+#include <strophe.h>
+
+#include "config.h"
+#include "common.h"
+#include "capabilities.h"
+#include "stanza.h"
+
+static GHashTable *capabilities;
+
+static void _caps_destroy(Capabilities *caps);
+
+void
+caps_init(void)
+{
+    capabilities = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
+        (GDestroyNotify)_caps_destroy);
+}
+
+void
+caps_add(const char * const caps_str, const char * const client)
+{
+    Capabilities *new_caps = malloc(sizeof(struct capabilities_t));
+
+    if (client != NULL) {
+        new_caps->client = strdup(client);
+    } else {
+        new_caps->client = NULL;
+    }
+
+    g_hash_table_insert(capabilities, strdup(caps_str), new_caps);
+}
+
+gboolean
+caps_contains(const char * const caps_str)
+{
+    return (g_hash_table_lookup(capabilities, caps_str) != NULL);
+}
+
+Capabilities *
+caps_get(const char * const caps_str)
+{
+    return g_hash_table_lookup(capabilities, caps_str);
+}
+
+char *
+caps_get_sha1_str(xmpp_stanza_t *query)
+{
+    GSList *identities = NULL;
+    GSList *features = NULL;
+    GSList *form_names = NULL;
+    GHashTable *forms = g_hash_table_new(g_str_hash, g_str_equal);
+
+    GString *s = g_string_new("");
+
+    xmpp_stanza_t *child = xmpp_stanza_get_children(query);
+    while (child != NULL) {
+        if (g_strcmp0(xmpp_stanza_get_name(child), STANZA_NAME_IDENTITY) == 0) {
+            char *category = xmpp_stanza_get_attribute(child, "category");
+            char *type = xmpp_stanza_get_attribute(child, "type");
+            char *lang = xmpp_stanza_get_attribute(child, "xml:lang");
+            char *name = xmpp_stanza_get_attribute(child, "name");
+
+            GString *identity_str = g_string_new(category);
+            g_string_append(identity_str, "/");
+            if (type != NULL) {
+                g_string_append(identity_str, type);
+            }
+            g_string_append(identity_str, "/");
+            if (lang != NULL) {
+                g_string_append(identity_str, lang);
+            }
+            g_string_append(identity_str, "/");
+            if (name != NULL) {
+                g_string_append(identity_str, name);
+            }
+            g_string_append(identity_str, "<");
+            identities = g_slist_insert_sorted(identities, identity_str->str, (GCompareFunc)octet_compare);
+        } else if (g_strcmp0(xmpp_stanza_get_name(child), STANZA_NAME_FEATURE) == 0) {
+            char *feature_str = xmpp_stanza_get_attribute(child, "var");
+            features = g_slist_insert_sorted(features, feature_str, (GCompareFunc)octet_compare);
+        } else if (g_strcmp0(xmpp_stanza_get_name(child), STANZA_NAME_X) == 0) {
+            if (strcmp(xmpp_stanza_get_ns(child), STANZA_NS_DATA) == 0) {
+                DataForm *form = stanza_get_form(child);
+                form_names = g_slist_insert_sorted(form_names, form->form_type, (GCompareFunc)octet_compare);
+                g_hash_table_insert(forms, form->form_type, form);
+            }
+        }
+        child = xmpp_stanza_get_next(child);
+    }
+
+    GSList *curr = identities;
+    while (curr != NULL) {
+        g_string_append(s, curr->data);
+        curr = g_slist_next(curr);
+    }
+
+    curr = features;
+    while (curr != NULL) {
+        g_string_append(s, curr->data);
+        g_string_append(s, "<");
+        curr = g_slist_next(curr);
+    }
+
+    curr = form_names;
+    while (curr != NULL) {
+        DataForm *form = g_hash_table_lookup(forms, curr->data);
+        g_string_append(s, form->form_type);
+        g_string_append(s, "<");
+
+        GSList *curr_field = form->fields;
+        while (curr_field != NULL) {
+            FormField *field = curr_field->data;
+            g_string_append(s, field->var);
+            GSList *curr_value = field->values;
+            while (curr_value != NULL) {
+                g_string_append(s, curr_value->data);
+                g_string_append(s, "<");
+                curr_value = g_slist_next(curr_value);
+            }
+            curr_field = g_slist_next(curr_value);
+        }
+    }
+
+    EVP_MD_CTX mdctx;
+    const EVP_MD *md;
+
+    unsigned char md_value[EVP_MAX_MD_SIZE];
+    unsigned int md_len;
+    OpenSSL_add_all_digests();
+    md = EVP_get_digestbyname("SHA1");
+    EVP_MD_CTX_init(&mdctx);
+    EVP_DigestInit_ex(&mdctx, md, NULL);
+    EVP_DigestUpdate(&mdctx, s->str, strlen(s->str));
+    EVP_DigestFinal_ex(&mdctx, md_value, &md_len);
+    EVP_MD_CTX_cleanup(&mdctx);
+
+    char *result = g_base64_encode(md_value, md_len);
+
+    g_string_free(s, TRUE);
+    g_slist_free(identities);
+    g_slist_free(features);
+
+    return result;
+}
+
+xmpp_stanza_t *
+caps_get_query_response_stanza(xmpp_ctx_t *ctx)
+{
+    xmpp_stanza_t *query = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(query, STANZA_NAME_QUERY);
+    xmpp_stanza_set_ns(query, XMPP_NS_DISCO_INFO);
+
+    xmpp_stanza_t *identity = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(identity, "identity");
+    xmpp_stanza_set_attribute(identity, "category", "client");
+    xmpp_stanza_set_attribute(identity, "type", "pc");
+
+    GString *name_str = g_string_new("Profanity ");
+    g_string_append(name_str, PACKAGE_VERSION);
+    if (strcmp(PACKAGE_STATUS, "development") == 0) {
+        g_string_append(name_str, "dev");
+    }
+    xmpp_stanza_set_attribute(identity, "name", name_str->str);
+
+    xmpp_stanza_t *feature_caps = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(feature_caps, STANZA_NAME_FEATURE);
+    xmpp_stanza_set_attribute(feature_caps, STANZA_ATTR_VAR, STANZA_NS_CAPS);
+
+    xmpp_stanza_t *feature_discoinfo = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(feature_discoinfo, STANZA_NAME_FEATURE);
+    xmpp_stanza_set_attribute(feature_discoinfo, STANZA_ATTR_VAR, XMPP_NS_DISCO_INFO);
+
+    xmpp_stanza_t *feature_muc = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(feature_muc, STANZA_NAME_FEATURE);
+    xmpp_stanza_set_attribute(feature_muc, STANZA_ATTR_VAR, STANZA_NS_MUC);
+
+    xmpp_stanza_t *feature_version = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(feature_version, STANZA_NAME_FEATURE);
+    xmpp_stanza_set_attribute(feature_version, STANZA_ATTR_VAR, STANZA_NS_VERSION);
+
+    xmpp_stanza_add_child(query, identity);
+    xmpp_stanza_add_child(query, feature_muc);
+    xmpp_stanza_add_child(query, feature_discoinfo);
+    xmpp_stanza_add_child(query, feature_caps);
+    xmpp_stanza_add_child(query, feature_version);
+
+    return query;
+}
+
+void
+caps_close(void)
+{
+    g_hash_table_destroy(capabilities);
+}
+
+static void
+_caps_destroy(Capabilities *caps)
+{
+    if (caps != NULL) {
+        FREE_SET_NULL(caps->client);
+        FREE_SET_NULL(caps);
+    }
+}
diff --git a/src/capabilities.h b/src/capabilities.h
new file mode 100644
index 00000000..ad160826
--- /dev/null
+++ b/src/capabilities.h
@@ -0,0 +1,41 @@
+/*
+ * capabilities.h
+ *
+ * Copyright (C) 2012, 2013 James Booth <boothj5@gmail.com>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity 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 Profanity.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef CAPABILITIES_H
+#define CAPABILITIES_H
+
+#include <glib.h>
+#include <strophe.h>
+
+typedef struct capabilities_t {
+    char *client;
+} Capabilities;
+
+void caps_init(void);
+void caps_add(const char * const caps_str, const char * const client);
+gboolean caps_contains(const char * const caps_str);
+Capabilities* caps_get(const char * const caps_str);
+char* caps_get_sha1_str(xmpp_stanza_t * const query);
+xmpp_stanza_t* caps_get_query_response_stanza(xmpp_ctx_t *ctx);
+void caps_close(void);
+
+#endif
diff --git a/src/command.c b/src/command.c
index 4e79b00d..229d0145 100644
--- a/src/command.c
+++ b/src/command.c
@@ -1518,6 +1518,7 @@ _cmd_who(gchar **args, struct cmd_help_t help)
 
             // not in groupchat window
             } else {
+                cons_show("");
                 GSList *list = get_contact_list();
 
                 // no arg, show all contacts
@@ -1673,7 +1674,7 @@ _cmd_info(gchar **args, struct cmd_help_t help)
     } else {
         if (win_current_is_groupchat()) {
             if (usr != NULL) {
-                win_room_show_status(usr);
+                win_room_show_info(usr);
             } else {
                 win_current_show("You must specify a nickname.");
             }
@@ -1681,17 +1682,17 @@ _cmd_info(gchar **args, struct cmd_help_t help)
             if (usr != NULL) {
                 win_current_show("No parameter required when in chat.");
             } else {
-                win_show_status();
+                win_show_info();
             }
         } else if (win_current_is_private()) {
             if (usr != NULL) {
                 win_current_show("No parameter required when in chat.");
             } else {
-                win_private_show_status();
+                win_private_show_info();
             }
         } else {
             if (usr != NULL) {
-                cons_show_status(usr);
+                cons_show_info(usr);
             } else {
                 cons_show("Usage: %s", help.usage);
             }
diff --git a/src/common.c b/src/common.c
index d79bff64..f0e14f45 100644
--- a/src/common.c
+++ b/src/common.c
@@ -167,3 +167,29 @@ prof_getline(FILE *stream)
     free(buf);
     return s;
 }
+
+int
+octet_compare(unsigned char *str1, unsigned char *str2)
+{
+    if ((strcmp((char *)str1, "") == 0) && (strcmp((char *)str2, "") == 0)) {
+        return 0;
+    }
+
+    if ((strcmp((char *)str1, "") == 0) && (strcmp((char *)str2, "") != 0)) {
+        return -1;
+    }
+
+    if ((strcmp((char *)str1, "") != 0) && (strcmp((char *)str2, "") == 0)) {
+        return 1;
+    }
+
+    if (str1[0] == str2[0]) {
+        return octet_compare(&str1[1], &str2[1]);
+    }
+
+    if (str1[0] < str2[0]) {
+        return -1;
+    }
+
+    return 1;
+}
diff --git a/src/common.h b/src/common.h
index e12045ad..5c615256 100644
--- a/src/common.h
+++ b/src/common.h
@@ -51,5 +51,6 @@ char * str_replace(const char *string, const char *substr,
 int str_contains(char str[], int size, char ch);
 char* encode_xml(const char * const xml);
 char * prof_getline(FILE *stream);
+int octet_compare(unsigned char *str1, unsigned char *str2);
 
 #endif
diff --git a/src/contact.c b/src/contact.c
index ef4b807d..aad7f630 100644
--- a/src/contact.c
+++ b/src/contact.c
@@ -25,6 +25,7 @@
 
 #include <glib.h>
 
+#include "common.h"
 #include "contact.h"
 
 struct p_contact_t {
@@ -33,6 +34,7 @@ struct p_contact_t {
     char *presence;
     char *status;
     char *subscription;
+    char *caps_str;
     gboolean pending_out;
     GDateTime *last_activity;
 };
@@ -40,7 +42,8 @@ struct p_contact_t {
 PContact
 p_contact_new(const char * const jid, const char * const name,
     const char * const presence, const char * const status,
-    const char * const subscription, gboolean pending_out)
+    const char * const subscription, gboolean pending_out,
+    const char * const caps_str)
 {
     PContact contact = malloc(sizeof(struct p_contact_t));
     contact->jid = strdup(jid);
@@ -64,10 +67,14 @@ p_contact_new(const char * const jid, const char * const name,
     if (subscription != NULL)
         contact->subscription = strdup(subscription);
     else
-        contact->subscription = strdup("none");;
+        contact->subscription = strdup("none");
 
-    contact->pending_out = pending_out;
+    if (caps_str != NULL)
+        contact->caps_str = strdup(caps_str);
+    else
+        contact->caps_str = NULL;
 
+    contact->pending_out = pending_out;
     contact->last_activity = NULL;
 
     return contact;
@@ -76,37 +83,18 @@ p_contact_new(const char * const jid, const char * const name,
 void
 p_contact_free(PContact contact)
 {
-    if (contact->jid != NULL) {
-        free(contact->jid);
-        contact->jid = NULL;
-    }
-
-    if (contact->name != NULL) {
-        free(contact->name);
-        contact->name = NULL;
-    }
-
-    if (contact->presence != NULL) {
-        free(contact->presence);
-        contact->presence = NULL;
-    }
-
-    if (contact->status != NULL) {
-        free(contact->status);
-        contact->status = NULL;
-    }
-
-    if (contact->subscription != NULL) {
-        free(contact->subscription);
-        contact->subscription = NULL;
-    }
+    FREE_SET_NULL(contact->jid);
+    FREE_SET_NULL(contact->name);
+    FREE_SET_NULL(contact->presence);
+    FREE_SET_NULL(contact->status);
+    FREE_SET_NULL(contact->subscription);
+    FREE_SET_NULL(contact->caps_str);
 
     if (contact->last_activity != NULL) {
         g_date_time_unref(contact->last_activity);
     }
 
-    free(contact);
-    contact = NULL;
+    FREE_SET_NULL(contact);
 }
 
 const char *
@@ -151,17 +139,17 @@ p_contact_last_activity(const PContact contact)
     return contact->last_activity;
 }
 
+const char *
+p_contact_caps_str(const PContact contact)
+{
+    return contact->caps_str;
+}
+
 void
 p_contact_set_presence(const PContact contact, const char * const presence)
 {
-    if (contact->presence != NULL) {
-        free(contact->presence);
-        contact->presence = NULL;
-    }
-
-    if (presence == NULL) {
-        contact->presence = NULL;
-    } else {
+    FREE_SET_NULL(contact->presence);
+    if (presence != NULL) {
         contact->presence = strdup(presence);
     }
 }
@@ -169,14 +157,8 @@ p_contact_set_presence(const PContact contact, const char * const presence)
 void
 p_contact_set_status(const PContact contact, const char * const status)
 {
-    if (contact->status != NULL) {
-        free(contact->status);
-        contact->status = NULL;
-    }
-
-    if (status == NULL) {
-        contact->status = NULL;
-    } else {
+    FREE_SET_NULL(contact->status);
+    if (status != NULL) {
         contact->status = strdup(status);
     }
 }
@@ -184,14 +166,8 @@ p_contact_set_status(const PContact contact, const char * const status)
 void
 p_contact_set_subscription(const PContact contact, const char * const subscription)
 {
-    if (contact->subscription != NULL) {
-        free(contact->subscription);
-        contact->subscription = NULL;
-    }
-
-    if (subscription == NULL) {
-        contact->subscription = strdup("none");
-    } else {
+    FREE_SET_NULL(contact->subscription);
+    if (subscription != NULL) {
         contact->subscription = strdup(subscription);
     }
 }
@@ -214,3 +190,12 @@ p_contact_set_last_activity(const PContact contact, GDateTime *last_activity)
         contact->last_activity = g_date_time_ref(last_activity);
     }
 }
+
+void
+p_contact_set_caps_str(const PContact contact, const char * const caps_str)
+{
+    FREE_SET_NULL(contact->caps_str);
+    if (caps_str != NULL) {
+        contact->caps_str = strdup(caps_str);
+    }
+}
diff --git a/src/contact.h b/src/contact.h
index a2842976..0e9ec42e 100644
--- a/src/contact.h
+++ b/src/contact.h
@@ -27,18 +27,21 @@ typedef struct p_contact_t *PContact;
 
 PContact p_contact_new(const char * const jid, const char * const name,
     const char * const presence, const char * const status,
-    const char * const subscription, gboolean pending_out);
+    const char * const subscription, gboolean pending_out,
+    const char * const caps_str);
 void p_contact_free(PContact contact);
 const char* p_contact_jid(PContact contact);
 const char* p_contact_name(PContact contact);
 const char* p_contact_presence(PContact contact);
 const char* p_contact_status(PContact contact);
 const char* p_contact_subscription(const PContact contact);
+const char* p_contact_caps_str(const PContact contact);
 GDateTime* p_contact_last_activity(const PContact contact);
 gboolean p_contact_pending_out(const PContact contact);
 void p_contact_set_presence(const PContact contact, const char * const presence);
 void p_contact_set_status(const PContact contact, const char * const status);
 void p_contact_set_subscription(const PContact contact, const char * const subscription);
+void p_contact_set_caps_str(const PContact contact, const char * const caps_str);
 void p_contact_set_pending_out(const PContact contact, gboolean pending_out);
 void p_contact_set_last_activity(const PContact contact, GDateTime *last_activity);
 
diff --git a/src/contact_list.c b/src/contact_list.c
index 2c9bc220..50a63475 100644
--- a/src/contact_list.c
+++ b/src/contact_list.c
@@ -70,7 +70,7 @@ contact_list_add(const char * const jid, const char * const name,
 
     if (contact == NULL) {
         contact = p_contact_new(jid, name, presence, status, subscription,
-            pending_out);
+            pending_out, NULL);
         g_hash_table_insert(contacts, strdup(jid), contact);
         p_autocomplete_add(ac, strdup(jid));
         added = TRUE;
@@ -87,9 +87,9 @@ contact_list_remove(const char * const jid)
 
 gboolean
 contact_list_update_contact(const char * const jid, const char * const presence,
-    const char * const status, GDateTime *last_activity)
+    const char * const status, GDateTime *last_activity, const char * const caps_str)
 {
-    gboolean changed = FALSE;
+    gboolean presence_changed = FALSE;
     PContact contact = g_hash_table_lookup(contacts, jid);
 
     if (contact == NULL) {
@@ -98,20 +98,24 @@ contact_list_update_contact(const char * const jid, const char * const presence,
 
     if (g_strcmp0(p_contact_presence(contact), presence) != 0) {
         p_contact_set_presence(contact, presence);
-        changed = TRUE;
+        presence_changed = TRUE;
     }
 
     if (g_strcmp0(p_contact_status(contact), status) != 0) {
         p_contact_set_status(contact, status);
-        changed = TRUE;
+        presence_changed = TRUE;
     }
 
     if (!_datetimes_equal(p_contact_last_activity(contact), last_activity)) {
         p_contact_set_last_activity(contact, last_activity);
-        changed = TRUE;
+        presence_changed = TRUE;
     }
 
-    return changed;
+    if (g_strcmp0(p_contact_caps_str(contact), caps_str) != 0) {
+        p_contact_set_caps_str(contact, caps_str);
+    }
+
+    return presence_changed;
 }
 
 void
@@ -122,7 +126,7 @@ contact_list_update_subscription(const char * const jid,
 
     if (contact == NULL) {
         contact = p_contact_new(jid, NULL, "offline", NULL, subscription,
-            pending_out);
+            pending_out, NULL);
         g_hash_table_insert(contacts, strdup(jid), contact);
     } else {
         p_contact_set_subscription(contact, subscription);
diff --git a/src/contact_list.h b/src/contact_list.h
index da95aeac..43350631 100644
--- a/src/contact_list.h
+++ b/src/contact_list.h
@@ -36,7 +36,7 @@ gboolean contact_list_add(const char * const jid, const char * const name,
     const char * const presence, const char * const status,
     const char * const subscription, gboolean pending_out);
 gboolean contact_list_update_contact(const char * const jid, const char * const presence,
-    const char * const status, GDateTime *last_activity);
+    const char * const status, GDateTime *last_activity, const char * const caps_str);
 void contact_list_update_subscription(const char * const jid,
     const char * const subscription, gboolean pending_out);
 gboolean contact_list_has_pending_subscriptions(void);
diff --git a/src/jabber.c b/src/jabber.c
index 2d69ebec..244921bf 100644
--- a/src/jabber.c
+++ b/src/jabber.c
@@ -26,6 +26,7 @@
 
 #include <strophe.h>
 
+#include "capabilities.h"
 #include "chat_session.h"
 #include "common.h"
 #include "contact_list.h"
@@ -84,9 +85,16 @@ static int _iq_handler(xmpp_conn_t * const conn,
     xmpp_stanza_t * const stanza, void * const userdata);
 static int _roster_handler(xmpp_conn_t * const conn,
     xmpp_stanza_t * const stanza, void * const userdata);
+static int _disco_response_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
+    void * const userdata);
+static int _disco_request_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
+    void * const userdata);
+static int _version_request_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
+    void * const userdata);
 static int _presence_handler(xmpp_conn_t * const conn,
     xmpp_stanza_t * const stanza, void * const userdata);
 static int _ping_timed_handler(xmpp_conn_t * const conn, void * const userdata);
+static char * _handle_presence_caps(xmpp_stanza_t * const stanza);
 
 void
 jabber_init(const int disable_tls)
@@ -414,6 +422,16 @@ jabber_update_presence(jabber_presence_t status, const char * const msg,
         xmpp_stanza_add_child(presence, query);
     }
 
+    // add caps
+    xmpp_stanza_t *caps = xmpp_stanza_new(jabber_conn.ctx);
+    xmpp_stanza_set_name(caps, STANZA_NAME_C);
+    xmpp_stanza_set_ns(caps, STANZA_NS_CAPS);
+    xmpp_stanza_set_attribute(caps, STANZA_ATTR_HASH, "sha-1");
+    xmpp_stanza_set_attribute(caps, STANZA_ATTR_NODE, "http://www.profanity.im");
+    xmpp_stanza_t *query = caps_get_query_response_stanza(jabber_conn.ctx);
+    char *sha1 = caps_get_sha1_str(query);
+    xmpp_stanza_set_attribute(caps, STANZA_ATTR_VER, sha1);
+    xmpp_stanza_add_child(presence, caps);
     xmpp_send(jabber_conn.conn, presence);
 
     // send presence for each room
@@ -783,11 +801,24 @@ _iq_handler(xmpp_conn_t * const conn,
     xmpp_stanza_t * const stanza, void * const userdata)
 {
     char *id = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_ID);
+    char *type = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TYPE);
 
     // handle the initial roster request
-    if ((id != NULL) && (strcmp(id, "roster") == 0)) {
+    if (g_strcmp0(id, "roster") == 0) {
         return _roster_handler(conn, stanza, userdata);
 
+    // handle disco responses
+    } else if ((id != NULL) && (g_str_has_prefix(id, "disco")) &&
+            (g_strcmp0(type, "result") == 0)) {
+        return _disco_response_handler(conn, stanza, userdata);
+
+    // handle disco requests
+    } else if (stanza_is_caps_request(stanza)) {
+        return _disco_request_handler(conn, stanza, userdata);
+
+    } else if (stanza_is_version_request(stanza)) {
+        return _version_request_handler(conn, stanza, userdata);
+
     // handle iq
     } else {
         char *type = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TYPE);
@@ -877,8 +908,8 @@ _iq_handler(xmpp_conn_t * const conn,
 }
 
 static int
-_roster_handler(xmpp_conn_t * const conn,
-    xmpp_stanza_t * const stanza, void * const userdata)
+_roster_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
+    void * const userdata)
 {
     xmpp_stanza_t *query, *item;
     char *type = xmpp_stanza_get_type(stanza);
@@ -921,6 +952,148 @@ _roster_handler(xmpp_conn_t * const conn,
 }
 
 static int
+_version_request_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
+    void * const userdata)
+{
+    xmpp_ctx_t *ctx = (xmpp_ctx_t *)userdata;
+
+    char *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
+    char *id = xmpp_stanza_get_id(stanza);
+
+    if (from != NULL) {
+        xmpp_stanza_t *response = xmpp_stanza_new(ctx);
+        xmpp_stanza_set_name(response, STANZA_NAME_IQ);
+        if (id != NULL) {
+            xmpp_stanza_set_id(response, id);
+        }
+        xmpp_stanza_set_attribute(response, STANZA_ATTR_TO, from);
+        xmpp_stanza_set_type(response, STANZA_TYPE_RESULT);
+
+        xmpp_stanza_t *query = xmpp_stanza_new(ctx);
+        xmpp_stanza_set_name(query, STANZA_NAME_QUERY);
+        xmpp_stanza_set_ns(query, STANZA_NS_VERSION);
+
+        xmpp_stanza_t *name = xmpp_stanza_new(ctx);
+        xmpp_stanza_set_name(name, "name");
+        xmpp_stanza_t *name_txt = xmpp_stanza_new(ctx);
+        xmpp_stanza_set_text(name_txt, "Profanity");
+        xmpp_stanza_add_child(name, name_txt);
+
+        xmpp_stanza_t *version = xmpp_stanza_new(ctx);
+        xmpp_stanza_set_name(version, "version");
+        xmpp_stanza_t *version_txt = xmpp_stanza_new(ctx);
+        GString *version_str = g_string_new(PACKAGE_VERSION);
+        if (strcmp(PACKAGE_STATUS, "development") == 0) {
+            g_string_append(version_str, "dev");
+        }
+        xmpp_stanza_set_text(version_txt, version_str->str);
+        xmpp_stanza_add_child(version, version_txt);
+
+        xmpp_stanza_add_child(query, name);
+        xmpp_stanza_add_child(query, version);
+        xmpp_stanza_add_child(response, query);
+
+        xmpp_send(conn, response);
+    }
+
+    return 1;
+}
+
+static int
+_disco_request_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
+    void * const userdata)
+{
+    xmpp_ctx_t *ctx = (xmpp_ctx_t *)userdata;
+
+    xmpp_stanza_t *incoming_query = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY);
+    char *node_str = xmpp_stanza_get_attribute(incoming_query, STANZA_ATTR_NODE);
+    char *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
+
+    if (from != NULL && node_str != NULL) {
+        xmpp_stanza_t *response = xmpp_stanza_new(ctx);
+        xmpp_stanza_set_name(response, STANZA_NAME_IQ);
+        xmpp_stanza_set_id(response, xmpp_stanza_get_id(stanza));
+        xmpp_stanza_set_attribute(response, STANZA_ATTR_TO, from);
+        xmpp_stanza_set_type(response, STANZA_TYPE_RESULT);
+        xmpp_stanza_t *query = caps_get_query_response_stanza(ctx);
+        xmpp_stanza_set_attribute(query, STANZA_ATTR_NODE, node_str);
+        xmpp_stanza_add_child(response, query);
+        xmpp_send(conn, response);
+    }
+
+    return 1;
+}
+
+static int
+_disco_response_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza,
+    void * const userdata)
+{
+    char *type = xmpp_stanza_get_type(stanza);
+    char *id = xmpp_stanza_get_id(stanza);
+
+    if (g_strcmp0(type, STANZA_TYPE_ERROR) == 0) {
+        log_error("Roster query failed");
+        return 1;
+    } else {
+        xmpp_stanza_t *query = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY);
+        char *node = xmpp_stanza_get_attribute(query, STANZA_ATTR_NODE);
+        if (node == NULL) {
+            return 1;
+        }
+
+        char *caps_key = NULL;
+
+        // xep-0115
+        if (g_strcmp0(id, "disco") == 0) {
+            caps_key = node;
+
+            // validate sha1
+            gchar **split = g_strsplit(node, "#", -1);
+            char *given_sha1 = split[1];
+            char *generated_sha1 = caps_get_sha1_str(query);
+
+            if (g_strcmp0(given_sha1, generated_sha1) != 0) {
+                log_info("Invalid SHA1 recieved for caps.");
+                return 1;
+            }
+        // non supported hash, or legacy caps
+        } else {
+            caps_key = id + 6;
+        }
+
+        // already cached
+        if (caps_contains(caps_key)) {
+            log_info("Client info already cached.");
+            return 1;
+        }
+
+        xmpp_stanza_t *identity = xmpp_stanza_get_child_by_name(query, "identity");
+
+        if (identity == NULL) {
+            return 1;
+        }
+
+        const char *category = xmpp_stanza_get_attribute(identity, "category");
+        if (category == NULL) {
+            return 1;
+        }
+
+        if (strcmp(category, "client") != 0) {
+            return 1;
+        }
+
+        const char *name = xmpp_stanza_get_attribute(identity, "name");
+        if (name == 0) {
+            return 1;
+        }
+
+        caps_add(caps_key, name);
+
+        return 1;
+    }
+}
+
+static int
 _ping_timed_handler(xmpp_conn_t * const conn, void * const userdata)
 {
     if (jabber_conn.conn_status == JABBER_CONNECTED) {
@@ -974,6 +1147,7 @@ _room_presence_handler(const char * const jid, xmpp_stanza_t * const stanza)
     } else {
         char *type = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TYPE);
         char *show_str, *status_str;
+        char *caps_key = _handle_presence_caps(stanza);
 
         xmpp_stanza_t *status = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_STATUS);
         if (status != NULL) {
@@ -999,18 +1173,18 @@ _room_presence_handler(const char * const jid, xmpp_stanza_t * const stanza)
                 show_str = "online";
             }
             if (!muc_get_roster_received(room)) {
-                muc_add_to_roster(room, nick, show_str, status_str);
+                muc_add_to_roster(room, nick, show_str, status_str, caps_key);
             } else {
                 char *old_nick = muc_complete_roster_nick_change(room, nick);
 
                 if (old_nick != NULL) {
-                    muc_add_to_roster(room, nick, show_str, status_str);
+                    muc_add_to_roster(room, nick, show_str, status_str, caps_key);
                     prof_handle_room_member_nick_change(room, old_nick, nick);
                 } else {
                     if (!muc_nick_in_roster(room, nick)) {
-                        prof_handle_room_member_online(room, nick, show_str, status_str);
+                        prof_handle_room_member_online(room, nick, show_str, status_str, caps_key);
                     } else {
-                        prof_handle_room_member_presence(room, nick, show_str, status_str);
+                        prof_handle_room_member_presence(room, nick, show_str, status_str, caps_key);
                     }
                 }
             }
@@ -1023,6 +1197,72 @@ _room_presence_handler(const char * const jid, xmpp_stanza_t * const stanza)
     return 1;
 }
 
+static char *
+_handle_presence_caps(xmpp_stanza_t * const stanza)
+{
+    char *caps_key = NULL;
+    char *node = NULL;
+    char *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
+    if (stanza_contains_caps(stanza)) {
+        char *hash_type = stanza_caps_get_hash(stanza);
+
+        // xep-0115
+        if (hash_type != NULL) {
+
+            // supported hash
+            if (strcmp(hash_type, "sha-1") == 0) {
+                node = stanza_get_caps_str(stanza);
+                caps_key = node;
+
+                if (node != NULL) {
+                    if (!caps_contains(caps_key)) {
+                        xmpp_stanza_t *iq = stanza_create_disco_iq(jabber_conn.ctx, "disco", from, node);
+                        xmpp_send(jabber_conn.conn, iq);
+                        xmpp_stanza_release(iq);
+                    }
+                }
+
+            // unsupported hash
+            } else {
+                node = stanza_get_caps_str(stanza);
+                caps_key = from;
+
+                if (node != NULL) {
+                    if (!caps_contains(caps_key)) {
+                        GString *id = g_string_new("disco_");
+                        g_string_append(id, from);
+                        xmpp_stanza_t *iq = stanza_create_disco_iq(jabber_conn.ctx, id->str, from, node);
+                        xmpp_send(jabber_conn.conn, iq);
+                        xmpp_stanza_release(iq);
+                        g_string_free(id, TRUE);
+                    }
+                }
+            }
+
+            return strdup(caps_key);
+
+        //ignore or handle legacy caps
+        } else {
+            node = stanza_get_caps_str(stanza);
+            caps_key = from;
+
+            if (node != NULL) {
+                if (!caps_contains(caps_key)) {
+                    GString *id = g_string_new("disco_");
+                    g_string_append(id, from);
+                    xmpp_stanza_t *iq = stanza_create_disco_iq(jabber_conn.ctx, id->str, from, node);
+                    xmpp_send(jabber_conn.conn, iq);
+                    xmpp_stanza_release(iq);
+                    g_string_free(id, TRUE);
+                }
+            }
+
+            return caps_key;
+        }
+    }
+    return NULL;
+}
+
 static int
 _presence_handler(xmpp_conn_t * const conn,
     xmpp_stanza_t * const stanza, void * const userdata)
@@ -1055,6 +1295,8 @@ _presence_handler(xmpp_conn_t * const conn,
             g_date_time_unref(now);
         }
 
+        char *caps_key = _handle_presence_caps(stanza);
+
         xmpp_stanza_t *status = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_STATUS);
         if (status != NULL)
             status_str = xmpp_stanza_get_text(status);
@@ -1069,7 +1311,7 @@ _presence_handler(xmpp_conn_t * const conn,
                 show_str = "online";
 
             if (strcmp(my_jid->barejid, from_jid->barejid) !=0) {
-                prof_handle_contact_online(from_jid->barejid, show_str, status_str, last_activity);
+                prof_handle_contact_online(from_jid->barejid, show_str, status_str, last_activity, caps_key);
             }
         } else if (strcmp(type, STANZA_TYPE_UNAVAILABLE) == 0) {
             if (strcmp(my_jid->barejid, from_jid->barejid) !=0) {
diff --git a/src/jabber.h b/src/jabber.h
index e1494851..02e540fc 100644
--- a/src/jabber.h
+++ b/src/jabber.h
@@ -23,6 +23,8 @@
 #ifndef JABBER_H
 #define JABBER_H
 
+#include <strophe.h>
+
 #include "accounts.h"
 #include "jid.h"
 
diff --git a/src/muc.c b/src/muc.c
index ece77818..efe22abd 100644
--- a/src/muc.c
+++ b/src/muc.c
@@ -204,7 +204,8 @@ muc_nick_in_roster(const char * const room, const char * const nick)
  */
 gboolean
 muc_add_to_roster(const char * const room, const char * const nick,
-    const char * const show, const char * const status)
+    const char * const show, const char * const status,
+    const char * const caps_str)
 {
     ChatRoom *chat_room = g_hash_table_lookup(rooms, room);
     gboolean updated = FALSE;
@@ -219,8 +220,7 @@ muc_add_to_roster(const char * const room, const char * const nick,
                     (g_strcmp0(p_contact_status(old), status) != 0)) {
             updated = TRUE;
         }
-
-        PContact contact = p_contact_new(nick, NULL, show, status, NULL, FALSE);
+        PContact contact = p_contact_new(nick, NULL, show, status, NULL, FALSE, caps_str);
         g_hash_table_replace(chat_room->roster, strdup(nick), contact);
     }
 
diff --git a/src/muc.h b/src/muc.h
index 8411cb39..8b8ed018 100644
--- a/src/muc.h
+++ b/src/muc.h
@@ -39,7 +39,8 @@ void muc_complete_room_nick_change(const char * const room,
     const char * const nick);
 
 gboolean muc_add_to_roster(const char * const room, const char * const nick,
-    const char * const show, const char * const status);
+    const char * const show, const char * const status,
+    const char * const caps_str);
 void muc_remove_from_roster(const char * const room, const char * const nick);
 GList * muc_get_roster(const char * const room);
 PAutocomplete muc_get_roster_ac(const char * const room);
diff --git a/src/profanity.c b/src/profanity.c
index 91d8be78..47707595 100644
--- a/src/profanity.c
+++ b/src/profanity.c
@@ -30,6 +30,7 @@
 #include <glib.h>
 
 #include "accounts.h"
+#include "capabilities.h"
 #include "chat_log.h"
 #include "chat_session.h"
 #include "command.h"
@@ -311,9 +312,9 @@ prof_handle_room_roster_complete(const char * const room)
 void
 prof_handle_room_member_presence(const char * const room,
     const char * const nick, const char * const show,
-    const char * const status)
+    const char * const status, const char * const caps_str)
 {
-    gboolean updated = muc_add_to_roster(room, nick, show, status);
+    gboolean updated = muc_add_to_roster(room, nick, show, status, caps_str);
 
     if (updated) {
         win_show_room_member_presence(room, nick, show, status);
@@ -323,9 +324,10 @@ prof_handle_room_member_presence(const char * const room,
 
 void
 prof_handle_room_member_online(const char * const room, const char * const nick,
-    const char * const show, const char * const status)
+    const char * const show, const char * const status,
+    const char * const caps_str)
 {
-    muc_add_to_roster(room, nick, show, status);
+    muc_add_to_roster(room, nick, show, status, caps_str);
     win_show_room_member_online(room, nick, show, status);
     win_current_page_off();
 }
@@ -347,11 +349,11 @@ prof_handle_leave_room(const char * const room)
 
 void
 prof_handle_contact_online(char *contact, char *show, char *status,
-    GDateTime *last_activity)
+    GDateTime *last_activity, char *caps_str)
 {
-    gboolean updated = contact_list_update_contact(contact, show, status, last_activity);
+    gboolean presence_changed = contact_list_update_contact(contact, show, status, last_activity, caps_str);
 
-    if (updated) {
+    if (presence_changed) {
         PContact result = contact_list_get_contact(contact);
         if (p_contact_subscription(result) != NULL) {
             if (strcmp(p_contact_subscription(result), "none") != 0) {
@@ -365,9 +367,9 @@ prof_handle_contact_online(char *contact, char *show, char *status,
 void
 prof_handle_contact_offline(char *contact, char *show, char *status)
 {
-    gboolean updated = contact_list_update_contact(contact, "offline", status, NULL);
+    gboolean presence_changed = contact_list_update_contact(contact, "offline", status, NULL, NULL);
 
-    if (updated) {
+    if (presence_changed) {
         PContact result = contact_list_get_contact(contact);
         if (p_contact_subscription(result) != NULL) {
             if (strcmp(p_contact_subscription(result), "none") != 0) {
@@ -541,6 +543,7 @@ _init(const int disable_tls, char *log_level)
     theme_init(theme);
     g_free(theme);
     ui_init();
+    caps_init();
     jabber_init(disable_tls);
     cmd_init();
     log_info("Initialising contact list");
@@ -553,6 +556,7 @@ _shutdown(void)
 {
     jabber_disconnect();
     contact_list_free();
+    caps_close();
     ui_close();
     chat_log_close();
     prefs_close();
diff --git a/src/profanity.h b/src/profanity.h
index 69f4adf5..a28bd292 100644
--- a/src/profanity.h
+++ b/src/profanity.h
@@ -34,7 +34,7 @@ void prof_handle_disconnect(const char * const jid);
 void prof_handle_failed_login(void);
 void prof_handle_typing(char *from);
 void prof_handle_contact_online(char *contact, char *show, char *status,
-    GDateTime *last_activity);
+    GDateTime *last_activity, char *caps_str);
 void prof_handle_contact_offline(char *contact, char *show, char *status);
 void prof_handle_incoming_message(char *from, char *message, gboolean priv);
 void prof_handle_delayed_message(char *from, char *message, GTimeVal tv_stamp,
@@ -51,12 +51,13 @@ void prof_handle_room_subject(const char * const room_jid,
     const char * const subject);
 void prof_handle_room_roster_complete(const char * const room);
 void prof_handle_room_member_online(const char * const room,
-    const char * const nick, const char * const show, const char * const status);
+    const char * const nick, const char * const show, const char * const status,
+    const char * const caps_str);
 void prof_handle_room_member_offline(const char * const room,
     const char * const nick, const char * const show, const char * const status);
 void prof_handle_room_member_presence(const char * const room,
     const char * const nick, const char * const show,
-    const char * const status);
+    const char * const status, const char * const caps_str);
 void prof_handle_leave_room(const char * const room);
 void prof_handle_room_member_nick_change(const char * const room,
     const char * const old_nick, const char * const nick);
diff --git a/src/stanza.c b/src/stanza.c
index 7bf9414a..5e90cf90 100644
--- a/src/stanza.c
+++ b/src/stanza.c
@@ -29,6 +29,8 @@
 #include "common.h"
 #include "stanza.h"
 
+static int _field_compare(FormField *f1, FormField *f2);
+
 xmpp_stanza_t *
 stanza_create_chat_state(xmpp_ctx_t *ctx, const char * const recipient,
     const char * const state)
@@ -178,6 +180,27 @@ stanza_create_roster_iq(xmpp_ctx_t *ctx)
     return iq;
 }
 
+xmpp_stanza_t *
+stanza_create_disco_iq(xmpp_ctx_t *ctx, const char * const id, const char * const to,
+    const char * const node)
+{
+    xmpp_stanza_t *iq = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(iq, STANZA_NAME_IQ);
+    xmpp_stanza_set_type(iq, STANZA_TYPE_GET);
+    xmpp_stanza_set_attribute(iq, STANZA_ATTR_TO, to);
+    xmpp_stanza_set_id(iq, id);
+
+    xmpp_stanza_t *query = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(query, STANZA_NAME_QUERY);
+    xmpp_stanza_set_ns(query, XMPP_NS_DISCO_INFO);
+    xmpp_stanza_set_attribute(query, STANZA_ATTR_NODE, node);
+
+    xmpp_stanza_add_child(iq, query);
+    xmpp_stanza_release(query);
+
+    return iq;
+}
+
 gboolean
 stanza_contains_chat_state(xmpp_stanza_t *stanza)
 {
@@ -391,3 +414,175 @@ stanza_get_idle_time(xmpp_stanza_t * const stanza)
         return result;
     }
 }
+
+gboolean
+stanza_contains_caps(xmpp_stanza_t * const stanza)
+{
+    xmpp_stanza_t *caps = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_C);
+
+    if (caps == NULL) {
+        return FALSE;
+    }
+
+    if (strcmp(xmpp_stanza_get_ns(caps), STANZA_NS_CAPS) != 0) {
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+gboolean
+stanza_is_version_request(xmpp_stanza_t * const stanza)
+{
+    char *type = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TYPE);
+
+    if (g_strcmp0(type, STANZA_TYPE_GET) != 0) {
+        return FALSE;
+    }
+
+    xmpp_stanza_t *query = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY);
+
+    if (query == NULL) {
+        return FALSE;
+    }
+
+    char *ns = xmpp_stanza_get_ns(query);
+
+    if (ns == NULL) {
+        return FALSE;
+    }
+
+    if (strcmp(ns, STANZA_NS_VERSION) != 0) {
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+gboolean
+stanza_is_caps_request(xmpp_stanza_t * const stanza)
+{
+    char *type = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TYPE);
+
+    if (g_strcmp0(type, STANZA_TYPE_GET) != 0) {
+        return FALSE;
+    }
+
+    xmpp_stanza_t *query = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY);
+
+    if (query == NULL) {
+        return FALSE;
+    }
+
+    char *ns = xmpp_stanza_get_ns(query);
+
+    if (ns == NULL) {
+        return FALSE;
+    }
+
+    if (strcmp(ns, XMPP_NS_DISCO_INFO) != 0) {
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+char *
+stanza_caps_get_hash(xmpp_stanza_t * const stanza)
+{
+    xmpp_stanza_t *caps = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_C);
+
+    if (caps == NULL) {
+        return NULL;
+    }
+
+    if (strcmp(xmpp_stanza_get_ns(caps), STANZA_NS_CAPS) != 0) {
+        return NULL;
+    }
+
+    char *result = xmpp_stanza_get_attribute(caps, STANZA_ATTR_HASH);
+
+    return result;
+
+}
+
+char *
+stanza_get_caps_str(xmpp_stanza_t * const stanza)
+{
+    xmpp_stanza_t *caps = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_C);
+
+    if (caps == NULL) {
+        return NULL;
+    }
+
+    if (strcmp(xmpp_stanza_get_ns(caps), STANZA_NS_CAPS) != 0) {
+        return NULL;
+    }
+
+    char *node = xmpp_stanza_get_attribute(caps, STANZA_ATTR_NODE);
+    char *ver = xmpp_stanza_get_attribute(caps, STANZA_ATTR_VER);
+
+    if ((node == NULL) || (ver == NULL)) {
+        return NULL;
+    }
+
+    GString *caps_gstr = g_string_new(node);
+    g_string_append(caps_gstr, "#");
+    g_string_append(caps_gstr, ver);
+    char *caps_str = caps_gstr->str;
+    g_string_free(caps_gstr, FALSE);
+
+    return  caps_str;
+}
+
+DataForm *
+stanza_get_form(xmpp_stanza_t * const stanza)
+{
+    DataForm *result = NULL;
+
+    xmpp_stanza_t *child = xmpp_stanza_get_children(stanza);
+
+    if (child != NULL) {
+        result = malloc(sizeof(struct data_form_t));
+        result->form_type = NULL;
+        result->fields = NULL;
+    }
+
+    //handle fields
+    while (child != NULL) {
+        char *var = xmpp_stanza_get_attribute(child, "var");
+
+        // handle FORM_TYPE
+        if (g_strcmp0(var, "FORM_TYPE")) {
+            xmpp_stanza_t *value = xmpp_stanza_get_child_by_name(child, "value");
+            char *value_text = xmpp_stanza_get_text(value);
+            result->form_type = strdup(value_text);
+
+        // handle regular fields
+        } else {
+            FormField *field = malloc(sizeof(struct form_field_t));
+            field->var = strdup(var);
+            field->values = NULL;
+            xmpp_stanza_t *value = xmpp_stanza_get_children(child);
+
+            // handle values
+            while (value != NULL) {
+                char *text = xmpp_stanza_get_text(value);
+                field->values = g_slist_insert_sorted(field->values, text, (GCompareFunc)octet_compare);
+                value = xmpp_stanza_get_next(value);
+            }
+
+            result->fields = g_slist_insert_sorted(result->fields, field, (GCompareFunc)_field_compare);
+        }
+
+        child = xmpp_stanza_get_next(child);
+    }
+
+    return result;
+}
+
+static int
+_field_compare(FormField *f1, FormField *f2)
+{
+    return octet_compare((unsigned char *)f1->var, (unsigned char *)f2->var);
+}
diff --git a/src/stanza.h b/src/stanza.h
index a0a22908..497a7388 100644
--- a/src/stanza.h
+++ b/src/stanza.h
@@ -47,6 +47,9 @@
 #define STANZA_NAME_TEXT "text"
 #define STANZA_NAME_SUBJECT "subject"
 #define STANZA_NAME_ITEM "item"
+#define STANZA_NAME_C "c"
+#define STANZA_NAME_IDENTITY "identity"
+#define STANZA_NAME_FEATURE "feature"
 
 #define STANZA_TYPE_CHAT "chat"
 #define STANZA_TYPE_GROUPCHAT "groupchat"
@@ -72,6 +75,10 @@
 #define STANZA_ATTR_ASK "ask"
 #define STANZA_ATTR_ID "id"
 #define STANZA_ATTR_SECONDS "seconds"
+#define STANZA_ATTR_NODE "node"
+#define STANZA_ATTR_VER "ver"
+#define STANZA_ATTR_VAR "var"
+#define STANZA_ATTR_HASH "hash"
 
 #define STANZA_TEXT_AWAY "away"
 #define STANZA_TEXT_DND "dnd"
@@ -82,8 +89,21 @@
 #define STANZA_NS_CHATSTATES "http://jabber.org/protocol/chatstates"
 #define STANZA_NS_MUC "http://jabber.org/protocol/muc"
 #define STANZA_NS_MUC_USER "http://jabber.org/protocol/muc#user"
+#define STANZA_NS_CAPS "http://jabber.org/protocol/caps"
 #define STANZA_NS_PING "urn:xmpp:ping"
 #define STANZA_NS_LASTACTIVITY "jabber:iq:last"
+#define STANZA_NS_DATA "jabber:x:data"
+#define STANZA_NS_VERSION "jabber:iq:version"
+
+typedef struct form_field_t {
+    char *var;
+    GSList *values;
+} FormField;
+
+typedef struct data_form_t {
+    char *form_type;
+    GSList *fields;
+} DataForm;
 
 xmpp_stanza_t* stanza_create_chat_state(xmpp_ctx_t *ctx,
     const char * const recipient, const char * const state);
@@ -106,6 +126,8 @@ xmpp_stanza_t* stanza_create_presence(xmpp_ctx_t *ctx, const char * const show,
 
 xmpp_stanza_t* stanza_create_roster_iq(xmpp_ctx_t *ctx);
 xmpp_stanza_t* stanza_create_ping_iq(xmpp_ctx_t *ctx);
+xmpp_stanza_t* stanza_create_disco_iq(xmpp_ctx_t *ctx, const char * const id,
+    const char * const to, const char * const node);
 
 gboolean stanza_contains_chat_state(xmpp_stanza_t *stanza);
 
@@ -115,8 +137,16 @@ gboolean stanza_is_muc_self_presence(xmpp_stanza_t * const stanza,
     const char * const self_jid);
 gboolean stanza_is_room_nick_change(xmpp_stanza_t * const stanza);
 
-char* stanza_get_new_nick(xmpp_stanza_t * const stanza);
+char * stanza_get_new_nick(xmpp_stanza_t * const stanza);
 
 int stanza_get_idle_time(xmpp_stanza_t * const stanza);
+char * stanza_get_caps_str(xmpp_stanza_t * const stanza);
+gboolean stanza_contains_caps(xmpp_stanza_t * const stanza);
+char * stanza_caps_get_hash(xmpp_stanza_t * const stanza);
+gboolean stanza_is_caps_request(xmpp_stanza_t * const stanza);
+
+gboolean stanza_is_version_request(xmpp_stanza_t * const stanza);
+
+DataForm * stanza_get_form(xmpp_stanza_t * const stanza);
 
 #endif
diff --git a/src/ui.h b/src/ui.h
index b69c1f19..1f8b81d5 100644
--- a/src/ui.h
+++ b/src/ui.h
@@ -133,8 +133,11 @@ void win_show_room_nick_change(const char * const room, const char * const nick)
 void win_show_room_member_presence(const char * const room,
     const char * const nick, const char * const show, const char * const status);
 void win_room_show_status(const char * const contact);
+void win_room_show_info(const char * const contact);
 void win_show_status(void);
 void win_private_show_status(void);
+void win_show_info(void);
+void win_private_show_info(void);
 
 // console window actions
 void cons_about(void);
@@ -161,6 +164,7 @@ void cons_show_contacts(GSList * list);
 void cons_check_version(gboolean not_available_msg);
 void cons_show_wins(void);
 void cons_show_status(const char * const contact);
+void cons_show_info(const char * const contact);
 void cons_show_themes(GSList *themes);
 
 // status bar actions
diff --git a/src/windows.c b/src/windows.c
index e0811dad..f281be24 100644
--- a/src/windows.c
+++ b/src/windows.c
@@ -44,6 +44,7 @@
 #include <ncurses.h>
 #endif
 
+#include "capabilities.h"
 #include "chat_log.h"
 #include "chat_session.h"
 #include "command.h"
@@ -109,8 +110,11 @@ static void _win_resize_all(void);
 static gint _win_get_unread(void);
 static void _win_show_history(WINDOW *win, int win_index,
     const char * const contact);
+static void _win_show_info(WINDOW *win, PContact pcontact);
 static gboolean _new_release(char *found_version);
 static void _ui_draw_win_title(void);
+static void _presence_colour_on(WINDOW *win, const char * const presence);
+static void _presence_colour_off(WINDOW *win, const char * const presence);
 
 static void _notify(const char * const message, int timeout,
     const char * const category);
@@ -850,35 +854,9 @@ win_show_room_roster(const char * const room, GList *roster, const char * const
             const char const *name = p_contact_jid(member);
             const char const *show = p_contact_presence(member);
 
-            if (strcmp(show, "away") == 0) {
-                wattron(win, COLOUR_AWAY);
-            } else if (strcmp(show, "chat") == 0) {
-                wattron(win, COLOUR_CHAT);
-            } else if (strcmp(show, "dnd") == 0) {
-                wattron(win, COLOUR_DND);
-            } else if (strcmp(show, "xa") == 0) {
-                wattron(win, COLOUR_XA);
-            } else if (strcmp(show, "online") == 0) {
-                wattron(win, COLOUR_ONLINE);
-            } else {
-                wattron(win, COLOUR_OFFLINE);
-            }
-
+            _presence_colour_on(win, show);
             wprintw(win, "%s", name);
-
-            if (strcmp(show, "away") == 0) {
-                wattroff(win, COLOUR_AWAY);
-            } else if (strcmp(show, "chat") == 0) {
-                wattroff(win, COLOUR_CHAT);
-            } else if (strcmp(show, "dnd") == 0) {
-                wattroff(win, COLOUR_DND);
-            } else if (strcmp(show, "xa") == 0) {
-                wattroff(win, COLOUR_XA);
-            } else if (strcmp(show, "online") == 0) {
-                wattroff(win, COLOUR_ONLINE);
-            } else {
-                wattroff(win, COLOUR_OFFLINE);
-            }
+            _presence_colour_off(win, show);
 
             if (roster->next != NULL) {
                 wprintw(win, ", ");
@@ -1181,6 +1159,57 @@ cons_show_wins(void)
 }
 
 void
+win_room_show_info(const char * const contact)
+{
+    PContact pcontact = muc_get_participant(win_current_get_recipient(), contact);
+
+    if (pcontact != NULL) {
+        _win_show_info(current->win, pcontact);
+    } else {
+        win_current_show("No such participant \"%s\" in room.", contact);
+    }
+
+}
+
+void
+cons_show_info(const char * const contact)
+{
+    PContact pcontact = contact_list_get_contact(contact);
+
+    if (pcontact != NULL) {
+        _win_show_info(console->win, pcontact);
+    } else {
+        cons_show("No such contact \"%s\" in roster.", contact);
+    }
+}
+
+void
+win_show_info(void)
+{
+    PContact pcontact = contact_list_get_contact(win_current_get_recipient());
+
+    if (pcontact != NULL) {
+        _win_show_info(current->win, pcontact);
+    } else {
+        win_current_show("No such contact \"%s\" in roster.", win_current_get_recipient());
+    }
+}
+
+void
+win_private_show_info(void)
+{
+    Jid *jid = jid_create(win_current_get_recipient());
+
+    PContact pcontact = muc_get_participant(jid->barejid, jid->resourcepart);
+
+    if (pcontact != NULL) {
+        _win_show_info(current->win, pcontact);
+    } else {
+        win_current_show("No such participant \"%s\" in room.", jid->resourcepart);
+    }
+}
+
+void
 cons_show_status(const char * const contact)
 {
     PContact pcontact = contact_list_get_contact(contact);
@@ -2030,6 +2059,42 @@ _win_resize_all(void)
 }
 
 static void
+_presence_colour_on(WINDOW *win, const char * const presence)
+{
+    if (g_strcmp0(presence, "online") == 0) {
+        wattron(win, COLOUR_ONLINE);
+    } else if (g_strcmp0(presence, "away") == 0) {
+        wattron(win, COLOUR_AWAY);
+    } else if (g_strcmp0(presence, "chat") == 0) {
+        wattron(win, COLOUR_CHAT);
+    } else if (g_strcmp0(presence, "dnd") == 0) {
+        wattron(win, COLOUR_DND);
+    } else if (g_strcmp0(presence, "xa") == 0) {
+        wattron(win, COLOUR_XA);
+    } else {
+        wattron(win, COLOUR_OFFLINE);
+    }
+}
+
+static void
+_presence_colour_off(WINDOW *win, const char * const presence)
+{
+    if (g_strcmp0(presence, "online") == 0) {
+        wattroff(win, COLOUR_ONLINE);
+    } else if (g_strcmp0(presence, "away") == 0) {
+        wattroff(win, COLOUR_AWAY);
+    } else if (g_strcmp0(presence, "chat") == 0) {
+        wattroff(win, COLOUR_CHAT);
+    } else if (g_strcmp0(presence, "dnd") == 0) {
+        wattroff(win, COLOUR_DND);
+    } else if (g_strcmp0(presence, "xa") == 0) {
+        wattroff(win, COLOUR_XA);
+    } else {
+        wattroff(win, COLOUR_OFFLINE);
+    }
+}
+
+static void
 _show_status_string(WINDOW *win, const char * const from,
     const char * const show, const char * const status,
     GDateTime *last_activity, const char * const pre,
@@ -2138,21 +2203,7 @@ _win_show_contact(ProfWin *window, PContact contact)
     GDateTime *last_activity = p_contact_last_activity(contact);
 
     _win_show_time(window->win, '-');
-
-    if (strcmp(presence, "online") == 0) {
-        wattron(window->win, COLOUR_ONLINE);
-    } else if (strcmp(presence, "away") == 0) {
-        wattron(window->win, COLOUR_AWAY);
-    } else if (strcmp(presence, "chat") == 0) {
-        wattron(window->win, COLOUR_CHAT);
-    } else if (strcmp(presence, "dnd") == 0) {
-        wattron(window->win, COLOUR_DND);
-    } else if (strcmp(presence, "xa") == 0) {
-        wattron(window->win, COLOUR_XA);
-    } else {
-        wattron(window->win, COLOUR_OFFLINE);
-    }
-
+    _presence_colour_on(window->win, presence);
     wprintw(window->win, "%s", jid);
 
     if (name != NULL) {
@@ -2186,20 +2237,7 @@ _win_show_contact(ProfWin *window, PContact contact)
     }
 
     wprintw(window->win, "\n");
-
-    if (strcmp(presence, "online") == 0) {
-        wattroff(window->win, COLOUR_ONLINE);
-    } else if (strcmp(presence, "away") == 0) {
-        wattroff(window->win, COLOUR_AWAY);
-    } else if (strcmp(presence, "chat") == 0) {
-        wattroff(window->win, COLOUR_CHAT);
-    } else if (strcmp(presence, "dnd") == 0) {
-        wattroff(window->win, COLOUR_DND);
-    } else if (strcmp(presence, "xa") == 0) {
-        wattroff(window->win, COLOUR_XA);
-    } else {
-        wattroff(window->win, COLOUR_OFFLINE);
-    }
+    _presence_colour_off(window->win, presence);
 }
 
 static void
@@ -2331,6 +2369,80 @@ _win_show_history(WINDOW *win, int win_index, const char * const contact)
     }
 }
 
+static void
+_win_show_info(WINDOW *win, PContact pcontact)
+{
+    const char *jid = p_contact_jid(pcontact);
+    const char *name = p_contact_name(pcontact);
+    const char *presence = p_contact_presence(pcontact);
+    const char *status = p_contact_status(pcontact);
+    const char *sub = p_contact_subscription(pcontact);
+    const char *caps_str = p_contact_caps_str(pcontact);
+    GDateTime *last_activity = p_contact_last_activity(pcontact);
+
+    _win_show_time(win, '-');
+    wprintw(win, "\n");
+    _win_show_time(win, '-');
+    _presence_colour_on(win, presence);
+    wprintw(win, "%s:\n", jid);
+    _presence_colour_off(win, presence);
+
+    if (name != NULL) {
+        _win_show_time(win, '-');
+        wprintw(win, "Name          : %s\n", name);
+    }
+
+    if (sub != NULL) {
+        _win_show_time(win, '-');
+        wprintw(win, "Subscription  : %s\n", sub);
+    }
+
+    _win_show_time(win, '-');
+    wprintw(win, "Presence      : ");
+    _presence_colour_on(win, presence);
+    wprintw(win, "%s\n", presence);
+    _presence_colour_off(win, presence);
+
+    if (status != NULL) {
+        _win_show_time(win, '-');
+        wprintw(win, "Message       : %s\n", status);
+    }
+
+    if (last_activity != NULL) {
+        GDateTime *now = g_date_time_new_now_local();
+        GTimeSpan span = g_date_time_difference(now, last_activity);
+
+        _win_show_time(win, '-');
+        wprintw(win, "Last activity : ");
+
+        int hours = span / G_TIME_SPAN_HOUR;
+        span = span - hours * G_TIME_SPAN_HOUR;
+        if (hours > 0) {
+            wprintw(win, "%dh", hours);
+        }
+
+        int minutes = span / G_TIME_SPAN_MINUTE;
+        span = span - minutes * G_TIME_SPAN_MINUTE;
+        wprintw(win, "%dm", minutes);
+
+        int seconds = span / G_TIME_SPAN_SECOND;
+        wprintw(win, "%ds", seconds);
+
+        wprintw(win, "\n");
+
+        g_date_time_unref(now);
+    }
+
+    if (caps_str != NULL) {
+        Capabilities *caps = caps_get(caps_str);
+        if ((caps != NULL) && (caps->client != NULL)) {
+            _win_show_time(win, '-');
+            wprintw(win, "Client        : %s\n", caps->client);
+        }
+    }
+
+}
+
 void
 _set_current(int index)
 {
diff --git a/tests/test_contact_list.c b/tests/test_contact_list.c
index f04e80d5..0f6d48bf 100644
--- a/tests/test_contact_list.c
+++ b/tests/test_contact_list.c
@@ -191,7 +191,7 @@ static void test_status_when_no_value(void)
 static void update_show(void)
 {
     contact_list_add("James", NULL, "away", NULL, NULL, FALSE);
-    contact_list_update_contact("James", "dnd", NULL, NULL);
+    contact_list_update_contact("James", "dnd", NULL, NULL, NULL);
     GSList *list = get_contact_list();
 
     assert_int_equals(1, g_slist_length(list));
@@ -203,7 +203,7 @@ static void update_show(void)
 static void set_show_to_null(void)
 {
     contact_list_add("James", NULL, "away", NULL, NULL, FALSE);
-    contact_list_update_contact("James", NULL, NULL, NULL);
+    contact_list_update_contact("James", NULL, NULL, NULL, NULL);
     GSList *list = get_contact_list();
 
     assert_int_equals(1, g_slist_length(list));
@@ -215,7 +215,7 @@ static void set_show_to_null(void)
 static void update_status(void)
 {
     contact_list_add("James", NULL, NULL, "I'm not here right now", NULL, FALSE);
-    contact_list_update_contact("James", NULL, "Gone to lunch", NULL);
+    contact_list_update_contact("James", NULL, "Gone to lunch", NULL, NULL);
     GSList *list = get_contact_list();
 
     assert_int_equals(1, g_slist_length(list));
@@ -227,7 +227,7 @@ static void update_status(void)
 static void set_status_to_null(void)
 {
     contact_list_add("James", NULL, NULL, "Gone to lunch", NULL, FALSE);
-    contact_list_update_contact("James", NULL, NULL, NULL);
+    contact_list_update_contact("James", NULL, NULL, NULL, NULL);
     GSList *list = get_contact_list();
 
     assert_int_equals(1, g_slist_length(list));