/* Main OperServ module.
 *
 * IRC Services is copyright (c) 1996-2002 Andrew Church.
 *     E-mail: <achurch@achurch.org>
 * Parts written by Andrew Kempe and others.
 * This program is free but copyrighted software; see the file COPYING for
 * details.
 */

#include "services.h"
#include "modules.h"
#include "conffile.h"
#include "language.h"
#include "commands.h"
#include "timeout.h"
#include "encrypt.h"
#include "ignore.h"
#include "modules/nickserv/nickserv.h"
#ifdef DEBUG_COMMANDS
# include "modules/chanserv/chanserv.h"  /* for send_user_info */
#endif

#include "operserv.h"
#include "maskdata.h"
#include "akill.h"
#include "news.h"

/* Maximum depth for SERVERMAP command */
#define MAXMAPLEVEL	64

/*************************************************************************/

static Module *module;
static Module *module_akill;
static Module *module_nickserv;

       char *s_OperServ;
static char *desc_OperServ;
       char *s_GlobalNoticer;
static char *desc_GlobalNoticer;
static char *OperDBName;
EXPORT_VAR(char *,s_OperServ)
EXPORT_VAR(char *,s_GlobalNoticer)

       char * ServicesRoot;
static int    WallOper;
static int    WallBadOS;
static int    WallOSChannel;
static int    WallSU;
static int    KillClonesAutokill;
static time_t KillClonesAutokillExpire;
static int    AllowRaw;
EXPORT_VAR(char *,ServicesRoot)

static int cb_command   = -1;
static int cb_expire_md = -1;
static int cb_help      = -1;
static int cb_help_cmds = -1;
static int cb_set       = -1;
static int cb_stats     = -1;
static int cb_stats_all = -1;

static int db_opened = 0;

/* Services super-user password */
static char supass[PASSMAX];
/* Is the password currently unset? */
static int8 no_supass = 1;

/*************************************************************************/

static void do_help(User *u);
static void do_global(User *u);
static void do_stats(User *u);
static void do_servermap(User *u);
static void do_admin(User *u);
static void do_oper(User *u);
static void do_os_mode(User *u);
static void do_clearmodes(User *u);
static void do_clearchan(User *u);
static void do_os_kick(User *u);
static void do_su(User *u);
static void do_set(User *u);
static void do_jupe(User *u);
static void do_raw(User *u);
static void do_update(User *u);
static void do_os_quit(User *u);
static void do_shutdown(User *u);
static void do_restart(User *u);
static void do_rehash(User *u);
static void do_listignore(User *u);
static void do_killclones(User *u);

#ifdef DEBUG_COMMANDS
static void send_server_list(User *u);
static void send_channel_list(User *u);
static void send_channel_users(User *u);
static void send_user_list(User *u);
static void send_user_info(User *u);
static void do_matchwild(User *u);
static void do_setcmode(User *u);
#endif

/*************************************************************************/

static Command cmds[] = {
    {"HELP",      do_help,      NULL,             -1,                  -1,-1},
    {"GLOBAL",    do_global,    NULL,             OPER_HELP_GLOBAL,    -1,-1},
    {"STATS",     do_stats,     NULL,             OPER_HELP_STATS,     -1,-1},
    {"UPTIME",    do_stats,     NULL,             OPER_HELP_STATS,     -1,-1},
    {"SERVERMAP", do_servermap, NULL,             OPER_HELP_SERVERMAP, -1,-1},
    /* Anyone can use the LIST option to the ADMIN and OPER commands; those
     * routines check privileges to ensure that only authorized users
     * modify the list. */
    {"ADMIN",     do_admin,     NULL,             OPER_HELP_ADMIN,     -1,-1},
    {"OPER",      do_oper,      NULL,             OPER_HELP_OPER,      -1,-1},

    /* Commands for Services opers: */
    {"MODE",      do_os_mode,   is_services_oper, OPER_HELP_MODE,      -1,-1},
    {"KICK",      do_os_kick,   is_services_oper, OPER_HELP_KICK,      -1,-1},
    {"CLEARMODES",do_clearmodes,is_services_oper, OPER_HELP_CLEARMODES,-1,-1},
    {"CLEARCHAN", do_clearchan, is_services_oper, OPER_HELP_CLEARCHAN, -1,-1},
    {"KILLCLONES",do_killclones,is_services_oper, OPER_HELP_KILLCLONES,-1,-1},

    /* Commands for Services admins: */
    {"SU",        do_su,        NULL,             OPER_HELP_SU,        -1,-1},
    {"SET",       do_set,       is_services_admin,OPER_HELP_SET,       -1,-1},
    {"SET READONLY",NULL,       NULL,           OPER_HELP_SET_READONLY,-1,-1},
    {"SET DEBUG", NULL,         NULL,             OPER_HELP_SET_DEBUG, -1,-1},
    {"SET SUPASS",NULL,         NULL,             OPER_HELP_SET_SUPASS,-1,-1},
    {"JUPE",      do_jupe,      is_services_admin,OPER_HELP_JUPE,      -1,-1},
    {"UPDATE",    do_update,    is_services_admin,OPER_HELP_UPDATE,    -1,-1},
    {"QUIT",      do_os_quit,   is_services_admin,OPER_HELP_QUIT,      -1,-1},
    {"SHUTDOWN",  do_shutdown,  is_services_admin,OPER_HELP_SHUTDOWN,  -1,-1},
    {"RESTART",   do_restart,   is_services_admin,OPER_HELP_RESTART,   -1,-1},
    {"REHASH",    do_rehash,    is_services_admin,OPER_HELP_REHASH,    -1,-1,
		IRCSERVICES_CONF },
    {"LISTIGNORE",do_listignore,is_services_admin,-1,                  -1,-1},

    /* Commands for Services super-user: */
    {"RAW",       do_raw,       is_services_root, OPER_HELP_RAW,       -1,-1},

#ifdef DEBUG_COMMANDS
    {"LISTSERVERS", send_server_list,   is_services_root, -1,-1,-1},
    {"LISTCHANS",   send_channel_list,  is_services_root, -1,-1,-1},
    {"LISTCHAN",    send_channel_users, is_services_root, -1,-1,-1},
    {"LISTUSERS",   send_user_list,     is_services_root, -1,-1,-1},
    {"LISTUSER",    send_user_info,     is_services_root, -1,-1,-1},
    {"LISTTIMERS",  send_timeout_list,  is_services_root, -1,-1,-1},
    {"MATCHWILD",   do_matchwild,       is_services_root, -1,-1,-1},
    {"SETCMODE",    do_setcmode,        is_services_root, -1,-1,-1},
#endif

    /* Fencepost: */
    { NULL }
};

/*************************************************************************/
/*************************** NickServ imports ****************************/
/*************************************************************************/

/* This is somewhat inefficient, but since it's not called often it should
 * do for the time being.  FIXME: find a more general way to implement
 * intermodule function imports. (cf. database/version4's extsyms.[ch])
 */

#define _get_ngi local__get_ngi

static NickGroupInfo *local__get_ngi(NickInfo *ni, const char *file, int line)
{
    typeof(_get_ngi) *p__get_ngi;

    if (!module_nickserv)
	return NULL;
    p__get_ngi = get_module_symbol(module_nickserv, "_get_ngi");
    if (!p__get_ngi) {
	module_log("Unable to find symbol `_get_ngi' in module"
		   " `nickserv/main' (called from %s:%d)", file, line);
	return NULL;
    }
    return p__get_ngi(ni, file, line);
}

/*************************************************************************/
/***************************** Main routines *****************************/
/*************************************************************************/

/* Introduce the OperServ pseudoclient and related clients. */

