/*
 *   Unreal Internet Relay Chat Daemon, src/user.c
 *   Copyright (C) 1990 Jarkko Oikarinen and
 *                      University of Oulu, Computing Center
 *
 *   See file AUTHORS in IRC package for additional names of
 *   the programmers.
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 1, or (at your option)
 *   any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/** @file
 * @brief User-related functions
 */

/* s_user.c 2.74 2/8/94 (C) 1988 University of Oulu, Computing Center and Jarkko Oikarinen */

#include "unrealircd.h"

MODVAR int dontspread = 0;

/** Inhibit labeled/response reply. This means it will result in an empty ACK
 *  because we cannot handle the command via labeled-reponse. Rare, but
 *  possible in for example /TRACE which multiple servers handle and which
 *  has no clear end.
 */
MODVAR int labeled_response_inhibit = 0;

/** Force a labeled/response reply (of course, only if a label is present etc.).
 * This is used in case the "a remote server is handling the request" was
 * incorrect and there were 0 responses. This is the case for PRIVMSG.
 * It will force an empty ACK.
 * No, this cannot be merged with the other one. Also, the other one
 * (labeled_response_inhibit) has priority over this one (labeled_response_force).
 */
MODVAR int labeled_response_force = 0;

/** Inhibit labeled/response END. Only used in /LIST.
 */
MODVAR int labeled_response_inhibit_end = 0;

/* FIXME: in a future major release that allows module API changes,
 * add sendflags to HOOKTYPE_USERMSG and use a SKIP_ECHO sendflags
 * flag instead of this global. See HOOKTYPE_CHANMSG which already
 * has sendflags.
 */
MODVAR int echo_message_inhibit = 0;

/* FIXME: in a future major release that allows module API changes,
 * add SKIP_ECHO and SKIP_HISTORY sendflags flags instead of these
 * globals. See HOOKTYPE_CHANMSG which already has sendflags.
 */
MODVAR int history_inhibit = 0;

/** Set to 1 if an UTF8 incompatible nick character set is in use */
MODVAR int non_utf8_nick_chars_in_use = 0;

/** Set a new vhost on the user
 * @param client	The client (user)
 * @param host		The new vhost
 */
void iNAH_host(Client *client, const char *host)
{
	if (!client->user)
		return;

	userhost_save_current(client);

	safe_strdup(client->user->virthost, host);
	if (MyConnect(client))
		sendto_server(NULL, 0, 0, NULL, ":%s SETHOST :%s", client->id, client->user->virthost);
	client->umodes |= UMODE_SETHOST|UMODE_HIDE;

	userhost_changed(client);
}

/** Convert a user mode string to a bitmask - only used by config.
 * @param umode		The user mode string
 * @returns the user mode value (long)
 */
long set_usermode(const char *umode)
{
	Umode *um;
	int newumode;
	int what;
	const char *m;

	newumode = 0;
	what = MODE_ADD;
	for (m = umode; *m; m++)
	{
		switch (*m)
		{
			case '+':
				what = MODE_ADD;
				break;
			case '-':
				what = MODE_DEL;
				break;
			case ' ':
			case '\n':
			case '\r':
			case '\t':
				break;
			default:
				for (um = usermodes; um; um = um->next)
				{
					if (um->letter == *m)
					{
						if (what == MODE_ADD)
							newumode |= um->mode;
						else
							newumode &= ~um->mode;
					}
				}
		}
	}

	return newumode;
}

/** Convert a target pointer to an 8 bit hash, used for target limiting. */
unsigned char hash_target(void *target)
{
	uintptr_t v = (uintptr_t)target;
	/* ircu does >> 16 and 8 but since our sizeof(Client) is
	 * towards 512 (and hence the alignment), that bit is useless.
	 * So we do >> 17 and 9.
	 */
	return (unsigned char)((v >> 17) ^ (v >> 9));
}

/** target_limit_exceeded
 * @param client   The client.
 * @param target The target client
 * @param name   The name of the target client (used in the error message)
 * @retval Returns 1 if too many targets were addressed (do not send!), 0 if ok to send.
 */
