/* Unreal protocol module for IRC Services.
 *
 * IRC Services is copyright (c) 1996-2009 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 "messages.h"
#include "timeout.h"
#include "modules/operserv/operserv.h"
#include "modules/operserv/maskdata.h"
#include "modules/nickserv/nickserv.h"
#include "modules/chanserv/chanserv.h"

#define UNREAL_HACK  /* For SJOIN; see comments in sjoin.c */
#include "banexcept.h"
#include "chanprot.h"
#include "halfop.h"
#include "invitemask.h"
#include "sjoin.h"
#include "svsnick.h"
#include "token.h"

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

/* Variable to hold the current module's pointer: */
static Module *module;

/* Variables for modules referenced by this one: */
static Module *module_operserv;
static Module *module_chanserv;


/* Variables set by configuration directives: */

/* Numeric to send, 0 for none */
static int32 ServerNumeric = 0;
/* Should we use TSCTL SVSTIME to set server timestamps? */
static int SetServerTimes = 0;
/* Frequency to send TSCTL SVSTIME at (0 = startup only) */
static time_t SVSTIMEFrequency = 0;
/* Timeout for sending SVSTIME */
static Timeout *to_svstime = NULL;


/* The following flag sets are set by init_modes().  We could have done
 * this using #defines as well, but this way is cleaner (there's no need
 * to make sure these values are synchronized with the list of modes
 * below). */
static int32 usermode_admin = 0;	/* +aANT */
static int32 usermode_secure = 0;	/* +z */
static int32 usermode_hiding = 0;	/* +I */
static int32 chanmode_admins_only = 0;	/* +A */
static int32 chanmode_secure_only = 0;	/* +z */
static int32 chanmode_no_hiding = 0;	/* +H */

/* This variable holds the version of the Unreal protocol the remote server
 * is using, extracted from its SERVER message. */
static int unreal_version = 0;

/* Does the remote server support NICKIP? */
static int has_nickip = 0;

/*************************************************************************/
/****************** Local interface to external symbols ******************/
/*************************************************************************/

/*
 * We import `s_ChanServ' from the "chanserv/main" module; this variable
 * holds a pointer to `s_ChanServ', obtained via get_module_symbol().  When
 * the ChanServ module is not loaded, this holds a pointer to `ServerName'
 * instead as a default value.
 *
 * Note that we use a #define here to substitute `*p_s_ChanServ' for
 * `s_ChanServ', so subsequent code can simply use `s_ChanServ' as if it
 * were declared normally.
 */

static char **p_s_ChanServ = &ServerName;
#define s_ChanServ (*p_s_ChanServ)

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

/*
 * We also import `is_services_admin' from the "operserv/main" module.
 * As with `s_ChanServ', the value is obtained via get_module_symbol(),
 * and defaults to NULL when the module is not loaded.  We access the
 * function via a wrapper (local_is_services_admin()), which checks
 * whether the function pointer is non-NULL before calling it, and returns
 * a default value if the pointer is NULL (in this case, if the OperServ
 * module is not loaded then we assume no one is a Services admin and
 * return false).
 *
 * Here, too, we use a #define to substitute `local_is_services_admin' for
 * `is_services_admin', so we can use the latter as if it were declared
 * normally.
 */

static int (*p_is_services_admin)(User *u) = NULL;

static int local_is_services_admin(User *u)
{
    return p_is_services_admin && (*p_is_services_admin)(u);
}
#define is_services_admin local_is_services_admin

/*************************************************************************/
/************************** User/channel modes ***************************/
/*************************************************************************/

/*
 * Here we define modes specific to this IRC protocol.  To simplify the
 * initialization code, we define each category of modes (user modes,
 * channel modes, and channel user modes) using the structure below, which
 * holds a mode character (typed as uint8 because it is used as an array
 * index) and a ModeData structure (defined in modes.h).
 */

struct modedata_init {
    uint8 mode;
    ModeData data;
};

/*
 * We also define new MI_ flags for certain modes; these are used by the
 * initialization code to set up the usermode_* and chanmode_* variables
 * above.  These flags must use only the bits defined in modes.h for local
 * use (MI_LOCAL_FLAGS, 0xFF000000).
 *
 * Note that the user mode flags and channel mode flags overlap; this is
 * okay, since they will never be used together.
 */

#define MI_ADMIN	0x01000000  /* Usermode given to admins */
#define MI_SECURE	0x02000000  /* Usermode for users on secure conn's */
#define MI_HIDING	0x04000000  /* Usermode for hiding users */
#define MI_ADMINS_ONLY	0x01000000  /* Chanmode for admin-only channels */
#define MI_SECURE_ONLY	0x02000000  /* Chanmode for secure-user-only chans */
#define MI_NO_HIDING	0x04000000  /* Chanmode for no-hiding channels */

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

/* New user modes: */

static const struct modedata_init new_usermodes[] = {
    {'g', {0x00000008}},	/* Receive globops */
    {'h', {0x00000010}},	/* Helpop */
    {'r', {0x00000020,0,0,0,MI_REGISTERED}},
				/* Registered nick */
    {'a', {0x00000040,0,0,0,MI_ADMIN}},
				/* Services admin */
    {'A', {0x00000080,0,0,0,MI_ADMIN}},
				/* Server admin */
    {'N', {0x00000100,0,0,0,MI_ADMIN}},
				/* Network admin */
    {'T', {0x00000200,0,0,0,MI_ADMIN}},
				/* Technical admin */
    {'x', {0x00000400}},	/* Mask hostname */
    {'S', {0x00000800}},	/* Services client */
    {'q', {0x00001000}},	/* Cannot be kicked from chans */
    {'I', {0x00002000,0,0,0,MI_HIDING}},
				/* Completely invisible (joins etc) */
    {'d', {0x00004000}},	/* Deaf */
    {'z', {0x00008000,0,0,0,MI_SECURE}},
				/* Using secure connection */
};

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

/*
 * New channel modes.  Unlike user modes and channel user modes, these are
 * used in database files (for channel mode locks), and thus should be as
 * portable as possible between different protocols.  To that end, any new
 * protocol should, when possible, use the same flag as existing protocols
 * for the same mode (or a different mode letter with the same function),
 * and should avoid using any flags used by other protocols for modes that
 * are peculiar to that protocol.
 */

static const struct modedata_init new_chanmodes[] = {
    {'R', {0x00000100,0,0,0,MI_REGNICKS_ONLY}},
				/* Only identified users can join */
    {'r', {0x00000200,0,0,0,MI_REGISTERED}},
				/* Set for all registered channels */
    {'c', {0x00000400,0,0}},	/* No ANSI colors in channel */
    {'O', {0x00000800,0,0,0,MI_OPERS_ONLY}},
				/* Only opers can join channel */
    {'A', {0x00001000,0,0,0,MI_OPERS_ONLY|MI_ADMINS_ONLY}},
				/* Only admins can join channel */
    {'z', {0x00002000,0,0,0,MI_SECURE_ONLY}},
				/* Only secure (+z) users allowed */
    {'Q', {0x00004000,0,0}},	/* No kicks */
    {'K', {0x00008000,0,0}},	/* No knocks */
    {'V', {0x00010000,0,0}},	/* No invites */
    {'H', {0x00020000,0,0,0,MI_NO_HIDING}},
				/* No hiding (+I) users */
    {'C', {0x00040000,0,0}},	/* No CTCPs */
    {'N', {0x00080000,0,0}},	/* No nick changing */
    {'S', {0x00100000,0,0}},	/* Strip colors */
    {'G', {0x00200000,0,0}},	/* Strip "bad words" */
    {'u', {0x00400000,0,0}},	/* Auditorium mode */
    {'f', {0x00800000,1,0}},	/* Flood limit */
    {'L', {0x01000000,1,0}},	/* Channel link */
    {'M', {0x02000000,0,0}},	/* Moderated to unregged nicks */
    {'T', {0x04000000,0,0}},	/* Disable notices to channel */
    {'j', {0x08000000,1,0}},	/* Join throttling */
    {'e', {0x80000000,1,1,0,MI_MULTIPLE}}, /* Ban exceptions */
    {'I', {0x80000000,1,1,0,MI_MULTIPLE}}, /* INVITE hosts */
};

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