static int introduce_operserv(const char *nick)
{
    if (!nick || irc_stricmp(nick, s_OperServ) == 0) {
	char modebuf[BUFSIZE];
	snprintf(modebuf, sizeof(modebuf), "oi%s", pseudoclient_modes);
	send_nick(s_OperServ, ServiceUser, ServiceHost, ServerName,
		  desc_OperServ, modebuf);
	if (nick)
	    return 1;
    }
    if (!nick || irc_stricmp(nick, s_GlobalNoticer) == 0) {
	char modebuf[BUFSIZE];
	snprintf(modebuf, sizeof(modebuf), "oi%s", pseudoclient_modes);
	send_nick(s_GlobalNoticer, ServiceUser, ServiceHost, ServerName,
		  desc_GlobalNoticer, modebuf);
	if (nick)
	    return 1;
    }
    return 0;
}

/*************************************************************************/

/* Main OperServ routine. */

static int operserv(const char *source, const char *target, char *buf)
{
    char *cmd;
    char *s;
    User *u = get_user(source);

    if (irc_stricmp(target, s_OperServ) != 0)
	return 0;

    if (!u) {
	module_log("user record for %s not found", source);
	notice(s_OperServ, source, "Access denied.");
	if (WallBadOS)
	    wallops(s_OperServ, "Denied access to %s from %s (user record"
	                        " missing)", s_OperServ, source);
	return 1;
    }

    if (!is_oper(u)) {
	notice_lang(s_OperServ, u, ACCESS_DENIED);
	if (WallBadOS)
	    wallops(s_OperServ, "Denied access to %s from %s (non-oper)",
		    s_OperServ, source);
	module_log("Non-oper %s!%s@%s sent: %s",
		   u->nick, u->username, u->host, buf);
	return 1;
    }

    /* Don't log stuff that might be passwords */
    if (strnicmp(buf, "SU ", 3) == 0) {
	module_log("%s: SU xxxxxx", source);
    } else if (strnicmp(buf, "SET ", 4) == 0
	       && (s = stristr(buf, "SUPASS")) != NULL
	       && strspn(buf+4, " ") == s-(buf+4)) {
	/* All that was needed to make sure someone doesn't fool us with
	 * "SET READONLY ON SUPASS".  Which wouldn't work anyway, but we
	 * ought to log it properly... */
	module_log("%s: SET SUPASS xxxxxx", source);
    } else {
	module_log("%s: %s", source, buf);
    }

    cmd = strtok(buf, " ");
    if (!cmd) {
	return 1;
    } else if (stricmp(cmd, "\1PING") == 0) {
	if (!(s = strtok(NULL, "")))
	    s = "\1";
	notice(s_OperServ, source, "\1PING %s", s);
    } else {
	if (call_callback_2(module, cb_command, u, cmd) <= 0)
	    run_cmd(s_OperServ, u, module, cmd);
    }

    return 1;
}

/*************************************************************************/

/* Return a /WHOIS response for OperServ or the global noticer. */

static int operserv_whois(const char *source, char *who, char *extra)
{
    if (irc_stricmp(who, s_OperServ) == 0) {
	send_cmd(ServerName, "311 %s %s %s %s * :%s", source, who,
		 ServiceUser, ServiceHost, desc_OperServ);
    } else if (irc_stricmp(who, s_GlobalNoticer) == 0) {
	send_cmd(ServerName, "311 %s %s %s %s * :%s", source, who,
		 ServiceUser, ServiceHost, desc_GlobalNoticer);
    } else {
	return 0;
    }
    send_cmd(ServerName, "312 %s %s %s :%s", source, who,
	     ServerName, ServerDesc);
    send_cmd(ServerName, "313 %s %s :is a network service", source, who);
    send_cmd(ServerName, "318 %s %s End of /WHOIS response.", source, who);
    return 1;
}

/*************************************************************************/

/* Watch for umode +o and send wallops.  This callback is only activated if
 * WallOper is specified in modules.conf.
 */

static int wall_oper_callback(User *u, int modechar, int add)
{
    if (modechar == 'o' && add)
	wallops(s_OperServ, "\2%s\2 is now an IRC operator.", u->nick);
    return 0;
}

/*************************************************************************/

/* Callback for saving data. */

static int do_save_data(void)
{
    sync_operserv_db(OperDBName);
    return 0;
}

/*************************************************************************/

/* Callback for NickServ REGISTER/LINK check; we disallow
 * registration/linking of the OperServ pseudoclient nickname.
 */

static int do_reglink_check(const User *u, const char *nick,
			    const char *pass, const char *email)
{
    return irc_stricmp(nick, s_OperServ) == 0;
}

/*************************************************************************/
/**************************** Privilege checks ***************************/
/*************************************************************************/

/* Does the given user have Services super-user privileges? */

EXPORT_FUNC(is_services_root)
int is_services_root(User *u)
{
    NickInfo *ni;
    int rootid;
    static int warned_ni = 0, warned_id = 0;

    if (u->flags & UF_SERVROOT)
	return 1;
    if (!(ni = get_nickinfo(ServicesRoot))) {
	if (!warned_ni) {
	    wallops(s_OperServ, "Warning: Services super-user nickname %s"
		    " is not registered", ServicesRoot);
	    warned_ni = 1;
	}
	module_log("warning: ServicesRoot nickname not registered");
	return 0;
    }
    warned_ni = 0;
    if (!(rootid = ni->nickgroup)) {
	if (!warned_id) {
	    wallops(s_OperServ, "Warning: Services super-user nickname %s"
		    " is forbidden or not properly registered", ServicesRoot);
	    warned_id = 1;
	}
	module_log("warning: ServicesRoot nickname forbidden or registered"
		   " data corrupt");
	return 0;
    }
    if (!is_oper(u) || !u->ni || u->ni->nickgroup != rootid)
	return 0;
    if (user_identified(u))
	return 1;
    return 0;
}

/*************************************************************************/

/* Does the given user have Services admin privileges? */

EXPORT_FUNC(is_services_admin)
int is_services_admin(User *u)
{
    if (!is_oper(u) || !user_identified(u))
	return 0;
    if (is_services_root(u))
	return 1;
    return u->ngi && u->ngi != NICKGROUPINFO_INVALID
		  && u->ngi->os_priv >= NP_SERVADMIN;
}

/*************************************************************************/

/* Does the given user have Services oper privileges? */

EXPORT_FUNC(is_services_oper)
int is_services_oper(User *u)
{
    if (!is_oper(u) || !user_identified(u))
	return 0;
    if (is_services_root(u))
	return 1;
    return u->ngi && u->ngi != NICKGROUPINFO_INVALID
		  && u->ngi->os_priv >= NP_SERVOPER;
}

/*************************************************************************/

/* Is the given nick a Services admin/root nick? */

/* NOTE: Do not use this to check if a user who is online is a services
 * admin or root. This function only checks if a user has the ABILITY to be
 * a services admin. Rather use is_services_admin(User *u). -TheShadow */

EXPORT_FUNC(nick_is_services_admin)
int nick_is_services_admin(NickInfo *ni)
{
    NickGroupInfo *ngi;

    if (!ni || !(ngi = get_ngi(ni)))
	return 0;
    if (stricmp(ni->nick, ServicesRoot) == 0)
	return 1;
    return ngi->os_priv >= NP_SERVADMIN;
}

/*************************************************************************/
/********************** Admin/oper list modification *********************/
/*************************************************************************/

#define LIST_ADMIN	0
#define LIST_OPER	1

#define MSG_EXISTS	0
#define MSG_EXISTS_NEXT	1
#define MSG_ADDED	2
#define MSG_TOOMANY	3
#define MSG_REMOVED	4
#define MSG_NOTFOUND	5