int target_limit_exceeded(Client *client, void *target, const char *name)
{
	u_char hash = hash_target(target);
	int i;
	int max_concurrent_conversations_users, max_concurrent_conversations_new_user_every;
	FloodSettings *settings;

	if (ValidatePermissionsForPath("immune:max-concurrent-conversations",client,NULL,NULL,NULL))
		return 0;

	if (client->local->targets[0] == hash)
		return 0;

	settings = get_floodsettings_for_user(client, FLD_CONVERSATIONS);
	max_concurrent_conversations_users = settings->limit[FLD_CONVERSATIONS];
	max_concurrent_conversations_new_user_every = settings->period[FLD_CONVERSATIONS];

	if (max_concurrent_conversations_users <= 0)
		return 0; /* unlimited */

	/* Shouldn't be needed, but better check here than access out-of-bounds memory */
	if (max_concurrent_conversations_users > MAXCCUSERS)
		max_concurrent_conversations_users = MAXCCUSERS;

	for (i = 1; i < max_concurrent_conversations_users; i++)
	{
		if (client->local->targets[i] == hash)
		{
			/* Move this target hash to the first position */
			memmove(&client->local->targets[1], &client->local->targets[0], i);
			client->local->targets[0] = hash;
			return 0;
		}
	}

	if (TStime() < client->local->nexttarget)
	{
		/* Target limit reached */
		client->local->nexttarget += 2; /* punish them some more */
		add_fake_lag(client, 2000); /* lag them up as well */

		flood_limit_exceeded_log(client, "max-concurrent-conversations");
		sendnumeric(client, ERR_TARGETTOOFAST, name, (long long)(client->local->nexttarget - TStime()));

		return 1;
	}

	/* If not set yet or in the very past, then adjust it.
	 * This is so client->local->nexttarget=0 will become client->local->nexttarget=currenttime-...
	 */
	if (TStime() > client->local->nexttarget +
	    (max_concurrent_conversations_users * max_concurrent_conversations_new_user_every))
	{
		client->local->nexttarget = TStime() - ((max_concurrent_conversations_users-1) * max_concurrent_conversations_new_user_every);
	}

	client->local->nexttarget += max_concurrent_conversations_new_user_every;

	/* Add the new target (first move the rest, then add us at position 0 */
	memmove(&client->local->targets[1], &client->local->targets[0], max_concurrent_conversations_users - 1);
	client->local->targets[0] = hash;

	return 0;
}

/** De-duplicate a string of "x,x,y,z" to "x,y,z"
 * @param buffer	Input string
 * @returns The new de-duplicated buffer (temporary storage, only valid until next canonize call)
 */
char *canonize(const char *buffer)
{
	static char cbuf[2048];
	char tbuf[2048];
	char *s, *t, *cp = cbuf;
	int  l = 0;
	char *p = NULL, *p2;

	*cp = '\0';

	if (!buffer)
		return NULL;

	strlcpy(tbuf, buffer, sizeof(tbuf));
	for (s = strtoken(&p, tbuf, ","); s; s = strtoken(&p, NULL, ","))
	{
		if (l)
		{
			for (p2 = NULL, t = strtoken(&p2, cbuf, ","); t;
			    t = strtoken(&p2, NULL, ","))
				if (!mycmp(s, t))
					break;
				else if (p2)
					p2[-1] = ',';
		}
		else
			t = NULL;
		if (!t)
		{
			if (l)
				*(cp - 1) = ',';
			else
				l = 1;
			strcpy(cp, s);
			if (p)
				cp += (p - s);
		}
		else if (p2)
			p2[-1] = ',';
	}
	return cbuf;
}

/** Get user modes as a string.
 * @param client	The client
 * @returns string of user modes (temporary storage)
 */
const char *get_usermode_string(Client *client)
{
	static char buf[128];
	Umode *um;

	strlcpy(buf, "+", sizeof(buf));
	for (um = usermodes; um; um = um->next)
		if (client->umodes & um->mode)
			strlcat_letter(buf, um->letter, sizeof(buf));

	return buf;
}

/** Get user modes as a string - buffer is specified.
 * @param client	The client
 * @param buf		The buffer to write to
 * @param buflen	The size of the buffer
 * @returns string of user modes (buf)
 */
const char *get_usermode_string_r(Client *client, char *buf, size_t buflen)
{
	Umode *um;

	strlcpy(buf, "+", buflen);
	for (um = usermodes; um; um = um->next)
		if (client->umodes & um->mode)
			strlcat_letter(buf, um->letter, buflen);

	return buf;
}