/* New channel user modes: */

static const struct modedata_init new_chanusermodes[] = {
    {'h', {0x00000004,1,1,'%'}},	/* Half-op */
    {'a', {0x00000008,1,1,'~'}},	/* Protected (no kick or deop by +o) */
    {'q', {0x00000010,1,1,'*',MI_CHANOWNER}},
					/* Channel owner */
};

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

/*
 * This routine inserts the data listed above into the global usermodes[],
 * chanmodes[], and chanusermodes[] arrays, initializes the local mode
 * variables (usermode_* and chanmode_*), and calls mode_setup().
 */

static void init_modes(void)
{
    int i;

    for (i = 0; i < lenof(new_usermodes); i++) {
	usermodes[new_usermodes[i].mode] = new_usermodes[i].data;
	if (new_usermodes[i].data.info & MI_ADMIN)
	    usermode_admin |= new_usermodes[i].data.flag;
	if (new_usermodes[i].data.info & MI_SECURE)
	    usermode_secure |= new_usermodes[i].data.flag;
	if (new_usermodes[i].data.info & MI_HIDING)
	    usermode_hiding |= new_usermodes[i].data.flag;
    }
    for (i = 0; i < lenof(new_chanmodes); i++) {
	chanmodes[new_chanmodes[i].mode] = new_chanmodes[i].data;
	if (new_chanmodes[i].data.info & MI_ADMINS_ONLY)
	    chanmode_admins_only |= new_chanmodes[i].data.flag;
	if (new_chanmodes[i].data.info & MI_SECURE_ONLY)
	    chanmode_secure_only |= new_chanmodes[i].data.flag;
	if (new_chanmodes[i].data.info & MI_NO_HIDING)
	    chanmode_no_hiding |= new_chanmodes[i].data.flag;
    }
    for (i = 0; i < lenof(new_chanusermodes); i++)
	chanusermodes[new_chanusermodes[i].mode] = new_chanusermodes[i].data;

    mode_setup();
};

/*************************************************************************/
/************************* IRC message receiving *************************/
/*************************************************************************/

/* Handler for the NICK message. */

static void m_nick(char *source, int ac, char **av)
{
    char *ipaddr;
    char *newav[10];

#ifdef CLEAN_COMPILE
    ipaddr = NULL;
#endif

    if (*source) {
	/* Old user changing nicks. */
	if (ac != 2) {
	    if (debug)
		module_log("debug: NICK message: wrong number of parameters"
			   " (%d) for source `%s'", ac, source);
	} else {
	    do_nick(source, ac, av);
	}
	return;
    }

    /*
     * New user.  Unreal's NICK message takes 10 parameters (11 for servers
     * supporting NICKIP):
     *
     *     NICK <nick> <hopcount> <timestamp> <username> <hostname>
     *          <server> <servicestamp> <modes> <fakehost> [<IP>] :<realname>
     *
     * Processing consists of:
     *     - Making sure the message has the right number of parameters
     *     - Rearranging parameters for use by do_nick()
     *     - Calling do_nick()
     *     - If do_nick() was successful (i.e. the user was accepted),
     *           setting the modes given in the NICK message for the user
     *
     * Previous versions of Services used to also check whether the
     * Services stamp sent by the remote server was zero, and assign a new
     * stamp if so; this is now handled by do_nick() in users.c, with the
     * MODE +d in do_user_servicestamp_change(), so we just pass along what
     * we got from the server for now.
     */

    if (ac != (has_nickip ? 11 : 10)) {
	if (debug)
	    module_log("debug: NICK message: wrong number of parameters (%d)"
		       " for new user", ac);
	return;
    }

    /* If this is a NICKIP server, decode and save the IP address, then
     * strip it out of the parameter list so that the code below sees a
     * standard set of parameters regardless of NICKIP support. */
    if (has_nickip) {
	if (strcmp(av[9],"*") == 0) {
	    ipaddr = "0.0.0.0";
	} else {
	    unsigned char tmpbuf[16];
	    const char *badip = NULL;  /* error message, or NULL if no error */
	    int len = decode_base64(av[9], tmpbuf, sizeof(tmpbuf));
	    if (len < 0 || len > sizeof(tmpbuf)) {
		badip = "Corrupt IP address";
	    } else if (len == 4) {
		ipaddr = unpack_ip(tmpbuf);
		if (!ipaddr)
		    badip = "Internal error decoding IPv4 address";
	    } else if (len == 16) {
		ipaddr = unpack_ip6(tmpbuf);
		if (!ipaddr)
		    badip = "Internal error decoding IPv6 address";
	    } else {
		badip = "Unrecognized IP address format";
	    }
	    if (badip) {
		static int warned_no_nickip = 0;
		if (!warned_no_nickip) {
		    wallops(NULL, "\2WARNING\2: %s for new nick %s", badip,
			    av[0]);
		    warned_no_nickip = 1;
		}
		module_log("WARNING: %s for new nick %s", badip, av[0]);
		ipaddr = "0.0.0.0";
	    }
	    av[9] = av[10];
	}
    } else {
	ipaddr = NULL;
    }

    /* Set up parameter list for do_nick():
     *    (current)     (desired)
     * 0: nick          nick
     * 1: hopcount      hopcount
     * 2: timestamp     timestamp
     * 3: username      username
     * 4: hostname      hostname
     * 5: server        server
     * 6: servicestamp  realname
     * 7: modes         servicestamp
     * 8: fakehost      ipaddr
     * 9: realname      fakehost
     */
    newav[0] = av[0];
    newav[1] = av[1];
    newav[2] = av[2];
    newav[3] = av[3];
    newav[4] = av[4];
    newav[5] = av[5];
    newav[6] = av[9];
    newav[7] = av[6];
    newav[8] = ipaddr;
    newav[9] = av[8];

    if (do_nick(source, 10, newav)) {
	/* The user was accepted; set modes. */
	newav[1] = av[7];
	do_umode(newav[0], 2, newav);
    }
}

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

/* PROTOCTL message handler. */

static void m_protoctl(char *source, int ac, char **av)
{
    static int got_nickv2 = 0;
    int i;

    for (i = 0; i < ac; i++) {
	if (stricmp(av[i], "NICKv2") == 0)
	    got_nickv2 = 1;
	if (stricmp(av[i], "NOQUIT") == 0)
	    protocol_features |= PF_NOQUIT;
	if (stricmp(av[i], "NICKIP") == 0)
	    has_nickip = 1;
    }
    /* Make sure we got a NICKv2 from the remote server; if not, abort. */
    if (!got_nickv2) {
	send_error("Need NICKv2 protocol");
	strscpy(quitmsg, "Remote server doesn't support NICKv2",
		sizeof(quitmsg));
	quitting = 1;
    }
}

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

/* UMODE2 (set user modes) message handler. */

static void m_umode2(char *source, int ac, char **av)
{
    char *new_av[2];

    if (ac != 1)
	return;
    new_av[0] = source;
    new_av[1] = av[0];
    do_umode(source, 2, new_av);
}

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

/* SETHOST/CHGHOST message handlers. */

