/*
 * -------------------------------------------------------
 * Copyright (C) 2003-2007 Tommi Saviranta <wnd@iki.fi>
 *      (C) 2002 Lee Hardy <lee@leeh.co.uk>
 *      (C) 1998-2002 Sebastian Kienzl <zap@riot.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.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* ifdef HAVE_CONFIG_H */

#include "server.h"
#include "client.h"
#include "llist.h"
#include "messages.h"
#include "irc.h"
#include "miau.h"
#include "error.h"
#include "tools.h"
#include "perm.h"
#include "ignore.h"
#include "commands.h"
#include "qlog.h"
#include "chanlog.h"
#include "privlog.h"
#include "log.h"
#include "onconnect.h"
#include "automode.h"
#include "remote.h"
#include "etc.h"
#include "common.h"
#include "dcc.h"

#include <string.h>
#include <sys/time.h>
#include <time.h>

#if HAVE_CRYPT_H
#include <crypt.h>
#endif /* ifdef HAVE_CRYPT_H */



serverlist_type	servers;
server_info	i_server;
connection_type	c_server;



/*
 * Drop connection to the server.
 *
 * Give client (and server) a reason, which may be set to NULL.
 */
void
server_drop(char *reason)
{
	llist_node	*node;
	llist_node	*nextnode;
	const char	*part_reason;

	if (reason == NULL) {
		part_reason = SERV_DISCONNECT;
	} else {
		part_reason = reason;
	}

	/* As we're no longer connected to server, send queue is useless. */
	irc_clear_queue();
	
	if (c_server.socket) {
		char buf[IRC_MSGLEN];
		int r;

		snprintf(buf, IRC_MSGLEN, "QUIT :%s\r\n", reason);
		buf[IRC_MSGLEN - 1] = '\0';
		r = sock_setblock(c_server.socket);
		irc_write_real(&c_server, buf);
#ifdef CHANLOG
		chanlog_write_entry_all(LOG_QUIT, LOGM_QUIT,
				get_short_localtime(), status.nickname, 
				reason, "", "");
#endif /* ifdef CHANLOG */
		sock_close(&c_server);
	}
	i_server.connected = 0;

	/*
	 * Walk active_channels and either remove channels or move to
	 * passive_channels, depending on cfg.rejoin.
	 */
	node = active_channels.head;
	while (node != NULL) {
		channel_type *data;
		nextnode = node->next;
		data = (channel_type *) node->data;

#ifdef AUTOMODE
		/* We no longer know if we're an operator or not. */
		data->oper = -1;
#endif /* ifdef AUTOMODE */
		/*
		 * We don't need to reset channel topic, it will be erased/reset
		 * when joining the channel.
		 */

		if (cfg.chandiscon == 1) {
			irc_mwrite(&c_clients, ":%s NOTICE %s :%s",
					status.nickname,
					(char *) data->name,
					part_reason);
		} else if (cfg.chandiscon == 2) {
			/*
			 * RFC 2812 says "Servers MUST be able to parse
			 * arguments in the form of a list of target, but
			 * SHOULD NOT use lists when sending PART messages to
			 * clients." and therefore we don't part all the
			 * channels with one command.
			 */
			irc_mwrite(&c_clients, ":%s!%s@%s PART %s :%s",
					status.nickname,
					i_client.username,
					i_client.hostname,
					(char *) data->name,
					part_reason);
		}

		if (cfg.rejoin == 1) {
			char *simple;
			llist_node *node;
			/*
			 * Need to move channel from active_channels to
			 * passive_channels. Also revert real channel name
			 * (which we know sure sure) back to simple form.
			 * There's no need to reset simple name -- it won't
			 * change.
			 */
			simple = channel_simplify_name(data->name);
			xfree(data->name);
			data->name = simple;
			data->name_set = 0;

			node = llist_create(data);
			llist_add_tail(node, &passive_channels);
		} else {
			/*
			 * * Not moving channels from list to list, therefore
			 * freeing resources.
			 */
			channel_free(data);
		}
		/* Remove channel node from old list. */
		llist_delete(node, &active_channels);

		node = nextnode;
	}

	if (cfg.chandiscon == 3) {
		irc_mwrite(&c_clients, ":miau PRIVMSG %s: %s",
				status.nickname,
				part_reason);
	}

	/* Reset server-name. */
	xfree(i_server.realname);
	i_server.realname = xstrdup("miau");

	/* Don't try connecting next server right away. */
	timers.connect = 0;

#ifdef UPTIME
	status.startup = 0;	/* Reset uptime-counter. */
#endif /* ifdef UPTIME */
} /* void server_drop(char *reason) */



/*
 * Set "fallback" or "failsafe" server.
 *
 * This server is needed when no server have been defined and we need some
 * real information about the server we're connected to.
 */
void
server_set_fallback(const llist_node *safenode)
{
	server_type	*fallback = (server_type *) servers.servers.head->data;
	server_type	*safe = (server_type *) safenode->data;
	/* Free old data. */

	/*
	 * If we try to replace failsafe-server with failsafe-server, data
	 * will be freed before it can be copied. This is why we won't do
	 * that.
	 */
	if (fallback == safe) {
		return;
	}

	xfree(fallback->name);
	xfree(fallback->password);

	/* Copy current values to fallback server. */
	fallback->name = xstrdup(safe->name);
	fallback->port = safe->port;
	fallback->password = (safe->password != NULL) ? 
		xstrdup(safe->password) : NULL;
	fallback->working = 1;
	fallback->timeout = safe->timeout;
} /* void server_set_fallback(const llist_node *safenode) */



