/*
 * ==================================================================
 * Filename:             m_spy.c
 * Description:          Real-time spying
 * Written by:		 AngryWolf <angrywolf@flashmail.com>
 * The idea was born by: Toxyc <toxyc@freemail.hu>
 * ==================================================================
 */

#include "config.h"
#include "struct.h"
#include "common.h"
#include "sys.h"
#include "numeric.h"
#include "msg.h"
#include "channel.h"
#include <time.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <io.h>
#endif
#include <fcntl.h>
#include "h.h"
#ifdef STRIPBADWORDS
#include "badwords.h"
#endif
#ifdef _WIN32
#include "version.h"
#endif

// ==================================================================
// Definitions & macros
// ==================================================================

#define MSG_SPY 		"OKUMA"
#define TOK_SPY 		"SPY"
#define MSG_SPYCHANNEL 		"SPYCHAN"
#define TOK_SPYCHANNEL		"SPC"
#define MSG_SPYSEND 		"SPYSEND"
#define TOK_SPYSEND 		"SPE"
#define MSG_SPYCHSEND 		"SPYCHSEND"
#define TOK_SPYCHSEND 		"SPT"
#define MSG_SPYFORWARD 		"SPYFORWARD"
#define TOK_SPYFORWARD 		"SPF"
#define MSG_SPYCHFORWARD 	"SPYCHFORWARD"
#define TOK_SPYCHFORWARD 	"SPG"
#define MSG_SPYSTATS 		"SPYSTATS"
#define TOK_SPYSTATS 		"SPS"

#define MyMod			SpyModInfo->handle
#define FlagSpy			'X'
#define LockFlag		'@'
#define LockFlagStr		"@"

#define SPY_NOTICE		":%s NOTICE %s :*** "
#define SPY_LINE		"=================================================="

#define ircfree(x)		if (x) free(x)
#define ircstrdup(x,y)		if (x) MyFree(x); if (!y) x = NULL; else x = strdup(y)
#define DelSnomask(x)		if (x) SnomaskDel(x); x = NULL
#define DelHook(x)		if (x) HookDel(x); x = NULL
#define DelCommand(x)		if (x) CommandDel(x); x = NULL


#define IsParam(x)		(parc > (x) && !BadPtr(parv[(x)]))
#define IsValidParam(x)		(parc > (x) && !BadPtr(parv[(x)]) && *parv[(x)] != '*')

#define AddLock(lock)		(lock ? LockFlagStr : "")
#define GetLock(str)		(*str == LockFlag ? 1 : 0)
#define SplitLock(lock, str)	del_prefix(str, &(lock), &(str))
#define MergeLock(lock, str)	AddLock(lock), str
#define MergeLockOpt(lock, str)	str ? AddLock(lock) : "", str ? str : "*"
#define MergeLockEmp(lock, str)	str ? AddLock(lock) : "", str ? str : ""

#define SpyForward5(cptr, cmd, who, nick1, nick2, channel, lock1, lock2) \
		sendto_serv_butone_token(cptr, me.name, MSG_SPYFORWARD, TOK_SPYFORWARD, \
		"%s %s %s%s %s%s %s", \
		cmd, who, MergeLock(lock1, nick1), MergeLockOpt(lock2, nick2), \
		channel ? channel : "*")

#define SpyForward4(cptr, cmd, who, nick1, nick2, lock1, lock2) \
		sendto_serv_butone_token(cptr, me.name, MSG_SPYFORWARD, TOK_SPYFORWARD, \
		"%s %s %s%s %s%s", \
		cmd, who, MergeLock(lock1, nick1), MergeLockOpt(lock2, nick2))

#define SpyForward3(cptr, cmd, who, nick1, lock1) \
		sendto_serv_butone_token(cptr, me.name, MSG_SPYFORWARD, TOK_SPYFORWARD, \
		"%s %s %s%s", \
		cmd, who, MergeLock(lock1, nick1))

#define SpyForward2(cptr, cmd, who) \
		sendto_serv_butone_token(cptr, me.name, MSG_SPYFORWARD, TOK_SPYFORWARD, \
		"%s %s", \
		cmd, who)

#define SpyForward1(cptr, cmd) \
		sendto_serv_butone_token(cptr, me.name, MSG_SPYFORWARD, TOK_SPYFORWARD, \
		"%s", \
		cmd)

#define SpyChForward4(cptr, cmd, who, channel, sendto, lock) \
		sendto_serv_butone_token(cptr, me.name, MSG_SPYCHFORWARD, TOK_SPYCHFORWARD, \
		"%s %s %s%s %s", \
		cmd, who, MergeLock(lock, channel), sendto ? sendto : "*")

#define SpyChForward3(cptr, cmd, who, channel, lock) \
		sendto_serv_butone_token(cptr, me.name, MSG_SPYCHFORWARD, TOK_SPYCHFORWARD, \
		"%s %s %s%s", \
		cmd, who, MergeLock(lock, channel))

#define SpyChForward2(cptr, cmd, who) \
		sendto_serv_butone_token(cptr, me.name, MSG_SPYCHFORWARD, TOK_SPYCHFORWARD, \
		"%s %s", \
		cmd, who)

typedef struct _spystruct	SpyStruct;
typedef struct _spychstruct	SpyChStruct;

struct _spystruct
{
	SpyStruct	*prev, *next;

	aClient		*who;		/* Who is spying */
	char		*nick1;		/* First nick to spy */
	char		*nick2;		/* Second nick to spy (may be NULL) */
	char		*chname;	/* Where to put messages (may be NULL) */
	u_int		lock1:1;		/* 1 if nick1 is locked, otherwise 0 */
	u_int		lock2:1;		/* 1 if nick2 is locked, otherwise 0 */
};

struct _spychstruct
{
	SpyChStruct	*prev, *next;

	aClient		*who;		/* Who is spying */
	char		*channel;	/* Channel to be spyed */
	char		*sendto;	/* Where to put messages (may be NULL) */
	u_int		lock;		/* True if nick1 is locked, otherwise 0 */
};

static SpyStruct	*FindSpyItem1(aClient *acptr, char *nick);
static SpyStruct	*FindSpyItem2(aClient *acptr, char *nick1, char *nick2);
static SpyChStruct	*FindSpyChItem(aClient *acptr, char *channel);
static void		AddSpyItem(aClient *acptr, char *nick1, char *nick2, char *chname, u_int lock1, u_int lock2);
static void		AddSpyChItem(aClient *acptr, char *channel, char *sendto, u_int lock);
static void		DelSpyItem(SpyStruct *s);
static void		DelSpyChItem(SpyChStruct *s);
static unsigned int	spy_paramcheck(aClient *cptr, int parc, char *parv[], char **nick1, char **nick2, char **channel, u_int *lock1, u_int *lock2);
static unsigned int	spych_paramcheck(aClient *cptr, int parc, char *parv[], char **channel, char **sendto, u_int *lock);
static void		spy_send_client(aClient *acptr, int notice, char *nick1, char *nick2, char *text);
static void		spy_send_channel(aClient *cptr, char *channel, int notice, char *nick1, char *nick2, char *text);
static void		spych_send_client(aClient *acptr, int notice, char *nick, char *channel, char *text);
static void		spych_send_channel(aClient *cptr, char *sendto, int notice, char *nick, char *channel, char *text);
static void		del_prefix(char *str, u_int *lock, char **newstr);

