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

#include "services.h"
#include "modules.h"
#include "conffile.h"
#include "commands.h"
#include "language.h"

#include "operserv.h"
#define NEED_MAKE_REASON
#include "maskdata.h"
#include "akill.h"

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

static Module *module;
static Module *module_operserv;

static int cb_send_akill     = -1;
static int cb_send_exclude   = -1;
static int cb_cancel_akill   = -1;
static int cb_cancel_exclude = -1;

static char * AutokillDBName;
static char * AutokillReason;
static int    ImmediatelySendAutokill;
static time_t AutokillExpiry;
static time_t OperMaxExpiry;
static int    WallOSAkill;
static int    WallAutokillExpire;

       int    EnableExclude; /* not static because main.c/do_help() needs it */
static time_t ExcludeExpiry;
static char * ExcludeReason;
EXPORT_VAR(int,EnableExclude)

static int db_opened = 0;

static void do_akill(User *u);
static void do_exclude(User *u);

static Command cmds[] = {
    {"AKILL",     do_akill,     is_services_oper, OPER_HELP_AKILL,     -1,-1},
    {"EXCLUDE",   do_exclude,   is_services_oper, OPER_HELP_EXCLUDE,   -1,-1},
    { NULL }
};

/*************************************************************************/
/************************** Internal functions ***************************/
/*************************************************************************/

/* Callback for saving data. */

static int do_save_data(void)
{
    sync_akill_db(AutokillDBName);
    return 0;
}

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

/* Send an autokill to the uplink server. */

static void send_akill(const MaskData *akill)
{
    char *username, *host;

    /* Don't send autokills if EnableExclude but no ircd support */
    if (EnableExclude && !(protocol_features & PF_AKILL_EXCL))
	return;

    username = sstrdup(akill->mask);
    host = strchr(username, '@');
    if (!host) {
	/* Glurp... this oughtn't happen, but if it does, let's not
	 * play with null pointers.  Yell and bail out.
	 */
	wallops(NULL, "Missing @ in autokill: %s", akill->mask);
	module_log("BUG: (send_akill) Missing @ in mask: %s", akill->mask);
	return;
    }
    *host++ = 0;
    call_callback_5(module, cb_send_akill,
		    username, host, akill->expires, akill->who,
		    make_reason(AutokillReason, akill));
    free(username);
}

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

/* Remove an autokill from the uplink server. */

static void cancel_akill(char *mask)
{
    char *s = strchr(mask, '@');
    if (s) {
	*s++ = 0;
	call_callback_2(module, cb_cancel_akill, mask, s);
    } else {
	module_log("BUG: (cancel_akill) Missing @ in mask: %s", mask);
    }
}

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

/* Send an autokill exclusion to the uplink server. */

static void send_exclude(const MaskData *exclude)
{
    char *username, *host;

    username = sstrdup(exclude->mask);
    host = strchr(username, '@');
    if (!host) {
	wallops(NULL, "Missing @ in autokill exclusion: %s", exclude->mask);
	module_log("BUG: (send_exclude) Missing @ in mask: %s", exclude->mask);
	return;
    }
    *host++ = 0;
    call_callback_5(module, cb_send_exclude,
		    username, host, exclude->expires, exclude->who,
		    make_reason(ExcludeReason, exclude));
    free(username);
}

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

/* Remove an autokill exclusion from the uplink server. */

static void cancel_exclude(char *mask)
{
    char *s = strchr(mask, '@');
    if (s) {
	*s++ = 0;
	call_callback_2(module, cb_cancel_exclude, mask, s);
    } else {
	module_log("BUG: (cancel_exclude) Missing @ in mask: %s", mask);
    }
}

/*************************************************************************/
/************************** External functions ***************************/
/*************************************************************************/

/* Does the user match any autokills?  Return 1 (and kill the user) if so,
 * else 0.
 */

static int check_akill(int ac, char **av)
{
    const char *nick = av[0], *username = av[3], *host = av[4];
    char buf[BUFSIZE];
    MaskData *akill;

    if (noakill)
	return 0;

    snprintf(buf, sizeof(buf), "%s@%s", username, host);
    if ((akill = get_matching_maskdata(MD_AKILL, buf)) != NULL) {
	if (EnableExclude && get_matching_maskdata(MD_EXCLUSION, buf))
	    return 0;
	/* Don't use kill_user(); that's for people who have already
	 * signed on.  This is called before the User structure is
	 * created. */
	send_cmd(s_OperServ, "KILL %s :%s (%s)", nick, s_OperServ,
		 make_reason(AutokillReason, akill));
	send_akill(akill);
	time(&akill->lastused);
	put_maskdata(MD_AKILL, akill);
	return 1;
    }
    return 0;
}

