/*
 *  ircd-ratbox: A slightly useful ircd.
 *  m_message.c: Sends a (PRIVMSG|NOTICE) message to a user or channel.
 *
 *  Copyright (C) 1990 Jarkko Oikarinen and University of Oulu, Co Center
 *  Copyright (C) 1996-2002 Hybrid Development Team
 *  Copyright (C) 2002-2005 ircd-ratbox development team
 *
 *  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 2 of the License, 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 *  USA
 */

#include "stdinc.h"
#include "client.h"
#include "ircd.h"
#include "numeric.h"
#include "s_conf.h"
#include "s_serv.h"
#include "msg.h"
#include "parse.h"
#include "modules.h"
#include "channel.h"
#include "match.h"
#include "hash.h"
#include "class.h"
#include "msg.h"
#include "packet.h"
#include "send.h"
#include "s_newconf.h"
#include "s_stats.h"
#include "tgchange.h"
#include "client_tags.h"
#include "response.h"
#include "s_assert.h"
#include "inline/stringops.h"

static const char message_desc[] =
	"Provides the PRIVMSG, NOTICE, TAGMSG commands to send messages to users and channels";

static const char shortname[MESSAGE_TYPE_COUNT] = {
	[MESSAGE_TYPE_PRIVMSG] = 'P',
	[MESSAGE_TYPE_NOTICE] = 'N',
	[MESSAGE_TYPE_TAGMSG] = 'T',
};

static const char *cmdname[MESSAGE_TYPE_COUNT] = {
	[MESSAGE_TYPE_PRIVMSG] = "PRIVMSG",
	[MESSAGE_TYPE_NOTICE] = "NOTICE",
	[MESSAGE_TYPE_TAGMSG] = "TAGMSG"
};

static const char *cmdname_lower[MESSAGE_TYPE_COUNT] = {
	[MESSAGE_TYPE_PRIVMSG] = "privmsg",
	[MESSAGE_TYPE_NOTICE] = "notice",
	[MESSAGE_TYPE_TAGMSG] = "tagmsg"
};