static Command		*AddCommand(char *msg, char *token, int (*func)());
DLLFUNC int		m_spy(aClient *cptr, aClient *sptr, int parc, char *parv[]);
DLLFUNC int		m_spychannel(aClient *cptr, aClient *sptr, int parc, char *parv[]);
DLLFUNC int		m_spysend(aClient *cptr, aClient *sptr, int parc, char *parv[]);
DLLFUNC int		m_spychsend(aClient *cptr, aClient *sptr, int parc, char *parv[]);
DLLFUNC int		m_spyforward(aClient *cptr, aClient *sptr, int parc, char *parv[]);
DLLFUNC int		m_spychforward(aClient *cptr, aClient *sptr, int parc, char *parv[]);
DLLFUNC int		m_spystats(aClient *cptr, aClient *sptr, int parc, char *parv[]);

DLLFUNC char		*spy_on_privmsg(aClient *, aClient *, aClient *, char *, int);
DLLFUNC char		*spy_on_chanmsg(aClient *, aClient *, aChannel *, char *, int);
DLLFUNC int		spy_on_quit(aClient *, char *);
DLLFUNC int		spy_on_remote_quit(aClient *, char *);
DLLFUNC int		spy_on_server_connect(aClient *);
DLLFUNC int		spy_on_local_nickchange(aClient *, char *);
DLLFUNC int		spy_on_remote_nickchange(aClient *, aClient *, char *);
DLLFUNC int		spy_on_channel_destroy(aChannel *);

// ==================================================================
// Module header
// ==================================================================

ModuleHeader MOD_HEADER(m_spy)
  = {
	"spy",
	"$Id: m_spy.c,v 3.6 2003/12/01 18:31:45 angrywolf Exp $",
	"real-time spying",
	"3.2-b8-1",
	NULL 
    };

// ==================================================================
// Main declarations
// ==================================================================

static char *SpyHelp[] =
{
    SPY_NOTICE	"\2Usage:\2",
    SPY_NOTICE	"/spy add <nick1> [<nick2> [<#channel>]]",
    SPY_NOTICE	"/spy del <nick1> [<nick2>]",
    SPY_NOTICE	"/spy list [<nick1> [<nick2>]] [<#channel>]",
    SPY_NOTICE	"/spy clear",
    SPY_NOTICE	"\2Examples:\2",
    SPY_NOTICE	"/spy add AngryWolf Toxyc #secret",
    SPY_NOTICE	"/spy del AngryWolf",
    SPY_NOTICE	"/spy list #secret",
    NULL
};

static char *SpyChHelp[] =
{
    SPY_NOTICE	"\2Usage:\2",
    SPY_NOTICE	"/spychan add <#channel> [<#sendto>]]",
    SPY_NOTICE	"/spychan del <#channel>",
    SPY_NOTICE	"/spychan list [<#channel>]",
    SPY_NOTICE	"/spychan clear",
    SPY_NOTICE	"\2Examples:\2",
    SPY_NOTICE	"/spychan add #warez #spy",
    SPY_NOTICE	"/spychan del #warez",
    SPY_NOTICE	"/spychan list",
    NULL
};

ModuleInfo		*SpyModInfo;
Command			*CmdSpy, *CmdSpychannel, *CmdSpysend, *CmdSpychsend;
Command			*CmdSpyforward, *CmdSpychforward, *CmdSpystats;
Snomask			*SpySnomask;
long			SNO_SPY;

static Hook		*HookPrivMsg;
static Hook		*HookChanMsg;
static Hook		*HookQuit;
static Hook		*HookRemoteQuit;
static Hook		*HookServConn;
static Hook		*HookLocalNick;
static Hook		*HookRemoteNick;
static Hook		*HookChanDest;

static SpyStruct	*SpyList = NULL;
static SpyChStruct	*SpyChList = NULL;

// ==================================================================
// Module initalization, loading and unloading
// ==================================================================

static Command *AddCommand(char *msg, char *token, int (*func)())
{
	Command *cmd;

	if (CommandExists(msg))
    	{
		config_error("Command %s already exists", msg);
		return NULL;
    	}
    	if (CommandExists(token))
	{
		config_error("Token %s already exists", token);
		return NULL;
    	}

	cmd = CommandAdd(MyMod, msg, token, func, MAXPARA, 0);

#ifndef _WIN32
	if (ModuleGetError(MyMod) != MODERR_NOERROR || !cmd)
#else
	if (!cmd)
#endif
	{
#ifndef _WIN32
		config_error("Error adding command %s: %s", msg,
			ModuleGetErrorStr(MyMod));
#else
		config_error("Error adding command %s", msg);
#endif
		return NULL; /* just to be sure */
	}

	return cmd;
}

static Snomask *AddSnomask(char flag, int (*allowed)(aClient *sptr, int what), long *mode)
{
        Snomask *s;

        *mode = 0;
        s = SnomaskAdd(MyMod, flag, allowed, mode);

#ifndef _WIN32
        if ((ModuleGetError(MyMod) != MODERR_NOERROR) || !s)
#else
        if (!s)
#endif
        {
#ifndef _WIN32
                sendto_realops("[\2m_spy\2] Error adding snomask %c: %s",
                        flag, ModuleGetErrorStr(MyMod));
#else
                sendto_realops("[\2m_spy\2] Error adding snomask %c",
                        flag);
#endif
                return NULL;
        }

        return s;
}

DLLFUNC int MOD_INIT(m_spy)(ModuleInfo *modinfo)
{
	int ret = MOD_SUCCESS;

	SpyModInfo	= modinfo;

	HookPrivMsg	= HookAddPCharEx(MyMod, HOOKTYPE_USERMSG, spy_on_privmsg);
	HookChanMsg	= HookAddPCharEx(MyMod, HOOKTYPE_CHANMSG, spy_on_chanmsg);
	HookQuit	= HookAddEx(MyMod, HOOKTYPE_LOCAL_QUIT, spy_on_quit);
	HookRemoteQuit	= HookAddEx(MyMod, HOOKTYPE_REMOTE_QUIT, spy_on_remote_quit);
	HookServConn	= HookAddEx(MyMod, HOOKTYPE_SERVER_CONNECT, spy_on_server_connect);
	HookLocalNick	= HookAddEx(MyMod, HOOKTYPE_LOCAL_NICKCHANGE, spy_on_local_nickchange);
	HookRemoteNick	= HookAddEx(MyMod, HOOKTYPE_REMOTE_NICKCHANGE, spy_on_remote_nickchange);
	HookChanDest	= HookAddEx(MyMod, HOOKTYPE_CHANNEL_DESTROY, spy_on_channel_destroy);

	CmdSpy		= AddCommand(MSG_SPY, TOK_SPY, m_spy);
	CmdSpychannel	= AddCommand(MSG_SPYCHANNEL, TOK_SPYCHANNEL, m_spychannel);
	CmdSpysend	= AddCommand(MSG_SPYSEND, TOK_SPYSEND, m_spysend);
	CmdSpychsend	= AddCommand(MSG_SPYCHSEND, TOK_SPYCHSEND, m_spychsend);
	CmdSpyforward	= AddCommand(MSG_SPYFORWARD, TOK_SPYFORWARD, m_spyforward);
	CmdSpychforward	= AddCommand(MSG_SPYCHFORWARD, TOK_SPYCHFORWARD, m_spychforward);
	CmdSpystats	= AddCommand(MSG_SPYSTATS, TOK_SPYSTATS, m_spystats);

	SpySnomask	= AddSnomask(FlagSpy, umode_allow_opers, &SNO_SPY);

        if (!SpySnomask || !CmdSpy || !CmdSpychannel || !CmdSpysend || !CmdSpychsend)
		ret = MOD_FAILED;
	if (!CmdSpyforward || !CmdSpychforward || !CmdSpystats)
		ret = MOD_FAILED;

	return ret;
}

