/* Main ChanServ module.
 *
 * IRC Services is copyright (c) 1996-2007 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 "encrypt.h"
#include "modules/nickserv/nickserv.h"
#include "modules/operserv/operserv.h"

#include "chanserv.h"
#include "cs-local.h"

/*************************************************************************/
/************************** Declaration section **************************/
/*************************************************************************/

static Module *module;
static Module *module_nickserv;

static int cb_clear     = -1;
static int cb_command   = -1;
static int cb_help      = -1;
static int cb_help_cmds = -1;
static int cb_invite    = -1;
static int cb_unban     = -1;

static int db_opened = 0;

       char *s_ChanServ;
static char *desc_ChanServ;
static char *ChanDBName;
EXPORT_VAR(char *,s_ChanServ)

static int    CSEnableRegister;
       int    CSRegisteredOnly;
       int32  CSMaxReg;
       int32  CSDefFlags;
       time_t CSExpire;
       int    CSShowPassword;
       int32  CSAccessMax;
       int32  CSAutokickMax;
       char * CSAutokickReason;
       time_t CSInhabit;
       time_t CSRestrictDelay;
       int    CSListOpersOnly;
       int32  CSListMax;
       time_t CSSuspendExpire;
       time_t CSSuspendGrace;
       int    CSForbidShortChannel;
EXPORT_VAR(int32,CSMaxReg)

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

/* Channel option list. */

