/* 
	ctrlproxy: A modular IRC proxy
	(c) 2002-2003 Jelmer Vernooij <jelmer@nl.linux.org>

	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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include "ctrlproxy.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <sys/stat.h>
#include <sys/types.h>

#define MAX_SUBST 256

struct file_info {
	FILE *file;
	time_t last_used;
};

struct log_custom_data {
    char *logfilename;
    GKeyFile *kf;
};

static GHashTable *files;

/* Translation table */
struct log_mapping {
	char *command;
	char subst;
	unsigned int index;
	/* If index is -1 */
	char *(*callback) (struct network *, const struct line *l, 
					   gboolean case_sensitive);
};

static char *get_hours(struct network *n, const struct line *l, 
					   gboolean case_sensitive) 
{ 
	time_t ti = time(NULL);
	struct tm *t = localtime(&ti);
	return g_strdup_printf("%02d", t->tm_hour);
}

static char *get_minutes(struct network *n, const struct line *l, 
						 gboolean case_sensitive) 
{ 
	time_t ti = time(NULL);
	struct tm *t = localtime(&ti);
	return g_strdup_printf("%02d", t->tm_min);
}

static char *get_seconds(struct network *n, const struct line *l, 
						 gboolean case_sensitive) 
{ 
	time_t ti = time(NULL);
	struct tm *t = localtime(&ti);
	return g_strdup_printf("%02d", t->tm_sec);
}

static char *get_seconds_since_1970(struct network *n, const struct line *l, 
									gboolean case_sensitive) 
{
	time_t ti = time(NULL);
	return g_strdup_printf("%ld", ti);
}

static char *get_day(struct network *n, const struct line *l, 
					 gboolean case_sensitive) 
{ 
	time_t ti = time(NULL);
	struct tm *t = localtime(&ti);
	return g_strdup_printf("%02d", t->tm_mday);
}

static char *get_month(struct network *n, const struct line *l, 
					   gboolean case_sensitive) 
{ 
	time_t ti = time(NULL);
	struct tm *t = localtime(&ti);
	return g_strdup_printf("%02d", t->tm_mon + 1);
}

static char *get_year(struct network *n, const struct line *l, 
					  gboolean case_sensitive) 
{ 
	time_t ti = time(NULL);
	struct tm *t = localtime(&ti);
	return g_strdup_printf("%04d", t->tm_year + 1900);
}

static char *get_user(struct network *n, const struct line *l, 
					  gboolean case_sensitive) 
{
	char *nick = NULL;
	char *user = NULL;

	if (l->origin != NULL) nick = g_strdup(l->origin);
	if (nick != NULL) user = strchr(nick, '!');
	if (user != NULL) { *user = '\0';user++; }

	if (case_sensitive) return g_ascii_strdown(user, -1);
	else return g_strdup(user);
}

static char *get_monthname(struct network *n, const struct line *l, 
						   gboolean case_sensitive) 
{ 
	char stime[512];
	time_t ti = time(NULL);
	strftime(stime, sizeof(stime), "%b", localtime(&ti));
	return g_strdup_printf("%s", stime);
}

static char *get_nick(struct network *n, const struct line *l, 
					  gboolean case_sensitive) 
{
	if (l->origin != NULL) {
		char *n = line_get_nick(l);
		if (case_sensitive) {
			char *r = g_ascii_strdown(n, -1);
			g_free(n);
			return r;
		}
		else return n;
	}
	
	return g_strdup("");
}

static char *get_network(struct network *n, const struct line *l, 
						 gboolean case_sensitive) 
{
	return g_strdup(n->info.name); 
}

static char *get_server(struct network *n, const struct line *l, 
						gboolean case_sensitive)
{
	if (n->connection.data.tcp.current_server)
		return g_strdup(n->connection.data.tcp.current_server->host);
	return g_strdup("");
}

static char *get_percent(struct network *n, const struct line *l, 
						 gboolean case_sensitive) 
{ 
	return g_strdup("%"); 
}

static const char *identifier = NULL;

static char *get_identifier(struct network *n, const struct line *l, 
							gboolean case_sensitive) 
{ 
	if (case_sensitive) return g_ascii_strdown(identifier, -1); 
	else return g_strdup(identifier); 
}

static char *get_modechanges(struct network *n, const struct line *l, 
							 gboolean case_sensitive) 
{
	char buf[512] = "";
	int i;

	for (i = 3 ; l->args[i+1] != NULL; i++)
		if (i == 3) sprintf(buf, "%s", l->args[i]);
		else sprintf(buf, "%s %s", buf, l->args[i]);

	return g_strdup(buf);
}