DLLFUNC int MOD_LOAD(m_spy)(int module_load)
{
	return MOD_SUCCESS;
}

DLLFUNC int MOD_UNLOAD(m_spy)(int module_unload)
{
	SpyStruct	*s;
	SpyChStruct	*sc;
	ListStruct 	*next;

	DelSnomask(SpySnomask);

	DelCommand(CmdSpystats);
	DelCommand(CmdSpychforward);
	DelCommand(CmdSpyforward);
	DelCommand(CmdSpychsend);
	DelCommand(CmdSpysend);
	DelCommand(CmdSpychannel);
	DelCommand(CmdSpy);

	DelHook(HookChanDest);
	DelHook(HookRemoteNick);
	DelHook(HookLocalNick);
	DelHook(HookServConn);
	DelHook(HookRemoteQuit);
	DelHook(HookQuit);
	DelHook(HookChanMsg);
	DelHook(HookPrivMsg);

	for (s = SpyList; s; s = (SpyStruct *) next)
	{
		next = (ListStruct *) s->next;
		DelSpyItem(s);
	}
	for (sc = SpyChList; sc; sc = (SpyChStruct *) next)
	{
		next = (ListStruct *) sc->next;
		DelSpyChItem(sc);
	}

	return MOD_SUCCESS;
}

// ==================================================================
// Functions for nicknames, channel names and prefixes
// ==================================================================

static void del_prefix(char *str, u_int *lock, char **newstr)
{
	*lock = 0;

	if (*str == LockFlag)
	{
		*lock = 1;
		str++;
	}

	*newstr = str;
}

// ==================================================================
// Misc functions
// ==================================================================

static SpyStruct *FindSpyItem1(aClient *acptr, char *nick)
{
	SpyStruct *s;
	
	for (s = SpyList; s; s = s->next)
		if (s->who == acptr && !s->nick2 && !strcasecmp(s->nick1, nick))
			break;

	return s;
}

static SpyStruct *FindSpyItem2(aClient *acptr, char *nick1, char *nick2)
{
	SpyStruct *s;
	
	for (s = SpyList; s; s = s->next)
		if (s->who == acptr && !strcasecmp(s->nick1, nick1)
		  && (s->nick2 && !strcasecmp(s->nick2, nick2)))
			break;

	return s;
}

static SpyChStruct *FindSpyChItem(aClient *acptr, char *channel)
{
	SpyChStruct *sc;
	
	for (sc = SpyChList; sc; sc = sc->next)
		if (sc->who == acptr && !strcasecmp(sc->channel, channel))
			break;

	return sc;
}

static void AddSpyItem(aClient *acptr, char *nick1, char *nick2, char *chname, u_int lock1, u_int lock2)
{
	SpyStruct	*s;

	s		= (SpyStruct *) MyMalloc(sizeof(SpyStruct));
	s->who		= acptr;
	s->nick1	= strdup(nick1);
	s->nick2	= BadPtr(nick2) ? NULL : strdup(nick2);
	s->chname	= BadPtr(chname) ? NULL : strdup(chname);
	s->lock1	= lock1;
	s->lock2	= lock2;

	AddListItem(s, SpyList);
}

static void AddSpyChItem(aClient *acptr, char *channel, char *sendto, u_int lock)
{
	SpyChStruct	*sc;

	sc		= (SpyChStruct *) MyMalloc(sizeof(SpyChStruct));
	sc->who		= acptr;
	sc->channel	= strdup(channel);
	sc->sendto	= BadPtr(sendto) ? NULL : strdup(sendto);
	sc->lock	= lock;

	AddListItem(sc, SpyChList);
}

static void DelSpyItem(SpyStruct *s)
{
	DelListItem(s, SpyList);
	MyFree(s->nick1);
	ircfree(s->nick2);
	ircfree(s->chname);
	MyFree(s);
}

static void DelSpyChItem(SpyChStruct *sc)
{
	DelListItem(sc, SpyChList);
	MyFree(sc->channel);
	ircfree(sc->sendto);
	MyFree(sc);
}

// ==================================================================
// display_text: a general function to display two or more lines
//     of text to a user
// ==================================================================

static void display_text(aClient *cptr, char *text[])
{
	char **pp;

	for (pp = text; *pp; pp++)
    		sendto_one(cptr, *pp, me.name, cptr->name);
	/* let user take 7 seconds to read it! */
        if (MyClient(cptr))
                cptr->since += 7;
}

// ==================================================================
// spy_paramcheck: checks for validity of /spy parameters
// ==================================================================

static unsigned int spy_paramcheck(aClient *cptr, int parc, char *parv[],
	char **nick1, char **nick2, char **channel, u_int *lock1, u_int *lock2)
{
	*nick1		= NULL;
	*nick2		= NULL;
	*channel	= NULL;
	*lock1		= 0;
	*lock2		= 0;

	if (IsParam(2))
	{
		if (*parv[2] == '#')
			*channel = parv[2];
		else
			*nick1	= parv[2];
	}
	if (IsParam(3))
	{
		if (*channel)
			return 0;
		else if (*parv[3] == '#')
		{
			*nick2		= NULL;
			*channel	= parv[3];
		}
		else
			*nick2		= parv[3];
	}
	if (IsParam(4))
	{
		if (*channel)
			return 0;
		*channel = parv[4];
	}

	if (*nick1)
		SplitLock(*lock1, *nick1);
	if (*nick2)
		SplitLock(*lock2, *nick2);

	return 1;
}

// ==================================================================
// spych_paramcheck: checks for validity of /spychannel parameters
// ==================================================================

static unsigned int spych_paramcheck(aClient *cptr, int parc, char *parv[],
	char **channel, char **sendto, u_int *lock)
{
	*channel	= NULL;
	*sendto		= NULL;
	*lock		= 0;

	if (!IsParam(2))
		return 0;

	*channel = parv[2];

	if (IsParam(3))
		*sendto = parv[3];

	if (*channel)
		SplitLock(*lock, *channel);

	return 1;
}

// ==================================================================
// spy_send_client & spy_send_channel: handle sending users's private
//	messages to clients & channels
// ==================================================================

static void spy_send_client(aClient *acptr, int notice, char *nick1, char *nick2, char *text)
{
	if (MyClient(acptr))
    		sendto_one(acptr, ":IRC!IRC@%s PRIVMSG %s :[%s %c> %s] %s",
	            	me.name, acptr->name,
			nick1, (notice ? '-' : '>'), nick2, text);
	else
		sendto_one(acptr->from, "%s %s %c %s %s :%s",
			IsToken(acptr->from) ? TOK_SPYSEND : MSG_SPYSEND,
			acptr->name, (notice ? 'N' : 'P'),
			nick1, nick2, text);
}

static void spy_send_channel(aClient *cptr, char *channel, int notice, char *nick1, char *nick2, char *text)
{
	aChannel	*chptr;

	if ((chptr = find_channel(channel, NullChn)) == NullChn)
		return;

        sendto_channel_butserv(chptr, &me, ":IRC!IRC@%s PRIVMSG %s :[%s %c> %s] %s",
                me.name, chptr->chname,
		nick1, (notice ? '-' : '>'), nick2, text);

	sendto_serv_butone_token(cptr, me.name, MSG_SPYSEND, TOK_SPYSEND,
		"%s %c %s %s :%s",
		chptr->chname, (notice ? 'N' : 'P'),
		nick1, nick2, text);
}