/** Get user modes as a string - this one does not work on 'client' but directly on 'umodes'.
 * @param umodes	The user modes that are set
 * @returns string of user modes (temporary storage)
 */
const char *get_usermode_string_raw(long umodes)
{
	static char buf[128];
	Umode *um;

	strlcpy(buf, "+", sizeof(buf));
	for (um = usermodes; um; um = um->next)
		if (umodes & um->mode)
			strlcat_letter(buf, um->letter, sizeof(buf));

	return buf;
}

/** Get user modes as a string - this one does not work on 'client' but directly on 'umodes'.
 * @param umodes	The user modes that are set
 * @param buf		The buffer to write to
 * @param buflen	The size of the buffer
 * @returns string of user modes (buf)
 */
const char *get_usermode_string_raw_r(long umodes, char *buf, size_t buflen)
{
	Umode *um;

	strlcpy(buf, "+", buflen);
	for (um = usermodes; um; um = um->next)
		if (umodes & um->mode)
			strlcat_letter(buf, um->letter, buflen);

	return buf;
}


/** Set a new snomask on the user.
 * The user is not informed of the change by this function.
 * @param client	The client
 * @param snomask	The snomask to add or delete (eg: "+k-c")
 */
void set_snomask(Client *client, const char *snomask)
{
	int what = MODE_ADD; /* keep this an int. -- Syzop */
	const char *p;
	int i;

	if (snomask == NULL)
	{
		remove_all_snomasks(client);
		return;
	}
	
	for (p = snomask; p && *p; p++)
	{
		switch (*p)
		{
			case '+':
				what = MODE_ADD;
				break;
			case '-':
				what = MODE_DEL;
				break;
			default:
				if (what == MODE_ADD)
				{
					if (!isalpha(*p) || !is_valid_snomask(*p))
						continue;
					addlettertodynamicstringsorted(&client->user->snomask, *p);
				} else {
					delletterfromstring(client->user->snomask, *p);
				}
				break;
		}
	}
	/* If the snomask becomes empty ("") then set it to NULL and user mode -s */
	if (client->user->snomask && !*client->user->snomask)
		remove_all_snomasks(client);
}

/** Build the MODE line with (modified) user modes for this user.
 * @author Originally by avalon.
 */
void build_umode_string(Client *client, long old, long sendmask, char *umode_buf)
{
	Umode *um;
	long flag;
	char *m;
	int what = MODE_NULL;

	/*
	 * build a string in umode_buf to represent the change in the user's
	 * mode between the new (client->flag) and 'old'.
	 */
	m = umode_buf;
	*m = '\0';
	for (um = usermodes; um; um = um->next)
	{
		flag = um->mode;
		if (MyUser(client) && !(flag & sendmask))
			continue;
		if ((flag & old) && !(client->umodes & flag))
		{
			if (what == MODE_DEL)
				*m++ = um->letter;
			else
			{
				what = MODE_DEL;
				*m++ = '-';
				*m++ = um->letter;
			}
		}
		else if (!(flag & old) && (client->umodes & flag))
		{
			if (what == MODE_ADD)
				*m++ = um->letter;
			else
			{
				what = MODE_ADD;
				*m++ = '+';
				*m++ = um->letter;
			}
		}
	}
	*m = '\0';
}

/** Send usermode change to other servers.
 * @param client	The client
 * @param show_to_user	Set to 1 to show the MODE change to the user
 * @param old		The old user modes set on the client
 */
void send_umode_out(Client *client, int show_to_user, long old)
{
	Client *acptr;
	char buf[512];

	build_umode_string(client, old, SEND_UMODES, buf);

	list_for_each_entry(acptr, &server_list, special_node)
	{
		if ((acptr != client) && (acptr != client->direction) && *buf)
		{
			sendto_one(acptr, NULL, ":%s UMODE2 %s",
			           client->name, buf);
		}
	}

	if (MyUser(client) && show_to_user)
	{
		build_umode_string(client, old, ALL_UMODES, buf);
		if (*buf)
			sendto_one(client, NULL, ":%s MODE %s :%s", client->name, client->name, buf);
	}
}