static struct log_mapping mappings[] = {
	{NULL, '@', -1, get_identifier },
	{NULL, 'h', -1, get_hours },
	{NULL, 'M', -1, get_minutes },
	{NULL, 's', -1, get_seconds },
	{NULL, 'd', -1, get_day },
	{NULL, 'B', -1, get_month },
	{NULL, 'Y', -1, get_year },
	{NULL, 'e', -1, get_seconds_since_1970 },
	{NULL, 'b', -1, get_monthname },
	{NULL, 'n', -1, get_nick },
	{NULL, 'u', -1, get_user },
	{NULL, 'N', -1, get_network },
	{NULL, 'S', -1, get_server },
	{NULL, '%', -1, get_percent },
	{"JOIN", 'c', 1, NULL },
	{"PART", 'c', 1, NULL },
	{"PART", 'm', 2, NULL },
	{"KICK", 'c', 1, NULL },
	{"KICK", 't', 2, NULL },
	{"KICK", 'r', 3, NULL },
	{"QUIT", 'm', 1, NULL },
	{"NOTICE", 't', 1, NULL },
	{"NOTICE", 'm', 2, NULL },
	{"PRIVMSG", 't', 1, NULL },
	{"PRIVMSG", 'm', 2, NULL },
	{"MSG", 't', 1, NULL },
	{"MSG", 'm', 2, NULL },
	{"TOPIC", 'c', 1, NULL },
	{"TOPIC", 't', 2, NULL },
	{"MODE", 't', 1, NULL },
	{"MODE", 'p', 2, NULL },
	{"MODE", 'c', -1, get_modechanges },
	{"NICK", 'r', 1, NULL },
	{NULL, '0', 0, NULL },
	{NULL, '1', 1, NULL },
	{NULL, '2', 2, NULL },
	{NULL, '3', 3, NULL },
	{NULL, '4', 4, NULL },
	{NULL, '5', 5, NULL },
	{NULL, '6', 6, NULL },
	{NULL, '7', 7, NULL },
	{NULL, '8', 8, NULL },
	{NULL, '9', 9, NULL },
	{ NULL }
};

static char *find_mapping(struct network *network, const struct line *l, char c, gboolean case_sensitive)
{
	int i;
	for (i = 0; mappings[i].subst; i++) {
		if (mappings[i].command && 
		   strcmp(mappings[i].command, l->args[0])) continue;
		if (mappings[i].subst != c) continue;
		if (mappings[i].index == -1) return mappings[i].callback(network, l, case_sensitive);
		if (mappings[i].index < l->argc) {
			if (case_sensitive) return g_ascii_strdown(l->args[mappings[i].index], -1);
			else return g_strdup(l->args[mappings[i].index]);
		}
	}
	return g_strdup("");
}

static void convertslashes(char *a)
{
	int j;
	for (j = 0; a[j]; j++) 
		if (a[j] == '/') a[j] = '_';
}

static void custom_subst(struct network *network, char **_new, 
						 const char *fmt, const struct line *l, 
						 const char *_identifier, 
						 gboolean case_sensitive, gboolean noslash)
{
	char *subst[MAX_SUBST];
	char *new;
	size_t len, curpos = 0;
	unsigned int i;

	identifier = _identifier;

	/* First, find all the mappings */
	len = strlen(fmt);
	memset(subst, 0, sizeof(char *) * MAX_SUBST);
	for (i = 0; i < strlen(fmt); i++) {
		if (fmt[i] == '%') {
			subst[(int)fmt[i+1]] = find_mapping(network, l, fmt[i+1], case_sensitive);	
			if (subst[(int)fmt[i+1]] == NULL) subst[(int)fmt[i+1]] = g_strdup("");
			if (noslash) convertslashes(subst[(int)fmt[i+1]]);
			len += strlen(subst[(int)fmt[i+1]]);
		}
	}

	len++; /* newline! */

	new = g_new(char, len);
	for (i = 0; i < strlen(fmt); i++) {
		if (fmt[i] == '%') {
			new[curpos] = '\0';
			strncat(new, subst[(int)fmt[i+1]], len);
			curpos+=strlen(subst[(int)fmt[i+1]]);
			i++;
		} else {
			new[curpos] = fmt[i];
			curpos++;
		}
	}
	new[curpos] = '\0';

	for (i = 0; i < MAX_SUBST; i++) { 
		if (subst[i]) 
			g_free(subst[i]); 
	}
	*_new = new;
}

/* Syntax:
Always:
 * %@ -> identifier
 * %h -> hours
 * %M -> minutes
 * %s -> seconds
 * %d -> day
 * %B -> month
 * %Y -> year
 * %e -> seconds since 1970
 * %b -> locale month name
 * %n -> initiating nick
 * %u -> initiating user
 * %N -> network name
 * %S -> server name
 * %% -> percent sign
If appropriate:
 * %c -> channel name
 * %m -> message

 -- JOIN: %c
 -- PART: %c, %m
 -- KICK: %t (target nick), %r (reason)
 -- QUIT: %m
 -- NOTICE/PRIVMSG: %t (target nick/channel), %m
 -- MODE: %p(mode change), %t, %c (target nicks)
 -- TOPIC: %t
 -- NICK: %r
 */

