/*
 * =================================================================
 * Filename:            activeusers.c
 * Description:		Channel dumper module
 * Author:		AngryWolf <angrywolf@flashmail.com>
 * Documentation:	activeusers.txt (comes with the package)
 * =================================================================
 */

#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

extern ConfigEntry	*config_find_entry(ConfigEntry *, char *);
extern void		sendto_one(aClient *to, char *pattern, ...);
extern void		sendto_realops(char *pattern, ...);

#define ircfree(x)	if (x) MyFree(x); x = NULL
#define ircstrdup(x,y)	if (x) MyFree(x); if (!y) x = NULL; else x = strdup(y)

typedef struct _monitorchan MonitorChan;

struct _monitorchan
{
	MonitorChan	*prev, *next;
	char		*channel;
	char		*db_file, *xml_file, *xsl_file;
#if !defined(_WIN32) && !defined(_AMIGA)
	long		perms;
#endif
	Event		*event;
};

static struct
{
	long		update_frequency;
	u_int		show_realhost;
} MyConf;

static struct
{
	unsigned	update_frequency : 1;
} ReqConf;

static int		cb_test(ConfigFile *, ConfigEntry *, int, int *);
static int		cb_conf(ConfigFile *, ConfigEntry *, int);
static int		cb_posttest(int *);
static int		cb_rehash();
static int		cb_stats(aClient *, char *);
static void		event_activeusers(MonitorChan *m);

static MonitorChan	*ChanList;
static Hook		*ConfTest, *ConfRun, *ConfPostTest, *ConfRehash;
static Hook		*ServerStats;

#ifndef STATIC_LINKING
static ModuleInfo	*MyModInfo;
 #define MyMod		MyModInfo->handle
 #define SAVE_MODINFO	MyModInfo = modinfo;
#else
 #define MyMod		NULL
 #define SAVE_MODINFO
#endif

ModuleHeader MOD_HEADER(activeusers)
  = {
	"activeusers",
	"$Id: activeusers.c,v 3.2 2004/07/12 17:39:12 angrywolf Exp $",
	"Channel member list dumper",
	"3.2-b8-1",
	NULL 
    };

DLLFUNC int MOD_TEST(activeusers)(ModuleInfo *modinfo)
{
	SAVE_MODINFO

	ConfTest	= HookAddEx(modinfo->handle, HOOKTYPE_CONFIGTEST, cb_test);
	ConfPostTest	= HookAddEx(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, cb_posttest);

	return MOD_SUCCESS;
}

DLLFUNC int MOD_INIT(activeusers)(ModuleInfo *modinfo)
{
	SAVE_MODINFO

	memset(&MyConf, 0, sizeof MyConf);
	memset(&ReqConf, 0, sizeof ReqConf);
	ChanList = NULL;

	ConfRun		= HookAddEx(modinfo->handle, HOOKTYPE_CONFIGRUN, cb_conf);
	ConfRehash	= HookAddEx(modinfo->handle, HOOKTYPE_REHASH, cb_rehash);
	ServerStats	= HookAddEx(modinfo->handle, HOOKTYPE_STATS, cb_stats);

	return MOD_SUCCESS;
}

DLLFUNC int MOD_LOAD(activeusers)(int module_load)
{
	MonitorChan *m;

	for (m = ChanList; m; m = m->next)
	{
		m->event = EventAddEx(MyMod, "activeusers",
			MyConf.update_frequency, 0, event_activeusers, m);

#ifndef STATIC_LINKING
		if (ModuleGetError(MyMod) != MODERR_NOERROR || !m->event)
			sendto_realops("Error adding monitorchan event for channel %s: %s",
				m->channel, ModuleGetErrorStr(MyMod));
#else
		if (!m->event)
			sendto_realops("Error adding monitorchan event for channel %s",
				m->channel);
#endif
	}

	return MOD_SUCCESS;
}