static MaxTarget *maxtargets = NULL; /**< For set::max-targets-per-command configuration */

static void maxtarget_add_sorted(MaxTarget *n)
{
	MaxTarget *e;

	if (!maxtargets)
	{
		maxtargets = n;
		return;
	}

	for (e = maxtargets; e; e = e->next)
	{
		if (strcmp(n->cmd, e->cmd) < 0)
		{
			/* Insert us before */
			if (e->prev)
				e->prev->next = n;
			else
				maxtargets = n; /* new head */
			n->prev = e->prev;

			n->next = e;
			e->prev = n;
			return;
		}
		if (!e->next)
		{
			/* Append us at end */
			e->next = n;
			n->prev = e;
			return;
		}
	}
}

/** Find a maxtarget structure for a cmd (internal) */
MaxTarget *findmaxtarget(const char *cmd)
{
	MaxTarget *m;

	for (m = maxtargets; m; m = m->next)
		if (!strcasecmp(m->cmd, cmd))
			return m;
	return NULL;
}

/** Set a maximum targets per command restriction */
void setmaxtargets(const char *cmd, int limit)
{
	MaxTarget *m = findmaxtarget(cmd);
	if (!m)
	{
		char cmdupper[64];
		strlcpy(cmdupper, cmd, sizeof(cmdupper));
		strtoupper(cmdupper);
		m = safe_alloc(sizeof(MaxTarget));
		safe_strdup(m->cmd, cmdupper);
		maxtarget_add_sorted(m);
	}
	m->limit = limit;
}

/** Free all set::max-targets-per-command configuration (internal) */
void freemaxtargets(void)
{
	MaxTarget *m, *m_next;

	for (m = maxtargets; m; m = m_next)
	{
		m_next = m->next;
		safe_free(m->cmd);
		safe_free(m);
	}
	maxtargets = NULL;
}

/** Return the maximum number of targets permitted for a command */
int max_targets_for_command(const char *cmd)
{
	MaxTarget *m = findmaxtarget(cmd);
	if (m)
		return m->limit;
	return 1; /* default to 1 */
}

void set_isupport_targmax(void)
{
	char buf[512], tbuf[64];
	MaxTarget *m;

	*buf = '\0';
	for (m = maxtargets; m; m = m->next)
	{
		if (m->limit == MAXTARGETS_MAX)
			snprintf(tbuf, sizeof(tbuf), "%s:", m->cmd);
		else
			snprintf(tbuf, sizeof(tbuf), "%s:%d", m->cmd, m->limit);

		if (*buf)
			strlcat(buf, ",", sizeof(buf));
		strlcat(buf, tbuf, sizeof(buf));
	}
	ISupportSet(NULL, "TARGMAX", buf);
}

/** Called between config test and config run */
void set_targmax_defaults(void)
{
	/* Free existing... */
	freemaxtargets();

	/* Set the defaults */
	setmaxtargets("PRIVMSG", 4);
	setmaxtargets("NOTICE", 1);
	setmaxtargets("TAGMSG", 1);
	setmaxtargets("NAMES", 1); // >1 is not supported
	setmaxtargets("WHOIS", 1);
	setmaxtargets("WHOWAS", 1); // >1 is not supported
	setmaxtargets("KICK", 4);
	setmaxtargets("LIST", MAXTARGETS_MAX);
	setmaxtargets("JOIN", MAXTARGETS_MAX);
	setmaxtargets("PART", MAXTARGETS_MAX);
	setmaxtargets("SAJOIN", MAXTARGETS_MAX);
	setmaxtargets("SAPART", MAXTARGETS_MAX);
	setmaxtargets("KILL", MAXTARGETS_MAX);
	setmaxtargets("DCCALLOW", MAXTARGETS_MAX);
	/* The following 3 are space-separated (and actually the previous
	 * mentioned DCCALLOW is both space-and-comma separated).
	 * It seems most IRCd's don't list space-separated targets list
	 * in TARGMAX... On the other hand, why not? It says nowhere in
	 * the TARGMAX specification that it's only for comma-separated
	 * commands. So let's be nice and consistent and inform the
	 * clients about the limits for such commands as well:
	 */
	setmaxtargets("USERHOST", MAXTARGETS_MAX); // not configurable
	setmaxtargets("USERIP", MAXTARGETS_MAX); // not configurable
	setmaxtargets("ISON", MAXTARGETS_MAX); // not configurable
	setmaxtargets("WATCH", MAXTARGETS_MAX); // not configurable
}

