about summary refs log tree commit diff stats
path: root/src/xmpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/xmpp')
-rw-r--r--src/xmpp/blocking.c8
-rw-r--r--src/xmpp/blocking.h2
-rw-r--r--src/xmpp/bookmark.c4
-rw-r--r--src/xmpp/bookmark.h2
-rw-r--r--src/xmpp/capabilities.c2
-rw-r--r--src/xmpp/capabilities.h2
-rw-r--r--src/xmpp/chat_session.c2
-rw-r--r--src/xmpp/chat_session.h2
-rw-r--r--src/xmpp/chat_state.c2
-rw-r--r--src/xmpp/chat_state.h2
-rw-r--r--src/xmpp/connection.c26
-rw-r--r--src/xmpp/connection.h4
-rw-r--r--src/xmpp/contact.c2
-rw-r--r--src/xmpp/contact.h2
-rw-r--r--src/xmpp/form.c2
-rw-r--r--src/xmpp/form.h2
-rw-r--r--src/xmpp/iq.c279
-rw-r--r--src/xmpp/iq.h2
-rw-r--r--src/xmpp/jid.c2
-rw-r--r--src/xmpp/jid.h2
-rw-r--r--src/xmpp/message.c14
-rw-r--r--src/xmpp/message.h2
-rw-r--r--src/xmpp/muc.c2
-rw-r--r--src/xmpp/muc.h2
-rw-r--r--src/xmpp/presence.c12
-rw-r--r--src/xmpp/presence.h2
-rw-r--r--src/xmpp/resource.c2
-rw-r--r--src/xmpp/resource.h2
-rw-r--r--src/xmpp/roster.c19
-rw-r--r--src/xmpp/roster.h3
-rw-r--r--src/xmpp/roster_list.c26
-rw-r--r--src/xmpp/roster_list.h4
-rw-r--r--src/xmpp/session.c2
-rw-r--r--src/xmpp/session.h2
-rw-r--r--src/xmpp/stanza.c115
-rw-r--r--src/xmpp/stanza.h10
-rw-r--r--src/xmpp/xmpp.h9
37 files changed, 462 insertions, 117 deletions
diff --git a/src/xmpp/blocking.c b/src/xmpp/blocking.c
index 5d1c52b2..d3c84f0d 100644
--- a/src/xmpp/blocking.c
+++ b/src/xmpp/blocking.c
@@ -1,7 +1,7 @@
 /*
  * blocking.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
@@ -73,7 +73,7 @@ blocking_request(void)
     }
     blocked_ac = autocomplete_new();
 
-    char *id = create_unique_id("blocked_list_request");
+    char *id = connection_create_stanza_id("blocked_list_request");
     iq_id_handler_add(id, _blocklist_result_handler, NULL, NULL);
 
     xmpp_ctx_t *ctx = connection_get_ctx();
@@ -115,7 +115,7 @@ blocked_add(char *jid)
 
     xmpp_ctx_t *ctx = connection_get_ctx();
 
-    char *id = create_unique_id("block");
+    char *id = connection_create_stanza_id("block");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
 
     xmpp_stanza_t *block = xmpp_stanza_new(ctx);
@@ -151,7 +151,7 @@ blocked_remove(char *jid)
 
     xmpp_ctx_t *ctx = connection_get_ctx();
 
-    char *id = create_unique_id("unblock");
+    char *id = connection_create_stanza_id("unblock");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
 
     xmpp_stanza_t *block = xmpp_stanza_new(ctx);
diff --git a/src/xmpp/blocking.h b/src/xmpp/blocking.h
index 61acb631..7b3863f1 100644
--- a/src/xmpp/blocking.h
+++ b/src/xmpp/blocking.h
@@ -1,7 +1,7 @@
 /*
  * blocking.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/bookmark.c b/src/xmpp/bookmark.c
index 65009224..99c17c1e 100644
--- a/src/xmpp/bookmark.c
+++ b/src/xmpp/bookmark.c
@@ -1,7 +1,7 @@
 /*
  * bookmark.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
@@ -333,7 +333,7 @@ _send_bookmarks(void)
 {
     xmpp_ctx_t *ctx = connection_get_ctx();
 
-    char *id = create_unique_id("bookmarks_update");
+    char *id = connection_create_stanza_id("bookmarks_update");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
     free(id);
 
diff --git a/src/xmpp/bookmark.h b/src/xmpp/bookmark.h
index f6c72c27..0823aee1 100644
--- a/src/xmpp/bookmark.h
+++ b/src/xmpp/bookmark.h
@@ -1,7 +1,7 @@
 /*
  * bookmark.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/capabilities.c b/src/xmpp/capabilities.c
index 2152f869..8d66b25a 100644
--- a/src/xmpp/capabilities.c
+++ b/src/xmpp/capabilities.c
@@ -1,7 +1,7 @@
 /*
  * capabilities.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/capabilities.h b/src/xmpp/capabilities.h
index 21ffe0b4..a4e25128 100644
--- a/src/xmpp/capabilities.h
+++ b/src/xmpp/capabilities.h
@@ -1,7 +1,7 @@
 /*
  * capabilities.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/chat_session.c b/src/xmpp/chat_session.c
index 799a9c47..1b57068f 100644
--- a/src/xmpp/chat_session.c
+++ b/src/xmpp/chat_session.c
@@ -1,7 +1,7 @@
 /*
  * chat_session.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/chat_session.h b/src/xmpp/chat_session.h
index d8de0330..564ee2b0 100644
--- a/src/xmpp/chat_session.h
+++ b/src/xmpp/chat_session.h
@@ -1,7 +1,7 @@
 /*
  * chat_session.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/chat_state.c b/src/xmpp/chat_state.c
index 9659eb8b..da5c1342 100644
--- a/src/xmpp/chat_state.c
+++ b/src/xmpp/chat_state.c
@@ -1,7 +1,7 @@
 /*
  * chat_state.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/chat_state.h b/src/xmpp/chat_state.h
index 51698cd1..f3d3adb0 100644
--- a/src/xmpp/chat_state.h
+++ b/src/xmpp/chat_state.h
@@ -1,7 +1,7 @@
 /*
  * chat_state.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/connection.c b/src/xmpp/connection.c
index 6c5e7323..2adda46e 100644
--- a/src/xmpp/connection.c
+++ b/src/xmpp/connection.c
@@ -1,7 +1,7 @@
 /*
  * connection.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
@@ -150,6 +150,9 @@ connection_connect(const char *const jid, const char *const passwd, const char *
 
     if (!tls_policy || (g_strcmp0(tls_policy, "force") == 0)) {
         xmpp_conn_set_flags(conn.xmpp_conn, XMPP_CONN_FLAG_MANDATORY_TLS);
+    } else if (g_strcmp0(tls_policy, "trust") == 0) {
+        xmpp_conn_set_flags(conn.xmpp_conn, XMPP_CONN_FLAG_MANDATORY_TLS);
+        xmpp_conn_set_flags(conn.xmpp_conn, XMPP_CONN_FLAG_TRUST_TLS);
     } else if (g_strcmp0(tls_policy, "disable") == 0) {
         xmpp_conn_set_flags(conn.xmpp_conn, XMPP_CONN_FLAG_DISABLE_TLS);
     } else if (g_strcmp0(tls_policy, "legacy") == 0) {
@@ -393,6 +396,27 @@ connection_free_uuid(char *uuid)
 }
 
 char*
+connection_create_stanza_id(char *prefix)
+{
+    char *result = NULL;
+    GString *result_str = g_string_new("");
+    char *uuid = connection_create_uuid();
+
+    if (prefix) {
+        g_string_printf(result_str, "prof_%s_%s", prefix, uuid);
+    } else {
+        g_string_printf(result_str, "prof_%s", uuid);
+    }
+
+    connection_free_uuid(uuid);
+
+    result = result_str->str;
+    g_string_free(result_str, FALSE);
+
+    return result;
+}
+
+char*
 connection_get_domain(void)
 {
     return conn.domain;
diff --git a/src/xmpp/connection.h b/src/xmpp/connection.h
index b0ff7cec..170bc2bf 100644
--- a/src/xmpp/connection.h
+++ b/src/xmpp/connection.h
@@ -1,7 +1,7 @@
 /*
  * connection.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
@@ -60,4 +60,6 @@ void connection_clear_data(void);
 void connection_add_available_resource(Resource *resource);
 void connection_remove_available_resource(const char *const resource);
 
+char* connection_create_stanza_id(char *prefix);
+
 #endif
diff --git a/src/xmpp/contact.c b/src/xmpp/contact.c
index a43372f3..49d0786c 100644
--- a/src/xmpp/contact.c
+++ b/src/xmpp/contact.c
@@ -1,7 +1,7 @@
 /*
  * contact.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/contact.h b/src/xmpp/contact.h
index 170df2ca..d3b2dee3 100644
--- a/src/xmpp/contact.h
+++ b/src/xmpp/contact.h
@@ -1,7 +1,7 @@
 /*
  * contact.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/form.c b/src/xmpp/form.c
index 6e26411a..d30eb0fc 100644
--- a/src/xmpp/form.c
+++ b/src/xmpp/form.c
@@ -1,7 +1,7 @@
 /*
  * form.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/form.h b/src/xmpp/form.h
index 21314ce2..04af8f6b 100644
--- a/src/xmpp/form.h
+++ b/src/xmpp/form.h
@@ -1,7 +1,7 @@
 /*
  * form.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/iq.c b/src/xmpp/iq.c
index 9bceb859..a77ef59b 100644
--- a/src/xmpp/iq.c
+++ b/src/xmpp/iq.c
@@ -1,7 +1,7 @@
 /*
  * iq.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
@@ -88,6 +88,11 @@ typedef struct privilege_set_t {
     char *privilege;
 } ProfPrivilegeSet;
 
+typedef struct command_config_data_t {
+    char *sessionid;
+    char *command;
+} CommandConfigData;
+
 static int _iq_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
 
 static void _error_handler(xmpp_stanza_t *const stanza);
@@ -120,6 +125,8 @@ static int _caps_response_for_jid_id_handler(xmpp_stanza_t *const stanza, void *
 static int _caps_response_legacy_id_handler(xmpp_stanza_t *const stanza, void *const userdata);
 static int _auto_pong_id_handler(xmpp_stanza_t *const stanza, void *const userdata);
 static int _room_list_id_handler(xmpp_stanza_t *const stanza, void *const userdata);
+static int _command_list_result_handler(xmpp_stanza_t *const stanza, void *const userdata);
+static int _command_exec_response_handler(xmpp_stanza_t *const stanza, void *const userdata);
 
 static void _iq_free_room_data(ProfRoomInfoData *roominfo);
 static void _iq_free_affiliation_set(ProfPrivilegeSet *affiliation_set);
@@ -318,8 +325,8 @@ iq_room_list_request(gchar *conferencejid, gchar *filter)
     log_debug("Rooms request not cached for: %s", conferencejid);
 
     xmpp_ctx_t * const ctx = connection_get_ctx();
-    char *id = create_unique_id("confreq");
-    xmpp_stanza_t *iq = stanza_create_disco_items_iq(ctx, id, conferencejid);
+    char *id = connection_create_stanza_id("confreq");
+    xmpp_stanza_t *iq = stanza_create_disco_items_iq(ctx, id, conferencejid, NULL);
 
     iq_id_handler_add(id, _room_list_id_handler, NULL, filter);
 
@@ -363,7 +370,7 @@ iq_http_upload_request(HTTPUpload *upload)
     }
 
     xmpp_ctx_t * const ctx = connection_get_ctx();
-    char *id = create_unique_id("http_upload_request");
+    char *id = connection_create_stanza_id("http_upload_request");
     xmpp_stanza_t *iq = stanza_create_http_upload_request(ctx, id, jid, upload);
     // TODO add free func
     iq_id_handler_add(id, _http_upload_response_id_handler, NULL, upload);
@@ -379,7 +386,7 @@ void
 iq_disco_info_request(gchar *jid)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
-    char *id = create_unique_id("disco_info");
+    char *id = connection_create_stanza_id("disco_info");
     xmpp_stanza_t *iq = stanza_create_disco_info_iq(ctx, id, jid, NULL);
 
     iq_id_handler_add(id, _disco_info_response_id_handler, NULL, NULL);
@@ -394,7 +401,7 @@ void
 iq_disco_info_request_onconnect(gchar *jid)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
-    char *id = create_unique_id("disco_info_onconnect");
+    char *id = connection_create_stanza_id("disco_info_onconnect");
     xmpp_stanza_t *iq = stanza_create_disco_info_iq(ctx, id, jid, NULL);
 
     iq_id_handler_add(id, _disco_info_response_id_handler_onconnect, NULL, NULL);
@@ -409,7 +416,7 @@ void
 iq_last_activity_request(gchar *jid)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
-    char *id = create_unique_id("lastactivity");
+    char *id = connection_create_stanza_id("lastactivity");
     xmpp_stanza_t *iq = stanza_create_last_activity_iq(ctx, id, jid);
 
     iq_id_handler_add(id, _last_activity_response_id_handler, NULL, NULL);
@@ -424,7 +431,7 @@ void
 iq_room_info_request(const char *const room, gboolean display_result)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
-    char *id = create_unique_id("room_disco_info");
+    char *id = connection_create_stanza_id("room_disco_info");
     xmpp_stanza_t *iq = stanza_create_disco_info_iq(ctx, id, room, NULL);
 
     ProfRoomInfoData *cb_data = malloc(sizeof(ProfRoomInfoData));
@@ -521,7 +528,7 @@ void
 iq_disco_items_request(gchar *jid)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
-    xmpp_stanza_t *iq = stanza_create_disco_items_iq(ctx, "discoitemsreq", jid);
+    xmpp_stanza_t *iq = stanza_create_disco_items_iq(ctx, "discoitemsreq", jid, NULL);
     iq_send_stanza(iq);
     xmpp_stanza_release(iq);
 }
@@ -530,7 +537,7 @@ void
 iq_disco_items_request_onconnect(gchar *jid)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
-    xmpp_stanza_t *iq = stanza_create_disco_items_iq(ctx, "discoitemsreq_onconnect", jid);
+    xmpp_stanza_t *iq = stanza_create_disco_items_iq(ctx, "discoitemsreq_onconnect", jid, NULL);
     iq_send_stanza(iq);
     xmpp_stanza_release(iq);
 }
@@ -584,10 +591,10 @@ iq_request_room_config_form(const char *const room_jid)
 }
 
 void
-iq_submit_room_config(const char *const room, DataForm *form)
+iq_submit_room_config(ProfConfWin *confwin)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
-    xmpp_stanza_t *iq = stanza_create_room_config_submit_iq(ctx, room, form);
+    xmpp_stanza_t *iq = stanza_create_room_config_submit_iq(ctx, confwin->roomjid, confwin->form);
 
     const char *id = xmpp_stanza_get_id(iq);
     iq_id_handler_add(id, _room_config_submit_id_handler, NULL, NULL);
@@ -597,10 +604,10 @@ iq_submit_room_config(const char *const room, DataForm *form)
 }
 
 void
-iq_room_config_cancel(const char *const room_jid)
+iq_room_config_cancel(ProfConfWin *confwin)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
-    xmpp_stanza_t *iq = stanza_create_room_config_cancel_iq(ctx, room_jid);
+    xmpp_stanza_t *iq = stanza_create_room_config_cancel_iq(ctx, confwin->roomjid);
     iq_send_stanza(iq);
     xmpp_stanza_release(iq);
 }
@@ -696,6 +703,62 @@ iq_send_ping(const char *const target)
     xmpp_stanza_release(iq);
 }
 
+void
+iq_command_list(const char *const target)
+{
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    const char *id = connection_create_stanza_id("cmdlist");
+    xmpp_stanza_t *iq = stanza_create_disco_items_iq(ctx, id, target, STANZA_NS_COMMAND);
+
+    iq_id_handler_add(id, _command_list_result_handler, NULL, NULL);
+
+    iq_send_stanza(iq);
+    xmpp_stanza_release(iq);
+}
+
+void
+iq_command_exec(const char *const target, const char *const command)
+{
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    xmpp_stanza_t *iq = stanza_create_command_exec_iq(ctx, target, command);
+    const char *id = xmpp_stanza_get_id(iq);
+
+    iq_id_handler_add(id, _command_exec_response_handler, free, strdup(command));
+
+    iq_send_stanza(iq);
+    xmpp_stanza_release(iq);
+}
+
+void
+iq_submit_command_config(ProfConfWin *confwin)
+{
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    CommandConfigData *data = (CommandConfigData *)confwin->userdata;
+    xmpp_stanza_t *iq = stanza_create_command_config_submit_iq(ctx, confwin->roomjid, data->command, data->sessionid, confwin->form);
+
+    const char *id = xmpp_stanza_get_id(iq);
+    iq_id_handler_add(id,  _command_exec_response_handler, free, strdup(data->command));
+
+    iq_send_stanza(iq);
+    xmpp_stanza_release(iq);
+    free(data->sessionid);
+    free(data->command);
+    free(data);
+}
+
+void
+iq_cancel_command_config(ProfConfWin *confwin)
+{
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    CommandConfigData *data = (CommandConfigData *)confwin->userdata;
+    xmpp_stanza_t *iq = stanza_create_room_config_cancel_iq(ctx, confwin->roomjid);
+    iq_send_stanza(iq);
+    xmpp_stanza_release(iq);
+    free(data->sessionid);
+    free(data->command);
+    free(data);
+}
+
 static void
 _error_handler(xmpp_stanza_t *const stanza)
 {
@@ -1012,6 +1075,186 @@ _room_list_id_handler(xmpp_stanza_t *const stanza, void *const userdata)
 }
 
 static int
+_command_list_result_handler(xmpp_stanza_t *const stanza, void *const userdata)
+{
+    const char *id = xmpp_stanza_get_id(stanza);
+    const char *type = xmpp_stanza_get_type(stanza);
+    char *from = strdup(xmpp_stanza_get_from(stanza));
+
+    if (id) {
+        log_debug("IQ command list result handler fired, id: %s.", id);
+    } else {
+        log_debug("IQ command list result handler fired.");
+    }
+
+    // handle error responses
+    if (g_strcmp0(type, STANZA_TYPE_ERROR) == 0) {
+        char *error_message = stanza_get_error_message(stanza);
+        log_debug("Error retrieving command list for %s: %s", from, error_message);
+        ProfWin *win = wins_get_by_string(from);
+        if (win) {
+            win_command_list_error(win, error_message);
+        }
+        free(error_message);
+        free(from);
+        return 0;
+    }
+
+    GSList *cmds = NULL;
+
+    xmpp_stanza_t *query = xmpp_stanza_get_child_by_ns(stanza, XMPP_NS_DISCO_ITEMS);
+    if (query) {
+        xmpp_stanza_t *child = xmpp_stanza_get_children(query);
+        while (child) {
+            const char *name = xmpp_stanza_get_name(child);
+            if (g_strcmp0(name, "item") == 0) {
+                const char *node = xmpp_stanza_get_attribute(child, STANZA_ATTR_NODE);
+                if (node) {
+                    cmds = g_slist_insert_sorted(cmds, (gpointer)node, (GCompareFunc)g_strcmp0);
+                }
+            }
+            child = xmpp_stanza_get_next(child);
+        }
+    }
+
+    ProfWin *win = wins_get_by_string(from);
+    if (win == NULL) {
+	    win = wins_get_console();
+    }
+
+    win_handle_command_list(win, cmds);
+    g_slist_free(cmds);
+    free(from);
+
+    return 0;
+}
+
+static int
+_command_exec_response_handler(xmpp_stanza_t *const stanza, void *const userdata)
+{
+    const char *id = xmpp_stanza_get_id(stanza);
+    const char *type = xmpp_stanza_get_type(stanza);
+    const char *from = xmpp_stanza_get_from(stanza);
+    char *command = userdata;
+
+    if (id) {
+        log_debug("IQ command exec response handler fired, id: %s.", id);
+    } else {
+        log_debug("IQ command exec response handler fired.");
+    }
+
+    ProfWin *win = wins_get_by_string(from);
+    if (win == NULL) {
+        /* No more window associated with this command.
+         * Fallback to console. */
+        win = wins_get_console();
+    }
+
+    // handle error responses
+    if (g_strcmp0(type, STANZA_TYPE_ERROR) == 0) {
+        char *error_message = stanza_get_error_message(stanza);
+        log_debug("Error executing command %s for %s: %s", command, from, error_message);
+        win_command_exec_error(win, command, error_message);
+        free(error_message);
+        return 0;
+    }
+
+    xmpp_stanza_t *cmd = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_COMMAND);
+    if (!cmd) {
+        log_error("No command element for command response");
+        win_command_exec_error(win, command, "Malformed command response");
+        return 0;
+    }
+
+
+    const char *status = xmpp_stanza_get_attribute(cmd, STANZA_ATTR_STATUS);
+    if (g_strcmp0(status, "completed") == 0) {
+        win_handle_command_exec_status(win, command, "completed");
+        xmpp_stanza_t *note = xmpp_stanza_get_child_by_name(cmd, "note");
+        if (note) {
+            const char *type = xmpp_stanza_get_attribute(note, "type");
+            const char *value = xmpp_stanza_get_text(note);
+            win_handle_command_exec_result_note(win, type, value);
+        }
+        xmpp_stanza_t *x = xmpp_stanza_get_child_by_ns(cmd, STANZA_NS_DATA);
+        if (x) {
+            xmpp_stanza_t *roster = xmpp_stanza_get_child_by_ns(x, XMPP_NS_ROSTER);
+            if (roster) {
+                /* Special handling of xep-0133 roster in response */
+                GSList *list = NULL;
+                xmpp_stanza_t *child = xmpp_stanza_get_children(roster);
+                while (child) {
+                    const char *barejid = xmpp_stanza_get_attribute(child, STANZA_ATTR_JID);
+                    gchar *barejid_lower = g_utf8_strdown(barejid, -1);
+                    const char *name = xmpp_stanza_get_attribute(child, STANZA_ATTR_NAME);
+                    const char *sub = xmpp_stanza_get_attribute(child, STANZA_ATTR_SUBSCRIPTION);
+                    const char *ask = xmpp_stanza_get_attribute(child, STANZA_ATTR_ASK);
+
+                    GSList *groups = NULL;
+                    groups = roster_get_groups_from_item(child);
+
+                    gboolean pending_out = FALSE;
+                    if (ask && (strcmp(ask, "subscribe") == 0)) {
+                        pending_out = TRUE;
+                    }
+
+                    PContact contact = p_contact_new(barejid_lower, name, groups, sub, NULL, pending_out);
+                    list = g_slist_insert_sorted(list, contact, (GCompareFunc)roster_compare_name);
+                    child = xmpp_stanza_get_next(child);
+                }
+
+                cons_show_roster(list);
+                g_slist_free(list);
+            } else {
+                DataForm *form = form_create(x);
+                ProfConfWin *confwin = (ProfConfWin*)wins_new_config(from, form, NULL, NULL, NULL);
+                confwin_handle_configuration(confwin, form);
+            }
+        }
+    } else if (g_strcmp0(status, "executing") == 0) {
+        win_handle_command_exec_status(win, command, "executing");
+
+        /* Looking for a jabber:x:data type form */
+        xmpp_stanza_t *x = xmpp_stanza_get_child_by_ns(cmd, STANZA_NS_DATA);
+        if (x == NULL) {
+            return 0;
+        }
+
+        const char *form_type = xmpp_stanza_get_type(x);
+        if (g_strcmp0(form_type, "form") != 0) {
+            log_error("Unsupported payload in command response");
+            win_command_exec_error(win, command, "Unsupported command response");
+            return 0;
+        }
+        const char *sessionid = xmpp_stanza_get_attribute(cmd, "sessionid");
+
+        DataForm *form = form_create(x);
+        CommandConfigData *data = malloc(sizeof(CommandConfigData));
+        if (sessionid == NULL) {
+            data->sessionid = NULL;
+        } else {
+            data->sessionid = strdup(sessionid);
+        }
+        data->command = command;
+        ProfConfWin *confwin = (ProfConfWin*)wins_new_config(from, form, iq_submit_command_config, iq_cancel_command_config, data);
+        confwin_handle_configuration(confwin, form);
+    } else if (g_strcmp0(status, "canceled") == 0) {
+        win_handle_command_exec_status(win, command, "canceled");
+        xmpp_stanza_t *note = xmpp_stanza_get_child_by_name(cmd, "note");
+        if (note) {
+            const char *type = xmpp_stanza_get_attribute(note, "type");
+            const char *value = xmpp_stanza_get_text(note);
+            win_handle_command_exec_result_note(win, type, value);
+        }
+    } else {
+        log_error("Unsupported command status %s", status);
+        win_command_exec_error(win, command, "Malformed command response");
+    }
+
+    return 0;
+}
+
+static int
 _enable_carbons_id_handler(xmpp_stanza_t *const stanza, void *const userdata)
 {
     const char *type = xmpp_stanza_get_type(stanza);
@@ -1088,8 +1331,8 @@ _autoping_timed_send(xmpp_conn_t *const conn, void *const userdata)
         return 1;
     }
 