static void m_sethost(char *source, int ac, char **av)
{
    User *u;

    if (ac != 1)
	return;
    u = get_user(source);
    if (!u) {
	module_log("m_sethost: user record for %s not found", source);
	return;
    }
    free(u->fakehost);
    u->fakehost = sstrdup(av[0]);
}

static void m_chghost(char *source, int ac, char **av)
{
    if (ac == 2) {
	if (debug) {
	    module_log("debug: m_chghost from %s calling m_sethost for %s",
		       source, av[0]);
	}
	m_sethost(av[0], ac-1, av+1);
    }
}

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

/* SETIDENT/CHGIDENT message handlers. */

static void m_setident(char *source, int ac, char **av)
{
    User *u;

    if (ac != 1)
	return;
    u = get_user(source);
    if (!u) {
	module_log("m_setident: user record for %s not found", source);
	return;
    }
    free(u->username);
    u->username = sstrdup(av[0]);
}

static void m_chgident(char *source, int ac, char **av)
{
    if (ac == 2) {
	if (debug) {
	    module_log("debug: m_chgident from %s calling m_setident for %s",
		       source, av[0]);
	}
	m_setident(av[0], ac-1, av+1);
    }
}

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

/* SETNAME/CHGNAME message handlers. */

static void m_setname(char *source, int ac, char **av)
{
    User *u;

    if (ac != 1)
	return;
    u = get_user(source);
    if (!u) {
	module_log("m_setname: user record for %s not found", source);
	return;
    }
    free(u->realname);
    u->realname = sstrdup(av[0]);
}

static void m_chgname(char *source, int ac, char **av)
{
    if (ac == 2) {
	if (debug) {
	    module_log("debug: m_chgname from %s calling m_setname for %s",
		       source, av[0]);
	}
	m_setname(av[0], ac-1, av+1);
    }
}

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

/* SJOIN message handler.  The actual code is in sjoin.c (note that we use
 * UNREAL_HACK, so the routine actually called is do_sjoin_unreal()).
 */

static void m_sjoin(char *source, int ac, char **av)
{
    if (ac < 3) {
	if (debug)
	    module_log("debug: SJOIN: expected >=3 params, got %d", ac);
	return;
    }
    do_sjoin(source, ac, av);
}

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

/* SVSMODE message handler. */

static void m_svsmode(char *source, int ac, char **av)
{
    if (*av[0] == '#') {
	if (ac >= 3 && strcmp(av[1],"-b") == 0) {
	    Channel *c = get_channel(av[0]);
	    User *u = get_user(av[2]);
	    if (c && u)
		clear_channel(c, CLEAR_BANS, u);
	} else {
	    module_log("Invalid SVS[2]MODE from %s for channel %s: %s",
		       source, av[0], merge_args(ac-1,av+1));
	}
    } else if (*av[0] == '&') {
	module_log("SVS[2]MODE from %s for invalid target (channel %s): %s",
		   source, av[0], merge_args(ac-1,av+1));
    } else {
	if (ac < 2)
	    return;
	do_umode(source, ac, av);
    }
}

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

/* TKL message handler.  Format of a TKL message:
 *    TKL + type user host set-by expire-time set-time reason 
 * or TKL - type user host removed-by
 */

static void m_tkl(char *source, int ac, char **av)
{
    /* If we get a TKL + that claims to have been set by Services, send a
     * TKL - for the same mask to remove it.  This is needed in the case
     * that a TKL is removed by Services while one or more servers are
     * split; in such a case, the split server would, when it relinked,
     * propogate the TKL back out across the network even though it was no
     * longer "valid" from Services' perspective.  Any TKLs that really are
     * valid will be re-added as soon as a user matches them.
     *
     * As a special case, when we receive a TKL + Z, we first check whether
     * such a TKL exists in the SZline list, and do not remove it if it's
     * found; Unreal does not report IP address information for users, so
     * we have no way to re-add the masks by checking connecting users.
     *
     * FIXME: Unreal 3.2 converts an SQLINE into a TKL and sends it back to
     * us!  Previous Unreal releases don't understand TKL Q, so for now
     * check these against the SQline list.
     */

    /* Pointer to the `get_maskdata' function defined in the database
     * module.  It would be more efficient to store this statically, but
     * doing so would require watching for the database module to be
     * unloaded, and as we should not normally receive many TKL messages
     * anyway, the additional CPU burden is negligible. */
    typeof(get_maskdata) *p_get_maskdata = NULL;

    if (ac < 5 || *av[0] != '+' || strcmp(av[4],ServerName) != 0)
	return;
    if (!(p_get_maskdata = get_module_symbol(NULL, "get_maskdata")))
	return;
    if (*av[1] == 'Z' && (*p_get_maskdata)(MD_SZLINE, av[3]))
	return;
    if (*av[1] == 'Q' && (*p_get_maskdata)(MD_SQLINE, av[3]))
	return;
    send_cmd(ServerName, "TKL - %c %s %s %s",
	     *av[1], av[2], av[3], ServerName);
}

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

/* List of all Unreal-specific messages (handlers defined above). */

static Message unreal_messages[] = {
    { "AKILL",     NULL },
    { "CHGHOST",   m_chghost },
    { "CHGIDENT",  m_chgident },
    { "CHGNAME",   m_chgname },
    { "EOS",       NULL },
    { "GLOBOPS",   NULL },
    { "GNOTICE",   NULL },
    { "GOPER",     NULL },
    { "HELP",      NULL },
    { "NETINFO",   NULL },
    { "NICK",      m_nick },
    { "PROTOCTL",  m_protoctl },
    { "RAKILL",    NULL },
    { "SENDSNO",   NULL },
    { "SETHOST",   m_sethost },
    { "SETIDENT",  m_setident },
    { "SETNAME",   m_setname },
    { "SILENCE",   NULL },
    { "SJOIN",     m_sjoin },
    { "SMO",       NULL },
    { "SQLINE",	   NULL },
    { "SVSMODE",   m_svsmode },
    { "SVS2MODE",  m_svsmode },
    { "SWHOIS",	   NULL },
    { "TKL",       m_tkl },
    { "UMODE2",    m_umode2 },
    { NULL }
};

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

/* List of all Unreal tokens (passed to init_token()). */

