about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/command/cmd_ac.c19
-rw-r--r--src/command/cmd_ac.h2
-rw-r--r--src/command/cmd_defs.c50
-rw-r--r--src/command/cmd_defs.h2
-rw-r--r--src/command/cmd_funcs.c304
-rw-r--r--src/command/cmd_funcs.h6
-rw-r--r--src/common.c76
-rw-r--r--src/common.h8
-rw-r--r--src/config/account.c19
-rw-r--r--src/config/account.h5
-rw-r--r--src/config/accounts.c3
-rw-r--r--src/config/accounts.h2
-rw-r--r--src/config/conflists.c2
-rw-r--r--src/config/conflists.h2
-rw-r--r--src/config/files.c2
-rw-r--r--src/config/files.h2
-rw-r--r--src/config/preferences.c12
-rw-r--r--src/config/preferences.h4
-rw-r--r--src/config/scripts.c2
-rw-r--r--src/config/scripts.h2
-rw-r--r--src/config/theme.c4
-rw-r--r--src/config/theme.h2
-rw-r--r--src/config/tlscerts.c2
-rw-r--r--src/config/tlscerts.h2
-rw-r--r--src/event/client_events.c2
-rw-r--r--src/event/client_events.h2
-rw-r--r--src/event/server_events.c16
-rw-r--r--src/event/server_events.h2
-rw-r--r--src/log.c4
-rw-r--r--src/log.h2
-rw-r--r--src/main.c2
-rw-r--r--src/otr/otr.c2
-rw-r--r--src/otr/otr.h2
-rw-r--r--src/otr/otrlib.h2
-rw-r--r--src/otr/otrlibv3.c2
-rw-r--r--src/otr/otrlibv4.c2
-rw-r--r--src/pgp/gpg.c2
-rw-r--r--src/pgp/gpg.h2
-rw-r--r--src/plugins/api.c2
-rw-r--r--src/plugins/api.h2
-rw-r--r--src/plugins/autocompleters.c2
-rw-r--r--src/plugins/autocompleters.h2
-rw-r--r--src/plugins/c_api.c2
-rw-r--r--src/plugins/c_api.h2
-rw-r--r--src/plugins/c_plugins.c2
-rw-r--r--src/plugins/c_plugins.h2
-rw-r--r--src/plugins/callbacks.c2
-rw-r--r--src/plugins/callbacks.h2
-rw-r--r--src/plugins/disco.c2
-rw-r--r--src/plugins/disco.h2
-rw-r--r--src/plugins/plugins.c39
-rw-r--r--src/plugins/plugins.h6
-rw-r--r--src/plugins/profapi.c2
-rw-r--r--src/plugins/profapi.h2
-rw-r--r--src/plugins/python_api.c2
-rw-r--r--src/plugins/python_api.h2
-rw-r--r--src/plugins/python_plugins.c2
-rw-r--r--src/plugins/python_plugins.h2
-rw-r--r--src/plugins/settings.c2
-rw-r--r--src/plugins/settings.h2
-rw-r--r--src/plugins/themes.c2
-rw-r--r--src/plugins/themes.h2
-rw-r--r--src/profanity.c2
-rw-r--r--src/profanity.h2
-rw-r--r--src/tools/autocomplete.c2
-rw-r--r--src/tools/autocomplete.h2
-rw-r--r--src/tools/http_upload.c2
-rw-r--r--src/tools/http_upload.h2
-rw-r--r--src/tools/p_sha1.c403
-rw-r--r--src/tools/p_sha1.h31
-rw-r--r--src/tools/parser.c2
-rw-r--r--src/tools/parser.h2
-rw-r--r--src/tools/tinyurl.c2
-rw-r--r--src/tools/tinyurl.h2
-rw-r--r--src/ui/buffer.c2
-rw-r--r--src/ui/buffer.h2
-rw-r--r--src/ui/chatwin.c2
-rw-r--r--src/ui/confwin.c (renamed from src/ui/mucconfwin.c)34
-rw-r--r--src/ui/console.c26
-rw-r--r--src/ui/core.c22
-rw-r--r--src/ui/inputwin.c13
-rw-r--r--src/ui/inputwin.h2
-rw-r--r--src/ui/mucwin.c2
-rw-r--r--src/ui/notifier.c2
-rw-r--r--src/ui/occupantswin.c2
-rw-r--r--src/ui/privwin.c2
-rw-r--r--src/ui/rosterwin.c2
-rw-r--r--src/ui/screen.c2
-rw-r--r--src/ui/screen.h2
-rw-r--r--src/ui/statusbar.c4
-rw-r--r--src/ui/statusbar.h2
-rw-r--r--src/ui/titlebar.c2
-rw-r--r--src/ui/titlebar.h2
-rw-r--r--src/ui/tray.c2
-rw-r--r--src/ui/tray.h2
-rw-r--r--src/ui/ui.h23
-rw-r--r--src/ui/win_types.h14
-rw-r--r--src/ui/window.c96
-rw-r--r--src/ui/window.h2
-rw-r--r--src/ui/window_list.c20
-rw-r--r--src/ui/window_list.h8
-rw-r--r--src/ui/xmlwin.c2
-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
139 files changed, 1111 insertions, 885 deletions
diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c
index f9d5a22a..58ad758a 100644
--- a/src/command/cmd_ac.c
+++ b/src/command/cmd_ac.c
@@ -1,7 +1,7 @@
 /*
  * cmd_ac.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.
  *
@@ -582,6 +582,7 @@ cmd_ac_init(void)
     tls_property_ac = autocomplete_new();
     autocomplete_add(tls_property_ac, "force");
     autocomplete_add(tls_property_ac, "allow");
+    autocomplete_add(tls_property_ac, "trust");
     autocomplete_add(tls_property_ac, "legacy");
     autocomplete_add(tls_property_ac, "disable");
 
@@ -664,7 +665,7 @@ cmd_ac_init(void)
     autocomplete_add(time_ac, "console");
     autocomplete_add(time_ac, "chat");
     autocomplete_add(time_ac, "muc");
-    autocomplete_add(time_ac, "mucconfig");
+    autocomplete_add(time_ac, "config");
     autocomplete_add(time_ac, "private");
     autocomplete_add(time_ac, "xml");
     autocomplete_add(time_ac, "statusbar");
@@ -1107,8 +1108,8 @@ cmd_ac_reset(ProfWin *window)
         muc_jid_autocomplete_reset(mucwin->roomjid);
     }
 
-    if (window->type == WIN_MUC_CONFIG) {
-        ProfMucConfWin *confwin = (ProfMucConfWin*)window;
+    if (window->type == WIN_CONFIG) {
+        ProfConfWin *confwin = (ProfConfWin*)window;
         assert(confwin->memcheck == PROFCONFWIN_MEMCHECK);
         if (confwin->form) {
             form_reset_autocompleters(confwin->form);
@@ -2349,13 +2350,13 @@ _inpblock_autocomplete(ProfWin *window, const char *const input, gboolean previo
 static char*
 _form_autocomplete(ProfWin *window, const char *const input, gboolean previous)
 {
-    if (window->type != WIN_MUC_CONFIG) {
+    if (window->type != WIN_CONFIG) {
         return NULL;
     }
 
     char *found = NULL;
 
-    ProfMucConfWin *confwin = (ProfMucConfWin*)window;
+    ProfConfWin *confwin = (ProfConfWin*)window;
     DataForm *form = confwin->form;
     if (form) {
         found = autocomplete_param_with_ac(input, "/form help", form->tag_ac, TRUE, previous);
@@ -2375,13 +2376,13 @@ _form_autocomplete(ProfWin *window, const char *const input, gboolean previous)
 static char*
 _form_field_autocomplete(ProfWin *window, const char *const input, gboolean previous)
 {
-    if (window->type != WIN_MUC_CONFIG) {
+    if (window->type != WIN_CONFIG) {
         return NULL;
     }
 
     char *found = NULL;
 
-    ProfMucConfWin *confwin = (ProfMucConfWin*)window;
+    ProfConfWin *confwin = (ProfConfWin*)window;
     DataForm *form = confwin->form;
     if (form == NULL) {
         return NULL;
@@ -2510,7 +2511,7 @@ _time_autocomplete(ProfWin *window, const char *const input, gboolean previous)
         return found;
     }
 
-    found = autocomplete_param_with_ac(input, "/time mucconfig", time_format_ac, TRUE, previous);
+    found = autocomplete_param_with_ac(input, "/time config", time_format_ac, TRUE, previous);
     if (found) {
         return found;
     }
diff --git a/src/command/cmd_ac.h b/src/command/cmd_ac.h
index d7b47eb5..cb9fdee2 100644
--- a/src/command/cmd_ac.h
+++ b/src/command/cmd_ac.h
@@ -1,7 +1,7 @@
 /*
  * cmd_ac.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/command/cmd_defs.c b/src/command/cmd_defs.c
index 418155c4..4447020b 100644
--- a/src/command/cmd_defs.c
+++ b/src/command/cmd_defs.c
@@ -1,7 +1,7 @@
 /*
  * cmd_defs.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.
  *
@@ -158,7 +158,7 @@ static struct cmd_t command_defs[] =
             CMD_TAG_CONNECTION)
         CMD_SYN(
             "/connect [<account>]",
-            "/connect <account> [server <server>] [port <port>] [tls force|allow|legacy|disable]")
+            "/connect <account> [server <server>] [port <port>] [tls force|allow|trust|legacy|disable]")
         CMD_DESC(
             "Login to a chat service. "
             "If no account is specified, the default is used if one is configured. "
@@ -169,6 +169,7 @@ static struct cmd_t command_defs[] =
             { "port <port>",       "The port to use if different to the default (5222, or 5223 for SSL)." },
             { "tls force",         "Force TLS connection, and fail if one cannot be established, this is default behaviour." },
             { "tls allow",         "Use TLS for the connection if it is available." },
+            { "tls trust",         "Force TLS connection and trust server's certificate." },
             { "tls legacy",        "Use legacy TLS for the connection. It means server doesn't support STARTTLS and TLS is forced just after TCP connection is established." },
             { "tls disable",       "Disable TLS for the connection." })
         CMD_EXAMPLES(
@@ -383,7 +384,7 @@ static struct cmd_t command_defs[] =
             "/roster add someone@contacts.org",
             "/roster add someone@contacts.org Buddy",
             "/roster remove someone@contacts.org",
-            "/roster nick myfriend@chat.org My Friend",
+            "/roster nick myfriend@chat.org \"My Friend\"",
             "/roster clearnick kai@server.com",
             "/roster size 15")
     },
@@ -424,7 +425,7 @@ static struct cmd_t command_defs[] =
             "View, add to, and remove from roster groups. "
             "Passing no argument will list all roster groups.")
         CMD_ARGS(
-            { "show <group>",             "List all roster items a group." },
+            { "show <group>",             "List all roster items in a group." },
             { "add <group> <contact>",    "Add a contact to a group." },
             { "remove <group> <contact>", "Remove a contact from a group." })
         CMD_EXAMPLES(
@@ -1265,8 +1266,8 @@ static struct cmd_t command_defs[] =
         CMD_TAGS(
             CMD_TAG_UI)
         CMD_SYN(
-            "/time console|chat|muc|mucconfig|private|xml set <format>",
-            "/time console|chat|muc|mucconfig|private|xml off",
+            "/time console|chat|muc|config|private|xml set <format>",
+            "/time console|chat|muc|config|private|xml off",
             "/time statusbar set <format>",
             "/time statusbar off",
             "/time lastactivity set <format>")
@@ -1283,8 +1284,8 @@ static struct cmd_t command_defs[] =
             { "chat off",                  "Do not show time in chat windows." },
             { "muc set <format>",          "Set time format for chat room windows." },
             { "muc off",                   "Do not show time in chat room windows." },
-            { "mucconfig set <format>",    "Set time format for chat room config windows." },
-            { "mucconfig off",             "Do not show time in chat room config windows." },
+            { "config set <format>",       "Set time format for config windows." },
+            { "config off",                "Do not show time in config windows." },
             { "private set <format>",      "Set time format for private chat windows." },
             { "private off",               "Do not show time in private chat windows." },
             { "xml set <format>",          "Set time format for XML console window." },
@@ -1934,7 +1935,7 @@ static struct cmd_t command_defs[] =
             "/autoaway message away|xa <message>|off",
             "/autoaway check on|off")
         CMD_DESC(
-            "Manage autoway settings for idle time.")
+            "Manage autoaway settings for idle time.")
         CMD_ARGS(
             { "mode idle",              "Sends idle time, status remains online." },
             { "mode away",              "Sends away and xa presence as well as idle time." },
@@ -2014,7 +2015,7 @@ static struct cmd_t command_defs[] =
             "/account set <account> otr <policy>",
             "/account set <account> pgpkeyid <pgpkeyid>",
             "/account set <account> startscript <script>",
-            "/account set <account> tls force|allow|legacy|disable",
+            "/account set <account> tls force|allow|trust|legacy|disable",
             "/account set <account> theme <theme>",
             "/account clear <account> password",
             "/account clear <account> eval_password",
@@ -2054,6 +2055,7 @@ static struct cmd_t command_defs[] =
             { "set <account> startscript <script>",     "Set the script to execute after connecting." },
             { "set <account> tls force",                "Force TLS connection, and fail if one cannot be established, this is default behaviour." },
             { "set <account> tls allow",                "Use TLS for the connection if it is available." },
+            { "set <account> tls trust",                "Force TLS connection and trust server's certificate." },
             { "set <account> tls legacy",               "Use legacy TLS for the connection. It means server doesn't support STARTTLS and TLS is forced just after TCP connection is established." },
             { "set <account> tls disable",              "Disable TLS for the connection." },
             { "set <account> <theme>",                  "Set the UI theme for the account." },
@@ -2084,6 +2086,8 @@ static struct cmd_t command_defs[] =
         CMD_SUBFUNCS(
             { "sourcepath",     cmd_plugins_sourcepath },
             { "install",        cmd_plugins_install },
+            { "uninstall",      cmd_plugins_uninstall },
+            { "update",         cmd_plugins_update },
             { "load",           cmd_plugins_load },
             { "unload",         cmd_plugins_unload },
             { "reload",         cmd_plugins_reload },
@@ -2095,6 +2099,8 @@ static struct cmd_t command_defs[] =
             "/plugins sourcepath set <path>",
             "/plugins sourcepath clear",
             "/plugins install [<path>]",
+            "/plugins uninstall [<plugin>]",
+            "/plugins update [<path>]",
             "/plugins unload [<plugin>]",
             "/plugins load [<plugin>]",
             "/plugins reload [<plugin>]",
@@ -2105,6 +2111,8 @@ static struct cmd_t command_defs[] =
             { "sourcepath set <path>",  "Set the default path to install plugins from, will be used if no arg is passed to /plugins install." },
             { "sourcepath clear",       "Clear the default plugins source path." },
             { "install [<path>]",       "Install a plugin, or all plugins found in a directory (recursive). Passing no argument will use the sourcepath if one is set." },
+            { "uninstall [<plugin>]",   "Uninstall a plugin." },
+            { "update [<path>]",        "Updates an installed plugin" },
             { "load [<plugin>]",        "Load a plugin that already exists in the plugin directory, passing no argument loads all found plugins." },
             { "unload [<plugin>]",      "Unload a loaded plugin, passing no argument will unload all plugins." },
             { "reload [<plugin>]",      "Reload a plugin, passing no argument will reload all plugins." },
@@ -2113,6 +2121,8 @@ static struct cmd_t command_defs[] =
             "/plugins sourcepath set /home/meee/projects/profanity-plugins",
             "/plugins install",
             "/plugins install /home/steveharris/Downloads/metal.py",
+            "/plugins update /home/steveharris/Downloads/metal.py",
+            "/plugins uninstall browser.py",
             "/plugins load browser.py",
             "/plugins unload say.py",
             "/plugins reload wikipedia.py")
@@ -2298,6 +2308,26 @@ static struct cmd_t command_defs[] =
         CMD_EXAMPLES(
             "/export /path/to/output.csv",
             "/export ~/contacts.csv")
+    },
+
+    { "/cmd",
+        parse_args, 1, 3, NULL,
+        CMD_SUBFUNCS(
+            { "list", cmd_command_list },
+            { "exec", cmd_command_exec })
+        CMD_NOMAINFUNC
+        CMD_NOTAGS
+        CMD_SYN(
+            "/cmd list [<jid>]",
+            "/cmd exec <command> [<jid>]")
+        CMD_DESC(
+            "Execute ad hoc commands.")
+        CMD_ARGS(
+            { "list",           "List supported ad hoc commands." },
+            { "exec <command>", "Execute a command." })
+        CMD_EXAMPLES(
+            "/cmd list",
+            "/cmd exec ping")
     }
 };
 
diff --git a/src/command/cmd_defs.h b/src/command/cmd_defs.h
index d318e7ed..e6ce1053 100644
--- a/src/command/cmd_defs.h
+++ b/src/command/cmd_defs.h
@@ -1,7 +1,7 @@
 /*
  * cmd_defs.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/command/cmd_funcs.c b/src/command/cmd_funcs.c
index c8aa22b4..6cdd40fb 100644
--- a/src/command/cmd_funcs.c
+++ b/src/command/cmd_funcs.c
@@ -1,7 +1,7 @@
 /*
  * cmd_funcs.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.
  *
@@ -70,6 +70,7 @@
 #include "ui/ui.h"
 #include "ui/window_list.h"
 #include "xmpp/xmpp.h"
+#include "xmpp/connection.h"
 #include "xmpp/contact.h"
 #include "xmpp/roster_list.h"
 #include "xmpp/jid.h"
@@ -204,7 +205,7 @@ cmd_tls_trust(ProfWin *window, const char *const command, gchar **args)
 #ifdef HAVE_LIBMESODE
     jabber_conn_status_t conn_status = connection_get_status();
     if (conn_status != JABBER_CONNECTED) {
-        cons_show("You are not currently connected.");
+        cons_show("You are currently not connected.");
         return TRUE;
     }
     if (!connection_is_secured()) {
@@ -350,6 +351,7 @@ cmd_connect(ProfWin *window, const char *const command, gchar **args)
     if (tls_policy &&
             (g_strcmp0(tls_policy, "force") != 0) &&
             (g_strcmp0(tls_policy, "allow") != 0) &&
+            (g_strcmp0(tls_policy, "trust") != 0) &&
             (g_strcmp0(tls_policy, "disable") != 0) &&
             (g_strcmp0(tls_policy, "legacy") != 0)) {
         cons_bad_cmd_usage(command);
@@ -391,6 +393,14 @@ cmd_connect(ProfWin *window, const char *const command, gchar **args)
     // connect with account
     ProfAccount *account = accounts_get_account(lower);
     if (account) {
+        // override account options with connect options
+        if (altdomain != NULL)
+            account_set_server(account, altdomain);
+        if (port != 0)
+            account_set_port(account, port);
+        if (tls_policy != NULL)
+            account_set_tls_policy(account, tls_policy);
+
         // use password if set
         if (account->password) {
             conn_status = cl_ev_connect_account(account);
@@ -814,6 +824,7 @@ _account_set_tls(char *account_name, char *policy)
 {
     if ((g_strcmp0(policy, "force") != 0)
             && (g_strcmp0(policy, "allow") != 0)
+            && (g_strcmp0(policy, "trust") != 0)
             && (g_strcmp0(policy, "disable") != 0)
             && (g_strcmp0(policy, "legacy") != 0)) {
         cons_show("TLS policy must be one of: force, allow, legacy or disable.");
@@ -3632,11 +3643,11 @@ cmd_decline(ProfWin *window, const char *const command, gchar **args)
 gboolean
 cmd_form_field(ProfWin *window, char *tag, gchar **args)
 {
-    if (window->type != WIN_MUC_CONFIG) {
+    if (window->type != WIN_CONFIG) {
         return TRUE;
     }
 
-    ProfMucConfWin *confwin = (ProfMucConfWin*)window;
+    ProfConfWin *confwin = (ProfConfWin*)window;
     DataForm *form = confwin->form;
     if (form) {
         if (!form_tag_exists(form, tag)) {
@@ -3657,14 +3668,14 @@ cmd_form_field(ProfWin *window, char *tag, gchar **args)
             if (g_strcmp0(value, "on") == 0) {
                 form_set_value(form, tag, "1");
                 win_println(window, THEME_DEFAULT, '-', "Field updated...");
-                mucconfwin_show_form_field(confwin, form, tag);
+                confwin_show_form_field(confwin, form, tag);
             } else if (g_strcmp0(value, "off") == 0) {
                 form_set_value(form, tag, "0");
                 win_println(window, THEME_DEFAULT, '-', "Field updated...");
-                mucconfwin_show_form_field(confwin, form, tag);
+                confwin_show_form_field(confwin, form, tag);
             } else {
                 win_println(window, THEME_DEFAULT, '-', "Invalid command, usage:");
-                mucconfwin_field_help(confwin, tag);
+                confwin_field_help(confwin, tag);
                 win_println(window, THEME_DEFAULT, '-', "");
             }
             break;
@@ -3675,24 +3686,24 @@ cmd_form_field(ProfWin *window, char *tag, gchar **args)
             value = args[0];
             if (value == NULL) {
                 win_println(window, THEME_DEFAULT, '-', "Invalid command, usage:");
-                mucconfwin_field_help(confwin, tag);
+                confwin_field_help(confwin, tag);
                 win_println(window, THEME_DEFAULT, '-', "");
             } else {
                 form_set_value(form, tag, value);
                 win_println(window, THEME_DEFAULT, '-', "Field updated...");
-                mucconfwin_show_form_field(confwin, form, tag);
+                confwin_show_form_field(confwin, form, tag);
             }
             break;
         case FIELD_LIST_SINGLE:
             value = args[0];
             if ((value == NULL) || !form_field_contains_option(form, tag, value)) {
                 win_println(window, THEME_DEFAULT, '-', "Invalid command, usage:");
-                mucconfwin_field_help(confwin, tag);
+                confwin_field_help(confwin, tag);
                 win_println(window, THEME_DEFAULT, '-', "");
             } else {
                 form_set_value(form, tag, value);
                 win_println(window, THEME_DEFAULT, '-', "Field updated...");
-                mucconfwin_show_form_field(confwin, form, tag);
+                confwin_show_form_field(confwin, form, tag);
             }
             break;
 
@@ -3703,32 +3714,32 @@ cmd_form_field(ProfWin *window, char *tag, gchar **args)
             }
             if ((g_strcmp0(cmd, "add") != 0) && (g_strcmp0(cmd, "remove"))) {
                 win_println(window, THEME_DEFAULT, '-', "Invalid command, usage:");
-                mucconfwin_field_help(confwin, tag);
+                confwin_field_help(confwin, tag);
                 win_println(window, THEME_DEFAULT, '-', "");
                 break;
             }
             if (value == NULL) {
                 win_println(window, THEME_DEFAULT, '-', "Invalid command, usage:");
-                mucconfwin_field_help(confwin, tag);
+                confwin_field_help(confwin, tag);
                 win_println(window, THEME_DEFAULT, '-', "");
                 break;
             }
             if (g_strcmp0(cmd, "add") == 0) {
                 form_add_value(form, tag, value);
                 win_println(window, THEME_DEFAULT, '-', "Field updated...");
-                mucconfwin_show_form_field(confwin, form, tag);
+                confwin_show_form_field(confwin, form, tag);
                 break;
             }
             if (g_strcmp0(args[0], "remove") == 0) {
                 if (!g_str_has_prefix(value, "val")) {
                     win_println(window, THEME_DEFAULT, '-', "Invalid command, usage:");
-                    mucconfwin_field_help(confwin, tag);
+                    confwin_field_help(confwin, tag);
                     win_println(window, THEME_DEFAULT, '-', "");
                     break;
                 }
                 if (strlen(value) < 4) {
                     win_println(window, THEME_DEFAULT, '-', "Invalid command, usage:");
-                    mucconfwin_field_help(confwin, tag);
+                    confwin_field_help(confwin, tag);
                     win_println(window, THEME_DEFAULT, '-', "");
                     break;
                 }
@@ -3736,7 +3747,7 @@ cmd_form_field(ProfWin *window, char *tag, gchar **args)
                 int index = strtol(&value[3], NULL, 10);
                 if ((index < 1) || (index > form_get_value_count(form, tag))) {
                     win_println(window, THEME_DEFAULT, '-', "Invalid command, usage:");
-                    mucconfwin_field_help(confwin, tag);
+                    confwin_field_help(confwin, tag);
                     win_println(window, THEME_DEFAULT, '-', "");
                     break;
                 }
@@ -3744,7 +3755,7 @@ cmd_form_field(ProfWin *window, char *tag, gchar **args)
                 removed = form_remove_text_multi_value(form, tag, index);
                 if (removed) {
                     win_println(window, THEME_DEFAULT, '-', "Field updated...");
-                    mucconfwin_show_form_field(confwin, form, tag);
+                    confwin_show_form_field(confwin, form, tag);
                 } else {
                     win_println(window, THEME_DEFAULT, '-', "Could not remove %s from %s", value, tag);
                 }
@@ -3757,13 +3768,13 @@ cmd_form_field(ProfWin *window, char *tag, gchar **args)
             }
             if ((g_strcmp0(cmd, "add") != 0) && (g_strcmp0(cmd, "remove"))) {
                 win_println(window, THEME_DEFAULT, '-', "Invalid command, usage:");
-                mucconfwin_field_help(confwin, tag);
+                confwin_field_help(confwin, tag);
                 win_println(window, THEME_DEFAULT, '-', "");
                 break;
             }
             if (value == NULL) {
                 win_println(window, THEME_DEFAULT, '-', "Invalid command, usage:");
-                mucconfwin_field_help(confwin, tag);
+                confwin_field_help(confwin, tag);
                 win_println(window, THEME_DEFAULT, '-', "");
                 break;
             }
@@ -3773,13 +3784,13 @@ cmd_form_field(ProfWin *window, char *tag, gchar **args)
                     added = form_add_unique_value(form, tag, value);
                     if (added) {
                         win_println(window, THEME_DEFAULT, '-', "Field updated...");
-                        mucconfwin_show_form_field(confwin, form, tag);
+                        confwin_show_form_field(confwin, form, tag);
                     } else {
                         win_println(window, THEME_DEFAULT, '-', "Value %s already selected for %s", value, tag);
                     }
                 } else {
                     win_println(window, THEME_DEFAULT, '-', "Invalid command, usage:");
-                    mucconfwin_field_help(confwin, tag);
+                    confwin_field_help(confwin, tag);
                     win_println(window, THEME_DEFAULT, '-', "");
                 }
                 break;
@@ -3790,13 +3801,13 @@ cmd_form_field(ProfWin *window, char *tag, gchar **args)
                     removed = form_remove_value(form, tag, value);
                     if (removed) {
                         win_println(window, THEME_DEFAULT, '-', "Field updated...");
-                        mucconfwin_show_form_field(confwin, form, tag);
+                        confwin_show_form_field(confwin, form, tag);
                     } else {
                         win_println(window, THEME_DEFAULT, '-', "Value %s is not currently set for %s", value, tag);
                     }
                 } else {
                     win_println(window, THEME_DEFAULT, '-', "Invalid command, usage:");
-                    mucconfwin_field_help(confwin, tag);
+                    confwin_field_help(confwin, tag);
                     win_println(window, THEME_DEFAULT, '-', "");
                 }
             }
@@ -3808,13 +3819,13 @@ cmd_form_field(ProfWin *window, char *tag, gchar **args)
             }
             if ((g_strcmp0(cmd, "add") != 0) && (g_strcmp0(cmd, "remove"))) {
                 win_println(window, THEME_DEFAULT, '-', "Invalid command, usage:");
-                mucconfwin_field_help(confwin, tag);
+                confwin_field_help(confwin, tag);
                 win_println(window, THEME_DEFAULT, '-', "");
                 break;
             }
             if (value == NULL) {
                 win_println(window, THEME_DEFAULT, '-', "Invalid command, usage:");
-                mucconfwin_field_help(confwin, tag);
+                confwin_field_help(confwin, tag);
                 win_println(window, THEME_DEFAULT, '-', "");
                 break;
             }
@@ -3822,7 +3833,7 @@ cmd_form_field(ProfWin *window, char *tag, gchar **args)
                 added = form_add_unique_value(form, tag, value);
                 if (added) {
                     win_println(window, THEME_DEFAULT, '-', "Field updated...");
-                    mucconfwin_show_form_field(confwin, form, tag);
+                    confwin_show_form_field(confwin, form, tag);
                 } else {
                     win_println(window, THEME_DEFAULT, '-', "JID %s already exists in %s", value, tag);
                 }
@@ -3832,7 +3843,7 @@ cmd_form_field(ProfWin *window, char *tag, gchar **args)
                 removed = form_remove_value(form, tag, value);
                 if (removed) {
                     win_println(window, THEME_DEFAULT, '-', "Field updated...");
-                    mucconfwin_show_form_field(confwin, form, tag);
+                    confwin_show_form_field(confwin, form, tag);
                 } else {
                     win_println(window, THEME_DEFAULT, '-', "Field %s does not contain %s", tag, value);
                 }
@@ -3857,7 +3868,7 @@ cmd_form(ProfWin *window, const char *const command, gchar **args)
         return TRUE;
     }
 
-    if (window->type != WIN_MUC_CONFIG) {
+    if (window->type != WIN_CONFIG) {
         cons_show("Command '/form' does not apply to this window.");
         return TRUE;
     }
@@ -3870,20 +3881,20 @@ cmd_form(ProfWin *window, const char *const command, gchar **args)
         return TRUE;
     }
 
-    ProfMucConfWin *confwin = (ProfMucConfWin*)window;
+    ProfConfWin *confwin = (ProfConfWin*)window;
     assert(confwin->memcheck == PROFCONFWIN_MEMCHECK);
 
     if (g_strcmp0(args[0], "show") == 0) {
-        mucconfwin_show_form(confwin);
+        confwin_show_form(confwin);
         return TRUE;
     }
 
     if (g_strcmp0(args[0], "help") == 0) {
         char *tag = args[1];
         if (tag) {
-            mucconfwin_field_help(confwin, tag);
+            confwin_field_help(confwin, tag);
         } else {
-            mucconfwin_form_help(confwin);
+            confwin_form_help(confwin);
 
             gchar **help_text = NULL;
             Command *command = cmd_get("/form");
@@ -3898,12 +3909,12 @@ cmd_form(ProfWin *window, const char *const command, gchar **args)
         return TRUE;
     }
 
-    if (g_strcmp0(args[0], "submit") == 0) {
-        iq_submit_room_config(confwin->roomjid, confwin->form);
+    if (g_strcmp0(args[0], "submit") == 0 && confwin->submit != NULL) {
+        confwin->submit(confwin);
     }
 
-    if (g_strcmp0(args[0], "cancel") == 0) {
-        iq_room_config_cancel(confwin->roomjid);
+    if (g_strcmp0(args[0], "cancel") == 0 && confwin->cancel != NULL) {
+        confwin->cancel(confwin);
     }
 
     if ((g_strcmp0(args[0], "submit") == 0) || (g_strcmp0(args[0], "cancel") == 0)) {
@@ -4264,7 +4275,7 @@ cmd_room(ProfWin *window, const char *const command, gchar **args)
     }
 
     if (g_strcmp0(args[0], "config") == 0) {
-        ProfMucConfWin *confwin = wins_get_muc_conf(mucwin->roomjid);
+        ProfConfWin *confwin = wins_get_conf(mucwin->roomjid);
 
         if (confwin) {
             ui_focus_win((ProfWin*)confwin);
@@ -5159,20 +5170,20 @@ cmd_time(ProfWin *window, const char *const command, gchar **args)
             cons_bad_cmd_usage(command);
             return TRUE;
         }
-    } else if (g_strcmp0(args[0], "mucconfig") == 0) {
+    } else if (g_strcmp0(args[0], "config") == 0) {
         if (args[1] == NULL) {
-            char *format = prefs_get_string(PREF_TIME_MUCCONFIG);
-            cons_show("MUC config time format: '%s'.", format);
+            char *format = prefs_get_string(PREF_TIME_CONFIG);
+            cons_show("config time format: '%s'.", format);
             prefs_free_string(format);
             return TRUE;
         } else if (g_strcmp0(args[1], "set") == 0 && args[2] != NULL) {
-            prefs_set_string(PREF_TIME_MUCCONFIG, args[2]);
-            cons_show("MUC config time format set to '%s'.", args[2]);
+            prefs_set_string(PREF_TIME_CONFIG, args[2]);
+            cons_show("config time format set to '%s'.", args[2]);
             wins_resize_all();
             return TRUE;
         } else if (g_strcmp0(args[1], "off") == 0) {
-            prefs_set_string(PREF_TIME_MUCCONFIG, "off");
-            cons_show("MUC config time display disabled.");
+            prefs_set_string(PREF_TIME_CONFIG, "off");
+            cons_show("config time display disabled.");
             wins_resize_all();
             return TRUE;
         } else {
@@ -6616,15 +6627,16 @@ cmd_plugins_install(ProfWin *window, const char *const command, gchar **args)
             return TRUE;
         }
 
+        GString* error_message = g_string_new(NULL);
         gchar *plugin_name = g_path_get_basename(path);
-        gboolean result = plugins_install(plugin_name, path);
+        gboolean result = plugins_install(plugin_name, path, error_message);
         if (result) {
             cons_show("Plugin installed: %s", plugin_name);
         } else {
-            cons_show("Failed to install plugin: %s", plugin_name);
+            cons_show("Failed to install plugin: %s. %s", plugin_name, error_message->str);
         }
         g_free(plugin_name);
-
+        g_string_free(error_message, TRUE);
         free(path);
         return TRUE;
     }
@@ -6663,6 +6675,88 @@ cmd_plugins_install(ProfWin *window, const char *const command, gchar **args)
 }
 
 gboolean
+cmd_plugins_update(ProfWin *window, const char *const command, gchar **args)
+{
+    char *path = args[1];
+    if (path == NULL) {
+        char* sourcepath = prefs_get_string(PREF_PLUGINS_SOURCEPATH);
+        if (sourcepath) {
+            path = strdup(sourcepath);
+            prefs_free_string(sourcepath);
+        } else {
+            cons_show("Either a path must be provided or the sourcepath property must be set, see /help plugins");
+            return TRUE;
+        }
+    } else if (path[0] == '~' && path[1] == '/') {
+        if (asprintf(&path, "%s/%s", getenv("HOME"), path+2) == -1) {
+            return TRUE;
+        }
+    } else {
+        path = strdup(path);
+    }
+
+    if (access(path, R_OK) != 0) {
+        cons_show("File not found: %s", path);
+        free(path);
+        return TRUE;
+    }
+
+    if (is_regular_file(path)) {
+        if (!g_str_has_suffix(path, ".py") && !g_str_has_suffix(path, ".so")) {
+            cons_show("Plugins must have one of the following extensions: '.py' '.so'");
+            free(path);
+            return TRUE;
+        }
+
+        GString* error_message = g_string_new(NULL);
+        gchar *plugin_name = g_path_get_basename(path);
+        if (plugins_unload(plugin_name)) {
+            if (plugins_uninstall(plugin_name)) {
+                if (plugins_install(plugin_name, path, error_message)) {
+                    cons_show("Plugin installed: %s", plugin_name);
+                } else {
+                    cons_show("Failed to install plugin: %s. %s", plugin_name, error_message->str);
+                }
+            } else {
+                cons_show("Failed to uninstall plugin: %s.", plugin_name);
+            }
+        } else {
+            cons_show("Failed to unload plugin: %s.", plugin_name);
+        }
+        g_free(plugin_name);
+        g_string_free(error_message, TRUE);
+        free(path);
+        return TRUE;
+    }
+
+    if (is_dir(path)) {
+        free(path);
+        return FALSE;
+    }
+
+    free(path);
+    cons_show("Argument must be a file or directory.");
+    return TRUE;
+}
+
+gboolean
+cmd_plugins_uninstall(ProfWin *window, const char *const command, gchar **args)
+{
+    if (args[1] == NULL) {
+        return FALSE;
+    }
+
+    gboolean res = plugins_uninstall(args[1]);
+    if (res) {
+        cons_show("Uninstalled plugin: %s", args[1]);
+    } else {
+        cons_show("Failed to uninstall plugin: %s", args[1]);
+    }
+
+    return TRUE;
+}
+
+gboolean
 cmd_plugins_load(ProfWin *window, const char *const command, gchar **args)
 {
     if (args[1] == NULL) {
@@ -7469,10 +7563,122 @@ cmd_encwarn(ProfWin *window, const char *const command, gchar **args)
     return TRUE;
 }
 
+gboolean
+cmd_command_list(ProfWin *window, const char *const command, gchar **args)
+{
+    jabber_conn_status_t conn_status = connection_get_status();
+
+    if (conn_status != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        return TRUE;
+    }
+
+    if (connection_supports(XMPP_FEATURE_COMMANDS) == FALSE) {
+        cons_show("Server does not support ad hoc commands.");
+        return TRUE;
+    }
+
+    char *jid = args[1];
+    if (jid == NULL) {
+        switch (window->type) {
+        case WIN_MUC:
+        {
+            ProfMucWin *mucwin = (ProfMucWin*)window;
+            assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
+            jid = mucwin->roomjid;
+            break;
+        }
+        case WIN_CHAT:
+        {
+            ProfChatWin *chatwin = (ProfChatWin*)window;
+            assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
+            jid = chatwin->barejid;
+            break;
+        }
+        case WIN_PRIVATE:
+        {
+            ProfPrivateWin *privatewin = (ProfPrivateWin*)window;
+            assert(privatewin->memcheck == PROFPRIVATEWIN_MEMCHECK);
+            jid = privatewin->fulljid;
+            break;
+        }
+        case WIN_CONSOLE:
+        {
+            jid = connection_get_domain();
+            break;
+        }
+        default:
+            cons_show("Cannot send ad hoc commands.");
+            return TRUE;
+        }
+    }
+
+    iq_command_list(jid);
+
+    cons_show("List available ad hoc commands");
+    return TRUE;
+}
+
+gboolean
+cmd_command_exec(ProfWin *window, const char *const command, gchar **args)
+{
+    jabber_conn_status_t conn_status = connection_get_status();
+
+    if (conn_status != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        return TRUE;
+    }
+
+    if (connection_supports(XMPP_FEATURE_COMMANDS) == FALSE) {
+        cons_show("Server does not support ad hoc commands.");
+        return TRUE;
+    }
+
+    char *jid = args[2];
+    if (jid == NULL) {
+        switch (window->type) {
+        case WIN_MUC:
+        {
+            ProfMucWin *mucwin = (ProfMucWin*)window;
+            assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
+            jid = mucwin->roomjid;
+            break;
+        }
+        case WIN_CHAT:
+        {
+            ProfChatWin *chatwin = (ProfChatWin*)window;
+            assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
+            jid = chatwin->barejid;
+            break;
+        }
+        case WIN_PRIVATE:
+        {
+            ProfPrivateWin *privatewin = (ProfPrivateWin*)window;
+            assert(privatewin->memcheck == PROFPRIVATEWIN_MEMCHECK);
+            jid = privatewin->fulljid;
+            break;
+        }
+        case WIN_CONSOLE:
+        {
+            jid = connection_get_domain();
+            break;
+        }
+        default:
+            cons_show("Cannot send ad hoc commands.");
+            return TRUE;
+        }
+    }
+
+    iq_command_exec(jid, args[1]);
+
+    cons_show("Execute %s...", args[1]);
+    return TRUE;
+}
+
 static gboolean
 _cmd_execute(ProfWin *window, const char *const command, const char *const inp)
 {
-    if (g_str_has_prefix(command, "/field") && window->type == WIN_MUC_CONFIG) {
+    if (g_str_has_prefix(command, "/field") && window->type == WIN_CONFIG) {
         gboolean result = FALSE;
         gchar **args = parse_args_with_freetext(inp, 1, 2, &result);
         if (!result) {
diff --git a/src/command/cmd_funcs.h b/src/command/cmd_funcs.h
index 0bbf338e..89166ba1 100644
--- a/src/command/cmd_funcs.h
+++ b/src/command/cmd_funcs.h
@@ -1,7 +1,7 @@
 /*
  * cmd_funcs.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.
  *
@@ -158,10 +158,14 @@ gboolean cmd_script(ProfWin *window, const char *const command, gchar **args);
 gboolean cmd_export(ProfWin *window, const char *const command, gchar **args);
 gboolean cmd_charset(ProfWin *window, const char *const command, gchar **args);
 gboolean cmd_console(ProfWin *window, const char *const command, gchar **args);
+gboolean cmd_command_list(ProfWin *window, const char *const command, gchar **args);
+gboolean cmd_command_exec(ProfWin *window, const char *const command, gchar **args);
 
 gboolean cmd_plugins(ProfWin *window, const char *const command, gchar **args);
 gboolean cmd_plugins_sourcepath(ProfWin *window, const char *const command, gchar **args);
 gboolean cmd_plugins_install(ProfWin *window, const char *const command, gchar **args);
+gboolean cmd_plugins_update(ProfWin *window, const char *const command, gchar **args);
+gboolean cmd_plugins_uninstall(ProfWin *window, const char *const command, gchar **args);
 gboolean cmd_plugins_load(ProfWin *window, const char *const command, gchar **args);
 gboolean cmd_plugins_unload(ProfWin *window, const char *const command, gchar **args);
 gboolean cmd_plugins_reload(ProfWin *window, const char *const command, gchar **args);
diff --git a/src/common.c b/src/common.c
index a3893c06..3e1b51b5 100644
--- a/src/common.c
+++ b/src/common.c
@@ -1,7 +1,7 @@
 /*
  * common.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.
  *
@@ -46,6 +46,7 @@
 #include <curl/curl.h>
 #include <curl/easy.h>
 #include <glib.h>
+#include <gio/gio.h>
 
 #ifdef HAVE_NCURSESW_NCURSES_H
 #include <ncursesw/ncurses.h>
@@ -55,7 +56,6 @@
 
 #include "log.h"
 #include "common.h"
-#include "tools/p_sha1.h"
 
 struct curl_data_t
 {
@@ -63,8 +63,6 @@ struct curl_data_t
     size_t size;
 };
 
-static unsigned long unique_id = 0;
-
 static size_t _data_callback(void *ptr, size_t size, size_t nmemb, void *data);
 
 gboolean
@@ -107,28 +105,16 @@ mkdir_recursive(const char *dir)
 }
 
 gboolean
-copy_file(const char *const sourcepath, const char *const targetpath)
+copy_file(const char *const sourcepath, const char *const targetpath, const gboolean overwrite_existing)
 {
-    int ch;
-    FILE *source = fopen(sourcepath, "rb");
-    if (source == NULL) {
-        return FALSE;
-    }
-
-    FILE *target = fopen(targetpath, "wb");
-    if (target == NULL) {
-        fclose(source);
-        return FALSE;
-    }
-
-    while((ch = fgetc(source)) != EOF) {
-        fputc(ch, target);
-    }
-
-    fclose(source);
-    fclose(target);
-
-    return TRUE;
+    GFile *source = g_file_new_for_path(sourcepath);
+    GFile *dest = g_file_new_for_path(targetpath);
+    GError *error = NULL;
+    GFileCopyFlags flags = overwrite_existing ? G_FILE_COPY_OVERWRITE : G_FILE_COPY_NONE;
+    gboolean success = g_file_copy (source, dest, flags, NULL, NULL, NULL, &error);
+    g_object_unref(source);
+    g_object_unref(dest);
+    return success;
 }
 
 char*
@@ -332,46 +318,6 @@ release_is_new(char *found_version)
     }
 }
 
-char*
-create_unique_id(char *prefix)
-{
-    char *result = NULL;
-    GString *result_str = g_string_new("");
-
-    unique_id++;
-    if (prefix) {
-        g_string_printf(result_str, "prof_%s_%lu", prefix, unique_id);
-    } else {
-        g_string_printf(result_str, "prof_%lu", unique_id);
-    }
-    result = result_str->str;
-    g_string_free(result_str, FALSE);
-
-    return result;
-}
-
-void
-reset_unique_id(void)
-{
-    unique_id = 0;
-}
-
-char*
-p_sha1_hash(char *str)
-{
-    P_SHA1_CTX ctx;
-    uint8_t digest[20];
-    uint8_t *input = (uint8_t*)malloc(strlen(str) + 1);
-    memcpy(input, str, strlen(str) + 1);
-
-    P_SHA1_Init(&ctx);
-    P_SHA1_Update(&ctx, input, strlen(str));
-    P_SHA1_Final(&ctx, digest);
-
-    free(input);
-    return g_base64_encode(digest, sizeof(digest));
-}
-
 static size_t
 _data_callback(void *ptr, size_t size, size_t nmemb, void *data)
 {
diff --git a/src/common.h b/src/common.h
index a53fdf9c..6d4ca39c 100644
--- a/src/common.h
+++ b/src/common.h
@@ -1,7 +1,7 @@
 /*
  * common.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.
  *
@@ -82,7 +82,7 @@ typedef enum {
 
 gboolean create_dir(char *name);
 gboolean mkdir_recursive(const char *dir);
-gboolean copy_file(const char *const src, const char *const target);
+gboolean copy_file(const char *const src, const char *const target, const gboolean overwrite_existing);
 char* str_replace(const char *string, const char *substr, const char *replacement);
 int str_contains(const char str[], int size, char ch);
 gboolean strtoi_range(char *str, int *saveptr, int min, int max, char **err_msg);
@@ -92,10 +92,6 @@ char* file_getline(FILE *stream);
 char* release_get_latest(void);
 gboolean release_is_new(char *found_version);
 
-char* p_sha1_hash(char *str);
-char* create_unique_id(char *prefix);
-void reset_unique_id(void);
-
 char* get_file_or_linked(char *loc, char *basedir);
 char* strip_arg_quotes(const char *const input);
 gboolean is_notify_enabled(void);
diff --git a/src/config/account.c b/src/config/account.c
index 705a5edf..93ba5078 100644
--- a/src/config/account.c
+++ b/src/config/account.c
@@ -1,7 +1,7 @@
 /*
  * account.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.
  *
@@ -241,3 +241,20 @@ account_free(ProfAccount *account)
     g_list_free_full(account->otr_always, g_free);
     free(account);
 }
+
+void account_set_server(ProfAccount *account, const char *server)
+{
+    free(account->server);
+    account->server = strdup(server);
+}
+
+void account_set_port(ProfAccount *account, int port)
+{
+    account->port = port;
+}
+
+void account_set_tls_policy(ProfAccount *account, const char *tls_policy)
+{
+    free(account->tls_policy);
+    account->tls_policy = strdup(tls_policy);
+}
diff --git a/src/config/account.h b/src/config/account.h
index 1262e518..68264c47 100644
--- a/src/config/account.h
+++ b/src/config/account.h
@@ -1,7 +1,7 @@
 /*
  * account.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.
  *
@@ -77,5 +77,8 @@ ProfAccount* account_new(const gchar *const name, const gchar *const jid,
 char* account_create_connect_jid(ProfAccount *account);
 gboolean account_eval_password(ProfAccount *account);
 void account_free(ProfAccount *account);
+void account_set_server(ProfAccount *account, const char *server);
+void account_set_port(ProfAccount *account, int port);
+void account_set_tls_policy(ProfAccount *account, const char *tls_policy);
 
 #endif
diff --git a/src/config/accounts.c b/src/config/accounts.c
index de898dd7..f53f53c9 100644
--- a/src/config/accounts.c
+++ b/src/config/accounts.c
@@ -1,7 +1,7 @@
 /*
  * accounts.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.
  *
@@ -291,6 +291,7 @@ accounts_get_account(const char *const name)
         gchar *tls_policy = g_key_file_get_string(accounts, name, "tls.policy", NULL);
         if (tls_policy && ((g_strcmp0(tls_policy, "force") != 0) &&
                 (g_strcmp0(tls_policy, "allow") != 0) &&
+                (g_strcmp0(tls_policy, "trust") != 0) &&
                 (g_strcmp0(tls_policy, "disable") != 0) &&
                 (g_strcmp0(tls_policy, "legacy") != 0))) {
             g_free(tls_policy);
diff --git a/src/config/accounts.h b/src/config/accounts.h
index c6a87c44..d41fb53e 100644
--- a/src/config/accounts.h
+++ b/src/config/accounts.h
@@ -1,7 +1,7 @@
 /*
  * accounts.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/config/conflists.c b/src/config/conflists.c
index 0d368757..e6416924 100644
--- a/src/config/conflists.c
+++ b/src/config/conflists.c
@@ -1,7 +1,7 @@
 /*
  * conflists.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/config/conflists.h b/src/config/conflists.h
index 0e46fb00..3b653834 100644
--- a/src/config/conflists.h
+++ b/src/config/conflists.h
@@ -1,7 +1,7 @@
 /*
  * conflists.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/config/files.c b/src/config/files.c
index e8de2edb..cd1c914f 100644
--- a/src/config/files.c
+++ b/src/config/files.c
@@ -1,7 +1,7 @@
 /*
  * files.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/config/files.h b/src/config/files.h
index 2eec9772..1d8d2890 100644
--- a/src/config/files.h
+++ b/src/config/files.h
@@ -1,7 +1,7 @@
 /*
  * files.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/config/preferences.c b/src/config/preferences.c
index e62c552c..265e11db 100644
--- a/src/config/preferences.c
+++ b/src/config/preferences.c
@@ -1,7 +1,7 @@
 /*
  * preferences.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.
  *
@@ -123,7 +123,7 @@ prefs_load(void)
         g_key_file_set_string(prefs, PREF_GROUP_UI, "time.console", val);
         g_key_file_set_string(prefs, PREF_GROUP_UI, "time.chat", val);
         g_key_file_set_string(prefs, PREF_GROUP_UI, "time.muc", val);
-        g_key_file_set_string(prefs, PREF_GROUP_UI, "time.mucconfig", val);
+        g_key_file_set_string(prefs, PREF_GROUP_UI, "time.config", val);
         g_key_file_set_string(prefs, PREF_GROUP_UI, "time.private", val);
         g_key_file_set_string(prefs, PREF_GROUP_UI, "time.xmlconsole", val);
         g_key_file_remove_key(prefs, PREF_GROUP_UI, "time", NULL);
@@ -1567,7 +1567,7 @@ _get_group(preference_t pref)
         case PREF_TIME_CONSOLE:
         case PREF_TIME_CHAT:
         case PREF_TIME_MUC:
-        case PREF_TIME_MUCCONFIG:
+        case PREF_TIME_CONFIG:
         case PREF_TIME_PRIVATE:
         case PREF_TIME_XMLCONSOLE:
         case PREF_TIME_STATUSBAR:
@@ -1777,8 +1777,8 @@ _get_key(preference_t pref)
             return "time.chat";
         case PREF_TIME_MUC:
             return "time.muc";
-        case PREF_TIME_MUCCONFIG:
-            return "time.mucconfig";
+        case PREF_TIME_CONFIG:
+            return "time.config";
         case PREF_TIME_PRIVATE:
             return "time.private";
         case PREF_TIME_XMLCONSOLE:
@@ -1967,7 +1967,7 @@ _get_default_string(preference_t pref)
             return "%H:%M:%S";
         case PREF_TIME_MUC:
             return "%H:%M:%S";
-        case PREF_TIME_MUCCONFIG:
+        case PREF_TIME_CONFIG:
             return "%H:%M:%S";
         case PREF_TIME_PRIVATE:
             return "%H:%M:%S";
diff --git a/src/config/preferences.h b/src/config/preferences.h
index bafe4a1f..65dee327 100644
--- a/src/config/preferences.h
+++ b/src/config/preferences.h
@@ -1,7 +1,7 @@
 /*
  * preferences.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.
  *
@@ -92,7 +92,7 @@ typedef enum {
     PREF_TIME_CONSOLE,
     PREF_TIME_CHAT,
     PREF_TIME_MUC,
-    PREF_TIME_MUCCONFIG,
+    PREF_TIME_CONFIG,
     PREF_TIME_PRIVATE,
     PREF_TIME_XMLCONSOLE,
     PREF_TIME_STATUSBAR,
diff --git a/src/config/scripts.c b/src/config/scripts.c
index 5980dcaa..aea914b4 100644
--- a/src/config/scripts.c
+++ b/src/config/scripts.c
@@ -1,7 +1,7 @@
 /*
  * scripts.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/config/scripts.h b/src/config/scripts.h
index 508a43c2..ef306c37 100644
--- a/src/config/scripts.h
+++ b/src/config/scripts.h
@@ -1,7 +1,7 @@
 /*
  * scripts.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/config/theme.c b/src/config/theme.c
index 13f9cc2b..a94cfb21 100644
--- a/src/config/theme.c
+++ b/src/config/theme.c
@@ -1,7 +1,7 @@
 /*
  * theme.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.
  *
@@ -413,7 +413,7 @@ _load_preferences(void)
     _set_string_preference("time.console", PREF_TIME_CONSOLE);
     _set_string_preference("time.chat", PREF_TIME_CHAT);
     _set_string_preference("time.muc", PREF_TIME_MUC);
-    _set_string_preference("time.mucconfig", PREF_TIME_MUCCONFIG);
+    _set_string_preference("time.config", PREF_TIME_CONFIG);
     _set_string_preference("time.private", PREF_TIME_PRIVATE);
     _set_string_preference("time.xmlconsole", PREF_TIME_XMLCONSOLE);
     _set_string_preference("time.statusbar", PREF_TIME_STATUSBAR);
diff --git a/src/config/theme.h b/src/config/theme.h
index c1d9870e..69584b30 100644
--- a/src/config/theme.h
+++ b/src/config/theme.h
@@ -1,7 +1,7 @@
 /*
  * theme.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/config/tlscerts.c b/src/config/tlscerts.c
index 23f6575c..95a256b5 100644
--- a/src/config/tlscerts.c
+++ b/src/config/tlscerts.c
@@ -1,7 +1,7 @@
 /*
  * tlscerts.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/config/tlscerts.h b/src/config/tlscerts.h
index 2fd568ad..fcde7db0 100644
--- a/src/config/tlscerts.h
+++ b/src/config/tlscerts.h
@@ -1,7 +1,7 @@
 /*
  * tlscerts.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/event/client_events.c b/src/event/client_events.c
index 94f75ba5..3b6218ea 100644
--- a/src/event/client_events.c
+++ b/src/event/client_events.c
@@ -1,7 +1,7 @@
 /*
  * client_events.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/event/client_events.h b/src/event/client_events.h
index 098f2648..346cb8ff 100644
--- a/src/event/client_events.h
+++ b/src/event/client_events.h
@@ -1,7 +1,7 @@
 /*
  * client_events.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/event/server_events.c b/src/event/server_events.c
index b735c22f..5dfa652f 100644
--- a/src/event/server_events.c
+++ b/src/event/server_events.c
@@ -1,7 +1,7 @@
 /*
  * server_events.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.
  *
@@ -896,8 +896,8 @@ sv_ev_muc_occupant_online(const char *const room, const char *const nick, const
         ProfMucWin *mucwin = wins_get_muc(room);
         if (mucwin) {
             mucwin_occupant_nick_change(mucwin, old_nick, nick);
+            wins_private_nick_change(mucwin->roomjid, old_nick, nick);
         }
-        wins_private_nick_change(mucwin->roomjid, old_nick, nick);
         free(old_nick);
 
         occupantswin_occupants(room);
@@ -914,11 +914,13 @@ sv_ev_muc_occupant_online(const char *const room, const char *const nick, const
         }
         prefs_free_string(muc_status_pref);
 
-        Jid *jidp = jid_create_from_bare_and_resource(mucwin->roomjid, nick);
-        ProfPrivateWin *privwin = wins_get_private(jidp->fulljid);
-        jid_destroy(jidp);
-        if (privwin) {
-            privwin_occupant_online(privwin);
+        if (mucwin) {
+            Jid *jidp = jid_create_from_bare_and_resource(mucwin->roomjid, nick);
+            ProfPrivateWin *privwin = wins_get_private(jidp->fulljid);
+            jid_destroy(jidp);
+            if (privwin) {
+                privwin_occupant_online(privwin);
+            }
         }
 
         occupantswin_occupants(room);
diff --git a/src/event/server_events.h b/src/event/server_events.h
index bbbee8a2..cc261487 100644
--- a/src/event/server_events.h
+++ b/src/event/server_events.h
@@ -1,7 +1,7 @@
 /*
  * server_events.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/log.c b/src/log.c
index 78306c3c..0133a6cf 100644
--- a/src/log.c
+++ b/src/log.c
@@ -1,7 +1,7 @@
 /*
  * log.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.
  *
@@ -228,7 +228,7 @@ _rotate_log_file(void)
     size_t len = strlen(log_file);
     char *log_file_new = malloc(len + 3);
 
-    strncpy(log_file_new, log_file, len);
+    memcpy(log_file_new, log_file, len);
     log_file_new[len] = '.';
     log_file_new[len+1] = '1';
     log_file_new[len+2] = 0;
diff --git a/src/log.h b/src/log.h
index a4c44ef6..43a34ca1 100644
--- a/src/log.h
+++ b/src/log.h
@@ -1,7 +1,7 @@
 /*
  * log.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/main.c b/src/main.c
index 91e17a9c..d2392a2b 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1,7 +1,7 @@
 /*
  * main.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/otr/otr.c b/src/otr/otr.c
index e84f0ca2..47d5adf6 100644
--- a/src/otr/otr.c
+++ b/src/otr/otr.c
@@ -1,7 +1,7 @@
 /*
  * otr.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/otr/otr.h b/src/otr/otr.h
index 1ac99a75..b595df50 100644
--- a/src/otr/otr.h
+++ b/src/otr/otr.h
@@ -1,7 +1,7 @@
 /*
  * otr.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/otr/otrlib.h b/src/otr/otrlib.h
index ba1440c3..4add81b1 100644
--- a/src/otr/otrlib.h
+++ b/src/otr/otrlib.h
@@ -1,7 +1,7 @@
 /*
  * otrlib.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/otr/otrlibv3.c b/src/otr/otrlibv3.c
index c1328ecf..a2348c5c 100644
--- a/src/otr/otrlibv3.c
+++ b/src/otr/otrlibv3.c
@@ -1,7 +1,7 @@
 /*
  * otrlibv3.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/otr/otrlibv4.c b/src/otr/otrlibv4.c
index dfcb4529..176d9a42 100644
--- a/src/otr/otrlibv4.c
+++ b/src/otr/otrlibv4.c
@@ -1,7 +1,7 @@
 /*
  * otrlibv4.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/pgp/gpg.c b/src/pgp/gpg.c
index bfebe8bd..8d2c1eab 100644
--- a/src/pgp/gpg.c
+++ b/src/pgp/gpg.c
@@ -1,7 +1,7 @@
 /*
  * gpg.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/pgp/gpg.h b/src/pgp/gpg.h
index 4a7d27b1..580e108a 100644
--- a/src/pgp/gpg.h
+++ b/src/pgp/gpg.h
@@ -1,7 +1,7 @@
 /*
  * gpg.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/plugins/api.c b/src/plugins/api.c
index d30914dc..4d8434e1 100644
--- a/src/plugins/api.c
+++ b/src/plugins/api.c
@@ -1,7 +1,7 @@
 /*
  * api.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/plugins/api.h b/src/plugins/api.h
index 0f4881f0..0ba89f59 100644
--- a/src/plugins/api.h
+++ b/src/plugins/api.h
@@ -1,7 +1,7 @@
 /*
  * api.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/plugins/autocompleters.c b/src/plugins/autocompleters.c
index 040fcf4d..814253dd 100644
--- a/src/plugins/autocompleters.c
+++ b/src/plugins/autocompleters.c
@@ -1,7 +1,7 @@
 /*
  * autocompleters.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/plugins/autocompleters.h b/src/plugins/autocompleters.h
index edb076f4..dd811894 100644
--- a/src/plugins/autocompleters.h
+++ b/src/plugins/autocompleters.h
@@ -1,7 +1,7 @@
 /*
  * autocompleters.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/plugins/c_api.c b/src/plugins/c_api.c
index f3679d8f..3578d8f0 100644
--- a/src/plugins/c_api.c
+++ b/src/plugins/c_api.c
@@ -1,7 +1,7 @@
 /*
  * c_api.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/plugins/c_api.h b/src/plugins/c_api.h
index 8416ca20..dbce52e7 100644
--- a/src/plugins/c_api.h
+++ b/src/plugins/c_api.h
@@ -1,7 +1,7 @@
 /*
  * c_api.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/plugins/c_plugins.c b/src/plugins/c_plugins.c
index 44010e95..5a3b5957 100644
--- a/src/plugins/c_plugins.c
+++ b/src/plugins/c_plugins.c
@@ -1,7 +1,7 @@
 /*
  * c_plugins.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/plugins/c_plugins.h b/src/plugins/c_plugins.h
index 4098170a..a82e92cc 100644
--- a/src/plugins/c_plugins.h
+++ b/src/plugins/c_plugins.h
@@ -1,7 +1,7 @@
 /*
  * c_plugins.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/plugins/callbacks.c b/src/plugins/callbacks.c
index d197a44b..c2e79ff1 100644
--- a/src/plugins/callbacks.c
+++ b/src/plugins/callbacks.c
@@ -1,7 +1,7 @@
 /*
  * callbacks.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/plugins/callbacks.h b/src/plugins/callbacks.h
index 23edd8eb..783f073e 100644
--- a/src/plugins/callbacks.h
+++ b/src/plugins/callbacks.h
@@ -1,7 +1,7 @@
 /*
  * callbacks.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/plugins/disco.c b/src/plugins/disco.c
index a2889f9a..ae97a758 100644
--- a/src/plugins/disco.c
+++ b/src/plugins/disco.c
@@ -1,7 +1,7 @@
 /*
  * disco.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/plugins/disco.h b/src/plugins/disco.h
index 07c6a946..ab19e2a0 100644
--- a/src/plugins/disco.h
+++ b/src/plugins/disco.h
@@ -1,7 +1,7 @@
 /*
  * disco.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/plugins/plugins.c b/src/plugins/plugins.c
index 5ec90d0c..41885541 100644
--- a/src/plugins/plugins.c
+++ b/src/plugins/plugins.c
@@ -1,7 +1,7 @@
 /*
  * plugins.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.
  *
@@ -34,6 +34,7 @@
 
 #include <string.h>
 #include <stdlib.h>
+#include <gio/gio.h>
 
 #include "log.h"
 #include "config.h"
@@ -149,16 +150,19 @@ plugins_install_all(const char *const path)
     get_file_paths_recursive(path, &contents);
 
     GSList *curr = contents;
+    GString *error_message = NULL;
     while (curr) {
+        error_message = g_string_new(NULL);
         if (g_str_has_suffix(curr->data, ".py") || g_str_has_suffix(curr->data, ".so")) {
             gchar *plugin_name = g_path_get_basename(curr->data);
-            if (plugins_install(plugin_name, curr->data)) {
+            if (plugins_install(plugin_name, curr->data, error_message)) {
                 result->installed = g_slist_append(result->installed, strdup(curr->data));
             } else {
                 result->failed = g_slist_append(result->failed, strdup(curr->data));
             }
         }
         curr = g_slist_next(curr);
+        g_string_free(error_message, TRUE);
     }
 
     g_slist_free_full(contents, g_free);
@@ -167,7 +171,25 @@ plugins_install_all(const char *const path)
 }
 
 gboolean
-plugins_install(const char *const plugin_name, const char *const filename)
+plugins_uninstall(const char *const plugin_name)
+{
+    plugins_unload(plugin_name);
+    char *plugins_dir = files_get_data_path(DIR_PLUGINS);   
+    GString *target_path = g_string_new(plugins_dir);
+    free(plugins_dir);
+    g_string_append(target_path, "/");
+    g_string_append(target_path, plugin_name);
+    GFile *file = g_file_new_for_path(target_path->str);
+    GError *error = NULL;
+    gboolean result = g_file_delete(file, NULL, &error);
+    g_object_unref(file);
+    g_error_free(error);
+    g_string_free(target_path, TRUE);
+    return result;
+}
+
+gboolean
+plugins_install(const char *const plugin_name, const char *const filename, GString *error_message)
 {
     char *plugins_dir = files_get_data_path(DIR_PLUGINS);
     GString *target_path = g_string_new(plugins_dir);
@@ -175,18 +197,19 @@ plugins_install(const char *const plugin_name, const char *const filename)
     g_string_append(target_path, "/");
     g_string_append(target_path, plugin_name);
 
-    ProfPlugin *plugin = g_hash_table_lookup(plugins, plugin_name);
-    if (plugin) {
-        plugins_unload(plugin_name);
+    if (g_file_test (target_path->str, G_FILE_TEST_EXISTS))
+    {
+        log_info("Failed to install plugin: %s, file exists", plugin_name);
+        g_string_assign(error_message, "File exists");
+        return FALSE;
     }
 
-    gboolean result = copy_file(filename, target_path->str);
+    gboolean result = copy_file(filename, target_path->str, false);
     g_string_free(target_path, TRUE);
 
     if (result) {
         result = plugins_load(plugin_name);
     }
-
     return result;
 }
 
diff --git a/src/plugins/plugins.h b/src/plugins/plugins.h
index 16d6874a..4267cb22 100644
--- a/src/plugins/plugins.h
+++ b/src/plugins/plugins.h
@@ -1,7 +1,7 @@
 /*
  * plugins.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.
  *
@@ -114,7 +114,9 @@ void plugins_shutdown(void);
 
 void plugins_free_install_result(PluginsInstallResult *result);
 
-gboolean plugins_install(const char *const plugin_name, const char *const filename);
+gboolean plugins_install(const char *const plugin_name, const char *const filename, GString * error_message);
+gboolean plugins_uninstall(const char *const plugin_name);
+gboolean plugins_update(const char *const plugin_name, const char *const filename, GString * error_message);
 PluginsInstallResult* plugins_install_all(const char *const path);
 gboolean plugins_load(const char *const name);
 GSList* plugins_load_all(void);
diff --git a/src/plugins/profapi.c b/src/plugins/profapi.c
index 11b9d154..53e175f9 100644
--- a/src/plugins/profapi.c
+++ b/src/plugins/profapi.c
@@ -1,7 +1,7 @@
 /*
  * prof_api.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/plugins/profapi.h b/src/plugins/profapi.h
index 48d9f189..68469b30 100644
--- a/src/plugins/profapi.h
+++ b/src/plugins/profapi.h
@@ -1,7 +1,7 @@
 /*
  * prof_api.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/plugins/python_api.c b/src/plugins/python_api.c
index 086ccbfb..99a96634 100644
--- a/src/plugins/python_api.c
+++ b/src/plugins/python_api.c
@@ -1,7 +1,7 @@
 /*
  * python_api.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/plugins/python_api.h b/src/plugins/python_api.h
index 8133c853..cf34c1e6 100644
--- a/src/plugins/python_api.h
+++ b/src/plugins/python_api.h
@@ -1,7 +1,7 @@
 /*
  * python_api.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/plugins/python_plugins.c b/src/plugins/python_plugins.c
index 6f7c2158..a6339793 100644
--- a/src/plugins/python_plugins.c
+++ b/src/plugins/python_plugins.c
@@ -1,7 +1,7 @@
 /*
  * python_plugins.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/plugins/python_plugins.h b/src/plugins/python_plugins.h
index 587d13d7..b5083ff0 100644
--- a/src/plugins/python_plugins.h
+++ b/src/plugins/python_plugins.h
@@ -1,7 +1,7 @@
 /*
  * python_plugins.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/plugins/settings.c b/src/plugins/settings.c
index e4286f22..6f4d37c5 100644
--- a/src/plugins/settings.c
+++ b/src/plugins/settings.c
@@ -1,7 +1,7 @@
 /*
  * settings.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/plugins/settings.h b/src/plugins/settings.h
index 3a81b2d1..cbff1bbf 100644
--- a/src/plugins/settings.h
+++ b/src/plugins/settings.h
@@ -1,7 +1,7 @@
 /*
  * settings.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/plugins/themes.c b/src/plugins/themes.c
index 85c26290..756a9793 100644
--- a/src/plugins/themes.c
+++ b/src/plugins/themes.c
@@ -1,7 +1,7 @@
 /*
  * themes.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/plugins/themes.h b/src/plugins/themes.h
index 4feb9f2e..470f7926 100644
--- a/src/plugins/themes.h
+++ b/src/plugins/themes.h
@@ -1,7 +1,7 @@
 /*
  * themes.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/profanity.c b/src/profanity.c
index 25bb5b42..1d4a2c35 100644
--- a/src/profanity.c
+++ b/src/profanity.c
@@ -1,7 +1,7 @@
 /*
  * profanity.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/profanity.h b/src/profanity.h
index 378eeddb..cf3040b5 100644
--- a/src/profanity.h
+++ b/src/profanity.h
@@ -1,7 +1,7 @@
 /*
  * profanity.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/tools/autocomplete.c b/src/tools/autocomplete.c
index 00f29fe0..79312e53 100644
--- a/src/tools/autocomplete.c
+++ b/src/tools/autocomplete.c
@@ -1,7 +1,7 @@
 /*
  * autocomplete.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/tools/autocomplete.h b/src/tools/autocomplete.h
index 64141b35..78bec44d 100644
--- a/src/tools/autocomplete.h
+++ b/src/tools/autocomplete.h
@@ -1,7 +1,7 @@
 /*
  * autocomplete.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/tools/http_upload.c b/src/tools/http_upload.c
index fc4c60f3..bebb7239 100644
--- a/src/tools/http_upload.c
+++ b/src/tools/http_upload.c
@@ -1,7 +1,7 @@
 /*
  * http_upload.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/tools/http_upload.h b/src/tools/http_upload.h
index 145207a4..7d61ea74 100644
--- a/src/tools/http_upload.h
+++ b/src/tools/http_upload.h
@@ -1,7 +1,7 @@
 /*
  * http_upload.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/tools/p_sha1.c b/src/tools/p_sha1.c
deleted file mode 100644
index 11adede3..00000000
--- a/src/tools/p_sha1.c
+++ /dev/null
@@ -1,403 +0,0 @@
-/** @file
- *  SHA-1 hash.
- */
-
-/*
-SHA-1 in C
-By Steve Reid <sreid@sea-to-sky.net>
-100% Public Domain
-
------------------
-Modified 7/98
-By James H. Brown <jbrown@burgoyne.com>
-Still 100% Public Domain
-
-Corrected a problem which generated improper hash values on 16 bit machines
-Routine SHA1Update changed from
-	void SHA1Update(P_SHA1_CTX* context, unsigned char* data, unsigned int
-len)
-to
-	void SHA1Update(P_SHA1_CTX* context, unsigned char* data, unsigned
-long len)
-
-The 'len' parameter was declared an int which works fine on 32 bit machines.
-However, on 16 bit machines an int is too small for the shifts being done
-against
-it.  This caused the hash function to generate incorrect values if len was
-greater than 8191 (8K - 1) due to the 'len << 3' on line 3 of SHA1Update().
-
-Since the file IO in main() reads 16K at a time, any file 8K or larger would
-be guaranteed to generate the wrong hash (e.g. Test Vector #3, a million
-"a"s).
-
-I also changed the declaration of variables i & j in SHA1Update to
-unsigned long from unsigned int for the same reason.
-
-These changes should make no difference to any 32 bit implementations since
-an
-int and a long are the same size in those environments.
-
---
-I also corrected a few compiler warnings generated by Borland C.
-1. Added #include <process.h> for exit() prototype
-2. Removed unused variable 'j' in SHA1Final
-3. Changed exit(0) to return(0) at end of main.
-
-ALL changes I made can be located by searching for comments containing 'JHB'
------------------
-Modified 8/98
-By Steve Reid <sreid@sea-to-sky.net>
-Still 100% public domain
-
-1- Removed #include <process.h> and used return() instead of exit()
-2- Fixed overwriting of finalcount in SHA1Final() (discovered by Chris Hall)
-3- Changed email address from steve@edmweb.com to sreid@sea-to-sky.net
-
------------------
-Modified 4/01
-By Saul Kravitz <Saul.Kravitz@celera.com>
-Still 100% PD
-Modified to run on Compaq Alpha hardware.
-
------------------
-Modified 07/2002
-By Ralph Giles <giles@artofcode.com>
-Still 100% public domain
-modified for use with stdint types, autoconf
-code cleanup, removed attribution comments
-switched SHA1Final() argument order for consistency
-use SHA1_ prefix for public api
-move public api to sha1.h
-*/
-
-/*
-Test Vectors (from FIPS PUB 180-1)
-"abc"
-  A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D
-"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
-  84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1
-A million repetitions of "a"
-  34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F
-*/
-
-/* #define SHA1HANDSOFF  */
-
-#include <stdio.h>
-#include <string.h>
-
-/* make sure the stdint.h types are available */
-#if defined(_MSC_VER) /* Microsoft Visual C++ */
-  typedef signed char             int8_t;
-  typedef short int               int16_t;
-  typedef int                     int32_t;
-  typedef __int64                 int64_t;
-  typedef unsigned char           uint8_t;
-  typedef unsigned short int      uint16_t;
-  typedef unsigned int            uint32_t;
-  /* no uint64_t */
-#else
-#include <stdint.h>
-#endif
-
-#include "p_sha1.h"
-
-static uint32_t host_to_be(uint32_t i);
-void P_SHA1_Transform(uint32_t state[5], const uint8_t buffer[64]);
-
-#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits))))
-
-/* blk0() and blk() perform the initial expand. */
-/* I got the idea of expanding during the round function from SSLeay */
-#define blk0(i) (block->l[i] = host_to_be(block->l[i]))
-#define blk(i) (block->l[i&15] = rol(block->l[(i+13)&15]^block->l[(i+8)&15] \
-    ^block->l[(i+2)&15]^block->l[i&15],1))
-
-/* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */
-#define R0(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk0(i)+0x5A827999+rol(v,5);w=rol(w,30);
-#define R1(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk(i)+0x5A827999+rol(v,5);w=rol(w,30);
-#define R2(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0x6ED9EBA1+rol(v,5);w=rol(w,30);
-#define R3(v,w,x,y,z,i) z+=(((w|x)&y)|(w&x))+blk(i)+0x8F1BBCDC+rol(v,5);w=rol(w,30);
-#define R4(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0xCA62C1D6+rol(v,5);w=rol(w,30);
-
-
-#ifdef VERBOSE  /* SAK */
-void SHAPrintContext(P_P_SHA1_CTX *context, char *msg){
-  printf("%s (%d,%d) %x %x %x %x %x\n",
-	 msg,
-	 context->count[0], context->count[1],
-	 context->state[0],
-	 context->state[1],
-	 context->state[2],
-	 context->state[3],
-	 context->state[4]);
-}
-#endif /* VERBOSE */
-
-static uint32_t host_to_be(uint32_t i)
-{
-#define le_to_be(i) ((rol((i),24) & 0xFF00FF00) | (rol((i),8) & 0x00FF00FF))
-#if defined(__BIG_ENDIAN__) || \
-    (defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && \
-    __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
-    return i;
-#elif defined(__LITTLE_ENDIAN__) || \
-    (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && \
-    __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
-    return le_to_be(i);
-#else /* fallback to run-time check */
-    static const union {
-        uint32_t u;
-        unsigned char c;
-    } check = {1};
-
-    return check.c ? le_to_be(i) : i;
-#endif
-}
-
-/* Hash a single 512-bit block. This is the core of the algorithm. */
-void P_SHA1_Transform(uint32_t state[5], const uint8_t buffer[64])
-{
-    uint32_t a, b, c, d, e;
-    typedef union {
-        uint8_t c[64];
-        uint32_t l[16];
-    } CHAR64LONG16;
-    CHAR64LONG16* block;
-
-#ifdef SHA1HANDSOFF
-    static uint8_t workspace[64];
-    block = (CHAR64LONG16*)workspace;
-    memcpy(block, buffer, 64);
-#else
-    block = (CHAR64LONG16*)buffer;
-#endif
-
-    /* Copy context->state[] to working vars */
-    a = state[0];
-    b = state[1];
-    c = state[2];
-    d = state[3];
-    e = state[4];
-
-    /* 4 rounds of 20 operations each. Loop unrolled. */
-    R0(a,b,c,d,e, 0); R0(e,a,b,c,d, 1); R0(d,e,a,b,c, 2); R0(c,d,e,a,b, 3);
-    R0(b,c,d,e,a, 4); R0(a,b,c,d,e, 5); R0(e,a,b,c,d, 6); R0(d,e,a,b,c, 7);
-    R0(c,d,e,a,b, 8); R0(b,c,d,e,a, 9); R0(a,b,c,d,e,10); R0(e,a,b,c,d,11);
-    R0(d,e,a,b,c,12); R0(c,d,e,a,b,13); R0(b,c,d,e,a,14); R0(a,b,c,d,e,15);
-    R1(e,a,b,c,d,16); R1(d,e,a,b,c,17); R1(c,d,e,a,b,18); R1(b,c,d,e,a,19);
-    R2(a,b,c,d,e,20); R2(e,a,b,c,d,21); R2(d,e,a,b,c,22); R2(c,d,e,a,b,23);
-    R2(b,c,d,e,a,24); R2(a,b,c,d,e,25); R2(e,a,b,c,d,26); R2(d,e,a,b,c,27);
-    R2(c,d,e,a,b,28); R2(b,c,d,e,a,29); R2(a,b,c,d,e,30); R2(e,a,b,c,d,31);
-    R2(d,e,a,b,c,32); R2(c,d,e,a,b,33); R2(b,c,d,e,a,34); R2(a,b,c,d,e,35);
-    R2(e,a,b,c,d,36); R2(d,e,a,b,c,37); R2(c,d,e,a,b,38); R2(b,c,d,e,a,39);
-    R3(a,b,c,d,e,40); R3(e,a,b,c,d,41); R3(d,e,a,b,c,42); R3(c,d,e,a,b,43);
-    R3(b,c,d,e,a,44); R3(a,b,c,d,e,45); R3(e,a,b,c,d,46); R3(d,e,a,b,c,47);
-    R3(c,d,e,a,b,48); R3(b,c,d,e,a,49); R3(a,b,c,d,e,50); R3(e,a,b,c,d,51);
-    R3(d,e,a,b,c,52); R3(c,d,e,a,b,53); R3(b,c,d,e,a,54); R3(a,b,c,d,e,55);
-    R3(e,a,b,c,d,56); R3(d,e,a,b,c,57); R3(c,d,e,a,b,58); R3(b,c,d,e,a,59);
-    R4(a,b,c,d,e,60); R4(e,a,b,c,d,61); R4(d,e,a,b,c,62); R4(c,d,e,a,b,63);
-    R4(b,c,d,e,a,64); R4(a,b,c,d,e,65); R4(e,a,b,c,d,66); R4(d,e,a,b,c,67);
-    R4(c,d,e,a,b,68); R4(b,c,d,e,a,69); R4(a,b,c,d,e,70); R4(e,a,b,c,d,71);
-    R4(d,e,a,b,c,72); R4(c,d,e,a,b,73); R4(b,c,d,e,a,74); R4(a,b,c,d,e,75);
-    R4(e,a,b,c,d,76); R4(d,e,a,b,c,77); R4(c,d,e,a,b,78); R4(b,c,d,e,a,79);
-
-    /* Add the working vars back into context.state[] */
-    state[0] += a;
-    state[1] += b;
-    state[2] += c;
-    state[3] += d;
-    state[4] += e;
-
-    /* Wipe variables */
-    a = b = c = d = e = 0;
-}
-
-
-/* SHA1Init - Initialize new context */
-void P_SHA1_Init(P_SHA1_CTX* context)
-{
-    /* SHA1 initialization constants */
-    context->state[0] = 0x67452301;
-    context->state[1] = 0xEFCDAB89;
-    context->state[2] = 0x98BADCFE;
-    context->state[3] = 0x10325476;
-    context->state[4] = 0xC3D2E1F0;
-    context->count[0] = context->count[1] = 0;
-}
-
-
-/* Run your data through this. */
-void P_SHA1_Update(P_SHA1_CTX* context, const uint8_t* data, const size_t len)
-{
-    size_t i, j;
-
-#ifdef VERBOSE
-    SHAPrintContext(context, "before");
-#endif
-
-    j = (context->count[0] >> 3) & 63;
-    if ((context->count[0] += len << 3) < (len << 3)) context->count[1]++;
-    context->count[1] += (len >> 29);
-    if ((j + len) > 63) {
-        memcpy(&context->buffer[j], data, (i = 64-j));
-        P_SHA1_Transform(context->state, context->buffer);
-        for ( ; i + 63 < len; i += 64) {
-            P_SHA1_Transform(context->state, data + i);
-        }
-        j = 0;
-    }
-    else i = 0;
-    memcpy(&context->buffer[j], &data[i], len - i);
-
-#ifdef VERBOSE
-    SHAPrintContext(context, "after ");
-#endif
-}
-
-
-/* Add padding and return the message digest. */
-void P_SHA1_Final(P_SHA1_CTX* context, uint8_t digest[P_SHA1_DIGEST_SIZE])
-{
-    uint32_t i;
-    uint8_t  finalcount[8];
-
-    for (i = 0; i < 8; i++) {
-        finalcount[i] = (unsigned char)((context->count[(i >= 4 ? 0 : 1)]
-         >> ((3-(i & 3)) * 8) ) & 255);  /* Endian independent */
-    }
-    P_SHA1_Update(context, (uint8_t *)"\200", 1);
-    while ((context->count[0] & 504) != 448) {
-        P_SHA1_Update(context, (uint8_t *)"\0", 1);
-    }
-    P_SHA1_Update(context, finalcount, 8);  /* Should cause a SHA1_Transform() */
-    for (i = 0; i < P_SHA1_DIGEST_SIZE; i++) {
-        digest[i] = (uint8_t)
-         ((context->state[i>>2] >> ((3-(i & 3)) * 8) ) & 255);
-    }
-
-    /* Wipe variables */
-    i = 0;
-    memset(context->buffer, 0, 64);
-    memset(context->state, 0, 20);
-    memset(context->count, 0, 8);
-    memset(finalcount, 0, 8);	/* SWR */
-
-#ifdef SHA1HANDSOFF  /* make SHA1Transform overwrite its own static vars */
-    P_SHA1_Transform(context->state, context->buffer);
-#endif
-}
-
-/*************************************************************/
-
-#if 0
-int main(int argc, char** argv)
-{
-int i, j;
-P_SHA1_CTX context;
-unsigned char digest[P_SHA1_DIGEST_SIZE], buffer[16384];
-FILE* file;
-
-    if (argc > 2) {
-        puts("Public domain SHA-1 implementation - by Steve Reid <sreid@sea-to-sky.net>");
-        puts("Modified for 16 bit environments 7/98 - by James H. Brown <jbrown@burgoyne.com>");	/* JHB */
-        puts("Produces the SHA-1 hash of a file, or stdin if no file is specified.");
-        return(0);
-    }
-    if (argc < 2) {
-        file = stdin;
-    }
-    else {
-        if (!(file = fopen(argv[1], "rb"))) {
-            fputs("Unable to open file.", stderr);
-            return(-1);
-        }
-    }
-    SHA1_Init(&context);
-    while (!feof(file)) {  /* note: what if ferror(file) */
-        i = fread(buffer, 1, 16384, file);
-        SHA1_Update(&context, buffer, i);
-    }
-    SHA1_Final(&context, digest);
-    fclose(file);
-    for (i = 0; i < P_SHA1_DIGEST_SIZE/4; i++) {
-        for (j = 0; j < 4; j++) {
-            printf("%02X", digest[i*4+j]);
-        }
-        putchar(' ');
-    }
-    putchar('\n');
-    return(0);	/* JHB */
-}
-#endif
-
-/* self test */
-
-#ifdef TEST
-
-static char *test_data[] = {
-    "abc",
-    "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
-    "A million repetitions of 'a'"};
-static char *test_results[] = {
-    "A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D",
-    "84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1",
-    "34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F"};
-
-void digest_to_hex(const uint8_t digest[P_SHA1_DIGEST_SIZE], char *output)
-{
-    int i,j;
-    char *c = output;
-
-    for (i = 0; i < P_SHA1_DIGEST_SIZE/4; i++) {
-        for (j = 0; j < 4; j++) {
-            sprintf(c,"%02X", digest[i*4+j]);
-            c += 2;
-        }
-        sprintf(c, " ");
-        c += 1;
-    }
-    *(c - 1) = '\0';
-}
-
-int main(int argc, char** argv)
-{
-    int k;
-    P_SHA1_CTX context;
-    uint8_t digest[20];
-    char output[80];
-
-    fprintf(stdout, "verifying SHA-1 implementation... ");
-
-    for (k = 0; k < 2; k++){
-        P_SHA1_Init(&context);
-        P_SHA1_Update(&context, (uint8_t*)test_data[k], strlen(test_data[k]));
-        P_SHA1_Final(&context, digest);
-	digest_to_hex(digest, output);
-
-        if (strcmp(output, test_results[k])) {
-            fprintf(stdout, "FAIL\n");
-            fprintf(stderr,"* hash of \"%s\" incorrect:\n", test_data[k]);
-            fprintf(stderr,"\t%s returned\n", output);
-            fprintf(stderr,"\t%s is correct\n", test_results[k]);
-            return (1);
-        }
-    }
-    /* million 'a' vector we feed separately */
-    P_SHA1_Init(&context);
-    for (k = 0; k < 1000000; k++)
-        P_SHA1_Update(&context, (uint8_t*)"a", 1);
-    P_SHA1_Final(&context, digest);
-    digest_to_hex(digest, output);
-    if (strcmp(output, test_results[2])) {
-        fprintf(stdout, "FAIL\n");
-        fprintf(stderr,"* hash of \"%s\" incorrect:\n", test_data[2]);
-        fprintf(stderr,"\t%s returned\n", output);
-        fprintf(stderr,"\t%s is correct\n", test_results[2]);
-        return (1);
-    }
-
-    /* success */
-    fprintf(stdout, "ok\n");
-    return(0);
-}
-#endif /* TEST */
diff --git a/src/tools/p_sha1.h b/src/tools/p_sha1.h
deleted file mode 100644
index 75443b01..00000000
--- a/src/tools/p_sha1.h
+++ /dev/null
@@ -1,31 +0,0 @@
-/* public api for steve reid's public domain SHA-1 implementation */
-/* this file is in the public domain */
-
-/** @file
- *  SHA-1 hash API.
- */
-
-#ifndef __P_SHA1_H
-#define __P_SHA1_H
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-typedef struct {
-    uint32_t state[5];
-    uint32_t count[2];
-    uint8_t  buffer[64];
-} P_SHA1_CTX;
-
-#define P_SHA1_DIGEST_SIZE 20
-
-void P_SHA1_Init(P_SHA1_CTX* context);
-void P_SHA1_Update(P_SHA1_CTX* context, const uint8_t* data, const size_t len);
-void P_SHA1_Final(P_SHA1_CTX* context, uint8_t digest[P_SHA1_DIGEST_SIZE]);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* __P_SHA1_H */
diff --git a/src/tools/parser.c b/src/tools/parser.c
index 74fef1e2..474a664e 100644
--- a/src/tools/parser.c
+++ b/src/tools/parser.c
@@ -1,7 +1,7 @@
 /*
  * parser.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/tools/parser.h b/src/tools/parser.h
index b099ce24..5d5c6028 100644
--- a/src/tools/parser.h
+++ b/src/tools/parser.h
@@ -1,7 +1,7 @@
 /*
  * parser.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/tools/tinyurl.c b/src/tools/tinyurl.c
index 36be4971..0750709f 100644
--- a/src/tools/tinyurl.c
+++ b/src/tools/tinyurl.c
@@ -1,7 +1,7 @@
 /*
  * tinyurl.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/tools/tinyurl.h b/src/tools/tinyurl.h
index 37e849c4..eb85f66f 100644
--- a/src/tools/tinyurl.h
+++ b/src/tools/tinyurl.h
@@ -1,7 +1,7 @@
 /*
  * tinyurl.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/ui/buffer.c b/src/ui/buffer.c
index 2d29ca65..b933ac8c 100644
--- a/src/ui/buffer.c
+++ b/src/ui/buffer.c
@@ -1,7 +1,7 @@
 /*
  * buffer.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/ui/buffer.h b/src/ui/buffer.h
index b16bcb51..243b003a 100644
--- a/src/ui/buffer.h
+++ b/src/ui/buffer.h
@@ -1,7 +1,7 @@
 /*
  * buffer.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/ui/chatwin.c b/src/ui/chatwin.c
index 31604db4..98431a60 100644
--- a/src/ui/chatwin.c
+++ b/src/ui/chatwin.c
@@ -1,7 +1,7 @@
 /*
  * chatwin.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/ui/mucconfwin.c b/src/ui/confwin.c
index 7a658a1e..042e0f8d 100644
--- a/src/ui/mucconfwin.c
+++ b/src/ui/confwin.c
@@ -1,7 +1,7 @@
 /*
- * mucconfwin.c
+ * confwin.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.
  *
@@ -40,10 +40,10 @@
 #include "ui/win_types.h"
 #include "ui/window_list.h"
 
-static void _mucconfwin_form_field(ProfWin *window, char *tag, FormField *field);
+static void _confwin_form_field(ProfWin *window, char *tag, FormField *field);
 
 void
-mucconfwin_show_form(ProfMucConfWin *confwin)
+confwin_show_form(ProfConfWin *confwin)
 {
     ProfWin *window = (ProfWin*) confwin;
     if (confwin->form->title) {
@@ -54,7 +54,7 @@ mucconfwin_show_form(ProfMucConfWin *confwin)
     }
     win_println(window, THEME_DEFAULT, '-', "");
 
-    mucconfwin_form_help(confwin);
+    confwin_form_help(confwin);
 
     GSList *fields = confwin->form->fields;
     GSList *curr_field = fields;
@@ -68,7 +68,7 @@ mucconfwin_show_form(ProfMucConfWin *confwin)
             }
         } else if (g_strcmp0(field->type, "hidden") != 0 && field->var) {
             char *tag = g_hash_table_lookup(confwin->form->var_to_tag, field->var);
-            _mucconfwin_form_field(window, tag, field);
+            _confwin_form_field(window, tag, field);
         }
 
         curr_field = g_slist_next(curr_field);
@@ -76,35 +76,37 @@ mucconfwin_show_form(ProfMucConfWin *confwin)
 }
 
 void
-mucconfwin_show_form_field(ProfMucConfWin *confwin, DataForm *form, char *tag)
+confwin_show_form_field(ProfConfWin *confwin, DataForm *form, char *tag)
 {
     assert(confwin != NULL);
 
     FormField *field = form_get_field_by_tag(form, tag);
     ProfWin *window = (ProfWin*)confwin;
-    _mucconfwin_form_field(window, tag, field);
+    _confwin_form_field(window, tag, field);
     win_println(window, THEME_DEFAULT, '-', "");
 }
 
 void
-mucconfwin_handle_configuration(ProfMucConfWin *confwin, DataForm *form)
+confwin_handle_configuration(ProfConfWin *confwin, DataForm *form)
 {
     assert(confwin != NULL);
 
     ProfWin *window = (ProfWin*)confwin;
     ui_focus_win(window);
 
-    mucconfwin_show_form(confwin);
+    confwin_show_form(confwin);
 
     win_println(window, THEME_DEFAULT, '-', "");
-    win_println(window, THEME_DEFAULT, '-', "Use '/form submit' to save changes.");
+    if (confwin->submit != NULL) {
+        win_println(window, THEME_DEFAULT, '-', "Use '/form submit' to save changes.");
+    }
     win_println(window, THEME_DEFAULT, '-', "Use '/form cancel' to cancel changes.");
     win_println(window, THEME_DEFAULT, '-', "See '/form help' for more information.");
     win_println(window, THEME_DEFAULT, '-', "");
 }
 
 void
-mucconfwin_field_help(ProfMucConfWin *confwin, char *tag)
+confwin_field_help(ProfConfWin *confwin, char *tag)
 {
     assert(confwin != NULL);
 
@@ -187,7 +189,7 @@ mucconfwin_field_help(ProfMucConfWin *confwin, char *tag)
 }
 
 void
-mucconfwin_form_help(ProfMucConfWin *confwin)
+confwin_form_help(ProfConfWin *confwin)
 {
     assert(confwin != NULL);
 
@@ -200,7 +202,7 @@ mucconfwin_form_help(ProfMucConfWin *confwin)
 }
 
 static void
-_mucconfwin_form_field(ProfWin *window, char *tag, FormField *field)
+_confwin_form_field(ProfWin *window, char *tag, FormField *field)
 {
     win_print(window, THEME_AWAY, '-', "[%s] ", tag);
     win_append(window, THEME_DEFAULT, "%s", field->label);
@@ -258,7 +260,7 @@ _mucconfwin_form_field(ProfWin *window, char *tag, FormField *field)
             if (value == NULL) {
                 win_appendln(window, THEME_OFFLINE, "FALSE");
             } else {
-                if (g_strcmp0(value, "0") == 0) {
+                if (g_strcmp0(value, "0") == 0 || g_strcmp0(value, "false") == 0) {
                     win_appendln(window, THEME_OFFLINE, "FALSE");
                 } else {
                     win_appendln(window, THEME_ONLINE, "TRUE");
@@ -331,7 +333,7 @@ _mucconfwin_form_field(ProfWin *window, char *tag, FormField *field)
 }
 
 char*
-mucconfwin_get_string(ProfMucConfWin *confwin)
+confwin_get_string(ProfConfWin *confwin)
 {
     assert(confwin != NULL);
 
diff --git a/src/ui/console.c b/src/ui/console.c
index 9bead705..e5c12158 100644
--- a/src/ui/console.c
+++ b/src/ui/console.c
@@ -1,7 +1,7 @@
 /*
  * console.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.
  *
@@ -1282,12 +1282,12 @@ cons_time_setting(void)
         cons_show("Time MUC (/time)                    : %s", pref_time_muc);
     prefs_free_string(pref_time_muc);
 
-    char *pref_time_mucconf = prefs_get_string(PREF_TIME_MUCCONFIG);
-    if (g_strcmp0(pref_time_mucconf, "off") == 0)
-        cons_show("Time MUC config (/time)             : OFF");
+    char *pref_time_conf = prefs_get_string(PREF_TIME_CONFIG);
+    if (g_strcmp0(pref_time_conf, "off") == 0)
+        cons_show("Time config (/time)             : OFF");
     else
-        cons_show("Time MUC config (/time)             : %s", pref_time_mucconf);
-    prefs_free_string(pref_time_mucconf);
+        cons_show("Time config (/time)             : %s", pref_time_conf);
+    prefs_free_string(pref_time_conf);
 
     char *pref_time_private = prefs_get_string(PREF_TIME_PRIVATE);
     if (g_strcmp0(pref_time_private, "off") == 0)
@@ -2387,13 +2387,13 @@ _cons_splash_logo(void)
     ProfWin *console = wins_get_console();
     win_println(console, THEME_DEFAULT, '-', "Welcome to");
 
-    win_println(console, THEME_SPLASH, '-', "                   ___            _           ");
-    win_println(console, THEME_SPLASH, '-', "                  / __)          (_)_         ");
-    win_println(console, THEME_SPLASH, '-', " ____   ____ ___ | |__ ____ ____  _| |_ _   _ ");
-    win_println(console, THEME_SPLASH, '-', "|  _ \\ / ___) _ \\|  __) _  |  _ \\| |  _) | | |");
-    win_println(console, THEME_SPLASH, '-', "| | | | |  | |_| | | ( ( | | | | | | |_| |_| |");
-    win_println(console, THEME_SPLASH, '-', "| ||_/|_|   \\___/|_|  \\_||_|_| |_|_|\\___)__  |");
-    win_println(console, THEME_SPLASH, '-', "|_|                                    (____/ ");
+    win_println(console, THEME_SPLASH, '-', "                  ___            _           ");
+    win_println(console, THEME_SPLASH, '-', "                 / __)          (_)_         ");
+    win_println(console, THEME_SPLASH, '-', " ____   ___ ___ | |__ ____ ____  _| |_ _   _ ");
+    win_println(console, THEME_SPLASH, '-', "|  _ \\ / __) _ \\|  __) _  |  _ \\| |  _) | | |");
+    win_println(console, THEME_SPLASH, '-', "| | ) | | | (_) | | | ( | | | | | | |_| |_| |");
+    win_println(console, THEME_SPLASH, '-', "| ||_/|_|  \\___/|_|  \\_||_|_| |_|_|\\___)__  |");
+    win_println(console, THEME_SPLASH, '-', "|_|                                   (____/ ");
     win_println(console, THEME_SPLASH, '-', "");
 
     if (strcmp(PACKAGE_STATUS, "development") == 0) {
diff --git a/src/ui/core.c b/src/ui/core.c
index 5246d06a..3e141819 100644
--- a/src/ui/core.c
+++ b/src/ui/core.c
@@ -1,7 +1,7 @@
 /*
  * core.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.
  *
@@ -632,8 +632,8 @@ ui_win_has_unsaved_form(int num)
 {
     ProfWin *window = wins_get_by_num(num);
 
-    if (window->type == WIN_MUC_CONFIG) {
-        ProfMucConfWin *confwin = (ProfMucConfWin*)window;
+    if (window->type == WIN_CONFIG) {
+        ProfConfWin *confwin = (ProfConfWin*)window;
         assert(confwin->memcheck == PROFCONFWIN_MEMCHECK);
         return confwin->form->modified;
     } else {
@@ -651,13 +651,13 @@ ui_focus_win(ProfWin *window)
     }
 
     ProfWin *old_current = wins_get_current();
-    if (old_current->type == WIN_MUC_CONFIG) {
-        ProfMucConfWin *confwin = (ProfMucConfWin*)old_current;
+    if (old_current->type == WIN_CONFIG) {
+        ProfConfWin *confwin = (ProfConfWin*)old_current;
         cmd_ac_remove_form_fields(confwin->form);
     }
 
-    if (window->type == WIN_MUC_CONFIG) {
-        ProfMucConfWin *confwin = (ProfMucConfWin*)window;
+    if (window->type == WIN_CONFIG) {
+        ProfConfWin *confwin = (ProfConfWin*)window;
         cmd_ac_add_form_fields(confwin->form);
     }
 
@@ -681,8 +681,8 @@ void
 ui_close_win(int index)
 {
     ProfWin *window = wins_get_by_num(index);
-    if (window && window->type == WIN_MUC_CONFIG) {
-        ProfMucConfWin *confwin = (ProfMucConfWin*)window;
+    if (window && window->type == WIN_CONFIG) {
+        ProfConfWin *confwin = (ProfConfWin*)window;
         if (confwin->form) {
             cmd_ac_remove_form_fields(confwin->form);
         }
@@ -1135,7 +1135,7 @@ ui_handle_room_config_submit_result(const char *const roomjid)
 
         GString *form_recipient = g_string_new(roomjid);
         g_string_append(form_recipient, " config");
-        form_window = (ProfWin*) wins_get_muc_conf(form_recipient->str);
+        form_window = (ProfWin*) wins_get_conf(form_recipient->str);
         g_string_free(form_recipient, TRUE);
 
         if (form_window) {
@@ -1167,7 +1167,7 @@ ui_handle_room_config_submit_result_error(const char *const roomjid, const char
 
         GString *form_recipient = g_string_new(roomjid);
         g_string_append(form_recipient, " config");
-        form_window = (ProfWin*) wins_get_muc_conf(form_recipient->str);
+        form_window = (ProfWin*) wins_get_conf(form_recipient->str);
         g_string_free(form_recipient, TRUE);
 
         if (form_window) {
diff --git a/src/ui/inputwin.c b/src/ui/inputwin.c
index 654a4602..89d86faf 100644
--- a/src/ui/inputwin.c
+++ b/src/ui/inputwin.c
@@ -1,7 +1,7 @@
 /*
  * inputwin.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.
  *
@@ -98,6 +98,7 @@ static void _inp_rl_linehandler(char *line);
 static int _inp_rl_tab_handler(int count, int key);
 static int _inp_rl_shift_tab_handler(int count, int key);
 static int _inp_rl_win_clear_handler(int count, int key);
+static int _inp_rl_win_close_handler(int count, int key);
 static int _inp_rl_win_1_handler(int count, int key);
 static int _inp_rl_win_2_handler(int count, int key);
 static int _inp_rl_win_3_handler(int count, int key);
@@ -387,6 +388,7 @@ _inp_rl_addfuncs(void)
     rl_add_funmap_entry("prof_subwin_pageup", _inp_rl_subwin_pageup_handler);
     rl_add_funmap_entry("prof_subwin_pagedown", _inp_rl_subwin_pagedown_handler);
     rl_add_funmap_entry("prof_win_clear", _inp_rl_win_clear_handler);
+    rl_add_funmap_entry("prof_win_close", _inp_rl_win_close_handler);
 }
 
 // Readline callbacks
@@ -504,6 +506,15 @@ _inp_rl_win_clear_handler(int count, int key)
 }
 
 static int
+_inp_rl_win_close_handler(int count, int key)
+{
+    ProfWin *win = wins_get_current();
+    gchar* args = 0;
+    cmd_close(win, 0, &args);
+    return 0;
+}
+
+static int
 _inp_rl_tab_handler(int count, int key)
 {
     if (rl_point != rl_end || !rl_line_buffer) {
diff --git a/src/ui/inputwin.h b/src/ui/inputwin.h
index db36cf70..af53694a 100644
--- a/src/ui/inputwin.h
+++ b/src/ui/inputwin.h
@@ -1,7 +1,7 @@
 /*
  * inputwin.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/ui/mucwin.c b/src/ui/mucwin.c
index f64a401e..fc13a9b1 100644
--- a/src/ui/mucwin.c
+++ b/src/ui/mucwin.c
@@ -1,7 +1,7 @@
 /*
  * mucwin.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/ui/notifier.c b/src/ui/notifier.c
index 452ae026..970d04eb 100644
--- a/src/ui/notifier.c
+++ b/src/ui/notifier.c
@@ -1,7 +1,7 @@
 /*
  * notifier.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/ui/occupantswin.c b/src/ui/occupantswin.c
index f6bb2197..ca507394 100644
--- a/src/ui/occupantswin.c
+++ b/src/ui/occupantswin.c
@@ -1,7 +1,7 @@
 /*
  * occupantswin.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/ui/privwin.c b/src/ui/privwin.c
index 8de59683..459c3e80 100644
--- a/src/ui/privwin.c
+++ b/src/ui/privwin.c
@@ -1,7 +1,7 @@
 /*
  * privwin.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/ui/rosterwin.c b/src/ui/rosterwin.c
index 3a38109d..89575e0d 100644
--- a/src/ui/rosterwin.c
+++ b/src/ui/rosterwin.c
@@ -1,7 +1,7 @@
 /*
  * rosterwin.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/ui/screen.c b/src/ui/screen.c
index 22548a75..dfb67ad9 100644
--- a/src/ui/screen.c
+++ b/src/ui/screen.c
@@ -1,7 +1,7 @@
 /*
  * screen.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/ui/screen.h b/src/ui/screen.h
index e382ab3e..f42cad46 100644
--- a/src/ui/screen.h
+++ b/src/ui/screen.h
@@ -1,7 +1,7 @@
 /*
  * screen.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/ui/statusbar.c b/src/ui/statusbar.c
index ac1d7498..d0ce7309 100644
--- a/src/ui/statusbar.c
+++ b/src/ui/statusbar.c
@@ -1,7 +1,7 @@
 /*
  * statusbar.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.
  *
@@ -544,7 +544,7 @@ _display_name(StatusBarTab *tab)
         } else {
             fullname = strdup(tab->identifier);
         }
-    } else if (tab->window_type == WIN_MUC_CONFIG) {
+    } else if (tab->window_type == WIN_CONFIG) {
         char *pref = prefs_get_string(PREF_STATUSBAR_ROOM);
         GString *display_str = g_string_new("");
         if (g_strcmp0("room", pref) == 0) {
diff --git a/src/ui/statusbar.h b/src/ui/statusbar.h
index de8b51cc..d97101e2 100644
--- a/src/ui/statusbar.h
+++ b/src/ui/statusbar.h
@@ -1,7 +1,7 @@
 /*
  * statusbar.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/ui/titlebar.c b/src/ui/titlebar.c
index 4f5383c2..f519fdd2 100644
--- a/src/ui/titlebar.c
+++ b/src/ui/titlebar.c
@@ -1,7 +1,7 @@
 /*
  * titlebar.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/ui/titlebar.h b/src/ui/titlebar.h
index 0aed4b2b..35c48040 100644
--- a/src/ui/titlebar.h
+++ b/src/ui/titlebar.h
@@ -1,7 +1,7 @@
 /*
  * titlebar.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/ui/tray.c b/src/ui/tray.c
index ecb0843b..4cdc2d2a 100644
--- a/src/ui/tray.c
+++ b/src/ui/tray.c
@@ -1,7 +1,7 @@
 /*
  * tray.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/ui/tray.h b/src/ui/tray.h
index c1e6ba77..b7b75fa9 100644
--- a/src/ui/tray.h
+++ b/src/ui/tray.h
@@ -1,7 +1,7 @@
 /*
  * tray.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/ui/ui.h b/src/ui/ui.h
index d344f855..ad5a1216 100644
--- a/src/ui/ui.h
+++ b/src/ui/ui.h
@@ -1,7 +1,7 @@
 /*
  * ui.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.
  *
@@ -217,13 +217,13 @@ void privwin_room_kicked(ProfPrivateWin *privwin, const char *const actor, const
 void privwin_room_banned(ProfPrivateWin *privwin, const char *const actor, const char *const reason);
 void privwin_room_joined(ProfPrivateWin *privwin);
 
-// MUC room config window
-void mucconfwin_handle_configuration(ProfMucConfWin *confwin, DataForm *form);
-void mucconfwin_show_form(ProfMucConfWin *confwin);
-void mucconfwin_show_form_field(ProfMucConfWin *confwin, DataForm *form, char *tag);
-void mucconfwin_form_help(ProfMucConfWin *confwin);
-void mucconfwin_field_help(ProfMucConfWin *confwin, char *tag);
-char* mucconfwin_get_string(ProfMucConfWin *confwin);
+// config window
+void confwin_handle_configuration(ProfConfWin *confwin, DataForm *form);
+void confwin_show_form(ProfConfWin *confwin);
+void confwin_show_form_field(ProfConfWin *confwin, DataForm *form, char *tag);
+void confwin_form_help(ProfConfWin *confwin);
+void confwin_field_help(ProfConfWin *confwin, char *tag);
+char* confwin_get_string(ProfConfWin *confwin);
 
 // xml console
 void xmlwin_show(ProfXMLWin *xmlwin, const char *const msg);
@@ -346,7 +346,7 @@ ProfWin* win_create_console(void);
 ProfWin* win_create_xmlconsole(void);
 ProfWin* win_create_chat(const char *const barejid);
 ProfWin* win_create_muc(const char *const roomjid);
-ProfWin* win_create_muc_config(const char *const title, DataForm *form);
+ProfWin* win_create_config(const char *const title, DataForm *form, ProfConfWinCallback submit, ProfConfWinCallback cancel, const void *userdata);
 ProfWin* win_create_private(const char *const fulljid);
 ProfWin* win_create_plugin(const char *const plugin_name, const char *const tag);
 void win_update_virtual(ProfWin *window);
@@ -377,6 +377,11 @@ void win_show_info(ProfWin *window, PContact contact);
 void win_clear(ProfWin *window);
 char* win_get_tab_identifier(ProfWin *window);
 char* win_to_string(ProfWin *window);
+void win_command_list_error(ProfWin *window, const char *const error);
+void win_command_exec_error(ProfWin *window, const char *const command, const char *const error, ...);
+void win_handle_command_list(ProfWin *window, GSList *cmds);
+void win_handle_command_exec_status(ProfWin *window, const char *const type, const char *const value);
+void win_handle_command_exec_result_note(ProfWin *window, const char *const type, const char *const value);
 
 // desktop notifications
 void notifier_initialise(void);
diff --git a/src/ui/win_types.h b/src/ui/win_types.h
index 7fa75b34..92618a36 100644
--- a/src/ui/win_types.h
+++ b/src/ui/win_types.h
@@ -1,7 +1,7 @@
 /*
  * win_types.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.
  *
@@ -128,7 +128,7 @@ typedef enum {
     WIN_CONSOLE,
     WIN_CHAT,
     WIN_MUC,
-    WIN_MUC_CONFIG,
+    WIN_CONFIG,
     WIN_PRIVATE,
     WIN_XML,
     WIN_PLUGIN
@@ -172,12 +172,18 @@ typedef struct prof_muc_win_t {
     char *message_char;
 } ProfMucWin;
 
-typedef struct prof_mucconf_win_t {
+typedef struct prof_conf_win_t ProfConfWin;
+typedef void (*ProfConfWinCallback)(ProfConfWin *);
+
+struct prof_conf_win_t {
     ProfWin window;
     char *roomjid;
     DataForm *form;
     unsigned long memcheck;
-} ProfMucConfWin;
+    ProfConfWinCallback submit;
+    ProfConfWinCallback cancel;
+    const void *userdata;
+};
 
 typedef struct prof_private_win_t {
     ProfWin window;
diff --git a/src/ui/window.c b/src/ui/window.c
index 5543707d..cc2c2062 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -1,7 +1,7 @@
 /*
  * window.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.
  *
@@ -203,13 +203,16 @@ win_create_muc(const char *const roomjid)
 }
 
 ProfWin*
-win_create_muc_config(const char *const roomjid, DataForm *form)
+win_create_config(const char *const roomjid, DataForm *form, ProfConfWinCallback submit, ProfConfWinCallback cancel, const void *userdata)
 {
-    ProfMucConfWin *new_win = malloc(sizeof(ProfMucConfWin));
-    new_win->window.type = WIN_MUC_CONFIG;
+    ProfConfWin *new_win = malloc(sizeof(ProfConfWin));
+    new_win->window.type = WIN_CONFIG;
     new_win->window.layout = _win_create_simple_layout();
     new_win->roomjid = strdup(roomjid);
     new_win->form = form;
+    new_win->submit = submit;
+    new_win->cancel = cancel;
+    new_win->userdata = userdata;
 
     new_win->memcheck = PROFCONFWIN_MEMCHECK;
 
@@ -289,8 +292,8 @@ win_get_title(ProfWin *window)
         assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
         return strdup(mucwin->roomjid);
     }
-    if (window->type == WIN_MUC_CONFIG) {
-        ProfMucConfWin *confwin = (ProfMucConfWin*) window;
+    if (window->type == WIN_CONFIG) {
+        ProfConfWin *confwin = (ProfConfWin*) window;
         assert(confwin->memcheck == PROFCONFWIN_MEMCHECK);
         GString *title = g_string_new(confwin->roomjid);
         g_string_append(title, " config");
@@ -338,10 +341,10 @@ win_get_tab_identifier(ProfWin *window)
             ProfMucWin *mucwin = (ProfMucWin*)window;
             return strdup(mucwin->roomjid);
         }
-        case WIN_MUC_CONFIG:
+        case WIN_CONFIG:
         {
-            ProfMucConfWin *mucconfwin = (ProfMucConfWin*)window;
-            return strdup(mucconfwin->roomjid);
+            ProfConfWin *confwin = (ProfConfWin*)window;
+            return strdup(confwin->roomjid);
         }
         case WIN_PRIVATE:
         {
@@ -383,10 +386,10 @@ win_to_string(ProfWin *window)
             ProfMucWin *mucwin = (ProfMucWin*)window;
             return mucwin_get_string(mucwin);
         }
-        case WIN_MUC_CONFIG:
+        case WIN_CONFIG:
         {
-            ProfMucConfWin *mucconfwin = (ProfMucConfWin*)window;
-            return mucconfwin_get_string(mucconfwin);
+            ProfConfWin *confwin = (ProfConfWin*)window;
+            return confwin_get_string(confwin);
         }
         case WIN_PRIVATE:
         {
@@ -491,11 +494,11 @@ win_free(ProfWin* window)
         free(mucwin->message_char);
         break;
     }
-    case WIN_MUC_CONFIG:
+    case WIN_CONFIG:
     {
-        ProfMucConfWin *mucconf = (ProfMucConfWin*)window;
-        free(mucconf->roomjid);
-        form_destroy(mucconf->form);
+        ProfConfWin *conf = (ProfConfWin*)window;
+        free(conf->roomjid);
+        form_destroy(conf->form);
         break;
     }
     case WIN_PRIVATE:
@@ -1389,8 +1392,8 @@ _win_print(ProfWin *window, const char show_char, int pad_indent, GDateTime *tim
         case WIN_MUC:
             time_pref = prefs_get_string(PREF_TIME_MUC);
             break;
-        case WIN_MUC_CONFIG:
-            time_pref = prefs_get_string(PREF_TIME_MUCCONFIG);
+        case WIN_CONFIG:
+            time_pref = prefs_get_string(PREF_TIME_CONFIG);
             break;
         case WIN_PRIVATE:
             time_pref = prefs_get_string(PREF_TIME_PRIVATE);
@@ -1724,3 +1727,60 @@ win_sub_newline_lazy(WINDOW *win)
         wmove(win, cury+1, 0);
     }
 }
+
+void
+win_command_list_error(ProfWin *window, const char *const error)
+{
+    assert(window != NULL);
+
+    win_println(window, THEME_ERROR, '!', "Error retrieving command list: %s", error);
+}
+
+void
+win_command_exec_error(ProfWin *window, const char *const command, const char *const error, ...)
+{
+    assert(window != NULL);
+    va_list arg;
+    va_start(arg, error);
+    GString *msg = g_string_new(NULL);
+    g_string_vprintf(msg, error, arg);
+
+    win_println(window, THEME_ERROR, '!', "Error executing command %s: %s", command, msg->str);
+
+    g_string_free(msg, TRUE);
+    va_end(arg);
+}
+
+void
+win_handle_command_list(ProfWin *window, GSList *cmds)
+{
+    assert(window != NULL);
+
+    if (cmds) {
+        win_println(window, THEME_DEFAULT, '!', "Ad hoc commands:");
+        GSList *curr_cmd = cmds;
+        while (curr_cmd) {
+            const char *cmd = curr_cmd->data;
+            win_println(window, THEME_DEFAULT, '!', "  %s", cmd);
+            curr_cmd = g_slist_next(curr_cmd);
+        }
+        win_println(window, THEME_DEFAULT, '!', "");
+    } else {
+        win_println(window, THEME_DEFAULT, '!', "No commands found");
+        win_println(window, THEME_DEFAULT, '!', "");
+    }
+}
+
+void
+win_handle_command_exec_status(ProfWin *window, const char *const command, const char *const value)
+{
+    assert(window != NULL);
+    win_println(window, THEME_DEFAULT, '!', "%s %s", command, value);
+}
+
+void
+win_handle_command_exec_result_note(ProfWin *window, const char *const type, const char *const value)
+{
+    assert(window != NULL);
+    win_println(window, THEME_DEFAULT, '!', value);
+}
diff --git a/src/ui/window.h b/src/ui/window.h
index 62f737de..17ebd226 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -1,7 +1,7 @@
 /*
  * window.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/ui/window_list.c b/src/ui/window_list.c
index 798f4e41..5ce68d63 100644
--- a/src/ui/window_list.c
+++ b/src/ui/window_list.c
@@ -1,7 +1,7 @@
 /*
  * window_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.
  *
@@ -139,16 +139,16 @@ wins_get_chat_unsubscribed(void)
     return result;
 }
 
-ProfMucConfWin*
-wins_get_muc_conf(const char *const roomjid)
+ProfConfWin*
+wins_get_conf(const char *const roomjid)
 {
     GList *values = g_hash_table_get_values(windows);
     GList *curr = values;
 
     while (curr) {
         ProfWin *window = curr->data;
-        if (window->type == WIN_MUC_CONFIG) {
-            ProfMucConfWin *confwin = (ProfMucConfWin*)window;
+        if (window->type == WIN_CONFIG) {
+            ProfConfWin *confwin = (ProfConfWin*)window;
             if (g_strcmp0(confwin->roomjid, roomjid) == 0) {
                 g_list_free(values);
                 return confwin;
@@ -364,7 +364,7 @@ wins_get_by_num(int i)
 }
 
 ProfWin*
-wins_get_by_string(char *str)
+wins_get_by_string(const char *str)
 {
     if (g_strcmp0(str, "console") == 0) {
         ProfWin *conswin = wins_get_console();
@@ -584,7 +584,7 @@ wins_close_by_num(int i)
                 autocomplete_remove(wins_close_ac, pluginwin->tag);
                 break;
             }
-            case WIN_MUC_CONFIG:
+            case WIN_CONFIG:
             default:
                 break;
             }
@@ -657,12 +657,12 @@ wins_new_muc(const char *const roomjid)
 }
 
 ProfWin*
-wins_new_muc_config(const char *const roomjid, DataForm *form)
+wins_new_config(const char *const roomjid, DataForm *form, ProfConfWinCallback submit, ProfConfWinCallback cancel, const void *userdata)
 {
     GList *keys = g_hash_table_get_keys(windows);
     int result = _wins_get_next_available_num(keys);
     g_list_free(keys);
-    ProfWin *newwin = win_create_muc_config(roomjid, form);
+    ProfWin *newwin = win_create_config(roomjid, form, submit, cancel, userdata);
     g_hash_table_insert(windows, GINT_TO_POINTER(result), newwin);
     return newwin;
 }
@@ -812,7 +812,7 @@ wins_get_prune_wins(void)
         ProfWin *window = curr->data;
         if (win_unread(window) == 0 &&
                 window->type != WIN_MUC &&
-                window->type != WIN_MUC_CONFIG &&
+                window->type != WIN_CONFIG &&
                 window->type != WIN_XML &&
                 window->type != WIN_CONSOLE) {
             result = g_slist_append(result, window);
diff --git a/src/ui/window_list.h b/src/ui/window_list.h
index 68e72739..6045d349 100644
--- a/src/ui/window_list.h
+++ b/src/ui/window_list.h
@@ -1,7 +1,7 @@
 /*
  * window_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.
  *
@@ -42,7 +42,7 @@ void wins_init(void);
 ProfWin* wins_new_xmlconsole(void);
 ProfWin* wins_new_chat(const char *const barejid);
 ProfWin* wins_new_muc(const char *const roomjid);
-ProfWin* wins_new_muc_config(const char *const roomjid, DataForm *form);
+ProfWin* wins_new_config(const char *const roomjid, DataForm *form, ProfConfWinCallback submit, ProfConfWinCallback cancel, const void *userdata);
 ProfWin* wins_new_private(const char *const fulljid);
 ProfWin* wins_new_plugin(const char *const plugin_name, const char *const tag);
 
@@ -56,7 +56,7 @@ ProfWin* wins_get_console(void);
 ProfChatWin* wins_get_chat(const char *const barejid);
 GList* wins_get_chat_unsubscribed(void);
 ProfMucWin* wins_get_muc(const char *const roomjid);
-ProfMucConfWin* wins_get_muc_conf(const char *const roomjid);
+ProfConfWin* wins_get_conf(const char *const roomjid);
 ProfPrivateWin* wins_get_private(const char *const fulljid);
 ProfPluginWin* wins_get_plugin(const char *const tag);
 ProfXMLWin* wins_get_xmlconsole(void);
@@ -68,7 +68,7 @@ ProfWin* wins_get_current(void);
 void wins_set_current_by_num(int i);
 
 ProfWin* wins_get_by_num(int i);
-ProfWin* wins_get_by_string(char *str);
+ProfWin* wins_get_by_string(const char *str);
 
 ProfWin* wins_get_next(void);
 ProfWin* wins_get_previous(void);
diff --git a/src/ui/xmlwin.c b/src/ui/xmlwin.c
index c76e91cf..5aa19f7d 100644
--- a/src/ui/xmlwin.c
+++ b/src/ui/xmlwin.c
@@ -1,7 +1,7 @@
 /*
  * xmlwin.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/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);