about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--Makefile.am2
-rw-r--r--configure.ac16
-rw-r--r--src/command/command.c2
-rw-r--r--src/command/commands.c453
-rw-r--r--src/otr/otr.c14
-rw-r--r--src/plugins/api.c20
-rw-r--r--src/plugins/api.h2
-rw-r--r--src/plugins/c_api.c7
-rw-r--r--src/plugins/disco.c65
-rw-r--r--src/plugins/disco.h43
-rw-r--r--src/plugins/plugins.c8
-rw-r--r--src/plugins/plugins.h2
-rw-r--r--src/plugins/profapi.c2
-rw-r--r--src/plugins/profapi.h2
-rw-r--r--src/plugins/python_api.c16
-rw-r--r--src/xmpp/capabilities.c22
-rw-r--r--src/xmpp/xmpp.h1
-rw-r--r--tests/unittests/test_cmd_account.c2
-rw-r--r--tests/unittests/xmpp/stub_xmpp.c1
19 files changed, 485 insertions, 195 deletions
diff --git a/Makefile.am b/Makefile.am
index ff08149c..95f6df92 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -50,6 +50,7 @@ core_sources = \
 	src/plugins/autocompleters.c src/plugins/autocompleters.h \
 	src/plugins/themes.c src/plugins/themes.h \
 	src/plugins/settings.c src/plugins/settings.h \
+	src/plugins/disco.c src/plugins/disco.h \
 	src/tray.h src/tray.c
 
 unittest_sources = \
@@ -84,6 +85,7 @@ unittest_sources = \
 	src/plugins/autocompleters.c src/plugins/autocompleters.h \
 	src/plugins/themes.c src/plugins/themes.h \
 	src/plugins/settings.c src/plugins/settings.h \
+	src/plugins/disco.c src/plugins/disco.h \
 	src/window_list.c src/window_list.h \
 	src/event/server_events.c src/event/server_events.h \
 	src/event/client_events.c src/event/client_events.h \
diff --git a/configure.ac b/configure.ac
index 0057c3e5..7cc51f2b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -64,7 +64,7 @@ AC_ARG_WITH([xscreensaver],
 AC_ARG_WITH([themes],
     [AS_HELP_STRING([--with-themes[[=PATH]]], [install themes (default yes)])])
 AC_ARG_ENABLE([icons],
-    [AS_HELP_STRING([--enable-icons], [enable icons])])
+    [AS_HELP_STRING([--enable-icons], [enable GTK tray icons])])
 
 ### plugins
 
@@ -120,7 +120,7 @@ fi
 ACX_PTHREAD
 LIBS="$PTHREAD_LIBS $LIBS"
 CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