static TokenInfo tokens[] = {
    /* 0x21 */
    { "!",  "PRIVMSG" },
    { "\"", "WHO" },
    { "#",  "WHOIS" },
    { "$",  "WHOWAS" },
    { "%",  "USER" },
    { "&",  "NICK" },
    { "'",  "SERVER" },
    { "(",  "LIST" },
    { ")",  "TOPIC" },
    { "*",  "INVITE" },
    { "+",  "VERSION" },
    { ",",  "QUIT" },
    { "-",  "SQUIT" },
    { ".",  "KILL" },
    { "/",  "INFO" },

    /* 0x30 */
    { "0",  "LINKS" },
    { "1",  "SUMMON" },
    { "2",  "STATS" },
    { "3",  "USERS" },
    { "4",  "HELP" },
    { "5",  "ERROR" },
    { "6",  "AWAY" },
    { "7",  "CONNECT" },
    { "8",  "PING" },
    { "9",  "PONG" },
    { ":",  NULL },
    { ";",  "OPER" },
    { "<",  "PASS" },
    { "=",  "WALLOPS" },
    { ">",  "TIME" },
    { "?",  "NAMES" },

    /* 0x40 */
    { "@",  "ADMIN" },
    { "A",  NULL },
    { "B",  "NOTICE" },
    { "C",  "JOIN" },
    { "D",  "PART" },
    { "E",  "LUSERS" },
    { "F",  "MOTD" },
    { "G",  "MODE" },
    { "H",  "KICK" },
    { "I",  "SERVICE" },
    { "J",  "USERHOST" },
    { "K",  "ISON" },
    { "L",  NULL },
    { "M",  NULL },
    { "N",  NULL },
    { "O",  "REHASH" },

    /* 0x50 */
    { "P",  "RESTART" },
    { "Q",  "CLOSE" },
    { "R",  "DIE" },
    { "S",  "HASH" },
    { "T",  "DNS" },
    { "U",  "SILENCE" },
    { "V",  "AKILL" },
    { "W",  "KLINE" },
    { "X",  "UNKLINE" },
    { "Y",  "RAKILL" },
    { "Z",  "GNOTICE" },
    { "[",  "GOPER" },
    { "\\", NULL },
    { "]",  "GLOBOPS" },
    { "^",  "LOCOPS" },
    { "_",  "PROTOCTL" },

    /* 0x60 */
    { "`",  "WATCH" },
    { "a",  NULL },
    { "b",  "TRACE" },
    { "c",  "SQLINE" },
    { "d",  "UNSQLINE" },
    { "e",  "SVSNICK" },
    { "f",  "SVSNOOP" },
    { "g",  "IDENTIFY" },
    { "h",  "SVSKILL" },
    { "i",  "NICKSERV" },
    { "j",  "CHANSERV" },
    { "k",  "OPERSERV" },
    { "l",  "MEMOSERV" },
    { "m",  "SERVICES" },
    { "n",  "SVSMODE" },
    { "o",  "SAMODE" },

    /* 0x70 */
    { "p",  "CHATOPS" },
    { "q",  "ZLINE" },
    { "r",  "UNZLINE" },
    { "s",  "HELPSERV" },
    { "t",  "RULES" },
    { "u",  "MAP" },
    { "v",  "SVS2MODE" },
    { "w",  "DALINFO" },
    { "x",  "ADCHAT" },
    { "y",  "MKPASSWD" },
    { "z",  "ADDLINE" },
    { "{",   NULL },
    { "|",  "UMODE2" },
    { "}",  "GLINE" },
    { "~",  "SJOIN" },

    /* 0x41xx */
    { "AA", "SETHOST" },
    { "AB", "TECHAT" },
    { "AC", "NACHAT" },
    { "AD", "SETIDENT" },
    { "AE", "SETNAME" },
    { "AF", "LAG" },
    { "AG", "SDESC" },
    { "AH", "STATSERV" },
    { "AI", "KNOCK" },
    { "AJ", "CREDITS" },
    { "AK", "LICENSE" },
    { "AL", "CHGHOST" },
    { "AM", "RPING" },
    { "AN", "RPONG" },
    { "AO", "NETINFO" },
    { "AP", "SENDUMODE" },
    { "AQ", "ADDMOTD" },
    { "AR", "ADDOMOTD" },
    { "AS", "SVSMOTD" },
    { "AT", NULL },
    { "AU", "SMO" },
    { "AV", "OPERMOTD" },
    { "AW", "TSCTL" },
    { "AX", "SAJOIN" },
    { "AY", "SAPART" },
    { "AZ", "CHGIDENT" },

    /* 0x42xx */
    { "BA", "SWHOIS" },
    { "BB", "SVSO" },
    { "BC", "SVSFLINE" },
    { "BD", "TKL" },
    { "BE", "VHOST" },
    { "BF", "BOTMOTD" },
    { "BG", "REMGLINE" },
    { "BH", "HTM" },
    { "BI", "DCCDENY" },
    { "BJ", "UNDCCDENY" },
    { "BK", "CHGNAME" },  /* also SVSNAME */
    { "BL", "SHUN" },
    { "BM", NULL },
    { "BN", "POST" },
    { "BO", "INFOSERV" },
    { "BP", "CYCLE" },
    { "BQ", "MODULE" },
    { "BR", "SVSJOIN" },
    { "BS", "BOTSERV" },
    { "BT", "SVSPART" },

    /* 0x45xx */
    { "ES", "EOS" },

    /* 0x53xx */
    { "Ss", "SENDSNO" },

    /* Fencepost */
    { NULL }
};

/*************************************************************************/
/************************** IRC message sending **************************/
/*************************************************************************/

/* Send a TSCTL SVSTIME command (internal use). */

static void do_send_svstime(Timeout *to)
{
#ifdef CLEAN_COMPILE
    to = to;
#endif
    send_cmd(ServerName, "TSCTL SVSTIME %ld", (long)time(NULL));
}

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

/* Send a NICK command for a new user. */

static void do_send_nick(const char *nick, const char *user, const char *host,
			 const char *server, const char *name,
			 const char *modes)
{
    /* NICK <nick> <hops> <TS> <user> <host> <server> <svsid> <mode>
     *      <fakehost> [<IP>] :<ircname>
     * The <IP> parameter is only included if the remote server specified
     * NICKIP in its PROTOCTL line.
     *
     * Note that if SETHOST has not been used, <fakehost> is <host> for VHP
     * servers but "*" for others.  We send <host> because that works in
     * both cases.
     */
    send_cmd(NULL, "NICK %s 1 %ld %s %s %s 0 +%s %s%s :%s", nick,
	     (long)time(NULL), user, host, server, modes, host,
	     has_nickip ? " *" : "", name);
}

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

/* Send a NICK command to change an existing user's nick. */

static void do_send_nickchange(const char *nick, const char *newnick)
{
    send_cmd(nick, "NICK %s %ld", newnick, (long)time(NULL));
}

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

/* Send a command to change a user's "real name". */

static void do_send_namechange(const char *nick, const char *newname)
{
    send_cmd(nick, "SETNAME :%s", newname);
}

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

/* Send a SERVER command, and anything else needed at the beginning of the
 * connection.
 */

static void do_send_server(void)
{
    send_cmd(NULL, "PROTOCTL SJOIN SJOIN2 SJ3 NICKv2 VHP VL NOQUIT UMODE2"
	     " TOKEN NICKIP");
    send_cmd(NULL, "PASS :%s", RemotePassword);
    /* Syntax:
     *     SERVER servername hopcount :U<protocol>-flags-numeric serverdesc
     * We use protocol 0, as we are protocol independent, and flags are *,
     * to prevent matching with version denying lines. */
    send_cmd(NULL, "SERVER %s 1 :U0-*-%d %s", ServerName, ServerNumeric,
	     ServerDesc);
    /* If requested, send TSCTL SVSTIME and start timeout. */
    if (SetServerTimes) {
	do_send_svstime(NULL);
	if (SVSTIMEFrequency)
	    to_svstime = add_timeout(SVSTIMEFrequency, do_send_svstime, 1);
    }
}

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

/* Send a SERVER command for a remote (juped) server. */

static void do_send_server_remote(const char *server, const char *reason)
{
    send_cmd(NULL, "SERVER %s 2 :%s", server, reason);
}

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

/* Send a WALLOPS (really a GLOBOPS). */

static void do_wallops(const char *source, const char *fmt, ...)
{
    va_list args;
    char buf[BUFSIZE];

    va_start(args, fmt);
    vsnprintf(buf, sizeof(buf), fmt, args);
    va_end(args);
    send_cmd(source ? source : ServerName, "GLOBOPS :%s", buf);
}

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

/* Send a NOTICE to all users on the network. */

static void do_notice_all(const char *source, const char *fmt, ...)
{
    va_list args;
    char buf[BUFSIZE];

    va_start(args, fmt);
    snprintf(buf, sizeof(buf), "NOTICE $* :%s", fmt);
    vsend_cmd(source, buf, args);
    va_end(args);
}

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

/* Send a command which modifies channel status. */

static void do_send_channel_cmd(const char *source, const char *fmt, ...)
{
    va_list args;

    va_start(args, fmt);
    vsend_cmd(source, fmt, args);
    va_end(args);
}

/*************************************************************************/
/******************************* Callbacks *******************************/
/*************************************************************************/