#define CHANOPT(x) \
    { #x, CI_##x, CHAN_INFO_OPT_##x, \
      CHAN_SET_##x##_ON, CHAN_SET_##x##_OFF, CHAN_SET_##x##_SYNTAX }
ChanOpt chanopts[] = {
    CHANOPT(KEEPTOPIC),
    CHANOPT(TOPICLOCK),
    CHANOPT(PRIVATE),
    CHANOPT(SECUREOPS),
    CHANOPT(LEAVEOPS),
    CHANOPT(RESTRICTED),
    CHANOPT(SECURE),
    CHANOPT(OPNOTICE),
    CHANOPT(ENFORCE),
    { "NOEXPIRE", CI_NOEXPIRE, -1, CHAN_SET_NOEXPIRE_ON,
	  CHAN_SET_NOEXPIRE_OFF, CHAN_SET_NOEXPIRE_SYNTAX },
    { NULL }
};
#undef CHANOPT

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

/* Local functions. */

static void do_help(User *u);
static void do_register(User *u);
static void do_identify(User *u);
static void do_drop(User *u);
static void do_info(User *u);
static void do_list(User *u);
static void do_op(User *u);
static void do_deop(User *u);
static void do_voice(User *u);
static void do_devoice(User *u);
static void do_halfop(User *u);
static void do_dehalfop(User *u);
static void do_protect(User *u);
static void do_deprotect(User *u);
static void do_invite(User *u);
static void do_unban(User *u);
static void do_cskick(User *u);
static void do_cstopic(User *u);
static void do_clear(User *u);
static void do_getpass(User *u);
static void do_forbid(User *u);
static void do_suspend(User *u);
static void do_unsuspend(User *u);
static void do_status(User *u);

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

/* Command list. */

static Command cmds[] = {
    { "HELP",     do_help,     NULL,  -1,                       -1,-1 },
    { "REGISTER", do_register, NULL,  CHAN_HELP_REGISTER,       -1,-1 },
    { "IDENTIFY", do_identify, NULL,  CHAN_HELP_IDENTIFY,       -1,-1 },
    { "DROP",     do_drop,     NULL,  -1,
		CHAN_HELP_DROP, CHAN_OPER_HELP_DROP },
    { "SET",      do_set,      NULL,  CHAN_HELP_SET, -1, CHAN_OPER_HELP_SET },
    { "SET FOUNDER",    NULL,  NULL,  CHAN_HELP_SET_FOUNDER,    -1,-1 },
    { "SET SUCCESSOR",  NULL,  NULL,  CHAN_HELP_SET_SUCCESSOR,  -1,-1 },
    { "SET PASSWORD",   NULL,  NULL,  CHAN_HELP_SET_PASSWORD,   -1,-1 },
    { "SET DESC",       NULL,  NULL,  CHAN_HELP_SET_DESC,       -1,-1 },
    { "SET URL",        NULL,  NULL,  CHAN_HELP_SET_URL,        -1,-1 },
    { "SET EMAIL",      NULL,  NULL,  CHAN_HELP_SET_EMAIL,      -1,-1 },
    { "SET ENTRYMSG",   NULL,  NULL,  CHAN_HELP_SET_ENTRYMSG,   -1,-1 },
    { "SET KEEPTOPIC",  NULL,  NULL,  CHAN_HELP_SET_KEEPTOPIC,  -1,-1 },
    { "SET TOPICLOCK",  NULL,  NULL,  CHAN_HELP_SET_TOPICLOCK,  -1,-1 },
    { "SET MLOCK",      NULL,  NULL,  CHAN_HELP_SET_MLOCK,      -1,-1 },
    { "SET HIDE",       NULL,  NULL,  CHAN_HELP_SET_HIDE,       -1,-1 },
    { "SET PRIVATE",    NULL,  NULL,  CHAN_HELP_SET_PRIVATE,	-1,-1 },
    { "SET RESTRICTED", NULL,  NULL,  CHAN_HELP_SET_RESTRICTED, -1,-1 },
    { "SET SECURE",     NULL,  NULL,  CHAN_HELP_SET_SECURE,     -1,-1 },
    { "SET SECUREOPS",  NULL,  NULL,  CHAN_HELP_SET_SECUREOPS,  -1,-1 },
    { "SET LEAVEOPS",   NULL,  NULL,  CHAN_HELP_SET_LEAVEOPS,   -1,-1 },
    { "SET OPNOTICE",   NULL,  NULL,  CHAN_HELP_SET_OPNOTICE,   -1,-1 },
    { "SET ENFORCE",    NULL,  NULL,  CHAN_HELP_SET_ENFORCE,    -1,-1 },
    { "SET NOEXPIRE",   NULL,  NULL,  -1, -1, CHAN_OPER_HELP_SET_NOEXPIRE },
    { "UNSET",    do_unset,    NULL,  CHAN_HELP_UNSET,
		-1, CHAN_OPER_HELP_UNSET },
    { "INFO",     do_info,     NULL,  CHAN_HELP_INFO,
		-1, CHAN_OPER_HELP_INFO },
    { "LIST",     do_list,     NULL,  -1,
		CHAN_HELP_LIST, CHAN_OPER_HELP_LIST },
    { "AKICK",    do_akick,    NULL,  CHAN_HELP_AKICK,          -1,-1,
		(void *)ACCLEV_SOP },
    { "OP",       do_op,       NULL,  CHAN_HELP_OP,             -1,-1,
		(void *)ACCLEV_AOP },
    { "DEOP",     do_deop,     NULL,  CHAN_HELP_DEOP,           -1,-1,
		(void *)ACCLEV_AOP },
    { "VOICE",    do_voice,    NULL,  CHAN_HELP_VOICE,          -1,-1,
		(void *)ACCLEV_VOP },
    { "DEVOICE",  do_devoice,  NULL,  CHAN_HELP_DEVOICE,        -1,-1,
		(void *)ACCLEV_VOP },
    { "INVITE",   do_invite,   NULL,  CHAN_HELP_INVITE,         -1,-1,
		(void *)ACCLEV_AOP },
    { "UNBAN",    do_unban,    NULL,  CHAN_HELP_UNBAN,          -1,-1,
		(void *)ACCLEV_AOP },
    { "KICK",     do_cskick,   NULL,  CHAN_HELP_KICK,           -1,-1,
		(void *)ACCLEV_AOP },
    { "TOPIC",    do_cstopic,  NULL,  CHAN_HELP_TOPIC,          -1,-1,
		(void *)ACCLEV_AOP },
    { "CLEAR",    do_clear,    NULL,  CHAN_HELP_CLEAR,          -1,-1,
		(void *)ACCLEV_SOP },
    { "STATUS",   do_status,   NULL,  CHAN_HELP_STATUS,         -1,-1,
		(void *)ACCLEV_SOP },
    { "GETPASS",  do_getpass,  is_services_admin,  -1,
		-1, CHAN_OPER_HELP_GETPASS },
    { "FORBID",   do_forbid,   is_services_admin,  -1,
		-1, CHAN_OPER_HELP_FORBID },
    { "SUSPEND",  do_suspend,  is_services_admin,  -1,
		-1, CHAN_OPER_HELP_SUSPEND },
    { "UNSUSPEND",do_unsuspend,is_services_admin,  -1,
		-1, CHAN_OPER_HELP_UNSUSPEND },
    { NULL }
};

static Command cmds_halfop[] = {
    { "HALFOP",   do_halfop,   NULL,  CHAN_HELP_HALFOP,         -1,-1,
		(void *)ACCLEV_AOP },
    { "DEHALFOP", do_dehalfop, NULL,  CHAN_HELP_DEHALFOP,       -1,-1,
		(void *)ACCLEV_AOP },
    { NULL }
};

static Command cmds_chanprot[] = {
    { "PROTECT",  do_protect,  NULL,  CHAN_HELP_PROTECT,        -1,-1,
		(void *)ACCLEV_AOP },
    { "DEPROTECT",do_deprotect,NULL,  CHAN_HELP_DEPROTECT,      -1,-1,
		(void *)ACCLEV_AOP },
    { NULL }
};

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

/* Introduce the ChanServ pseudoclient. */

static int introduce_chanserv(const char *nick)
{
    if (!nick || irc_stricmp(nick, s_ChanServ) == 0) {
	char modebuf[BUFSIZE];
	snprintf(modebuf, sizeof(modebuf), "o%s", pseudoclient_modes);
	send_nick(s_ChanServ, ServiceUser, ServiceHost, ServerName,
		  desc_ChanServ, modebuf);
	return nick ? 1 : 0;
    }
    return 0;
}

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

/* Main ChanServ routine. */

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

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

    if (!u) {
	module_log("user record for %s not found", source);
	notice(s_ChanServ, source, getstring(NULL, INTERNAL_ERROR));
	return 1;
    }

    cmd = strtok(buf, " ");

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

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

/* Return a /WHOIS response for ChanServ. */

static int chanserv_whois(const char *source, char *who, char *extra)
{
    if (irc_stricmp(who, s_ChanServ) != 0)
	return 0;
    send_cmd(ServerName, "311 %s %s %s %s * :%s", source, who,
	     ServiceUser, ServiceHost, desc_ChanServ);
    send_cmd(ServerName, "312 %s %s %s :%s", source, who,
	     ServerName, ServerDesc);
    send_cmd(ServerName, "318 %s %s End of /WHOIS response.", source, who);
    return 1;
}

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

/* Save channel database. */

static int do_save_data()
{
    sync_channel_db(ChanDBName);
    return 0;
}

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

/* Callback for newly-created channels. */

static int do_channel_create(Channel *c, User *u, int32 modes)
{
    /* Store ChannelInfo pointer in channel record */
    c->ci = get_channelinfo(c->name);
    if (c->ci) {
	/* Store return pointer in ChannelInfo record */
	c->ci->c = c;
    }

    /* Restore locked modes and saved topic */
    /* Note: these should be outside the c->ci test to ensure any spurious
     *       +r modes are cleared */
    check_modes(c);
    restore_topic(c);

    return 0;
}

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

/* Callback for users trying to join channels. */

static int do_channel_join_check(const char *channel, User *user)
{
    return check_kick(user, channel);
}

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

/* Callback for users joining channels. */

static int do_channel_join(Channel *c, struct c_userlist *u)
{
    User *user = u->user;
    ChannelInfo *ci = c->ci;

    check_chan_user_modes(NULL, u, c, -1);
    if (ci && ci->entry_message)
	notice(s_ChanServ, user->nick, "(%s) %s", ci->name, ci->entry_message);
    return 0;
}

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

/* Callback for users leaving channels.  Update the channel's last used
 * time if the user was an auto-op user.
 */

static int do_channel_part(Channel *c, User *u, const char *reason)
{
    if (c->ci && check_access(u, c->ci, CA_AUTOOP)) {
	c->ci->last_used = time(NULL);
	put_channelinfo(c->ci);
    }
    return 0;
}

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

/* Callback for channels being deleted. */

static int do_channel_delete(Channel *c)
{
    if (c->ci)
	c->ci->c = NULL;
    return 0;
}

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

/* Callback for channel mode changes. */

static int do_channel_mode_change(const char *source_unused, Channel *c)
{
    check_modes(c);
    return 0;
}

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

/* Callback for channel user mode changes. */

static int do_channel_umode_change(const char *source, Channel *c,
				   struct c_userlist *u, int32 oldmodes)
{
    if (!(u->mode & CUMODE_o))
	u->flags &= ~CUFLAG_DEOPPED;
    check_chan_user_modes(source, u, c, oldmodes);
    return 0;
}

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

/* Callback for channel topic changes. */

static int do_channel_topic(Channel *c, const char *topic, const char *setter,
			    time_t topic_time)
{
    ChannelInfo *ci = c->ci;

    if (check_topiclock(c, topic_time))
	return 1;
    record_topic(ci, topic, setter, topic_time);
    return 0;
}

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

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

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

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

/* Callback for users who have identified to their nicks: give them modes
 * as if they had just joined the channel.
 */

static int do_nick_identified(User *u, int old_authstat)
{
    struct u_chanlist *uc;	/* Node in list of channels the user is in */
    struct c_userlist *cu;	/* Node in list of users in a channel */

    LIST_FOREACH (uc, u->chans) {
	LIST_SEARCH_SCALAR(uc->chan->users, user, u, cu);
	if (!cu) {
	    module_log("do_nick_identified(): BUG: user record not found in"
		       " channel %s for user %s", uc->chan->name, u->nick);
	    continue;
	}
	/* Use an empty source to force a mode recheck */
	check_chan_user_modes("", cu, uc->chan, -1);
    }
    return 0;
}

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

/* Remove a (deleted or expired) nickname group from all channel lists. */

static int do_nickgroup_delete(const NickGroupInfo *ngi, const char *oldnick)
{
    int i;
    int id = ngi->id;
    ChannelInfo *ci;

    for (ci = first_channelinfo(); ci; ci = next_channelinfo()) {
	int modified = 0;
	if (ci->founder == id) {
	    int was_suspended = (ci->suspendinfo != NULL);
	    char name_save[CHANMAX];
	    if (was_suspended)
		strscpy(name_save, ci->name, CHANMAX);
	    if (ci->successor) {
		NickGroupInfo *ngi2 = get_ngi_id(ci->successor);
		if (!ngi2) {
		    module_log("Unable to access successor group %u for"
			       " deleted channel %s, deleting channel",
			       ci->successor, ci->name);
		    goto delete;
		} else if (check_channel_limit(ngi2, NULL) < 0) {
		    module_log("Transferring foundership of %s from deleted"
			       " nick %s to successor %s", ci->name,
			       oldnick, ngi_mainnick(ngi2));
		    uncount_chan(ci);
		    ci->founder = ci->successor;
		    ci->successor = 0;
		    count_chan(ci);
		} else {
		    module_log("Successor (%s) of %s owns too many channels,"
			       " deleting channel", ngi_mainnick(ngi2),
			       ci->name);
		    goto delete;
		}
	    } else {
		module_log("Deleting channel %s owned by deleted nick %s",
			   ci->name, oldnick);
	      delete:
		delchan(ci);
		if (was_suspended) {
		    /* Channel was suspended, so make it forbidden */
		    module_log("Channel %s was suspended, forbidding it",
			       name_save);
		    ci = makechan(name_save);
		    ci->flags |= CI_VERBOTEN;
		}
		continue;
	    }
	    modified = 1;
	}
	if (ci->successor == id) {
	    ci->successor = 0;
	    modified = 1;
	}
	ARRAY_FOREACH (i, ci->access) {
	    if (ci->access[i].nickgroup == id) {
		ci->access[i].nickgroup = 0;
		modified = 1;
	    }
	}
	if (modified)
	    put_channelinfo(ci);
    }
    return 0;
}

/*************************************************************************/
/*********************** ChanServ command routines ***********************/
/*************************************************************************/

/* Short routine for do_help() to return the proper access level string for
 * a given level based on which access modules are loaded.  Assumes numeric
 * levels if no access module is loaded.
 */
static const char *getstring_cmdacc(NickGroupInfo *ngi, int16 level)
{
    int str_levxop, str_lev, str_xop;

    switch (level) {
      case ACCLEV_SOP:
	str_levxop = CHAN_HELP_REQSOP_LEVXOP;
	str_lev    = CHAN_HELP_REQSOP_LEV;
	str_xop    = CHAN_HELP_REQSOP_XOP;
	break;
      case ACCLEV_AOP:
	str_levxop = CHAN_HELP_REQAOP_LEVXOP;
	str_lev    = CHAN_HELP_REQAOP_LEV;
	str_xop    = CHAN_HELP_REQAOP_XOP;
	break;
      case ACCLEV_HOP:
	str_levxop = CHAN_HELP_REQHOP_LEVXOP;
	str_lev    = CHAN_HELP_REQHOP_LEV;
	str_xop    = CHAN_HELP_REQHOP_XOP;
	break;
      case ACCLEV_VOP:
	str_levxop = CHAN_HELP_REQVOP_LEVXOP;
	str_lev    = CHAN_HELP_REQVOP_LEV;
	str_xop    = CHAN_HELP_REQVOP_XOP;
	break;
      default:
	module_log("BUG: weird level (%d) in getstring_cmdacc()", level);
	return "???";
    }
    if (find_module("chanserv/access-xop")) {
	if (find_module("chanserv/access-levels"))
	    return getstring(ngi, str_levxop);
	else
	    return getstring(ngi, str_xop);
    } else {
	return getstring(ngi, str_lev);
    } 
}


static void do_help(User *u)
{
    char *cmd = strtok_remaining();
    Command *cmdrec;

    if (!cmd) {
	notice_help(s_ChanServ, u, CHAN_HELP);
	if (CSExpire)
	    notice_help(s_ChanServ, u, CHAN_HELP_EXPIRES,
			maketime(u->ngi,CSExpire,0));
    } else if (call_callback_2(module, cb_help, u, cmd) > 0) {
	return;
    } else if (stricmp(cmd, "COMMANDS") == 0) {
	notice_help(s_ChanServ, u, CHAN_HELP_COMMANDS);
	if (find_module("chanserv/sendpass"))
	    notice_help(s_ChanServ, u, CHAN_HELP_COMMANDS_SENDPASS);
	notice_help(s_ChanServ, u, CHAN_HELP_COMMANDS_DROP);
	if (find_module("chanserv/access-levels"))
	    notice_help(s_ChanServ, u, CHAN_HELP_COMMANDS_LEVELS);
	if (find_module("chanserv/access-xop")) {
	    notice_help(s_ChanServ, u, CHAN_HELP_COMMANDS_XOP);
	    if (protocol_features & PF_HALFOP)
		notice_help(s_ChanServ, u, CHAN_HELP_COMMANDS_HOP);
	}
	notice_help(s_ChanServ, u, CHAN_HELP_COMMANDS_OPVOICE);
	if (protocol_features & PF_HALFOP)
	    notice_help(s_ChanServ, u, CHAN_HELP_COMMANDS_HALFOP);
	if (protocol_features & PF_CHANPROT)
	    notice_help(s_ChanServ, u, CHAN_HELP_COMMANDS_PROTECT);
	notice_help(s_ChanServ, u, CHAN_HELP_COMMANDS_INVITE);
	if (!CSListOpersOnly)
	    notice_help(s_ChanServ, u, CHAN_HELP_COMMANDS_LIST);
	notice_help(s_ChanServ, u, CHAN_HELP_COMMANDS_AKICK);
	call_callback_2(module, cb_help_cmds, u, 0);
	if (is_oper(u)) {
	    notice_help(s_ChanServ, u, CHAN_OPER_HELP_COMMANDS);
	    if (EnableGetpass)
		notice_help(s_ChanServ, u, CHAN_OPER_HELP_COMMANDS_GETPASS);
	    notice_help(s_ChanServ, u, CHAN_OPER_HELP_COMMANDS_FORBID);
	    if (CSListOpersOnly)
		notice_help(s_ChanServ, u, CHAN_HELP_COMMANDS_LIST);
	    call_callback_2(module, cb_help_cmds, u, 1);
	    notice_help(s_ChanServ, u, CHAN_OPER_HELP_COMMANDS_END);
	}
    } else if (!CSEnableRegister && is_oper(u) && stricmp(cmd,"REGISTER")==0) {
	notice_help(s_ChanServ, u, CHAN_HELP_REGISTER);
	notice_help(s_ChanServ, u, CHAN_HELP_REGISTER_ADMINONLY);
    } else if (stricmp(cmd, "LIST") == 0) {
	if (is_oper(u))
	    notice_help(s_ChanServ, u, CHAN_OPER_HELP_LIST);
	else
	    notice_help(s_ChanServ, u, CHAN_HELP_LIST);
	if (CSListOpersOnly)
	    notice_help(s_ChanServ, u, CHAN_HELP_LIST_OPERSONLY);
    } else if (stricmp(cmd, "KICK") == 0) {
	cmdrec = lookup_cmd(module, cmd);
	notice_help(s_ChanServ, u, CHAN_HELP_KICK,
	     getstring_cmdacc(u->ngi, cmdrec ? (int)(long)cmdrec->help_param1 : -1));
	if (protocol_features & PF_CHANPROT)
	    notice_help(s_ChanServ, u, CHAN_HELP_KICK_PROTECTED);
    } else if (stricmp(cmd, "CLEAR") == 0) {
	notice_help(s_ChanServ, u, CHAN_HELP_CLEAR);
	if (protocol_features & PF_BANEXCEPT)
	    notice_help(s_ChanServ, u, CHAN_HELP_CLEAR_EXCEPTIONS);
	if (protocol_features & PF_INVITEMASK)
	    notice_help(s_ChanServ, u, CHAN_HELP_CLEAR_INVITES);
	notice_help(s_ChanServ, u, CHAN_HELP_CLEAR_MID);
	if (protocol_features & PF_HALFOP)
	    notice_help(s_ChanServ, u, CHAN_HELP_CLEAR_HALFOPS);
	cmdrec = lookup_cmd(module, cmd);
	notice_help(s_ChanServ, u, CHAN_HELP_CLEAR_END,
	     getstring_cmdacc(u->ngi, cmdrec ? (int)(long)cmdrec->help_param1 : -1));
    } else if ((stricmp(cmd, "AKICK") == 0
		|| stricmp(cmd, "OP") == 0
		|| stricmp(cmd, "DEOP") == 0
		|| stricmp(cmd, "VOICE") == 0
		|| stricmp(cmd, "DEVOICE") == 0
		|| stricmp(cmd, "HALFOP") == 0
		|| stricmp(cmd, "DEHALFOP") == 0
		|| stricmp(cmd, "PROTECT") == 0
		|| stricmp(cmd, "DEPROTECT") == 0
		|| stricmp(cmd, "INVITE") == 0
		|| stricmp(cmd, "UNBAN") == 0
		|| stricmp(cmd, "KICK") == 0
		|| stricmp(cmd, "TOPIC") == 0
		|| stricmp(cmd, "CLEAR") == 0
		|| stricmp(cmd, "STATUS") == 0)
	    && (cmdrec = lookup_cmd(module, cmd)) != NULL
    ) {
	notice_help(s_ChanServ, u, cmdrec->helpmsg_all,
		    getstring_cmdacc(u->ngi, (int)(long)cmdrec->help_param1));
    } else {
	help_cmd(s_ChanServ, u, module, cmd);
    }
}

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

static void do_register(User *u)
{
    char *chan = strtok(NULL, " ");
    char *pass = strtok(NULL, " ");
    char *desc = strtok_remaining();
    NickInfo *ni = u->ni;
    NickGroupInfo *ngi = u->ngi;
    Channel *c;
    ChannelInfo *ci;
    struct u_chaninfolist *uc;
    int max;

    if (readonly) {
	notice_lang(s_ChanServ, u, CHAN_REGISTER_DISABLED);
	return;
    }

    if (!desc) {
	syntax_error(s_ChanServ, u, "REGISTER", CHAN_REGISTER_SYNTAX);
    } else if (strcmp(chan, "#") == 0) {
	notice_lang(s_ChanServ, u, CHAN_REGISTER_SHORT_CHANNEL);
    } else if (*chan == '&') {
	notice_lang(s_ChanServ, u, CHAN_REGISTER_NOT_LOCAL);
    } else if (*chan != '#') {
	notice_lang(s_ChanServ, u, CHAN_REGISTER_INVALID_NAME);
    } else if (!ni) {
	notice_lang(s_ChanServ, u, CHAN_MUST_REGISTER_NICK, s_NickServ);
    } else if (!user_identified(u)) {
	notice_lang(s_ChanServ, u, CHAN_MUST_IDENTIFY_NICK,
		s_NickServ, s_NickServ);

    } else if ((ci = get_channelinfo(chan)) != NULL) {
	if (ci->flags & CI_VERBOTEN) {
	    module_log("Attempt to register forbidden channel %s by %s!%s@%s",
		       ci->name, u->nick, u->username, u->host);
	    notice_lang(s_ChanServ, u, CHAN_MAY_NOT_BE_REGISTERED, chan);
	} else if (ci->suspendinfo) {
	    module_log("Attempt to register suspended channel %s by %s!%s@%s",
		       ci->name, u->nick, u->username, u->host);
	    notice_lang(s_ChanServ, u, CHAN_ALREADY_REGISTERED, chan);
	} else {
	    notice_lang(s_ChanServ, u, CHAN_ALREADY_REGISTERED, chan);
	}

    } else if (!is_chanop(u, chan)) {
	notice_lang(s_ChanServ, u, CHAN_MUST_BE_CHANOP);

    } else if (!is_services_admin(u) && check_channel_limit(ngi, &max) >= 0) {
	notice_lang(s_ChanServ, u, ngi->channels_count > max
					? CHAN_EXCEEDED_CHANNEL_LIMIT
					: CHAN_REACHED_CHANNEL_LIMIT, max);

    } else if (!(c = get_channel(chan))) {
	/* Should not fail because we checked is_chanop() above, but just
	 * in case... */
	module_log("Channel %s not found for REGISTER", chan);
	notice_lang(s_ChanServ, u, CHAN_REGISTRATION_FAILED);

    } else if (!(ci = makechan(chan))) {
	module_log("makechan() failed for REGISTER %s", chan);
	notice_lang(s_ChanServ, u, CHAN_REGISTRATION_FAILED);

    } else {
	int len = strlen(pass);

	/* Password length check and truncation (like NickServ REGISTER) */
	max = encrypt_check_len(len, PASSMAX);
	if ((max == 0 && len > PASSMAX-1) || max > PASSMAX-1)
	    max = PASSMAX-1;
	if (max > 0) {
	    memset(pass+max, 0, len-max);
	    len = max;
	    notice_lang(s_ChanServ, u, PASSWORD_TRUNCATED, max);
	}
	if (encrypt(pass, len, ci->founderpass, PASSMAX) < 0) {
	    module_log("Couldn't encrypt password for %s (REGISTER)", chan);
	    notice_lang(s_ChanServ, u, CHAN_REGISTRATION_FAILED);
	    delchan(ci);
	    return;
	}
	c->ci = ci;
	ci->c = c;
	ci->flags = CSDefFlags;
	ci->mlock_on = CMODE_n | CMODE_t;
	ci->memos.memomax = MEMOMAX_DEFAULT;
	ci->last_used = ci->time_registered;
	ci->founder = u->ngi->id;
	ci->desc = sstrdup(desc);
	if (c->topic) {
	    ci->last_topic = sstrdup(c->topic);
	    strscpy(ci->last_topic_setter, c->topic_setter, NICKMAX);
	    ci->last_topic_time = c->topic_time;
	}
	count_chan(ci);
	put_channelinfo(ci);
	module_log("Channel %s registered by %s!%s@%s",
		   chan, u->nick, u->username, u->host);
	notice_lang(s_ChanServ, u, CHAN_REGISTERED, chan, u->nick);
	if (CSShowPassword)
	    notice_lang(s_ChanServ, u, CHAN_PASSWORD_IS, pass);
	memset(pass, 0, len);
	uc = smalloc(sizeof(*uc));
	LIST_INSERT(uc, u->id_chans);
	strscpy(uc->chan, ci->name, CHANMAX);
	/* Implement new mode lock */
	check_modes(ci->c);

    }
}

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

static void do_identify(User *u)
{
    char *chan = strtok(NULL, " ");
    char *pass = strtok_remaining();
    ChannelInfo *ci;
    struct u_chaninfolist *uc;

    if (!pass) {
	syntax_error(s_ChanServ, u, "IDENTIFY", CHAN_IDENTIFY_SYNTAX);
    } else if (!(ci = get_channelinfo(chan))) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_REGISTERED, chan);
    } else if (ci->flags & CI_VERBOTEN) {
	notice_lang(s_ChanServ, u, CHAN_X_FORBIDDEN, chan);
    } else if (ci->suspendinfo) {
	notice_lang(s_ChanServ, u, CHAN_X_SUSPENDED, chan);
    } else {
	int res = check_password(pass, ci->founderpass);
	if (res == 1) {
	    ci->bad_passwords = 0;
	    ci->last_used = time(NULL);
	    put_channelinfo(ci);
	    if (!is_identified(u, ci)) {
		uc = smalloc(sizeof(*uc));
		LIST_INSERT(uc, u->id_chans);
		strscpy(uc->chan, ci->name, CHANMAX);
		module_log("%s!%s@%s identified for %s",
			   u->nick, u->username, u->host, ci->name);
	    }
	    notice_lang(s_ChanServ, u, CHAN_IDENTIFY_SUCCEEDED, chan);
	} else if (res < 0) {
	    module_log("check_password failed for %s", ci->name);
	    notice_lang(s_ChanServ, u, CHAN_IDENTIFY_FAILED);
	} else {
	    module_log("Failed IDENTIFY for %s by %s!%s@%s",
		       ci->name, u->nick, u->username, u->host);
	    chan_bad_password(u, ci);
	}

    }
}

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