/** Is the user handshake finished and can register_user() be called?
 * This checks things like: do we have a NICK, USER, nospoof,
 * and any other things modules may add:
 * eg: the cap module checks if client capability negotiation
 * is in progress
 */
int is_handshake_finished(Client *client)
{
	Hook *h;
	int n;

	for (h = Hooks[HOOKTYPE_IS_HANDSHAKE_FINISHED]; h; h = h->next)
	{
		n = (*(h->func.intfunc))(client);
		if (n == 0)
			return 0; /* We can stop already */
	}

	/* I figured these can be here, in the core: */
	if (client->user && *client->user->username && client->name[0] && IsNotSpoof(client))
		return 1;

	return 0;
}

/** Should we show connection info to the user?
 * This depends on the set::show-connect-info setting but also
 * on various other properties, such as serversonly ports,
 * websocket, etc.
 * If someone needs it, then we can also call a hook here. Just tell us.
 */
int should_show_connect_info(Client *client)
{
	if (SHOWCONNECTINFO &&
	    !client->server &&
	    !IsServersOnlyListener(client->local->listener) &&
	    !client->local->listener->websocket_options)
	{
		return 1;
	}
	return 0;
}

/* (helper function for uid_get) */
static char uid_int_to_char(int v)
{
	if (v < 10)
		return '0'+v;
	else
		return 'A'+v-10;
}

/** Acquire a new unique UID */
const char *uid_get(void)
{
	Client *acptr;
	static char uid[IDLEN+1];
	static int uidcounter = 0;

	uidcounter++;
	if (uidcounter == 36*36)
		uidcounter = 0;

	do
	{
		snprintf(uid, sizeof(uid), "%s%c%c%c%c%c%c",
			me.id,
			uid_int_to_char(getrandom8() % 36),
			uid_int_to_char(getrandom8() % 36),
			uid_int_to_char(getrandom8() % 36),
			uid_int_to_char(getrandom8() % 36),
			uid_int_to_char(uidcounter / 36),
			uid_int_to_char(uidcounter % 36));
		acptr = find_client(uid, NULL);
	} while (acptr);

	return uid;
}

/** Get cloaked host for user */
const char *getcloak(Client *client)
{
	if (!*client->user->cloakedhost)
	{
		/* need to calculate (first-time) */
		make_cloakedhost(client, client->user->realhost, client->user->cloakedhost, sizeof(client->user->cloakedhost));
	}

	return client->user->cloakedhost;
}

/** Calculate the cloaked host for a client.
 * @param client	The client
 * @param curr		The real host or real IP
 * @param buf		Buffer to store the new cloaked host in
 * @param buflen	Length of the buffer (should be HOSTLEN+1)
 */
void make_cloakedhost(Client *client, const char *curr, char *buf, size_t buflen)
{
	const char *p;
	char host[256], *q;
	const char *mask;

	/* Convert host to lowercase and cut off at 255 bytes just to be sure */
	for (p = curr, q = host; *p && (q < host+sizeof(host)-1); p++, q++)
		*q =  tolower(*p);
	*q = '\0';

	/* Call the cloaking layer */
	if (RCallbacks[CALLBACKTYPE_CLOAK_EX] != NULL)
		mask = RCallbacks[CALLBACKTYPE_CLOAK_EX]->func.stringfunc(client, host);
	else if (RCallbacks[CALLBACKTYPE_CLOAK] != NULL)
		mask = RCallbacks[CALLBACKTYPE_CLOAK]->func.stringfunc(host);
	else
		mask = curr;

	strlcpy(buf, mask, buflen);
}

/** Called after a user is logged in (or out) of a services account */
void user_account_login(MessageTag *recv_mtags, Client *client)
{
	if (MyConnect(client))
	{
		find_shun(client);
		if (find_tkline_match(client, 0) && IsDead(client))
			return;
	}
	update_known_user_cache(client);
	RunHook(HOOKTYPE_ACCOUNT_LOGIN, client, recv_mtags);
}