/* Callback for messages received from the uplink server; we use this to
 * check for inappropriate servicestamp changes and reverse them.
 * Normally, this would be done with a user mode change callback instead,
 * but Unreal uses the +d mode without a parameter to mean "deaf", so we
 * have to check here for +d with a parameter.  We also watch for the
 * SERVER message from the uplink server to determine what protocol
 * version is in use.
 */

static int do_receive_message(char *source, char *cmd, int ac, char **av)
{
    if (((stricmp(cmd, "MODE") == 0 || strcmp(cmd, "G") == 0)
	 && ac > 2 && *av[0] != '#' && strchr(av[1],'d'))
     || ((stricmp(cmd, "UMODE2") == 0 || strcmp(cmd, "|") == 0)
	 && ac > 1 && strchr(av[0],'d'))
    ) {
	User *user = get_user(*cmd=='U' ? source : av[0]);
	if (user) {
	    module_log("%s tried to change services stamp for %s",
		       cmd, user->nick);
	    send_cmd(ServerName, "SVSMODE %s +d %u", user->nick,
		     user->servicestamp);
	}
    } else if (stricmp(cmd, "SERVER") == 0) {
	const char *s, *t;
	int ver;
	if (ac < 3) {
	    module_log("SERVER: not enough parameters");
	} else if (*av[2] != 'U'
		   || !(s = strchr(av[2], '-'))
		   || (ver = strtoul(av[2]+1, (char **)&t, 10), t != s)
	) {
	    module_log("SERVER: bad/missing protocol ID");
	} else {
	    /* Successfully parsed */
	    unreal_version = ver;
	}
    }
    return 0;
}

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

/*
 * Callback for new users; we copy the fake hostname parameter from the
 * user argument area to the User structure
 */

static int do_user_create(User *user, int ac, char **av)
{
    user->fakehost = sstrdup(av[9]);
    return 0;
}

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

/*
 * Callback for users who get assigned new Services stamps: send the change
 * out to the network (MODE +d <stamp>).
 */

static int do_user_servicestamp_change(User *user)
{
    send_cmd(ServerName, "SVSMODE %s +d %u", user->nick, user->servicestamp);
    return 0;
}

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

/*
 * User mode change callback; we check for inappropriate mode r/a changes
 * and reverse them, and give newly-opered identified Services admins +a.
 */

static int do_user_mode(User *user, int modechar, int add, char **av)
{
    switch (modechar) {
      case 'o':
	if (add) {
	    /* Since is_services_admin() returns true only for opered
	     * users, and +o is not yet actually set in the User structure,
	     * we have to set it temporarily to get the information we
	     * want.  We clear it again afterwards because another callback
	     * may be expecting it to still be unset. */
	    user->mode |= UMODE_o;
	    if (user_identified(user) && is_services_admin(user))
		send_cmd(ServerName, "SVSMODE %s +a", user->nick);
	    user->mode &= ~UMODE_o;
	}
	return 0;

      case 'r':
	if (user_identified(user)) {
	    if (!add)
		send_cmd(ServerName, "SVSMODE %s +r", user->nick);
	} else {
	    if (add)
		send_cmd(ServerName, "SVSMODE %s -r", user->nick);
	}
	return 1;

      case 'a':
	if (is_oper(user)) {
	    if (is_services_admin(user)) {
		if (!add)
		    send_cmd(ServerName, "SVSMODE %s +a", user->nick);
	    } else {
		if (add)
		    send_cmd(ServerName, "SVSMODE %s -a", user->nick);
	    }
	    return 1;
	}
    }
    return 0;
}

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

/*
 * Channel mode change callback; we handle modes L/f/j here.
 */

static int do_channel_mode(const char *source, Channel *channel,
			   int modechar, int add, char **av)
{
    int32 flag = mode_char_to_flag(modechar, MODE_CHANNEL);

    switch (modechar) {

      case 'L':
	free(channel->link);
	if (add) {
	    channel->mode |= flag;
	    channel->link = sstrdup(av[0]);
	} else {
	    channel->mode &= ~flag;
	    channel->link = NULL;
	}
	return 1;

      case 'f':
	free(channel->flood);
	if (add) {
	    channel->mode |= flag;
	    channel->flood = sstrdup(av[0]);
	} else {
	    channel->mode &= ~flag;
	    channel->flood = NULL;
	}
	return 1;

      case 'j':
        if (add) {
	    int ok = 0;
	    char *s;
	    int joinrate1 = strtol(av[0], &s, 0);
	    if (*s == ':') {
		int joinrate2 = strtol(s+1, &s, 0);
		if (!*s) {
		    if (joinrate1 && joinrate2) {
			channel->mode |= flag;
			channel->joinrate1 = joinrate1;
			channel->joinrate2 = joinrate2;
		    } else {
			channel->mode &= ~flag;
			channel->joinrate1 = 0;
			channel->joinrate2 = 0;
			ok = 1;
		    }
		    ok = 1;
		}
	    } else if (joinrate1 == 0) {
		channel->mode &= ~flag;
		channel->joinrate1 = 0;
		channel->joinrate2 = 0;
		ok = 1;
	    }
	    if (!ok) {
		module_log("warning: invalid MODE +j %s for %s", av[0],
			   channel->name);
	    }
        } else {
            channel->mode &= ~flag;
            channel->joinrate1 = 0;
            channel->joinrate2 = 0;
        }
        return 1;

    } /* switch (mode) */

    return 0;
}

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

/* Callback to handle clearing bans and exceptions for clear_channel().
 * Normally, the versions in actions.c and modules/protocol/banexcept.c
 * will do the job, but Unreal has "extended ban types" which confuse the
 * usermask comparison, so we need to write our own versions of the
 * routines.  Hooray for feeping creaturism... */

static void unreal_clear_bans_excepts(const char *sender, Channel *chan,
				      int what, const User *u);
static int do_clear_channel(const char *sender, Channel *chan, int what,
			    const void *param)
{
    if (what & (CLEAR_USERS | CLEAR_BANS | CLEAR_EXCEPTS)) {
	unreal_clear_bans_excepts(sender, chan, what,
				  (what & CLEAR_USERS) ? NULL : param);
    }
    return 0;
}


static void unreal_clear_bans_excepts(const char *sender, Channel *chan,
				      int what, const User *u)
{
    int i, count;
    char **list;

    if (what & (CLEAR_USERS | CLEAR_BANS)) {
	if (chan->bans_count) {
	    count = chan->bans_count;
	    list = smalloc(sizeof(char *) * count);
	    memcpy(list, chan->bans, sizeof(char *) * count);
	    for (i = 0; i < count; i++) {
		char *s = list[i];
		if (*s == '~'
		 && ((s[1] == '!' && s[2] && s[3] == ':')
		  || (s[1] && s[2] == ':'))
		) {
		    if (s[1] == '!')
			s += 4;
		    else
			s += 3;
		}
		if (!u || match_usermask(s, u))
		    set_cmode(sender, chan, "-b", list[i]);
		if (u && u->ipaddr) {
		    char tmpbuf[BUFSIZE];
		    int nicklen = snprintf(tmpbuf, sizeof(tmpbuf), "%s!", u->nick);
		    snprintf(tmpbuf+nicklen, sizeof(tmpbuf)-nicklen, "%s@%s",
			     u->username, u->ipaddr);
		    if (match_wild_nocase(s, tmpbuf))
			set_cmode(sender, chan, "-b", list[i]);
		    if (match_wild_nocase(s, tmpbuf+nicklen))
			set_cmode(sender, chan, "-b", list[i]);
		}
	    }
	    free(list);
	}
    }

    if (what & (CLEAR_USERS | CLEAR_EXCEPTS)) {
	if (chan->excepts_count) {
	    count = chan->excepts_count;
	    list = smalloc(sizeof(char *) * count);
	    memcpy(list, chan->excepts, sizeof(char *) * count);
	    for (i = 0; i < count; i++) {
		char *s = list[i];
		if (*s == '~'
		 && ((s[1] == '!' && s[2] && s[3] == ':')
		  || (s[1] && s[3] == ':'))
		) {
		    if (s[1] == '!')
			s += 4;
		    else
			s += 3;
		}
		if (!u || match_usermask(s, u))
		    set_cmode(sender, chan, "-e", list[i]);
		if (u && u->ipaddr) {
		    char tmpbuf[BUFSIZE];
		    int nicklen = snprintf(tmpbuf, sizeof(tmpbuf), "%s!", u->nick);
		    snprintf(tmpbuf+nicklen, sizeof(tmpbuf)-nicklen, "%s@%s",
			     u->username, u->ipaddr);
		    if (match_wild_nocase(s, tmpbuf))
			set_cmode(sender, chan, "-e", list[i]);
		    if (match_wild_nocase(s, tmpbuf+nicklen))
			set_cmode(sender, chan, "-e", list[i]);
		}
	    }
	    free(list);
	}
    }
}

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