static void do_drop(User *u)
{
    char *chan = strtok(NULL, " ");
    ChannelInfo *ci;
    int is_servadmin = is_services_admin(u);
    Channel *c;

    if (readonly && !is_servadmin) {
	notice_lang(s_ChanServ, u, CHAN_DROP_DISABLED);
	return;
    }

    if (!chan) {
	syntax_error(s_ChanServ, u, "DROP", CHAN_DROP_SYNTAX);
    } else if (!(ci = get_channelinfo(chan))) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_REGISTERED, chan);
    } else if (!is_servadmin && (ci->flags & CI_VERBOTEN)) {
	notice_lang(s_ChanServ, u, CHAN_X_FORBIDDEN, chan);
    } else if (!is_servadmin && ci->suspendinfo) {
	notice_lang(s_ChanServ, u, CHAN_X_SUSPENDED, chan);
    } else if (!is_servadmin && !is_identified(u, ci)) {
	notice_lang(s_ChanServ, u, CHAN_IDENTIFY_REQUIRED, s_ChanServ, chan);
    } else {
	const char *founder;
	char tmpbuf[64];

	if (readonly)  /* in this case we know they're a Services admin */
	    notice_lang(s_ChanServ, u, READ_ONLY_MODE);
	if (ci->founder) {
	    NickGroupInfo *ngi = get_ngi_id(ci->founder);
	    if (ngi) {
		founder = ngi_mainnick(ngi);
	    } else {
		snprintf(tmpbuf, sizeof(tmpbuf), "<unknown: ID %u>",
			 ci->founder);
		founder = tmpbuf;
	    }
	} else {
	    founder = "<none>";
	}
	module_log("Channel %s (founder %s) dropped by %s!%s@%s",
		   ci->name, founder, u->nick, u->username, u->host);
	delchan(ci);
	if (chanmode_reg && (c = get_channel(chan))) {
	    c->mode &= ~chanmode_reg;
	    send_cmode_cmd(s_ChanServ, chan, "-%s",
		     mode_flags_to_string(chanmode_reg, MODE_CHANNEL));
	}
	notice_lang(s_ChanServ, u, CHAN_DROPPED, chan);
    }
}

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