DLLFUNC int MOD_UNLOAD(activeusers)(int module_unload)
{
	HookDel(ConfPostTest);
	HookDel(ConfRun);
	HookDel(ConfRehash);
	HookDel(ConfTest);
	HookDel(ServerStats);
	cb_rehash();

	return MOD_SUCCESS;
}

static int cb_rehash()
{
	MonitorChan	*m;
	ListStruct 	*next;

	for (m = ChanList; m; m = (MonitorChan *) next)
	{
		next = (ListStruct *) m->next;
		DelListItem(m, ChanList);
		EventDel(m->event);
		MyFree(m->channel);
		ircfree(m->db_file);
		ircfree(m->xml_file);
		ircfree(m->xsl_file);
	}

	return 1;
}

static int cb_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
	ConfigEntry	*cep, *cepp;
	int		errors = 0;

	if (type != CONFIG_SET)
		return 0;

	if (!strcmp(ce->ce_varname, "activeusers"))
	{
		for (cep = ce->ce_entries; cep; cep = cep->ce_next)
		{
			if (!cep->ce_varname)
			{
				config_error("%s:%i: blank set::activeusers item",
					cep->ce_fileptr->cf_filename,
					cep->ce_varlinenum);	
				errors++;
				continue;
			}
			if (!cep->ce_vardata)
			{
				config_error("%s:%i: set::activeusers item without name",
					cep->ce_fileptr->cf_filename, cep->ce_varlinenum);
					errors++;
				continue;
			}
			if (!strcmp(cep->ce_varname, "update-frequency"))
				ReqConf.update_frequency = 1;
			else if (!strcmp(cep->ce_varname, "show-realhost"))
				;
			else if (!strcmp(cep->ce_varname, "monitorchan"))
			{
				cepp = config_find_entry(cep->ce_entries, "xml-file");

				if (!config_find_entry(cep->ce_entries, "db-file") && !cepp)
				{
					config_error("%s:%i: set::activeusers::monitorchan without "
						"db-file or xml-file item",
						cep->ce_fileptr->cf_filename, cep->ce_varlinenum);
					continue;
				}
				if (!cepp && config_find_entry(cep->ce_entries, "xsl-file"))
				{
					config_error("%s:%i: set::activeusers::monitorchan::xsl-file "
						"has no sense without an xml-file item",
						cep->ce_fileptr->cf_filename, cep->ce_varlinenum);
					continue;
				}

				for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next)
				{
					if (!cepp->ce_varname)
					{
						config_error("%s:%i: blank set::activeusers::monitorchan item",
							cepp->ce_fileptr->cf_filename,
							cepp->ce_varlinenum);	
						errors++;
						continue;
					}
					if (!cepp->ce_vardata)
					{
						config_error("%s:%i: set::activeusers::monitorchan item "
							"without name",
							cepp->ce_fileptr->cf_filename,
							cepp->ce_varlinenum);
						errors++;
						continue;
					}
					if (!strcmp(cepp->ce_varname, "db-file"))
						 ;
					else if (!strcmp(cepp->ce_varname, "xml-file"))
						 ;
					else if (!strcmp(cepp->ce_varname, "xsl-file"))
						 ;
#if !defined(_WIN32) && !defined(_AMIGA)
					else if (!strcmp(cepp->ce_varname, "chmod"))
					{
						char *s;
						long chmod;

						chmod = strtol(cepp->ce_vardata, &s, 8);
						if (*s || chmod < 0 || chmod > 01777)
						{
							config_error("%s:%i: set::activeusers::monitorchan::chmod: "
								"expected an octal value between 0000 and 1777",
								cepp->ce_fileptr->cf_filename,
								cepp->ce_varlinenum);
							errors++;
							continue;
						}
					}
#endif
					else
					{
						config_error("%s:%i: unknown set::activeusers::monitorchan "
							"directive %s",
							cepp->ce_fileptr->cf_filename, cepp->ce_varlinenum,
							cepp->ce_varname);
						errors++;
						continue;
					}
				}
			}
			else 
			{
				config_error("%s:%i: unknown directive set::activeusers::%s",
					cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname);
				errors++;
			}
			
		}
		*errs = errors;
		return errors ? -1 : 1;
	}
	else
		return 0;
}