static int privlist_msgs[2][6] = {
    { OPER_ADMIN_EXISTS,
      0,
      OPER_ADMIN_ADDED,
      OPER_ADMIN_TOO_MANY,
      OPER_ADMIN_REMOVED,
      OPER_ADMIN_NOT_FOUND,
    },
    { OPER_OPER_EXISTS,
      OPER_ADMIN_EXISTS,
      OPER_OPER_ADDED,
      OPER_OPER_TOO_MANY,
      OPER_OPER_REMOVED,
      OPER_OPER_NOT_FOUND,
    },
};

/*************************************************************************/

/* Add a nick to the Services admin/oper list.  u is the command sender. */

static void privlist_add(User *u, int listid, const char *nick)
{
    int16 level = (listid==LIST_ADMIN ? NP_SERVADMIN : NP_SERVOPER);
    int16 nextlevel = (listid==LIST_ADMIN ? 0 : NP_SERVADMIN);
    int *msgs = privlist_msgs[listid];
    NickInfo *ni;
    NickGroupInfo *ngi;

    if (!(ni = get_nickinfo(nick))) {
	notice_lang(s_OperServ, u, NICK_X_NOT_REGISTERED, nick);
	return;
    }
    if (!(ngi = get_ngi(ni))) {
	notice_lang(s_OperServ, u, INTERNAL_ERROR);
	return;
    }
    if (nextlevel && ngi->os_priv >= nextlevel) {
	notice_lang(s_OperServ, u, msgs[MSG_EXISTS_NEXT], nick);
	return;
    } else if (ngi->os_priv >= level) {
	notice_lang(s_OperServ, u, msgs[MSG_EXISTS], nick);
	return;
    }
    ngi->os_priv = level;
    put_nickgroupinfo(ngi);
    notice_lang(s_OperServ, u, msgs[MSG_ADDED], nick);
    if (readonly)
	notice_lang(s_OperServ, u, READ_ONLY_MODE);
    else
	put_nickgroupinfo(ngi);
}

/*************************************************************************/

/* Remove a nick from the Services admin/oper list. */

static void privlist_rem(User *u, int listid, const char *nick)
{
    int16 level = (listid==LIST_ADMIN ? NP_SERVADMIN : NP_SERVOPER);
    int16 nextlevel = (listid==LIST_ADMIN ? 0 : NP_SERVADMIN);
    int *msgs = privlist_msgs[listid];
    NickInfo *ni;
    NickGroupInfo *ngi;

    if (!(ni = get_nickinfo(nick))) {
	notice_lang(s_OperServ, u, NICK_X_NOT_REGISTERED, nick);
	return;
    }
    if (!(ngi = get_ngi(ni))) {
	notice_lang(s_OperServ, u, INTERNAL_ERROR);
	return;
    }
    if (ngi->os_priv < level || (nextlevel && ngi->os_priv >= nextlevel)) {
	notice_lang(s_OperServ, u, msgs[MSG_NOTFOUND], nick);
	return;
    }
    ngi->os_priv = 0;
    put_nickgroupinfo(ngi);
    notice_lang(s_OperServ, u, msgs[MSG_REMOVED], nick);
    if (readonly)
	notice_lang(s_OperServ, u, READ_ONLY_MODE);
    else
	put_nickgroupinfo(ngi);
}

/*************************************************************************/
/*********************** OperServ command functions **********************/
/*************************************************************************/

/* HELP command. */

static void do_help(User *u)
{
    char *cmd = strtok_remaining();
    Module *mod;

    if (!cmd) {
	notice_help(s_OperServ, u, OPER_HELP);
    } else if (call_callback_2(module, cb_help, u, cmd) > 0) {
	return;
    } else if (stricmp(cmd, "COMMANDS") == 0) {
	notice_help(s_OperServ, u, OPER_HELP_COMMANDS);
	call_callback_2(module, cb_help_cmds, u, 0);
	notice_help(s_OperServ, u, OPER_HELP_COMMANDS_SERVOPER);
	if ((mod = find_module("operserv/akill"))) {
	    int *p_EnableExclude;
	    notice_help(s_OperServ, u, OPER_HELP_COMMANDS_AKILL);
	    p_EnableExclude = get_module_symbol(mod, "EnableExclude");
	    if (p_EnableExclude && *p_EnableExclude)
		notice_help(s_OperServ, u, OPER_HELP_COMMANDS_EXCLUDE);
	}
	if (find_module("operserv/sline"))
	    notice_help(s_OperServ, u, OPER_HELP_COMMANDS_SLINE);
	if (find_module("operserv/sessions"))
	    notice_help(s_OperServ, u, OPER_HELP_COMMANDS_SESSION);
	if (find_module("operserv/news"))
	    notice_help(s_OperServ, u, OPER_HELP_COMMANDS_NEWS);
	call_callback_2(module, cb_help_cmds, u, 1);
	notice_help(s_OperServ, u, OPER_HELP_COMMANDS_SERVADMIN);
	call_callback_2(module, cb_help_cmds, u, 2);
	notice_help(s_OperServ, u, OPER_HELP_COMMANDS_SERVROOT);
	if (AllowRaw)
	    notice_help(s_OperServ, u, OPER_HELP_COMMANDS_RAW);
	call_callback_2(module, cb_help_cmds, u, 3);
    } else {
	help_cmd(s_OperServ, u, module, cmd);
    }
}

/*************************************************************************/

/* Global notice sending via GlobalNoticer. */

static void do_global(User *u)
{
    char *msg = strtok_remaining();

    if (!msg) {
	syntax_error(s_OperServ, u, "GLOBAL", OPER_GLOBAL_SYNTAX);
	return;
    }
    notice_all(s_GlobalNoticer, "%s", msg);
}

/*************************************************************************/

/* STATS command. */

static void do_stats(User *u)
{
    time_t uptime = time(NULL) - start_time;
    char *extra = strtok_remaining();
    int days = uptime/86400, hours = (uptime/3600)%24,
        mins = (uptime/60)%60, secs = uptime%60;
    char timebuf[BUFSIZE];

    if (extra && stricmp(extra, "UPTIME") == 0)
	extra = NULL;

    if (extra && stricmp(extra, "ALL") != 0) {
	if (call_callback_2(module, cb_stats, u, extra) > 0) {
	    /* nothing */
	} else if (stricmp(extra, "RESET") == 0) {
	    int i;
	    maxusercnt = usercnt;
	    maxusertime = time(NULL);
	    notice_lang(s_OperServ, u, OPER_STATS_RESET_USER_COUNT);
	    if ((i = OSDATA_MAXUSERCNT,  !put_operserv_data(i, &maxusercnt))
	     || (i = OSDATA_MAXUSERTIME, !put_operserv_data(i, &maxusertime))
	    ) {
		module_log("STATS RESET: put_operserv_data(%d) failed", i);
		wallops(s_OperServ, "Unable to update OperServ data!");
	    }
	} else if (stricmp(extra, "NETWORK") == 0) {
	    uint32 read, written, socksize, totalsize;
	    int ratio1, ratio2;
	    sock_rwstat(servsock, &read, &written);
	    sock_bufstat(servsock, &socksize, &totalsize, &ratio1, &ratio2);
	    socksize /= 1024;
	    totalsize /= 1024;
	    notice_lang(s_OperServ, u, OPER_STATS_KBYTES_READ, read);
	    notice_lang(s_OperServ, u, OPER_STATS_KBYTES_WRITTEN, written);
	    if (ratio1)
		notice_lang(s_OperServ, u, OPER_STATS_NETBUF_SOCK_PERCENT,
			    socksize, ratio1);
	    else
		notice_lang(s_OperServ, u, OPER_STATS_NETBUF_SOCK, socksize);
	    if (ratio2)
		notice_lang(s_OperServ, u, OPER_STATS_NETBUF_TOTAL_PERCENT,
			    totalsize, ratio2);
	    else
		notice_lang(s_OperServ, u, OPER_STATS_NETBUF_TOTAL, totalsize);
	} else {
	    notice_lang(s_OperServ, u, OPER_STATS_UNKNOWN_OPTION,
			strupper(extra));
	}
	return;
    }

    notice_lang(s_OperServ, u, OPER_STATS_CURRENT_USERS, usercnt, opcnt);
    strftime_lang(timebuf, sizeof(timebuf), u->ngi,
		  STRFTIME_DATE_TIME_FORMAT, maxusertime);
    notice_lang(s_OperServ, u, OPER_STATS_MAX_USERS, maxusercnt, timebuf);
    if (days >= 1) {
	const char *str = getstring(u->ngi, days!=1 ? STR_DAYS : STR_DAY);
	notice_lang(s_OperServ, u, OPER_STATS_UPTIME_DHMS,
		days, str, hours, mins, secs);
    } else {
	notice_lang(s_OperServ, u, OPER_STATS_UPTIME_HM_MS,
		    maketime(u->ngi, uptime, MT_DUALUNIT|MT_SECONDS));
    }

    if (extra && stricmp(extra, "ALL") == 0 && is_services_admin(u)) {
	long count, mem;

	get_user_stats(&count, &mem);
	notice_lang(s_OperServ, u, OPER_STATS_ALL_USER_MEM,
			count, (mem+512) / 1024);
	get_channel_stats(&count, &mem);
	notice_lang(s_OperServ, u, OPER_STATS_ALL_CHANNEL_MEM,
			count, (mem+512) / 1024);
	get_server_stats(&count, &mem);
	notice_lang(s_OperServ, u, OPER_STATS_ALL_SERVER_MEM,
			count, (mem+512) / 1024);
	call_callback_2(module, cb_stats_all, u, s_OperServ);

    }
}