/*
 * Resets all servers to 'working'.
 */
void
server_reset(void)
{
	llist_node	*node;

	for (node = servers.servers.head->next; node != NULL;
			node = node->next) {
		((server_type *) node->data)->working = 1;
	}
} /* void server_reset(void) */



/*
 * Change server.
 *
 * If next is set and there are no more servers to connect, miau will either
 * reset all servers to working or quit, depending on the configuration.
 *
 * If disable != 0, old server will be marked as disfunctional.
 */
void
server_change(int next, int disable)
{
	llist_node *i;

	if (status.good_server == 1) {
		status.good_server = 0;
		return;
	}

	i_server.connected = 0;

	if (servers.amount == 1) {
		server_check_list();

		return;
	}

	if (status.reconnectdelay == CONN_DISABLED) {
		return;
	}

	i = i_server.current;

	if (servers.fresh == 1) {
		/*
		 * We don't know which server of those new ones we are on, so
		 * let's use the fallback one. It's the server we're on anyway.
		 */
		i_server.current = servers.servers.head;
		i = servers.servers.head;
	}

	if (disable == 1 && i != servers.servers.head) {
		((server_type *) i_server.current->data)->working = 0;
	}

	if (next == 1) {
		do {
			i_server.current = i_server.current->next;
			if (i_server.current == NULL) {
				i_server.current = servers.servers.head->next;
			}
		} while (! ((server_type *) i_server.current->data)->working &&
				i_server.current != i);
	}
	
	if (((server_type *) i_server.current->data)->working == 0) {
		if (cfg.nevergiveup == 1) {
			report(MIAU_OUTOFSERVERSNEVER);
			server_reset();
			i_server.current = servers.servers.head->next;
		} else {
			error(MIAU_OUTOFSERVERS);
			exit(EXIT_SUCCESS);
		}
	}

	if (next == 0) {
		report(SOCK_RECONNECTNOW,
				((server_type *) i_server.current->data)->name);
		timers.connect = cfg.reconnectdelay - 1;
		if (timers.connect < 0) {
			timers.connect = 0;
		}
	} else if (i == i_server.current) {
		report(SOCK_RECONNECT,
				((server_type *) i_server.current->data)->name,
				cfg.reconnectdelay);
		timers.connect = 0;
	}
} /* void server_change(int next, int disable) */



void
server_commands(char *command, char *param, int *pass)
{
	upcase(command);

	if (xstrcmp(command, "PING") == 0) {
		*pass = 0;
		if (param != NULL && *param == ':') param++;
		/* Don't make this global (see ERROR) */
		if (param) {
			/*
			 * Although there should be no need to PONG the server
			 * if there is stuff in queue (server shouldn't need
			 * to ping client if it's sending data), we priorize
			 * PONGs over everything else - just in case.
			 */
			irc_write_head(&c_server, "PONG :%s", param);
		}
	}

	else if (xstrcmp(command, "ERROR") == 0) {
		*pass = 0;

		server_drop((param == NULL) ? SERV_ERR : param);
		irc_mwrite(&c_clients, MIAU_CLOSINGLINK,
				(param == NULL) ? SERV_ERR : param);
		drop_newclient(NULL);
		error(IRC_SERVERERROR, (param == NULL) ? "unknown" : param);

		server_change(1, i_server.connected == 0);
	}
} /* void server_commands(char *command, char *param, int *pass) */



static void
parse_msg_me_ctcp(const char *origin, const char *nick, const char *hostname,
		const char *param1, const char *param2, int cmdindex, int *pass)
{
#ifdef DCCBOUNCE
	if (c_clients.connected > 0
			&& cmdindex == CMD_PRIVMSG
			&& cfg.dccbounce
			&& (xstrcasecmp(param2 + 2, "DCC\1") == 0)) {
		char dcct[IRC_MSGLEN];
		strncpy(dcct, param2 + 1, IRC_MSGLEN);
		if (dcc_initiate(dcct, IRC_MSGLEN, 0)) {
			irc_mwrite(&c_clients, ":%s PRIVMSG %s :%s",
					origin, param1, dcct);
			*pass = 0;
		}
	}
#endif /* ifdef DCCBOUNCE */
#ifdef DCCBOUNCE
#ifdef CTCTPREPLIES
	else
#endif /* ifdef CTCPREPLIES */
#endif /* ifdef DCCBOUNCE */
#ifdef CTCPREPLIES
	if (! is_ignore(hostname, IGNORE_CTCP)
			&& c_clients.connected == 0
			&& status.allowreply == 1) {
		report(CLNT_CTCP, param2 + 1, origin);
		
		if (xstrcmp(param2 + 2, "VERSION\1") == 0) {
			irc_notice(&c_server, nick, VERSIONREPLY);
		}
		
		else if (xstrcmp(param2 + 2, "PING") == 0) {
			if (strlen(param2 + 1) > 6) {
				irc_notice(&c_server, nick, "%s", param2 + 1);
			}
		}
		
		else if (xstrcmp(param2 + 2, "CLIENTINFO\1") == 0) {
			irc_notice(&c_server, nick, CLIENTINFOREPLY);
		}

		ignore_add(hostname, 6, IGNORE_CTCP);
		status.allowreply = 0;
		timers.reply = 0;
	} /* CTCP-replies */
	else if (is_ignore(hostname, IGNORE_CTCP)
			|| status.allowreply == 0) {
		report(CLNT_CTCPNOREPLY, param2 + 1, origin);
	}
#endif /* ifdef CTCPREPLIES */
} /* static void parse_msg_me_ctcp(const char *origin, const char *nick,
		const char *hostname, const char *param1, const char *param2,
		int cmdindex, int *pass) */