/*************************************************************************/
/************************** AKILL list editing ***************************/
/*************************************************************************/

/* Note that all string parameters are assumed to be non-NULL; expiry must
 * be set to the time when the autokill should expire (0 for none).  Mask
 * is converted to lowercase on return.
 */

EXPORT_FUNC(create_akill)
void create_akill(char *mask, const char *reason, const char *who,
		  time_t expiry)
{
    MaskData *akill;

    strlower(mask);
    if (maskdata_count(MD_AKILL) >= 32767) {
	module_log("Attempt to add autokill to full list!");
	return;
    }
    akill = scalloc(1, sizeof(*akill));
    akill->mask = sstrdup(mask);
    akill->reason = sstrdup(reason);
    akill->time = time(NULL);
    akill->expires = expiry;
    strscpy(akill->who, who, NICKMAX);
    akill = add_maskdata(MD_AKILL, akill);
    if (ImmediatelySendAutokill)
	send_akill(akill);
}

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

/* Handle an OperServ AKILL command. */

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

static int check_add_akill(User *u, char *mask, time_t *expiry_ptr);
static void do_add_akill(User *u, uint8 type, MaskData *md);
static void do_del_akill(User *u, uint8 type, MaskData *md);

static MaskDataCmdInfo akill_cmd_info = {
    "AKILL",		/* command name */
    MD_AKILL,		/* MaskData type */
    &AutokillExpiry,	/* default expiry time pointer */

    OPER_AKILL_SYNTAX,
    OPER_AKILL_ADD_SYNTAX,
    OPER_AKILL_DEL_SYNTAX,
    OPER_TOO_MANY_AKILLS,
    OPER_AKILL_EXISTS,
    OPER_AKILL_ADDED,
    OPER_AKILL_NOT_FOUND,
    OPER_AKILL_REMOVED,
    OPER_AKILL_LIST_HEADER,
    OPER_AKILL_LIST_FORMAT,
    OPER_AKILL_VIEW_FORMAT,
    OPER_AKILL_VIEW_UNUSED_FORMAT,
    OPER_AKILL_COUNT,

    (void (*))strlower,	/* function to mangle masks for add/delete */
    check_add_akill,	/* function to check validity of mask on add */
    do_add_akill,	/* function to call on mask addition */
    do_del_akill,	/* function to call on mask removal */
    NULL,		/* function to call for unknown commands */
};

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

static void do_akill(User *u)
{
    if (is_services_admin(u) || !OperMaxExpiry)
	akill_cmd_info.def_expiry_ptr = &AutokillExpiry;
    else
	akill_cmd_info.def_expiry_ptr = &OperMaxExpiry;
    do_maskdata_cmd(&akill_cmd_info, u);
}

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

static int check_add_akill(User *u, char *mask, time_t *expiry_ptr)
{
    char *s, *t;
    time_t len;

    if (strchr(mask, '!')) {
	notice_lang(s_OperServ, u, OPER_AKILL_NO_NICK);
	notice_lang(s_OperServ, u, BAD_USERHOST_MASK);
	return 0;
    }
    s = strchr(mask, '@');
    if (!s || s == mask || s[1] == 0) {
	notice_lang(s_OperServ, u, BAD_USERHOST_MASK);
	return 0;
    }

    /* Make sure mask is not too general. */
    *s++ = 0;
    if (strchr(mask,'*') != NULL && mask[strspn(mask,"*?")] == 0
     && ((t = strchr(mask,'?')) == NULL || strchr(t,'?') == NULL)
    ) {
	/* Username part matches anything; check host part */
	if (strchr(s,'*') != NULL && s[strspn(s,"*?.")] == 0
	 && ((t = strchr(mask,'.')) == NULL || strchr(t,'.') == NULL)
	) {
	    /* Hostname mask matches anything or nearly anything, so
	     * disallow mask. */
	    notice_lang(s_OperServ, u, OPER_AKILL_MASK_TOO_GENERAL);
	    return 0;
	}
    }
    s[-1] = '@';	/* Replace "@" that we killed above */

    /* Check expiration limit for non-servadmins. */
    len = *expiry_ptr - time(NULL);
    if (OperMaxExpiry && !is_services_admin(u)
     && (!*expiry_ptr || len > OperMaxExpiry)
    ) {
	notice_lang(s_OperServ, u, OPER_AKILL_EXPIRY_LIMITED,
		    maketime(u->ngi, OperMaxExpiry, MT_DUALUNIT));
	return 0;
    }

    return 1;
}

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