/*************************************************************************/

/* Server map display. */

static void map_server(User *u, Server *s, int level);  /* defined below */

static void do_servermap(User *u)
{
    Server *root = get_server("");
    if (!root) {
	module_log("BUG: root server not found for SERVERMAP");
	notice_lang(s_OperServ, u, INTERNAL_ERROR);
	return;
    }
    map_server(u, root, 0);
}


/* Recursively print the server map.  This routine prints the given server
 * at level `level' of indentation (0 = left margin), then prints its
 * children in child->sibling->sibling... order.
 */

static void map_server(User *u, Server *s, int level)
{
    static int need_bar[MAXMAPLEVEL]; /* which levs need a continuation bar? */
    static const char *indentstr = "  +-- ";
    static const char *barstr    = "  |   ";
    static const char *nobarstr  = "      ";

    char buf[BUFSIZE], *ptr;
    int i;

    ptr = buf;
    *ptr = 0;
    i = 0;
    while (i < level-1) {
	ptr += snprintf(ptr, sizeof(buf)-(ptr-buf), "%s",
			need_bar[i] ? barstr : nobarstr);
	i++;
    }
    if (level)
	ptr += snprintf(ptr, sizeof(buf)-(ptr-buf), "%s", indentstr);
    notice(s_OperServ, u->nick, "%s%s%s", buf,
	   s ? (*s->name ? s->name : ServerName) : "...",
	   (s && s->fake) ? "(*)" : "");
    if (s && s->child) {
	if (level+1 >= MAXMAPLEVEL) {
	    map_server(u, NULL, level+1);
	    return;
	}
	for (s = s->child; s; s = s->sibling) {
	    need_bar[level] = (s->sibling != NULL);
	    map_server(u, s, level+1);
	}
    }
}

/*************************************************************************/

/* Channel mode changing (MODE command). */

static void do_os_mode(User *u)
{
    int argc;
    char **argv;
    char *s = strtok_remaining();
    char *chan, *modes;
    Channel *c;

    if (!s) {
	syntax_error(s_OperServ, u, "MODE", OPER_MODE_SYNTAX);
	return;
    }
    chan = s;
    s += strcspn(s, " ");
    if (!*s) {
	syntax_error(s_OperServ, u, "MODE", OPER_MODE_SYNTAX);
	return;
    }
    *s = 0;
    modes = (s+1) + strspn(s+1, " ");
    if (!*modes) {
	syntax_error(s_OperServ, u, "MODE", OPER_MODE_SYNTAX);
	return;
    }
    if (!(c = get_channel(chan))) {
	notice_lang(s_OperServ, u, CHAN_X_NOT_IN_USE, chan);
    } else if (c->bouncy_modes) {
	notice_lang(s_OperServ, u, OPER_BOUNCY_MODES);
	return;
    } else {
	send_cmd(s_OperServ, "MODE %s %s", chan, modes);
	if (WallOSChannel)
	    wallops(s_OperServ, "%s used MODE %s on %s", u->nick, modes, chan);
	*s = ' ';
	argc = split_buf(chan, &argv, 1);
	do_cmode(s_OperServ, argc, argv);
    }
}

/*************************************************************************/

/* Clear all modes from a channel. */

static void do_clearmodes(User *u)
{
    char *s;
    char *chan = strtok(NULL, " ");
    Channel *c;
    int all = 0;

    if (!chan) {
	syntax_error(s_OperServ, u, "CLEARMODES", OPER_CLEARMODES_SYNTAX);
    } else if (!(c = get_channel(chan))) {
	notice_lang(s_OperServ, u, CHAN_X_NOT_IN_USE, chan);
    } else if (c->bouncy_modes) {
	notice_lang(s_OperServ, u, OPER_BOUNCY_MODES);
	return;
    } else {
	s = strtok(NULL, " ");
	if (s) {
	    if (stricmp(s, "ALL") == 0) {
		all = 1;
	    } else {
		syntax_error(s_OperServ,u,"CLEARMODES",OPER_CLEARMODES_SYNTAX);
		return;
	    }
	}
	if (WallOSChannel)
	    wallops(s_OperServ, "%s used CLEARMODES%s on %s",
			u->nick, all ? " ALL" : "", chan);
	if (all) {
	    clear_channel(s_OperServ, c, CLEAR_UMODES, (void *)MODE_ALL);
	    clear_channel(s_OperServ, c, CLEAR_MODES | CLEAR_BANS, NULL);
	    notice_lang(s_OperServ, u, OPER_CLEARMODES_ALL_DONE, chan);
	} else {
	    clear_channel(s_OperServ, c, CLEAR_MODES | CLEAR_BANS, NULL);
	    notice_lang(s_OperServ, u, OPER_CLEARMODES_DONE, chan);
	}
    }
}

/*************************************************************************/

/* Remove all users from a channel. */

static void do_clearchan(User *u)
{
    char *chan = strtok(NULL, " ");
    Channel *c;
    char buf[BUFSIZE];

    if (!chan) {
	syntax_error(s_OperServ, u, "CLEARCHAN", OPER_CLEARCHAN_SYNTAX);
    } else if (!(c = get_channel(chan))) {
	notice_lang(s_OperServ, u, CHAN_X_NOT_IN_USE, chan);
    } else if (c->bouncy_modes) {
	notice_lang(s_OperServ, u, OPER_BOUNCY_MODES);
	return;
    } else {
	if (WallOSChannel)
	    wallops(s_OperServ, "%s used CLEARCHAN on %s", u->nick, chan);
	snprintf(buf, sizeof(buf), "CLEARCHAN by %s", u->nick);
	clear_channel(s_OperServ, c, CLEAR_USERS, buf);
	notice_lang(s_OperServ, u, OPER_CLEARCHAN_DONE, chan);
    }
}

/*************************************************************************/

/* Kick a user from a channel (KICK command). */