/** Should we hide the idle time of 'target' to user 'client'?
 * This depends on the set::hide-idle-time policy.
 */
int hide_idle_time(Client *client, Client *target)
{
	/* First of all, IRCOps bypass the restriction */
	if (IsOper(client))
		return 0;

	/* Other than that, it depends on the settings: */
	switch (iConf.hide_idle_time)
	{
		case HIDE_IDLE_TIME_NEVER:
			return 0;
		case HIDE_IDLE_TIME_ALWAYS:
			return 1;
		case HIDE_IDLE_TIME_USERMODE:
		case HIDE_IDLE_TIME_OPER_USERMODE:
			if (target->umodes & UMODE_HIDLE)
				return 1;
			return 0;
		default:
			return 0;
	}
}

/** Get creation time of a client.
 * @param client	The client to check (user, server, anything)
 * @returns the time when the client first connected to IRC, or 0 for unknown.
 */
time_t get_creationtime(Client *client)
{
	const char *str;

	/* Shortcut for local clients */
	if (client->local)
		return client->local->creationtime;

	/* Otherwise, hopefully available through this... */
	str = moddata_client_get(client, "creationtime");
	if (!BadPtr(str) && (*str != '0'))
		return atoll(str);
	return 0;
}

/** Get how long a client is connected to IRC.
 * @param client	The client to check
 * @returns how long the client is connected to IRC (number of seconds)
 */
long get_connected_time(Client *client)
{
	const char *str;
	long connect_time = 0;

	/* Shortcut for local clients */
	if (client->local)
		return TStime() - client->local->creationtime;

	/* Otherwise, hopefully available through this... */
	str = moddata_client_get(client, "creationtime");
	if (!BadPtr(str) && (*str != '0'))
		return TStime() - atoll(str);
	return 0;
}

/** Return extended information about user for the "Client connecting" line.
 * @returns A string such as "[secure] [reputation: 5]", never returns NULL.
 */
const char *get_connect_extinfo(Client *client)
{
	static char retbuf[512];
	char tmp[512];
	const char *s;
	const char *secgroups;
	NameValuePrioList *list = NULL, *e;

	/* From modules... */
	RunHook(HOOKTYPE_CONNECT_EXTINFO, client, &list);

	/* And some built-in: */

	/* "vhost": this should be first */
	if (IsHidden(client))
		add_nvplist(&list, -1000000, "vhost", client->user->virthost);

	/* "class": second */
	if (MyUser(client) && client->local->class)
		add_nvplist(&list, -100000, "class", client->local->class->name);

	/* "secure": TLS */
	s = tls_get_cipher(client);
	if (s)
		add_nvplist(&list, -1000, "secure", s);
	else if (IsSecure(client) || IsSecureConnect(client))
		add_nvplist(&list, -1000, "secure", NULL); /* old server or otherwise no details (eg: fake secure) */

	/* services account? */
	if (IsLoggedIn(client))
		add_nvplist(&list, -500, "account", client->user->account);

	/* security groups */
	secgroups = get_security_groups(client);
	if (secgroups)
		add_nvplist(&list, 100, "security-groups", secgroups);
	
	/* tkl shunned */
	if (IsShunned(client))
		add_nvplist(&list, 110, "shunned", NULL);

	*retbuf = '\0';
	for (e = list; e; e = e->next)
	{
		if (e->value)
			snprintf(tmp, sizeof(tmp), "[%s: %s] ", e->name, e->value);
		else
			snprintf(tmp, sizeof(tmp), "[%s] ", e->name);
		strlcat(retbuf, tmp, sizeof(retbuf));
	}
	/* Cut off last space (unless empty string) */
	if (*retbuf)
		retbuf[strlen(retbuf)-1] = '\0';

	/* Free the list, as it was only used to build retbuf */
	free_nvplist(list);

	return retbuf;
}

/** Log a message that flood protection kicked in for the client.
 * This sends to the +f snomask at the moment.
 * @param client	The client to check flood for (local user)
 * @param opt		The flood option (eg FLD_AWAY)
 */
void flood_limit_exceeded_log(Client *client, const char *floodname)
{
	char buf[1024];

	// NOTE: If you ever change this format, there are a few more
	// direct unreal_log() calls with "FLOOD_BLOCKED" in the file
	// src/modules/targetfloodprot.c, so update those as well.
	unreal_log(ULOG_INFO, "flood", "FLOOD_BLOCKED", client,
	           "Flood blocked ($flood_type) from $client.details [$client.ip]",
	           log_data_string("flood_type", floodname));
}

