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

#include "services.h"
#include "modules.h"
#include "conffile.h"
#include "language.h"
#include "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 "sjoin.h"
#include "svsnick.h"
#include "token.h"

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

/* Variable to hold the current module's pointer (required for all modules): */
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;


/* 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_hiding = 0;	/* +I */
static int32 chanmode_admins_only = 0;	/* +A */
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;

/*************************************************************************/
/****************** 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'.
 */

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_HIDING	0x02000000  /* Usermode for hiding users */
#define MI_ADMINS_ONLY	0x01000000  /* Chanmode for admin-only channels */
#define MI_NO_HIDING	0x02000000  /* 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 */
};

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

/*
 * 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}},	/* 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}},	/* 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 */
    {'e', {0x80000000,1,1,0,MI_MULTIPLE}},
				/* Ban exceptions */
};

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

/* 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_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_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 *tmp;
    char *newmodes;

    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:
     *
     *     NICK <nick> <hopcount> <timestamp> <username> <hostname>
     *          <server> <servicestamp> <modes> <fakehost> :<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 != 10) {
	if (debug)
	    module_log("debug: NICK message: wrong number of parameters (%d)"
		       " for new user", ac);
	return;
    }
    /* Save the new user's modes modes and strip the parameter out. */
    newmodes = av[7];
    av[7] = av[9];
    /* We keep ac as is because we insert a NULL (IP address) later. */

    /* Swap parameters so the Services timestamp is in the right place. */
    tmp = av[6];
    av[6] = av[7];
    av[7] = tmp;

    /* Move fakehost into user argument area. */
    av[9] = av[8];
    av[8] = NULL;

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

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

/* PROTOCTL message handler. */

static void m_protoctl(char *source, int ac, char **av)
{
    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;
    }
    /* Make sure we got a NICKv2 from the remote server; if not, abort. */
    if (!got_nickv2) {
	send_error("Need NICKv2 protocol");
	quitmsg = "Remote server doesn't support NICKv2";
	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)
	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)
	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)
	m_setname(av[0], ac-1, av+1);
}

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

/* SJOIN message handler.  The actual code is in sjoin.c.  Note */

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_unreal(source, ac, av);
}

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

/* SVSMODE message handler. */

static void m_svsmode(char *source, int ac, char **av)
{
    if (*av[0] == '#' || *av[0] == '&') {
	module_log("SVSMODE 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. */

static void m_tkl(char *source, int ac, char **av)
{
    /* Clear out any Services-set TKL we get; if it's really valid, the
     * relevant modules will re-add it for us. */
    if (ac < 5 || *av[0] != '+' || strcmp(av[4],ServerName) != 0)
	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 },
    { "GLOBOPS",   NULL },
    { "GNOTICE",   NULL },
    { "GOPER",     NULL },
    { "HELP",      NULL },
    { "NETINFO",   NULL },
    { "NICK",      m_nick },
    { "PROTOCTL",  m_protoctl },
    { "RAKILL",    NULL },
    { "SETHOST",   m_sethost },
    { "SETIDENT",  m_setident },
    { "SETNAME",   m_setname },
    { "SILENCE",   NULL },
    { "SJOIN",     m_sjoin },
    { "SMO",       NULL },
    { "SQLINE",	   NULL },
    { "SVSMODE",   m_svsmode },
    { "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" },

    /* 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> :<ircname> */
    /* 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", nick,
	     (long)time(NULL), user, host, server, modes, host, 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");
    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;
	    /* Unreal 3.2 (protocol version 2303) and above support
	     * autokill exclusions, so make a note of that */
	    if (ver >= 2303)
		protocol_features |= PF_AKILL_EXCL;
	}
    }
    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 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;
    }
    return 0;
}

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

/*
 * 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(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(ServerName, "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 and +f.
 * (-L and -f 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 (!c->link || irc_stricmp(ci->mlock_link, c->link) != 0)
		set_cmode(s_ChanServ, c, "+L", ci->mlock_link);
	    return 1;
	  case 'f':
	    if (!c->flood || irc_stricmp(ci->mlock_flood, c->flood) != 0)
		set_cmode(s_ChanServ, c, "+f", ci->mlock_flood);
	    return 1;
	}
    }
    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
 * and no-hiding channel modes here.
 */

static int do_check_kick(User *user, ChannelInfo *ci, char **mask_ret,
			 const char **reason_ret)
{
    /* 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 ((ci->mlock_on & 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 hiding users into no-hiding channels */
    if ((ci->mlock_on & 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;
    }

    return 0;
}

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

/*
 * ChanServ SET MLOCK callback; we handle locking modes +/-L and +/-f.
 */

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_link && !ci->mlock_limit) {
	    /* +L requires +l */
	    notice_lang(s_ChanServ, u, CHAN_SET_MLOCK_LINK_NO_LIMIT, '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" */
	    s = av[0];
	    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_FLOOD_BAD, mode);
	    return 1;
	  }
	}
    }
    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_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_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_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_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; config.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, "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_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.
 */

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). */

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

    remove_callback(NULL, "set topic", do_set_topic);
    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;
}

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