static void do_os_kick(User *u)
{
    char *argv[3];
    char *chan, *nick, *s;
    Channel *c;

    chan = strtok(NULL, " ");
    nick = strtok(NULL, " ");
    s = strtok_remaining();
    if (!chan || !nick || !s) {
	syntax_error(s_OperServ, u, "KICK", OPER_KICK_SYNTAX);
	return;
    }
    if (!(c = get_channel(chan))) {
	notice_lang(s_OperServ, u, CHAN_X_NOT_IN_USE, chan);
    } else if (c->bouncy_modes) {
	notice_lang(s_OperServ, u, OPER_BOUNCY_MODES);
	return;
    }
    send_cmd(s_OperServ, "KICK %s %s :%s (%s)", chan, nick, u->nick, s);
    if (WallOSChannel)
	wallops(s_OperServ, "%s used KICK on %s/%s", u->nick, nick, chan);
    argv[0] = chan;
    argv[1] = nick;
    argv[2] = s;
    do_kick(s_OperServ, 3, argv);
}

/*************************************************************************/

/* Services admin list viewing/modification. */

static void do_admin(User *u)
{
    char *cmd, *nick;

    if (!module_nickserv) {
	notice_lang(s_OperServ, u, OPER_ADMIN_NO_NICKSERV);
	return;
    }
    cmd = strtok(NULL, " ");
    if (!cmd)
	cmd = "";

    if (stricmp(cmd, "ADD") == 0) {
	if (!is_services_root(u)) {
	    notice_lang(s_OperServ, u, PERMISSION_DENIED);
	    return;
	}
	nick = strtok(NULL, " ");
	if (nick)
	    privlist_add(u, LIST_ADMIN, nick);
	else
	    syntax_error(s_OperServ, u, "ADMIN", OPER_ADMIN_ADD_SYNTAX);

    } else if (stricmp(cmd, "DEL") == 0) {
	if (!is_services_root(u)) {
	    notice_lang(s_OperServ, u, PERMISSION_DENIED);
	    return;
	}
	nick = strtok(NULL, " ");
	if (nick)
	    privlist_rem(u, LIST_ADMIN, nick);
	else
	    syntax_error(s_OperServ, u, "ADMIN", OPER_ADMIN_DEL_SYNTAX);

    } else if (stricmp(cmd, "LIST") == 0) {
	NickGroupInfo *ngi;
	notice_lang(s_OperServ, u, OPER_ADMIN_LIST_HEADER);
	for (ngi = first_nickgroupinfo(); ngi; ngi = next_nickgroupinfo()) {
	    if (ngi->os_priv >= NP_SERVADMIN)
		notice(s_OperServ, u->nick, "%s", ngi->nicks[ngi->mainnick]);
	}

    } else {
	syntax_error(s_OperServ, u, "ADMIN", OPER_ADMIN_SYNTAX);
    }
}

/*************************************************************************/

/* Services oper list viewing/modification. */

static void do_oper(User *u)
{
    char *cmd, *nick;

    if (!module_nickserv) {
	notice_lang(s_OperServ, u, OPER_OPER_NO_NICKSERV);
	return;
    }
    cmd = strtok(NULL, " ");
    if (!cmd)
	cmd = "";

    if (stricmp(cmd, "ADD") == 0) {
	if (!is_services_admin(u)) {
	    notice_lang(s_OperServ, u, PERMISSION_DENIED);
	    return;
	}
	nick = strtok(NULL, " ");
	if (nick)
	    privlist_add(u, LIST_OPER, nick);
	else
	    syntax_error(s_OperServ, u, "OPER", OPER_OPER_ADD_SYNTAX);

    } else if (stricmp(cmd, "DEL") == 0) {
	if (!is_services_admin(u)) {
	    notice_lang(s_OperServ, u, PERMISSION_DENIED);
	    return;
	}
	nick = strtok(NULL, " ");
	if (nick)
	    privlist_rem(u, LIST_OPER, nick);
	else
	    syntax_error(s_OperServ, u, "OPER", OPER_OPER_DEL_SYNTAX);

    } else if (stricmp(cmd, "LIST") == 0) {
	NickGroupInfo *ngi;
	notice_lang(s_OperServ, u, OPER_OPER_LIST_HEADER);
	for (ngi = first_nickgroupinfo(); ngi; ngi = next_nickgroupinfo()) {
	    if (ngi->os_priv >= NP_SERVOPER && ngi->os_priv < NP_SERVADMIN)
		notice(s_OperServ, u->nick, "%s", ngi->nicks[ngi->mainnick]);
	}

    } else {
	syntax_error(s_OperServ, u, "OPER", OPER_OPER_SYNTAX);
    }
}

/*************************************************************************/

/* Obtain Services root privileges.  We check permissions here instead of
 * letting run_cmd() do it for us so we can send out warnings when a
 * non-admin tries to use the command, and allow anyone to use it when
 * NickServ isn't loaded.
 */

static void do_su(User *u)
{
    char *password = strtok_remaining();
    int res;

    if (module_nickserv && !is_services_admin(u)) {
	wallops(s_OperServ, "\2NOTICE:\2 %s!%s@%s attempted to use SU "
		"command (not Services admin)",
		u->nick, u->username, u->host);
	notice_lang(s_OperServ, u, PERMISSION_DENIED);
	return;
    }

    if (!password) {
	syntax_error(s_OperServ, u, "SU", OPER_SU_SYNTAX);
    } else if (no_supass) {
	notice_lang(s_OperServ, u, OPER_SU_NO_PASSWORD);
    } else if ((res = check_password(password, supass)) < 0) {
	notice_lang(s_OperServ, u, OPER_SU_FAILED);
    } else if (res == 0) {
	module_log("Failed SU by %s!%s@%s", u->nick, u->username, u->host);
	wallops(s_OperServ, "\2NOTICE:\2 Failed SU by %s!%s@%s",
		u->nick, u->username, u->host);
	bad_password(s_OperServ, u, "Services root");
    } else {
	u->flags |= UF_SERVROOT;
	if (WallSU)
	    wallops(s_OperServ,
		    "%s!%s@%s obtained Services super-user privileges",
		    u->nick, u->username, u->host);
	notice_lang(s_OperServ, u, OPER_SU_SUCCEEDED);
    }
}

/*************************************************************************/

/* Set various Services runtime options. */

static void do_set(User *u)
{
    char *option = strtok(NULL, " ");
    char *setting = strtok_remaining();

    if (!option || (!setting && stricmp(option, "SUPASS") != 0)) {
	syntax_error(s_OperServ, u, "SET", OPER_SET_SYNTAX);
	return;
    }

    if (call_callback_3(module, cb_set, u, option, setting) > 0)
	return;

    if (stricmp(option, "IGNORE") == 0) {
	if (stricmp(setting, "on") == 0) {
	    allow_ignore = 1;
	    notice_lang(s_OperServ, u, OPER_SET_IGNORE_ON);
	} else if (stricmp(setting, "off") == 0) {
	    allow_ignore = 0;
	    notice_lang(s_OperServ, u, OPER_SET_IGNORE_OFF);
	} else {
	    notice_lang(s_OperServ, u, OPER_SET_IGNORE_ERROR);
	}

    } else if (stricmp(option, "READONLY") == 0) {
	if (stricmp(setting, "on") == 0) {
	    readonly = 1;
	    log("Read-only mode activated");
	    close_log();
	    notice_lang(s_OperServ, u, OPER_SET_READONLY_ON);
	} else if (stricmp(setting, "off") == 0) {
	    readonly = 0;
	    open_log();
	    log("Read-only mode deactivated");
	    notice_lang(s_OperServ, u, OPER_SET_READONLY_OFF);
	} else {
	    notice_lang(s_OperServ, u, OPER_SET_READONLY_ERROR);
	}

    } else if (stricmp(option, "DEBUG") == 0) {
	if (stricmp(setting, "on") == 0) {
	    debug = 1;
	    log("Debug mode activated");
	    notice_lang(s_OperServ, u, OPER_SET_DEBUG_ON);
	} else if (stricmp(setting, "off") == 0 ||
				(*setting == '0' && atoi(setting) == 0)) {
	    log("Debug mode deactivated");
	    debug = 0;
	    notice_lang(s_OperServ, u, OPER_SET_DEBUG_OFF);
	} else if (isdigit(*setting) && atoi(setting) > 0) {
	    debug = atoi(setting);
	    log("Debug mode activated (level %d)", debug);
	    notice_lang(s_OperServ, u, OPER_SET_DEBUG_LEVEL, debug);
	} else {
	    notice_lang(s_OperServ, u, OPER_SET_DEBUG_ERROR);
	}

    } else if (stricmp(option, "SUPASS") == 0) {
	int len;
	char newpass[PASSMAX];

	if (!is_services_root(u)) {
	    notice_lang(s_OperServ, u, PERMISSION_DENIED);
	    return;
	}
	if (!setting) {
	    no_supass = 1;
	    put_operserv_data(OSDATA_SUPASS, NULL);
	    notice_lang(s_OperServ, u, OPER_SET_SUPASS_NONE);
	    return;
	}
	len = strlen(setting);
	if (len >= PASSMAX) {
	    memset(setting+PASSMAX-1, 0, len-(PASSMAX-1));
	    len = PASSMAX-1;
	    notice_lang(s_OperServ, u, PASSWORD_TRUNCATED, len);
	}
	if (encrypt(setting, len, newpass, PASSMAX) < 0) {
	    notice_lang(s_OperServ, u, OPER_SET_SUPASS_FAILED);
	} else {
	    no_supass = 0;
	    memcpy(supass, newpass, PASSMAX);
	    put_operserv_data(OSDATA_SUPASS, supass);
	    notice_lang(s_OperServ, u, OPER_SET_SUPASS_OK);
	}

    } else {
	notice_lang(s_OperServ, u, OPER_SET_UNKNOWN_OPTION, option);
    }
}

