/********************************************************************\
  * BitlBee -- An IRC to other IM-networks gateway                     *
  *                                                                    *
  * Copyright 2002-2013 Wilmer van der Gaast and others                *
  \********************************************************************/

/* User manager (root) commands                                         */

/*
  This program 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 2 of the License, or
  (at your option) any later version.

  This program 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 with
  the Debian GNU/Linux distribution in /usr/share/common-licenses/GPL;
  if not, write to the Free Software Foundation, Inc., 51 Franklin St.,
  Fifth Floor, Boston, MA  02110-1301  USA
*/

#define BITLBEE_CORE
#include "commands.h"
#include "bitlbee.h"
#include "help.h"
#include "ipc.h"

void root_command_string(irc_t *irc, char *command)
{
	root_command(irc, split_command_parts(command, 0));
}

#define MIN_ARGS(x, y ...)                                                    \
	do                                                                     \
	{                                                                      \
		int blaat;                                                     \
		for (blaat = 0; blaat <= x; blaat++) {                         \
			if (cmd[blaat] == NULL)                               \
			{                                                      \
				irc_rootmsg(irc, "Not enough parameters given (need %d).", x); \
				return y;                                      \
			} }                                                      \
	} while (0)

void root_command(irc_t *irc, char *cmd[])
{
	int i, len;

	if (!cmd[0]) {
		return;
	}

	len = strlen(cmd[0]);
	for (i = 0; root_commands[i].command; i++) {
		if (g_strncasecmp(root_commands[i].command, cmd[0], len) == 0) {
			if (root_commands[i + 1].command &&
			    g_strncasecmp(root_commands[i + 1].command, cmd[0], len) == 0) {
				/* Only match on the first letters if the match is unique. */
				break;
			}

			MIN_ARGS(root_commands[i].required_parameters);

			root_commands[i].execute(irc, cmd);
			return;
		}
	}

	irc_rootmsg(irc, "Unknown command: %s. Please use \x02help commands\x02 to get a list of available commands.",
	            cmd[0]);
}

static void cmd_help(irc_t *irc, char **cmd)
{
	char param[80];
	int i;
	char *s;

	memset(param, 0, sizeof(param));
	for (i = 1; (cmd[i] != NULL && (strlen(param) < (sizeof(param) - 1))); i++) {
		if (i != 1) {   // prepend space except for the first parameter
			strcat(param, " ");
		}
		strncat(param, cmd[i], sizeof(param) - strlen(param) - 1);
	}

	s = help_get(&(global.help), param);
	if (!s) {
		s = help_get(&(global.help), "");
	}

	if (s) {
		irc_rootmsg(irc, "%s", s);
		g_free(s);
	} else {
		irc_rootmsg(irc, "Error opening helpfile.");
	}
}

static void cmd_account(irc_t *irc, char **cmd);
static void bitlbee_whatsnew(irc_t *irc);

static void cmd_identify(irc_t *irc, char **cmd)
{
	storage_status_t status;
	gboolean load = TRUE;
	char *password = cmd[1];

	if (irc->status & USTATUS_IDENTIFIED) {
		irc_rootmsg(irc, "You're already logged in.");
		return;
	}

	if (cmd[1] == NULL) {
	} else if (strncmp(cmd[1], "-no", 3) == 0) {
		load = FALSE;
		password = cmd[2];
		if (password == NULL) {
			irc->status |= OPER_HACK_IDENTIFY_NOLOAD;
		}
	} else if (strncmp(cmd[1], "-force", 6) == 0) {
		password = cmd[2];
		if (password == NULL) {
			irc->status |= OPER_HACK_IDENTIFY_FORCE;
		}
	} else if (irc->b->accounts != NULL) {
		irc_rootmsg(irc,
		            "You're trying to identify yourself, but already have "
		            "at least one IM account set up. "
		            "Use \x02identify -noload\x02 or \x02identify -force\x02 "
		            "instead (see \x02help identify\x02).");
		return;
	}

	if (password == NULL) {
		irc_rootmsg(irc, "About to identify, use /OPER to enter the password");
		irc->status |= OPER_HACK_IDENTIFY;
		return;
	}

	status = auth_check_pass(irc, irc->user->nick, password);
	if (load && (status == STORAGE_OK)) {
		status = storage_load(irc, password);
	}

	switch (status) {
	case STORAGE_INVALID_PASSWORD:
		irc_rootmsg(irc, "Incorrect password");
		break;
	case STORAGE_NO_SUCH_USER:
		irc_rootmsg(irc, "The nick is (probably) not registered");
		break;
	case STORAGE_OK:
		irc_rootmsg(irc, "Password accepted%s",
		            load ? ", settings and accounts loaded" : "");
		irc->status |= USTATUS_IDENTIFIED;
		irc_umode_set(irc, "+R", 1);

		if (irc->caps & CAP_SASL) {
			irc_user_t *iu = irc->user;
			irc_send_num(irc, 900, "%s!%s@%s %s :You are now logged in as %s",
				iu->nick, iu->user, iu->host, iu->nick, iu->nick);
		}

		bitlbee_whatsnew(irc);

		/* The following code is a bit hairy now. With takeover
		   support, we shouldn't immediately auto_connect in case
		   we're going to offer taking over an existing session.
		   Do it in 200ms since that should give the parent process
		   enough time to come back to us. */
		if (load) {
			irc_channel_auto_joins(irc, NULL);
			if (!set_getbool(&irc->default_channel->set, "auto_join")) {
				irc_channel_del_user(irc->default_channel, irc->user,
				                     IRC_CDU_PART, "auto_join disabled "
				                     "for this channel.");
			}
			if (set_getbool(&irc->b->set, "auto_connect")) {
				irc->login_source_id = b_timeout_add(200,
				                                     cmd_identify_finish, irc);
			}
		}

		/* If ipc_child_identify() returns FALSE, it means we're
		   already sure that there's no takeover target (only
		   possible in 1-process daemon mode). Start auto_connect
		   immediately. */
		if (!ipc_child_identify(irc) && load) {
			cmd_identify_finish(irc, 0, 0);
		}

		break;
	case STORAGE_OTHER_ERROR:
	default:
		irc_rootmsg(irc, "Unknown error while loading configuration");
		break;
	}
}

gboolean cmd_identify_finish(gpointer data, gint fd, b_input_condition cond)
{
	char *account_on[] = { "account", "on", NULL };
	irc_t *irc = data;

	if (set_getbool(&irc->b->set, "auto_connect")) {
		cmd_account(irc, account_on);
	}

	b_event_remove(irc->login_source_id);
	irc->login_source_id = -1;
	return FALSE;
}