/*
 * NickServ post-IDENTIFY callback; we set user mode +a for Services
 * admins if they're opered.
 */

static int do_nick_identified(User *u, int old_status)
{
    if (is_oper(u) && is_services_admin(u))
	send_cmd(ServerName, "SVSMODE %s +a", u->nick);
    return 0;
}

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

/*
 * Set-topic callback; we use this to adjust timestamps as necessary to
 * force a topic change.
 */

static int do_set_topic(const char *source, Channel *c, const char *topic,
			const char *setter, time_t t)
{
    if (setter)
	return 0;
    if (t <= c->topic_time)
	t = c->topic_time + 1;  /* Force topic change */
    c->topic_time = t;
    send_cmd(source, "TOPIC %s %s %ld :%s", c->name, c->topic_setter,
	     (long)c->topic_time, c->topic ? c->topic : "");
    return 1;
}

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

/*
 * ChanServ channel mode check callback; we handle setting modes +L, +f,
 * and +j. (-L/-f/-j are handled by the default handler, which just sends a
 * "MODE -L/-f" as appropriate.)
 */

static int do_check_modes(Channel *c, ChannelInfo *ci, int add, int32 flag)
{
    if (add) {
	switch (mode_flag_to_char(flag, MODE_CHANNEL)) {

	  case 'L':
	    if (!ci->mlock_link) {
		module_log("warning: removing +L from channel %s mode lock"
			   " (missing parameter)", ci->name);
		ci->mlock_on &= ~mode_char_to_flag('L', MODE_CHANNEL);
	    } else {
		if (!c->link || irc_stricmp(ci->mlock_link, c->link) != 0)
		    set_cmode(s_ChanServ, c, "+L", ci->mlock_link);
	    }
	    return 1;

	  case 'f':
	    if (!ci->mlock_flood) {
		module_log("warning: removing +f from channel %s mode lock"
			   " (missing parameter)", ci->name);
		ci->mlock_on &= ~mode_char_to_flag('f', MODE_CHANNEL);
	    } else {
		if (!c->flood || irc_stricmp(ci->mlock_flood, c->flood) != 0)
		    set_cmode(s_ChanServ, c, "+f", ci->mlock_flood);
	    }
	    return 1;

	  case 'j':
	    if (sgn(ci->mlock_joinrate1) != sgn(ci->mlock_joinrate2)) {
		module_log("warning: removing +j from channel %s mode lock"
			   " (invalid parameter: %d:%d)", ci->name,
			   ci->mlock_joinrate1, ci->mlock_joinrate2);
		ci->mlock_on &= ~mode_char_to_flag('j', MODE_CHANNEL);
		ci->mlock_joinrate1 = ci->mlock_joinrate2 = 0;
	    } else if (ci->mlock_joinrate1 < 0) {
		if (c->joinrate1 || c->joinrate2)
		    set_cmode(s_ChanServ, c, "-j");
	    } else {
		if (c->joinrate1 != ci->mlock_joinrate1
		 || c->joinrate2 != ci->mlock_joinrate2
		) {
		    char buf[BUFSIZE];
		    snprintf(buf, sizeof(buf), "%d:%d",
			     ci->mlock_joinrate1, ci->mlock_joinrate2);
		    set_cmode(s_ChanServ, c, "+j", buf);
		}
	    }
	    return 1;

	} /* switch (modechar) */
    } /* if (add) */
    return 0;
}

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

/*
 * Callback for ChanServ channel user mode checking; we prevent all
 * automatic mode changes for hiding (+I) users (since the mode changes
 * would give away their presence) or service pseudoclients (+S).
 */

static int do_check_chan_user_modes(const char *source, User *user,
				    Channel *c, int32 modes)
{
    /* Don't do anything to service pseudoclients */
    if (user->mode & mode_char_to_flag('S', MODE_USER))
	return 1;

    /* Don't do anything to hiding users either */
    return ((user->mode & usermode_hiding) ? 1 : 0);
}

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

/*
 * Callback for ChanServ channel entrance checking; we enforce admin-only,
 * secure-only, and no-hiding channel modes here.
 */

static int do_check_kick(User *user, const char *chan, ChannelInfo *ci,
			 char **mask_ret, const char **reason_ret)
{
    /* Retrieve the channel's Channel record, if present */
    Channel *c = get_channel(chan);

    /* Don't do anything to service pseudoclients */
    if (user->mode & mode_char_to_flag('S', MODE_USER))
	return 2;

    /* Don't let plain opers into +A (admin only) channels */
    if ((((c?c->mode:0) | (ci?ci->mlock_on:0)) & chanmode_admins_only)
     && !(user->mode & usermode_admin)
    ) {
	*mask_ret = create_mask(user, 1);
	*reason_ret = getstring(user->ngi, CHAN_NOT_ALLOWED_TO_JOIN);
	return 1;
    }
    /* Don't let users not on secure connections (-z) into +z channels */
    if ((((c?c->mode:0) | (ci?ci->mlock_on:0)) & chanmode_secure_only)
     && !(user->mode & usermode_secure)
    ) {
	*mask_ret = create_mask(user, 1);
	*reason_ret = getstring(user->ngi, CHAN_NOT_ALLOWED_TO_JOIN);
	return 1;
    }
    /* Don't let hiding users into no-hiding channels */
    if ((((c?c->mode:0) | (ci?ci->mlock_on:0)) & chanmode_no_hiding)
     && (user->mode & usermode_hiding)
    ) {
	*mask_ret = create_mask(user, 1);
	*reason_ret = getstring(user->ngi, CHAN_NOT_ALLOWED_TO_JOIN);
	return 1;
    }

    /* Let other processing continue as usual */
    return 0;
}

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

/*
 * ChanServ SET MLOCK callback; we handle locking modes f, j, K, and L.
 */