/** Is the flood limit exceeded for an option? eg for away-flood.
 * @param client	The client to check flood for (local user)
 * @param opt		The flood option (eg FLD_AWAY)
 * @note This increments the flood counter as well.
 * @returns 1 if exceeded, 0 if not.
 */
int flood_limit_exceeded(Client *client, FloodOption opt)
{
	FloodSettings *f;

	if (!MyUser(client))
		return 0;

	f = get_floodsettings_for_user(client, opt);
	if (f->limit[opt] <= 0)
		return 0; /* No limit set or unlimited */

	/* Ok, let's do the flood check */
	if ((client->local->flood[opt].t + f->period[opt]) <= timeofday)
	{
		/* Time exceeded, reset */
		client->local->flood[opt].count = 0;
		client->local->flood[opt].t = timeofday;
	}
	if (client->local->flood[opt].count <= f->limit[opt])
		client->local->flood[opt].count++;
	if (client->local->flood[opt].count > f->limit[opt])
	{
		flood_limit_exceeded_log(client, floodoption_names[opt]);
		return 1; /* Flood limit hit! */
	}

	return 0;
}

/** Get the appropriate anti-flood settings block for this user.
 * @param client	The client, should be locally connected.
 * @param opt		The flood option we are interested in
 * @returns The FloodSettings for this user, never returns NULL.
 */
FloodSettings *get_floodsettings_for_user(Client *client, FloodOption opt)
{
	SecurityGroup *s;
	FloodSettings *f;

	/* Go through all security groups by order of priority
	 * (eg: first "known-users", then "unknown-users").
	 * For each of these:
	 * - Check if a set::anti-flood::xxxx block exists for this group
	 * - Check if the limit is non-zero (eg there is any limit set)
	 * If any of these are false then we continue with next block
	 * that matches.
	 */

	// XXX: alternatively, instead of this double loop,
	//      do a post-conf thing and sort iConf.floodsettings
	//      according to the security-group { } order.
	for (s = securitygroups; s; s = s->next)
	{
		if (user_allowed_by_security_group(client, s) &&
		    ((f = find_floodsettings_block(s->name))) &&
		    f->limit[opt])
		{
			return f;
		}
	}

	/* Return default settings block (which may have a zero limit set) */
	f = find_floodsettings_block("unknown-users");
	if (!f)
		abort(); /* impossible */

	return f;
}

MODVAR const char *floodoption_names[] = {
	"nick-flood",
	"join-flood",
	"away-flood",
	"invite-flood",
	"knock-flood",
	"max-concurrent-conversations",
	"lag-penalty",
	"vhost-flood",
	"multiline",
	NULL
};

/** Lookup GEO information for an IP address.
 * @param ip	The IP to lookup
 * @returns A struct containing all the details. Must be freed by caller!
 */
GeoIPResult *geoip_lookup(const char *ip)
{
	if (!RCallbacks[CALLBACKTYPE_GEOIP_LOOKUP])
		return NULL;
	return RCallbacks[CALLBACKTYPE_GEOIP_LOOKUP]->func.pvoidfunc(ip);
}

void free_geoip_result(GeoIPResult *r)
{
	if (!r)
		return;
	safe_free(r->country_code);
	safe_free(r->country_name);
	safe_free(r->asname);
	safe_free(r);
}

/** Grab geoip information for client */
GeoIPResult *geoip_client(Client *client)
{
	ModData *m = moddata_client_get_raw(client, "geoip");
	if (!m)
		return NULL;
	return m->ptr; /* can still be NULL */
}

/** Get the oper block that was used to become OPER.
 * @param client	The client to fetch the info for
 * @returns the oper block name (eg: "OpEr") or NULL.
 */
const char *get_operlogin(Client *client)
{
	if (client->user->operlogin)
		return client->user->operlogin;
	return moddata_client_get(client, "operlogin");
}

/** Get the operclass of the IRCOp.
 * @param client	The client to fetch the info for
 * @returns the operclass name or NULL
 */