static void cmd_register(irc_t *irc, char **cmd)
{
	char s[16];

	if (global.conf->authmode == AUTHMODE_REGISTERED) {
		irc_rootmsg(irc, "This server does not allow registering new accounts");
		return;
	}

	if (cmd[1] == NULL) {
		irc_rootmsg(irc, "About to register, use /OPER to enter the password");
		irc->status |= OPER_HACK_REGISTER;
		return;
	}

	switch (storage_save(irc, cmd[1], FALSE)) {
	case STORAGE_ALREADY_EXISTS:
		irc_rootmsg(irc, "Nick is already registered");
		break;

	case STORAGE_OK:
		irc_rootmsg(irc, "Account successfully created");
		irc_setpass(irc, cmd[1]);
		irc->status |= USTATUS_IDENTIFIED;
		irc_umode_set(irc, "+R", 1);

		if (irc->caps & CAP_SASL) {
			irc_user_t *iu = irc->user;
			irc_send_num(irc, 900, "%s!%s@%s %s :You are now logged in as %s",
				iu->nick, iu->user, iu->host, iu->nick, iu->nick);
		}

		/* Set this var now, or anyone who logs in to his/her
		   newly created account for the first time gets the
		   whatsnew story. */
		g_snprintf(s, sizeof(s), "%d", BITLBEE_VERSION_CODE);
		set_setstr(&irc->b->set, "last_version", s);
		break;

	default:
		irc_rootmsg(irc, "Error registering");
		break;
	}
}

static void cmd_drop(irc_t *irc, char **cmd)
{
	storage_status_t status;

	status = auth_check_pass(irc, irc->user->nick, cmd[1]);
	if (status == STORAGE_OK) {
		status = storage_remove(irc->user->nick);
	}

	switch (status) {
	case STORAGE_NO_SUCH_USER:
		irc_rootmsg(irc, "That account does not exist");
		break;
	case STORAGE_INVALID_PASSWORD:
		irc_rootmsg(irc, "Password invalid");
		break;
	case STORAGE_OK:
		irc_setpass(irc, NULL);
		irc->status &= ~USTATUS_IDENTIFIED;
		irc_umode_set(irc, "-R", 1);
		irc_rootmsg(irc, "Account `%s' removed", irc->user->nick);
		break;
	default:
		irc_rootmsg(irc, "Error: `%d'", status);
		break;
	}
}

static void cmd_save(irc_t *irc, char **cmd)
{
	if ((irc->status & USTATUS_IDENTIFIED) == 0) {
		irc_rootmsg(irc, "Please create an account first (see \x02help register\x02)");
	} else if (storage_save(irc, NULL, TRUE) == STORAGE_OK) {
		irc_rootmsg(irc, "Configuration saved");
	} else {
		irc_rootmsg(irc, "Configuration could not be saved!");
	}
}

static void cmd_showset(irc_t *irc, set_t **head, char *key)
{
	set_t *set;
	char *val;

	if ((val = set_getstr(head, key))) {
		irc_rootmsg(irc, "%s = `%s'", key, val);
	} else if (!(set = set_find(head, key))) {
		irc_rootmsg(irc, "Setting `%s' does not exist.", key);
		if (*head == irc->b->set) {
			irc_rootmsg(irc, "It might be an account or channel setting. "
			            "See \x02help account set\x02 and \x02help channel set\x02.");
		}
	} else if (set->flags & SET_PASSWORD) {
		irc_rootmsg(irc, "%s = `********' (hidden)", key);
	} else {
		irc_rootmsg(irc, "%s is empty", key);
	}
}

typedef set_t** (*cmd_set_findhead)(irc_t*, char*);
typedef int (*cmd_set_checkflags)(irc_t*, set_t *set);

static int cmd_set_real(irc_t *irc, char **cmd, set_t **head, cmd_set_checkflags checkflags)
{
	char *set_name = NULL, *value = NULL;
	gboolean del = FALSE;

	if (cmd[1] && g_strncasecmp(cmd[1], "-del", 4) == 0) {
		MIN_ARGS(2, 0);
		set_name = cmd[2];
		del = TRUE;
	} else {
		set_name = cmd[1];
		value = cmd[2];
	}

	if (set_name && (value || del)) {
		set_t *s = set_find(head, set_name);
		int st;

		if (s && s->flags & SET_LOCKED) {
			irc_rootmsg(irc, "This setting can not be changed");
			return 0;
		}
		if (s && checkflags && checkflags(irc, s) == 0) {
			return 0;
		}

		if (del) {
			st = set_reset(head, set_name);
		} else {
			st = set_setstr(head, set_name, value);
		}

		if (set_getstr(head, set_name) == NULL &&
		    set_find(head, set_name)) {
			/* This happens when changing the passwd, for example.
			   Showing these msgs instead gives slightly clearer
			   feedback. */
			if (st) {
				irc_rootmsg(irc, "Setting changed successfully");
			} else {
				irc_rootmsg(irc, "Failed to change setting");
			}
		} else {
			cmd_showset(irc, head, set_name);
		}
	} else if (set_name) {
		cmd_showset(irc, head, set_name);
	} else {
		set_t *s = *head;
		while (s) {
			if (set_isvisible(s)) {
				cmd_showset(irc, &s, s->key);
			}
			s = s->next;
		}
	}

	return 1;
}

static int cmd_account_set_checkflags(irc_t *irc, set_t *s)
{
	account_t *a = s->data;

	if (a->ic && s && s->flags & ACC_SET_OFFLINE_ONLY) {
		irc_rootmsg(irc, "This setting can only be changed when the account is %s-line", "off");
		return 0;
	} else if (!a->ic && s && s->flags & ACC_SET_ONLINE_ONLY) {
		irc_rootmsg(irc, "This setting can only be changed when the account is %s-line", "on");
		return 0;
	} else if (a->flags & ACC_FLAG_LOCKED && s && s->flags & ACC_SET_LOCKABLE) {
		irc_rootmsg(irc, "This setting can not be changed for locked accounts");
		return 0;
	}

	return 1;
}