static FILE *find_add_channel_file(struct log_custom_data *data, 
								   struct network *network, 
								   const struct line *l, 
								   const char *identifier, 
								   gboolean create_file) 
{
	char *n = NULL, *dn, *p;
	struct file_info *fi;
	if (data->logfilename == NULL) 
		return NULL;

	custom_subst(network, &n, data->logfilename, l, identifier, TRUE, TRUE);
	fi = g_hash_table_lookup(files, n);

	if (fi == NULL && create_file) {
		dn = g_strdup(n);
		
		/* Only include directory-part */
		p = strrchr(dn, '/');
		if (p != NULL) 
			*p = '\0';

		/* Check if directory needs to be created */
		if (!g_file_test(dn, G_FILE_TEST_IS_DIR) && g_mkdir(dn, 0700) == -1) {
			log_network(LOG_ERROR, network, "Couldn't create directory %s for logging!", dn);
			g_free(dn);
			g_free(n);
			return NULL;
		}
		g_free(dn);

		fi = g_new0(struct file_info, 1);
		
		/* Then open the correct filename */
		fi->file = fopen(n, "a+");
		if (fi->file == NULL) {
			log_network(LOG_ERROR, network, "Couldn't open file %s for logging!", n);
			g_free(n);
			g_free(fi);
			return NULL;
		}
		g_hash_table_insert(files, n, fi);
	} else g_free(n);

	if (fi == NULL)
		return NULL;

	fi->last_used = time(NULL);
	return fi->file;
}

static void file_write_line(struct log_custom_data *data, struct network *network, const char *fmt,
				   const struct line *l, const char *t, gboolean create_file)
{
	FILE *f;
	char *s;

	f = find_add_channel_file(data, network, l, t, create_file);
	if (f == NULL) return;

	custom_subst(network, &s, fmt, l, t, FALSE, FALSE);

	fputs(s, f);
	fputc('\n', f);
	fflush(f);

	g_free(s);
}

static void file_write_line_target(struct log_custom_data *data, struct network *network, const char *fmt,
				   const struct line *l, const char *t, gboolean create_file)
{
	if (strchr(t, ',') != NULL) {
		char **channels = g_strsplit(t, ",", 0);
		int i;

		for (i = 0; channels[i]; i++) {
			file_write_line(data, network, fmt, l, channels[i], TRUE);
		}
		g_strfreev(channels);
	} else
		file_write_line(data, network, fmt, l, t, TRUE);
}

static void file_write_target(struct log_custom_data *data, 
							  struct network *network, const char *n, 
							  const struct line *l) 
{
	char *t, *fmt;
	
	fmt = g_key_file_get_string(data->kf, "log-custom", n, NULL);
	if (fmt == NULL) 
		return;
	
	g_assert(l->args[0]);
	g_assert(l->args[1]);
	g_assert(network->state);
	g_assert(network->state->me.nick);

	if (!irccmp(&network->state->info, network->state->me.nick, l->args[1])) {
		if (l->origin != NULL)
			t = line_get_nick(l);
		else 
			t = g_strdup("_messages_");
		file_write_line(data, network, fmt, l, t, TRUE);
		g_free(t);
	} else
		file_write_line_target(data, network, fmt, l, l->args[1], TRUE);
}

static void file_write_channel_only(struct log_custom_data *data, 
									struct network *network, const char *n, 
									const struct line *l)
{
	char *fmt;

	fmt = g_key_file_get_string(data->kf, "log-custom", n, NULL);
	if (fmt == NULL) return;

	file_write_line_target(data, network, fmt, l, l->args[1], TRUE);
}

static void file_write_channel_query(struct log_custom_data *data, 
									 struct network *network, const char *n, 
									 const struct line *l)
{
	char *fmt;
	char *nick;
	GList *gl;
	struct network_nick *nn;

	if (l->origin == NULL) 
		return;

	g_assert(n);

	fmt = g_key_file_get_string(data->kf, "log-custom", n, NULL);
	if (fmt == NULL) 
		return;

	/* check for the query first */
	nick = line_get_nick(l);
	file_write_line(data, network, fmt, l, nick, FALSE);
	
	nn = find_network_nick(network->state, nick);
	g_free(nick);
	g_assert(nn);

	/* now, loop thru the users' channels */
	for (gl = nn->channel_nicks; gl; gl = gl->next) {
		struct channel_nick *cn = gl->data;
		file_write_line(data, network, fmt, l, cn->channel->name, TRUE);
	}
}

static gboolean log_custom_data(struct network *network, const struct line *l, 
								enum data_direction dir, void *userdata)
{
    struct log_custom_data *data = userdata;
	char *nick = NULL;
	if (l->args == NULL || l->args[0] == NULL)
		return TRUE;