/*************************************************************************/

static void do_jupe(User *u)
{
    char *jserver = strtok(NULL, " ");
    char *reason = strtok_remaining();
    char buf[BUFSIZE];
    Server *server;

    if (!jserver) {
	syntax_error(s_OperServ, u, "JUPE", OPER_JUPE_SYNTAX);
    } else if (!strchr(jserver, '.')) {
	notice_lang(s_OperServ, u, OPER_JUPE_INVALID_NAME);
    } else if ((server = get_server(jserver)) != NULL && server->fake) {
	notice_lang(s_OperServ, u, OPER_JUPE_ALREADY_JUPED, jserver);
    } else {
	wallops(s_OperServ, "\2Juping\2 %s by request of \2%s\2.",
		jserver, u->nick);
	if (reason)
	    snprintf(buf, sizeof(buf), "%s (%s)", reason, u->nick);
	else
	    snprintf(buf, sizeof(buf), "Jupitered by %s", u->nick);
	send_server_remote(jserver, buf);
	do_server("", -1, &jserver);
    }
}

/*************************************************************************/

static void do_raw(User *u)
{
    char *text = strtok_remaining();

    if (!text)
	syntax_error(s_OperServ, u, "RAW", OPER_RAW_SYNTAX);
    else
	send_cmd(NULL, "%s", text);
}

/*************************************************************************/

static void do_update(User *u)
{
    notice_lang(s_OperServ, u, OPER_UPDATING);
    save_data = 1;
}

/*************************************************************************/

static void do_os_quit(User *u)
{
    quitmsg = malloc(28 + strlen(u->nick));
    if (!quitmsg)
	quitmsg = "QUIT command received, but out of memory!";
    else
	sprintf(quitmsg, "QUIT command received from %s", u->nick);
    quitting = 1;
}

/*************************************************************************/

static void do_shutdown(User *u)
{
    quitmsg = malloc(32 + strlen(u->nick));
    if (!quitmsg)
	quitmsg = "SHUTDOWN command received, but out of memory!";
    else
	sprintf(quitmsg, "SHUTDOWN command received from %s", u->nick);
    save_data = 1;
    delayed_quit = 1;
}

/*************************************************************************/

static void do_restart(User *u)
{
    quitmsg = malloc(31 + strlen(u->nick));
    if (!quitmsg)
	quitmsg = "RESTART command received, but out of memory!";
    else
	sprintf(quitmsg, "RESTART command received from %s", u->nick);
    save_data = 1;
    delayed_quit = 2;
}

/*************************************************************************/

static void do_rehash(User *u)
{
    /* Don't let ourselves be unloaded */
    modules_allow_use_self++;
    use_module(module);

    notice_lang(s_OperServ, u, OPER_REHASHING);
    wallops(NULL, "Rehashing configuration files (REHASH from %s)", u->nick);
    if (reconfigure())
	notice_lang(s_OperServ, u, OPER_REHASHED);
    else
	notice_lang(s_OperServ, u, OPER_REHASH_ERROR);

    /* Restore normal module behavior */
    unuse_module(module);
    modules_allow_use_self--;
}

/*************************************************************************/

static void do_listignore(User *u)
{
    int sent_header = 0;
    IgnoreData *id;

    for (id = first_ignore(); id; id = next_ignore()) {
	if (!sent_header) {
	    notice_lang(s_OperServ, u, OPER_IGNORE_LIST);
	    sent_header = 1;
	}
	notice(s_OperServ, u->nick, "%ld %s", (long)id->time, id->who);
    }
    if (!sent_header)
	notice_lang(s_OperServ, u, OPER_IGNORE_LIST_EMPTY);
}

/*************************************************************************/

/* Kill all users matching a certain host.  The host is obtained from the
 * supplied nick.  The raw hostmask is not supplied with the command in an
 * effort to prevent abuse and mistakes from being made--which might cause
 * *.com to be killed. It also makes it very quick and simple to use--which
 * is usually what you want when someone starts loading numerous clones.  In
 * addition to killing the clones, we add a temporary autokill to prevent
 * them from immediately reconnecting.
 * Syntax: KILLCLONES nick
 * -TheShadow (29 Mar 1999)
 *
 * Added KillClonesAutokill support  --AC
 */

static void do_killclones(User *u)
{
    char *clonenick = strtok(NULL, " ");
    User *cloneuser;
    typeof(create_akill) *p_create_akill =
	module_akill ? get_module_symbol(module_akill,"create_akill") : NULL;

    if (!clonenick) {
	notice_lang(s_OperServ, u, OPER_KILLCLONES_SYNTAX);

    } else if (!(cloneuser = get_user(clonenick))) {
	notice_lang(s_OperServ, u, OPER_KILLCLONES_UNKNOWN_NICK, clonenick);

    } else {
	char clonemask[BUFSIZE];
	User *user;
	int count = 0;

	snprintf(clonemask, sizeof(clonemask), "*!*@%s", cloneuser->host);
	for (user = first_user(); user; user = next_user()) {
	    if (match_usermask(clonemask, user) != 0) {
		char killreason[32];
		count++;
		snprintf(killreason, sizeof(killreason),
			 "Cloning [%d]", count);
		kill_user(NULL, user->nick, killreason);
	    }
	}
	module_log("KILLCLONES: %d clone(s) matching %s killed.",
		   count, clonemask);

	if (KillClonesAutokill && p_create_akill) {
	    const char akillreason[] = "Temporary KILLCLONES akill.";
	    p_create_akill(clonemask+2, akillreason, u->nick,
			   time(NULL) + KillClonesAutokillExpire);
	    wallops(s_OperServ, getstring(NULL,OPER_KILLCLONES_KILLED_AKILL),
		    u->nick, clonemask, count, clonemask+2);
	} else {
	    wallops(s_OperServ, getstring(NULL,OPER_KILLCLONES_KILLED),
		    u->nick, clonemask, count);
	}

    }
}

/*************************************************************************/

#ifdef DEBUG_COMMANDS

/* All commands from here down are enabled only when DEBUG_COMMANDS is
 * also enabled. */

/*************************************************************************/

/* Send the current list of servers to the given user. */