static int cb_conf(ConfigFile *cf, ConfigEntry *ce, int type)
{
	ConfigEntry	*cep, *cepp;
	MonitorChan	*m;

	if (type != CONFIG_SET)
		return 0;

	if (!strcmp(ce->ce_varname, "activeusers"))
	{
		for (cep = ce->ce_entries; cep; cep = cep->ce_next)
		{
			if (!strcmp(cep->ce_varname, "update-frequency"))
				MyConf.update_frequency = config_checkval(cep->ce_vardata, CFG_TIME);
			else if (!strcmp(cep->ce_varname, "show-realhost"))
				MyConf.show_realhost = config_checkval(cep->ce_vardata, CFG_YESNO);
			else if (!strcmp(cep->ce_varname, "monitorchan"))
			{
				m = (MonitorChan *) MyMallocEx(sizeof(MonitorChan));
				m->channel = strdup(cep->ce_vardata);
#if !defined(_WIN32) && !defined(_AMIGA)
				m->perms = -1;
#endif

				for (cepp = cep->ce_entries; cepp; cepp = cepp->ce_next)
				{
					if (!strcmp(cepp->ce_varname, "db-file"))
					{
						ircstrdup(m->db_file, cepp->ce_vardata);
					}
					else if (!strcmp(cepp->ce_varname, "xml-file"))
					{
						ircstrdup(m->xml_file, cepp->ce_vardata);
					}
					else if (!strcmp(cepp->ce_varname, "xsl-file"))
					{
						ircstrdup(m->xsl_file, cepp->ce_vardata);
					}
#if !defined(_WIN32) && !defined(_AMIGA)
					else if (!strcmp(cepp->ce_varname, "chmod"))
						m->perms = strtol(cepp->ce_vardata, NULL, 8);
#endif
				}
				AddListItem(m, ChanList);
			}
		}
	}

	return 0;
}

static int cb_posttest(int *errs)
{
	int errors = 0;

	if (!ReqConf.update_frequency)
	{
		config_error("set::activeusers::update-frequency missing");
		errors++;
	}

	*errs = errors;
	return errors ? -1 : 1;	
}

static int cb_stats(aClient *sptr, char *stats)
{
	if (*stats == 'S')
	{
		sendto_one(sptr, ":%s %i %s :activeusers::update-frequency: %s",
			me.name, RPL_TEXT, sptr->name, pretty_time_val(MyConf.update_frequency));
		sendto_one(sptr, ":%s %i %s :activeusers::show-realhost: %d",
			me.name, RPL_TEXT, sptr->name, MyConf.show_realhost);
	}
        return 0;
}

/*
 * strftime on Windows doesn't support format parameters %F and %T (and
 * some other parameters as well). More details at:
 *
 * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclib/
 *        html/_crt_strftime.2c_.wcsftime.asp
 */

static char *MyDate(TS t)
{
	static char	buf[201];
	struct tm	*tp = localtime(&t);

	if (!tp)
		return NULL;

	strftime(buf, 201, "%Y-%m-%d %H:%M:%S", tp);
	return buf;
}