	if (l->origin != NULL) 
		nick = line_get_nick(l);

	/* Loop thru possible values for %@ */

	/* There are a few possibilities:
	 * - log to line_get_nick(l) or l->args[1] file depending on which 
	 *   was the current user (PRIVMSG, NOTICE, ACTION, MODE) (target)
	 * - log to all channels line_get_nick(l) was on, and to query, if applicable (NICK, QUIT) (channel_query)
	 * - log to channel only (KICK, PART, JOIN, TOPIC) (channel_only)
	 */

	if (dir == FROM_SERVER && !g_strcasecmp(l->args[0], "JOIN")) {
		file_write_target(data, network, "join", l); 
	} else if (dir == FROM_SERVER && !g_strcasecmp(l->args[0], "PART")) {
		file_write_channel_only(data, network, "part", l);
	} else if (!g_strcasecmp(l->args[0], "PRIVMSG")) {
		if (l->args[2][0] == '\001') { 
			l->args[2][strlen(l->args[2])-1] = '\0';
			if(!g_ascii_strncasecmp(l->args[2], "\001ACTION ", 8)) { 
				l->args[2]+=8;
				file_write_target(data, network, "action", l);
				l->args[2]-=8;
			}
			l->args[2][strlen(l->args[2])] = '\001';
			/* Ignore all other ctcp messages */
		} else {
			file_write_target(data, network, "msg", l);
		}
	} else if (!g_strcasecmp(l->args[0], "NOTICE")) {
		file_write_target(data, network, "notice", l);
	} else if (!g_strcasecmp(l->args[0], "MODE") && l->args[1] != NULL && 
			  is_channelname(l->args[1], &network->state->info) && 
			  dir == FROM_SERVER) {
		file_write_target(data, network, "mode", l);
	} else if (!g_strcasecmp(l->args[0], "QUIT")) {
		file_write_channel_query(data, network, "quit", l);
	} else if (!g_strcasecmp(l->args[0], "KICK") && l->args[1] && l->args[2] && dir == FROM_SERVER) {
		if (strchr(l->args[1], ',') == NULL) {
			file_write_channel_only(data, network, "kick", l);
		} else { 
			char *channels = g_strdup(l->args[1]);
			char *nicks = g_strdup(l->args[1]);
			char *p,*n; char cont = 1;
			char *_nick;

			p = channels;
			_nick = nicks;
			while (cont) {
				n = strchr(p, ',');

				if(!n) cont = 0;
				else *n = '\0';

				file_write_channel_only(data, network, "kick", l);

				p = n+1;
				_nick = strchr(_nick, ',');
				if(!_nick)break;
				_nick++;
			}
			
			g_free(channels);
			g_free(nicks);
		}
	} else if (!g_strcasecmp(l->args[0], "TOPIC") && dir == FROM_SERVER && l->args[1]) {
		if (l->args[2] != NULL) 
			file_write_channel_only(data, network, "topic", l);
		else 
			file_write_channel_only(data, network, "notopic", l);
	} else if (!g_strcasecmp(l->args[0], "NICK") && dir == FROM_SERVER && 
			   l->args[1] != NULL) {
		file_write_channel_query(data, network, "nickchange", l);
	}

	g_free(nick);

	return TRUE;
}

static void free_file_info(void *_data)
{
	struct file_info *data = _data;

	fclose(data->file);
	g_free(data);
}

static void load_config(struct global *global)
{
	GKeyFile *kf = global->config->keyfile;
	struct log_custom_data *data;

	if (!g_key_file_has_group(kf, "log-custom")) {
		del_log_filter("log_custom");
		return;
	}

	data = g_new0(struct log_custom_data, 1);

	add_log_filter("log_custom", log_custom_data, data, 1000);

	data->logfilename = g_key_file_get_string(kf, "log-custom", "logfilename", NULL);
	data->kf = kf;
}

#define CLEANUP_THRESHOLD (60 * 60 * 24)

static gboolean eval_remove(gpointer key, gpointer value, gpointer user_data)
{
	struct file_info *fi = value;

	return (fi->last_used < time(NULL) - CLEANUP_THRESHOLD);
}

static gboolean cleanup(void *_data)
{
	g_hash_table_foreach_remove(files, eval_remove, NULL);
	return TRUE;
}

static gboolean init_plugin(void)
{
	files = g_hash_table_new_full(g_str_hash, g_str_equal, 
			g_free, free_file_info);
	g_timeout_add(60 * 60, cleanup, NULL);
	register_load_config_notify(load_config);
	return TRUE;
}

struct plugin_ops plugin = {
	.name = "log_custom",
	.version = 0,
	.init = init_plugin,
};


syntax highlighted by Code2HTML, v. 0.9.1