void send_server_list(User *user)
{
    Server *server;

    for (server = first_server(); server; server = next_server()) {
	notice(s_OperServ, user->nick, "%s fake:%d hub:%s child:%s sibling:%s",
	       server->name, server->fake,
	       server->hub ? server->hub->name : "-",
	       server->child ? server->child->name : "-",
	       server->sibling ? server->sibling->name : "-");

    }
}

/*************************************************************************/

/* Send the current list of channels to the named user. */

static void send_channel_list(User *user)
{
    Channel *c;
    char buf[BUFSIZE], *end;
    struct c_userlist *u;
    const char *source = user->nick;

    for (c = first_channel(); c; c = next_channel()) {
	notice(s_OperServ, source, "%s %ld +%s %d %s :%s",
	       c->name, (long)c->creation_time,
	       mode_flags_to_string(c->mode, MODE_CHANNEL),
	       c->limit, c->key ? c->key : "-",
	       c->topic ? c->topic : "");
	end = buf;
	end += snprintf(end, sizeof(buf)-(end-buf), "%s", c->name);
	LIST_FOREACH (u, c->users) {
	    end += snprintf(end, sizeof(buf)-(end-buf), " +%s/%s",
			    mode_flags_to_string(u->mode,MODE_CHANUSER),
			    u->user->nick);
	}
	notice(s_OperServ, source, buf);
    }
}

/*************************************************************************/

/* Send list of users on a single channel, taken from strtok(). */

static void send_channel_users(User *user)
{
    char *chan = strtok(NULL, " ");
    Channel *c = chan ? get_channel(chan) : NULL;
    struct c_userlist *u;
    const char *source = user->nick;

    if (!c) {
	notice(s_OperServ, source, "Channel %s not found!",
		chan ? chan : "(null)");
	return;
    }
    notice(s_OperServ, source, "Channel %s users:", chan);
    LIST_FOREACH (u, c->users)
	notice(s_OperServ, source, "%x/%s", u->mode, u->user->nick);
}

/*************************************************************************/

/* Send the current list of users to the given user. */

static void send_user_list(User *user)
{
    User *u;
    const char *source = user->nick;

    for (u = first_user(); u; u = next_user()) {
	char buf[5];
	if (u->ni)
	    snprintf(buf, sizeof(buf), "%04X", u->ni->status & 0xFFFF);
	else
	    strcpy(buf, "-");
	notice(s_OperServ, source, "%s!%s@%s %s +%s %ld %u %s %s :%s",
	       u->nick, u->username, u->host, u->fakehost ? u->fakehost : "-",
	       mode_flags_to_string(u->mode, MODE_USER), (long)u->signon,
	       u->servicestamp, u->server->name, buf, u->realname);
    }
}

/*************************************************************************/

/* Send information about a single user to the given user.  Target nick is
 * taken from strtok(). */

static void send_user_info(User *user)
{
    char *nick = strtok(NULL, " ");
    User *u = nick ? get_user(nick) : NULL;
    char buf[BUFSIZE], *s;
    struct u_chanlist *c;
    struct u_chaninfolist *ci;
    const char *source = user->nick;

    if (!u) {
	notice(s_OperServ, source, "User %s not found!",
		nick ? nick : "(null)");
	return;
    }
    if (u->ni)
	snprintf(buf, sizeof(buf), "%04X", u->ni->status & 0xFFFF);
    else
	strcpy(buf, "-");
    notice(s_OperServ, source, "%s!%s@%s %s +%s %ld %u %s %s :%s",
	   u->nick, u->username, u->host, u->fakehost ? u->fakehost : "-",
	   mode_flags_to_string(u->mode, MODE_USER), (long)u->signon,
	   u->servicestamp, u->server->name, buf, u->realname);
    buf[0] = 0;
    s = buf;
    LIST_FOREACH (c, u->chans)
	s += snprintf(s, sizeof(buf)-(s-buf), " %s", c->chan->name);
    notice(s_OperServ, source, "%s%s", u->nick, buf);
    buf[0] = 0;
    s = buf;
    LIST_FOREACH (ci, u->id_chans)
	s += snprintf(s, sizeof(buf)-(s-buf), " %s", ci->chan);
    notice(s_OperServ, source, "%s%s", u->nick, buf);
}

/*************************************************************************/

/* Perform a wildcard match and report the result (either 0 or 1) to the
 * given user. */

static void do_matchwild(User *u)
{
    char *pat = strtok(NULL, " ");
    char *str = strtok(NULL, " ");
    if (pat && str)
	notice(s_OperServ, u->nick, "%d", match_wild(pat, str));
    else
	notice(s_OperServ, u->nick, "Syntax error.");
}

/*************************************************************************/

/* Call set_cmode() with ServerName and parameters taken from strtok() (up
 * to SETCMODE_NPARAMS). */

#define SETCMODE_NPARAMS	10

static void do_setcmode(User *u)
{
    char *channame, *params[SETCMODE_NPARAMS];
    Channel *channel;
    int i;

    channame = strtok(NULL, " ");
    for (i = 0; i < SETCMODE_NPARAMS; i++)
	params[i] = strtok(NULL, " ");
    if (!channame) {
	notice(s_OperServ, u->nick, "SETCMODE: not enough parameters");
	return;
    }
    if (strcmp(channame, "-") == 0) {
	channel = NULL;
    } else {
	channel = get_channel(channame);
	if (!channel) {
	    notice(s_OperServ, u->nick, "SETCMODE: channel not found (%s)",
		   channame);
	    return;
	}
    }
    set_cmode(channel ? ServerName : NULL, channel,
	      params[0], params[1], params[2], params[3], params[4],
	      params[5], params[6], params[7], params[8], params[9]);
    notice(s_OperServ, u->nick, "SETCMODE: done");
}

/*************************************************************************/

#endif	/* DEBUG_COMMANDS */

/*************************************************************************/
/*********** MaskData expiration check (for database modules) ************/
/*************************************************************************/

/* Check for expiration of a MaskData; delete it and return 1 if expired,
 * and return 0 if not expired.
 */

EXPORT_FUNC(check_expire_maskdata)
int check_expire_maskdata(uint8 type, MaskData *md)
{
    if (md->expires && md->expires <= time(NULL)) {
	call_callback_2(module, cb_expire_md, type, md);
	del_maskdata(type, md);
	return 1;
    }
    return 0;
}

/*************************************************************************/
/***************************** Module stuff ******************************/
/*************************************************************************/

const int32 module_version = MODULE_VERSION_CODE;

ConfigDirective module_config[] = {
    { "AllowRaw",         { { CD_SET, 0, &AllowRaw } } },
    { "GlobalName",       { { CD_STRING, CF_DIRREQ, &s_GlobalNoticer },
                            { CD_STRING, 0, &desc_GlobalNoticer } } },
    { "KillClonesAutokill",{{ CD_SET, 0, &KillClonesAutokill },
			    { CD_TIME, 0, &KillClonesAutokillExpire } } },
    { "OperServDB",       { { CD_STRING, CF_DIRREQ, &OperDBName } } },
    { "OperServName",     { { CD_STRING, CF_DIRREQ, &s_OperServ },
                            { CD_STRING, 0, &desc_OperServ } } },
    { "ServicesRoot",     { { CD_STRING, CF_DIRREQ, &ServicesRoot } } },
    { "WallBadOS",        { { CD_SET, 0, &WallBadOS } } },
    { "WallOper",         { { CD_SET, 0, &WallOper } } },
    { "WallOSChannel",    { { CD_SET, 0, &WallOSChannel } } },
    { "WallSU",           { { CD_SET, 0, &WallSU } } },
    { NULL }
};

/* For enabling/disabling RAW command (AllowRaw) */
static Command *cmd_RAW = NULL;

/*************************************************************************/