static void m_message(enum message_type, struct MsgBuf *, struct Client *, struct Client *, int, const char **);
static void m_privmsg(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
static void m_notice(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
static void m_tagmsg(struct MsgBuf *, struct Client *, struct Client *, int, const char **);
static void m_echo(struct MsgBuf *, struct Client *, struct Client *, int, const char **);

static void echo_msg(struct Client *, struct Client *, enum message_type, const char *text, struct MsgBuf *msgbuf_p);

static void expire_tgchange(void *unused);
static struct ev_entry *expire_tgchange_event;

static uint64_t CAP_ECHO;

struct entity
{
	union
	{
		struct Client *target_p;
		struct Channel *chptr;
	};
	char mask[NICKLEN + HOSTLEN + 2];
	int type;
	int flags;
};

static int
modinit(void)
{
	expire_tgchange_event = rb_event_addish("expire_tgchange", expire_tgchange, NULL, 300);
	expire_tgchange(NULL);
	return 0;
}

static void
moddeinit(void)
{
	rb_event_delete(expire_tgchange_event);
}

struct Message privmsg_msgtab = {
	"PRIVMSG", 0, 0, 0, 0,
	{mg_unreg, {m_privmsg, 0}, {m_privmsg, 0}, mg_ignore, mg_ignore, {m_privmsg, 0}}
};
struct Message notice_msgtab = {
	"NOTICE", 0, 0, 0, 0,
	{mg_unreg, {m_notice, 0}, {m_notice, 0}, {m_notice, 0}, mg_ignore, {m_notice, 0}}
};
struct Message tagmsg_msgtab = {
	"TAGMSG", 0, 0, 0, 0,
	{mg_unreg, {m_tagmsg, 0}, {m_tagmsg, 0}, mg_ignore, mg_ignore, {m_tagmsg, 0}}
};
struct Message echo_msgtab = {
	"ECHO", 0, 0, 0, 0,
	{mg_unreg, mg_ignore, {m_echo, 3}, mg_ignore, mg_ignore, mg_ignore}
};

mapi_clist_av1 message_clist[] = { &privmsg_msgtab, &notice_msgtab, &echo_msgtab, &tagmsg_msgtab, NULL };

mapi_cap_list_av2 message_cap_list[] = {
	{ MAPI_CAP_SERVER, "ECHO", NULL, &CAP_ECHO },
	{ 0, NULL, NULL, NULL }
};

DECLARE_MODULE_AV2(message, modinit, moddeinit, message_clist, NULL, NULL, message_cap_list, NULL, message_desc);

static int build_target_list(enum message_type msgtype,
			     struct Client *client_p,
			     struct Client *source_p, const char *nicks_channels, const char *text,
			     struct MsgBuf *msgbuf_p);

static bool flood_attack_client(enum message_type msgtype, struct Client *source_p, struct Client *target_p);

/* Fifteen seconds should be plenty for a client to reply a ctcp */
#define LARGE_CTCP_TIME 15

#define ENTITY_NONE    0
#define ENTITY_CHANNEL 1
#define ENTITY_CHANNEL_OPMOD 2
#define ENTITY_CHANOPS_ON_CHANNEL 3
#define ENTITY_CLIENT  4
#define ENTITY_TARGET_ON_SERVER 5
#define ENTITY_MASS_MESSAGE 6

static struct entity targets[512];
static int ntargets = 0;

static void msg_channel(enum message_type msgtype,
			struct Client *client_p,
			struct Client *source_p, struct Channel *chptr, const char *text,
			struct MsgBuf *msgbuf_p);

static void msg_channel_opmod(enum message_type msgtype,
			      struct Client *client_p,
			      struct Client *source_p, struct Channel *chptr, const char *text,
			      struct MsgBuf *msgbuf_p);

static void msg_channel_flags(enum message_type msgtype,
			      struct Client *client_p,
			      struct Client *source_p,
			      struct Channel *chptr, int flags, const char *text,
			      struct MsgBuf *msgbuf_p);

static void msg_client(enum message_type msgtype,
		       struct Client *source_p, struct Client *target_p, const char *text,
		       struct MsgBuf *msgbuf_p);

static void msg_client_targeted(enum message_type msgtype,
	struct Client *source_p, const char *nick, struct Client *target_p,
	const char *text, struct MsgBuf *msgbuf_p);

static void msg_mass(enum message_type msgtype, struct Client *client_p,
	struct Client *source_p, const char *mask, const char *text, struct MsgBuf *msgbuf_p);

/*
** m_privmsg
**
** massive cleanup
** rev argv 6/91
**
**   Another massive cleanup Nov, 2000
** (I don't think there is a single line left from 6/91. Maybe.)
** m_privmsg and m_notice do basically the same thing.
** in the original 2.8.2 code base, they were the same function
** "m_message.c." When we did the great cleanup in conjuncton with bleep
** of ircu fame, we split m_privmsg.c and m_notice.c.
** I don't see the point of that now. Its harder to maintain, its
** easier to introduce bugs into one version and not the other etc.
** Really, the penalty of an extra function call isn't that big a deal folks.
** -db Nov 13, 2000
**
*/

static void
m_privmsg(struct MsgBuf *msgbuf_p, struct Client *client_p, struct Client *source_p, int parc, const char *parv[])
{
	m_message(MESSAGE_TYPE_PRIVMSG, msgbuf_p, client_p, source_p, parc, parv);
}

static void
m_notice(struct MsgBuf *msgbuf_p, struct Client *client_p, struct Client *source_p, int parc, const char *parv[])
{
	m_message(MESSAGE_TYPE_NOTICE, msgbuf_p, client_p, source_p, parc, parv);
}


static void
m_tagmsg(struct MsgBuf *msgbuf_p, struct Client *client_p, struct Client *source_p, int parc, const char *parv[]) {
	if (MyClient(source_p) && !IsClientCapable(source_p, CLICAP_MESSAGE_TAGS))
		return;

	m_message(MESSAGE_TYPE_TAGMSG, msgbuf_p, client_p, source_p, parc, parv);
}


/*
 * inputs	- flag privmsg or notice
 *		- pointer to client_p
 *		- pointer to source_p
 *		- pointer to channel
 */
static void
m_message(enum message_type msgtype, struct MsgBuf *msgbuf_p,
	  struct Client *client_p, struct Client *source_p, int parc, const char *parv[])
{
	/* let hooks work on a copy of msgbuf_p so they don't mutate the tags in incoming_message */
	struct MsgBuf msgbuf;
	memcpy(&msgbuf, msgbuf_p, sizeof(struct MsgBuf));

	if (parc < 2 || EmptyString(parv[1]))
	{
		if (msgtype != MESSAGE_TYPE_NOTICE)
			sendto_one(source_p, form_str(ERR_NORECIPIENT), me.name,
				   source_p->name, cmdname[msgtype]);
		return;
	}

	if (msgtype != MESSAGE_TYPE_TAGMSG && (parc < 3 || EmptyString(parv[2])))
	{
		if (msgtype == MESSAGE_TYPE_PRIVMSG)
			sendto_one(source_p, form_str(ERR_NOTEXTTOSEND), me.name, source_p->name);
		return;
	}

	if (msgtype == MESSAGE_TYPE_TAGMSG)
	{
		bool found_client_tag = false;
		for (int i = 0; i < msgbuf_p->n_tags; i++)
		{
			if (*msgbuf_p->tags[i].key == '+')
			{
				found_client_tag = true;
				break;
			}
		}

		if (!found_client_tag)
			return;
	}

	/* Finish the flood grace period if they're not messaging themselves
	 * as some clients (ircN) do this as a "lag check"
	 */
	if (MyClient(source_p) && !IsFloodDone(source_p) && irccmp(source_p->name, parv[1]))
		flood_endgrace(source_p);

	/* Lots of things dereference text and it'll be NULL if we get a TAGMSG.
	 * Normalize to empty string to avoid segfaults. */
	const char *text = parv[2];
	if (text == NULL)
		text = "";

	/* start a batch for multi-target messages */
	if (strchr(parv[1], ',') != NULL)
		begin_local_response_batch();

	build_target_list(msgtype, client_p, source_p, parv[1], text, &msgbuf);
	if (ntargets <= 0)
		return;

	for (int i = 0; i < ntargets; i++)
	{
		switch (targets[i].type)
		{
		case ENTITY_CHANNEL:
			msg_channel(msgtype, client_p, source_p, targets[i].chptr, text, &msgbuf);
			break;

		case ENTITY_CHANNEL_OPMOD:
			msg_channel_opmod(msgtype, client_p, source_p, targets[i].chptr, text, &msgbuf);
			break;

		case ENTITY_CHANOPS_ON_CHANNEL:
			msg_channel_flags(msgtype, client_p, source_p, targets[i].chptr, targets[i].flags, text, &msgbuf);
			break;

		case ENTITY_CLIENT:
			msg_client(msgtype, source_p, targets[i].target_p, text, &msgbuf);
			break;

		case ENTITY_TARGET_ON_SERVER:
			msg_client_targeted(msgtype, source_p, targets[i].mask, targets[i].target_p, text, &msgbuf);
			break;

		case ENTITY_MASS_MESSAGE:
			msg_mass(msgtype, client_p, source_p, targets[i].mask, text, &msgbuf);
			break;
		}
	}
}

static void
m_echo(struct MsgBuf *msgbuf_p, struct Client *client_p, struct Client *source_p,
		int parc, const char *parv[])
{
	struct Client *target_p = find_person(parv[2]);
	enum message_type msgtype;

	if (target_p == NULL)
		return;

	switch (parv[1][0])
	{
	case 'P': msgtype = MESSAGE_TYPE_PRIVMSG; break;
	case 'N': msgtype = MESSAGE_TYPE_NOTICE; break;
	case 'T': msgtype = MESSAGE_TYPE_TAGMSG; break;
	default: return;
	}

	const char *text = parv[3];
	if (text == NULL)
		text = "";

	echo_msg(source_p, target_p, msgtype, parv[3], msgbuf_p);
}

/*
 * duplicate_*
 *
 * inputs	- pointer to check
 *		- pointer to table of entities
 *		- number of valid entities so far
 * output	- true if duplicate pointer in table, false if not.
 *		  note, this does the canonize using pointers
 * side effects	- NONE
 */
static bool
duplicate_client(struct Client *ptr)
{
	for (int i = 0; i < ntargets; i++)
		if (targets[i].target_p == ptr)
			return true;
	return false;
}

static bool
duplicate_channel(struct Channel *ptr)
{
	for (int i = 0; i < ntargets; i++)
		if (targets[i].chptr == ptr)
			return true;
	return false;
}

static bool
duplicate_mask(const char *mask)
{
	for (int i = 0; i < ntargets; i++)
		if (!strcmp(targets[i].mask, mask))
			return true;
	return false;
}

/*
 * build_target_list
 *
 * inputs	- pointer to given client_p (server)
 *		- pointer to given source (oper/client etc.)
 *		- pointer to list of nicks/channels
 *		- pointer to table to place results
 *		- pointer to text (only used if source_p is an oper)
 *		- tags
 * output	- number of valid entities
 * side effects	- target_table is modified to contain a list of
 *		  pointers to channels or clients
 *		  if source client is an oper
 *		  all the classic old bizzare oper privmsg tricks
 *		  are parsed and sent as is, if prefixed with $
 *		  to disambiguate.
 */

static int
build_target_list(enum message_type msgtype, struct Client *client_p,
		  struct Client *source_p, const char *nicks_channels, const char *text, struct MsgBuf *msgbuf_p)
{
	int type;
	char *p, *nick, *target_list;
	struct Channel *chptr = NULL;
	struct Client *target_p;

	target_list = LOCAL_COPY(nicks_channels);	/* skip strcpy for non-lazyleafs */

	ntargets = 0;

	for(nick = rb_strtok_r(target_list, ",", &p); nick; nick = rb_strtok_r(NULL, ",", &p))
	{
		char *with_prefix;
		rb_strlcpy(targets[ntargets].mask, nick, sizeof(targets[ntargets].mask));
		/*
		 * channels are privmsg'd a lot more than other clients, moved up
		 * here plain old channel msg?
		 */

		if(IsChanPrefix(*nick))
		{
			/* ignore send of local channel to a server (should not happen) */
			if(IsServer(client_p) && *nick == '&')
				continue;

			if((chptr = find_channel(nick)) != NULL)
			{
				if(!duplicate_channel(chptr))
				{
					if(ntargets >= ConfigFileEntry.max_targets)
					{
						sendto_one(source_p, form_str(ERR_TOOMANYTARGETS),
							   me.name, source_p->name, nick);
						return (1);
					}
					targets[ntargets].chptr = chptr;
					targets[ntargets++].type = ENTITY_CHANNEL;
				}
			}

			/* non-existent channel */
			else if(msgtype == MESSAGE_TYPE_PRIVMSG)
				sendto_one_numeric(source_p, ERR_NOSUCHNICK,
						   form_str(ERR_NOSUCHNICK), nick);

			continue;
		}

		if(MyClient(source_p))
			target_p = find_named_person(nick);
		else
			target_p = find_person(nick);

		/* look for a privmsg to another client */
		if(target_p)
		{
			if(!duplicate_client(target_p))
			{
				if(ntargets >= ConfigFileEntry.max_targets)
				{
					sendto_one(source_p, form_str(ERR_TOOMANYTARGETS),
						   me.name, source_p->name, nick);
					return (1);
				}
				targets[ntargets].target_p = target_p;
				targets[ntargets].type = ENTITY_CLIENT;
				targets[ntargets++].flags = 0;
			}
			continue;
		}

		/* @#channel or +#channel message ? */

		type = 0;
		with_prefix = nick;
		/*  allow %+@ if someone wants to do that */
		for(;;)
		{
			if(*nick == '@')
				type |= CHFL_CHANOP;
			else if(*nick == '%')
				type |= CHFL_CHANOP | CHFL_HALFOP;
			else if(*nick == '+')
				type |= CHFL_CHANOP | CHFL_HALFOP | CHFL_VOICE;
			else
				break;
			nick++;
		}

		if(type != 0)
		{
			/* no recipient.. */
			if(EmptyString(nick))
			{
				sendto_one(source_p, form_str(ERR_NORECIPIENT),
					   me.name, source_p->name, cmdname[msgtype]);
				continue;
			}

			/* At this point, nick+1 should be a channel name i.e. #foo or &foo
			 * if the channel is found, fine, if not report an error
			 */

			if((chptr = find_channel(nick)) != NULL)
			{
				struct membership *msptr;

				msptr = find_channel_membership(chptr, source_p);

				if(!IsServer(source_p) && !IsService(source_p) &&
				   !(msptr && msptr->flags & (CHFL_CHANOP | CHFL_HALFOP | CHFL_VOICE)))
				{
					sendto_one(source_p, form_str(ERR_CHANOPRIVSNEEDED),
						   get_id(&me, source_p),
						   get_id(source_p, source_p),
						   with_prefix);
					continue;
				}

				if(!duplicate_channel(chptr))
				{
					if(ntargets >= ConfigFileEntry.max_targets)
					{
						sendto_one(source_p, form_str(ERR_TOOMANYTARGETS),
							   me.name, source_p->name, nick);
						return (1);
					}
					targets[ntargets].chptr = chptr;
					targets[ntargets].type = ENTITY_CHANOPS_ON_CHANNEL;
					targets[ntargets++].flags = type;
				}
			}
			else if(msgtype == MESSAGE_TYPE_PRIVMSG)
			{
				sendto_one_numeric(source_p, ERR_NOSUCHNICK,
						   form_str(ERR_NOSUCHNICK), nick);
			}

			continue;
		}

		if(IsServer(client_p) && *nick == '=' && nick[1] == '#')
		{
			nick++;
			if((chptr = find_channel(nick)) != NULL)
			{
				if(!duplicate_channel(chptr))
				{
					if(ntargets >= ConfigFileEntry.max_targets)
					{
						sendto_one(source_p, form_str(ERR_TOOMANYTARGETS),
							   me.name, source_p->name, nick);
						return (1);
					}
					targets[ntargets].chptr = chptr;
					targets[ntargets++].type = ENTITY_CHANNEL_OPMOD;
				}
			}

			/* non existant channel */
			else if(msgtype == MESSAGE_TYPE_PRIVMSG)
				sendto_one_numeric(source_p, ERR_NOSUCHNICK,
						   form_str(ERR_NOSUCHNICK), nick);

			continue;
		}

		char *server = strchr(targets[ntargets].mask, '@');
		if (server != NULL)
		{
			*server++ = '\0';
			target_p = find_server(source_p, server);
			if (target_p == NULL)
			{
				if (msgtype == MESSAGE_TYPE_PRIVMSG)
					sendto_one_numeric(source_p, ERR_NOSUCHSERVER,
						form_str(ERR_NOSUCHSERVER), server);
				continue;
			}

			/* This was not very useful except for bypassing certain
			 * restrictions. Note that we still allow sending to
			 * remote servers this way, for messaging pseudoservers
			 * securely whether they have a service{} block or not.
			 * -- jilles
			 */
			if (IsMe(target_p))
			{
				if (msgtype == MESSAGE_TYPE_PRIVMSG)
					sendto_one_numeric(source_p, ERR_NOSUCHNICK,
						   form_str(ERR_NOSUCHNICK), nick);
				continue;
			}

			if (!duplicate_client(target_p) && !duplicate_mask(targets[ntargets].mask))
			{
				if (ntargets >= ConfigFileEntry.max_targets)
				{
					sendto_one(source_p, form_str(ERR_TOOMANYTARGETS),
						me.name, source_p->name, nick);
					return 1;
				}

				targets[ntargets].target_p = target_p;
				targets[ntargets++].type = ENTITY_TARGET_ON_SERVER;
			}

			continue;
		}

		if (*nick == '$')
		{
			if (MyClient(source_p) && !IsOperMassNotice(source_p))
			{
				if (!IsOper(source_p))
					sendto_one_numeric(source_p, ERR_NOPRIVILEGES,
						form_str(ERR_NOPRIVILEGES));
				else
					sendto_one(source_p, form_str(ERR_NOPRIVS),
						me.name, source_p->name, "mass_notice");
				continue;
			}

			if (*(nick + 1) != '$' && *(nick + 1) != '#')
			{
				sendto_one(source_p,
					":%s NOTICE %s :The command %s %s is no longer supported, please use $%s",
					me.name, source_p->name, cmdname[msgtype], nick, nick);
				continue;
			}

			if (!duplicate_mask(targets[ntargets].mask))
			{
				if (ntargets >= ConfigFileEntry.max_targets)
				{
					sendto_one(source_p, form_str(ERR_TOOMANYTARGETS),
						me.name, source_p->name, nick);
					return 1;
				}

				targets[ntargets].target_p = NULL;
				targets[ntargets++].type = ENTITY_MASS_MESSAGE;
			}

			continue;
		}

		/* no matching anything found - error if PRIVMSG */
		if(msgtype == MESSAGE_TYPE_PRIVMSG)
		{
			/* dont give this numeric when source is local,
			 * because its misleading --anfl
			 */
			if(!MyClient(source_p) && IsDigit(*nick))
				sendto_one(source_p, ":%s %d %s * :Target left IRC. "
					   "Failed to deliver: [%.20s]",
					   get_id(&me, source_p), ERR_NOSUCHNICK,
					   get_id(source_p, source_p), text);
			else
				sendto_one_numeric(source_p, ERR_NOSUCHNICK,
						   form_str(ERR_NOSUCHNICK), nick);
		}

	}
	return (1);
}

/*
 * msg_channel
 *
 * inputs	- flag privmsg or notice
 *		- pointer to client_p
 *		- pointer to source_p
 *		- pointer to channel
 * output	- NONE
 * side effects	- message given channel
 *
 * XXX - We need to rework this a bit, it's a tad ugly. --nenolod
 */
static void
msg_channel(enum message_type msgtype,
	    struct Client *client_p, struct Client *source_p, struct Channel *chptr,
	    const char *text, struct MsgBuf *msgbuf_p)
{
	int result;
	hook_data_privmsg_channel hdata;
	uint64_t cli_cap = NOCAPS;
	uint64_t serv_cap = NOCAPS;
	const char *fmt = "%s %s :%s";

	if (msgtype == MESSAGE_TYPE_TAGMSG)
	{
		cli_cap = CLICAP_MESSAGE_TAGS;
		serv_cap = CAP_STAG;
		fmt = "%s %s";
	}

	if(MyClient(source_p))
	{
		/* idle time shouldn't be reset by notices or tagmsg */
		if(msgtype == MESSAGE_TYPE_PRIVMSG)
			source_p->localClient->last = rb_current_time();
	}

	hdata.msgtype = msgtype;
	hdata.source_p = source_p;
	hdata.chptr = chptr;
	hdata.text = text;
	hdata.approved = 0;
	hdata.msgbuf = msgbuf_p;

	call_hook(h_privmsg_channel, &hdata);

	/* memory buffer address may have changed, update pointer */
	text = hdata.text;

	if (hdata.approved != 0)
		return;

	/* hook may have reduced the string to nothing. */
	if (EmptyString(text) && msgtype != MESSAGE_TYPE_TAGMSG)
	{
		/* could be empty after colour stripping and
		 * that would cause problems later */
		if(msgtype != MESSAGE_TYPE_NOTICE)
			sendto_one(source_p, form_str(ERR_NOTEXTTOSEND), me.name, source_p->name);
		return;
	}

	/* chanops and voiced can flood their own channel with impunity */
	if((result = can_send(chptr, source_p, NULL)))
	{
		if(result != CAN_SEND_OPV && MyClient(source_p) &&
		   !IsOperGeneral(source_p) &&
		   !add_channel_target(source_p, chptr))
		{
			sendto_one(source_p, form_str(ERR_TARGCHANGE),
				   me.name, source_p->name, chptr->chname);
			return;
		}
		if(result == CAN_SEND_OPV ||
		   !flood_attack_channel(msgtype, source_p, chptr, chptr->chname))
		{
			sendto_channel_flags_tags(client_p, ALL_MEMBERS, source_p, chptr,
				cli_cap, NOCAPS, serv_cap, NOCAPS,
				msgbuf_p->n_tags, msgbuf_p->tags, fmt, cmdname[msgtype], chptr->chname, text);
		}
	}
	else if(chptr->mode.mode & MODE_OPMODERATE &&
			(!(chptr->mode.mode & MODE_NOPRIVMSGS) ||
			 IsMember(source_p, chptr)))
	{
		if(MyClient(source_p) && !IsOperGeneral(source_p) &&
		   !add_channel_target(source_p, chptr))
		{
			sendto_one(source_p, form_str(ERR_TARGCHANGE),
				   me.name, source_p->name, chptr->chname);
			return;
		}
		if(!flood_attack_channel(msgtype, source_p, chptr, chptr->chname))
		{
			sendto_channel_opmod_tags(client_p, source_p, chptr,
				cli_cap, NOCAPS, serv_cap, NOCAPS,
				cmdname[msgtype], text, msgbuf_p->n_tags, msgbuf_p->tags);
		}
	}
	else
	{
		if(msgtype == MESSAGE_TYPE_PRIVMSG)
			sendto_one_numeric(source_p, ERR_CANNOTSENDTOCHAN,
					   form_str(ERR_CANNOTSENDTOCHAN), chptr->chname);
	}
}
/*
 * msg_channel_opmod
 *
 * inputs	- flag privmsg or notice
 *		- pointer to client_p
 *		- pointer to source_p
 *		- pointer to channel
 * output	- NONE
 * side effects	- message given channel ops
 *
 * XXX - We need to rework this a bit, it's a tad ugly. --nenolod
 */
static void
msg_channel_opmod(enum message_type msgtype,
		  struct Client *client_p, struct Client *source_p,
		  struct Channel *chptr, const char *text, struct MsgBuf *msgbuf_p)
{
	hook_data_privmsg_channel hdata;
	uint64_t cli_cap = msgtype == MESSAGE_TYPE_TAGMSG ? CLICAP_MESSAGE_TAGS : NOCAPS;
	uint64_t serv_cap = msgtype == MESSAGE_TYPE_TAGMSG ? CAP_STAG : NOCAPS;

	hdata.msgtype = msgtype;
	hdata.source_p = source_p;
	hdata.chptr = chptr;
	hdata.text = text;
	hdata.approved = 0;
	hdata.msgbuf = msgbuf_p;

	call_hook(h_privmsg_channel, &hdata);

	/* memory buffer address may have changed, update pointer */
	text = hdata.text;

	if (hdata.approved != 0)
		return;

	/* hook may have reduced the string to nothing. */
	if (EmptyString(text) && msgtype != MESSAGE_TYPE_TAGMSG)
	{
		/* could be empty after colour stripping and
		 * that would cause problems later */
		if(msgtype != MESSAGE_TYPE_NOTICE)
			sendto_one(source_p, form_str(ERR_NOTEXTTOSEND), me.name, source_p->name);
		return;
	}

	if(chptr->mode.mode & MODE_OPMODERATE &&
			(!(chptr->mode.mode & MODE_NOPRIVMSGS) ||
			 IsMember(source_p, chptr)))
	{
		if(!flood_attack_channel(msgtype, source_p, chptr, chptr->chname))
		{
			sendto_channel_opmod_tags(client_p, source_p, chptr,
				cli_cap, NOCAPS, serv_cap, NOCAPS,
				cmdname[msgtype], text, msgbuf_p->n_tags, msgbuf_p->tags);
		}
	}
	else
	{
		if(msgtype == MESSAGE_TYPE_PRIVMSG)
			sendto_one_numeric(source_p, ERR_CANNOTSENDTOCHAN,
					   form_str(ERR_CANNOTSENDTOCHAN), chptr->chname);
	}
}

/*
 * msg_channel_flags
 *
 * inputs	- flag 0 if PRIVMSG 1 if NOTICE. RFC
 *		  say NOTICE must not auto reply
 *		- pointer to client_p
 *		- pointer to source_p
 *		- pointer to channel
 *		- flags
 *		- pointer to text to send
 * output	- NONE
 * side effects	- message given channel either chanop or voice
 */
static void
msg_channel_flags(enum message_type msgtype, struct Client *client_p,
		  struct Client *source_p, struct Channel *chptr, int flags, const char *text, struct MsgBuf *msgbuf_p)
{
	int type;
	char c;
	hook_data_privmsg_channel hdata;
	uint64_t cli_cap = NOCAPS;
	uint64_t serv_cap = NOCAPS;
	const char *fmt = "%s %c%s :%s";

	if (msgtype == MESSAGE_TYPE_TAGMSG)
	{
		cli_cap = CLICAP_MESSAGE_TAGS;
		serv_cap = CAP_STAG;
		fmt = "%s %c%s";
	}

	if(flags & CHFL_VOICE)
	{
		type = ONLY_CHANOPSHALFOPSVOICED;
		c = '+';
	}
	else if(flags & CHFL_HALFOP)
	{
		type = ONLY_CHANOPSHALFOPS;
		c = '%';
	}
	else
	{
		type = ONLY_CHANOPS;
		c = '@';
	}

	if(MyClient(source_p))
	{
		/* idle time shouldn't be reset by notices or tagmsg */
		if(msgtype == MESSAGE_TYPE_PRIVMSG)
			source_p->localClient->last = rb_current_time();
	}

	hdata.msgtype = msgtype;
	hdata.source_p = source_p;
	hdata.chptr = chptr;
	hdata.text = text;
	hdata.approved = 0;
	hdata.msgbuf = msgbuf_p;

	call_hook(h_privmsg_channel, &hdata);

	/* memory buffer address may have changed, update pointer */
	text = hdata.text;

	if (hdata.approved != 0)
		return;

	if (EmptyString(text) && msgtype != MESSAGE_TYPE_TAGMSG)
	{
		/* could be empty after colour stripping and
		 * that would cause problems later */
		if(msgtype != MESSAGE_TYPE_NOTICE)
			sendto_one(source_p, form_str(ERR_NOTEXTTOSEND), me.name, source_p->name);
		return;
	}

	sendto_channel_flags_tags(client_p, type, source_p, chptr,
		cli_cap, NOCAPS, serv_cap, NOCAPS,
		msgbuf_p->n_tags, msgbuf_p->tags, fmt, cmdname[msgtype], c, chptr->chname, text);
}

static void
expire_tgchange(void *unused)
{
	tgchange *target;
	rb_dlink_node *ptr, *next_ptr;

	RB_DLINK_FOREACH_SAFE(ptr, next_ptr, tgchange_list.head)
	{
		target = ptr->data;

		if(target->expiry < rb_current_time())
		{
			rb_dlinkDelete(ptr, &tgchange_list);
			rb_patricia_remove(tgchange_tree, target->pnode);
			rb_free(target->ip);
			rb_free(target);
		}
	}
}

static void
echo_msg(struct Client *source_p, struct Client *target_p,
		enum message_type msgtype, const char *text, struct MsgBuf *msgbuf_p)
{
	const char *fmt = msgtype == MESSAGE_TYPE_TAGMSG ? ":%s!%s@%s %s %s" : ":%s!%s@%s %s %s :%s";

	if (MyClient(target_p))
	{
		if (!IsClientCapable(target_p, CLICAP_ECHO_MESSAGE))
			return;

		sendto_one_tags(target_p, NOCAPS, NOCAPS,
			msgbuf_p->n_tags, msgbuf_p->tags, fmt,
			target_p->name, target_p->username, target_p->host,
			cmdname[msgtype],
			source_p->name,
			text);
		return;
	}

	sendto_one_tags(target_p, CAP_ECHO, NOCAPS,
		msgbuf_p->n_tags, msgbuf_p->tags, ":%s ECHO %c %s :%s",
		use_id(source_p),
		shortname[msgtype],
		use_id(target_p),
		text);
}

/*
 * msg_client
 *
 * inputs	- flag 0 if PRIVMSG 1 if NOTICE. RFC
 *		  say NOTICE must not auto reply
 * 		- pointer to source_p source (struct Client *)
 *		- pointer to target_p target (struct Client *)
 *		- pointer to text
 * output	- NONE
 * side effects	- message given channel either chanop or voice
 */
static void
msg_client(enum message_type msgtype,
	   struct Client *source_p, struct Client *target_p, const char *text, struct MsgBuf *msgbuf_p)
{
	int do_floodcount = 0;
	hook_data_privmsg_user hdata;

	if(MyClient(source_p))
	{
		/* idle time shouldn't be reset by notices or tagmsg */
		if(msgtype == MESSAGE_TYPE_PRIVMSG)
			source_p->localClient->last = rb_current_time();

		/* auto cprivmsg/cnotice */
		do_floodcount = !IsOperGeneral(source_p) &&
			!find_allowing_channel(source_p, target_p);

		/* target change stuff, dont limit ctcp replies as that
		 * would allow people to start filling up random users
		 * targets just by ctcping them
		 */
		if((msgtype == MESSAGE_TYPE_PRIVMSG || *text != '\001') &&
		   ConfigFileEntry.target_change && do_floodcount)
		{
			if(!add_target(source_p, target_p))
			{
				sendto_one(source_p, form_str(ERR_TARGCHANGE),
					   me.name, source_p->name, target_p->name);
				return;
			}
		}

		if (do_floodcount && msgtype == MESSAGE_TYPE_NOTICE && *text == '\001' &&
				target_p->large_ctcp_sent + LARGE_CTCP_TIME >= rb_current_time())
			do_floodcount = 0;

		if (do_floodcount &&
				flood_attack_client(msgtype, source_p, target_p))
			return;
	}
	else if(source_p->from == target_p->from)
	{
		sendto_realops_snomask(SNO_DEBUG, L_NETWIDE,
				     "Send message to %s[%s] dropped from %s(Fake Dir)",
				     target_p->name, target_p->from->name, source_p->name);
		return;
	}

	if(MyConnect(source_p) && (msgtype == MESSAGE_TYPE_PRIVMSG) && target_p->user && target_p->user->away)
		sendto_one_numeric(source_p, RPL_AWAY, form_str(RPL_AWAY),
				   target_p->name, target_p->user->away);

	hdata.msgtype = msgtype;
	hdata.source_p = source_p;
	hdata.target_p = target_p;
	hdata.text = text;
	hdata.approved = 0;
	hdata.msgbuf = msgbuf_p;

	call_hook(h_privmsg_user, &hdata);

	/* buffer location may have changed. */
	text = hdata.text;

	if (hdata.approved != 0)
		return;

	if (MyClient(target_p))
	{
		if (EmptyString(text) && msgtype != MESSAGE_TYPE_TAGMSG)
		{
			/* could be empty after colour stripping and
			 * that would cause problems later */
			if(msgtype != MESSAGE_TYPE_NOTICE)
				sendto_one(source_p, form_str(ERR_NOTEXTTOSEND), me.name, source_p->name);
			return;
		}

		/* suppress labeled-response on the primary message if they're messaging themselves */
		struct ResponseInfo *info = outgoing_response_info;
		if (source_p == target_p)
			suspend_response_batch();

		if (msgtype == MESSAGE_TYPE_TAGMSG && IsClientCapable(target_p, CLICAP_MESSAGE_TAGS))
		{
			add_reply_target(target_p, source_p);
			sendto_anywhere_tags(target_p, source_p, cmdname[msgtype],
				NOCAPS, NOCAPS, msgbuf_p->n_tags, msgbuf_p->tags, "");
		}
		else if (msgtype != MESSAGE_TYPE_TAGMSG)
		{
			add_reply_target(target_p, source_p);
			sendto_anywhere_tags(target_p, source_p, cmdname[msgtype],
				NOCAPS, NOCAPS, msgbuf_p->n_tags, msgbuf_p->tags, ":%s", text);
		}

		resume_response_batch(info);
		echo_msg(target_p, source_p, msgtype, text, msgbuf_p);
	}
	else
	{
		begin_remote_response_batch(1, target_p->servptr->name);
		sendto_anywhere_tags(target_p, source_p, cmdname[msgtype],
			msgtype == MESSAGE_TYPE_TAGMSG ? CAP_STAG : NOCAPS,
			NOCAPS, msgbuf_p->n_tags, msgbuf_p->tags,
			msgtype == MESSAGE_TYPE_TAGMSG ? "" : ":%s",
			text);

		/* if the remote doesn't support ECHO, generate a local echo-message instead */
		if (!IsServerCapable(target_p->from, CAP_ECHO))
			echo_msg(target_p, source_p, msgtype, text, msgbuf_p);
	}
}

static void
msg_client_targeted(enum message_type msgtype,
	struct Client *source_p, const char *nick, struct Client *target_p,
	const char *text, struct MsgBuf *msgbuf_p)
{
	s_assert(!IsMe(target_p) && IsServer(target_p));

	begin_remote_response_batch(1, target_p->servptr->name);
	sendto_one_tags(target_p,
		msgtype == MESSAGE_TYPE_TAGMSG ? CAP_STAG : NOCAPS,
		NOCAPS, msgbuf_p->n_tags, msgbuf_p->tags,
		msgtype == MESSAGE_TYPE_TAGMSG ? ":%s %s %s@%s" : ":%s %s %s@%s :%s",
		get_id(source_p, target_p), cmdname[msgtype], nick, target_p->name, text);

	/* ECHO doesn't work with special syntax, so generate a local echo instead */
	if (MyClient(source_p) && IsClientCapable(source_p, CLICAP_ECHO_MESSAGE))
	{
		const char *fmt = msgtype == MESSAGE_TYPE_TAGMSG
			? ":%s!%s@%s %s %s@%s"
			: ":%s!%s@%s %s %s@%s :%s";

		sendto_one_tags(source_p, NOCAPS, NOCAPS,
			msgbuf_p->n_tags, msgbuf_p->tags, fmt,
			source_p->name, source_p->username, source_p->host,
			cmdname[msgtype], nick, target_p->name, text);
	}
}

static void
msg_mass(enum message_type msgtype, struct Client *client_p, struct Client *source_p,
	const char *mask, const char *text, struct MsgBuf *msgbuf_p)
{
	uint64_t cli_cap = msgtype == MESSAGE_TYPE_TAGMSG ? CLICAP_MESSAGE_TAGS : NOCAPS;
	uint64_t serv_cap = msgtype == MESSAGE_TYPE_TAGMSG ? CAP_STAG : NOCAPS;

	/* skip past initial '$' */
	mask++;

	begin_local_response_batch();

	if (MyClient(source_p))
	{
		/* tag data could easily exceed max IRC message length, so including the tags in the snote
		 * text isn't really workable. Sending a mass-tagmsg in itself is already pretty questionable,
		 * but the spec allows for it. */
		sendto_realops_snomask(SNO_GENERAL, L_ALL | L_NETWIDE,
				msgtype == MESSAGE_TYPE_TAGMSG ? "%s sent mass-%s to %s" : "%s sent mass-%s to %s: %s",
				get_oper_name(source_p),
				cmdname_lower[msgtype],
				mask, text);
	}

	/* suppress labeled-response on the message (it might be going to themselves) */
	struct ResponseInfo *info = suspend_response_batch();

	sendto_match_butone_tags(IsServer(client_p) ? client_p : NULL, source_p,
		mask + 1,
		(*mask == '#') ? MATCH_HOST : MATCH_SERVER,
		cli_cap, NOCAPS, serv_cap, NOCAPS, msgbuf_p->n_tags, msgbuf_p->tags,
		msgtype == MESSAGE_TYPE_TAGMSG ? "%s $%s" : "%s $%s :%s",
		cmdname[msgtype], mask, text);

	if (msgtype == MESSAGE_TYPE_PRIVMSG && *text == '\001')
		source_p->large_ctcp_sent = rb_current_time();

	resume_response_batch(info);

	/* ECHO doesn't work with special syntax, so generate a local echo instead */
	if (MyClient(source_p) && IsClientCapable(source_p, CLICAP_ECHO_MESSAGE))
	{
		const char *fmt = msgtype == MESSAGE_TYPE_TAGMSG
			? ":%s!%s@%s %s %s"
			: ":%s!%s@%s %s %s :%s";

		sendto_one_tags(source_p, NOCAPS, NOCAPS,
			msgbuf_p->n_tags, msgbuf_p->tags, fmt,
			source_p->name, source_p->username, source_p->host,
			cmdname[msgtype], mask, text);
	}
}

/*
 * flood_attack_client
 * inputs       - flag 0 if PRIVMSG 1 if NOTICE. RFC
 *                says NOTICE must not auto reply
 *              - pointer to source Client
 *		- pointer to target Client
 * output	- true if target is under flood attack
 * side effects	- check for flood attack on target target_p
 */
static bool
flood_attack_client(enum message_type msgtype, struct Client *source_p, struct Client *target_p)
{
	int delta;

	/* Services could get many messages legitimately and
	 * can be messaged without rate limiting via aliases
	 * and msg user@server.
	 * -- jilles
	 */
	if(GlobalSetOptions.floodcount && IsClient(source_p) && source_p != target_p && !IsService(target_p) && !HasPrivilege(target_p, "oper:free_target"))
	{
		if((target_p->first_received_message_time + 1) < rb_current_time())
		{
			delta = rb_current_time() - target_p->first_received_message_time;
			target_p->received_number_of_privmsgs -= delta;
			target_p->first_received_message_time = rb_current_time();
			if(target_p->received_number_of_privmsgs <= 0)
			{
				target_p->received_number_of_privmsgs = 0;
				target_p->flood_noticed = 0;
			}
		}

		if((target_p->received_number_of_privmsgs >=
		    GlobalSetOptions.floodcount) || target_p->flood_noticed)
		{
			if(target_p->flood_noticed == 0)
			{
				sendto_realops_snomask(SNO_BOTS, L_NETWIDE,
						     "Possible Flooder %s[%s@%s] on %s target: %s",
						     source_p->name, source_p->username,
						     source_p->orighost,
						     source_p->servptr->name, target_p->name);
				target_p->flood_noticed = 1;
				/* add a bit of penalty */
				target_p->received_number_of_privmsgs += 2;
			}
			if(MyClient(source_p) && (msgtype == MESSAGE_TYPE_PRIVMSG))
				sendto_one(source_p,
					   ":%s NOTICE %s :*** Message to %s throttled due to flooding",
					   me.name, source_p->name, target_p->name);
			return true;
		}
		else
			target_p->received_number_of_privmsgs++;
	}

	return false;
}