static void cmd_account(irc_t *irc, char **cmd)
{
	account_t *a;
	int len;

	if (global.conf->authmode == AUTHMODE_REGISTERED && !(irc->status & USTATUS_IDENTIFIED)) {
		irc_rootmsg(irc, "This server only accepts registered users");
		return;
	}

	len = strlen(cmd[1]);

	if (len >= 1 && g_strncasecmp(cmd[1], "add", len) == 0) {
		struct prpl *prpl;

		MIN_ARGS(3);

		if (!global.conf->allow_account_add) {
			irc_rootmsg(irc, "This server does not allow adding new accounts");
			return;
		}

		if (cmd[4] == NULL) {
			for (a = irc->b->accounts; a; a = a->next) {
				if (strcmp(a->pass, PASSWORD_PENDING) == 0) {
					irc_rootmsg(irc, "Enter password for account %s "
					            "first (use /OPER)", a->tag);
					return;
				}
			}

			irc->status |= OPER_HACK_ACCOUNT_PASSWORD;
		}

		prpl = find_protocol(cmd[2]);

		if (prpl == NULL) {
			char *msg = explain_unknown_protocol(cmd[2]);
			irc_rootmsg(irc, "Unknown protocol");
			irc_rootmsg(irc, "%s", msg);
			g_free(msg);
			return;
		}

		for (a = irc->b->accounts; a; a = a->next) {
			if (a->prpl == prpl && prpl->handle_cmp(a->user, cmd[3]) == 0) {
				irc_rootmsg(irc, "Warning: You already have an account with "
				            "protocol `%s' and username `%s'. Are you accidentally "
				            "trying to add it twice?", prpl->name, cmd[3]);
			}
		}

		a = account_add(irc->b, prpl, cmd[3], cmd[4] ? cmd[4] : PASSWORD_PENDING);
		if (cmd[5]) {
			irc_rootmsg(irc, "Warning: Passing a servername/other flags to `account add' "
			            "is now deprecated. Use `account set' instead.");
			set_setstr(&a->set, "server", cmd[5]);
		}

		irc_rootmsg(irc, "Account successfully added with tag %s", a->tag);

		if (cmd[4] == NULL) {
			set_t *oauth = set_find(&a->set, "oauth");
			if (oauth && bool2int(set_value(oauth))) {
				*a->pass = '\0';
				irc_rootmsg(irc, "No need to enter a password for this "
				            "account since it's using OAuth");
			} else if (prpl->options & PRPL_OPT_NO_PASSWORD) {
				*a->pass = '\0';
			} else if (prpl->options & PRPL_OPT_PASSWORD_OPTIONAL) {
				*a->pass = '\0';
				irc_rootmsg(irc, "Passwords are optional for this account. "
				            "If you wish to enter the password with /OPER, do "
				            "account %s set -del password", a->tag);
			} else {
				irc_rootmsg(irc, "You can now use the /OPER command to "
				            "enter the password");
				if (oauth) {
					irc_rootmsg(irc, "Alternatively, enable OAuth if "
					            "the account supports it: account %s "
					            "set oauth on", a->tag);
				}
			}
		} else if (prpl->options & PRPL_OPT_NO_PASSWORD) {
			irc_rootmsg(irc, "Note: this account doesn't use password for login");
		}

		return;
	} else if (len >= 1 && g_strncasecmp(cmd[1], "list", len) == 0) {
		int i = 0;

		if (strchr(irc->umode, 'b')) {
			irc_rootmsg(irc, "Account list:");
		}

		for (a = irc->b->accounts; a; a = a->next) {
			char *con = NULL, *protocol = NULL;

			if (a->ic && (a->ic->flags & OPT_LOGGED_IN)) {
				con = " (connected)";
			} else if (a->ic) {
				con = " (connecting)";
			} else if (a->reconnect) {
				con = " (awaiting reconnect)";
			} else {
				con = "";
			}

			if (a->prpl == &protocol_missing) {
				protocol = g_strdup_printf("%s (missing!)", set_getstr(&a->set, "_protocol_name"));
			} else {
				protocol = g_strdup(a->prpl->name);
			}

			irc_rootmsg(irc, "%2d (%s): %s, %s%s", i, a->tag, protocol, a->user, con);
			g_free(protocol);

			i++;
		}
		irc_rootmsg(irc, "End of account list");

		return;
	} else if (cmd[2]) {
		/* Try the following two only if cmd[2] == NULL */
	} else if (len >= 2 && g_strncasecmp(cmd[1], "on", len) == 0) {
		if (irc->b->accounts) {
			irc_rootmsg(irc, "Trying to get all accounts connected...");

			for (a = irc->b->accounts; a; a = a->next) {
				if (!a->ic && a->auto_connect && a->prpl != &protocol_missing) {
					if (strcmp(a->pass, PASSWORD_PENDING) == 0) {
						irc_rootmsg(irc, "Enter password for account %s "
						            "first (use /OPER)", a->tag);
					} else {
						account_on(irc->b, a);
					}
				}
			}
		} else {
			irc_rootmsg(irc, "No accounts known. Use `account add' to add one.");
		}

		return;
	} else if (len >= 2 && g_strncasecmp(cmd[1], "off", len) == 0) {
		irc_rootmsg(irc, "Deactivating all active (re)connections...");

		for (a = irc->b->accounts; a; a = a->next) {
			if (a->ic) {
				account_off(irc->b, a);
			} else if (a->reconnect) {
				cancel_auto_reconnect(a);
			}
		}

		return;
	}

	MIN_ARGS(2);
	len = strlen(cmd[2]);

	/* At least right now, don't accept on/off/set/del as account IDs even
	   if they're a proper match, since people not familiar with the new
	   syntax yet may get a confusing/nasty surprise. */
	if (g_strcasecmp(cmd[1], "on") == 0 ||
	    g_strcasecmp(cmd[1], "off") == 0 ||
	    g_strcasecmp(cmd[1], "set") == 0 ||
	    g_strcasecmp(cmd[1], "del") == 0 ||
	    (a = account_get(irc->b, cmd[1])) == NULL) {
		irc_rootmsg(irc, "Could not find account `%s'.", cmd[1]);

		return;
	}

	if (len >= 1 && g_strncasecmp(cmd[2], "del", len) == 0) {
		if (a->flags & ACC_FLAG_LOCKED) {
			irc_rootmsg(irc, "Account is locked, can't delete");
		}
		else if (a->ic) {
			irc_rootmsg(irc, "Account is still logged in, can't delete");
		} else {
			account_del(irc->b, a);
			irc_rootmsg(irc, "Account deleted");
		}
	} else if (len >= 2 && g_strncasecmp(cmd[2], "on", len) == 0) {
		if (a->ic) {
			irc_rootmsg(irc, "Account already online");
		} else if (strcmp(a->pass, PASSWORD_PENDING) == 0) {
			irc_rootmsg(irc, "Enter password for account %s "
			            "first (use /OPER)", a->tag);
		} else if (a->prpl == &protocol_missing) {
			char *proto = set_getstr(&a->set, "_protocol_name");
			char *msg = explain_unknown_protocol(proto);
			irc_rootmsg(irc, "Unknown protocol `%s'", proto);
			irc_rootmsg(irc, "%s", msg);
			g_free(msg);
		} else {
			account_on(irc->b, a);
		}
	} else if (len >= 2 && g_strncasecmp(cmd[2], "off", len) == 0) {
		if (a->ic) {
			account_off(irc->b, a);
		} else if (a->reconnect) {
			cancel_auto_reconnect(a);
			irc_rootmsg(irc, "Reconnect cancelled");
		} else {
			irc_rootmsg(irc, "Account already offline");
		}
	} else if (len >= 1 && g_strncasecmp(cmd[2], "set", len) == 0) {
		cmd_set_real(irc, cmd + 2, &a->set, cmd_account_set_checkflags);
	} else {
		irc_rootmsg(irc,
		            "Unknown command: %s [...] %s. Please use \x02help commands\x02 to get a list of available commands.", "account",
		            cmd[2]);
	}
}