/* SADMINS, and users who have identified for a channel, can now cause its
 * entry message and successor to be displayed by supplying the ALL
 * parameter.
 * Syntax: INFO channel [ALL]
 * -TheShadow (29 Mar 1999)
 */

/* Check the status of show_all and make a note of having done so.  See
 * comments at nickserv/main.c/do_info() for details. */
#define CHECK_SHOW_ALL (used_all++, show_all)

static void do_info(User *u)
{
    char *chan = strtok(NULL, " ");
    char *param = strtok(NULL, " ");
    ChannelInfo *ci;
    NickGroupInfo *ngi, *ngi2 = NULL;
    char buf[BUFSIZE], *end, *s;
    int is_servadmin = is_services_admin(u);
    int can_show_all = 0, show_all = 0, used_all = 0;

    if (!chan) {
        syntax_error(s_ChanServ, u, "INFO", CHAN_INFO_SYNTAX);
    } else if (!(ci = get_channelinfo(chan))) {
        notice_lang(s_ChanServ, u, CHAN_X_NOT_REGISTERED, chan);
    } else if (ci->flags & CI_VERBOTEN) {
        notice_lang(s_ChanServ, u, CHAN_X_FORBIDDEN, chan);
    } else if (!ci->founder) {
        /* Paranoia... this shouldn't be able to happen */
	module_log("INFO: non-forbidden channel %s has no founder, deleting",
		   ci->name);
        delchan(ci);
        notice_lang(s_ChanServ, u, CHAN_X_NOT_REGISTERED, chan);
    } else if (!(ngi = get_ngi_id(ci->founder))
	       || (ci->successor && !(ngi2 = get_ngi_id(ci->successor)))
    ) {
	notice_lang(s_ChanServ, u, INTERNAL_ERROR);
    } else {

	/* Update last used time if the channel is currently in use. */
	if (ci->c) {
	    struct c_userlist *cu;
	    LIST_FOREACH (cu, ci->c->users) {
		if (check_access(cu->user, ci, CA_AUTOOP)) {
		    if (debug >= 2)
			module_log("debug: updating last used time for %s"
				   " (INFO)", ci->name);
		    ci->last_used = time(NULL);
		    put_channelinfo(ci);
		    break;
		}
	    }
	}

        /* Only show all the channel's settings to sadmins and founders. */
	can_show_all = (is_founder(u, ci) || is_servadmin);

        if ((param && stricmp(param, "ALL") == 0) && can_show_all)
            show_all = 1;

        notice_lang(s_ChanServ, u, CHAN_INFO_HEADER, chan);
	notice_lang(s_ChanServ, u, CHAN_INFO_FOUNDER, ngi_mainnick(ngi));
	if (ngi2 != NULL && CHECK_SHOW_ALL) {
	    notice_lang(s_ChanServ, u, CHAN_INFO_SUCCESSOR,
			ngi_mainnick(ngi2));
	}
	notice_lang(s_ChanServ, u, CHAN_INFO_DESCRIPTION, ci->desc);
	strftime_lang(buf, sizeof(buf), u->ngi, STRFTIME_DATE_TIME_FORMAT,
		      ci->time_registered);
	notice_lang(s_ChanServ, u, CHAN_INFO_TIME_REGGED, buf);
	strftime_lang(buf, sizeof(buf), u->ngi, STRFTIME_DATE_TIME_FORMAT,
		      ci->last_used);
	notice_lang(s_ChanServ, u, CHAN_INFO_LAST_USED, buf);

	/* Do not show last_topic if channel is mlock'ed +s or +p, or if the
	 * channel's current modes include +s or +p. -TheShadow */
	/* But show it if we're showing all info. --AC */
	if (ci->last_topic) {
	    int mlock_sp = (ci->mlock_on & (CMODE_s | CMODE_p));
	    int mode_sp = (ci->c && (ci->c->mode & (CMODE_s | CMODE_p)));
	    int hide = (ci->flags & CI_HIDE_TOPIC);
	    if ((!mlock_sp && !mode_sp && !hide) || CHECK_SHOW_ALL) {
		notice_lang(s_ChanServ, u, CHAN_INFO_LAST_TOPIC,
			    ci->last_topic);
		notice_lang(s_ChanServ, u, CHAN_INFO_TOPIC_SET_BY,
			    ci->last_topic_setter);
	    }
	}

	if (ci->entry_message && CHECK_SHOW_ALL)
	    notice_lang(s_ChanServ, u, CHAN_INFO_ENTRYMSG, ci->entry_message);
	if (ci->url)
	    notice_lang(s_ChanServ, u, CHAN_INFO_URL, ci->url);
	if (ci->email && (!(ci->flags & CI_HIDE_EMAIL) || CHECK_SHOW_ALL))
	    notice_lang(s_ChanServ, u, CHAN_INFO_EMAIL, ci->email);
	s = chanopts_to_string(ci, u->ngi);
	notice_lang(s_ChanServ, u, CHAN_INFO_OPTIONS,
		    *s ? s : getstring(u->ngi, CHAN_INFO_OPT_NONE));
	end = buf;
	*end = 0;
	if (ci->mlock_on || ci->mlock_key || ci->mlock_limit)
	    end += snprintf(end, sizeof(buf)-(end-buf), "+%s",
			    mode_flags_to_string(ci->mlock_on, MODE_CHANNEL));
	if (ci->mlock_off)
	    end += snprintf(end, sizeof(buf)-(end-buf), "-%s",
			    mode_flags_to_string(ci->mlock_off, MODE_CHANNEL));
	if (*buf && (!(ci->flags & CI_HIDE_MLOCK) || CHECK_SHOW_ALL))
	    notice_lang(s_ChanServ, u, CHAN_INFO_MODE_LOCK, buf);

        if ((ci->flags & CI_NOEXPIRE) && CHECK_SHOW_ALL)
	    notice_lang(s_ChanServ, u, CHAN_INFO_NO_EXPIRE);

	if (ci->suspendinfo) {
	    notice_lang(s_ChanServ, u, CHAN_X_SUSPENDED, chan);
	    if (CHECK_SHOW_ALL) {
		SuspendInfo *si = ci->suspendinfo;
		char timebuf[BUFSIZE], expirebuf[BUFSIZE];

		strftime_lang(timebuf, sizeof(timebuf), u->ngi,
			      STRFTIME_DATE_TIME_FORMAT, si->suspended);
		expires_in_lang(expirebuf, sizeof(expirebuf), u->ngi,
				si->expires);
		notice_lang(s_ChanServ, u, CHAN_INFO_SUSPEND_DETAILS,
			    si->who, timebuf, expirebuf);
		notice_lang(s_ChanServ, u, CHAN_INFO_SUSPEND_REASON,
			    si->reason);
	    }
	}

	if (can_show_all && !show_all && used_all)
	    notice_lang(s_ChanServ, u, CHAN_INFO_SHOW_ALL, s_ChanServ,
			ci->name);

    }
}

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

/* SADMINS can search for channels based on their CI_VERBOTEN and
 * CI_NOEXPIRE flags and suspension status. This works in the same way as
 * NickServ's LIST command.
 * Syntax for sadmins: LIST pattern [FORBIDDEN] [NOEXPIRE] [SUSPENDED]
 * Also fixed CI_PRIVATE channels being shown to non-sadmins.
 * -TheShadow
 */