static int do_load_module(Module *mod, const char *modname)
{
    if (strcmp(modname, "operserv/akill") == 0) {
	module_akill = mod;

    } else if (strcmp(modname, "nickserv/main") == 0) {
	char **p_s_NickServ;
	Command *cmd;

	module_nickserv = mod;
	p_s_NickServ = get_module_symbol(mod, "s_NickServ");
	if (p_s_NickServ) {
	    cmd = lookup_cmd(module, "ADMIN");
	    if (cmd)
		cmd->help_param1 = *p_s_NickServ;
	    cmd = lookup_cmd(module, "OPER");
	    if (cmd)
		cmd->help_param1 = *p_s_NickServ;
	} else {
	    module_log("Unable to resolve NickServ symbol `s_NickServ'");
	}
	if (!add_callback(mod, "REGISTER/LINK check", do_reglink_check))
	    module_log("Unable to register NickServ REGISTER/LINK check"
		       " callback");
    }

    return 0;
}

/*************************************************************************/

static int do_unload_module(Module *mod)
{
    if (mod == module_akill) {
	module_akill = NULL;
    } else if (mod == module_nickserv) {
	Command *cmd;
	cmd = lookup_cmd(module, "ADMIN");
	if (cmd)
	    cmd->help_param1 = "NickServ";
	cmd = lookup_cmd(module, "OPER");
	if (cmd)
	    cmd->help_param1 = "NickServ";
	remove_callback(module_nickserv, "REGISTER/LINK check",
			do_reglink_check);
	module_nickserv = NULL;
    }
    return 0;
}

/*************************************************************************/

static int do_reconfigure(int after_configure)
{
    static char old_s_OperServ[NICKMAX];
    static char *old_desc_OperServ = NULL;
    static char *old_OperDBName    = NULL;

    if (!after_configure) {
	/* Before reconfiguration: save old values. */
	free(old_desc_OperServ);  /* these might be non-NULL if a */
	free(old_OperDBName);     /* previous reconfigure failed  */
	strscpy(old_s_OperServ, s_OperServ, NICKMAX);
	old_desc_OperServ = strdup(desc_OperServ);
	old_OperDBName    = strdup(OperDBName);
    } else {
	Command *cmd;
	/* After reconfiguration: handle value changes. */
	if (strcmp(old_s_OperServ, s_OperServ) != 0)
	    send_nickchange(old_s_OperServ, s_OperServ);
	if (!old_desc_OperServ || strcmp(old_desc_OperServ,desc_OperServ) != 0)
	    send_namechange(s_OperServ, desc_OperServ);
	if (!old_OperDBName || strcmp(old_OperDBName, OperDBName) != 0) {
	    module_log("reconfigure: new database name will only take"
		       " effect after restart");
	    /* Restore the old database name */
	    free(OperDBName);
	    OperDBName = old_OperDBName;
	    /* Make sure the old name isn't freed below */
	    old_OperDBName = NULL;
	}
	/* Free and clear old values */
	free(old_desc_OperServ);
	free(old_OperDBName);
	old_desc_OperServ = NULL;
	old_OperDBName = NULL;
	/* Activate/deactivate commands */
	if (cmd_RAW) {
	    if (AllowRaw)
		cmd_RAW->name = "RAW";
	    else
		cmd_RAW->name = "";
	}
	/* Update command help parameters */
	if (module_nickserv) {
	    char **p_s_NickServ;
	    p_s_NickServ = get_module_symbol(module_nickserv, "s_NickServ");
	    if (p_s_NickServ) {
		cmd = lookup_cmd(module, "ADMIN");
		if (cmd)
		    cmd->help_param1 = *p_s_NickServ;
		cmd = lookup_cmd(module, "OPER");
		if (cmd)
		    cmd->help_param1 = *p_s_NickServ;
	    } else {
		module_log("reconfigure: Unable to resolve NickServ symbol:"
			   " s_NickServ");
	    }
	}
	cmd = lookup_cmd(module, "GLOBAL");
	if (cmd)
	    cmd->help_param1 = s_GlobalNoticer;
    }  /* if (!after_configure) */
    return 0;
}

/*************************************************************************/

int init_module(Module *module_)
{
    Command *cmd;
    int i;
    char *supass_ret;

    module = module_;

    if (!new_commandlist(module) || !register_commands(module, cmds)) {
	module_log("Unable to register commands");
	exit_module(0);
	return 0;
    }

    cb_command   = register_callback(module, "command");
    cb_expire_md = register_callback(module, "expire maskdata");
    cb_help      = register_callback(module, "HELP");
    cb_help_cmds = register_callback(module, "HELP COMMANDS");
    cb_set       = register_callback(module, "SET");
    cb_stats     = register_callback(module, "STATS");
    cb_stats_all = register_callback(module, "STATS ALL");
    if (cb_command < 0 || cb_help < 0 || cb_help_cmds < 0 || cb_set < 0
     || cb_stats < 0 || cb_stats_all < 0
    ) {
	module_log("Unable to register callbacks");
	exit_module(0);
	return 0;
    }

    if (!add_callback(NULL, "load module", do_load_module)
     || !add_callback(NULL, "unload module", do_unload_module)
     || !add_callback(NULL, "reconfigure", do_reconfigure)
     || !add_callback(NULL, "save data", do_save_data)
     || !add_callback(NULL, "introduce_user", introduce_operserv)
     || !add_callback(NULL, "m_privmsg", operserv)
     || !add_callback(NULL, "m_whois", operserv_whois)
     || (WallOper && !add_callback(NULL, "user MODE", wall_oper_callback))
    ) {
	module_log("Unable to add callbacks");
	exit_module(0);
	return 0;
    }

    if ((i = -1, !open_operserv_db(OperDBName))
     || (i = OSDATA_MAXUSERCNT,  !get_operserv_data(i, &maxusercnt))
     || (i = OSDATA_MAXUSERTIME, !get_operserv_data(i, &maxusertime))
     || (i = OSDATA_SUPASS,      !get_operserv_data(i, &supass_ret))
    ) {
	module_log("Unable to read from database! (code %d)", i);
	exit_module(0);
	return 0;
    }
    if (supass_ret) {
	no_supass = 0;
	memcpy(supass, supass_ret, PASSMAX);
    } else {
	no_supass = 1;
    }
    db_opened = 1;

    cmd_RAW = lookup_cmd(module, "RAW");
    if (cmd_RAW && !AllowRaw)
	cmd_RAW->name = "";
    cmd = lookup_cmd(module, "GLOBAL");
    if (cmd)
	cmd->help_param1 = s_GlobalNoticer;

    return 1;
}

/*************************************************************************/

int exit_module(int shutdown_unused)
{
#ifdef CLEAN_COMPILE
    shutdown_unused = shutdown_unused;
#endif

    if (cmd_RAW)
	cmd_RAW->name = "RAW";

    if (db_opened)
	close_operserv_db(OperDBName);

    if (module_nickserv)
	do_unload_module(module_nickserv);
    if (module_akill)
	do_unload_module(module_akill);

    remove_callback(NULL, "user MODE", wall_oper_callback);
    remove_callback(NULL, "m_whois", operserv_whois);
    remove_callback(NULL, "m_privmsg", operserv);
    remove_callback(NULL, "introduce_user", introduce_operserv);
    remove_callback(NULL, "save data", do_save_data);
    remove_callback(NULL, "reconfigure", do_reconfigure);
    remove_callback(NULL, "unload module", do_unload_module);
    remove_callback(NULL, "load module", do_load_module);

    unregister_callback(module, cb_stats_all);
    unregister_callback(module, cb_stats);
    unregister_callback(module, cb_set);
    unregister_callback(module, cb_help_cmds);
    unregister_callback(module, cb_help);
    unregister_callback(module, cb_command);

    unregister_commands(module, cmds);
    del_commandlist(module);

    return 1;
}

/*************************************************************************/