static void cmd_channel(irc_t *irc, char **cmd)
{
	irc_channel_t *ic;
	int len;

	len = strlen(cmd[1]);

	if (len >= 1 && g_strncasecmp(cmd[1], "list", len) == 0) {
		GSList *l;
		int i = 0;

		if (strchr(irc->umode, 'b')) {
			irc_rootmsg(irc, "Channel list:");
		}

		for (l = irc->channels; l; l = l->next) {
			irc_channel_t *ic = l->data;

			irc_rootmsg(irc, "%2d. %s, %s channel%s", i, ic->name,
			            set_getstr(&ic->set, "type"),
			            ic->flags & IRC_CHANNEL_JOINED ? " (joined)" : "");

			i++;
		}
		irc_rootmsg(irc, "End of channel list");

		return;
	}

	if ((ic = irc_channel_get(irc, cmd[1])) == NULL) {
		/* If this doesn't match any channel, maybe this is the short
		   syntax (only works when used inside a channel). */
		if ((ic = irc->root->last_channel) &&
		    (len = strlen(cmd[1])) &&
		    g_strncasecmp(cmd[1], "set", len) == 0) {
			cmd_set_real(irc, cmd + 1, &ic->set, NULL);
		} else {
			irc_rootmsg(irc, "Could not find channel `%s'", cmd[1]);
		}

		return;
	}

	MIN_ARGS(2);
	len = strlen(cmd[2]);

	if (len >= 1 && g_strncasecmp(cmd[2], "set", len) == 0) {
		cmd_set_real(irc, cmd + 2, &ic->set, NULL);
	} else if (len >= 1 && g_strncasecmp(cmd[2], "del", len) == 0) {
		if (!(ic->flags & IRC_CHANNEL_JOINED) &&
		    ic != ic->irc->default_channel) {
			irc_rootmsg(irc, "Channel %s deleted.", ic->name);
			irc_channel_free(ic);
		} else {
			irc_rootmsg(irc, "Couldn't remove channel (main channel %s or "
			            "channels you're still in cannot be deleted).",
			            irc->default_channel->name);
		}
	} else {
		irc_rootmsg(irc,
		            "Unknown command: %s [...] %s. Please use \x02help commands\x02 to get a list of available commands.", "channel",
		            cmd[1]);
	}
}

static void cmd_add(irc_t *irc, char **cmd)
{
	account_t *a;
	int add_on_server = 1;
	char *handle = NULL, *s;

	if (g_strcasecmp(cmd[1], "-tmp") == 0) {
		MIN_ARGS(3);
		add_on_server = 0;
		cmd++;
	}

	if (!(a = account_get(irc->b, cmd[1]))) {
		irc_rootmsg(irc, "Invalid account");
		return;
	} else if (!(a->ic && (a->ic->flags & OPT_LOGGED_IN))) {
		irc_rootmsg(irc, "That account is not on-line");
		return;
	}

	if (cmd[3]) {
		if (!nick_ok(irc, cmd[3])) {
			irc_rootmsg(irc, "The requested nick `%s' is invalid", cmd[3]);
			return;
		} else if (irc_user_by_name(irc, cmd[3])) {
			irc_rootmsg(irc, "The requested nick `%s' already exists", cmd[3]);
			return;
		} else {
			nick_set_raw(a, cmd[2], cmd[3]);
		}
	}

	if ((a->flags & ACC_FLAG_HANDLE_DOMAINS) && cmd[2][0] != '_' &&
	    (!(s = strchr(cmd[2], '@')) || s[1] == '\0')) {
		/* If there's no @ or it's the last char, append the user's
		   domain name now. Exclude handles starting with a _ so
		   adding _xmlconsole will keep working. */
		if (s) {
			*s = '\0';
		}
		if ((s = strchr(a->user, '@'))) {
			cmd[2] = handle = g_strconcat(cmd[2], s, NULL);
		}
	}

	if (add_on_server) {
		irc_channel_t *ic;
		char *s, *group = NULL;;

		if ((ic = irc->root->last_channel) &&
		    (s = set_getstr(&ic->set, "fill_by")) &&
		    strcmp(s, "group") == 0 &&
		    (group = set_getstr(&ic->set, "group"))) {
			irc_rootmsg(irc, "Adding `%s' to contact list (group %s)",
			            cmd[2], group);
		} else {
			irc_rootmsg(irc, "Adding `%s' to contact list", cmd[2]);
		}

		a->prpl->add_buddy(a->ic, cmd[2], group);
	} else {
		bee_user_t *bu;
		irc_user_t *iu;

		/* Only for add -tmp. For regular adds, this callback will
		   be called once the IM server confirms. */
		if ((bu = bee_user_new(irc->b, a->ic, cmd[2], BEE_USER_LOCAL)) &&
		    (iu = bu->ui_data)) {
			irc_rootmsg(irc, "Temporarily assigned nickname `%s' "
			            "to contact `%s'", iu->nick, cmd[2]);
		}
	}

	g_free(handle);
}

static void cmd_remove(irc_t *irc, char **cmd)
{
	irc_user_t *iu;
	bee_user_t *bu;
	char *s;

	if (!(iu = irc_user_by_name(irc, cmd[1])) || !(bu = iu->bu)) {
		irc_rootmsg(irc, "Buddy `%s' not found", cmd[1]);
		return;
	}
	s = g_strdup(bu->handle);

	bu->ic->acc->prpl->remove_buddy(bu->ic, bu->handle, NULL);
	nick_del(bu);
	if (g_hash_table_contains(irc->nick_user_hash, iu->key)) {
		bee_user_free(irc->b, bu);
	}

	irc_rootmsg(irc, "Buddy `%s' (nick %s) removed from contact list", s, cmd[1]);
	g_free(s);

	return;
}

static void cmd_info(irc_t *irc, char **cmd)
{
	struct im_connection *ic;
	account_t *a;

	if (!cmd[2]) {
		irc_user_t *iu = irc_user_by_name(irc, cmd[1]);
		if (!iu || !iu->bu) {
			irc_rootmsg(irc, "Nick `%s' does not exist", cmd[1]);
			return;
		}
		ic = iu->bu->ic;
		cmd[2] = iu->bu->handle;
	} else if (!(a = account_get(irc->b, cmd[1]))) {
		irc_rootmsg(irc, "Invalid account");
		return;
	} else if (!((ic = a->ic) && (a->ic->flags & OPT_LOGGED_IN))) {
		irc_rootmsg(irc, "That account is not on-line");
		return;
	}

	if (!ic->acc->prpl->get_info) {
		irc_rootmsg(irc, "Command `%s' not supported by this protocol", cmd[0]);
	} else {
		ic->acc->prpl->get_info(ic, cmd[2]);
	}
}