static void do_list(User *u)
{
    char *pattern = strtok(NULL, " ");
    char *keyword;
    ChannelInfo *ci;
    int nchans;
    char buf[BUFSIZE];
    int is_servadmin = is_services_admin(u);
    int32 matchflags = 0; /* CI_ flags a chan must match one of to qualify */
    int match_susp = 0;	/* nonzero to match suspended channels */


    if (CSListOpersOnly && (!u || !is_oper(u))) {
	notice_lang(s_ChanServ, u, PERMISSION_DENIED);
	return;
    }

    if (!pattern) {
	syntax_error(s_ChanServ, u, "LIST",
		     is_oper(u) ? CHAN_LIST_OPER_SYNTAX : CHAN_LIST_SYNTAX);
    } else {
	nchans = 0;

	while (is_servadmin && (keyword = strtok(NULL, " "))) {
	    if (stricmp(keyword, "FORBIDDEN") == 0) {
		matchflags |= CI_VERBOTEN;
	    } else if (stricmp(keyword, "NOEXPIRE") == 0) {
		matchflags |= CI_NOEXPIRE;
	    } else if (stricmp(keyword, "SUSPENDED") == 0) {
		match_susp = 1;
	    } else {
		syntax_error(s_ChanServ, u, "LIST",
		     is_oper(u) ? CHAN_LIST_OPER_SYNTAX : CHAN_LIST_SYNTAX);
	    }
	}

	notice_lang(s_ChanServ, u, CHAN_LIST_HEADER, pattern);
	for (ci = first_channelinfo(); ci; ci = next_channelinfo()) {
	    if (!is_servadmin && (ci->flags & (CI_PRIVATE | CI_VERBOTEN)))
		continue;
	    if (matchflags || match_susp) {
		if (!((ci->flags & matchflags) || (ci->suspendinfo && match_susp)))
		    continue;
	    }

	    snprintf(buf, sizeof(buf), "%-20s  %s", ci->name,
		     ci->desc ? ci->desc : "");
	    if (irc_stricmp(pattern, ci->name) == 0
	     || match_wild_nocase(pattern, buf)
	    ) {
		if (++nchans <= CSListMax) {
		    char noexpire_char = ' ', suspended_char = ' ';
		    if (is_servadmin) {
			if (ci->flags & CI_NOEXPIRE)
			    noexpire_char = '!';
			if (ci->suspendinfo)
			    suspended_char = '*';
		    }

		    /* This can only be true for SADMINS - normal users
		     * will never get this far with a VERBOTEN channel.
		     * -TheShadow */
		    if (ci->flags & CI_VERBOTEN) {
			snprintf(buf, sizeof(buf), "%-20s  [Forbidden]",
				 ci->name);
		    }

		    notice(s_ChanServ, u->nick, "  %c%c%s",
			   suspended_char, noexpire_char, buf);
		}
	    }
	}
	notice_lang(s_ChanServ, u, CHAN_LIST_END,
		    nchans>CSListMax ? CSListMax : nchans, nchans);
    }

}

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

/* Internal routine to handle all op/voice-type requests. */

static struct {
    const char *cmd;
    int add;
    char mode;
    int target_acc;	/* Target access (CA_*) at which we refuse command */
    int success_msg, already_msg, failure_msg;
} opvoice_data[] = {
    { "VOICE",     1, 'v', -1,
	  CHAN_VOICE_SUCCEEDED, CHAN_VOICE_ALREADY, CHAN_VOICE_FAILED },
    { "HALFOP",    1, 'h', CA_AUTODEOP,
	  CHAN_HALFOP_SUCCEEDED, CHAN_HALFOP_ALREADY, CHAN_HALFOP_FAILED },
    { "OP",        1, 'o', CA_AUTODEOP,
	  CHAN_OP_SUCCEEDED, CHAN_OP_ALREADY, CHAN_OP_FAILED },
    { "PROTECT",   1, 'a', -1,
	  CHAN_PROTECT_SUCCEEDED, CHAN_PROTECT_ALREADY, CHAN_PROTECT_FAILED },

    { "DEVOICE",   0, 'v', CA_AUTOVOICE,
	  CHAN_DEVOICE_SUCCEEDED, CHAN_DEVOICE_ALREADY, CHAN_DEVOICE_FAILED },
    { "DEHALFOP",  0, 'h', CA_AUTOHALFOP,
	  CHAN_DEHALFOP_SUCCEEDED, CHAN_DEHALFOP_ALREADY,
	  CHAN_DEHALFOP_FAILED },
    { "DEOP",      0, 'o', CA_AUTOOP,
	  CHAN_DEOP_SUCCEEDED, CHAN_DEOP_ALREADY, CHAN_DEOP_FAILED },
    { "DEPROTECT", 0, 'a', CA_AUTOPROTECT,
	  CHAN_DEPROTECT_SUCCEEDED, CHAN_DEPROTECT_ALREADY,
	  CHAN_DEPROTECT_FAILED },
};

static void do_opvoice(User *u, const char *cmd)
{
    const char *cmd2 = (strnicmp(cmd,"DE",2) == 0 ? cmd+2 : cmd);
    char *chan = strtok(NULL, " ");
    char *target = strtok(NULL, " ");
    Channel *c;
    ChannelInfo *ci;
    User *target_user;
    int i;
    int add, target_acc, success_msg, failure_msg, already_msg;
    int target_nextacc;  /* Target level upper bound for DEVOICE, DEHALFOP */
    int32 mode;

    ARRAY2_SEARCH(opvoice_data, lenof(opvoice_data), cmd, cmd, strcmp, i);
    if (i >= lenof(opvoice_data)) {
	module_log("do_opvoice: BUG: command `%s' not found in table", cmd);
	notice_lang(s_ChanServ, u, INTERNAL_ERROR);
	return;
    }
    add            = opvoice_data[i].add;
    mode           = mode_char_to_flag(opvoice_data[i].mode, MODE_CHANUSER);
    target_acc     = opvoice_data[i].target_acc;
    success_msg    = opvoice_data[i].success_msg;
    already_msg    = opvoice_data[i].already_msg;
    failure_msg    = opvoice_data[i].failure_msg;
    if (strcmp(cmd, "DEVOICE") == 0)
	target_nextacc = (protocol_features & PF_HALFOP)
		       ? CA_AUTOHALFOP : CA_AUTOOP;
    else if (strcmp(cmd, "DEHALFOP") == 0)
	target_nextacc = CA_AUTOOP;
    else
	target_nextacc = -1;
    /* Special case for DEPROTECT: also remove channel-owner mode, if any */
    if (strcmp(cmd, "DEPROTECT") == 0 && chanusermode_owner)
	mode |= chanusermode_owner;

    if (target) {
	target_user = get_user(target);
    } else {
	target = u->nick;
	target_user = u;
    }
    if (!chan) {
	syntax_error(s_ChanServ, u, cmd, CHAN_OPVOICE_SYNTAX);
    } else if (!(c = get_channel(chan))) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_IN_USE, chan);
    } else if (c->bouncy_modes) {
	notice_lang(s_ChanServ, u, CHAN_BOUNCY_MODES, cmd);
    } else if (!(ci = c->ci)) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_REGISTERED, chan);
    } else if (ci->flags & CI_VERBOTEN) {
	notice_lang(s_ChanServ, u, CHAN_X_FORBIDDEN, chan);
    } else if (!u || !check_access_cmd(u, ci, cmd2, NULL)) {
	notice_lang(s_ChanServ, u, PERMISSION_DENIED);
    } else if (!target_user) {
	notice_lang(s_ChanServ, u, NICK_X_NOT_IN_USE, target);
    } else if (target_user != u && !(!add && !(ci->flags & CI_ENFORCE))
	               /* Allow changing own mode; allow deops if !ENFORCE */
	       && target_acc >= 0 && check_access(target_user, ci, target_acc)
	               /* Disallow if user is at/above disallow level... */
	       && (target_nextacc < 0
		   || !check_access(target_user, ci, target_nextacc))
	               /* ... and below level-above-disallow-level (if any) */
    ) {
	notice_lang(s_ChanServ, u, failure_msg, target, chan);
    } else {
	struct c_userlist *cu;
	char modebuf[3];
	int32 umode, thismode;

	/* Retrieve c_userlist entry and see (1) if they're even on the
	 * channel and (2) if they already have / don't have the mode */
	LIST_SEARCH_SCALAR(c->users, user, target_user, cu);
	if (!cu) {
	    notice_lang(s_ChanServ, u, NICK_X_NOT_ON_CHAN_X, target, chan);
	    return;
	}
	umode = cu->mode & mode;
	if (add)
	    umode ^= mode;  /* make note of which ones they DON'T have */
	if (!umode) {
	    /* Target user already has (or doesn't have, if !add) mode(s),
	     * so don't do anything */
	    notice_lang(s_ChanServ, u, already_msg, target, chan);
	    return;
	}

	/* Set appropriate mode(s) */
	modebuf[0] = add ? '+' : '-';
	modebuf[2] = 0;
	thismode = 1;
	while (umode) {
	    while (!(umode & thismode))
		thismode <<= 1;
	    if (!thismode) {  /* impossible */
		module_log("BUG: thismode==0 in opvoice!");
		break;
	    }
	    modebuf[1] = mode_flag_to_char(thismode, MODE_CHANUSER);
	    set_cmode(s_ChanServ, c, modebuf, target);
	    umode &= ~thismode;
	}
	set_cmode(NULL, c);  /* Flush mode change out */
	if (ci->flags & CI_OPNOTICE) {
	    notice(s_ChanServ, chan, "%s command used for %s by %s",
		   cmd, target, u->nick);
	}
	notice_lang(s_ChanServ, u, success_msg, target, chan);
	/* If it was an OP command, update the last-used time */
	if (strcmp(cmd, "OP") == 0) {
	    ci->last_used = time(NULL);
	    put_channelinfo(ci);
	}
    }
}

static void do_op(User *u)
{
    do_opvoice(u, "OP");
}

static void do_deop(User *u)
{
    do_opvoice(u, "DEOP");
}

static void do_voice(User *u)
{
    do_opvoice(u, "VOICE");
}

static void do_devoice(User *u)
{
    do_opvoice(u, "DEVOICE");
}

static void do_halfop(User *u)
{
    do_opvoice(u, "HALFOP");
}

static void do_dehalfop(User *u)
{
    do_opvoice(u, "DEHALFOP");
}

static void do_protect(User *u)
{
    do_opvoice(u, "PROTECT");
}

static void do_deprotect(User *u)
{
    do_opvoice(u, "DEPROTECT");
}

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