static void spych_send_client(aClient *acptr, int notice, char *nick, char *channel, char *text)
{
	if (MyClient(acptr))
    		sendto_one(acptr, ":IRC!IRC@%s PRIVMSG %s :[%s] %c%s%c %s",
	            	me.name, acptr->name, channel,
			(notice ? '-' : '<'), nick, (notice ? '-' : '>'), text);
	else
		sendto_one(acptr->from, "%s %s %c %s %s :%s",
			IsToken(acptr->from) ? TOK_SPYCHSEND : MSG_SPYCHSEND,
			acptr->name, (notice ? 'N' : 'P'),
			nick, channel, text);
}

static void spych_send_channel(aClient *cptr, char *sendto, int notice, char *nick, char *channel, char *text)
{
	aChannel	*chptr;

	if ((chptr = find_channel(sendto, NullChn)) == NullChn)
		return;

        sendto_channel_butserv(chptr, &me, ":IRC!IRC@%s PRIVMSG %s :[%s] %c%s%c %s",
                me.name, chptr->chname, channel,
		(notice ? '-' : '<'), nick, (notice ? '-' : '>'), text);

	sendto_serv_butone_token(cptr, me.name, MSG_SPYCHSEND, TOK_SPYCHSEND,
		"%s %c %s %s :%s",
		chptr->chname, (notice ? 'N' : 'P'),
		nick, channel, text);
}

// ==================================================================
// Events
// ==================================================================

DLLFUNC int spy_on_quit(aClient *sptr, char *comment)
{
	SpyStruct	*s;
	SpyChStruct	*sc;
	ListStruct 	*next;

	// sendto_realops("(spy_on_quit) sptr->name=[%s]", sptr->name);

	for (s = SpyList; s; s = (SpyStruct *) next)
	{
		next = (ListStruct *) s->next;
		if (s->who == sptr)
			DelSpyItem(s);
		else if (!s->lock1 && !strcasecmp(s->nick1, sptr->name))
			DelSpyItem(s);
		else if (s->nick2 && !s->lock2 && !strcasecmp(s->nick2, sptr->name))
			DelSpyItem(s);
	}
	for (sc = SpyChList; sc; sc = (SpyChStruct *) next)
	{
		next = (ListStruct *) sc->next;
		if (sc->who == sptr)
			DelSpyChItem(sc);
	}

	return 0;
}

// ==================================================================

DLLFUNC int spy_on_remote_quit(aClient *sptr, char *comment)
{
	SpyStruct	*s;
	SpyChStruct	*sc;
	ListStruct 	*next;

	// sendto_realops("(spy_on_remote_quit) sptr->name=[%s]", sptr->name);

	for (s = SpyList; s; s = (SpyStruct *) next)
	{
		next = (ListStruct *) s->next;
		if (s->who == sptr)
			DelSpyItem(s);
		else if (!s->lock1 && !strcasecmp(s->nick1, sptr->name))
			DelSpyItem(s);
		else if (s->nick2 && !s->lock2 && !strcasecmp(s->nick2, sptr->name))
			DelSpyItem(s);
	}
	for (sc = SpyChList; sc; sc = (SpyChStruct *) next)
	{
		next = (ListStruct *) sc->next;
		if (sc->who == sptr)
			DelSpyChItem(sc);
	}

	return 0;
}

// ==================================================================

DLLFUNC int spy_on_local_nickchange(aClient *sptr, char *nick)
{
	SpyStruct	*s;

	for (s = SpyList; s; s = s->next)
	{
		if (!s->lock1 && !strcasecmp(s->nick1, sptr->name))
		{
			ircstrdup(s->nick1, nick);
		}
		if (s->nick2 && !s->lock2 && !strcasecmp(s->nick2, sptr->name))
		{
			ircstrdup(s->nick2, nick);
		}
	}

	return 0;
}

DLLFUNC int spy_on_remote_nickchange(aClient *cptr, aClient *sptr, char *nick)
{
	SpyStruct	*s;

	for (s = SpyList; s; s = s->next)
	{
		if (!s->lock1 && !strcasecmp(s->nick1, sptr->name))
		{
			ircstrdup(s->nick1, nick);
		}
		if (s->nick2 && !s->lock2 && !strcasecmp(s->nick2, sptr->name))
		{
			ircstrdup(s->nick2, nick);
		}
	}

	return 0;
}

// ==================================================================

DLLFUNC int spy_on_channel_destroy(aChannel *chptr)
{
	SpyChStruct	*sc;
	ListStruct	*next;

	for (sc = SpyChList; sc; sc = (SpyChStruct *) next)
	{
		next = (ListStruct *) sc->next;

		if (!sc->lock && !strcasecmp(sc->channel, chptr->chname))
			DelSpyChItem(sc);
	}

	return 0;
}

// ==================================================================

DLLFUNC int spy_on_server_connect(aClient *sptr)
{
	SpyStruct	*s;
	SpyChStruct	*sc;

	// sendto_realops("(spy_on_server_connect) sptr->name=[%s]", sptr->name);

	/* TODO: optimize this! */
	sendto_one(sptr, ":%s %s CONNECT",
		me.name, (IsToken(sptr) ? TOK_SPYFORWARD : MSG_SPYFORWARD));
	sendto_one(sptr, ":%s %s CONNECT",
		me.name, (IsToken(sptr) ? TOK_SPYCHFORWARD : MSG_SPYCHFORWARD));

	for (s = SpyList; s; s = s->next)
	{
		sendto_one(sptr, ":%s %s ADD2 %s %s%s %s%s %s",
			me.name,
			(IsToken(sptr) ? TOK_SPYFORWARD : MSG_SPYFORWARD),
			s->who->name,
			MergeLock(s->lock1, s->nick1),
			MergeLockOpt(s->lock2, s->nick2),
			s->chname ? s->chname : "*");
	}
	for (sc = SpyChList; sc; sc = sc->next)
	{
		sendto_one(sptr, ":%s %s ADD2 %s %s%s %s",
			me.name,
			(IsToken(sptr) ? TOK_SPYCHFORWARD : MSG_SPYCHFORWARD),
			sc->who->name,
			MergeLock(sc->lock, sc->channel),
			sc->sendto ? sc->sendto : "*");
	}

	return 0;
}

// ==================================================================

DLLFUNC char *spy_on_privmsg(aClient *cptr, aClient *sptr, aClient *acptr, char *text, int notice)
{
	SpyStruct	*s;
	aChannel	*chptr;
	char		*nick1, *nick2;

	if (!MyClient(sptr))
		return text;

	/* First do some sorting */

	if (smycmp(sptr->name, acptr->name) > 0)
	{
    		nick1 = acptr->name;
    		nick2 = sptr->name;
	}
	else
	{
		nick1 = sptr->name;
		nick2 = acptr->name;
	}

	for (s = SpyList; s; s = s->next)
	{
		if (
			/* spy every messages sent and received by one client */
			(
				!s->nick2 &&
				(!strcasecmp(s->nick1, sptr->name) || !strcasecmp(s->nick1, acptr->name))
			) ||
			/* spy messages between two specific users */
			(
				!strcasecmp(s->nick1, nick1) && !strcasecmp(s->nick2, nick2)
			)
		   )
		{
			if (s->chname)
					spy_send_channel(&me, s->chname, notice,
						sptr->name, acptr->name, text);
				else
					spy_send_client(s->who, notice,
						sptr->name, acptr->name, text);
		}
	}

	return text;
}