static void cmd_rename(irc_t *irc, char **cmd)
{
	irc_user_t *iu, *old;
	gboolean del = g_strcasecmp(cmd[1], "-del") == 0;

	iu = irc_user_by_name(irc, cmd[del ? 2 : 1]);

	if (iu == NULL) {
		irc_rootmsg(irc, "Nick `%s' does not exist", cmd[del ? 2 : 1]);
	} else if (del) {
		if (iu->bu) {
			bee_irc_user_nick_reset(iu);
		}
		irc_rootmsg(irc, "Nickname reset to `%s'", iu->nick);
	} else if (iu == irc->user) {
		irc_rootmsg(irc, "Use /nick to change your own nickname");
	} else if (!nick_ok(irc, cmd[2])) {
		irc_rootmsg(irc, "Nick `%s' is invalid", cmd[2]);
	} else if ((old = irc_user_by_name(irc, cmd[2])) && old != iu) {
		irc_rootmsg(irc, "Nick `%s' already exists", cmd[2]);
	} else {
		if (!irc_user_set_nick(iu, cmd[2])) {
			irc_rootmsg(irc, "Error while changing nick");
			return;
		}

		if (iu == irc->root) {
			/* If we're called internally (user did "set root_nick"),
			   let's not go O(INF). :-) */
			if (strcmp(cmd[0], "set_rename") != 0) {
				set_setstr(&irc->b->set, "root_nick", cmd[2]);
			}
		} else if (iu->bu) {
			nick_set(iu->bu, cmd[2]);
		}

		irc_rootmsg(irc, "Nick successfully changed");
	}
}

char *set_eval_root_nick(set_t *set, char *new_nick)
{
	irc_t *irc = set->data;

	if (strcmp(irc->root->nick, new_nick) != 0) {
		char *cmd[] = { "set_rename", irc->root->nick, new_nick, NULL };

		cmd_rename(irc, cmd);
	}

	return strcmp(irc->root->nick, new_nick) == 0 ? new_nick : SET_INVALID;
}

static void cmd_block(irc_t *irc, char **cmd)
{
	struct im_connection *ic;
	account_t *a;

	if (!cmd[2] && (a = account_get(irc->b, cmd[1])) && a->ic) {
		char *format;
		GSList *l;

		if (strchr(irc->umode, 'b') != NULL) {
			format = "%s\t%s";
		} else {
			format = "%-32.32s  %-16.16s";
		}

		irc_rootmsg(irc, format, "Handle", "Nickname");
		for (l = a->ic->deny; l; l = l->next) {
			bee_user_t *bu = bee_user_by_handle(irc->b, a->ic, l->data);
			irc_user_t *iu = bu ? bu->ui_data : NULL;
			irc_rootmsg(irc, format, l->data, iu ? iu->nick : "(none)");
		}
		irc_rootmsg(irc, "End of list.");

		return;
	} else if (!cmd[2]) {
		irc_user_t *iu = irc_user_by_name(irc, cmd[1]);
		if (!iu || !iu->bu) {
			irc_rootmsg(irc, "Nick `%s' does not exist", cmd[1]);
			return;
		}
		ic = iu->bu->ic;
		cmd[2] = iu->bu->handle;
	} else if (!(a = account_get(irc->b, cmd[1]))) {
		irc_rootmsg(irc, "Invalid account");
		return;
	} else if (!((ic = a->ic) && (a->ic->flags & OPT_LOGGED_IN))) {
		irc_rootmsg(irc, "That account is not on-line");
		return;
	}

	if (!ic->acc->prpl->add_deny || !ic->acc->prpl->rem_permit) {
		irc_rootmsg(irc, "Command `%s' not supported by this protocol", cmd[0]);
	} else {
		imc_rem_allow(ic, cmd[2]);
		imc_add_block(ic, cmd[2]);
		irc_rootmsg(irc, "Buddy `%s' moved from allow- to block-list", cmd[2]);
	}
}

static void cmd_allow(irc_t *irc, char **cmd)
{
	struct im_connection *ic;
	account_t *a;

	if (!cmd[2] && (a = account_get(irc->b, cmd[1])) && a->ic) {
		char *format;
		GSList *l;

		if (strchr(irc->umode, 'b') != NULL) {
			format = "%s\t%s";
		} else {
			format = "%-32.32s  %-16.16s";
		}

		irc_rootmsg(irc, format, "Handle", "Nickname");
		for (l = a->ic->permit; l; l = l->next) {
			bee_user_t *bu = bee_user_by_handle(irc->b, a->ic, l->data);
			irc_user_t *iu = bu ? bu->ui_data : NULL;
			irc_rootmsg(irc, format, l->data, iu ? iu->nick : "(none)");
		}
		irc_rootmsg(irc, "End of list.");

		return;
	} else if (!cmd[2]) {
		irc_user_t *iu = irc_user_by_name(irc, cmd[1]);
		if (!iu || !iu->bu) {
			irc_rootmsg(irc, "Nick `%s' does not exist", cmd[1]);
			return;
		}
		ic = iu->bu->ic;
		cmd[2] = iu->bu->handle;
	} else if (!(a = account_get(irc->b, cmd[1]))) {
		irc_rootmsg(irc, "Invalid account");
		return;
	} else if (!((ic = a->ic) && (a->ic->flags & OPT_LOGGED_IN))) {
		irc_rootmsg(irc, "That account is not on-line");
		return;
	}

	if (!ic->acc->prpl->rem_deny || !ic->acc->prpl->add_permit) {
		irc_rootmsg(irc, "Command `%s' not supported by this protocol", cmd[0]);
	} else {
		imc_rem_block(ic, cmd[2]);
		imc_add_allow(ic, cmd[2]);

		irc_rootmsg(irc, "Buddy `%s' moved from block- to allow-list", cmd[2]);
	}
}

static void cmd_yesno(irc_t *irc, char **cmd)
{
	query_t *q = NULL;
	int numq = 0;

	if (irc->queries == NULL) {
		/* Alright, alright, let's add a tiny easter egg here. */
		static irc_t *last_irc = NULL;
		static time_t last_time = 0;
		static int times = 0;
		static const char *msg[] = {
			"Oh yeah, that's right.",
			"Alright, alright. Now go back to work.",
			"Buuuuuuuuuuuuuuuurp... Excuse me!",
			"Yes?",
			"No?",
		};

		if (last_irc == irc && time(NULL) - last_time < 15) {
			if ((++times >= 3)) {
				irc_rootmsg(irc, "%s", msg[rand() % (sizeof(msg) / sizeof(char*))]);
				last_irc = NULL;
				times = 0;
				return;
			}
		} else {
			last_time = time(NULL);
			last_irc = irc;
			times = 0;
		}

		irc_rootmsg(irc, "Did I ask you something?");
		return;
	}

	/* If there's an argument, the user seems to want to answer another question than the
	   first/last (depending on the query_order setting) one. */
	if (cmd[1]) {
		if (sscanf(cmd[1], "%d", &numq) != 1) {
			irc_rootmsg(irc, "Invalid query number");
			return;
		}

		for (q = irc->queries; q; q = q->next, numq--) {
			if (numq == 0) {
				break;
			}
		}

		if (!q) {
			irc_rootmsg(irc, "Uhm, I never asked you something like that...");
			return;
		}
	}

	if (g_strcasecmp(cmd[0], "yes") == 0) {
		query_answer(irc, q, 1);
	} else if (g_strcasecmp(cmd[0], "no") == 0) {
		query_answer(irc, q, 0);
	}
}