static void do_invite(User *u)
{
    char *chan = strtok(NULL, " ");
    Channel *c;
    ChannelInfo *ci;

    if (!chan) {
	syntax_error(s_ChanServ, u, "INVITE", CHAN_INVITE_SYNTAX);
    } else if (!(c = get_channel(chan))) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_IN_USE, chan);
    } else if (c->bouncy_modes) {
	notice_lang(s_ChanServ, u, CHAN_BOUNCY_MODES, "INVITE");
    } else if (!(ci = c->ci)) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_REGISTERED, chan);
    } else if (ci->flags & CI_VERBOTEN) {
	notice_lang(s_ChanServ, u, CHAN_X_FORBIDDEN, chan);
    } else if (!u || !check_access_cmd(u, ci, "INVITE", NULL)) {
	notice_lang(s_ChanServ, u, PERMISSION_DENIED);
    } else if (call_callback_3(module, cb_invite, u, c, ci) <= 0) {
	send_cmd(s_ChanServ, "INVITE %s %s", u->nick, chan);
	notice_lang(s_ChanServ, u, CHAN_INVITE_OK, u->nick, chan);
    }
}

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

static void do_unban(User *u)
{
    char *chan = strtok(NULL, " ");
    Channel *c;
    ChannelInfo *ci;

    if (!chan) {
	syntax_error(s_ChanServ, u, "UNBAN", CHAN_UNBAN_SYNTAX);
    } else if (!(c = get_channel(chan))) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_IN_USE, chan);
    } else if (c->bouncy_modes) {
	notice_lang(s_ChanServ, u, CHAN_BOUNCY_MODES, "UNBAN");
    } else if (!(ci = c->ci)) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_REGISTERED, chan);
    } else if (ci->flags & CI_VERBOTEN) {
	notice_lang(s_ChanServ, u, CHAN_X_FORBIDDEN, chan);
    } else if (!u || !check_access_cmd(u, ci, "UNBAN", NULL)) {
	notice_lang(s_ChanServ, u, PERMISSION_DENIED);
    } else if (call_callback_3(module, cb_unban, u, c, ci) <= 0) {
	clear_channel(c, CLEAR_BANS, u);
	notice_lang(s_ChanServ, u, CHAN_UNBANNED, chan);
    }
}

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

/* do_kick() is used by users.c, so we use a different function name */

static void do_cskick(User *u)
{
    char *chan = strtok(NULL, " ");
    char *target = strtok(NULL, " ");
    char *reason = strtok_remaining();
    Channel *c;
    ChannelInfo *ci;
    User *target_user;

    if (!target) {
	syntax_error(s_ChanServ, u, "KICK", CHAN_KICK_SYNTAX);
    } else if (!(c = get_channel(chan))) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_IN_USE, chan);
    } else if (c->bouncy_modes) {
	notice_lang(s_ChanServ, u, CHAN_BOUNCY_MODES, "KICK");
    } else if (!(ci = c->ci)) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_REGISTERED, chan);
    } else if (ci->flags & CI_VERBOTEN) {
	notice_lang(s_ChanServ, u, CHAN_X_FORBIDDEN, chan);
    } else if (!u || !check_access_cmd(u, ci, "KICK", NULL)) {
	notice_lang(s_ChanServ, u, PERMISSION_DENIED);
    } else if (!(target_user = get_user(target))) {
	notice_lang(s_ChanServ, u, NICK_X_NOT_IN_USE, target);
    } else {
	struct c_userlist *cu;
	char reasonbuf[BUFSIZE];
	char *kick_av[3];

	/* Retrieve c_userlist entry and see (1) if they're even on the
	 * channel and (2) if they're protected (if the ircd supports that) */
	LIST_SEARCH_SCALAR(c->users, user, target_user, cu);
	if (!cu) {
	    notice_lang(s_ChanServ, u, NICK_X_NOT_ON_CHAN_X, target, chan);
	    return;
	}
	if (protocol_features & PF_CHANPROT) {
	    int32 modes_to_check = chanusermode_owner
				 | mode_char_to_flag('a', MODE_CHANUSER);
	    if (cu->mode & modes_to_check) {
		notice_lang(s_ChanServ, u, CHAN_KICK_PROTECTED, target, chan);
		return;
	    }
	}
	/* Also prevent Services opers and above from being kicked */
	if (is_services_oper(target_user)) {
	    notice_lang(s_ChanServ, u, CHAN_KICK_PROTECTED, target, chan);
	    return;
	}

	/* Construct reason string: "KICK by Nick" / "KICK by Nick (reason)" */
	if (reason && !*reason)
	    reason = NULL;
	snprintf(reasonbuf, sizeof(reasonbuf), "KICK by %s%s%s%s", u->nick,
		 reason ? " (" : "", reason ? reason : "", reason ? ")" : "");

	/* Actually kick user */
	send_cmd(s_ChanServ, "KICK %s %s :%s", chan, target, reasonbuf);
	kick_av[0] = chan;
	kick_av[1] = target;
	kick_av[2] = reasonbuf;
	do_kick(s_ChanServ, 3, kick_av);
	notice_lang(s_ChanServ, u, CHAN_KICKED, target, chan);
    }
}

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

static void do_cstopic(User *u)
{
    char *chan = strtok(NULL, " ");
    char *topic = strtok_remaining();
    Channel *c;
    ChannelInfo *ci;

    if (!topic) {
	syntax_error(s_ChanServ, u, "TOPIC", CHAN_TOPIC_SYNTAX);
    } else if (!(c = get_channel(chan))) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_IN_USE, chan);
    } else if (c->bouncy_modes) {
	notice_lang(s_ChanServ, u, CHAN_BOUNCY_MODES, "TOPIC");
    } else if (!(ci = c->ci)) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_REGISTERED, chan);
    } else if (ci->flags & CI_VERBOTEN) {
	notice_lang(s_ChanServ, u, CHAN_X_FORBIDDEN, chan);
    } else if (!u || !check_access_cmd(u, ci, "TOPIC", NULL)) {
	notice_lang(s_ChanServ, u, PERMISSION_DENIED);
    } else {
	time_t now = time(NULL);
	set_topic(s_ChanServ, c, topic, u->nick, now);
	record_topic(ci, topic, u->nick, now);
    }
}

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

static void do_clear(User *u)
{
    char *chan = strtok(NULL, " ");
    char *what = strtok(NULL, " ");
    Channel *c;
    ChannelInfo *ci;

    if (!what) {
	syntax_error(s_ChanServ, u, "CLEAR", CHAN_CLEAR_SYNTAX);
    } else if (!(c = get_channel(chan))) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_IN_USE, chan);
    } else if (c->bouncy_modes) {
	notice_lang(s_ChanServ, u, CHAN_BOUNCY_MODES, "CLEAR");
    } else if (!(ci = c->ci)) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_REGISTERED, chan);
    } else if (ci->flags & CI_VERBOTEN) {
	notice_lang(s_ChanServ, u, CHAN_X_FORBIDDEN, chan);
    } else if (!u || !check_access_cmd(u, ci, "CLEAR", what)) {
	notice_lang(s_ChanServ, u, PERMISSION_DENIED);
    } else if (call_callback_3(module, cb_clear, u, c, what) > 0) {
	return;
    } else if (stricmp(what, "BANS") == 0) {
	clear_channel(c, CLEAR_BANS, NULL);
	notice_lang(s_ChanServ, u, CHAN_CLEARED_BANS, chan);
    } else if (stricmp(what, "MODES") == 0) {
	clear_channel(c, CLEAR_MODES, NULL);
	notice_lang(s_ChanServ, u, CHAN_CLEARED_MODES, chan);
    } else if (stricmp(what, "OPS") == 0) {
	clear_channel(c, CLEAR_UMODES, (void *)CUMODE_o);
	notice_lang(s_ChanServ, u, CHAN_CLEARED_OPS, chan);
    } else if (stricmp(what, "VOICES") == 0) {
	clear_channel(c, CLEAR_UMODES, (void *)CUMODE_v);
	notice_lang(s_ChanServ, u, CHAN_CLEARED_VOICES, chan);
    } else if (stricmp(what, "USERS") == 0) {
	char buf[BUFSIZE];
	snprintf(buf, sizeof(buf), "CLEAR USERS command from %s", u->nick);
	clear_channel(c, CLEAR_USERS, buf);
	notice_lang(s_ChanServ, u, CHAN_CLEARED_USERS, chan);
    } else {
	syntax_error(s_ChanServ, u, "CLEAR", CHAN_CLEAR_SYNTAX);
    }
}

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

static void do_status(User *u)
{
    ChannelInfo *ci;
    User *u2;
    char *nick, *chan;

    chan = strtok(NULL, " ");
    nick = strtok(NULL, " ");
    if (!nick || strtok(NULL, " ")) {
	notice(s_ChanServ, u->nick, "STATUS ? ? ERROR Syntax error");
	return;
    }
    if (!(ci = get_channelinfo(chan))) {
	char *temp = chan;
	chan = nick;
	nick = temp;
	ci = get_channelinfo(chan);
    }
    if (!ci) {
	notice(s_ChanServ, u->nick, "STATUS %s %s ERROR Channel not"
	       " registered", chan, nick);
    } else if (ci->flags & CI_VERBOTEN) {
	notice(s_ChanServ, u->nick, "STATUS %s %s ERROR Channel forbidden",
	       chan, nick);
    } else if (!is_services_admin(u)
	       && !check_access_cmd(u, ci, "STATUS", NULL)) {
	notice(s_ChanServ, u->nick, "STATUS %s %s ERROR Permission denied",
	       chan, nick);
    } else if ((u2 = get_user(nick)) != NULL) {
	int acc = get_access(u2, ci);
	int have_acclev = (find_module("chanserv/access-levels") != NULL);
	int have_accxop = (find_module("chanserv/access-xop") != NULL);
	char accbuf[BUFSIZE];

	if (have_accxop) {
	    const char *xop;
	    if (acc == ACCLEV_FOUNDER)
		xop = "Founder";
	    else if (acc >= ACCLEV_SOP)
		xop = "SOP";
	    else if (acc >= ACCLEV_AOP)
		xop = "AOP";
	    else if (acc >= ACCLEV_HOP && (protocol_features & PF_HALFOP))
		xop = "HOP";
	    else if (acc >= ACCLEV_VOP)
		xop = "VOP";
	    else
		xop = "---";
	    if (have_acclev)
		snprintf(accbuf, sizeof(accbuf), "%d (%s)", acc, xop);
	    else
		snprintf(accbuf, sizeof(accbuf), "%s", xop);
	} else {  /* access-levels only, or none */
	    snprintf(accbuf, sizeof(accbuf), "%d", acc);
	}
	notice(s_ChanServ, u->nick, "STATUS %s %s %s", chan, nick, accbuf);
    } else { /* !u2 */
	notice(s_ChanServ, u->nick, "STATUS %s %s ERROR Nick not online",
	       chan, nick);
    }
}

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