static void do_add_akill(User *u, uint8 type, MaskData *md)
{
#ifdef CLEAN_COMPILE
    type = type;
#endif
    if (WallOSAkill) {
	char buf[BUFSIZE];
	expires_in_lang(buf, sizeof(buf), NULL, md->expires);
	wallops(s_OperServ, "%s added an AKILL for \2%s\2 (%s)",
		u->nick, md->mask, buf);
    }
    if (ImmediatelySendAutokill)
	send_akill(md);
}

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

static void do_del_akill(User *u, uint8 type, MaskData *md)
{
#ifdef CLEAN_COMPILE
    type = type;
#endif
    cancel_akill(md->mask);
}

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

/* Handle an OperServ EXCLUDE command. */

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

static int check_add_exclude(User *u, char *mask, time_t *expiry_ptr);
static void do_add_exclude(User *u, uint8 type, MaskData *md);
static void do_del_exclude(User *u, uint8 type, MaskData *md);

static MaskDataCmdInfo exclude_cmd_info = {
    "EXCLUDE",		/* command name */
    MD_EXCLUSION,	/* MaskData type */
    &ExcludeExpiry,	/* default expiry time pointer */

    OPER_EXCLUDE_SYNTAX,
    OPER_EXCLUDE_ADD_SYNTAX,
    OPER_EXCLUDE_DEL_SYNTAX,
    OPER_TOO_MANY_EXCLUDES,
    OPER_EXCLUDE_EXISTS,
    OPER_EXCLUDE_ADDED,
    OPER_EXCLUDE_NOT_FOUND,
    OPER_EXCLUDE_REMOVED,
    OPER_EXCLUDE_LIST_HEADER,
    OPER_EXCLUDE_LIST_FORMAT,
    OPER_EXCLUDE_VIEW_FORMAT,
    OPER_EXCLUDE_VIEW_UNUSED_FORMAT,
    OPER_EXCLUDE_COUNT,

    (void (*))strlower,	/* function to mangle masks for add/delete */
    check_add_exclude,	/* function to check validity of mask on add */
    do_add_exclude,	/* function to call on mask addition */
    do_del_exclude,	/* function to call on mask removal */
    NULL,		/* function to call for unknown commands */
};

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

static void do_exclude(User *u)
{
    if (is_services_admin(u) || !OperMaxExpiry)
	exclude_cmd_info.def_expiry_ptr = &AutokillExpiry;
    else
	exclude_cmd_info.def_expiry_ptr = &OperMaxExpiry;
    do_maskdata_cmd(&exclude_cmd_info, u);
}

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

static int check_add_exclude(User *u, char *mask, time_t *expiry_ptr)
{
    char *s;

    s = strchr(mask, '@');
    if (!s || s == mask || s[1] == 0) {
	notice_lang(s_OperServ, u, BAD_USERHOST_MASK);
	return 0;
    }
    return 1;
}

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

static void do_add_exclude(User *u, uint8 type, MaskData *md)
{
#ifdef CLEAN_COMPILE
    type = type;
#endif
    if (WallOSAkill) {
	char buf[BUFSIZE];
	expires_in_lang(buf, sizeof(buf), NULL, md->expires);
	wallops(s_OperServ, "%s added an EXCLUDE for \2%s\2 (%s)",
		u->nick, md->mask, buf);
    }
    if (ImmediatelySendAutokill)
	send_exclude(md);
}

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

static void do_del_exclude(User *u, uint8 type, MaskData *md)
{
#ifdef CLEAN_COMPILE
    type = type;
#endif
    cancel_exclude(md->mask);
}

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

/* Callback for autokill expiration. */

static int do_expire_maskdata(uint32 type, MaskData *md)
{
    if (type == MD_AKILL) {
	if (WallAutokillExpire)
	    wallops(s_OperServ, "AKILL on %s has expired", md->mask);
	cancel_akill(md->mask);
    }
    return 0;
}

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

/* Callback to display help text for HELP AKILL. */

static int do_help(User *u, const char *param)
{
    if (stricmp(param, "AKILL") == 0) {
	notice_help(s_OperServ, u, OPER_HELP_AKILL);
	if (OperMaxExpiry)
	    notice_help(s_OperServ, u, OPER_HELP_AKILL_OPERMAXEXPIRY,
			maketime(u->ngi, OperMaxExpiry, MT_DUALUNIT));
	notice_help(s_OperServ, u, OPER_HELP_AKILL_END);
	return 1;
    }
    return 0;
}

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

const int32 module_version = MODULE_VERSION_CODE;