static void cmd_set(irc_t *irc, char **cmd)
{
	cmd_set_real(irc, cmd, &irc->b->set, NULL);
}

static void cmd_blist(irc_t *irc, char **cmd)
{
	int online = 0, away = 0, offline = 0, ismatch = 0;
	GList *l, *users;
	GRegex *regex = NULL;
	GError *error = NULL;
	char s[256];
	char *format;
	int n_online = 0, n_away = 0, n_offline = 0;

	if (cmd[1] && g_strcasecmp(cmd[1], "all") == 0) {
		online = offline = away = 1;
	} else if (cmd[1] && g_strcasecmp(cmd[1], "offline") == 0) {
		offline = 1;
	} else if (cmd[1] && g_strcasecmp(cmd[1], "away") == 0) {
		away = 1;
	} else if (cmd[1] && g_strcasecmp(cmd[1], "online") == 0) {
		online = 1;
	} else {
		online = away = 1;
	}

	if (cmd[2]) {
		regex = g_regex_new(cmd[2], G_REGEX_CASELESS, 0, &error);
	}

	if (error) {
		irc_rootmsg(irc, "%s", error->message);
		g_error_free(error);
	}

	if (strchr(irc->umode, 'b') != NULL) {
		format = "%s\t%s\t%s";
	} else {
		format = "%-24.24s  %-40.40s  %s";
	}

	irc_rootmsg(irc, format, "Nick", "Handle/Account", "Status");

	if (irc->root->last_channel &&
	    strcmp(set_getstr(&irc->root->last_channel->set, "type"), "control") != 0) {
		irc->root->last_channel = NULL;
	}

	users = g_hash_table_get_values(irc->nick_user_hash);
	users = g_list_sort(users, irc_user_cmp);

	for (l = users; l; l = l->next) {
		irc_user_t *iu = l->data;
		bee_user_t *bu = iu->bu;

		if (!regex || g_regex_match(regex, iu->nick, 0, NULL)) {
			ismatch = 1;
		} else {
			ismatch = 0;
		}

		if (!bu || (irc->root->last_channel && !irc_channel_wants_user(irc->root->last_channel, iu))) {
			continue;
		}

		if ((bu->flags & (BEE_USER_ONLINE | BEE_USER_AWAY)) == BEE_USER_ONLINE) {
			if (ismatch == 1 && online == 1) {
				char st[256] = "Online";

				if (bu->status_msg) {
					g_snprintf(st, sizeof(st) - 1, "Online (%s)", bu->status_msg);
				}

				g_snprintf(s, sizeof(s) - 1, "%s %s", bu->handle, bu->ic->acc->tag);
				irc_rootmsg(irc, format, iu->nick, s, st);
			}

			n_online++;
		}

		if ((bu->flags & BEE_USER_ONLINE) && (bu->flags & BEE_USER_AWAY)) {
			if (ismatch == 1 && away == 1) {
				g_snprintf(s, sizeof(s) - 1, "%s %s", bu->handle, bu->ic->acc->tag);
				irc_rootmsg(irc, format, iu->nick, s, irc_user_get_away(iu));
			}
			n_away++;
		}

		if (!(bu->flags & BEE_USER_ONLINE)) {
			if (ismatch == 1 && offline == 1) {
				g_snprintf(s, sizeof(s) - 1, "%s %s", bu->handle, bu->ic->acc->tag);
				irc_rootmsg(irc, format, iu->nick, s, "Offline");
			}
			n_offline++;
		}
	}

	g_list_free(users);

	irc_rootmsg(irc, "%d buddies (%d available, %d away, %d offline)", n_online + n_away + n_offline, n_online,
	            n_away, n_offline);

	if (regex) {
		g_regex_unref(regex);
	}
}

static gint prplcmp(gconstpointer a, gconstpointer b)
{
	const struct prpl *pa = a;
	const struct prpl *pb = b;

	return g_strcasecmp(pa->name, pb->name);
}

static void prplstr(GList *prpls, GString *gstr)
{
	const char *last = NULL;
	GList *l;
	struct prpl *p;

	prpls = g_list_copy(prpls);
	prpls = g_list_sort(prpls, prplcmp);

	for (l = prpls; l; l = l->next) {
		p = l->data;

		if (last && g_strcasecmp(p->name, last) == 0) {
			/* Ignore duplicates (mainly for libpurple) */
			continue;
		}

		if (gstr->len != 0) {
			g_string_append(gstr, ", ");
		}

		g_string_append(gstr, p->name);
		last = p->name;
	}

	g_list_free(prpls);
}

static void cmd_plugins_info(irc_t *irc, char **cmd)
{
	GList *l;
	struct plugin_info *info;

	MIN_ARGS(2);

	for (l = get_plugins(); l; l = l->next) {
		info = l->data;
		if (g_strcasecmp(cmd[2], info->name) == 0) {
			break;
		}
	}

	if (!l) {
		return;
	}

	irc_rootmsg(irc, "%s:", info->name);
	irc_rootmsg(irc, "  Version: %s", info->version);

	if (info->description) {
		irc_rootmsg(irc, "  Description: %s", info->description);
	}

	if (info->author) {
		irc_rootmsg(irc, "  Author: %s", info->author);
	}

	if (info->url) {
		irc_rootmsg(irc, "  URL: %s", info->url);
	}
}

static void cmd_plugins(irc_t *irc, char **cmd)
{
	GList *prpls;
	GString *gstr;

	if (cmd[1] && g_strcasecmp(cmd[1], "info") == 0) {
		cmd_plugins_info(irc, cmd);
		return;
	}

#ifdef WITH_PLUGINS
	GList *l;
	struct plugin_info *info;
	char *format;

	if (strchr(irc->umode, 'b') != NULL) {
		format = "%s\t%s";
	} else {
		format = "%-30s  %s";
	}

	irc_rootmsg(irc, format, "Plugin", "Version");

	for (l = get_plugins(); l; l = l->next) {
		char *c;
		info = l->data;

		/* some purple plugins like to include several versions separated by newlines... */
		if ((c = strchr(info->version, '\n'))) {
			char *version = g_strndup(info->version, c - info->version);
			irc_rootmsg(irc, format, info->name, version);
			g_free(version);
		} else {
			irc_rootmsg(irc, format, info->name, info->version);
		}
	}
#endif

	irc_rootmsg(irc, " ");

	gstr = g_string_new(NULL);
	prpls = get_protocols();

	if (prpls) {
		prplstr(prpls, gstr);
		irc_rootmsg(irc, "Enabled Protocols: %s", gstr->str);
		g_string_truncate(gstr, 0);
	}

	prpls = get_protocols_disabled();

	if (prpls) {
		prplstr(prpls, gstr);
		irc_rootmsg(irc, "Disabled Protocols: %s", gstr->str);
	}

	g_string_free(gstr, TRUE);
}