DLLFUNC char *spy_on_chanmsg(aClient *cptr, aClient *sptr, aChannel *chptr, char *text, int notice)
{
	SpyChStruct	*sc;

	if (!MyClient(sptr))
		return text;

	for (sc = SpyChList; sc; sc = sc->next)
	{
		if (!strcasecmp(chptr->chname, sc->channel))
		{
			if (sc->sendto)
				spych_send_channel(&me, sc->sendto, notice,
					sptr->name, chptr->chname, text);
			else
				spy_send_client(sc->who, notice,
					sptr->name, chptr->chname, text);
		}
	}

	return text;
}

// ==================================================================
// m_spy: Adds, removes or lists nicknames in spy list
// ==================================================================

DLLFUNC int m_spy(aClient *cptr, aClient *sptr, int parc, char *parv[])
{
	SpyStruct	*s;
	char		*tmp = NULL, *cmd;
	char		*nick1, *nick2, *channel;
	u_int		lock1, lock2;

        if (!MyClient(cptr) || !IsOper(cptr) || !IsSAdmin(cptr))
	{
        	sendto_one(sptr, err_str(ERR_NOPRIVILEGES), me.name, parv[0]);
		return -1;
	}

	cmd = IsParam(1) ? parv[1] : NULL;

        if (!cmd)
	{
		display_text(cptr, SpyHelp);
		return -1;
	}

	/* ADD */
	if (!strcasecmp(cmd, "add"))
	{
		if (!spy_paramcheck(cptr, parc, parv, &nick1, &nick2, &channel, &lock1, &lock2))
		{
			display_text(cptr, SpyHelp);
			return -1;
		}
		if (!nick1 || IsParam(5))
		{
			display_text(cptr, SpyHelp);
			return -1;
		}
		if (nick2 && !strcasecmp(nick1, nick2))
		{
    			sendto_one(sptr, ":%s NOTICE %s :*** %s %s: Nicks cannot be the same",
	            		me.name, sptr->name,
				nick1, nick2);
			return -1;
		}
		if (channel && *channel != '#')
		{
    			sendto_one(sptr, ":%s NOTICE %s :*** %s: Invalid channel name",
	            		me.name, sptr->name, channel);
			return -1;
		}
		if (nick2 && (smycmp(nick1, nick2) > 0))
		{
			tmp	= nick1;
			nick1	= nick2;
			nick2	= tmp;
		}
		if ((nick2 && FindSpyItem2(sptr, nick1, nick2)) || (!nick2 && FindSpyItem1(sptr, nick1)))
		{
    			sendto_one(sptr, ":%s NOTICE %s :*** %s %s: Already on your private spy list",
	            		me.name, sptr->name,
				nick1, nick2 ? nick2 : "<irc>");
			return -1;
		}
		AddSpyItem(sptr, nick1, nick2, channel, lock1, lock2);
    		sendto_one(sptr, ":%s NOTICE %s :*** %s %s: Added to your private spy list",
	            	me.name, sptr->name,
			nick1, nick2 ? nick2 : "<irc>");
		SpyForward5(&me, "ADD", sptr->name, nick1, nick2, channel, lock1, lock2);
		sendto_snomask(SNO_SPY, "*** [\2m_spy\2] %s used /SPY ADD %s%s" "%s" "%s%s" "%s" "%s",
			sptr->name, MergeLock(lock1, nick1),
			nick2 ? " " : "", MergeLockEmp(lock2, nick2),
			channel ? " " : "", channel ? channel : "");
	}

	/* DEL */
	else if (!strcasecmp(cmd, "del"))
	{
		if (!spy_paramcheck(cptr, parc, parv, &nick1, &nick2, &channel, &lock1, &lock2))
		{
			display_text(cptr, SpyHelp);
			return -1;
		}
		if (!nick1 || channel || IsParam(4))
		{
			display_text(cptr, SpyHelp);
			return -1;
		}
		if (nick2 && (smycmp(nick1, nick2) > 0))
		{
			tmp	= nick1;
			nick1	= nick2;
			nick2	= tmp;
		}
		if ((nick2 && !(s = FindSpyItem2(sptr, nick1, nick2))) || (!nick2 && !(s = FindSpyItem1(sptr, nick1))))
		{
    			sendto_one(sptr, ":%s NOTICE %s :*** %s %s: Not yet on your private spy list",
	            		me.name, sptr->name,
				nick1, nick2 ? nick2 : "<irc>");
			return -1;
		}
		SpyForward4(&me, "DEL", s->who->name, s->nick1, s->nick2, 0, 0);
    		sendto_one(sptr, ":%s NOTICE %s :*** %s %s: Deleted from your private spy list",
	            	me.name, sptr->name,
			nick1, nick2 ? nick2 : "<irc>");
		sendto_snomask(SNO_SPY, "*** [\2m_spy\2] %s used /SPY DEL %s%s%s",
			sptr->name, s->nick1, s->nick2 ? " " : "", s->nick2 ? s->nick2 : "");
		DelSpyItem(s);
	}

	/* LIST */
	else if (!strcasecmp(cmd, "list"))
	{
		enum ListType { LT_n1n2c, LT_n1n2, LT_n1c, LT_n1, LT_c, LT_none };
		unsigned int found = 0, type = 0, show = 0, empty = 1;

		if (!spy_paramcheck(cptr, parc, parv, &nick1, &nick2, &channel, &lock1, &lock2))
		{
			display_text(cptr, SpyHelp);
			return -1;
		}
		if (channel)
		{
			if (nick2)
				type = LT_n1n2c;
			else if (nick1)
				type = LT_n1c;
			else
				type = LT_c;
		}
		else if (nick2)
			type = LT_n1n2;
		else if (nick1)
			type = LT_n1;
		else
			type = LT_none;

		if (nick1 && nick2 && (smycmp(nick1, nick2) > 0))
		{
			tmp	= nick1;
			nick1	= nick2;
			nick2	= tmp;
		}

		for (s = SpyList; s; s = s->next)
			if (s->who == sptr)
			{
				empty	= 0;
				show	= 0;

				switch (type)
				{
					case LT_n1n2c:
						if (
							s->nick2 &&
							s->chname &&
							!strcasecmp(s->nick1, nick1) &&
							!strcasecmp(s->nick2, nick2) &&
							!strcasecmp(s->chname, channel)
						   )
							show = 1;
						break;
					case LT_n1n2:
						if (
							s->nick2 &&
							!strcasecmp(s->nick1, nick1) &&
							!strcasecmp(s->nick2, nick2)
						   )
							show = 1;
						break;
					case LT_n1c:
						if (
							s->chname &&
							!strcasecmp(s->nick1, nick1) &&
							!strcasecmp(s->chname, channel)
						   )
							show = 1;
						break;
					case LT_n1:
						if (!strcasecmp(s->nick1, nick1))
							show = 1;
						break;
					case LT_c:
						if (s->chname && !strcasecmp(s->chname, channel))
							show = 1;
						break;
					default:
						show = 1;
				}

				if (show)
				{
					found++;
				
    					sendto_one(sptr, ":%s NOTICE %s :*** %s%s %s%s %s",
	            				me.name, sptr->name,
						MergeLock(s->lock1, s->nick1),
						MergeLockOpt(s->lock2, s->nick2),
						s->chname ? s->chname : "<irc>");
				}
			}

		if (!found)
		{
    			sendto_one(sptr, ":%s NOTICE %s :*** %s",
	    			me.name, sptr->name,
				empty ? "Your spy list is empty" : "No matching entries");
		}
		else
		{
			sendto_one(sptr, ":%s NOTICE %s :*** " SPY_LINE,
				me.name, sptr->name);
			sendto_one(sptr, ":%s NOTICE %s :*** %d entr%s",
				me.name, sptr->name,
				found, found > 1 ? "ies" : "y");
		}
	}

	/* CLEAR */
	else if (!strcasecmp(cmd, "clear"))
	{
		ListStruct *next;

		for (s = SpyList; s; s = (SpyStruct *) next)
		{
			next = (ListStruct *) s->next;
			if (s->who == sptr)
				DelSpyItem(s);
		}
    		sendto_one(sptr, ":%s NOTICE %s :*** Your private spy list is now empty",
	            	me.name, sptr->name);
		SpyForward2(&me, "CLEAR", sptr->name);
	}

	/* <INVALID OPTION> */
	else
	{
    		sendto_one(sptr, ":%s NOTICE %s :*** Invalid option %s",
	            	me.name, sptr->name, cmd);
		return -1;
	}

	return 0;
}