static int
parse_msg_me(const char *origin, const char *nick, const char *hostname,
		const char *param1, const char *param2, int cmdindex, int *pass)
{
	if (is_perm(&ignorelist, origin)) {
		return 0;
	}

#ifdef PRIVLOG
	/* Should we log? */
	if ((c_clients.connected == 0 && (cfg.privlog & 0x01))
			|| (c_clients.connected > 0 && (cfg.privlog & 0x02))) {
		privlog_write(nick, PRIVLOG_IN, cmdindex, param2 + 1);
	}	
#endif /* ifdef PRIVLOG */

	/* Is this a special (CTCP/DCC) -message ? */
	if (param2[1] == '\1') {
		parse_msg_me_ctcp(origin, nick, hostname, param1, param2,
				cmdindex, pass);
		return 0;
	}
#ifdef NEED_CMDPASSWD
	/* Remote command for bouncer. */
	else if (cfg.cmdpasswd != NULL && param2 != NULL
			&& param2[0] != '\0' && param2[1] == ':') {
		int passok = 0;
		int passlen;
		char *lparam;
		lparam = xstrdup(param2 + 2);
		
		passlen = pos(lparam, ' ');
		if (passlen != -1) {
			lparam[passlen] = '\0';

			if (strlen(cfg.cmdpasswd) == 13) {
				/* Assume it's crypted */
				if (xstrcmp(crypt(lparam, cfg.cmdpasswd),
							cfg.cmdpasswd) == 0) {
					passok = 1;
				}
			} else if (xstrcmp(lparam, cfg.cmdpasswd) == 0) {
				passok = 1;
			}
			lparam[passlen] = ' ';
		}

		if (passok) {
			char *t;
			char *command;
			char *params = NULL;

			t = strtok(lparam, " ");
			command = strtok(NULL, " ");
			if (command != NULL) {
				params = strchr(command, '\0') + 1;
			}
			if (params != NULL) {
				upcase(command);
				*pass = remote_cmd(command, params, nick);
				xfree(lparam);
				return 0;
			}
		}
		xfree(lparam);
	}
#endif /* ifdef NEED_CMDPASSWD */

	/* Normal PRIVMSG/NOTICE to client. */
#ifdef INBOX
#ifndef QUICKLOG
	/*
	 * Note that we do inbox here only is privmsglog is enabled and
	 * quicklogging is disabled.
	 */
	if (inbox != NULL) {
		/* termination + validity guaranteed */
		fprintf(inbox, "%s <%s> %s\n",
				get_short_localtime(), origin, param2 + 1);
		fflush(inbox);
	}
#endif /* ifdef QUICKLOG */
#endif /* ifdef INBOX */

	if (cfg.forwardmsg) {
		int pos;

		timers.forward = 0;
		if (forwardmsg == NULL) {
			/* initial size */
			/* need space for terminator */
			forwardmsgsize = 1;
		}
		pos = forwardmsgsize - 1;
		forwardmsgsize += strlen(origin) + strlen(param2 + 1) + 4;
		/* strlen("() \n") == 4 */
		forwardmsg = (char *) xrealloc(forwardmsg, forwardmsgsize);
		/* paranoid! */
		snprintf(forwardmsg + pos, forwardmsgsize - pos,
				"(%s) %s\n",
				origin, param2 + 1);
		forwardmsg[forwardmsgsize - 1] = '\0';
	}

	return 1;
} /* static int parse_msg_me(const char *origin, const char *nick,
		const char *hostname, const char *param1, const char *param2,
		int cmdindex, int *pass) */



static int
parse_msg_chan(const char *origin, const char *nick, const char *hostname,
		const char *param1, const char *param2, int cmdindex, int *pass)
{
#ifdef CHANLOG
	channel_type	*chptr;
#endif /* ifdef CHANLOG */
	const char *chan;

	/* channel wallops - notice @#channel etc :-) */
	if ((param1[0] == '@' || param1[0] == '%' || param1[0] == '+')
			&& channel_is_name(param1 + 1) != 0) {
		chan = param1 + 1;
	} else {
		chan = param1;
	}

#ifdef CHANLOG
	/*
	 * evil kludge: it's way too easy to confuse normal message to
	 * channel "++foo" with a channel wallop (mode + to channel
	 * "+foo"), so we have to try both. At least we know to try
	 * the more obvious first.
	 */
	chptr = channel_find(chan, LIST_ACTIVE);
	if (chptr == NULL) {
		chptr = channel_find(param1, LIST_ACTIVE);
	}
	if (chptr != NULL && chanlog_has_log(chptr, LOG_MESSAGE)) {
		char *t;

		t = log_prepare_entry(nick, param2 + 1);
		if (t == NULL) {
			if (cmdindex == CMD_PRIVMSG + MINCOMMANDVALUE) {
				chanlog_write_entry(chptr, LOGM_MESSAGE,
						get_short_localtime(),
						nick, param2 + 1);
			} else { /* must be notice then */
				chanlog_write_entry(chptr, LOGM_NOTICE,
						get_short_localtime(),
						nick, param2 + 1);
			}
		} else {
			chanlog_write_entry(chptr, "%s", t);
		}
	}
#endif /* ifdef CHANLOG */

	return 0;
} /*  static int parse_msg_chan(const char *origin, const char *nick,
		const char *hostname, const char *param1, const char *param2,
		int cmdindex, int *pass) */