/* Assumes that permission checking has already been done. */

static void do_getpass(User *u)
{
    char *chan = strtok(NULL, " ");
    char pass[PASSMAX];
    ChannelInfo *ci;
    int i;

    if (!chan) {
	syntax_error(s_ChanServ, u, "GETPASS", CHAN_GETPASS_SYNTAX);
    } else if (!(ci = get_channelinfo(chan))) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_REGISTERED, chan);
    } else if (ci->flags & CI_VERBOTEN) {
	notice_lang(s_ChanServ, u, CHAN_X_FORBIDDEN, chan);
    } else if ((i = decrypt(ci->founderpass, pass, PASSMAX)) < 0) {
	module_log("decrypt() failed for GETPASS on %s", ci->name);
	notice_lang(s_ChanServ, u, INTERNAL_ERROR);
    } else if (i == 0) {
	notice_lang(s_ChanServ, u, CHAN_GETPASS_UNAVAILABLE);
    } else {
	module_log("%s!%s@%s used GETPASS on %s",
		   u->nick, u->username, u->host, ci->name);
	if (WallGetpass) {
	    wallops(s_ChanServ, "\2%s\2 used GETPASS on channel \2%s\2",
		    u->nick, chan);
	}
	notice_lang(s_ChanServ, u, CHAN_GETPASS_PASSWORD_IS,
		    chan, ci->founderpass);
    }
}

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

static void do_forbid(User *u)
{
    ChannelInfo *ci;
    char *chan = strtok(NULL, " ");

    /* Assumes that permission checking has already been done. */
    if (!chan || *chan != '#') {
	syntax_error(s_ChanServ, u, "FORBID", CHAN_FORBID_SYNTAX);
	return;
    } else if (strcmp(chan, "#") == 0) {
	notice_lang(s_ChanServ, u, CHAN_FORBID_SHORT_CHANNEL);
	return;
    }
    if (readonly)
	notice_lang(s_ChanServ, u, READ_ONLY_MODE);
    if ((ci = get_channelinfo(chan)) != NULL)
	delchan(ci);
    ci = makechan(chan);
    if (ci) {
	Channel *c;
	module_log("%s!%s@%s set FORBID for channel %s",
		   u->nick, u->username, u->host, ci->name);
	ci->flags |= CI_VERBOTEN;
	ci->time_registered = time(NULL);
	notice_lang(s_ChanServ, u, CHAN_FORBID_SUCCEEDED, chan);
	c = get_channel(chan);
	if (c) {
	    ci->c = c;  /* for completeness */
	    c->ci = ci;
	    clear_channel(c, CLEAR_USERS,
			  "Use of this channel has been forbidden");
	}
    } else {
	module_log("Valid FORBID for %s by %s!%s@%s failed",
		   ci->name, u->nick, u->username, u->host);
	notice_lang(s_ChanServ, u, CHAN_FORBID_FAILED, chan);
    }
}

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

static void do_suspend(User *u)
{
    ChannelInfo *ci;
    char *expiry, *chan, *reason;
    time_t expires;

    chan = strtok(NULL, " ");
    if (chan && *chan == '+') {
	expiry = chan+1;
	chan = strtok(NULL, " ");
    } else {
	expiry = NULL;
    }
    reason = strtok_remaining();

    if (!reason) {
	syntax_error(s_ChanServ, u, "SUSPEND", CHAN_SUSPEND_SYNTAX);
    } else if (!(ci = get_channelinfo(chan))) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_REGISTERED, chan);
    } else if (ci->flags & CI_VERBOTEN) {
	notice_lang(s_ChanServ, u, CHAN_X_FORBIDDEN, chan);
    } else if (ci->suspendinfo) {
	notice_lang(s_ChanServ, u, CHAN_SUSPEND_ALREADY_SUSPENDED, chan);
    } else {
	Channel *c;
	if (expiry)
	    expires = dotime(expiry);
	else
	    expires = CSSuspendExpire;
	if (expires < 0) {
	    notice_lang(s_ChanServ, u, BAD_EXPIRY_TIME);
	    return;
	} else if (expires > 0) {
	    expires += time(NULL);	/* Set an absolute time */
	}
	module_log("%s!%s@%s suspended %s",
		   u->nick, u->username, u->host, ci->name);
	suspend_channel(ci, reason, u->nick, expires);
	notice_lang(s_ChanServ, u, CHAN_SUSPEND_SUCCEEDED, chan);
	c = get_channel(chan);
	if (c)
	    clear_channel(c, CLEAR_USERS,
			  "Use of this channel has been forbidden");
	if (readonly)
	    notice_lang(s_ChanServ, u, READ_ONLY_MODE);
    }
}

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

static void do_unsuspend(User *u)
{
    ChannelInfo *ci;
    char *chan = strtok(NULL, " ");

    if (!chan) {
	syntax_error(s_ChanServ, u, "UNSUSPEND", CHAN_UNSUSPEND_SYNTAX);
    } else if (!(ci = get_channelinfo(chan))) {
	notice_lang(s_ChanServ, u, CHAN_X_NOT_REGISTERED, chan);
    } else if (ci->flags & CI_VERBOTEN) {
	notice_lang(s_ChanServ, u, CHAN_X_FORBIDDEN, chan);
    } else if (!ci->suspendinfo) {
	notice_lang(s_ChanServ, u, CHAN_UNSUSPEND_NOT_SUSPENDED, chan);
    } else {
	if (readonly)
	    notice_lang(s_ChanServ, u, READ_ONLY_MODE);
	module_log("%s!%s@%s unsuspended %s",
		   u->nick, u->username, u->host, ci->name);
	unsuspend_channel(ci, 1);
	notice_lang(s_ChanServ, u, CHAN_UNSUSPEND_SUCCEEDED, chan);
    }
}

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

const int32 module_version = MODULE_VERSION_CODE;

static int CSDefKeepTopic;
static int CSDefSecureOps;
static int CSDefPrivate;
static int CSDefTopicLock;
static int CSDefLeaveOps;
static int CSDefSecure;
static int CSDefOpNotice;
static int CSDefEnforce;
static int CSDefHideEmail;
static int CSDefHideTopic;
static int CSDefHideMlock;

ConfigDirective module_config[] = {
    { "ChanServDB",       { { CD_STRING, CF_DIRREQ, &ChanDBName } } },
    { "ChanServName",     { { CD_STRING, CF_DIRREQ, &s_ChanServ },
                            { CD_STRING, 0, &desc_ChanServ } } },
    { "CSAccessMax",      { { CD_POSINT, CF_DIRREQ, &CSAccessMax } } },
    { "CSAutokickMax",    { { CD_POSINT, CF_DIRREQ, &CSAutokickMax } } },
    { "CSAutokickReason", { { CD_STRING, CF_DIRREQ, &CSAutokickReason } } },
    { "CSDefEnforce",     { { CD_SET, 0, &CSDefEnforce } } },
    { "CSDefHideEmail",   { { CD_SET, 0, &CSDefHideEmail } } },
    { "CSDefHideMlock",   { { CD_SET, 0, &CSDefHideMlock } } },
    { "CSDefHideTopic",   { { CD_SET, 0, &CSDefHideTopic } } },
    { "CSDefKeepTopic",   { { CD_SET, 0, &CSDefKeepTopic } } },
    { "CSDefLeaveOps",    { { CD_SET, 0, &CSDefLeaveOps } } },
    { "CSDefOpNotice",    { { CD_SET, 0, &CSDefOpNotice } } },
    { "CSDefPrivate",     { { CD_SET, 0, &CSDefPrivate } } },
    { "CSDefSecure",      { { CD_SET, 0, &CSDefSecure } } },
    { "CSDefSecureOps",   { { CD_SET, 0, &CSDefSecureOps } } },
    { "CSDefTopicLock",   { { CD_SET, 0, &CSDefTopicLock } } },
    { "CSEnableRegister", { { CD_SET, 0, &CSEnableRegister } } },
    { "CSExpire",         { { CD_TIME, 0, &CSExpire } } },
    { "CSForbidShortChannel",{{CD_SET, 0, &CSForbidShortChannel } } },
    { "CSInhabit",        { { CD_TIME, CF_DIRREQ, &CSInhabit } } },
    { "CSListMax",        { { CD_POSINT, CF_DIRREQ, &CSListMax } } },
    { "CSListOpersOnly",  { { CD_SET, 0, &CSListOpersOnly } } },
    { "CSMaxReg",         { { CD_POSINT, 0, &CSMaxReg } } },
    { "CSRegisteredOnly", { { CD_SET, 0, &CSRegisteredOnly } } },
    { "CSRestrictDelay",  { { CD_TIME, 0, &CSRestrictDelay } } },
    { "CSShowPassword",   { { CD_SET, 0, &CSShowPassword } } },
    { "CSSuspendExpire",  { { CD_TIME, 0 , &CSSuspendExpire },
                            { CD_TIME, 0 , &CSSuspendGrace } } },
    { NULL }
};

/* Pointers to command records (for !CSEnableCommand) */
static Command *cmd_REGISTER;
static Command *cmd_GETPASS;

/* Previous value of clear_channel() sender */
static char old_clearchan_sender[NICKMAX];
static int old_clearchan_sender_set = 0;

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