static void cmd_qlist(irc_t *irc, char **cmd)
{
	query_t *q = irc->queries;
	int num;

	if (!q) {
		irc_rootmsg(irc, "There are no pending questions.");
		return;
	}

	irc_rootmsg(irc, "Pending queries:");

	for (num = 0; q; q = q->next, num++) {
		if (q->ic) { /* Not necessary yet, but it might come later */
			irc_rootmsg(irc, "%d, %s: %s", num, q->ic->acc->tag, q->question);
		} else {
			irc_rootmsg(irc, "%d, BitlBee: %s", num, q->question);
		}
	}
}

static void cmd_chat(irc_t *irc, char **cmd)
{
	account_t *acc;

	if (g_strcasecmp(cmd[1], "add") == 0) {
		bee_chat_info_t *ci;
		char *channel, *room, *s;
		struct irc_channel *ic;
		guint i;

		MIN_ARGS(3);

		if (!(acc = account_get(irc->b, cmd[2]))) {
			irc_rootmsg(irc, "Invalid account");
			return;
		} else if (!acc->prpl->chat_join) {
			irc_rootmsg(irc, "Named chatrooms not supported on that account.");
			return;
		}

		if (cmd[3][0] == '!') {
			if (!acc->ic || !(acc->ic->flags & OPT_LOGGED_IN)) {
				irc_rootmsg(irc, "Not logged in to account.");
				return;
			} else if (!acc->prpl->chat_list) {
				irc_rootmsg(irc, "Listing chatrooms not supported on that account.");
				return;
			}

			i = g_ascii_strtoull(cmd[3] + 1, NULL, 10);
			ci = g_slist_nth_data(acc->ic->chatlist, i - 1);

			if (ci == NULL) {
				irc_rootmsg(irc, "Invalid chatroom index");
				return;
			}

			room = ci->title;
		} else {
			room = cmd[3];
		}

		if (cmd[4] == NULL) {
			channel = g_strdup(room);
			if ((s = strchr(channel, '@'))) {
				*s = 0;
			}
		} else {
			channel = g_strdup(cmd[4]);
		}

		if (strchr(CTYPES, channel[0]) == NULL) {
			s = g_strdup_printf("#%s", channel);
			g_free(channel);
			channel = s;

			irc_channel_name_strip(channel);
		}

		if ((ic = irc_channel_new(irc, channel)) &&
		    set_setstr(&ic->set, "type", "chat") &&
		    set_setstr(&ic->set, "chat_type", "room") &&
		    set_setstr(&ic->set, "account", cmd[2]) &&
		    set_setstr(&ic->set, "room", room)) {
			irc_rootmsg(irc, "Chatroom successfully added, join with \002/join %s\002", channel);
		} else {
			if (ic) {
				irc_channel_free(ic);

				irc_rootmsg(irc, "Error adding chatroom.");
			} else if (irc_channel_by_name(irc, channel)) {
				irc_rootmsg(irc, "A channel named `%s' already exists. "
				            "Join with \002/join %s\002 or see \002help channel\002 to modify it",
				            channel, channel);
			} else {
				irc_rootmsg(irc, "Error creating channel for chatroom.");
			}
		}
		g_free(channel);
	} else if (g_strcasecmp(cmd[1], "list") == 0) {
		MIN_ARGS(2);

		if (!(acc = account_get(irc->b, cmd[2]))) {
			irc_rootmsg(irc, "Invalid account");
			return;
		} else if (!acc->ic || !(acc->ic->flags & OPT_LOGGED_IN)) {
			irc_rootmsg(irc, "Not logged in to account.");
			return;
		} else if (!acc->prpl->chat_list) {
			irc_rootmsg(irc, "Listing chatrooms not supported on that account.");
			return;
		}

		acc->prpl->chat_list(acc->ic, cmd[3]);
	} else if (g_strcasecmp(cmd[1], "with") == 0) {
		irc_user_t *iu;

		MIN_ARGS(2);

		if ((iu = irc_user_by_name(irc, cmd[2])) &&
		    iu->bu && iu->bu->ic->acc->prpl->chat_with) {
			if (!iu->bu->ic->acc->prpl->chat_with(iu->bu->ic, iu->bu->handle)) {
				irc_rootmsg(irc, "(Possible) failure while trying to open "
				            "a groupchat with %s.", iu->nick);
			}
		} else {
			irc_rootmsg(irc, "Can't open a groupchat with %s.", cmd[2]);
		}
	} else if (g_strcasecmp(cmd[1], "set") == 0 ||
	           g_strcasecmp(cmd[1], "del") == 0) {
		irc_rootmsg(irc, "Unknown command: chat %s. Did you mean \002channel %s\002?", cmd[1], cmd[1]);
	} else {
		irc_rootmsg(irc,
		            "Unknown command: %s %s. Please use \x02help commands\x02 to get a list of available commands.", "chat",
		            cmd[1]);
	}
}

/* some arbitrary numbers */
#define CHAT_TITLE_LEN_MIN 20
#define CHAT_TITLE_LEN_MAX 100

void cmd_chat_list_finish(struct im_connection *ic)
{
	account_t *acc = ic->acc;
	bee_chat_info_t *ci;
	char *hformat, *iformat, *topic, *padded;
	GSList *l;
	guint i = 0;
	long title_len, new_len;
	irc_t *irc = ic->bee->ui_data;

	if (ic->chatlist == NULL) {
		irc_rootmsg(irc, "No existing chatrooms");
		return;
	}

	/* find a reasonable width for the table */
	title_len = CHAT_TITLE_LEN_MIN;

	for (l = ic->chatlist; l; l = l->next) {
		ci = l->data;
		new_len = g_utf8_strlen(ci->title, -1);

		if (new_len >= CHAT_TITLE_LEN_MAX) {
			title_len = CHAT_TITLE_LEN_MAX;
			break;
		} else if (title_len < new_len) {
			title_len = new_len;
		}
	}

	if (strchr(irc->umode, 'b') != NULL) {
		hformat = "%s\t%s\t%s";
		iformat = "%u\t%s\t%s";
	} else {
		hformat = "%s  %s  %s";
		iformat = "%5u  %s  %s";
	}

	padded = str_pad_and_truncate("Title", title_len, NULL);
	irc_rootmsg(irc, hformat, "Index", padded, "Topic");
	g_free(padded);

	for (l = ic->chatlist; l; l = l->next) {
		ci = l->data;
		topic = ci->topic ? ci->topic : "";

		padded = str_pad_and_truncate(ci->title ? ci->title : "", title_len, "[...]");
		irc_rootmsg(irc, iformat, ++i, padded, topic);
		g_free(padded);
	}

	irc_rootmsg(irc, "%u %s chatrooms", i, acc->tag);
}