static int
parse_privmsg(char *param1, char *param2, char *nick, char *hostname,
		const int cmdindex, int *pass)
{
	char *origin;
	int osize;
	int isprivmsg = 0;

	if (nick == NULL || hostname == NULL || param2 == NULL) {
#ifdef ENDUSERDEBUG
		enduserdebug("parse_privmsg(): "
				"param1 = %s, param2 = %s, "
				"nick = %s, hostname = %d",
				param1 == NULL ? "NULL" : param1,
				param2 == NULL ? "NULL" : param2,
				nick == NULL ? "NULL" : nick,
				hostname == NULL ? "NULL" : hostname);
#endif /* ifdef ENDUSERDEBUG */
		return 0;
	}

	/* paranoid */
	osize = strlen(nick) + strlen(hostname) + 2;
	origin = xmalloc(osize);
	snprintf(origin, osize, "%s!%s", nick, hostname);
	origin[osize - 1] = '\0';

	/* who is it for? */
	if (xstrcasecmp(param1, status.nickname) == 0) {
		parse_msg_me(origin, nick, hostname, param1, param2,
				cmdindex, pass);
		isprivmsg = 1;
	} else {
		parse_msg_chan(origin, nick, hostname, param1, param2,
				cmdindex, pass);
	}

	xfree(origin);

	return isprivmsg;
} /* static int parse_privmsg(char *param1, char *param2, char *nick,
	char *hostname, const int cmdindex, int *pass) */



int
server_read(void)
{
	char	*backup = NULL;
	char	*origin, *command, *param1, *param2;
	int	rstate;
	int	pass = 0;
	int	commandno;

	rstate = irc_read(&c_server);

	if (rstate <= 0) {
		return rstate;
	}

	if (c_server.buffer[0] == '\0') {
		return 0;
	}
	
	/* new data... go for it ! */
	pass = 1;

	backup = xstrdup(c_server.buffer);
		
	if (c_server.buffer[0] == ':') {
		/* reply */
		origin = strtok(c_server.buffer + 1, " ");
		command = strtok(NULL, " ");
		param1 = strtok(NULL, " ");
		param2 = strtok(NULL, "\0");
#ifdef DEBUG
#ifdef OBSOLETE
		printf("[%s] [%s] [%s] [%s]\n", origin, command,
				param1, param2);
#endif /* ifdef OBSOLETE */
#endif /* ifdef DEBUG */
		if (command != 0) {
			commandno = atoi(command);
			if (commandno == 0) {
				commandno = MINCOMMANDVALUE +
					command_find(command);
			}
			server_reply(commandno, backup, origin,
					param1, param2, &pass);
		}
	}
			
	else {
		/* Command */
		command = strtok(c_server.buffer, " ");
		param1 = strtok(NULL, "\0");
				
		if (command) {
			server_commands(command, param1, &pass);
		}
	}

	/* We wouldn't need to check c_clients.connected... */
	if (c_clients.connected > 0 && pass) {
		/*
		 * Having '"%s", buffer' instead of plain
		 * 'buffer' is essential because we don't want
		 * our string processed any further by va.
		 */
		irc_mwrite(&c_clients, "%s", backup);
	}

	xfree(backup);
	
	return 0;
} /* int server_read(void) */




/*
 * Check number of servers and consistency of i_server.currect.
 * 
 * If there are only fallback-server (or no servers at all !?) left,
 * warn the user about this. Also, if the server we're connected to is on the
 * list, set i_server.current to index of it.
 */
void
server_check_list(void)
{
	llist_node	*ptr;
	
	if (servers.amount <= 1) {
		/* There are no other servers ! */
		/* This is important ! */
		irc_mwrite(&c_clients, ":miau NOTICE %s :%s", 
				status.nickname,
				CLNT_NOSERVERS);
		/* Don't try to reconnect. */
		status.reconnectdelay = CONN_DISABLED;
		return;
	}

	/* Next try to connect due in cfg.reconndelay seconds. */
	status.reconnectdelay = cfg.reconnectdelay;

	/* See if our server is on the list. */
	for (ptr = servers.servers.head->next; ptr != NULL; ptr = ptr->next) {
		/* We'll just forget the passwords. Right ? */
		if (xstrcmp(((server_type *) i_server.current->data)->name,
					((server_type *) ptr->data)->name) == 0
				&& ((server_type *)
					i_server.current->data)->port ==
				((server_type *) ptr->data)->port) {
			i_server.current = ptr;
			servers.fresh = 0;
		}
	}
} /* void server_check_list(void) */