-    if (connection_supports(STANZA_NS_PING) == FALSE) {
-        log_warning("Server doesn't advertise %s feature, disabling autoping.", STANZA_NS_PING);
+    if (connection_supports(XMPP_FEATURE_PING) == FALSE) {
+        log_warning("Server doesn't advertise %s feature, disabling autoping.", XMPP_FEATURE_PING);
         prefs_set_autoping(0);
         cons_show_error("Server ping not supported, autoping disabled.");
         xmpp_conn_t *conn = connection_get_conn();
@@ -1540,8 +1783,8 @@ _room_config_id_handler(xmpp_stanza_t *const stanza, void *const userdata)
     }
 
     DataForm *form = form_create(x);
-    ProfMucConfWin *confwin = (ProfMucConfWin*)wins_new_muc_config(from, form);
-    mucconfwin_handle_configuration(confwin, form);
+    ProfConfWin *confwin = (ProfConfWin*)wins_new_config(from, form, iq_submit_room_config, iq_room_config_cancel, NULL);
+    confwin_handle_configuration(confwin, form);
 
     return 0;
 }
diff --git a/src/xmpp/iq.h b/src/xmpp/iq.h
index b06568b6..025d5e9f 100644
--- a/src/xmpp/iq.h
+++ b/src/xmpp/iq.h
@@ -1,7 +1,7 @@
 /*
  * iq.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/jid.c b/src/xmpp/jid.c
index 0af54c72..51e1fa3c 100644
--- a/src/xmpp/jid.c
+++ b/src/xmpp/jid.c
@@ -1,7 +1,7 @@
 /*
  * jid.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/jid.h b/src/xmpp/jid.h
index e4e4d272..fc0e388f 100644
--- a/src/xmpp/jid.h
+++ b/src/xmpp/jid.h
@@ -1,7 +1,7 @@
 /*
  * jid.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/message.c b/src/xmpp/message.c
index 9f665892..6a2197f3 100644
--- a/src/xmpp/message.c
+++ b/src/xmpp/message.c
@@ -1,7 +1,7 @@
 /*
  * message.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
@@ -139,7 +139,7 @@ message_send_chat(const char *const barejid, const char *const msg, const char *
 
     char *state = chat_session_get_state(barejid);
     char *jid = chat_session_get_jid(barejid);
-    char *id = create_unique_id("msg");
+    char *id = connection_create_stanza_id("msg");
 
     xmpp_stanza_t *message = xmpp_message_new(ctx, STANZA_TYPE_CHAT, jid, id);
     xmpp_message_set_body(message, msg);
@@ -170,7 +170,7 @@ message_send_chat_pgp(const char *const barejid, const char *const msg, gboolean
 
     char *state = chat_session_get_state(barejid);
     char *jid = chat_session_get_jid(barejid);
-    char *id = create_unique_id("msg");
+    char *id = connection_create_stanza_id("msg");
 
     xmpp_stanza_t *message = NULL;
 #ifdef HAVE_LIBGPGME
@@ -229,7 +229,7 @@ message_send_chat_otr(const char *const barejid, const char *const msg, gboolean
 
     char *state = chat_session_get_state(barejid);
     char *jid = chat_session_get_jid(barejid);
-    char *id = create_unique_id("msg");
+    char *id = connection_create_stanza_id("msg");
 
     xmpp_stanza_t *message = xmpp_message_new(ctx, STANZA_TYPE_CHAT, barejid, id);
     xmpp_message_set_body(message, msg);
@@ -258,7 +258,7 @@ void
 message_send_private(const char *const fulljid, const char *const msg, const char *const oob_url)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
-    char *id = create_unique_id("prv");
+    char *id = connection_create_stanza_id("prv");
 
     xmpp_stanza_t *message = xmpp_message_new(ctx, STANZA_TYPE_CHAT, fulljid, id);
     xmpp_message_set_body(message, msg);
@@ -277,7 +277,7 @@ void
 message_send_groupchat(const char *const roomjid, const char *const msg, const char *const oob_url)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
-    char *id = create_unique_id("muc");
+    char *id = connection_create_stanza_id("muc");
 
     xmpp_stanza_t *message = xmpp_message_new(ctx, STANZA_TYPE_GROUPCHAT, roomjid, id);
     xmpp_message_set_body(message, msg);
@@ -584,7 +584,7 @@ _message_send_receipt(const char *const fulljid, const char *const message_id)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
 
-    char *id = create_unique_id("receipt");
+    char *id = connection_create_stanza_id("receipt");
     xmpp_stanza_t *message = xmpp_message_new(ctx, NULL, fulljid, id);
     free(id);
 
diff --git a/src/xmpp/message.h b/src/xmpp/message.h
index 47bb17ed..dee9be2d 100644
--- a/src/xmpp/message.h
+++ b/src/xmpp/message.h
@@ -1,7 +1,7 @@
 /*
  * message.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/muc.c b/src/xmpp/muc.c
index 9f2f9f61..62a589d7 100644
--- a/src/xmpp/muc.c
+++ b/src/xmpp/muc.c
@@ -1,7 +1,7 @@
 /*
  * muc.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/muc.h b/src/xmpp/muc.h
index e50e2a87..e0b035f2 100644
--- a/src/xmpp/muc.h
+++ b/src/xmpp/muc.h
@@ -1,7 +1,7 @@
 /*
  * muc.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/presence.c b/src/xmpp/presence.c
index 8efb60e0..6d615de4 100644
--- a/src/xmpp/presence.c
+++ b/src/xmpp/presence.c
@@ -1,7 +1,7 @@
 /*
  * presence.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
@@ -128,7 +128,7 @@ presence_subscription(const char *const jid, const jabber_subscr_t action)
     xmpp_ctx_t * const ctx = connection_get_ctx();
     xmpp_stanza_t *presence = xmpp_presence_new(ctx);
 
-    char *id = create_unique_id("sub");
+    char *id = connection_create_stanza_id("sub");
     xmpp_stanza_set_id(presence, id);
     free(id);
 
@@ -211,7 +211,7 @@ presence_send(const resource_presence_t presence_type, const int idle, char *sig
     xmpp_ctx_t * const ctx = connection_get_ctx();
     xmpp_stanza_t *presence = xmpp_presence_new(ctx);
 
-    char *id = create_unique_id("presence");
+    char *id = connection_create_stanza_id("presence");
     xmpp_stanza_set_id(presence, id);
     free(id);
 
@@ -579,7 +579,7 @@ _handle_caps(const char *const jid, XMPPCaps *caps)
                 caps_map_jid_to_ver(jid, caps->ver);
             } else {
                 log_info("Capabilities cache miss: %s, for %s, sending service discovery request", caps->ver, jid);
-                char *id = create_unique_id("caps");
+                char *id = connection_create_stanza_id("caps");
                 iq_send_caps_request(jid, id, caps->node, caps->ver);
                 free(id);
             }
@@ -588,14 +588,14 @@ _handle_caps(const char *const jid, XMPPCaps *caps)
     // unsupported hash, xep-0115, associate with JID, no cache
     } else if (caps->hash) {
         log_info("Hash %s not supported: %s, sending service discovery request", caps->hash, jid);
-        char *id = create_unique_id("caps");
+        char *id = connection_create_stanza_id("caps");
         iq_send_caps_request_for_jid(jid, id, caps->node, caps->ver);
         free(id);
 
    // no hash, legacy caps, cache against node#ver
    } else if (caps->node && caps->ver) {
         log_info("No hash specified: %s, legacy request made for %s#%s", jid, caps->node, caps->ver);
-        char *id = create_unique_id("caps");
+        char *id = connection_create_stanza_id("caps");
         iq_send_caps_request_legacy(jid, id, caps->node, caps->ver);
         free(id);
     } else {
diff --git a/src/xmpp/presence.h b/src/xmpp/presence.h
index 6045f72c..cbdfd20b 100644
--- a/src/xmpp/presence.h
+++ b/src/xmpp/presence.h
@@ -1,7 +1,7 @@
 /*
  * presence.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/resource.c b/src/xmpp/resource.c
index 734de3a0..fc6b503e 100644
--- a/src/xmpp/resource.c
+++ b/src/xmpp/resource.c
@@ -1,7 +1,7 @@
 /*
  * resource.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/resource.h b/src/xmpp/resource.h
index 9376a51c..ef934c2b 100644
--- a/src/xmpp/resource.h
+++ b/src/xmpp/resource.h
@@ -1,7 +1,7 @@
 /*
  * resource.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/roster.c b/src/xmpp/roster.c
index e7d9cfb2..9be154e7 100644
--- a/src/xmpp/roster.c
+++ b/src/xmpp/roster.c
@@ -1,7 +1,7 @@
 /*
  * roster.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
@@ -75,9 +75,6 @@ static int _group_add_id_handler(xmpp_stanza_t *const stanza, void *const userda
 static int _group_remove_id_handler(xmpp_stanza_t *const stanza, void *const userdata);
 static void _free_group_data(GroupData *data);
 
-// helper functions
-GSList* _get_groups_from_item(xmpp_stanza_t *item);
-
 void
 roster_request(void)
 {
@@ -91,7 +88,7 @@ void
 roster_send_add_new(const char *const barejid, const char *const name)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
-    char *id = create_unique_id("roster");
+    char *id = connection_create_stanza_id("roster");
     xmpp_stanza_t *iq = stanza_create_roster_set(ctx, id, barejid, name, NULL);
     free(id);
     iq_send_stanza(iq);
@@ -111,7 +108,7 @@ void
 roster_send_name_change(const char *const barejid, const char *const new_name, GSList *groups)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
-    char *id = create_unique_id("roster");
+    char *id = connection_create_stanza_id("roster");
     xmpp_stanza_t *iq = stanza_create_roster_set(ctx, id, barejid, new_name, groups);
     free(id);
     iq_send_stanza(iq);
@@ -130,7 +127,7 @@ roster_send_add_to_group(const char *const group, PContact contact)
 
     new_groups = g_slist_append(new_groups, strdup(group));
     // add an id handler to handle the response
-    char *unique_id = create_unique_id(NULL);
+    char *unique_id = connection_create_stanza_id(NULL);
     GroupData *data = malloc(sizeof(GroupData));
     data->group = strdup(group);
     if (p_contact_name(contact)) {
@@ -174,7 +171,7 @@ roster_send_remove_from_group(const char *const group, PContact contact)
     xmpp_ctx_t * const ctx = connection_get_ctx();
 
     // add an id handler to handle the response
-    char *unique_id = create_unique_id(NULL);
+    char *unique_id = connection_create_stanza_id(NULL);
     GroupData *data = malloc(sizeof(GroupData));
     data->group = strdup(group);
     if (p_contact_name(contact)) {
@@ -254,7 +251,7 @@ roster_set_handler(xmpp_stanza_t *const stanza)
             pending_out = TRUE;
         }
 
-        GSList *groups = _get_groups_from_item(item);
+        GSList *groups = roster_get_groups_from_item(item);
 
         // update the local roster
         PContact contact = roster_get_contact(barejid_lower);
@@ -301,7 +298,7 @@ roster_result_handler(xmpp_stanza_t *const stanza)
             pending_out = TRUE;
         }
 
-        GSList *groups = _get_groups_from_item(item);
+        GSList *groups = roster_get_groups_from_item(item);
 
         gboolean added = roster_add(barejid_lower, name, groups, sub, pending_out);
         if (!added) {
@@ -318,7 +315,7 @@ roster_result_handler(xmpp_stanza_t *const stanza)
 }
 
 GSList*
-_get_groups_from_item(xmpp_stanza_t *item)
+roster_get_groups_from_item(xmpp_stanza_t *item)
 {
     GSList *groups = NULL;
     xmpp_stanza_t *group_element = xmpp_stanza_get_children(item);
diff --git a/src/xmpp/roster.h b/src/xmpp/roster.h
index 15614377..969044a4 100644
--- a/src/xmpp/roster.h
+++ b/src/xmpp/roster.h
@@ -1,7 +1,7 @@
 /*
  * roster.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
@@ -38,5 +38,6 @@
 void roster_request(void);
 void roster_set_handler(xmpp_stanza_t *const stanza);
 void roster_result_handler(xmpp_stanza_t *const stanza);
+GSList* roster_get_groups_from_item(xmpp_stanza_t *const item);
 
 #endif
diff --git a/src/xmpp/roster_list.c b/src/xmpp/roster_list.c
index a2c5653d..5ebdc08f 100644
--- a/src/xmpp/roster_list.c
+++ b/src/xmpp/roster_list.c
@@ -1,7 +1,7 @@
 /*
  * roster_list.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
@@ -73,8 +73,6 @@ static gboolean _key_equals(void *key1, void *key2);
 static gboolean _datetimes_equal(GDateTime *dt1, GDateTime *dt2);
 static void _replace_name(const char *const current_name, const char *const new_name, const char *const barejid);
 static void _add_name_and_barejid(const char *const name, const char *const barejid);
-static gint _compare_name(PContact a, PContact b);
-static gint _compare_presence(PContact a, PContact b);
 
 void
 roster_create(void)
@@ -397,7 +395,7 @@ roster_get_contacts_by_presence(const char *const presence)
     while (g_hash_table_iter_next(&iter, &key, &value)) {
         PContact contact = (PContact)value;
         if (g_strcmp0(p_contact_presence(contact), presence) == 0) {
-            result = g_slist_insert_sorted(result, value, (GCompareFunc)_compare_name);
+            result = g_slist_insert_sorted(result, value, (GCompareFunc)roster_compare_name);
         }
     }
 
@@ -417,9 +415,9 @@ roster_get_contacts(roster_ord_t order)
 
     GCompareFunc cmp_func;
     if (order == ROSTER_ORD_PRESENCE) {
-        cmp_func = (GCompareFunc) _compare_presence;
+        cmp_func = (GCompareFunc) roster_compare_presence;
     } else {
-        cmp_func = (GCompareFunc) _compare_name;
+        cmp_func = (GCompareFunc) roster_compare_name;
     }
 
     g_hash_table_iter_init(&iter, roster->contacts);
@@ -444,7 +442,7 @@ roster_get_contacts_online(void)
     g_hash_table_iter_init(&iter, roster->contacts);
     while (g_hash_table_iter_next(&iter, &key, &value)) {
         if(strcmp(p_contact_presence(value), "offline"))
-            result = g_slist_insert_sorted(result, value, (GCompareFunc)_compare_name);
+            result = g_slist_insert_sorted(result, value, (GCompareFunc)roster_compare_name);
     }
 
     // return all contact structs
@@ -499,9 +497,9 @@ roster_get_group(const char *const group, roster_ord_t order)
 
     GCompareFunc cmp_func;
     if (order == ROSTER_ORD_PRESENCE) {
-        cmp_func = (GCompareFunc) _compare_presence;
+        cmp_func = (GCompareFunc) roster_compare_presence;
     } else {
-        cmp_func = (GCompareFunc) _compare_name;
+        cmp_func = (GCompareFunc) roster_compare_name;
     }
 
     g_hash_table_iter_init(&iter, roster->contacts);
@@ -605,8 +603,8 @@ _add_name_and_barejid(const char *const name, const char *const barejid)
     }
 }
 
-static gint
-_compare_name(PContact a, PContact b)
+gint
+roster_compare_name(PContact a, PContact b)
 {
     const char * utf8_str_a = NULL;
     const char * utf8_str_b = NULL;
@@ -645,8 +643,8 @@ _get_presence_weight(const char *presence)
     }
 }
 
-static gint
-_compare_presence(PContact a, PContact b)
+gint
+roster_compare_presence(PContact a, PContact b)
 {
     const char *presence_a = p_contact_presence(a);
     const char *presence_b = p_contact_presence(b);
@@ -663,6 +661,6 @@ _compare_presence(PContact a, PContact b)
 
     // otherwise order by name
     } else {
-        return _compare_name(a, b);
+        return roster_compare_name(a, b);
     }
 }
diff --git a/src/xmpp/roster_list.h b/src/xmpp/roster_list.h
index a0b01625..5120043c 100644
--- a/src/xmpp/roster_list.h
+++ b/src/xmpp/roster_list.h
@@ -1,7 +1,7 @@
 /*
  * roster_list.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
@@ -70,5 +70,7 @@ char* roster_group_autocomplete(const char *const search_str, gboolean previous)
 char* roster_barejid_autocomplete(const char *const search_str, gboolean previous);
 GSList* roster_get_contacts_by_presence(const char *const presence);
 char* roster_get_msg_display_name(const char *const barejid, const char *const resource);
+gint roster_compare_name(PContact a, PContact b);
+gint roster_compare_presence(PContact a, PContact b);
 
 #endif
diff --git a/src/xmpp/session.c b/src/xmpp/session.c
index e06b03f1..de7fb7ac 100644
--- a/src/xmpp/session.c
+++ b/src/xmpp/session.c
@@ -1,7 +1,7 @@
 /*
  * session.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/session.h b/src/xmpp/session.h
index a3413e9e..53200939 100644
--- a/src/xmpp/session.h
+++ b/src/xmpp/session.h
@@ -1,7 +1,7 @@
 /*
  * session.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
diff --git a/src/xmpp/stanza.c b/src/xmpp/stanza.c
index 2f690828..534ee06b 100644
--- a/src/xmpp/stanza.c
+++ b/src/xmpp/stanza.c
@@ -1,7 +1,7 @@
 /*
  * stanza.c
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
@@ -45,6 +45,7 @@
 #include <stdio.h>
 #include <libgen.h>
 #include <inttypes.h>
+#include <assert.h>
 
 #include <glib.h>
 
@@ -66,6 +67,7 @@
 #include "xmpp/muc.h"
 
 static void _stanza_add_unique_id(xmpp_stanza_t *stanza, char *prefix);
+static char* _stanza_create_sha1_hash(char *str);
 
 #if 0
 xmpp_stanza_t*
@@ -132,7 +134,7 @@ xmpp_stanza_t*
 stanza_create_bookmarks_pubsub_add(xmpp_ctx_t *ctx, const char *const jid,
     const gboolean autojoin, const char *const nick)
 {
-    char *id = create_unique_id("bookmark_add");
+    char *id = connection_create_stanza_id("bookmark_add");
     xmpp_stanza_t *stanza = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
     free(id);
 
@@ -278,7 +280,7 @@ stanza_create_http_upload_request(xmpp_ctx_t *ctx, const char *const id,
 xmpp_stanza_t*
 stanza_enable_carbons(xmpp_ctx_t *ctx)
 {
-    char *id = create_unique_id("carbons");
+    char *id = connection_create_stanza_id("carbons");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
     free(id);
 
@@ -295,7 +297,7 @@ stanza_enable_carbons(xmpp_ctx_t *ctx)
 xmpp_stanza_t*
 stanza_disable_carbons(xmpp_ctx_t *ctx)
 {
-    char *id = create_unique_id("carbons");
+    char *id = connection_create_stanza_id("carbons");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
     free(id);
 
@@ -312,7 +314,7 @@ stanza_disable_carbons(xmpp_ctx_t *ctx)
 xmpp_stanza_t*
 stanza_create_chat_state(xmpp_ctx_t *ctx, const char *const fulljid, const char *const state)
 {
-    char *id = create_unique_id(NULL);
+    char *id = connection_create_stanza_id(NULL);
     xmpp_stanza_t *msg = xmpp_message_new(ctx, STANZA_TYPE_CHAT, fulljid, id);
     free(id);
 
@@ -432,7 +434,7 @@ stanza_attach_x_oob_url(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza, const char *cons
 xmpp_stanza_t*
 stanza_create_roster_remove_set(xmpp_ctx_t *ctx, const char *const barejid)
 {
-    char *id = create_unique_id("roster");
+    char *id = connection_create_stanza_id("roster");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
     free(id);
 
@@ -498,7 +500,7 @@ xmpp_stanza_t*
 stanza_create_invite(xmpp_ctx_t *ctx, const char *const room,
     const char *const contact, const char *const reason, const char *const password)
 {
-    char *id = create_unique_id(NULL);
+    char *id = connection_create_stanza_id(NULL);
     xmpp_stanza_t *message = xmpp_message_new(ctx, NULL, contact, id);
     free(id);
 
@@ -524,7 +526,7 @@ xmpp_stanza_t*
 stanza_create_mediated_invite(xmpp_ctx_t *ctx, const char *const room,
     const char *const contact, const char *const reason)
 {
-    char *id = create_unique_id(NULL);
+    char *id = connection_create_stanza_id(NULL);
     xmpp_stanza_t *message = xmpp_message_new(ctx, NULL, room, id);
     free(id);
 
@@ -616,7 +618,7 @@ stanza_create_room_leave_presence(xmpp_ctx_t *ctx, const char *const room,
 xmpp_stanza_t*
 stanza_create_instant_room_request_iq(xmpp_ctx_t *ctx, const char *const room_jid)
 {
-    char *id = create_unique_id("room");
+    char *id = connection_create_stanza_id("room");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
     free(id);
     xmpp_stanza_set_to(iq, room_jid);
@@ -642,7 +644,7 @@ stanza_create_instant_room_request_iq(xmpp_ctx_t *ctx, const char *const room_ji
 xmpp_stanza_t*
 stanza_create_instant_room_destroy_iq(xmpp_ctx_t *ctx, const char *const room_jid)
 {
-    char *id = create_unique_id("room");
+    char *id = connection_create_stanza_id("room");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
     free(id);
     xmpp_stanza_set_to(iq, room_jid);
@@ -666,7 +668,7 @@ stanza_create_instant_room_destroy_iq(xmpp_ctx_t *ctx, const char *const room_ji
 xmpp_stanza_t*
 stanza_create_room_config_request_iq(xmpp_ctx_t *ctx, const char *const room_jid)
 {
-    char *id = create_unique_id("room");
+    char *id = connection_create_stanza_id("room");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_GET, id);
     free(id);
     xmpp_stanza_set_to(iq, room_jid);
@@ -684,7 +686,7 @@ stanza_create_room_config_request_iq(xmpp_ctx_t *ctx, const char *const room_jid
 xmpp_stanza_t*
 stanza_create_room_config_cancel_iq(xmpp_ctx_t *ctx, const char *const room_jid)
 {
-    char *id = create_unique_id("room");
+    char *id = connection_create_stanza_id("room");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
     free(id);
     xmpp_stanza_set_to(iq, room_jid);
@@ -710,7 +712,7 @@ stanza_create_room_config_cancel_iq(xmpp_ctx_t *ctx, const char *const room_jid)
 xmpp_stanza_t*
 stanza_create_room_affiliation_list_iq(xmpp_ctx_t *ctx, const char *const room, const char *const affiliation)
 {
-    char *id = create_unique_id("affiliation_get");
+    char *id = connection_create_stanza_id("affiliation_get");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_GET, id);
     free(id);
     xmpp_stanza_set_to(iq, room);
@@ -734,7 +736,7 @@ stanza_create_room_affiliation_list_iq(xmpp_ctx_t *ctx, const char *const room,
 xmpp_stanza_t*
 stanza_create_room_role_list_iq(xmpp_ctx_t *ctx, const char *const room, const char *const role)
 {
-    char *id = create_unique_id("role_get");
+    char *id = connection_create_stanza_id("role_get");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_GET, id);
     free(id);
     xmpp_stanza_set_to(iq, room);
@@ -759,7 +761,7 @@ xmpp_stanza_t*
 stanza_create_room_affiliation_set_iq(xmpp_ctx_t *ctx, const char *const room, const char *const jid,
     const char *const affiliation, const char *const reason)
 {
-    char *id = create_unique_id("affiliation_set");
+    char *id = connection_create_stanza_id("affiliation_set");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
     free(id);
     xmpp_stanza_set_to(iq, room);
@@ -797,7 +799,7 @@ xmpp_stanza_t*
 stanza_create_room_role_set_iq(xmpp_ctx_t *const ctx, const char *const room, const char *const nick,
     const char *const role, const char *const reason)
 {
-    char *id = create_unique_id("role_set");
+    char *id = connection_create_stanza_id("role_set");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
     free(id);
     xmpp_stanza_set_to(iq, room);
@@ -835,7 +837,7 @@ xmpp_stanza_t*
 stanza_create_room_kick_iq(xmpp_ctx_t *const ctx, const char *const room, const char *const nick,
     const char *const reason)
 {
-    char *id = create_unique_id("room_kick");
+    char *id = connection_create_stanza_id("room_kick");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
     free(id);
     xmpp_stanza_set_to(iq, room);
@@ -872,7 +874,7 @@ stanza_create_room_kick_iq(xmpp_ctx_t *const ctx, const char *const room, const
 xmpp_stanza_t*
 stanza_create_software_version_iq(xmpp_ctx_t *ctx, const char *const fulljid)
 {
-    char *id = create_unique_id("sv");
+    char *id = connection_create_stanza_id("sv");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_GET, id);
     free(id);
     xmpp_stanza_set_to(iq, fulljid);
@@ -924,7 +926,7 @@ stanza_create_disco_info_iq(xmpp_ctx_t *ctx, const char *const id, const char *c
 
 xmpp_stanza_t*
 stanza_create_disco_items_iq(xmpp_ctx_t *ctx, const char *const id,
-    const char *const jid)
+    const char *const jid, const char *const node)
 {
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_GET, id);
     xmpp_stanza_set_to(iq, jid);
@@ -932,6 +934,9 @@ stanza_create_disco_items_iq(xmpp_ctx_t *ctx, const char *const 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_ITEMS);
+    if (node) {
+        xmpp_stanza_set_attribute(query, STANZA_ATTR_NODE, node);
+    }
 
     xmpp_stanza_add_child(iq, query);
     xmpp_stanza_release(query);
@@ -958,7 +963,7 @@ stanza_create_last_activity_iq(xmpp_ctx_t *ctx, const char *const id, const char
 xmpp_stanza_t*
 stanza_create_room_config_submit_iq(xmpp_ctx_t *ctx, const char *const room, DataForm *form)
 {
-    char *id = create_unique_id("roomconf_submit");
+    char *id = connection_create_stanza_id("roomconf_submit");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
     free(id);
     xmpp_stanza_set_to(iq, room);
@@ -1035,7 +1040,7 @@ stanza_contains_chat_state(xmpp_stanza_t *stanza)
 xmpp_stanza_t*
 stanza_create_ping_iq(xmpp_ctx_t *ctx, const char *const target)
 {
-    char *id = create_unique_id("ping");
+    char *id = connection_create_stanza_id("ping");
     xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_GET, id);
     free(id);
     if (target) {
@@ -1143,7 +1148,7 @@ stanza_create_caps_sha1_from_query(xmpp_stanza_t *const query)
         curr = g_slist_next(curr);
     }
 
-    char *result = p_sha1_hash(s->str);
+    char *result = _stanza_create_sha1_hash(s->str);
 
     g_string_free(s, TRUE);
     g_slist_free_full(identities, g_free);
@@ -2038,10 +2043,74 @@ stanza_parse_presence(xmpp_stanza_t *stanza, int *err)
     return result;
 }
 
+xmpp_stanza_t*
+stanza_create_command_exec_iq(xmpp_ctx_t *ctx, const char *const target,
+    const char *const node)
+{
+    char *id = connection_create_stanza_id("cmdexec");
+    xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
+    free(id);
+    xmpp_stanza_set_to(iq, target);
+
+    xmpp_stanza_t *command = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(command, STANZA_NAME_COMMAND);
+
+    xmpp_stanza_set_ns(command, STANZA_NS_COMMAND);
+    xmpp_stanza_set_attribute(command, "node", node);
+    xmpp_stanza_set_attribute(command, "action", "execute");
+
+    xmpp_stanza_add_child(iq, command);
+    xmpp_stanza_release(command);
+
+    return iq;
+}
+
+xmpp_stanza_t*
+stanza_create_command_config_submit_iq(xmpp_ctx_t *ctx, const char *const room,
+    const char *const node, const char *const sessionid, DataForm *form)
+{
+    char *id = connection_create_stanza_id("commandconf_submit");
+    xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
+    free(id);
+    xmpp_stanza_set_to(iq, room);
+
+    xmpp_stanza_t *command = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(command, STANZA_NAME_COMMAND);
+    xmpp_stanza_set_ns(command, STANZA_NS_COMMAND);
+    xmpp_stanza_set_attribute(command, "node", node);
+    if (sessionid != NULL) {
+        xmpp_stanza_set_attribute(command, "sessionid", sessionid);
+    }
+
+    xmpp_stanza_t *x = form_create_submission(form);
+    xmpp_stanza_add_child(command, x);
+    xmpp_stanza_release(x);
+
+    xmpp_stanza_add_child(iq, command);
+    xmpp_stanza_release(command);
+
+    return iq;
+}
+
 static void
 _stanza_add_unique_id(xmpp_stanza_t *stanza, char *prefix)
 {
-    char *id = create_unique_id(prefix);
+    char *id = connection_create_stanza_id(prefix);
     xmpp_stanza_set_id(stanza, id);
     free(id);
 }
+
+static char*
+_stanza_create_sha1_hash(char *str)
+{
+   unsigned char *digest = (unsigned char*)malloc(XMPP_SHA1_DIGEST_SIZE);
+   assert(digest != NULL);
+
+   xmpp_sha1_digest((unsigned char*)str, strlen(str), digest);
+
+   char *b64 = g_base64_encode(digest, XMPP_SHA1_DIGEST_SIZE);
+   assert(b64 != NULL);
+   free(digest);
+
+   return b64;
+}
diff --git a/src/xmpp/stanza.h b/src/xmpp/stanza.h
index bd161616..d3c3c9dc 100644
--- a/src/xmpp/stanza.h
+++ b/src/xmpp/stanza.h
@@ -1,7 +1,7 @@
 /*
  * stanza.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
@@ -99,6 +99,7 @@
 #define STANZA_NAME_PUT "put"
 #define STANZA_NAME_GET "get"
 #define STANZA_NAME_URL "url"
+#define STANZA_NAME_COMMAND "command"
 
 // error conditions
 #define STANZA_NAME_BAD_REQUEST "bad-request"
@@ -156,6 +157,7 @@
 #define STANZA_ATTR_REASON "reason"
 #define STANZA_ATTR_AUTOJOIN "autojoin"
 #define STANZA_ATTR_PASSWORD "password"
+#define STANZA_ATTR_STATUS "status"
 
 #define STANZA_TEXT_AWAY "away"
 #define STANZA_TEXT_DND "dnd"
@@ -186,6 +188,7 @@
 #define STANZA_NS_HTTP_UPLOAD "urn:xmpp:http:upload"
 #define STANZA_NS_X_OOB "jabber:x:oob"
 #define STANZA_NS_BLOCKING "urn:xmpp:blocking"
+#define STANZA_NS_COMMAND "http://jabber.org/protocol/commands"
 
 #define STANZA_DATAFORM_SOFTWARE "urn:xmpp:dataforms:softwareinfo"
 
@@ -278,6 +281,9 @@ xmpp_stanza_t* stanza_create_room_subject_message(xmpp_ctx_t *ctx, const char *c
 xmpp_stanza_t* stanza_create_room_kick_iq(xmpp_ctx_t *const ctx, const char *const room, const char *const nick,
     const char *const reason);
 
+xmpp_stanza_t* stanza_create_command_exec_iq(xmpp_ctx_t *ctx, const char *const target, const char *const node);
+xmpp_stanza_t* stanza_create_command_config_submit_iq(xmpp_ctx_t *ctx, const char *const room, const char *const node, const char *const sessionid, DataForm *form);
+
 int stanza_get_idle_time(xmpp_stanza_t *const stanza);
 
 void stanza_attach_priority(xmpp_ctx_t *const ctx, xmpp_stanza_t *const presence, const int pri);
@@ -292,7 +298,7 @@ EntityCapabilities* stanza_create_caps_from_query_element(xmpp_stanza_t *query);
 
 const char* stanza_get_presence_string_from_type(resource_presence_t presence_type);
 xmpp_stanza_t* stanza_create_software_version_iq(xmpp_ctx_t *ctx, const char *const fulljid);
-xmpp_stanza_t* stanza_create_disco_items_iq(xmpp_ctx_t *ctx, const char *const id, const char *const jid);
+xmpp_stanza_t* stanza_create_disco_items_iq(xmpp_ctx_t *ctx, const char *const id, const char *const jid, const char *const node);
 
 char* stanza_get_status(xmpp_stanza_t *stanza, char *def);
 char* stanza_get_show(xmpp_stanza_t *stanza, char *def);
diff --git a/src/xmpp/xmpp.h b/src/xmpp/xmpp.h
index d4a29196..c9403090 100644
--- a/src/xmpp/xmpp.h
+++ b/src/xmpp/xmpp.h
@@ -1,7 +1,7 @@
 /*
  * xmpp.h
  *
- * Copyright (C) 2012 - 2018 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  *
  * This file is part of Profanity.
  *
@@ -60,6 +60,7 @@
 #define XMPP_FEATURE_RECEIPTS "urn:xmpp:receipts"
 #define XMPP_FEATURE_LASTACTIVITY "jabber:iq:last"
 #define XMPP_FEATURE_MUC "http://jabber.org/protocol/muc"
+#define XMPP_FEATURE_COMMANDS "http://jabber.org/protocol/commands"
 
 typedef enum {
     JABBER_CONNECTING,
@@ -170,8 +171,8 @@ void iq_set_autoping(int seconds);
 void iq_confirm_instant_room(const char *const room_jid);
 void iq_destroy_room(const char *const room_jid);
 void iq_request_room_config_form(const char *const room_jid);
-void iq_submit_room_config(const char *const room, DataForm *form);
-void iq_room_config_cancel(const char *const room_jid);
+void iq_submit_room_config(ProfConfWin *confwin);
+void iq_room_config_cancel(ProfConfWin *confwin);
 void iq_send_ping(const char *const target);
 void iq_room_info_request(const char *const room, gboolean display_result);
 void iq_room_affiliation_list(const char *const room, char *affiliation);
@@ -182,6 +183,8 @@ void iq_room_role_set(const char *const room, const char *const nick, char *role
 void iq_room_role_list(const char * const room, char *role);
 void iq_autoping_check(void);
 void iq_http_upload_request(HTTPUpload *upload);
+void iq_command_list(const char *const target);
+void iq_command_exec(const char *const target, const char *const command);
 
 EntityCapabilities* caps_lookup(const char *const jid);
 void caps_close(void);