const char *get_operclass(Client *client)
{
	const char *operlogin = NULL;

	if (MyUser(client) && client->user->operlogin)
	{
		ConfigItem_oper *oper;
		operlogin = client->user->operlogin;
		oper = find_oper(operlogin);
		if (oper && oper->operclass)
			return oper->operclass;
	}

	/* Remote user or locally no longer available
	 * (eg oper block removed but user is still oper)
	 */
	return moddata_client_get(client, "operclass");
}

/** Find a tag - case insensitive comparisson. */
Tag *find_tag(Client *client, const char *name)
{
	Tag *e;

	if (!MyConnect(client))
		return NULL;

	for (e = client->local->tags; e; e = e->next)
	{
		if (!strcasecmp(e->name, name))
		{
			return e;
		}
	}
	return NULL;
}

void bump_tag_serial(Client *client)
{
	client->local->tags_serial++;
	if (client->local->tags_serial > 10000)
		client->local->tags_serial = 0;
}

Tag *add_tag(Client *client, const char *name, int value)
{
	Tag *e = safe_alloc(sizeof(Tag)+strlen(name)+1);
	strcpy(e->name, name); /* safe, allocated above */
	e->value = value;
	AddListItem(e, client->local->tags);
	bump_tag_serial(client);
	return e;
}

void free_all_tags(Client *client)
{
	Tag *n, *n_next;

	for (n = client->local->tags; n; n = n_next)
	{
		n_next = n->next;
		safe_free(n);
	}
	bump_tag_serial(client);
}

void del_tag(Client *client, const char *name)
{
	Tag *e = find_tag(client, name);
	if (e)
	{
		DelListItem(e, client->local->tags);
		safe_free(e);
		bump_tag_serial(client);
		return;
	}
}

int inchannel_compareflags(char symbol, const char *member_modes)
{
	const char *required_modes = NULL;

	if (symbol == '+')
		required_modes = "vhoaq";
	else if (symbol == '%')
		required_modes = "hoaq";
	else if (symbol == '@')
		required_modes = "oaq";
	else if (symbol == '&')
		required_modes = "aq";
	else if (symbol == '~')
		required_modes = "q";
	else
		return 0; /* unknown prefix character */

	if (check_channel_access_string(member_modes, required_modes))
		return 1;

	return 0;
}

/** Walk through all channels the user is in and return the highest member count of all those channels */
int highest_channel_member_count(Client *client)
{
	Membership *m;
	int highest = 0;

	for (m = client->user->channel; m; m=m->next)
		if (m->channel->users > highest)
			highest = m->channel->users;

	return highest;
}

/** Update the client->known_user_cached value and call
 * HOOKTYPE_KNOWN_USER_CACHE_CHANGE hooks if it changed.
 * @param client	Client
 * @returns 1 if all OK, 0 if client was killed via dead_socket().
 */
int update_known_user_cache(Client *client)
{
	char oldvalue = client->known_user_cached;
	Hook *h;

	client->known_user_cached = user_allowed_by_security_group_name(client, "known-users") ? 1 : 0;

	if (client->known_user_cached != oldvalue)
	{
		for (h = Hooks[HOOKTYPE_KNOWN_USER_CACHE_CHANGE]; h; h = h->next)
		{
			int n = h->func.intfunc(client);
			if (n == HOOK_DENY)
			{
				/* When using HOOK_DENY, the client must be killed
				 * by dead_socket().
				 * A) It should not be forgotten
				 * B) Not by exit_client() since that is dangerous.
				 */
				if (!IsDeadSocket(client) || IsDead(client))
				{
					unreal_log(ULOG_WARNING, "user", "BUG_HOOKTYPE_KNOWN_USER_CACHE_CHANGE", client,
					           "Module $module returned HOOK_DENY but did not use dead_socket(). Dangerous!",
					           log_data_string("module", h->owner->header->name));
				}
#ifdef DEBUGMODE
				if (IsDead(client))
					abort(); /* You should have used dead_socket() and not exit_client() */
#endif
				if (!IsDeadSocket(client))
				{
#ifdef DEBUGMODE
					abort(); /* You should have used dead_socket() */
#else
					dead_socket(client, "Invalid user cache callback");
#endif
				}
				return 0;
			}
		}
	}

	return 1;
}