void
server_reply(const int command, char *original, char *origin, char *param1,
		char *param2, int *pass)
{
	channel_type	*chptr;
	char		*work = NULL;
	char		*nick, *hostname;
	char		*t;
	int		isprivmsg = 0;
	int		n;
	int		newserv_disconn = 0;

	t = strchr(origin, '!');
	if (t != NULL) {
		*t = '\0';
		t++;
		nick = xstrdup(origin);
		hostname = xstrdup(t);
	} else {
		nick = xstrdup(origin);
		hostname = xstrdup(origin);
	}

	switch (command) {
		/* Replies. */
	
		/* Just signed in to server. */
		case RPL_WELCOME:
			i_server.connected++;

			xfree(i_server.realname);
			i_server.realname = xstrdup(origin);

			xfree(status.nickname);
			status.nickname = xstrdup(param1);

			xfree(i_server.greeting[0]);
			i_server.greeting[0] = xstrdup(param2);
			n = lastpos(i_server.greeting[0], ' ');
			if (n != -1) {
				i_server.greeting[0][n] = '\0';
			}

			xfree(status.idhostname);
			t = strchr(param2, '!');
			if (t != NULL) {
				status.idhostname = xstrdup(t + 1);
				status.goodhostname =
					pos(status.idhostname, '@') + 1;
			} else {
				/*
				 * While not giving hostname is ok with RFC,
				 * clients like Chatzilla expect to get it.
				 * Thanks to James Ross and Oliver Eikemeier
				 * for pointing this out.
				 *
			 * http://bugzilla.mozilla.org/show_bug.cgi?id=242095
				 */
				status.idhostname = xstrdup("miau@miau");
				status.goodhostname = 5;
			}

#ifdef UPTIME
			if (! status.startup) {
				time(&status.startup);
			}
#endif /* ifdef UPTIME */

			report(IRC_CONNECTED, i_server.realname);

#ifdef ONCONNECT
			onconnect_do();
#endif /* ifdef ONCONNECT */

			/* Set user modes if any and if no clients connected. */
			if (cfg.usermode != NULL && c_clients.connected == 0) {
				irc_write(&c_server, "MODE %s %s",
						status.nickname,
						cfg.usermode);
			}
			/* 
			 * Be default we're not away, but set_away() may
			 * change this.
			 */
			status.awaystate &= ~AWAY;
			set_away(NULL);	/* No special message. */

			/* See if we should join channels. */
			timers.join = JOINTRYINTERVAL;
			/* Also reset channel's join-count. */
			LLIST_WALK_H(passive_channels.head, channel_type *);
				data->jointries = JOINTRIES_UNSET;
			LLIST_WALK_F;

			for (n = 0; n < RPL_ISUPPORT_LEN; n++) {
				FREE(i_server.isupport[n]);
			}
			newserv_disconn = NEWSERV_DISCONN_ALWAYS;

			break;

		/* More registeration-time replies... */
		case RPL_YOURHOST:
			{
				const char *t = i_server.greeting[0];
				if (t != NULL && xstrcasecmp(param2, t) != 0) {
					newserv_disconn =
						NEWSERV_DISCONN_MYINFO;
				}
			}
		case RPL_CREATED:
		case RPL_MYINFO:
			xfree(i_server.greeting[command - 1]);
			i_server.greeting[command - 1] = xstrdup(param2);
			if (newserv_disconn == 0) {
				newserv_disconn = NEWSERV_DISCONN_ALWAYS;
			}
			break;
		
		/* Supported features */
		case RPL_ISUPPORT:
			for (n = 0; n < RPL_ISUPPORT_LEN; n++) {
				if (i_server.isupport[n] == NULL) {
					i_server.isupport[n] = xstrdup(param2);
					break;
				}
			}
			newserv_disconn = NEWSERV_DISCONN_ALWAYS;
			break;

		/* This server is restricted. */
		case RPL_RESTRICTED:
			if (cfg.jumprestricted) {
				server_drop(CLNT_RESTRICTED);
				report(SERV_RESTRICTED);
				server_change(1, 1);
			}
			break;
			
		/* Channel has no topic. */
		case RPL_NOTOPIC:
			/*
			 * :<server> RPL_NOTOPIC <client> <channel>
			 * 	:No topic is set
			 */
			t = strchr(param2, ' ');
			if (t != NULL) {
				*t = '\0';
			} else {
				break;
			}

			chptr = channel_find(param2, LIST_ACTIVE);
			if (chptr != NULL) {
				channel_topic(chptr, NULL);
			}
			break;

		/* Channel topic is... */
		case RPL_TOPIC:
			/* :<server> RPL_TOPIC <client> <channel> :<topic> */
			{
				channel_type	*chptr;
				char		*p;
				
				p = strchr(param2, ' ');
				if (p != NULL) {
					*p++ = '\0';
				} else {
					break;
				}
				
				chptr = channel_find(param2, LIST_ACTIVE);
				if (chptr != NULL) {
					channel_topic(chptr, p);
				}
			}
			break;
		
		/* Who set this topic ? */
		case RPL_TOPICWHO:
			/*
			 * :<server> RPL_TOPICWHO <client>
			 *	<channel> <who> <time>
			 */
			{
				channel_type	*chptr;
				char		*p;
				char		*topicwho;
				
				p = strchr(param2, ' ');
				if (p != NULL) {
					*p++ = '\0';
				} else {
					break;
				}
				
				topicwho = p;
				
				p = strchr(topicwho, ' ');
				if (p != NULL) {
					*p++ = '\0';
				} else {
					break;
				}
				
				chptr = channel_find(param2, LIST_ACTIVE);
				if (chptr != NULL) {
					channel_when(chptr, topicwho, p);
				}
			}
			break;

		/* Channel has modes. */
		case RPL_CHANNELMODEIS:
			/*
			 * :server RPL_CNANNELMODEIS <client>
			 *	<channel> <mode> <mode params>
			 */
			/* Kludge. :-) */
			t = strchr(param2, ' ');
			if (t != NULL) {
				char	*channel;
				*t = '\0';
				channel = xstrdup(param2);
				*t = ' ';
				
				parse_modes(channel, nextword(param2));
				xfree(channel);
			}
			break;

		case RPL_NAMREPLY:
			/*
			 * :server RPL_NAMREPLY <client>
			 *	<type> <channel> :<[@ / +]<nick>> ...
			 */
			{
				char	*channel;
				work = xstrdup(param2);

				t = strchr(work, ' ');
				if (t == NULL) {
#ifdef ENDUSERDEBUG
					enduserdebug("no channel at NAMREPLY");
#endif /* ifdef ENDUSERDEBUG */
					break;
				}
				channel = strtok(t + 1, " ");
				chptr = channel_find(channel, LIST_ACTIVE);
#ifdef AUTOMODE
				if (chptr == NULL || chptr->oper != -1) {
#else /* ifdef AUTOMODE */
				if (chptr == NULL) {
#endif /* ifdef else AUTOMODE */
#ifdef ENDUSERDEBUG
					if (chptr == NULL) {
						enduserdebug("NAMREPLY on unjoined channel (%s)", channel);
					}
#endif /* ifdef ENDUSERDEBUG */
					break;
				}

				t = strtok(NULL, " ");
#ifdef ENDUSERDEBUG
				if (t[0] != ':') {
					enduserdebug("no users on NAMREPLY");
					break;
				}
#endif /* ifdef ENDUSERDEBUG */
				n = 0;
				while (t != NULL) {
					n++;
					t = strtok(NULL, " ");
				}
#ifdef AUTOMODE
				chptr->oper = (n == 1 ? 1 : 0);
#endif /* ifdef AUTOMODE */
			}
			break;


		/* Error replies. */

		/* Couldn't join channel. */
		case ERR_INVITEONLYCHAN:
		case ERR_CHANNELISFULL:
		case ERR_TOOMANYTARGETS:
		case ERR_BANNEDFROMCHAN:
		case ERR_BADCHANNELKEY:
		case ERR_BADCHANMASK:
		case ERR_TOOMANYCHANNELS:
		case ERR_UNAVAILRESOURCE:
			/* 
			 * Look for channel and see if we're tryingto join it.
			 */
			work = xstrdup(param2);
			t = strtok(work, " ");
			chptr = channel_find(t, LIST_PASSIVE);
			if (chptr == NULL) {
				break;
			}

			if (chptr->jointries > 0) {
				/*
				 * Automatic join, suppress error.
				 * Nice things, btw, when last try was made
				 * to join the channel, jointries was set to
				 * 0 which means we get to pass the message
				 * to the client.
				 */
				*pass = 0;
			}
			break;
				

			
		/* Commands. */

		/* Someone chaning nick. */
		case CMD_NICK + MINCOMMANDVALUE:
			/* Is that us who changed nick ? */
			if (xstrcasecmp(status.nickname, nick) == 0) {
				xfree(status.nickname);
				status.nickname = xstrdup(param1 + 1);

				if (xstrcasecmp(status.nickname,
						(char *) nicknames.nicks.head->data) == 0) {
					status.got_nick = 1;
					report(MIAU_GOTNICK, status.nickname);
					status.getting_nick = 0;
				} else {
					status.got_nick = 0;
				}
			}

#ifdef CHANLOG
			chanlog_write_entry_all(LOG_NICK, LOGM_NICK,
					get_short_localtime(),
					nick, param1 + 1);
#endif /* ifdef CHANLOG */
			break;

		/* Ping ?  Pong. */
		case CMD_PONG + MINCOMMANDVALUE:
			/* We don't need to reset timer - it is done in irc.c */
#ifdef PINGSTAT
			ping_got++;
#endif /* ifdef PINGSTAT */
			*pass = 0;
			break;

		/* Look ma, he's flying. */
		case CMD_KICK + MINCOMMANDVALUE:
			chptr = channel_find(param1, LIST_ACTIVE);
			if (chptr == NULL) {
#ifdef ENDUSERDEBUG
				enduserdebug("KICK on channel we're not on");
				enduserdebug("command = %d param1 = '%s' "
						"param2 = '%s'",
						command, param1, param2);
#endif /* ifdef ENDUSERDEBUG */
				break;
			}

#ifdef CHANLOG
			if (chanlog_has_log(chptr, LOG_PART)) {
				t = strchr(param2, ' ');
				/* Ugly, done because we cant break up param2 */
				if (t != NULL) {
					char	*target;

					*t = '\0';
					target = xstrdup(param2);
					*t = ' ';

					chanlog_write_entry(chptr, LOGM_KICK,
							get_short_localtime(),
							target, nick, 
							nextword(param2) + 1);
					xfree(target);
				}
			}
#endif /* ifdef CHANLOG */

			/* Me being kicked ? */
			{
				size_t t;
				t = pos(param2, ' ');
				if (xstrncasecmp(status.nickname,
							param2, t) == 0 &&
						strlen(status.nickname) == t) {
					report(CLNT_KICK, origin, param1,
							nextword(param2) + 1);
					channel_rem(chptr, LIST_ACTIVE);
				}
			}
			break;

		/* Someone joining. */
		case CMD_JOIN + MINCOMMANDVALUE:
			n = (param1[0] == ':' ? 1 : 0);
			/* Was that me ? */
			if (xstrcasecmp(status.nickname, nick) == 0) {
				/* Add channel to active list. */
				channel_add(param1 + n, param2, LIST_ACTIVE);
			}

			/* Get pointer to this channel. */
			chptr = channel_find(param1 + n, LIST_ACTIVE);
			if (chptr == NULL) {
#ifdef ENDUSERDEBUG
				enduserdebug("JOIN on channel we're not on");
				enduserdebug("command = %d / param1 + n = '%s'"
						" / param2 = '%s'",
						command, param1 + n, param2);
#endif /* ifdef ENDUSERDEBUG */
				break;
			}

#ifdef AUTOMODE
			/* Don't care if it's me joining. */
			if (xstrcasecmp(nick, status.nickname) != 0) {
				automode_queue(nick, hostname, chptr);
			}
#endif /* ifdef AUTOMODE */
	
#ifdef CHANLOG
			if (chanlog_has_log(chptr, LOG_JOIN)) {
				chanlog_write_entry(chptr, LOGM_JOIN,
						get_short_localtime(),
						nick, hostname,
						chptr->simple_name);
			}
#endif /* ifdef CHANLOG */

			break;

		/* ...and someone parting our party. */
		case CMD_PART + MINCOMMANDVALUE:
			/* Get pointer to this channel. */
			if (param1[0] == ':') {
				/* channel name as prefix? eww... */
				param1++;
			}
			chptr = channel_find(param1, LIST_ACTIVE);
			/* 
			 * If we have sent PART (or JOIN 0) lets assume we
			 * have parted those channels. This means that channel
			 * entries of those channels are removed from
			 * active_channels and therefore cannot be found. This
			 * is why why "chptr == NULL" is nothing to worry
			 * about.
			 */
			if (chptr == NULL) { break; }

#ifdef AUTOMODE
			/* No automodes for that person. */
			automode_drop_channel(chptr, nick, '\0');
#endif /* ifdef AUTOMODE */

#ifdef CHANLOG
			if (chanlog_has_log(chptr, LOG_PART)) {
				chanlog_write_entry(chptr, LOGM_PART,
						get_short_localtime(),
						nick, chptr->simple_name,
						(param2) ? (param2 + 1) ?
						param2 + 1 : "" : "");
			}
#endif /* ifdef CHANLOG */

			/* Remove channel from list if it was me leaving. */
			if (xstrcasecmp(nick, status.nickname) == 0) {
				channel_rem(chptr, LIST_ACTIVE);
			}

			break;

		/* Someone's leaving for good. */
		case CMD_QUIT + MINCOMMANDVALUE:
#ifdef AUTOMODE
			automode_drop_nick(nick, '\0');
#endif /* ifdef AUTOMODE */

#ifdef CHANLOG
			chanlog_write_entry_all(LOG_QUIT, LOGM_QUIT,
					get_short_localtime(),
					nick, param1 + 1, " ", param2);
#endif /* ifdef CHANLOG */

			break;

		/* Ouch. That must hurt. Someone just went down, hard. */
		case CMD_KILL + MINCOMMANDVALUE:
			/* Me ?-) */
			if (xstrcasecmp(status.nickname, nick) == 0) {
				error(IRC_KILL, nick);
			}
			*pass = 0;	/* We'll handle this by ourself. */

			break;

		/* Changing modes... */
		case CMD_MODE + MINCOMMANDVALUE:
			if (status.goodhostname == 0
					&& pos(hostname, '@') != -1
					&& xstrcasecmp(param1,
						status.nickname) == 0) {
				xfree(status.idhostname);
				status.idhostname = xstrdup(hostname);
				status.goodhostname = pos(hostname, '@') + 1;
			}
			chptr = channel_find(param1, LIST_ACTIVE);
			if (chptr == NULL) {
#ifdef ENDUSERDEBUG
				if (xstrcasecmp(param1, status.nickname) != 0) {
					enduserdebug("MODE on unknown channel");
				}
#endif /* ifdef ENDUSERDEBUG */
				break;
			}

			parse_modes(param1, param2);
			
#ifdef CHANLOG
			if (chanlog_has_log(chptr, LOG_MODE)) {
				chanlog_write_entry(chptr, LOGM_MODE,
						get_short_localtime(),
						nick, param2);
			}
#endif /* ifdef CHANLOG */

			break;

		/* Someone changing topic. */
		case CMD_TOPIC + MINCOMMANDVALUE:
			/* :<source> TOPIC <channel> :<topic> */

			chptr = channel_find(param1, LIST_ACTIVE);
			if (chptr == NULL) {
#ifdef ENDUSERDEBUG
				enduserdebug("TOPIC on channel we're not on");
#endif /* ifdef ENDUSERDEBUG */
				break;
			}

			{
				struct timeval	now;
				struct timezone	tz;
				char		timebuf[20];

				channel_topic(chptr, param2);

				gettimeofday(&now, &tz);
				/*
				 * strftime("%s") can't be used (not in ISO C),
				 * This should work as a replacement,
				 */
				snprintf(timebuf, 20, "%d", (int) now.tv_sec
						- tz.tz_minuteswest * 60);
				timebuf[19] = '\0';
				channel_when(chptr, origin, timebuf);
			}

#ifdef CHANLOG
			if (chanlog_has_log(chptr, LOG_MISC)) {
				chanlog_write_entry(chptr, LOGM_TOPIC,
						get_short_localtime(), nick,
						(param2 + 1) ? param2 + 1 : "");
			}
#endif /* ifdef CHANLOG */
			
			break;

		/* I hear someone talking... */
		case CMD_NOTICE + MINCOMMANDVALUE:
		case CMD_PRIVMSG + MINCOMMANDVALUE:
			/* hostname = username@hostname */
			isprivmsg = parse_privmsg(param1, param2,
					nick, hostname, command, pass);
			break;

#ifdef OBSOLETE /* Dummy. */
		default:
			/* Got something we don't recognize. */
			break;
#endif /* ifdef OBSOLETE */
	}

	if (command < MINCOMMANDVALUE) {
		/*
		 * Client is connected, we're registered with the server and
		 * there are error messages we want to handle.
		 */
		if (i_server.connected && status.getting_nick > 0 &&
				(command == ERR_NICKNAMEINUSE ||
					command == ERR_UNAVAILRESOURCE)) {
			/* Decrease counter for sent NICK-commands. */
			status.getting_nick--;
			*pass = 0;
		}

		/*
		 * There are a few things we don't need to pass to qlog:
		 *	- message from server acknowleding we're away
		 *	- welcome-messages
		 */
		if (c_clients.connected == 0 &&
				(command == RPL_NOWAWAY || command <= 4)) {
			*pass = 0;
		}

		/*
		 * We're trying to connect the server and we're getting
		 * something from the server.
		 */
		if (i_server.connected != 2 &&
				(command == ERR_UNAVAILRESOURCE ||
					command == ERR_NICKNAMEINUSE ||
					command == ERR_ERRONEUSNICKNAME)) {
			get_nick(command == ERR_UNAVAILRESOURCE ?
					IRC_NICKUNAVAIL :
					(command == ERR_NICKNAMEINUSE) ?
					IRC_NICKINUSE : IRC_BADNICK);
		}
	} /* Command 000-999 */
	
#ifdef QUICKLOG
	/* Perhaps we need to write something to quicklog. */
	if ((c_clients.connected == 0 || cfg.flushqlog == 0) && *pass &&
				param1 != NULL) {
		qlog_write(isprivmsg, "%s", original);
	}
#endif /* ifdef QUICKLOG */

	if (cfg.newserv_disconn == newserv_disconn
			&& cfg.newserv_disconn != NEWSERV_DISCONN_NONE) {
		drop_newclient(CLNT_SERVINFO);
		client_drop(NULL, CLNT_SERVINFO, DISCONNECT_REPORT, 1, NULL);
	}

	xfree(work);
	xfree(nick);
	xfree(hostname);
} /* void server_reply(const int command, char *original, char *origin,
		char *param1, char *param2, int *pass) */



void
parse_modes(const char *channel, const char *original)
{
	channel_type	*chptr = channel_find(channel, LIST_ACTIVE);
	char		*buf;
	char		*ptr;
	char		*param;
	char		modetype = '+';

	if (chptr == NULL) {
#ifdef ENDUSERDEBUG
		enduserdebug("MODE on channel we're not on");
#endif /* ifdef ENDUSERDEBUG */
		return;
	}

	buf = xstrdup(original);

	ptr = strtok(buf, " ");
	param = strtok(NULL, " ");
	while (ptr[0] != '\0') {
		/* See if we found modetype. */
		if (ptr[0] == '+' || ptr[0] == '-') {
			modetype = ptr[0];
			/* Assume there's only [-+]. */
			ptr++;
		}

		switch (ptr[0]) {
			/* miau thinks 'o' and 'O' are the same. */
			case 'O':	/* Channel creator. */
			case 'o':	/* Channel operator. */
			case 'v':	/* Voice privilege. */
#ifdef AUTOMODE
				/*
				 * Appears that some servers think 'O' flag is
				 * for "oper only" channel (no parameter), some
				 * think it's a user flag for channel creator.
				 * This means if 'O' comes with no parameter,
				 * we can pretty much safely ignore it.
				 */
				
				if (ptr[0] == 'O' || param == NULL) {
					break;
				}
				if (ptr[0] == 'O') {
					ptr[0] = 'o';
				}
				if (modetype == '-') {	/* Taking... */
					if (xstrcasecmp(status.nickname,
								param) == 0
							&& ptr[0] == 'o') {
						/* They took my pride... */
						chptr->oper = 0;
					}
				} else {
					if (xstrcasecmp(status.nickname,
								param) == 0
							&& ptr[0] == 'o') {
						chptr->oper = 1;
					} else {
						automode_drop_channel(chptr,
								param,
								ptr[0]);
					}
				}

#endif /* ifdef AUTOMODE */
				param = strtok(NULL, " ");
				break;

			case 'k':	/* Channel key. */
				/*
				 * If there's a channel with key "123" and user
				 * limit "321" at GalaxyNet, mode query returns
				 * "+lk 123". I suppose we just have to live
				 * without that missing parameter.
				 */
				if (modetype == '+' && param != NULL) {
					chptr->key = xstrdup(param);
				}
				/* No need to clear unset key. */
				/* Even removing key needs parameter. */
				param = strtok(NULL, " ");
				break;
				
			case 'l':	/* Limit. */
				/*
				 * It's not like we would care, but we need
				 * to jump to next parameter.
				 */
				if (modetype == '+') {
					param = strtok(NULL, " ");
				}
				break;

			case 'b':	/* Ban mask. */
			case 'e':	/* Exception mask. */
			case 'I':	/* Invitation mask. */
				/* Get next parameter. */
				param = strtok(NULL, " ");
				break;

#if USE_DISABLED
			case 'a':	/* anonymous */
			case 'i':	/* invite-only */
			case 'm':	/* moderated */
			case 'n':	/* no messages from outside */
			case 'q':	/* quiet */
			case 's':	/* secret */
			case 'r':	/* server re-op */
			case 't':	/* topic */
#endif /* ifdef USE_DISABLED */
		}

		ptr++;
	}

	xfree(buf);
} /* void parse_modes(const char *channel, const char *original) */


syntax highlighted by Code2HTML, v. 0.9.1