-CC="$PTHREAD_CC"
+AS_IF([test "x$PTHREAD_CC" != x], [ CC="$PTHREAD_CC" ])
 
 ### Check for libmesode, fall back to libstrophe
 PKG_CHECK_MODULES([libmesode], [libmesode],
@@ -166,12 +166,12 @@ PKG_CHECK_MODULES([glib], [glib-2.0 >= 2.26], [],
 PKG_CHECK_MODULES([curl], [libcurl], [],
     [AC_MSG_ERROR([libcurl is required for profanity])])
 
-# Checks GTK+ 2.0
-PKG_CHECK_MODULES([GTK], [gtk+-2.0 >= 2.24.10],
-                  [AC_DEFINE([HAVE_GTK], [1], [libgtk module])],
-                  [AS_IF([test "x$enable_icons" = xyes],
-                         [AC_MSG_ERROR([gtk+-2.0 or higher is required for icons])],
-                         [AC_MSG_NOTICE([gtk+-2.0 not found, icons not enabled])])])
+AS_IF([test "x$enable_icons" != xno],
+    [PKG_CHECK_MODULES([GTK], [gtk+-2.0 >= 2.24.10],
+        [AC_DEFINE([HAVE_GTK], [1], [libgtk module])],
+        [AS_IF([test "x$enable_icons" = xyes],
+            [AC_MSG_ERROR([gtk+-2.0 or higher is required for icons])],
+            [AC_MSG_NOTICE([gtk+-2.0 not found, icons not enabled])])])])
 
 AS_IF([test "x$PLATFORM" != xosx],
     [AC_CHECK_LIB([readline], [main], [],
diff --git a/src/command/command.c b/src/command/command.c
index 906f2708..5895ad9d 100644
--- a/src/command/command.c
+++ b/src/command/command.c
@@ -3288,7 +3288,7 @@ _cmd_execute(ProfWin *window, const char *const command, const char *const inp)
             ui_invalid_command_usage(cmd->cmd, cmd->setting_func);
             return TRUE;
         }
-        if (args[0] && cmd->sub_funcs) {
+        if (args[0] && cmd->sub_funcs[0][0]) {
             int i = 0;
             while (cmd->sub_funcs[i][0]) {
                 if (g_strcmp0(args[0], (char*)cmd->sub_funcs[i][0]) == 0) {
diff --git a/src/command/commands.c b/src/command/commands.c
index 7e2825e8..58777a2d 100644
--- a/src/command/commands.c
+++ b/src/command/commands.c
@@ -636,205 +636,302 @@ cmd_account_default(ProfWin *window, const char *const command, gchar **args)
 }
 
 gboolean
-cmd_account_set(ProfWin *window, const char *const command, gchar **args)
+_account_set_jid(char *account_name, char *jid)
 {
-    if (g_strv_length(args) != 4) {
-        cons_bad_cmd_usage(command);
-        return TRUE;
+    Jid *jidp = jid_create(jid);
+    if (jidp == NULL) {
+        cons_show("Malformed jid: %s", jid);
+    } else {
+        accounts_set_jid(account_name, jidp->barejid);
+        cons_show("Updated jid for account %s: %s", account_name, jidp->barejid);
+        if (jidp->resourcepart) {
+            accounts_set_resource(account_name, jidp->resourcepart);
+            cons_show("Updated resource for account %s: %s", account_name, jidp->resourcepart);
+        }
+        cons_show("");
     }
+    jid_destroy(jidp);
 
-    char *account_name = args[1];
-    char *property = args[2];
-    char *value = args[3];
+    return TRUE;
+}
 
-    if (!accounts_account_exists(account_name)) {
-        cons_show("Account %s doesn't exist", account_name);
-        cons_show("");
-        return TRUE;
-    }
+gboolean
+_account_set_server(char *account_name, char *server)
+{
+    accounts_set_server(account_name, server);
+    cons_show("Updated server for account %s: %s", account_name, server);
+    cons_show("");
+    return TRUE;
+}
 
-    if (strcmp(property, "jid") == 0) {
-        Jid *jid = jid_create(args[3]);
-        if (jid == NULL) {
-            cons_show("Malformed jid: %s", value);
-        } else {
-            accounts_set_jid(account_name, jid->barejid);
-            cons_show("Updated jid for account %s: %s", account_name, jid->barejid);
-            if (jid->resourcepart) {
-                accounts_set_resource(account_name, jid->resourcepart);
-                cons_show("Updated resource for account %s: %s", account_name, jid->resourcepart);
-            }
-            cons_show("");
-        }
-        jid_destroy(jid);
-    } else if (strcmp(property, "server") == 0) {
-        accounts_set_server(account_name, value);
-        cons_show("Updated server for account %s: %s", account_name, value);
+gboolean
+_account_set_port(char *account_name, char *port)
+{
+    int porti;
+    char *err_msg = NULL;
+    gboolean res = strtoi_range(port, &porti, 1, 65535, &err_msg);
+    if (!res) {
+        cons_show(err_msg);
         cons_show("");
-    } else if (strcmp(property, "port") == 0) {
-        int port;
-        char *err_msg = NULL;
-        gboolean res = strtoi_range(value, &port, 1, 65535, &err_msg);
-        if (!res) {
-            cons_show(err_msg);
-            cons_show("");
-            free(err_msg);
-            return TRUE;
-        } else {
-            accounts_set_port(account_name, port);
-            cons_show("Updated port for account %s: %s", account_name, value);
-            cons_show("");
-        }
-    } else if (strcmp(property, "resource") == 0) {
-        accounts_set_resource(account_name, value);
-        if (jabber_get_connection_status() == JABBER_CONNECTED) {
-            cons_show("Updated resource for account %s: %s, you will need to reconnect to pick up the change.", account_name, value);
-        } else {
-            cons_show("Updated resource for account %s: %s", account_name, value);
-        }
+        free(err_msg);
+    } else {
+        accounts_set_port(account_name, porti);
+        cons_show("Updated port for account %s: %s", account_name, port);
         cons_show("");
-    } else if (strcmp(property, "password") == 0) {
-        if(accounts_get_account(account_name)->eval_password) {
-            cons_show("Cannot set password when eval_password is set.");
-        } else {
-            accounts_set_password(account_name, value);
-            cons_show("Updated password for account %s", account_name);
-            cons_show("");
-        }
-    } else if (strcmp(property, "eval_password") == 0) {
-        if(accounts_get_account(account_name)->password) {
-            cons_show("Cannot set eval_password when password is set.");
-        } else {
-            accounts_set_eval_password(account_name, value);
-            cons_show("Updated eval_password for account %s", account_name);
-            cons_show("");
-        }
-    } else if (strcmp(property, "muc") == 0) {
-        accounts_set_muc_service(account_name, value);
-        cons_show("Updated muc service for account %s: %s", account_name, value);
+    }
+    return TRUE;
+}
+
+gboolean
+_account_set_resource(char *account_name, char *resource)
+{
+    accounts_set_resource(account_name, resource);
+    if (jabber_get_connection_status() == JABBER_CONNECTED) {
+        cons_show("Updated resource for account %s: %s, reconnect to pick up the change.", account_name, resource);
+    } else {
+        cons_show("Updated resource for account %s: %s", account_name, resource);
+    }
+    cons_show("");
+    return TRUE;
+}
+
+gboolean
+_account_set_password(char *account_name, char *password)
+{
+    ProfAccount *account = accounts_get_account(account_name);
+    if (account->eval_password) {
+        cons_show("Cannot set password when eval_password is set.");
+    } else {
+        accounts_set_password(account_name, password);
+        cons_show("Updated password for account %s", account_name);
         cons_show("");
-    } else if (strcmp(property, "nick") == 0) {
-        accounts_set_muc_nick(account_name, value);
-        cons_show("Updated muc nick for account %s: %s", account_name, value);
+    }
+    account_free(account);
+    return TRUE;
+}
+
+gboolean
+_account_set_eval_password(char *account_name, char *eval_password)
+{
+    ProfAccount *account = accounts_get_account(account_name);
+    if(account->password) {
+        cons_show("Cannot set eval_password when password is set.");
+    } else {
+        accounts_set_eval_password(account_name, eval_password);
+        cons_show("Updated eval_password for account %s", account_name);
         cons_show("");
-    } else if (strcmp(property, "otr") == 0) {
-        if ((g_strcmp0(value, "manual") != 0)
-                && (g_strcmp0(value, "opportunistic") != 0)
-                && (g_strcmp0(value, "always") != 0)) {
-            cons_show("OTR policy must be one of: manual, opportunistic or always.");
-        } else {
-            accounts_set_otr_policy(account_name, value);
-            cons_show("Updated OTR policy for account %s: %s", account_name, value);
-            cons_show("");
-        }
-    } else if (strcmp(property, "status") == 0) {
-        if (!valid_resource_presence_string(value) && (strcmp(value, "last") != 0)) {
-            cons_show("Invalid status: %s", value);
-        } else {
-            accounts_set_login_presence(account_name, value);
-            cons_show("Updated login status for account %s: %s", account_name, value);
-        }
+    }
+    account_free(account);
+    return TRUE;
+}
+
+gboolean
+_account_set_muc(char *account_name, char *muc)
+{
+    accounts_set_muc_service(account_name, muc);
+    cons_show("Updated muc service for account %s: %s", account_name, muc);
+    cons_show("");
+    return TRUE;
+}
+
+gboolean
+_account_set_nick(char *account_name, char *nick)
+{
+    accounts_set_muc_nick(account_name, nick);
+    cons_show("Updated muc nick for account %s: %s", account_name, nick);
+    cons_show("");
+    return TRUE;
+}
+
+gboolean
+_account_set_otr(char *account_name, char *policy)
+{
+    if ((g_strcmp0(policy, "manual") != 0)
+            && (g_strcmp0(policy, "opportunistic") != 0)
+            && (g_strcmp0(policy, "always") != 0)) {
+        cons_show("OTR policy must be one of: manual, opportunistic or always.");
+    } else {
+        accounts_set_otr_policy(account_name, policy);
+        cons_show("Updated OTR policy for account %s: %s", account_name, policy);
         cons_show("");
-    } else if (strcmp(property, "pgpkeyid") == 0) {
+    }
+    return TRUE;
+}
+
+gboolean
+_account_set_status(char *account_name, char *status)
+{
+    if (!valid_resource_presence_string(status) && (strcmp(status, "last") != 0)) {
+        cons_show("Invalid status: %s", status);
+    } else {
+        accounts_set_login_presence(account_name, status);
+        cons_show("Updated login status for account %s: %s", account_name, status);
+    }
+    cons_show("");
+    return TRUE;
+}
+
+gboolean
+_account_set_pgpkeyid(char *account_name, char *pgpkeyid)
+{
 #ifdef HAVE_LIBGPGME
-        char *err_str = NULL;
-        if (!p_gpg_valid_key(value, &err_str)) {
-            cons_show("Invalid PGP key ID specified: %s, see /pgp keys", err_str);
-        } else {
-            accounts_set_pgp_keyid(account_name, value);
-            cons_show("Updated PGP key ID for account %s: %s", account_name, value);
-        }
-        free(err_str);
+    char *err_str = NULL;
+    if (!p_gpg_valid_key(pgpkeyid, &err_str)) {
+        cons_show("Invalid PGP key ID specified: %s, see /pgp keys", err_str);
+    } else {
+        accounts_set_pgp_keyid(account_name, pgpkeyid);
+        cons_show("Updated PGP key ID for account %s: %s", account_name, pgpkeyid);
+    }
+    free(err_str);
 #else
-        cons_show("PGP support is not included in this build.");
+    cons_show("PGP support is not included in this build.");
 #endif
-        cons_show("");
-    } else if (strcmp(property, "startscript") == 0) {
-        accounts_set_script_start(account_name, value);
-        cons_show("Updated start script for account %s: %s", account_name, value);
-    } else if (strcmp(property, "theme") == 0) {
-        if (theme_exists(value)) {
-            accounts_set_theme(account_name, value);
-            if (jabber_get_connection_status() == JABBER_CONNECTED) {
-                ProfAccount *account = accounts_get_account(jabber_get_account_name());
-                if (account) {
-                    if (g_strcmp0(account->name, account_name) == 0) {
-                        theme_load(value);
-                        ui_load_colours();
-                        if (prefs_get_boolean(PREF_ROSTER)) {
-                            ui_show_roster();
-                        } else {
-                            ui_hide_roster();
-                        }
-                        if (prefs_get_boolean(PREF_OCCUPANTS)) {
-                            ui_show_all_room_rosters();
-                        } else {
-                            ui_hide_all_room_rosters();
-                        }
-                        ui_redraw();
-                    }
-                    account_free(account);
-                }
-            }
-            cons_show("Updated theme for account %s: %s", account_name, value);
-        } else {
-            cons_show("Theme does not exist: %s", value);
-        }
-    } else if (strcmp(property, "tls") == 0) {
-        if ((g_strcmp0(value, "force") != 0)
-                && (g_strcmp0(value, "allow") != 0)
-                && (g_strcmp0(value, "disable") != 0)) {
-            cons_show("TLS policy must be one of: force, allow or disable.");
-        } else {
-            accounts_set_tls_policy(account_name, value);
-            cons_show("Updated TLS policy for account %s: %s", account_name, value);
-            cons_show("");
-        }
-    } else if (valid_resource_presence_string(property)) {
-        int intval;
-        char *err_msg = NULL;
-        gboolean res = strtoi_range(value, &intval, -128, 127, &err_msg);
-        if (res) {
-            resource_presence_t presence_type = resource_presence_from_string(property);
-            switch (presence_type)
-            {
-            case (RESOURCE_ONLINE):
-                accounts_set_priority_online(account_name, intval);
-                break;
-            case (RESOURCE_CHAT):
-                accounts_set_priority_chat(account_name, intval);
-                break;
-            case (RESOURCE_AWAY):
-                accounts_set_priority_away(account_name, intval);
-                break;
-            case (RESOURCE_XA):
-                accounts_set_priority_xa(account_name, intval);
-                break;
-            case (RESOURCE_DND):
-                accounts_set_priority_dnd(account_name, intval);
-                break;
-            }
+    cons_show("");
+    return TRUE;
+}
 
-            jabber_conn_status_t conn_status = jabber_get_connection_status();
-            if (conn_status == JABBER_CONNECTED) {
-                char *connected_account = jabber_get_account_name();
-                resource_presence_t last_presence = accounts_get_last_presence(connected_account);
-                if (presence_type == last_presence) {
-                    char *message = jabber_get_presence_message();
-                    cl_ev_presence_send(last_presence, message, 0);
+gboolean
+_account_set_startscript(char *account_name, char *script)
+{
+    accounts_set_script_start(account_name, script);
+    cons_show("Updated start script for account %s: %s", account_name, script);
+    return TRUE;
+}
+
+gboolean
+_account_set_theme(char *account_name, char *theme)
+{
+    if (!theme_exists(theme)) {
+        cons_show("Theme does not exist: %s", theme);
+        return TRUE;
+    }
+
+    accounts_set_theme(account_name, theme);
+    if (jabber_get_connection_status() == JABBER_CONNECTED) {
+        ProfAccount *account = accounts_get_account(jabber_get_account_name());
+        if (account) {
+            if (g_strcmp0(account->name, account_name) == 0) {
+                theme_load(theme);
+                ui_load_colours();
+                if (prefs_get_boolean(PREF_ROSTER)) {
+                    ui_show_roster();
+                } else {
+                    ui_hide_roster();
                 }
+                if (prefs_get_boolean(PREF_OCCUPANTS)) {
+                    ui_show_all_room_rosters();
+                } else {
+                    ui_hide_all_room_rosters();
+                }
+                ui_redraw();
             }
-            cons_show("Updated %s priority for account %s: %s", property, account_name, value);
-            cons_show("");
-        } else {
-            cons_show(err_msg);
-            free(err_msg);
+            account_free(account);
         }
+    }
+    cons_show("Updated theme for account %s: %s", account_name, theme);
+    return TRUE;
+}
+
+gboolean
+_account_set_tls(char *account_name, char *policy)
+{
+    if ((g_strcmp0(policy, "force") != 0)
+            && (g_strcmp0(policy, "allow") != 0)
+            && (g_strcmp0(policy, "disable") != 0)) {
+        cons_show("TLS policy must be one of: force, allow or disable.");
     } else {
-        cons_show("Invalid property: %s", property);
+        accounts_set_tls_policy(account_name, policy);
+        cons_show("Updated TLS policy for account %s: %s", account_name, policy);
+        cons_show("");
+    }
+    return TRUE;
+}
+
+gboolean
+_account_set_presence_priority(char *account_name, char *presence, char *priority)
+{
+    int intval;
+    char *err_msg = NULL;
+    gboolean res = strtoi_range(priority, &intval, -128, 127, &err_msg);
+    if (!res) {
+        cons_show(err_msg);
+        free(err_msg);
+        return TRUE;
+    }
+
+    resource_presence_t presence_type = resource_presence_from_string(presence);
+    switch (presence_type)
+    {
+    case (RESOURCE_ONLINE):
+        accounts_set_priority_online(account_name, intval);
+        break;
+    case (RESOURCE_CHAT):
+        accounts_set_priority_chat(account_name, intval);
+        break;
+    case (RESOURCE_AWAY):
+        accounts_set_priority_away(account_name, intval);
+        break;
+    case (RESOURCE_XA):
+        accounts_set_priority_xa(account_name, intval);
+        break;
+    case (RESOURCE_DND):
+        accounts_set_priority_dnd(account_name, intval);
+        break;
+    }
+
+    jabber_conn_status_t conn_status = jabber_get_connection_status();
+    if (conn_status == JABBER_CONNECTED) {
+        char *connected_account = jabber_get_account_name();
+        resource_presence_t last_presence = accounts_get_last_presence(connected_account);
+        if (presence_type == last_presence) {
+            char *message = jabber_get_presence_message();
+            cl_ev_presence_send(last_presence, message, 0);
+        }
+    }
+    cons_show("Updated %s priority for account %s: %s", presence, account_name, priority);
+    cons_show("");
+    return TRUE;
+}
+
+gboolean
+cmd_account_set(ProfWin *window, const char *const command, gchar **args)
+{
+    if (g_strv_length(args) != 4) {
+        cons_bad_cmd_usage(command);
+        return TRUE;
+    }
+
+    char *account_name = args[1];
+    if (!accounts_account_exists(account_name)) {
+        cons_show("Account %s doesn't exist", account_name);
         cons_show("");
+        return TRUE;
     }
 
+    char *property = args[2];
+    char *value = args[3];
+    if (strcmp(property, "jid") == 0)           return _account_set_jid(account_name, value);
+    if (strcmp(property, "server") == 0)        return _account_set_server(account_name, value);
+    if (strcmp(property, "port") == 0)          return _account_set_port(account_name, value);
+    if (strcmp(property, "resource") == 0)      return _account_set_resource(account_name, value);
+    if (strcmp(property, "password") == 0)      return _account_set_password(account_name, value);
+    if (strcmp(property, "eval_password") == 0) return _account_set_eval_password(account_name, value);
+    if (strcmp(property, "muc") == 0)           return _account_set_muc(account_name, value);
+    if (strcmp(property, "nick") == 0)          return _account_set_nick(account_name, value);
+    if (strcmp(property, "otr") == 0)           return _account_set_otr(account_name, value);
+    if (strcmp(property, "status") == 0)        return _account_set_status(account_name, value);
+    if (strcmp(property, "pgpkeyid") == 0)      return _account_set_pgpkeyid(account_name, value);
+    if (strcmp(property, "startscript") == 0)   return _account_set_startscript(account_name, value);
+    if (strcmp(property, "theme") == 0)         return _account_set_theme(account_name, value);
+    if (strcmp(property, "tls") == 0)           return _account_set_tls(account_name, value);
+
+    if (valid_resource_presence_string(property)) {
+        return _account_set_presence_priority(account_name, property, value);
+    }
+
+    cons_show("Invalid property: %s", property);
+    cons_show("");
+
     return TRUE;
 }
 
diff --git a/src/otr/otr.c b/src/otr/otr.c
index b1cf1e16..7b7b8e8e 100644
--- a/src/otr/otr.c
+++ b/src/otr/otr.c
@@ -138,7 +138,7 @@ cb_write_fingerprints(void *opdata)
     GString *fpsfilename = g_string_new(basedir->str);
     g_string_append(fpsfilename, "fingerprints.txt");
     err = otrl_privkey_write_fingerprints(user_state, fpsfilename->str);
-    if (!err == GPG_ERR_NO_ERROR) {
+    if (err != GPG_ERR_NO_ERROR) {
         log_error("Failed to write fingerprints file");
         cons_show_error("Failed to create fingerprints file");
     }
@@ -243,7 +243,7 @@ otr_on_connect(ProfAccount *account)
     } else {
         log_info("Loading OTR private key %s", keysfilename->str);
         err = otrl_privkey_read(user_state, keysfilename->str);
-        if (!err == GPG_ERR_NO_ERROR) {
+        if (err != GPG_ERR_NO_ERROR) {
             log_warning("Failed to read OTR private key file: %s", keysfilename->str);
             cons_show_error("Failed to read OTR private key file: %s", keysfilename->str);
             g_string_free(basedir, TRUE);
@@ -271,7 +271,7 @@ otr_on_connect(ProfAccount *account)
     } else {
         log_info("Loading OTR fingerprints %s", fpsfilename->str);
         err = otrl_privkey_read_fingerprints(user_state, fpsfilename->str, NULL, NULL);
-        if (!err == GPG_ERR_NO_ERROR) {
+        if (err != GPG_ERR_NO_ERROR) {
             log_error("Failed to load OTR fingerprints file: %s", fpsfilename->str);
             g_string_free(basedir, TRUE);
             g_string_free(keysfilename, TRUE);
@@ -415,7 +415,7 @@ otr_keygen(ProfAccount *account)
     cons_show("Moving the mouse randomly around the screen may speed up the process!");
     ui_update();
     err = otrl_privkey_generate(user_state, keysfilename->str, account->jid, "xmpp");
-    if (!err == GPG_ERR_NO_ERROR) {
+    if (err != GPG_ERR_NO_ERROR) {
         g_string_free(basedir, TRUE);
         g_string_free(keysfilename, TRUE);
         log_error("Failed to generate private key");
@@ -430,7 +430,7 @@ otr_keygen(ProfAccount *account)
     g_string_append(fpsfilename, "fingerprints.txt");
     log_debug("Generating fingerprints file %s for %s", fpsfilename->str, jid);
     err = otrl_privkey_write_fingerprints(user_state, fpsfilename->str);
-    if (!err == GPG_ERR_NO_ERROR) {
+    if (err != GPG_ERR_NO_ERROR) {
         g_string_free(basedir, TRUE);
         g_string_free(keysfilename, TRUE);
         log_error("Failed to create fingerprints file");
@@ -440,7 +440,7 @@ otr_keygen(ProfAccount *account)
     log_info("Fingerprints file created");
 
     err = otrl_privkey_read(user_state, keysfilename->str);
-    if (!err == GPG_ERR_NO_ERROR) {
+    if (err != GPG_ERR_NO_ERROR) {
         g_string_free(basedir, TRUE);
         g_string_free(keysfilename, TRUE);
         log_error("Failed to load private key");
@@ -449,7 +449,7 @@ otr_keygen(ProfAccount *account)
     }
 
     err = otrl_privkey_read_fingerprints(user_state, fpsfilename->str, NULL, NULL);
-    if (!err == GPG_ERR_NO_ERROR) {
+    if (err != GPG_ERR_NO_ERROR) {
         g_string_free(basedir, TRUE);
         g_string_free(keysfilename, TRUE);
         log_error("Failed to load fingerprints");
diff --git a/src/plugins/api.c b/src/plugins/api.c
index 1aedee1f..0de9bab0 100644
--- a/src/plugins/api.c
+++ b/src/plugins/api.c
@@ -40,10 +40,12 @@
 
 #include "log.h"
 #include "event/server_events.h"
+#include "event/client_events.h"
 #include "plugins/callbacks.h"
 #include "plugins/autocompleters.h"
 #include "plugins/themes.h"
 #include "plugins/settings.h"
+#include "plugins/disco.h"
 #include "profanity.h"
 #include "ui/ui.h"
 #include "config/theme.h"
@@ -426,3 +428,21 @@ api_incoming_message(const char *const barejid, const char *const resource, cons
     // TODO handle all states
     sv_ev_activity((char*)barejid, (char*)resource, FALSE);
 }
+
+void
+api_disco_add_feature(char *feature)
+{
+    if (feature == NULL) {
+        return;
+    }
+
+    disco_add_feature(feature);
+    caps_reset_ver();
+
+    // resend presence to update server's disco info data for this client
+    if (jabber_get_connection_status() == JABBER_CONNECTED) {
+        resource_presence_t last_presence = accounts_get_last_presence(jabber_get_account_name());
+        cl_ev_presence_send(last_presence, jabber_get_presence_message(), 0);
+    }
+}
+
diff --git a/src/plugins/api.h b/src/plugins/api.h
index c22fb217..a835cd8b 100644
--- a/src/plugins/api.h
+++ b/src/plugins/api.h
@@ -86,4 +86,6 @@ void api_settings_set_int(const char *const group, const char *const key, int va
 
 void api_incoming_message(const char *const barejid, const char *const resource, const char *const message);
 
+void api_disco_add_feature(char *feature);
+
 #endif
diff --git a/src/plugins/c_api.c b/src/plugins/c_api.c
index 464bf39b..770fd9a3 100644
--- a/src/plugins/c_api.c
+++ b/src/plugins/c_api.c
@@ -260,6 +260,12 @@ c_api_incoming_message(char *barejid, char *resource, char *message)
     api_incoming_message(barejid, resource, message);
 }
 
+static void
+c_api_disco_add_feature(char *feature)
+{
+    api_disco_add_feature(feature);
+}
+
 void
 c_command_callback(PluginCommand *command, gchar **args)
 {
@@ -320,4 +326,5 @@ c_api_init(void)
     prof_settings_get_int = c_api_settings_get_int;
     prof_settings_set_int = c_api_settings_set_int;
     prof_incoming_message = c_api_incoming_message;
+    prof_disco_add_feature = c_api_disco_add_feature;
 }
diff --git a/src/plugins/disco.c b/src/plugins/disco.c
new file mode 100644
index 00000000..1d47ad9a
--- /dev/null
+++ b/src/plugins/disco.c
@@ -0,0 +1,65 @@
+/*
+ * disco.c
+ *
+ * Copyright (C) 2012 - 2016 James Booth <boothj5@gmail.com>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Profanity.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * In addition, as a special exception, the copyright holders give permission to
+ * link the code of portions of this program with the OpenSSL library under
+ * certain conditions as described in each individual source file, and
+ * distribute linked combinations including the two.
+ *
+ * You must obey the GNU General Public License in all respects for all of the
+ * code used other than OpenSSL. If you modify file(s) with this exception, you
+ * may extend this exception to your version of the file(s), but you are not
+ * obligated to do so. If you do not wish to do so, delete this exception
+ * statement from your version. If you delete this exception statement from all
+ * source files in the program, then also delete it here.
+ *
+ */
+
+#include <string.h>
+#include <stdlib.h>
+
+#include <glib.h>
+
+static GList *disco_features = NULL;
+
+void
+disco_add_feature(char* feature)
+{
+    if (feature == NULL) {
+        return;
+    }
+
+    disco_features = g_list_append(disco_features, strdup(feature));
+}
+
+GList*
+disco_get_features(void)
+{
+    return disco_features;
+}
+
+void
+disco_close(void)
+{
+    if (disco_features) {
+        g_list_free_full(disco_features, free);
+        disco_features = NULL;
+    }
+}
diff --git a/src/plugins/disco.h b/src/plugins/disco.h
new file mode 100644
index 00000000..9c7975c2
--- /dev/null
+++ b/src/plugins/disco.h
@@ -0,0 +1,43 @@
+/*
+ * disco.h
+ *
+ * Copyright (C) 2012 - 2016 James Booth <boothj5@gmail.com>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Profanity.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * In addition, as a special exception, the copyright holders give permission to
+ * link the code of portions of this program with the OpenSSL library under
+ * certain conditions as described in each individual source file, and
+ * distribute linked combinations including the two.
+ *
+ * You must obey the GNU General Public License in all respects for all of the
+ * code used other than OpenSSL. If you modify file(s) with this exception, you
+ * may extend this exception to your version of the file(s), but you are not
+ * obligated to do so. If you do not wish to do so, delete this exception
+ * statement from your version. If you delete this exception statement from all
+ * source files in the program, then also delete it here.
+ *
+ */
+
+#ifndef DISCO_H
+#define DISCO_H
+
+void disco_add_feature(char *feature);
+GList* disco_get_features(void);
+void disco_close(void);
+
+#endif
+
diff --git a/src/plugins/plugins.c b/src/plugins/plugins.c
index cb222a32..6cde92ad 100644
--- a/src/plugins/plugins.c
+++ b/src/plugins/plugins.c
@@ -45,6 +45,7 @@
 #include "plugins/plugins.h"
 #include "plugins/themes.h"
 #include "plugins/settings.h"
+#include "plugins/disco.h"
 
 #ifdef HAVE_PYTHON
 #include "plugins/python_plugins.h"
@@ -665,6 +666,12 @@ plugins_on_room_win_focus(const char *const roomjid)
     }
 }
 
+GList*
+plugins_get_disco_features(void)
+{
+    return disco_get_features();
+}
+
 void
 plugins_shutdown(void)
 {
@@ -695,6 +702,7 @@ plugins_shutdown(void)
     plugin_themes_close();
     plugin_settings_close();
     callbacks_close();
+    disco_close();
 }
 
 gchar *
diff --git a/src/plugins/plugins.h b/src/plugins/plugins.h
index d59bc280..b7eb4c80 100644
--- a/src/plugins/plugins.h
+++ b/src/plugins/plugins.h
@@ -153,4 +153,6 @@ GList* plugins_get_command_names(void);
 gchar * plugins_get_dir(void);
 CommandHelp* plugins_get_help(const char *const cmd);
 
+GList* plugins_get_disco_features(void);
+
 #endif
diff --git a/src/plugins/profapi.c b/src/plugins/profapi.c
index 6e8637b0..4b18da3b 100644
--- a/src/plugins/profapi.c
+++ b/src/plugins/profapi.c
@@ -83,3 +83,5 @@ int (*prof_settings_get_int)(char *group, char *key, int def) = NULL;
 void (*prof_settings_set_int)(char *group, char *key, int value) = NULL;
 
 void (*prof_incoming_message)(char *barejid, char *resource, char *message) = NULL;
+
+void (*prof_disco_add_feature)(char *feature) = NULL;
diff --git a/src/plugins/profapi.h b/src/plugins/profapi.h
index 7d0ed6e5..54497c8d 100644
--- a/src/plugins/profapi.h
+++ b/src/plugins/profapi.h
@@ -84,4 +84,6 @@ void (*prof_settings_set_int)(char *group, char *key, int value);
 
 void (*prof_incoming_message)(char *barejid, char *resource, char *message);
 
+void (*prof_disco_add_feature)(char *feature);
+
 #endif
diff --git a/src/plugins/python_api.c b/src/plugins/python_api.c
index 34e81f11..5c4cf913 100644
--- a/src/plugins/python_api.c
+++ b/src/plugins/python_api.c
@@ -665,6 +665,21 @@ python_api_incoming_message(PyObject *self, PyObject *args)
     return Py_BuildValue("");
 }
 
+static PyObject*
+python_api_disco_add_feature(PyObject *self, PyObject *args)
+{
+    char *feature = NULL;
+    if (!PyArg_ParseTuple(args, "s", &feature)) {
+        return Py_BuildValue("");
+    }
+
+    allow_python_threads();
+    api_disco_add_feature(feature);
+    disable_python_threads();
+
+    return Py_BuildValue("");
+}
+
 void
 python_command_callback(PluginCommand *command, gchar **args)
 {
@@ -766,6 +781,7 @@ static PyMethodDef apiMethods[] = {
     { "settings_get_int", python_api_settings_get_int, METH_VARARGS, "Get a integer setting." },
     { "settings_set_int", python_api_settings_set_int, METH_VARARGS, "Set a integer setting." },
     { "incoming_message", python_api_incoming_message, METH_VARARGS, "Show an incoming message." },
+    { "disco_add_feature", python_api_disco_add_feature, METH_VARARGS, "Add a feature to disco info response." },
     { NULL, NULL, 0, NULL }
 };
 
diff --git a/src/xmpp/capabilities.c b/src/xmpp/capabilities.c
index 02c29bd6..b4a6467b 100644
--- a/src/xmpp/capabilities.c
+++ b/src/xmpp/capabilities.c
@@ -57,6 +57,7 @@
 #include "xmpp/stanza.h"
 #include "xmpp/form.h"
 #include "xmpp/capabilities.h"
+#include "plugins/plugins.h"
 
 static gchar *cache_loc;
 static GKeyFile *cache;
@@ -552,6 +553,15 @@ caps_get_my_sha1(xmpp_ctx_t *const ctx)
     return my_sha1;
 }
 
+void
+caps_reset_ver(void)
+{
+    if (my_sha1) {
+        g_free(my_sha1);
+        my_sha1 = NULL;
+    }
+}
+
 xmpp_stanza_t*
 caps_create_query_response_stanza(xmpp_ctx_t *const ctx)
 {
@@ -632,6 +642,18 @@ caps_create_query_response_stanza(xmpp_ctx_t *const ctx)
     xmpp_stanza_add_child(query, feature_ping);
     xmpp_stanza_add_child(query, feature_receipts);
 
+    GList *plugin_features = plugins_get_disco_features();
+    GList *curr = plugin_features;
+    while (curr) {
+        xmpp_stanza_t *feature = xmpp_stanza_new(ctx);
+        xmpp_stanza_set_name(feature, STANZA_NAME_FEATURE);
+        xmpp_stanza_set_attribute(feature, STANZA_ATTR_VAR, curr->data);
+        xmpp_stanza_add_child(query, feature);
+        xmpp_stanza_release(feature);
+
+        curr = g_list_next(curr);
+    }
+
     xmpp_stanza_release(feature_receipts);
     xmpp_stanza_release(feature_ping);
     xmpp_stanza_release(feature_conference);
diff --git a/src/xmpp/xmpp.h b/src/xmpp/xmpp.h
index eedac7ed..d6e6031b 100644
--- a/src/xmpp/xmpp.h
+++ b/src/xmpp/xmpp.h
@@ -190,6 +190,7 @@ void iq_http_upload_request(HTTPUpload *upload);
 Capabilities* caps_lookup(const char *const jid);
 void caps_close(void);
 void caps_destroy(Capabilities *caps);
+void caps_reset_ver(void);
 
 gboolean bookmark_add(const char *jid, const char *nick, const char *password, const char *autojoin_str);
 gboolean bookmark_update(const char *jid, const char *nick, const char *password, const char *autojoin_str);
diff --git a/tests/unittests/test_cmd_account.c b/tests/unittests/test_cmd_account.c
index f46c0b93..701deb8d 100644
--- a/tests/unittests/test_cmd_account.c
+++ b/tests/unittests/test_cmd_account.c
@@ -398,7 +398,7 @@ void cmd_account_set_resource_sets_resource_with_online_message(void **state)
     expect_string(accounts_set_resource, account_name, "a_account");
     expect_string(accounts_set_resource, value, "a_resource");
 
-    expect_cons_show("Updated resource for account a_account: a_resource, you will need to reconnect to pick up the change.");
+    expect_cons_show("Updated resource for account a_account: a_resource, reconnect to pick up the change.");
     expect_cons_show("");
 
     gboolean result = cmd_account_set(NULL, CMD_ACCOUNT, args);
diff --git a/tests/unittests/xmpp/stub_xmpp.c b/tests/unittests/xmpp/stub_xmpp.c
index 6437de86..a68186dc 100644
--- a/tests/unittests/xmpp/stub_xmpp.c
+++ b/tests/unittests/xmpp/stub_xmpp.c
@@ -190,6 +190,7 @@ Capabilities* caps_lookup(const char * const jid)
 
 void caps_close(void) {}
 void caps_destroy(Capabilities *caps) {}
+void caps_reset_ver(void) {}
 
 gboolean bookmark_add(const char *jid, const char *nick, const char *password, const char *autojoin_str)
 {