// ==================================================================
// m_spychannel: Adds, removes or lists channel names in channel
//     spy list
// ==================================================================

DLLFUNC int m_spychannel(aClient *cptr, aClient *sptr, int parc, char *parv[])
{
	SpyChStruct	*sc;
	char		*cmd, *channel, *sendto;
	u_int		lock;

        if (!MyClient(cptr) || !IsOper(cptr) || !IsSAdmin(cptr))
	{
        	sendto_one(sptr, err_str(ERR_NOPRIVILEGES), me.name, parv[0]);
		return -1;
	}

	cmd = IsParam(1) ? parv[1] : NULL;

        if (!cmd)
	{
		display_text(cptr, SpyChHelp);
		return -1;
	}

	/* ADD */
	if (!strcasecmp(cmd, "add"))
	{
		if (!spych_paramcheck(cptr, parc, parv, &channel, &sendto, &lock))
		{
			display_text(cptr, SpyChHelp);
			return -1;
		}
		if (*channel != '#')
		{
    			sendto_one(sptr, ":%s NOTICE %s :*** %s: Invalid channel name",
	            		me.name, sptr->name, channel);
			return -1;
		}
		if (sendto && *sendto != '#')
		{
    			sendto_one(sptr, ":%s NOTICE %s :*** %s: Invalid channel name",
	            		me.name, sptr->name, sendto);
			return -1;
		}
		if (FindSpyChItem(sptr, channel))
		{
    			sendto_one(sptr, ":%s NOTICE %s :*** %s: Already on your channel spy list",
	            		me.name, sptr->name, channel);
			return -1;
		}
		if (sendto && !strcasecmp(channel, sendto))
		{
    			sendto_one(sptr, ":%s NOTICE %s :*** %s %s: Source and target channels cannot be the same",
	            		me.name, sptr->name, channel, sendto);
			return -1;
		}
		AddSpyChItem(sptr, channel, sendto, lock);
    		sendto_one(sptr, ":%s NOTICE %s :*** %s: Added to your channel spy list",
	            	me.name, sptr->name, channel);
		SpyChForward4(&me, "ADD", sptr->name, channel, sendto, lock);
		sendto_snomask(SNO_SPY, "*** [\2m_spy\2] %s used /SPYCHANNEL ADD %s%s" "%s" "%s",
			sptr->name, MergeLock(lock, channel), sendto ? " " : "",
			sendto ? sendto : "");
	}

	/* DEL */
	else if (!strcasecmp(cmd, "del"))
	{
		if (!spych_paramcheck(cptr, parc, parv, &channel, &sendto, &lock))
		{
			display_text(cptr, SpyChHelp);
			return -1;
		}
		if (!(sc = FindSpyChItem(sptr, channel)))
		{
    			sendto_one(sptr, ":%s NOTICE %s :*** %s: Not yet on your channel spy list",
	            		me.name, sptr->name, channel);
			return -1;
		}
		SpyChForward3(&me, "DEL", sc->who->name, sc->channel, 0);
    		sendto_one(sptr, ":%s NOTICE %s :*** %s: Deleted from your channel spy list",
	            	me.name, sptr->name, channel);
		sendto_snomask(SNO_SPY, "*** [\2m_spy\2] %s used /SPYCHANNEL DEL %s",
			sptr->name, sc->channel);
		DelSpyChItem(sc);
	}

	/* LIST */
	else if (!strcasecmp(cmd, "list"))
	{
		unsigned int found = 0, empty = 1;

		sendto	= NULL;
		channel	= IsParam(2) ? parv[2] : NULL;

		if (channel)
			SplitLock(lock, channel);

		for (sc = SpyChList; sc; sc = sc->next)
			if (sc->who == sptr)
			{
				empty = 0;

				if (!channel || !strcasecmp(sc->channel, channel))
				{
					found++;
				
    					sendto_one(sptr, ":%s NOTICE %s :*** %s%s %s",
	            				me.name, sptr->name,
						MergeLock(sc->lock, sc->channel),
						sc->sendto ? sc->sendto : "<irc>");
				}
			}

		if (!found)
		{
    			sendto_one(sptr, ":%s NOTICE %s :*** %s",
	    			me.name, sptr->name,
				empty ? "Your channel spy list is empty" : "No matching entries");
		}
		else
		{
			sendto_one(sptr, ":%s NOTICE %s :*** " SPY_LINE,
				me.name, sptr->name);
			sendto_one(sptr, ":%s NOTICE %s :*** %d entr%s",
				me.name, sptr->name,
				found, found > 1 ? "ies" : "y");
		}
	}

	/* CLEAR */
	else if (!strcasecmp(cmd, "clear"))
	{
		ListStruct *next;

		for (sc = SpyChList; sc; sc = (SpyChStruct *) next)
		{
			next = (ListStruct *) sc->next;
			if (sc->who == sptr)
				DelSpyChItem(sc);
		}
    		sendto_one(sptr, ":%s NOTICE %s :*** Your channel spy list is now empty",
	            	me.name, sptr->name);
		SpyChForward2(&me, "CLEAR", sptr->name);
	}

	/* <INVALID OPTION> */
	else
	{
    		sendto_one(sptr, ":%s NOTICE %s :*** Invalid option %s",
	            	me.name, sptr->name, cmd);
		return -1;
	}

	return 0;
}

// ==================================================================
// m_spyforward: Keeps SpyList updated on all servers
// ==================================================================