static void handle_config(void)
{
    CSDefFlags = 0;
    if (CSDefKeepTopic)
	CSDefFlags |= CI_KEEPTOPIC;
    if (CSDefSecureOps)
	CSDefFlags |= CI_SECUREOPS;
    if (CSDefPrivate)
	CSDefFlags |= CI_PRIVATE;
    if (CSDefTopicLock)
	CSDefFlags |= CI_TOPICLOCK;
    if (CSDefLeaveOps)
	CSDefFlags |= CI_LEAVEOPS;
    if (CSDefSecure)
	CSDefFlags |= CI_SECURE;
    if (CSDefOpNotice)
	CSDefFlags |= CI_OPNOTICE;
    if (CSDefEnforce)
	CSDefFlags |= CI_ENFORCE;
    if (CSDefHideEmail)
	CSDefFlags |= CI_HIDE_EMAIL;
    if (CSDefHideTopic)
	CSDefFlags |= CI_HIDE_TOPIC;
    if (CSDefHideMlock)
	CSDefFlags |= CI_HIDE_MLOCK;

    if (CSMaxReg > MAX_CHANNELCOUNT) {
	module_log("CSMaxReg upper-bounded at MAX_CHANNELCOUNT (%d)",
		   MAX_CHANNELCOUNT);
	CSMaxReg = MAX_CHANNELCOUNT;
    }
}

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

static int do_reconfigure(int after_configure)
{
    static char old_s_ChanServ[NICKMAX];
    static char *old_desc_ChanServ = NULL;
    static char *old_ChanDBName    = NULL;
    static int old_CSEnableRegister;

    if (!after_configure) {
	/* Before reconfiguration: save old values. */
	strscpy(old_s_ChanServ, s_ChanServ, NICKMAX);
	old_desc_ChanServ    = strdup(desc_ChanServ);
	old_ChanDBName       = strdup(ChanDBName);
	old_CSEnableRegister = CSEnableRegister;
    } else {
	Command *cmd;
	/* After reconfiguration: handle value changes. */
	handle_config();
	if (strcmp(old_s_ChanServ, s_ChanServ) != 0) {
	    if (strcmp(set_clear_channel_sender(PTR_INVALID),old_s_ChanServ)==0)
		set_clear_channel_sender(s_ChanServ);
	    send_nickchange(old_s_ChanServ, s_ChanServ);
	}
	if (!old_desc_ChanServ || strcmp(old_desc_ChanServ,desc_ChanServ) != 0)
	    send_namechange(s_ChanServ, desc_ChanServ);
	if (!old_ChanDBName || strcmp(old_ChanDBName, ChanDBName) != 0) {
	    module_log("reconfigure: new database name will only take"
		       " effect after restart");
	    /* Restore the old database name */
	    free(ChanDBName);
	    ChanDBName = old_ChanDBName;
	    /* Make sure the old name isn't freed below */
	    old_ChanDBName = NULL;
	}
	free(old_desc_ChanServ);
	free(old_ChanDBName);
	if (CSEnableRegister && !old_CSEnableRegister) {
	    cmd_REGISTER->helpmsg_all = cmd_REGISTER->helpmsg_oper;
	    cmd_REGISTER->helpmsg_oper = -1;
	    cmd_REGISTER->has_priv = NULL;
	} else if (!CSEnableRegister && old_CSEnableRegister) {
	    cmd_REGISTER->has_priv = is_services_admin;
	    cmd_REGISTER->helpmsg_oper = cmd_REGISTER->helpmsg_all;
	    cmd_REGISTER->helpmsg_all = -1;
	}
	if (EnableGetpass)
	    cmd_GETPASS->name = "GETPASS";
	else
	    cmd_GETPASS->name = "";
	/* Update command help parameters */
	cmd_REGISTER->help_param1 = s_NickServ;
	if ((cmd = lookup_cmd(module, "SET SECURE")) != NULL) {
	    cmd->help_param1 = s_NickServ;
	    cmd->help_param2 = s_NickServ;
	}
    }  /* if (!after_configure) */
    return 0;
}

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

int init_module(Module *module_)
{
    Command *cmd;


    module = module_;

    handle_config();

    module_nickserv = find_module("nickserv/main");
    if (!module_nickserv) {
	module_log("NickServ main module not loaded");
	exit_module(0);
	return 0;
    }
    use_module(module_nickserv);

    if (!new_commandlist(module) || !register_commands(module, cmds)
	|| ((protocol_features & PF_HALFOP)
	    && !register_commands(module, cmds_halfop))
	|| ((protocol_features & PF_CHANPROT)
	    && !register_commands(module, cmds_chanprot))
    ) {
	module_log("Unable to register commands");
	exit_module(0);
	return 0;
    }

    cb_clear     = register_callback(module, "CLEAR");
    cb_command   = register_callback(module, "command");
    cb_help      = register_callback(module, "HELP");
    cb_help_cmds = register_callback(module, "HELP COMMANDS");
    cb_invite    = register_callback(module, "INVITE");
    cb_unban     = register_callback(module, "UNBAN");
    if (cb_command < 0 || cb_clear < 0 || cb_help < 0 || cb_help_cmds < 0
     || cb_invite < 0 || cb_unban < 0
    ) {
	module_log("Unable to register callbacks");
	exit_module(0);
	return 0;
    }

    cmd_REGISTER = lookup_cmd(module, "REGISTER");
    if (!cmd_REGISTER) {
	module_log("BUG: unable to find REGISTER command entry");
	exit_module(0);
	return 0;
    }
    cmd_REGISTER->help_param1 = s_NickServ;
    if (!CSEnableRegister) {
	cmd_REGISTER->has_priv = is_services_admin;
	cmd_REGISTER->helpmsg_oper = cmd_REGISTER->helpmsg_all;
	cmd_REGISTER->helpmsg_all = -1;
    }
    cmd_GETPASS = lookup_cmd(module, "GETPASS");
    if (!cmd_GETPASS) {
	module_log("BUG: unable to find GETPASS command entry");
	exit_module(0);
	return 0;
    }
    if (!EnableGetpass)
	cmd_GETPASS->name = "";
    cmd = lookup_cmd(module, "SET SECURE");
    if (cmd) {
	cmd->help_param1 = s_NickServ;
	cmd->help_param2 = s_NickServ;
    }
    cmd = lookup_cmd(module, "SET SUCCESSOR");
    if (cmd)
	cmd->help_param1 = (char *)(long)CSMaxReg;
    cmd = lookup_cmd(module, "SUSPEND");
    if (cmd)
	cmd->help_param1 = s_OperServ;

    if (!add_callback(NULL, "reconfigure", do_reconfigure)
     || !add_callback(NULL, "introduce_user", introduce_chanserv)
     || !add_callback(NULL, "m_privmsg", chanserv)
     || !add_callback(NULL, "m_whois", chanserv_whois)
     || !add_callback(NULL, "save data", do_save_data)
     || !add_callback(NULL, "channel create", do_channel_create)
     || !add_callback(NULL, "channel JOIN check", do_channel_join_check)
     || !add_callback(NULL, "channel JOIN", do_channel_join)
     || !add_callback(NULL, "channel PART", do_channel_part)
     || !add_callback(NULL, "channel delete", do_channel_delete)
     || !add_callback(NULL, "channel mode change", do_channel_mode_change)
     || !add_callback(NULL, "channel umode change", do_channel_umode_change)
     || !add_callback(NULL, "channel TOPIC", do_channel_topic)
     || !add_callback(module_nickserv, "REGISTER/LINK check", do_reglink_check)
     || !add_callback(module_nickserv, "identified", do_nick_identified)
     || !add_callback(module_nickserv, "nickgroup delete", do_nickgroup_delete)
    ) {
	module_log("Unable to add callbacks");
	exit_module(0);
	return 0;
    }

    if (!init_access(module) || !init_check(module) || !init_set(module)
     || !init_util(module)
    ) {
	exit_module(0);
	return 0;
    }

    open_channel_db(ChanDBName);
    db_opened = 1;

    if (linked)
	introduce_chanserv(NULL);

    strscpy(old_clearchan_sender, set_clear_channel_sender(s_ChanServ),
	    sizeof(old_clearchan_sender));
    old_clearchan_sender_set = 1;

    return 1;
}

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

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

    if (old_clearchan_sender_set) {
	set_clear_channel_sender(old_clearchan_sender);
	old_clearchan_sender_set = 0;
    }

    if (linked)
	send_cmd(s_ChanServ, "QUIT :");

    if (db_opened)
	close_channel_db(ChanDBName);

    exit_util();
    exit_set();
    exit_check();
    exit_access();

    remove_callback(NULL, "channel TOPIC", do_channel_topic);
    remove_callback(NULL, "channel umode change", do_channel_umode_change);
    remove_callback(NULL, "channel mode change", do_channel_mode_change);
    remove_callback(NULL, "channel delete", do_channel_delete);
    remove_callback(NULL, "channel PART", do_channel_part);
    remove_callback(NULL, "channel JOIN", do_channel_join);
    remove_callback(NULL, "channel JOIN check", do_channel_join_check);
    remove_callback(NULL, "channel create", do_channel_create);
    remove_callback(NULL, "save data", do_save_data);
    remove_callback(NULL, "m_whois", chanserv_whois);
    remove_callback(NULL, "m_privmsg", chanserv);
    remove_callback(NULL, "introduce_user", introduce_chanserv);
    remove_callback(NULL, "reconfigure", do_reconfigure);

    cmd_GETPASS->name = "GETPASS";
    if (!CSEnableRegister) {
	cmd_REGISTER->helpmsg_all = cmd_REGISTER->helpmsg_oper;
	cmd_REGISTER->helpmsg_oper = -1;
	cmd_REGISTER->has_priv = NULL;
    }

    unregister_callback(module, cb_unban);
    unregister_callback(module, cb_invite);
    unregister_callback(module, cb_help_cmds);
    unregister_callback(module, cb_help);
    unregister_callback(module, cb_command);
    unregister_callback(module, cb_clear);

    if (protocol_features & PF_CHANPROT)
	unregister_commands(module, cmds_chanprot);
    if (protocol_features & PF_HALFOP)
	unregister_commands(module, cmds_halfop);
    unregister_commands(module, cmds);
    del_commandlist(module);

    if (module_nickserv) {
	remove_callback(module_nickserv, "nickgroup delete",
			do_nickgroup_delete);
	remove_callback(module_nickserv, "identified", do_nick_identified);
	remove_callback(module_nickserv, "REGISTER/LINK check",
			do_reglink_check);
	unuse_module(module_nickserv);
	module_nickserv = NULL;
    }

    return 1;
}

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


syntax highlighted by Code2HTML, v. 0.9.1