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

#include "services.h"
#include "modules.h"
#include "conffile.h"
#include "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 AkillChanExpiry;
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_akillchan(User *u);
static void do_exclude(User *u);

static Command cmds[] = {
    {"AKILL",     do_akill,     is_services_oper, OPER_HELP_AKILL,     -1,-1},
    {"AKILLCHAN", do_akillchan, is_services_oper, OPER_HELP_AKILLCHAN, -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;
    static int warned_exclude = 0;

    /* Don't send autokills if EnableExclude but no ircd support */
    if (EnableExclude && !(protocol_features & PF_AKILL_EXCL)) {
	if (!warned_exclude) {
	    wallops(s_OperServ, "Warning: Autokill exclusions are enabled,"
		    " but this IRC server does not support autokill"
		    " exclusions; autokills will not be sent to servers.");
	    module_log("EnableExclude on server type without exclusions--"
		       "autokill sending disabled");
	    warned_exclude = 1;
	}
	return;
    } else {
	warned_exclude = 0;
    }

    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) >= MAX_MASKDATA) {
	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, uint8 type, 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, uint8 type, char *mask, time_t *expiry_ptr)
{
    char *s, *t;
    time_t len;

#ifdef CLEAN_COMPILE
    type = type;
#endif
    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+1,'?') == NULL)
    ) {
	/* Username part matches anything; check host part */
	if (strchr(s,'*') != NULL && s[strspn(s,"*?.")] == 0
	 && ((t = strchr(mask,'.')) == NULL || strchr(t+1,'.') == 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 AKILLCHAN command. */

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

static void do_akillchan(User *u)
{
    char *channel, *expiry_str, *reason, *s;
    int kill;  /* kill users in the channel? */
    int32 expiry;
    Channel *c;
    struct c_userlist *cu, *cu2;
    int count;
    int old_immed;  /* saved value of ImmediatelySendAutokill */

    kill = 0;
    expiry_str = NULL;

    s = strtok(NULL, " ");
    if (s && stricmp(s,"KILL") == 0) {
	kill = 1;
	s = strtok(NULL, " ");
    }
    if (s && *s == '+') {
	expiry_str = s+1;
	s = strtok(NULL, " ");
    }
    if (!s || *s != '#') {
	syntax_error(s_OperServ, u, "AKILLCHAN", OPER_AKILLCHAN_SYNTAX);
	return;
    }
    channel = s;
    reason = strtok_remaining();
    if (!reason) {
	syntax_error(s_OperServ, u, "AKILLCHAN", OPER_AKILLCHAN_SYNTAX);
	return;
    }

    if (!(c = get_channel(channel))) {
	notice_lang(s_OperServ, u, CHAN_X_NOT_IN_USE, channel);
	return;
    }
    if (expiry_str) {
	expiry = dotime(expiry_str);
    } else {
	if (!is_services_admin(u) && OperMaxExpiry
	 && (!AkillChanExpiry || OperMaxExpiry < AkillChanExpiry)
	) {
	    expiry = OperMaxExpiry;
	} else {
	    expiry = AkillChanExpiry;
	}
    }
    if (expiry)
	expiry += time(NULL);

    if (WallOSAkill)
	wallops(s_OperServ, "%s used AKILLCHAN for \2%s\2", u->nick, c->name);

    count = 0;
    old_immed = ImmediatelySendAutokill;
    ImmediatelySendAutokill = 1;
    LIST_FOREACH_SAFE (cu, c->users, cu2) {
	char buf[BUFSIZE];
	if (is_oper(cu->user))
	    continue;
	/* Killing the user before adding the autokill opens a small hole
	 * in that the user may be able to reconnect before the new
	 * autokill reaches the server, but under normal conditions the
	 * actual chance of this is vanishingly small.  On the other hand,
	 * the chance of people complaining about "user not found" errors
	 * from ircds that kill users on autokill if we do the autokill
	 * first is significantly greater... */
	/* But as long as you kill the user first, make sure you save the
	 * hostname _before_ it gets freed, idiot. */
	snprintf(buf, sizeof(buf), "*@%s", cu->user->host);
	if (kill)
	    kill_user(s_OperServ, cu->user->nick, reason);
	if (!get_maskdata(MD_AKILL, buf))
	    create_akill(buf, reason, u->nick, expiry);
	count++;
    }
    ImmediatelySendAutokill = old_immed;

    if (count == 1) {
	notice_lang(s_OperServ, u,
		    kill ? OPER_AKILLCHAN_KILLED_ONE
			 : OPER_AKILLCHAN_AKILLED_ONE);
    } else {
	notice_lang(s_OperServ, u,
		    kill ? OPER_AKILLCHAN_KILLED : OPER_AKILLCHAN_AKILLED,
		    count);
    }
}

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

/* Handle an OperServ EXCLUDE command. */

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

static int check_add_exclude(User *u, uint8 type, 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, uint8 type, char *mask,
			     time_t *expiry_ptr)
{
    char *s;

#ifdef CLEAN_COMPILE
    type = type;
#endif
    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);
    }
    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 on connection to uplink server. */

static int do_connect(void)
{
    if (ImmediatelySendAutokill) {
	MaskData *akill;
	for (akill = first_maskdata(MD_AKILL); akill;
	     akill = next_maskdata(MD_AKILL)
	) {
	    send_akill(akill);
	}
    }
    return 0;
}

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

/* 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 1;
    }
    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;
    } else if (stricmp(param, "AKILLCHAN") == 0) {
	notice_help(s_OperServ, u, OPER_HELP_AKILLCHAN,
		    maketime(u->ngi, AkillChanExpiry, 0));
	return 1;
    }
    return 0;
}

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

const int32 module_version = MODULE_VERSION_CODE;

ConfigDirective module_config[] = {
    { "AkillChanExpiry",  { { CD_TIME, 0, &AkillChanExpiry } } },
    { "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)
{
    if (after_configure) {
	/* 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, "connect", do_connect)
     || !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;

    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, "connect", do_connect);
    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;
}

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


syntax highlighted by Code2HTML, v. 0.9.1