DLLFUNC int m_spyforward(aClient *cptr, aClient *sptr, int parc, char *parv[])
{
	SpyStruct	*s;
	ListStruct 	*next;
	aClient		*acptr;
	char		*tmp, *cmd, *who, *nick1, *nick2, *channel;
	u_int		lock1, lock2;

        if (IsClient(cptr))
	{
        	sendto_one(sptr, err_str(ERR_NOPRIVILEGES), me.name, parv[0]);
		return -1;
	}

	cmd	= IsParam(1)		? parv[1] : NULL;
	who	= IsParam(2)		? parv[2] : NULL;
	nick1	= IsParam(3)		? parv[3] : NULL;
	nick2	= IsValidParam(4)	? parv[4] : NULL;
	channel	= IsValidParam(5)	? parv[5] : NULL;

	/* sendto_realops("(\2m_spyforward\2) cptr=%s, sptr=%s, cmd=%s who=%s nick1=%s nick2=%s channel=%s",
		cptr->name, sptr->name,
		cmd	? cmd		: "(null)",
		who	? who		: "(null)",
		nick1	? nick1		: "(null)",
		nick2	? nick2		: "(null)",
		channel	? channel	: "(null)"); */

        if (!cmd)
		return -1;
	if (nick1)
		SplitLock(lock1, nick1);
	if (nick2)
		SplitLock(lock2, nick2);

	/* ADD */
	if (!strcasecmp(cmd, "add"))
	{
		if (!nick1)
			return -1;
		if (nick2 && !strcasecmp(nick1, nick2))
			return -1;
		if (nick2 && (smycmp(nick1, nick2) > 0))
		{
			tmp	= nick1;
			nick1	= nick2;
			nick2	= tmp;
		}
		if (!(acptr = find_person(who, NULL)))
			return -1;
		if ((nick2 && FindSpyItem2(acptr, nick1, nick2)) || (!nick2 && FindSpyItem1(acptr, nick1)))
			return -1;
		AddSpyItem(acptr, nick1, nick2, channel, lock1, lock2);
		SpyForward5(cptr, "ADD", acptr->name, nick1, nick2, channel, lock1, lock2);
		sendto_snomask(SNO_SPY, "*** [\2m_spy\2] %s used /SPY ADD %s%s" "%s" "%s%s" "%s" "%s",
			acptr->name, MergeLock(lock1, nick1),
			nick2 ? " " : "", MergeLockEmp(lock2, nick2),
			channel ? " " : "", channel ? channel : "");
	}

	/* ADD2 -- Like ADD, but without forwarding */
	if (!strcasecmp(cmd, "add2"))
	{
		if (!nick1)
			return -1;
		if (nick2 && !strcasecmp(nick1, nick2))
			return -1;
		if (nick2 && (smycmp(nick1, nick2) > 0))
		{
			tmp	= nick1;
			nick1	= nick2;
			nick2	= tmp;
		}
		if (!(acptr = find_person(who, NULL)))
			return -1;
		if ((nick2 && FindSpyItem2(acptr, nick1, nick2)) || (!nick2 && FindSpyItem1(acptr, nick1)))
			return -1;
		AddSpyItem(acptr, nick1, nick2, channel, lock1, lock2);
	}

	/* DEL */
	else if (!strcasecmp(cmd, "del"))
	{
		if (!nick1)
			return -1;
		if (nick2 && (smycmp(nick1, nick2) > 0))
		{
			tmp	= nick1;
			nick1	= nick2;
			nick2	= tmp;
		}
		if (!(acptr = find_person(who, NULL)))
			return -1;
		if ((nick2 && !(s = FindSpyItem2(acptr, nick1, nick2))) || (!nick2 && !(s = FindSpyItem1(acptr, nick1))))
			return -1;
		sendto_snomask(SNO_SPY, "*** [\2m_spy\2] %s used /SPY DEL %s%s%s",
			acptr->name, s->nick1, s->nick2 ? " " : "", s->nick2 ? s->nick2 : "");
		SpyForward4(cptr, "DEL", acptr->name, s->nick1, s->nick2, 0, 0);
		DelSpyItem(s);
	}

	/* CLEAR */
	else if (!strcasecmp(cmd, "clear"))
	{
		if (!who)
			return -1;
		if (!(acptr = find_person(who, NULL)))
			return -1;
		for (s = SpyList; s; s = (SpyStruct *) next)
		{
			next = (ListStruct *) s->next;
			if (s->who == acptr)
				DelSpyItem(s);
		}
		SpyForward2(cptr, "CLEAR", acptr->name);
	}

	/* CONNECT */
	else if (!strcasecmp(cmd, "connect"))
	{
		for (s = SpyList; s; s = s->next)
			/* act as a client who is just adding a spy entry */
			SpyForward5(&me, "ADD", s->who->name, s->nick1, s->nick2,
				s->chname, s->lock1, s->lock2);
	}

	/* <INVALID OPTION> */
	else
	{
		return -1;
	}

	return 0;
}

// ==================================================================
// m_spychforward: Keeps SpyChList updated on all servers
// ==================================================================

DLLFUNC int m_spychforward(aClient *cptr, aClient *sptr, int parc, char *parv[])
{
	SpyChStruct	*sc;
	ListStruct 	*next;
	aClient		*acptr;
	char		*cmd, *who, *channel, *sendto;
	u_int		lock;

        if (IsClient(cptr))
	{
        	sendto_one(sptr, err_str(ERR_NOPRIVILEGES), me.name, parv[0]);
		return -1;
	}

	cmd	= IsParam(1)		? parv[1] : NULL;
	who	= IsParam(2)		? parv[2] : NULL;
	channel	= IsValidParam(3)	? parv[3] : NULL;
	sendto	= IsValidParam(4)	? parv[4] : NULL;

	/* sendto_realops("(\2m_spychforward\2) cptr=%s, sptr=%s, cmd=%s who=%s channel=%s sendto=%s",
		cptr->name, sptr->name,
		cmd	? cmd		: "(null)",
		who	? who		: "(null)",
		channel	? channel	: "(null)",
		sendto	? sendto	: "(null)"); */

        if (!cmd)
		return -1;
	if (channel)
		SplitLock(lock, channel);

	/* ADD */
	if (!strcasecmp(cmd, "add"))
	{
		if (!channel)
			return -1;
		if (!(acptr = find_person(who, NULL)))
			return -1;
		if (FindSpyChItem(acptr, channel))
			return -1;
		AddSpyChItem(acptr, channel, sendto, lock);
		SpyChForward4(cptr, "ADD", acptr->name, channel, sendto, lock);
		sendto_snomask(SNO_SPY, "*** [\2m_spy\2] %s used /SPYCHANNEL ADD %s%s" "%s" "%s",
			acptr->name, MergeLock(lock, channel),
			sendto ? " " : "", sendto ? sendto : "");
	}

	/* ADD2 -- Like ADD, but without forwarding */
	if (!strcasecmp(cmd, "add2"))
	{
		if (!channel)
			return -1;
		if (!(acptr = find_person(who, NULL)))
			return -1;
		if (FindSpyChItem(acptr, channel))
			return -1;
		AddSpyChItem(acptr, channel, sendto, lock);
	}

	/* DEL */
	else if (!strcasecmp(cmd, "del"))
	{
		if (!channel)
			return -1;
		if (!(acptr = find_person(who, NULL)))
			return -1;
		if (!(sc = FindSpyChItem(acptr, channel)))
			return -1;
		sendto_snomask(SNO_SPY, "*** [\2m_spy\2] %s used /SPYCHANNEL DEL %s",
			acptr->name, sc->channel);
		SpyChForward3(cptr, "DEL", acptr->name, sc->channel, 0);
		DelSpyChItem(sc);
	}

	/* CLEAR */
	else if (!strcasecmp(cmd, "clear"))
	{
		if (!who)
			return -1;
		if (!(acptr = find_person(who, NULL)))
			return -1;
		for (sc = SpyChList; sc; sc = (SpyChStruct *) next)
		{
			next = (ListStruct *) sc->next;
			if (sc->who == acptr)
				DelSpyChItem(sc);
		}
		SpyChForward2(cptr, "CLEAR", acptr->name);
	}

	/* CONNECT */
	else if (!strcasecmp(cmd, "connect"))
	{
		for (sc = SpyChList; sc; sc = sc->next)
			/* act as a client who is just adding a channel spy entry */
			SpyChForward4(&me, "ADD", sc->who->name, sc->channel,
				sc->sendto, sc->lock);
	}

	/* <INVALID OPTION> */
	else
	{
		return -1;
	}

	return 0;
}

// ==================================================================
// m_spysend: Sends users' private messages to a client on the network
//	Easier way?
// ==================================================================