ConfigDirective module_config[] = {
    { "AutokillDB",       { { CD_STRING, CF_DIRREQ, &AutokillDBName } } },
    { "AutokillExpiry",   { { CD_TIME, 0, &AutokillExpiry } } },
    { "AutokillReason",   { { CD_STRING, CF_DIRREQ, &AutokillReason } } },
    { "EnableExclude",    { { CD_SET, 0, &EnableExclude } } },
    { "ExcludeExpiry",    { { CD_TIME, 0, &ExcludeExpiry } } },
    { "ExcludeReason",    { { CD_STRING, 0, &ExcludeReason } } },
    { "ImmediatelySendAutokill",{{CD_SET, 0, &ImmediatelySendAutokill } } },
    { "OperMaxExpiry",    { { CD_TIME, 0, &OperMaxExpiry } } },
    { "WallAutokillExpire",{{ CD_SET, 0, &WallAutokillExpire } } },
    { "WallOSAkill",      { { CD_SET, 0, &WallOSAkill } } },
    { NULL }
};

/* Pointer to EXCLUDE command record (for EnableExclude) */
static Command *cmd_EXCLUDE;

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

static int do_reconfigure(int after_configure)
{
    static int old_EnableExclude = 0;

    if (!after_configure) {
	/* Before reconfiguration: save old values. */
	old_EnableExclude = EnableExclude;
    } else {
	/* After reconfiguration: handle value changes. */
	if (EnableExclude && !ExcludeReason) {
	    module_log("EXCLUDE enabled but ExcludeReason not set; disabling"
		       " EXCLUDE");
	    EnableExclude = 0;
	}
	if (EnableExclude)
	    cmd_EXCLUDE->name = "EXCLUDE";
	else
	    cmd_EXCLUDE->name = "";
    }  /* if (!after_configure) */
    return 0;
}

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

int init_module(Module *module_)
{
    module = module_;

    if (EnableExclude && !ExcludeReason) {
	module_log("EXCLUDE enabled but ExcludeReason not set");
	return 0;
    }

    module_operserv = find_module("operserv/main");
    if (!module_operserv) {
	module_log("Main OperServ module not loaded");
	return 0;
    }
    use_module(module_operserv);

    if (!register_commands(module_operserv, cmds)) {
	module_log("Unable to register commands");
	exit_module(0);
	return 0;
    }
    cmd_EXCLUDE = lookup_cmd(module_operserv, "EXCLUDE");
    if (!cmd_EXCLUDE) {
	module_log("BUG: unable to find EXCLUDE command entry");
	exit_module(0);
	return 0;
    }
    if (!EnableExclude)
	cmd_EXCLUDE->name = "";

    cb_send_akill     = register_callback(module, "send_akill");
    cb_send_exclude   = register_callback(module, "send_exclude");
    cb_cancel_akill   = register_callback(module, "cancel_akill");
    cb_cancel_exclude = register_callback(module, "cancel_exclude");
    if (cb_send_akill < 0 || cb_send_exclude < 0 || cb_cancel_akill < 0
     || cb_cancel_exclude < 0
    ) {
	module_log("Unable to register callbacks");
	exit_module(0);
	return 0;
    }

    if (!add_callback(NULL, "reconfigure", do_reconfigure)
     || !add_callback(NULL, "user check", check_akill)
     || !add_callback(NULL, "save data", do_save_data)
     || !add_callback(module_operserv, "expire maskdata", do_expire_maskdata)
     || !add_callback(module_operserv, "HELP", do_help)
    ) {
	module_log("Unable to add callbacks");
	exit_module(0);
	return 0;
    }

    open_akill_db(AutokillDBName);
    db_opened = 1;

    if (ImmediatelySendAutokill) {
	MaskData *akill;
	for (akill = first_maskdata(MD_AKILL); akill;
	     akill = next_maskdata(MD_AKILL)
	) {
	    send_akill(akill);
	}
    }

    return 1;
}

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

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

    if (db_opened)
	close_akill_db(AutokillDBName);

    remove_callback(NULL, "save data", do_save_data);
    remove_callback(NULL, "user check", check_akill);
    remove_callback(NULL, "reconfigure", do_reconfigure);

    unregister_callback(module, cb_cancel_exclude);
    unregister_callback(module, cb_cancel_akill);
    unregister_callback(module, cb_send_exclude);
    unregister_callback(module, cb_send_akill);

    if (module_operserv) {
	remove_callback(module_operserv, "HELP", do_help);
	remove_callback(module_operserv, "expire maskdata",do_expire_maskdata);
	unregister_commands(module_operserv, cmds);
	unuse_module(module_operserv);
	module_operserv = NULL;
    }

    cmd_EXCLUDE->name = "EXCLUDE";
    return 1;
}

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