static void cmd_group(irc_t *irc, char **cmd)
{
	GSList *l;
	int len;

	len = strlen(cmd[1]);
	if (g_strncasecmp(cmd[1], "list", len) == 0) {
		int n = 0;

		if (strchr(irc->umode, 'b')) {
			irc_rootmsg(irc, "Group list:");
		}

		for (l = irc->b->groups; l; l = l->next) {
			bee_group_t *bg = l->data;
			irc_rootmsg(irc, "%d. %s", n++, bg->name);
		}
		irc_rootmsg(irc, "End of group list");
	} else if (g_strncasecmp(cmd[1], "info", len) == 0) {
		bee_group_t *bg;
		int n = 0;

		MIN_ARGS(2);
		bg = bee_group_by_name(irc->b, cmd[2], FALSE);

		if (bg) {
			if (strchr(irc->umode, 'b')) {
				irc_rootmsg(irc, "Members of %s:", cmd[2]);
			}
			for (l = irc->b->users; l; l = l->next) {
				bee_user_t *bu = l->data;
				if (bu->group == bg) {
					irc_rootmsg(irc, "%d. %s", n++, bu->nick ? : bu->handle);
				}
			}
			irc_rootmsg(irc, "End of member list");
		} else {
			irc_rootmsg(irc,
			            "Unknown group: %s. Please use \x02group list\x02 to get a list of available groups.",
			            cmd[2]);
		}
	} else {
		irc_rootmsg(irc,
		            "Unknown command: %s %s. Please use \x02help commands\x02 to get a list of available commands.", "group",
		            cmd[1]);
	}
}

static void cmd_transfer(irc_t *irc, char **cmd)
{
	GSList *files = irc->file_transfers;
	GSList *next;

	enum { LIST, REJECT, CANCEL };
	int subcmd = LIST;
	int fid;

	if (!files) {
		irc_rootmsg(irc, "No pending transfers");
		return;
	}

	if (cmd[1] && (strcmp(cmd[1], "reject") == 0)) {
		subcmd = REJECT;
	} else if (cmd[1] && (strcmp(cmd[1], "cancel") == 0) &&
	           cmd[2] && (sscanf(cmd[2], "%d", &fid) == 1)) {
		subcmd = CANCEL;
	}

	for (; files; files = next) {
		next = files->next;
		file_transfer_t *file = files->data;

		switch (subcmd) {
		case LIST:
			if (file->status == FT_STATUS_LISTENING) {
				irc_rootmsg(irc,
				            "Pending file(id %d): %s (Listening...)", file->local_id, file->file_name);
			} else {
				int kb_per_s = 0;
				time_t diff = time(NULL) - file->started ? : 1;
				if ((file->started > 0) && (file->bytes_transferred > 0)) {
					kb_per_s = file->bytes_transferred / 1024 / diff;
				}

				irc_rootmsg(irc,
				            "Pending file(id %d): %s (%10zd/%zd kb, %d kb/s)", file->local_id,
				            file->file_name,
				            file->bytes_transferred / 1024, file->file_size / 1024, kb_per_s);
			}
			break;
		case REJECT:
			if (file->status == FT_STATUS_LISTENING) {
				irc_rootmsg(irc, "Rejecting file transfer for %s", file->file_name);
				imcb_file_canceled(file->ic, file, "Denied by user");
			}
			break;
		case CANCEL:
			if (file->local_id == fid) {
				irc_rootmsg(irc, "Canceling file transfer for %s", file->file_name);
				imcb_file_canceled(file->ic, file, "Canceled by user");
			}
			break;
		}
	}
}

/* Maybe this should be a stand-alone command as well? */
static void bitlbee_whatsnew(irc_t *irc)
{
	int last = set_getint(&irc->b->set, "last_version");
	char s[16], *msg;

	if (last >= BITLBEE_VERSION_CODE) {
		return;
	}

	msg = help_get_whatsnew(&(global.help), last);

	if (msg) {
		irc_rootmsg(irc, "%s: This seems to be your first time using this "
		            "this version of BitlBee. Here's a list of new "
		            "features you may like to know about:\n\n%s\n",
		            irc->user->nick, msg);
	}

	g_free(msg);

	g_snprintf(s, sizeof(s), "%d", BITLBEE_VERSION_CODE);
	set_setstr(&irc->b->set, "last_version", s);
}

/* IMPORTANT: Keep this list sorted! The short command logic needs that. */
command_t root_commands[] = {
	{ "account",        1, cmd_account,        0 },
	{ "add",            2, cmd_add,            0 },
	{ "allow",          1, cmd_allow,          0 },
	{ "blist",          0, cmd_blist,          0 },
	{ "block",          1, cmd_block,          0 },
	{ "channel",        1, cmd_channel,        0 },
	{ "chat",           1, cmd_chat,           0 },
	{ "drop",           1, cmd_drop,           0 },
	{ "ft",             0, cmd_transfer,       0 },
	{ "group",          1, cmd_group,          0 },
	{ "help",           0, cmd_help,           0 },
	{ "identify",       0, cmd_identify,       0 },
	{ "info",           1, cmd_info,           0 },
	{ "no",             0, cmd_yesno,          0 },
	{ "plugins",        0, cmd_plugins,        0 },
	{ "qlist",          0, cmd_qlist,          0 },
	{ "register",       0, cmd_register,       0 },
	{ "remove",         1, cmd_remove,         0 },
	{ "rename",         2, cmd_rename,         0 },
	{ "save",           0, cmd_save,           0 },
	{ "set",            0, cmd_set,            0 },
	{ "transfer",       0, cmd_transfer,       0 },
	{ "yes",            0, cmd_yesno,          0 },
	/* Not expecting too many plugins adding root commands so just make a
	   dumb array with some empty entried at the end. */
	{ NULL },
	{ NULL },
	{ NULL },
	{ NULL },
	{ NULL },
	{ NULL },
	{ NULL },
	{ NULL },
	{ NULL },
};
static const int num_root_commands = sizeof(root_commands) / sizeof(command_t);

gboolean root_command_add(const char *command, int params, void (*func)(irc_t *, char **args), int flags)
{
	int i;

	if (root_commands[num_root_commands - 2].command) {
		/* Planning fail! List is full. */
		return FALSE;
	}

	for (i = 0; root_commands[i].command; i++) {
		if (g_strcasecmp(root_commands[i].command, command) == 0) {
			return FALSE;
		} else if (g_strcasecmp(root_commands[i].command, command) > 0) {
			break;
		}
	}
	memmove(root_commands + i + 1, root_commands + i,
	        sizeof(command_t) * (num_root_commands - i - 1));

	root_commands[i].command = g_strdup(command);
	root_commands[i].required_parameters = params;
	root_commands[i].execute = func;
	root_commands[i].flags = flags;

	return TRUE;
}