static int do_set_mlock(User *u, ChannelInfo *ci, int mode, int add, char **av)
{
    if (!mode) {
	/* Final check of new mode lock */
	if ((ci->mlock_on & mode_char_to_flag('K',MODE_CHANNEL))
	 && !(ci->mlock_on & CMODE_i)
	) {
	    /* +K requires +i */
	    notice_lang(s_ChanServ, u, CHAN_SET_MLOCK_REQUIRES, 'K', 'i');
	    return 1;
	}
	if (ci->mlock_link && !ci->mlock_limit) {
	    /* +L requires +l */
	    notice_lang(s_ChanServ, u, CHAN_SET_MLOCK_REQUIRES, 'L', 'l');
	    return 1;
	}
	return 0;
    }

    /* Single mode set/clear */

    if (add) {

	switch (mode) {

	  case 'L':
	    if (!valid_chan(av[0])) {
		/* Invalid channel name */
		notice_lang(s_ChanServ, u, CHAN_SET_MLOCK_LINK_BAD, mode);
		return 1;
	    }
	    if (irc_stricmp(av[0], ci->name) == 0) {
		/* Trying to link to the same channel */
		notice_lang(s_ChanServ, u, CHAN_SET_MLOCK_LINK_SAME, mode);
		return 1;
	    }
	    ci->mlock_link = sstrdup(av[0]);
	    break;

	  case 'f': {
	    char *s, *t;
	    /* Legal format for flood mode is:
	     *    "digits:digits"
	     * or "*digits:digits"
	     * or "[TYPE,TYPE...]:digits"
	     *    where TYPE is 1-999 followed by one of cjkmnt possibly
	     *    followed by # and (a letter possibly followed by digits) */
	    s = av[0];
	    if (*s == '[') {
		int ok = 1;
		char c;
		s++;
		do {
		    if (!strchr("0123456789", *s)) {
			ok = 0;
			break;
		    }
		    t = s;
		    s += strspn(s, "0123456789");
		    c = *s;
		    *s = 0;
		    if (atoi(t) < 1 || atoi(t) > 999) {
			ok = 0;
			break;
		    }
		    *s = c;
		    if (!strchr("cjkmnt", *s++)) {
			ok = 0;
			break;
		    }
		    if (*s == '#') {
			s++;
			if (!strchr("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", *s++)) {
			    ok = 0;
			    break;
			}
			s += strspn(s, "0123456789");
		    }
		    if (*s != ',' && *s != ']')
			ok = 0;
		    s++;
		} while (ok && s[-1] != ']');
		if (ok && *s++ != ':')
		    ok = 0;
		if (ok && s[strspn(s,"0123456789")] == 0) {
		    /* String is valid */
		    ci->mlock_flood = sstrdup(av[0]);
		    break;
		}
	    } else {
		if (*s == '*')
		    s++;
		t = strchr(s, ':');
		if (t) {
		    t++;
		    if (s[strspn(s,"0123456789")] == ':'
		     && t[strspn(t,"0123456789")] == 0
		    ) {
			/* String is valid */
			ci->mlock_flood = sstrdup(av[0]);
			break;
		    }
		}
	    }
	    /* String is invalid */
	    notice_lang(s_ChanServ, u, CHAN_SET_MLOCK_BAD_PARAM, mode);
	    return 1;
	  } /* case 'f' */

          case 'j': {
	    int ok = 0;
	    char *s;
	    ci->mlock_joinrate1 = strtol(av[0], &s, 0);
	    if (ci->mlock_joinrate1 > 0 && *s == ':') {
		ci->mlock_joinrate2 = strtol(s+1, &s, 0);
		if (ci->mlock_joinrate2 > 0 && !*s)
		    ok = 1;
	    }
	    if (!ok) {
		notice_lang(s_ChanServ, u, CHAN_SET_MLOCK_BAD_PARAM, mode);
		return 1;
	    }
	    break;
	  } /* case 'j' */

	} /* switch (mode) */

    } else {  /* !add -> lock off */

        switch (mode) {
	  case 'j':
	    ci->mlock_joinrate1 = ci->mlock_joinrate2 = -1;
	    break;
	} /* switch (mode) */

    } /* if (add) */

    return 0;
}

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

/*
 * Callback for sending autokills; required for autokill-supporting
 * protocols.
 */

static int do_send_akill(const char *username, const char *host,
			 time_t expires, const char *who, const char *reason)
{
    send_cmd(ServerName, "TKL + G %s %s %s %ld %ld :%s", username, host,
	     ServerName, (long)expires, (long)time(NULL), reason);
    return 1;
}

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

/*
 * Callback for removing autokills; required for autokill-supporting
 * protocols.
 */

static int do_cancel_akill(const char *username, const char *host)
{
    send_cmd(ServerName, "TKL - G %s %s %s", username, host, ServerName);
    return 1;
}

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

/*
 * Callback for sending autokill exclusions; required for
 * exclusion-supporting protocols.
 */

static int do_send_exclude(const char *username, const char *host,
			   time_t expires, const char *who, const char *reason)
{
    send_cmd(ServerName, "TKL + E %s %s %s %ld %ld :%s", username, host,
	     ServerName, (long)expires, (long)time(NULL), reason);
    return 1;
}

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

/*
 * Callback for removing autokill exclusions; required for
 * exclusion-supporting protocols.
 */

static int do_cancel_exclude(const char *username, const char *host)
{
    send_cmd(ServerName, "TKL - E * %s %s %s", username, host, ServerName);
    return 1;
}

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

/*
 * Callbacks for sending S-lines; required for S*LINE-supporting
 * protocols.
 */

static int do_send_sgline(const char *mask, time_t expires, const char *who,
			  const char *reason)
{
    char buf[BUFSIZE], *s;

    s = buf;
    while (*reason && s-buf < sizeof(buf)-1) {
	if (*reason == ' ')
	    *s = '_';
	else
	    *s = *reason;
	reason++;
	s++;
    }
    *s = 0;
    send_cmd(ServerName, "SVSNLINE + %s :%s", buf, mask);
    return 1;
}

static int do_send_sqline(const char *mask, time_t expires, const char *who,
			  const char *reason)
{
    send_cmd(ServerName, "SQLINE %s :%s", mask, reason);
    return 1;
}

static int do_send_szline(const char *mask, time_t expires, const char *who,
			  const char *reason)
{
    send_cmd(ServerName, "TKL + Z * %s %s %ld %ld :%s", mask,
	     ServerName, (long)expires, (long)time(NULL), reason);
    return 1;
}

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

/*
 * Callbacks for removing S-lines; required for S*LINE-supporting
 * protocols.
 */

static int do_cancel_sgline(const char *mask)
{
    send_cmd(ServerName, "SVSNLINE - :%s", mask);
    return 1;
}

static int do_cancel_sqline(const char *mask)
{
    send_cmd(ServerName, "UNSQLINE %s", mask);
    return 1;
}

static int do_cancel_szline(const char *mask)
{
    send_cmd(ServerName, "TKL - Z * %s %s", mask, ServerName);
    return 1;
}

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

/* Module version identifier, required for all modules. */
const int32 module_version = MODULE_VERSION_CODE;

/*
 * Configuration directives for this module (required--if the module has
 * no configuration directives, this consists of a single NULL entry).
 * Note that we include configuration directives for SJOIN which are
 * defined in sjoin.h.
 */
ConfigDirective module_config[] = {
    { "ServerNumeric",	  { { CD_POSINT, 0, &ServerNumeric } } },
    { "SetServerTimes",   { { CD_SET, 0, &SetServerTimes },
			    { CD_TIME, CF_OPTIONAL, &SVSTIMEFrequency } } },
    SJOIN_CONFIG,
    { NULL }
};

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

/*
 * Load-module callback, used to look up symbols and add callbacks in
 * other modules.
 */