DLLFUNC int m_spysend(aClient *cptr, aClient *sptr, int parc, char *parv[])
{
	aClient		*acptr;
	char		*from, *to, *cmd, *nick1, *nick2, *text;
	unsigned	notice;

	if (!IsServer(sptr) || !IsParam(5))
		return -1;

	to	= IsParam(1)		? parv[1] : NULL;
	cmd	= IsParam(2)		? parv[2] : NULL;
	nick1	= IsParam(3)		? parv[3] : NULL;
	nick2	= IsParam(4)		? parv[4] : NULL;
	text	= IsParam(5)		? parv[5] : NULL;

	if (!text)
		return -1;

	notice	= *cmd == 'N' ? 1 : 0;

	if (*to == '#')
		spy_send_channel(cptr, to, notice,
			nick1, nick2, text);
	else
	{
		if (!(acptr = find_person(to, NULL)))
			return -1;
		spy_send_client(acptr, notice,
			nick1, nick2, text);
	}

	return 0;
}

// ==================================================================
// m_spychsend: Sends users' channel messages to a client on the network
//	Easier way?
// ==================================================================

DLLFUNC int m_spychsend(aClient *cptr, aClient *sptr, int parc, char *parv[])
{
	aClient		*acptr;
	char		*to, *cmd, *nick, *channel, *text;
	unsigned	notice;

	if (!IsServer(sptr) || !IsParam(5))
		return -1;

	to	= IsParam(1)		? parv[1] : NULL;
	cmd	= IsParam(2)		? parv[2] : NULL;
	nick	= IsParam(3)		? parv[3] : NULL;
	channel	= IsParam(4)		? parv[4] : NULL;
	text	= IsParam(5)		? parv[5] : NULL;

	if (!text)
		return -1;

	notice	= *cmd == 'N' ? 1 : 0;

	if (*to == '#')
		spy_send_channel(cptr, to, notice,
			nick, channel, text);
	else
	{
		if (!(acptr = find_person(to, NULL)))
			return -1;
		spy_send_client(acptr, notice,
			nick, channel, text);
	}

	return 0;
}

/* 
 * m_spystats
 *
 * Displays SpyList statistics based on a given nickname. If you leave out
 * <nickname>, all SpyList entries will be displayed.
 *
 * :prefix SPYSTATS [<nickname>]
 * parv[0] - (sender)
 * parv[1] - channel|message
 * parv[2] - <nickname> (optional)
 *
 */

DLLFUNC int m_spystats(aClient *cptr, aClient *sptr, int parc, char *parv[])
{
	SpyStruct	*s;
	SpyChStruct	*sc;
	aClient		*acptr = NULL;
	unsigned long	item = 0, count = 0, total = 0;
	unsigned long	subcount = 0, subtotal = 0;
	char		*cmd;

        if (!MyClient(cptr) || !IsOper(cptr) || !IsSAdmin(cptr))
	{
        	sendto_one(sptr, err_str(ERR_NOPRIVILEGES), me.name, parv[0]);
		return -1;
	}
	if (!IsParam(1))
	{
    		sendto_one(sptr, ":%s NOTICE %s :*** Usage: /spystats private|channel [<nickname>]",
	    		me.name, sptr->name);
		return 0;
	}

	cmd = parv[1];

	/* PRIVATE */
	if (!strcasecmp(cmd, "private"))
	{
		if (!SpyList)
		{
    			sendto_one(sptr, ":%s NOTICE %s :*** Private spy list is empty",
	    			me.name, sptr->name);
			return 0;
		}
		if (IsParam(2) && !(acptr = find_person(parv[2], NULL)))
		{
	    		sendto_one(sptr, err_str(ERR_NOSUCHNICK),
				me.name, sptr->name, parv[2]);
	    		return -1;
		}

		for (s = SpyList; s; s = s->next)
		{
			item = sizeof(*s) + 1; /* 1 byte is the size of (lock1 + lock2) */
			item += sizeof(char) * (strlen(s->nick1) + 1);
			if (s->nick2)
				item += sizeof(char) * (strlen(s->nick2) + 1);
			if (s->chname)
				item += sizeof(char) * (strlen(s->chname) + 1);
			if (acptr && s->who == acptr)
			{
				subtotal += item;
				subcount++;
			}
			total += item;
			count++;

			if (!acptr || s->who == acptr)
    				sendto_one(sptr, ":%s NOTICE %s :*** %s (%s%s): %s%s %s (mem: %ld)",
	    				me.name, sptr->name,
					s->who->name,
					MergeLock(s->lock1, s->nick1),
					MergeLockOpt(s->lock2, s->nick2),
					s->chname ? s->chname : "<irc>",
					item);
		}

		if (acptr)
		{
			sendto_one(sptr, ":%s NOTICE %s :*** " SPY_LINE,
				me.name, sptr->name);
    			sendto_one(sptr, ":%s NOTICE %s :*** Sub total (%s): %ld entr%s (mem: %ld)",
	    			me.name, sptr->name,
				acptr->name,
				subcount, count > 1 ? "ies" : "y",
				subtotal);
		}

		sendto_one(sptr, ":%s NOTICE %s :*** " SPY_LINE,
			me.name, sptr->name);

		sendto_one(sptr, ":%s NOTICE %s :*** Total: %d entr%s (mem: %ld)",
			me.name, sptr->name,
			count, count > 1 ? "ies" : "y",
			total);
	}

	/* CHANNEL */
	else if (!strcasecmp(cmd, "channel"))
	{
		if (!SpyChList)
		{
    			sendto_one(sptr, ":%s NOTICE %s :*** Channel spy list is empty",
	    			me.name, sptr->name);
			return 0;
		}
		if (IsParam(2) && !(acptr = find_person(parv[2], NULL)))
		{
	    		sendto_one(sptr, err_str(ERR_NOSUCHNICK),
				me.name, sptr->name, parv[2]);
	    		return -1;
		}

		for (sc = SpyChList; sc; sc = sc->next)
		{
			item = sizeof(*sc) + sizeof(sc->lock);
			item += sizeof(char) * (strlen(sc->channel) + 1);
			if (sc->sendto)
				item += sizeof(char) * (strlen(sc->sendto) + 1);
			if (acptr && sc->who == acptr)
			{
				subtotal += item;
				subcount++;
			}
			total += item;
			count++;

			if (!acptr || sc->who == acptr)
    				sendto_one(sptr, ":%s NOTICE %s :*** %s (%s%s): %s (mem: %ld)",
	    				me.name, sptr->name,
					sc->who->name,
					MergeLock(sc->lock, sc->channel),
					sc->sendto ? sc->sendto : "<irc>",
					item);
		}

		if (acptr)
		{
			sendto_one(sptr, ":%s NOTICE %s :*** " SPY_LINE,
				me.name, sptr->name);
    			sendto_one(sptr, ":%s NOTICE %s :*** Sub total (%s): %ld entr%s (mem: %ld)",
	    			me.name, sptr->name,
				acptr->name,
				subcount, count > 1 ? "ies" : "y",
				subtotal);
		}

		sendto_one(sptr, ":%s NOTICE %s :*** " SPY_LINE,
			me.name, sptr->name);

		sendto_one(sptr, ":%s NOTICE %s :*** Total: %d entr%s (mem: %ld)",
			me.name, sptr->name,
			count, count > 1 ? "ies" : "y",
			total);
	}

	/* <INVALID OPTION> */
	else
	{
    		sendto_one(sptr, ":%s NOTICE %s :*** Invalid option %s",
	            	me.name, sptr->name, cmd);
		return -1;
	}
	
	return 0;
}