static void update_db_file(MonitorChan *m)
{
	FILE		*fp;
	aChannel	*chptr;
	aClient		*acptr;
	Member		*lp;

	if (!(fp = fopen(m->db_file, "w")))
    	{
		sendto_realops("[activeusers] Failed opening file %s: %s",
    			m->db_file, strerror(errno));
		return;
	}

	/*
	 * First line contains <last_mod_time> <update_freq>
	 * Every other line: <nick> <user> <host> <firsttime> <idletime> [<prefix>]
	 */
	fprintf(fp, "%ld %ld\n", (long) time(NULL), MyConf.update_frequency);

	if ((chptr = find_channel(m->channel, NullChn)) != NullChn)
	{
		for (lp = chptr->members; lp; lp = lp->next)
		{
			acptr = lp->cptr;

#ifdef PREFIX_AQ
			fprintf(fp, "%s %s %s %ld %ld %s%s%s%s%s\n",
#else
			fprintf(fp, "%s %s %s %ld %ld %s%s%s \n",
#endif
				acptr->name,
				acptr->user->username,
				MyConf.show_realhost ? acptr->user->realhost : GetHost(acptr),
				(long) acptr->firsttime,
				(long) TStime() - acptr->last,
#ifdef PREFIX_AQ
				(lp->flags & CHFL_CHANOWNER) ? "~" : "",
				(lp->flags & CHFL_CHANPROT) ? "&" : "",
#endif
				(lp->flags & CHFL_CHANOP) ? "@" : "",
				(lp->flags & CHFL_HALFOP) ? "%" : "",
				(lp->flags & CHFL_VOICE) ? "+" : ""
			);
		}
	}

	fclose(fp);

#if !defined(_WIN32) && !defined(_AMIGA)
	if (m->perms != -1)
		chmod(m->db_file, m->perms);
#endif

}

static void update_xml_file(MonitorChan *m)
{
	FILE		*fp;
	aChannel	*chptr;
	aClient		*acptr;
	Member		*lp;

	if (!(fp = fopen(m->xml_file, "w")))
    	{
		sendto_realops("[activeusers] Failed opening file %s: %s",
    			m->xml_file, strerror(errno));
		return;
	}

	fprintf(fp, "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n");

	if (m->xsl_file)
		fprintf(fp, "<?xml-stylesheet type=\"text/xsl\" href=\"%s\"?>\n",
			m->xsl_file);

	fprintf(fp, "<activeusers>\n");
	fprintf(fp, "<header>\n");
	fprintf(fp, "<lastmod>%s</lastmod>\n",
		MyDate(time(NULL)));
	fprintf(fp, "<refresh>%s</refresh>\n",
		pretty_time_val(MyConf.update_frequency));
	fprintf(fp, "</header>\n");

	if ((chptr = find_channel(m->channel, NullChn)) != NullChn)
	{
		for (lp = chptr->members; lp; lp = lp->next)
		{
			acptr = lp->cptr;

			fprintf(fp, "<users>\n");
			fprintf(fp, "<nickname>%s</nickname>\n",
				acptr->name);
			fprintf(fp, "<username>%s</username>\n",
				acptr->user->username);
			fprintf(fp, "<hostname>%s</hostname>\n",
				MyConf.show_realhost ? acptr->user->realhost :
					GetHost(acptr));
			fprintf(fp, "<connecttime>%s</connecttime>\n",
				MyDate(acptr->firsttime));
			fprintf(fp, "<idletime>%s</idletime>\n",
				pretty_time_val(TStime() - acptr->last));

#ifdef PREFIX_AQ
			fprintf(fp, "<prefix>%s%s%s%s%s</prefix>\n",
#else
			fprintf(fp, "<prefix>%s%s%s</prefix>\n",
#endif
#ifdef PREFIX_AQ
				(lp->flags & CHFL_CHANOWNER) ? "~" : "",
				(lp->flags & CHFL_CHANPROT) ? "&amp;" : "",
#endif
				(lp->flags & CHFL_CHANOP) ? "@" : "",
				(lp->flags & CHFL_HALFOP) ? "%" : "",
				(lp->flags & CHFL_VOICE) ? "+" : ""
			);

			fprintf(fp, "</users>\n");
		}
	}

	fprintf(fp, "</activeusers>\n");
	fclose(fp);

#if !defined(_WIN32) && !defined(_AMIGA)
	if (m->perms != -1)
		chmod(m->xml_file, m->perms);
#endif

}

static void event_activeusers(MonitorChan *m)
{
	/*
	 * The only reason for using separate functions
	 * is to make the code a bit more human-readable.
	 */

	if (m->db_file)
		update_db_file(m);
	if (m->xml_file)
		update_xml_file(m);
}