static int do_load_module(Module *mod, const char *modname)
{
    if (strcmp(modname, "operserv/main") == 0) {
	module_operserv = mod;
	p_is_services_admin = get_module_symbol(mod, "is_services_admin");
	if (!p_is_services_admin) {
	    module_log("warning: unable to look up symbol `is_services_admin'"
		       " in module `operserv/main'");
	}
    } else if (strcmp(modname, "operserv/akill") == 0) {
	if (!add_callback(mod, "send_akill", do_send_akill))
	    module_log("Unable to add send_akill callback");
	if (!add_callback(mod, "cancel_akill", do_cancel_akill))
	    module_log("Unable to add cancel_akill callback");
	if (unreal_version >= 2303) {
	    if (!add_callback(mod, "send_exclude", do_send_exclude))
		module_log("Unable to add send_exclude callback");
	    if (!add_callback(mod, "cancel_exclude", do_cancel_exclude))
		module_log("Unable to add cancel_exclude callback");
	}
    } else if (strcmp(modname, "operserv/sline") == 0) {
	if (!add_callback(mod, "send_sgline", do_send_sgline))
	    module_log("Unable to add send_sgline callback");
	if (!add_callback(mod, "send_sqline", do_send_sqline))
	    module_log("Unable to add send_sqline callback");
	if (!add_callback(mod, "send_szline", do_send_szline))
	    module_log("Unable to add send_szline callback");
	if (!add_callback(mod, "cancel_sgline", do_cancel_sgline))
	    module_log("Unable to add cancel_sgline callback");
	if (!add_callback(mod, "cancel_sqline", do_cancel_sqline))
	    module_log("Unable to add cancel_sqline callback");
	if (!add_callback(mod, "cancel_szline", do_cancel_szline))
	    module_log("Unable to add cancel_szline callback");
    } else if (strcmp(modname, "nickserv/main") == 0) {
	if (!add_callback(mod, "identified", do_nick_identified))
	    module_log("Unable to add NickServ identified callback");
    } else if (strcmp(modname, "chanserv/main") == 0) {
	module_chanserv = mod;
	p_s_ChanServ = get_module_symbol(mod, "s_ChanServ");
	if (!p_s_ChanServ) {
	    /* FIXME: make a generalized logging routine for this problem? */
	    module_log("Unable to resolve symbol `s_ChanServ' in module"
		       " `chanserv/main'");
	    p_s_ChanServ = &ServerName;
	}
	if (!add_callback(mod, "check_modes", do_check_modes))
	    module_log("Unable to add ChanServ check_modes callback");
	if (!add_callback(mod, "check_chan_user_modes",
			  do_check_chan_user_modes))
	    module_log("Unable to add ChanServ check_chan_user_modes"
		       " callback");
	if (!add_callback(mod, "check_kick", do_check_kick))
	    module_log("Unable to add ChanServ check_kick callback");
	if (!add_callback(mod, "SET MLOCK", do_set_mlock))
	    module_log("Unable to add ChanServ SET MLOCK callback");
    }
    return 0;
}

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

/*
 * Unload-module callback, used to clear pointers to symbols in modules
 * about to be unloaded.
 */

static int do_unload_module(Module *mod)
{
    if (mod == module_operserv) {
	module_operserv = NULL;
	p_is_services_admin = NULL;
    } else if (mod == module_chanserv) {
	module_chanserv = NULL;
	p_s_ChanServ = &ServerName;
    }
    return 0;
}

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

/* Module initialization routine (required). */

int init_module(Module *module_)
{
    unsigned char c;


    /* Copy passed-in parameter (module handle) to module-global variable. */
    module = module_;

    /* Set protocol information variables. */
    protocol_name     = "Unreal";
    protocol_version  = "3.1.1+";
    protocol_features = PF_SZLINE | PF_SVSJOIN;
    protocol_nickmax  = 30;

    /* Make sure the ServerNumeric configuration setting is valid.  This
     * check is "<0" instead of "<1" because the numeric may not be
     * specified at all; conffile.c will check to ensure that 0 is not
     * given. */
    if (ServerNumeric < 0 || ServerNumeric > 254) {
    	config_error(MODULES_CONF, 0,
		     "ServerNumeric must be in the range 1..254");
	return 0;
    }

    /* Register messages.  Note that this must be done before calling
     * init_token(), which depends on all needed messages being registered
     * (in order to look up message handlers for tokens). */
    if (!register_messages(unreal_messages)) {
	module_log("Unable to register messages");
	return 0;
    }

    /* Add callbacks. */
    if (!add_callback(NULL, "load module", do_load_module)
     || !add_callback(NULL, "unload module", do_unload_module)
     || !add_callback(NULL, "receive message", do_receive_message)
     || !add_callback(NULL, "user create", do_user_create)
     || !add_callback(NULL, "user servicestamp change",
		      do_user_servicestamp_change)
     || !add_callback(NULL, "user MODE", do_user_mode)
     || !add_callback(NULL, "channel MODE", do_channel_mode)
     || !add_callback(NULL, "clear channel", do_clear_channel)
     || !add_callback(NULL, "set topic", do_set_topic)
    ) {
	module_log("Unable to add callbacks");
	return 0;
    }

    /* Initialize subsystems in separate files. */
    if (!init_banexcept(module)
     || !init_chanprot(module)
     || !init_halfop(module)
     || !init_invitemask(module)
     || !init_sjoin(module)
     || !init_svsnick(module)
     || !init_token(module, tokens)
    ) {
	return 0;
    }

    /* Initialize mode data. */
    init_modes();

    /* Adjust irc_stricmp() behavior (Unreal treats [ \ ] and { | } as
     * distinct, unlike RFC 1459). */
    irc_lowertable['['] = '[';
    irc_lowertable['\\'] = '\\';
    irc_lowertable[']'] = ']';

    /* Adjust valid_nick() behavior (Unreal allows all characters from 'A'
     * to '}' inclusive both at the beginning and in the middle of a nick). */
    for (c = 'A'; c <= '}'; c++)
	valid_nick_table[c] = 3;
    for (c = 0; c < 32; c++)
	valid_chan_table[c] = 0;
    valid_chan_table[':'] = 0;
    valid_chan_table[160] = 0;

    /* Set up function pointers and mode strings for send.c. */
    send_nick          = do_send_nick;
    send_nickchange    = do_send_nickchange;
    send_namechange    = do_send_namechange;
    send_server        = do_send_server;
    send_server_remote = do_send_server_remote;
    wallops            = do_wallops;
    notice_all         = do_notice_all;
    send_channel_cmd   = do_send_channel_cmd;
    pseudoclient_modes = "Sqd";
    enforcer_modes     = "d";

    /* Unreal has U:lines, so adjust the "bouncy modes" message. */
    setstring(OPER_BOUNCY_MODES, OPER_BOUNCY_MODES_U_LINE);

    /* Return success. */
    return 1;
}

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

/*
 * Module cleanup routine (required).  We never permit ourselves to be
 * unloaded except during final shutdown; thus we don't worry about
 * restoring things to their initial state.
 */

int exit_module(int shutdown)
{
    if (!shutdown) {
	/* Do not allow removal of this module */
	return 0;
    }

    /* Clean things up in reverse order.  We don't worry about fixing up
     * irc_lowertable and so on because we're shutting down anyway;
     * however, we do need to make sure to at least remove all callbacks
     * before exiting, to avoid invalid function pointers being called
     * (e.g. for the unload module callback). */

    if (to_svstime) {
	del_timeout(to_svstime);
	to_svstime = NULL;
    }

    exit_token();
    exit_svsnick();
    exit_sjoin();
    exit_invitemask();
    exit_halfop();
    exit_chanprot();
    exit_banexcept();

    remove_callback(NULL, "set topic", do_set_topic);
    remove_callback(NULL, "clear channel", do_clear_channel);
    remove_callback(NULL, "channel MODE", do_channel_mode);
    remove_callback(NULL, "user MODE", do_user_mode);
    remove_callback(NULL, "user servicestamp change",
		    do_user_servicestamp_change);
    remove_callback(NULL, "user create", do_user_create);
    remove_callback(NULL, "receive message", do_receive_message);
    remove_callback(NULL, "unload module", do_unload_module);
    remove_callback(NULL, "load module", do_load_module);

    unregister_messages(unreal_messages);

    /* All finished, return success */
    return 1;
}

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