/* X-Chat
 * Copyright (C) 1998 Peter Zelezny.
 *
 * 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <time.h>

#ifdef WIN32
#include <io.h>
#else
#include <unistd.h>
#endif

#define WANTARPA
#define WANTDNS
#include "inet.h"

#include "zoitechat.h"
#include "util.h"
#include "ignore.h"
#include "fe.h"
#include "modes.h"
#include "network.h"
#include "notify.h"
#include "outbound.h"
#include "inbound.h"
#include "server.h"
#include "servlist.h"
#include "sts.h"
#include "text.h"

void reply_state_clear (session *sess);

gboolean
reply_msgid_valid (const char *msgid)
{
	const char *p;

	if (!msgid || !*msgid || *msgid == ':')
		return FALSE;

	for (p = msgid; *p; p++)
	{
		if (*p == ' ' || *p == '\r' || *p == '\n')
			return FALSE;
	}

	return TRUE;
}

static void
reply_item_free (reply_item *item)
{
	if (!item)
		return;
	g_free (item->msgid);
	g_free (item->nick);
	g_free (item->text);
	g_free (item);
}

void
reply_cache_free (session *sess)
{
	if (!sess)
		return;
	g_slist_free_full (sess->reply_items, (GDestroyNotify) reply_item_free);
	sess->reply_items = NULL;
	reply_state_clear (sess);
}

reply_item *
reply_cache_find (session *sess, const char *msgid)
{
	GSList *list;

	if (!sess || !reply_msgid_valid (msgid))
		return NULL;

	for (list = sess->reply_items; list; list = list->next)
	{
		reply_item *item = list->data;
		if (!strcmp (item->msgid, msgid))
			return item;
	}

	return NULL;
}

reply_item *
reply_cache_latest_from (session *sess, const char *nick)
{
	GSList *list;

	if (!sess || !nick || !*nick)
		return NULL;

	for (list = sess->reply_items; list; list = list->next)
	{
		reply_item *item = list->data;
		if (!sess->server->p_cmp (item->nick, nick))
			return item;
	}

	return NULL;
}

void
reply_state_clear (session *sess)
{
	if (!sess)
		return;
	g_clear_pointer (&sess->reply_msgid, g_free);
	g_clear_pointer (&sess->reply_target, g_free);
	g_clear_pointer (&sess->reply_nick, g_free);
	g_clear_pointer (&sess->reply_text, g_free);
}

void
reply_state_set (session *sess, const char *msgid, const char *target, const char *nick, const char *text)
{
	if (!sess || !reply_msgid_valid (msgid))
		return;
	reply_state_clear (sess);
	sess->reply_msgid = g_strdup (msgid);
	sess->reply_target = g_strdup (target && *target ? target : sess->channel);
	sess->reply_nick = g_strdup (nick && *nick ? nick : _("message"));
	sess->reply_text = g_strdup (text && *text ? text : _("Original message unavailable"));
}

static void
reply_cache_add (session *sess, const char *msgid, const char *nick, const char *text, time_t timestamp)
{
	reply_item *item;

	if (!sess || !reply_msgid_valid (msgid) || !nick || !text)
		return;

	item = reply_cache_find (sess, msgid);
	if (item)
	{
		g_free (item->nick);
		g_free (item->text);
		item->nick = g_strdup (nick);
		item->text = g_strdup (text);
		item->timestamp = timestamp;
		return;
	}

	item = g_new0 (reply_item, 1);
	item->msgid = g_strdup (msgid);
	item->nick = g_strdup (nick);
	item->text = g_strdup (text);
	item->timestamp = timestamp;
	sess->reply_items = g_slist_prepend (sess->reply_items, item);

	if (g_slist_length (sess->reply_items) > 200)
	{
		GSList *last = g_slist_last (sess->reply_items);
		item = last->data;
		sess->reply_items = g_slist_delete_link (sess->reply_items, last);
		reply_item_free (item);
	}
}

static void
reply_context_print (session *sess, const message_tags_data *tags_data)
{
	reply_item *item;

	if (!sess || !tags_data || !tags_data->reply)
		return;

	item = reply_cache_find (sess, tags_data->reply);
	if (item)
		PrintTextTimeStampf (sess, tags_data->timestamp, "\00314│ ↪ %s: %.180s\017\n", item->nick, item->text);
	else
		PrintTextTimeStamp (sess, "\00314│ ↪ Original message unavailable\017\n", tags_data->timestamp);
}
#include "ctcp.h"
#include "zoitechatc.h"
#include "chanopt.h"


void
clear_channel (session *sess)
{
	if (sess->channel[0])
		strcpy (sess->waitchannel, sess->channel);
	sess->channel[0] = 0;
	sess->doing_who = FALSE;
	sess->done_away_check = FALSE;

	log_close (sess);

	if (sess->current_modes)
	{
		g_free (sess->current_modes);
		sess->current_modes = NULL;
	}

	if (sess->mode_timeout_tag)
	{
		fe_timeout_remove (sess->mode_timeout_tag);
		sess->mode_timeout_tag = 0;
	}

	fe_clear_channel (sess);
	userlist_clear (sess);
	fe_set_nonchannel (sess, FALSE);
	fe_set_title (sess);
}

void
set_topic (session *sess, char *topic, char *stripped_topic)
{
	/* The topic of dialogs are the users hostname which is logged is new */
	if (sess->type == SESS_DIALOG && (!sess->topic || strcmp(sess->topic, stripped_topic))
		&& sess->logfd != -1)
	{
		char tbuf[1024];
		g_snprintf (tbuf, sizeof (tbuf), "[%s has address %s]\n", sess->channel, stripped_topic);
		write (sess->logfd, tbuf, strlen (tbuf));
	}

	g_free (sess->topic);
	sess->topic = g_strdup (stripped_topic);
	fe_set_topic (sess, topic, stripped_topic);
}

static session *
find_session_from_nick (char *nick, server *serv)
{
	session *sess;
	GSList *list = sess_list;

	sess = find_dialog (serv, nick);
	if (sess)
		return sess;

	if (serv->front_session)
	{
		if (!g_ascii_strcasecmp(nick, "ChanServ") || userlist_find (serv->front_session, nick))
			return serv->front_session;
	}

	if (current_sess && current_sess->server == serv)
	{
		if (userlist_find (current_sess, nick))
			return current_sess;
	}

	while (list)
	{
		sess = list->data;
		if (sess->server == serv)
		{
			if (userlist_find (sess, nick))
				return sess;
		}
		list = list->next;
	}
	return NULL;
}

static session *
inbound_open_dialog (server *serv, char *from,
							const message_tags_data *tags_data)
{
	session *sess;

	sess = new_ircwindow (serv, from, SESS_DIALOG, 0);
	/* for playing sounds */
	EMIT_SIGNAL_TIMESTAMP (XP_TE_OPENDIALOG, sess, NULL, NULL, NULL, NULL, 0,
								  tags_data->timestamp);

	return sess;
}

static void
inbound_make_idtext (server *serv, char *idtext, int max, int id)
{
	idtext[0] = 0;
	if (serv->have_idmsg || serv->have_accnotify)
	{
		if (id)
		{
			safe_strcpy (idtext, prefs.hex_irc_id_ytext, max);
		} else
		{
			safe_strcpy (idtext, prefs.hex_irc_id_ntext, max);
		}
		/* convert codes like %C,%U to the proper ones */
		check_special_chars (idtext, TRUE);
	}
}

void
inbound_privmsg (server *serv, char *from, char *ip, char *text, int id,
					  const message_tags_data *tags_data)
{
	session *sess;
	struct User *user;
	char idtext[64];
	gboolean nodiag = FALSE;

	sess = find_dialog (serv, from);

	if (sess || prefs.hex_gui_autoopen_dialog)
	{
		/*0=ctcp  1=priv will set hex_gui_autoopen_dialog=0 here is flud detected */
		if (!sess)
		{
			if (flood_check (from, ip, serv, current_sess, 1))
				/* Create a dialog session */
				sess = inbound_open_dialog (serv, from, tags_data);
			else
				sess = serv->server_session;
			if (!sess)
				return; /* ?? */
		}

		if (ip && ip[0])
			set_topic (sess, ip, ip);
		inbound_chanmsg (serv, NULL, NULL, from, text, FALSE, tags_data->identified, tags_data);
		return;
	}

	sess = find_session_from_nick (from, serv);
	if (!sess)
	{
		sess = serv->front_session;
		nodiag = TRUE; /* We don't want it to look like a normal message in front sess */
	}

	user = userlist_find (sess, from);
	if (user)
	{
		user->lasttalk = time (0);
		if (user->account)
			id = TRUE;
	}
	
	inbound_make_idtext (serv, idtext, sizeof (idtext), id);

	if (sess->type == SESS_DIALOG && !nodiag)
		EMIT_SIGNAL_TIMESTAMP (XP_TE_DPRIVMSG, sess, from, text, idtext, NULL, 0,
									  tags_data->timestamp);
	else
		EMIT_SIGNAL_TIMESTAMP (XP_TE_PRIVMSG, sess, from, text, idtext, NULL, 0, 
									  tags_data->timestamp);
}

/* used for Alerts section. Masks can be separated by commas and spaces. */

static char *
alert_normalize_word (const char *text)
{
	GString *normalized;
	char *composed;
	const char *p;

	composed = g_utf8_normalize (text, -1, G_NORMALIZE_ALL_COMPOSE);
	if (!composed)
		composed = g_strdup (text);

	normalized = g_string_sized_new (strlen (composed));
	p = composed;

	while (*p)
	{
		gunichar ch = g_utf8_get_char ((const guchar *)p);

		/* Ignore selector/joiner codepoints that vary by input method. */
		if (ch != 0x200D && ch != 0xFE0E && ch != 0xFE0F)
			g_string_append_unichar (normalized, ch);

		p = g_utf8_next_char (p);
	}

	g_free (composed);
	return g_string_free (normalized, FALSE);
}

gboolean
alert_match_word (char *word, char *masks)
{
	char *p = masks;
	char endchar;
	char *word_normalized;
	char *mask_normalized;
	int res;

	word_normalized = alert_normalize_word (word);

	if (masks[0] == 0)
	{
		g_free (word_normalized);
		return FALSE;
	}

	while (1)
	{
		/* if it's a 0, space or comma, the word has ended. */
		if (*p == 0 || *p == ' ' || *p == ',')
		{
			endchar = *p;
			*p = 0;
			mask_normalized = alert_normalize_word (g_strchug (masks));
			res = match (mask_normalized, word_normalized);
			g_free (mask_normalized);
			*p = endchar;

			if (res)
			{
				g_free (word_normalized);
				return TRUE;	/* yes, matched! */
			}

			masks = p + 1;
			if (*p == 0)
			{
				g_free (word_normalized);
				return FALSE;
			}
		}
		p++;
	}
}

gboolean
alert_match_text (char *text, char *masks)
{
	unsigned char *p = text;
	unsigned char endchar;
	gunichar ch;
	GUnicodeType ch_type;
	int res;

	if (masks[0] == 0)
		return FALSE;

	while (1)
	{
		ch = g_utf8_get_char (p);
		ch_type = g_unichar_type (ch);

		if (g_unichar_isdigit (ch) || g_unichar_isalpha (ch))
		{
			p += g_utf8_skip [p[0]];
			continue;
		}

		/* if it's RFC1459 <special>, it can be inside a word */
		switch (ch)
		{
		case '-': case '[': case ']': case '\\':
		case '`': case '^': case '{': case '}':
		case '_': case '|':
			p += g_utf8_skip [p[0]];
			continue;
		}

		/* Symbols (including emoji) can be part of highlighted words. */
		if (ch_type == G_UNICODE_MATH_SYMBOL ||
			 ch_type == G_UNICODE_CURRENCY_SYMBOL ||
			 ch_type == G_UNICODE_MODIFIER_SYMBOL ||
			 ch_type == G_UNICODE_OTHER_SYMBOL)
		{
			p += g_utf8_skip [p[0]];
			continue;
		}

		/* Delimiters end the word. */
		if (*p == 0 || g_unichar_isspace (ch) || g_unichar_ispunct (ch) ||
			 g_unichar_iscntrl (ch))
		{
			endchar = *p;
			*p = 0;
			res = alert_match_word (text, masks);
			*p = endchar;

			if (res)
				return TRUE;	/* yes, matched! */

			text = p + g_utf8_skip [p[0]];
			if (*p == 0)
				return FALSE;
		}

		p += g_utf8_skip [p[0]];
	}
}

static int
is_hilight (char *from, char *text, session *sess, server *serv)
{
	if (alert_match_word (from, prefs.hex_irc_no_hilight))
		return 0;

	text = strip_color (text, -1, STRIP_ALL);

	if (alert_match_text (text, serv->nick) ||
		 alert_match_text (text, prefs.hex_irc_extra_hilight) ||
		 alert_match_word (from, prefs.hex_irc_nick_hilight))
	{
		g_free (text);
		if (sess != current_tab)
		{
			sess->tab_state |= TAB_STATE_NEW_HILIGHT;
			lastact_update (sess);
		}
		return 1;
	}

	g_free (text);
	return 0;
}

void
inbound_action (session *sess, char *chan, char *from, char *ip, char *text,
					 int fromme, int id, const message_tags_data *tags_data)
{
	session *def = sess;
	server *serv = sess->server;
	struct User *user;
	char nickchar[2] = "\000";
	char idtext[64];
	int privaction = FALSE;

	if (!fromme)
	{
		if (is_channel (serv, chan))
		{
			sess = find_channel (serv, chan);
		} else
		{
			/* it's a private action! */
			privaction = TRUE;
			/* find a dialog tab for it */
			sess = find_dialog (serv, from);
			/* if non found, open a new one */
			if (!sess && prefs.hex_gui_autoopen_dialog)
			{
				/* but only if it wouldn't flood */
				if (flood_check (from, ip, serv, current_sess, 1))
					sess = inbound_open_dialog (serv, from, tags_data);
				else
					sess = serv->server_session;
			}
			if (!sess)
			{
				sess = find_session_from_nick (from, serv);
				/* still not good? */
				if (!sess)
					sess = serv->front_session;
			}
		}
	}

	if (!sess)
		sess = def;

	if (sess != current_tab)
	{
		if (fromme)
			sess->tab_state |= TAB_STATE_NEW_DATA;
		else
			sess->tab_state |= TAB_STATE_NEW_MSG;
		lastact_update (sess);
	}

	user = userlist_find (sess, from);
	if (user)
	{
		nickchar[0] = user->prefix[0];
		user->lasttalk = time (0);
		if (user->account)
			id = TRUE;
		if (user->me)
			fromme = TRUE;
	}

	if (!fromme)
		fe_set_typing (sess, from, "done");

	inbound_make_idtext (serv, idtext, sizeof (idtext), id);

	if (!fromme && !privaction)
	{
		if (is_hilight (from, text, sess, serv))
		{
			EMIT_SIGNAL_TIMESTAMP (XP_TE_HCHANACTION, sess, from, text, nickchar,
										  idtext, 0, tags_data->timestamp);
			return;
		}
	}

	if (fromme)
		EMIT_SIGNAL_TIMESTAMP (XP_TE_UACTION, sess, from, text, nickchar, idtext,
									  0, tags_data->timestamp);
	else if (!privaction)
		EMIT_SIGNAL_TIMESTAMP (XP_TE_CHANACTION, sess, from, text, nickchar,
									  idtext, 0, tags_data->timestamp);
	else if (sess->type == SESS_DIALOG)
		EMIT_SIGNAL_TIMESTAMP (XP_TE_DPRIVACTION, sess, from, text, idtext, NULL,
									  0, tags_data->timestamp);
	else
		EMIT_SIGNAL_TIMESTAMP (XP_TE_PRIVACTION, sess, from, text, idtext, NULL, 0,
									  tags_data->timestamp);
}

void
inbound_chanmsg (server *serv, session *sess, char *chan, char *from, 
					  char *text, char fromme, int id, 
					  const message_tags_data *tags_data)
{
	struct User *user;
	int hilight = FALSE;
	char nickchar[2] = "\000";
	char idtext[64];

	if (!sess)
	{
		if (chan)
		{
			sess = find_channel (serv, chan);
			if (!sess && !is_channel (serv, chan))
				sess = find_dialog (serv, chan);
		} else
		{
			sess = find_dialog (serv, from);
		}
		if (!sess)
			return;
	}

	if (sess != current_tab)
	{
		sess->tab_state |= TAB_STATE_NEW_MSG;
		lastact_update (sess);
	}

	user = userlist_find (sess, from);
	if (user)
	{
		if (user->account)
			id = TRUE;
		nickchar[0] = user->prefix[0];
		user->lasttalk = time (0);
		if (user->me)
			fromme = TRUE;
	}

	if (!fromme)
		fe_set_typing (sess, from, "done");

	if (fromme)
	{
		if (prefs.hex_away_auto_unmark && serv->is_away &&
			 (!tags_data->timestamp || serv->have_echo_message))
			sess->server->p_set_back (sess->server);
		reply_context_print (sess, tags_data);
		EMIT_SIGNAL_TIMESTAMP (XP_TE_UCHANMSG, sess, from, text, nickchar, NULL,
									  0, tags_data->timestamp);
		reply_cache_add (sess, tags_data->msgid, from, text, tags_data->timestamp);
		return;
	}

	inbound_make_idtext (serv, idtext, sizeof (idtext), id);

	if (is_hilight (from, text, sess, serv))
		hilight = TRUE;

	reply_context_print (sess, tags_data);

	if (sess->type == SESS_DIALOG)
		EMIT_SIGNAL_TIMESTAMP (XP_TE_DPRIVMSG, sess, from, text, idtext, NULL, 0,
									  tags_data->timestamp);
	else if (hilight)
		EMIT_SIGNAL_TIMESTAMP (XP_TE_HCHANMSG, sess, from, text, nickchar, idtext,
									  0, tags_data->timestamp);
	else
		EMIT_SIGNAL_TIMESTAMP (XP_TE_CHANMSG, sess, from, text, nickchar, idtext,
									  0, tags_data->timestamp);

	reply_cache_add (sess, tags_data->msgid, from, text, tags_data->timestamp);
}

void
inbound_newnick (server *serv, char *nick, char *newnick, int quiet,
					  const message_tags_data *tags_data)
{
	int me = FALSE;
	session *sess;
	GSList *list = sess_list;

	if (!serv->p_cmp (nick, serv->nick))
	{
		me = TRUE;
		safe_strcpy (serv->nick, newnick, NICKLEN);
	}

	while (list)
	{
		sess = list->data;
		if (sess->server == serv)
		{
			if (userlist_change (sess, nick, newnick) || (me && sess->type == SESS_SERVER))
			{
				if (!quiet)
				{
					if (me)
						EMIT_SIGNAL_TIMESTAMP (XP_TE_UCHANGENICK, sess, nick, 
													  newnick, NULL, NULL, 0,
													  tags_data->timestamp);
					else
						EMIT_SIGNAL_TIMESTAMP (XP_TE_CHANGENICK, sess, nick,
													  newnick, NULL, NULL, 0, tags_data->timestamp);
				}
			}
			if (sess->type == SESS_DIALOG && !serv->p_cmp (sess->channel, nick))
			{
				safe_strcpy (sess->channel, newnick, CHANLEN);
				fe_set_channel (sess);
			}
			fe_set_title (sess);
		}
		list = list->next;
	}

	dcc_change_nick (serv, nick, newnick);

	if (me)
		fe_set_nick (serv, newnick);
}

/* find a "<none>" tab */
static session *
find_unused_session (server *serv)
{
	session *sess;
	GSList *list = sess_list;
	while (list)
	{
		sess = (session *) list->data;
		if (sess->type == SESS_CHANNEL && sess->channel[0] == 0 &&
			 sess->server == serv)
		{
			if (sess->waitchannel[0] == 0)
				return sess;
		}
		list = list->next;
	}
	return NULL;
}

static session *
find_session_from_waitchannel (char *chan, struct server *serv)
{
	session *sess;
	GSList *list = sess_list;
	while (list)
	{
		sess = (session *) list->data;
		if (sess->server == serv && sess->channel[0] == 0 && sess->type == SESS_CHANNEL)
		{
			if (!serv->p_cmp (chan, sess->waitchannel))
				return sess;
		}
		list = list->next;
	}
	return NULL;
}

void
inbound_ujoin (server *serv, char *chan, char *nick, char *ip,
					const message_tags_data *tags_data)
{
	session *sess;
	int found_unused = FALSE;

	/* already joined? probably a bnc */
	sess = find_channel (serv, chan);
	if (!sess)
	{
		/* see if a window is waiting to join this channel */
		sess = find_session_from_waitchannel (chan, serv);
		if (!sess)
		{
			/* find a "<none>" tab and use that */
			sess = find_unused_session (serv);
			found_unused = sess != NULL;
			if (!sess)
				/* last resort, open a new tab/window */
				sess = new_ircwindow (serv, chan, SESS_CHANNEL, 1);
		}
	}

	safe_strcpy (sess->channel, chan, CHANLEN);
	if (found_unused)
	{
		chanopt_load (sess);
		scrollback_load (sess);
		if (sess->scrollwritten && sess->scrollback_replay_marklast)
			sess->scrollback_replay_marklast (sess);
	}

	fe_set_channel (sess);
	fe_set_title (sess);
	fe_set_nonchannel (sess, TRUE);
	userlist_clear (sess);

	log_open_or_close (sess);

	sess->waitchannel[0] = 0;
	sess->ignore_date = TRUE;
	sess->ignore_mode = TRUE;
	sess->ignore_names = TRUE;
	sess->end_of_names = FALSE;

	/* sends a MODE */
	serv->p_join_info (sess->server, chan);

	EMIT_SIGNAL_TIMESTAMP (XP_TE_UJOIN, sess, nick, chan, ip, NULL, 0,
								  tags_data->timestamp);

	if (prefs.hex_irc_who_join)
	{
		/* sends WHO #channel */
		serv->p_user_list (sess->server, chan);
		sess->doing_who = TRUE;
	}
}

void
inbound_ukick (server *serv, char *chan, char *kicker, char *reason,
					const message_tags_data *tags_data)
{
	session *sess = find_channel (serv, chan);
	if (sess)
	{
		EMIT_SIGNAL_TIMESTAMP (XP_TE_UKICK, sess, serv->nick, chan, kicker, 
									  reason, 0, tags_data->timestamp);
		clear_channel (sess);
		if (prefs.hex_irc_auto_rejoin)
		{
			serv->p_join (serv, chan, sess->channelkey);
			safe_strcpy (sess->waitchannel, chan, CHANLEN);
		}
	}
}

void
inbound_upart (server *serv, char *chan, char *ip, char *reason,
					const message_tags_data *tags_data)
{
	session *sess = find_channel (serv, chan);
	if (sess)
	{
		if (*reason)
			EMIT_SIGNAL_TIMESTAMP (XP_TE_UPARTREASON, sess, serv->nick, ip, chan,
										  reason, 0, tags_data->timestamp);
		else
			EMIT_SIGNAL_TIMESTAMP (XP_TE_UPART, sess, serv->nick, ip, chan, NULL,
										  0, tags_data->timestamp);
		clear_channel (sess);
	}
}

void
inbound_nameslist (server *serv, char *chan, char *names,
						 const message_tags_data *tags_data)
{
	session *sess;
	char **name_list;
	char *host, *nopre_name;
	char name[NICKLEN];
	int i;
	size_t offset;

	sess = find_channel (serv, chan);
	if (!sess)
	{
		EMIT_SIGNAL_TIMESTAMP (XP_TE_USERSONCHAN, serv->server_session, chan,
									  names, NULL, NULL, 0, tags_data->timestamp);
		return;
	}
	if (!sess->ignore_names)
		EMIT_SIGNAL_TIMESTAMP (XP_TE_USERSONCHAN, sess, chan, names, NULL, NULL,
									  0, tags_data->timestamp);

	if (sess->end_of_names)
	{
		sess->end_of_names = FALSE;
		userlist_clear (sess);
	}

	name_list = g_strsplit (names, " ", -1);
	for (i = 0; name_list[i]; i++)
	{
		host = NULL;
		offset = sizeof(name);

		if (name_list[i][0] == 0)
			continue;

		if (serv->have_uhnames)
		{
			offset = 0;
			nopre_name = name_list[i];

			/* Ignore prefixes so '!' won't cause issues */
			while (strchr (serv->nick_prefixes, *nopre_name) != NULL)
			{
				nopre_name++;
				offset++;
			}

			offset += strcspn (nopre_name, "!");
			if (offset++ < strlen (name_list[i]))
				host = name_list[i] + offset;
		}

		g_strlcpy (name, name_list[i], MIN(offset, sizeof(name)));

		userlist_add (sess, name, host, NULL, NULL, tags_data);
	}
	g_strfreev (name_list);
}

void
inbound_topic (server *serv, char *chan, char *topic_text,
					const message_tags_data *tags_data)
{
	session *sess = find_channel (serv, chan);
	char *stripped_topic;

	if (sess)
	{
		stripped_topic = strip_color (topic_text, -1, STRIP_ALL);
		set_topic (sess, topic_text, stripped_topic);
		g_free (stripped_topic);
	} else
		sess = serv->server_session;

	EMIT_SIGNAL_TIMESTAMP (XP_TE_TOPIC, sess, chan, topic_text, NULL, NULL, 0,
								  tags_data->timestamp);
}

void
inbound_topicnew (server *serv, char *nick, char *chan, char *topic,
						const message_tags_data *tags_data)
{
	session *sess;
	char *stripped_topic;

	sess = find_channel (serv, chan);
	if (sess)
	{
		EMIT_SIGNAL_TIMESTAMP (XP_TE_NEWTOPIC, sess, nick, topic, chan, NULL, 0,
									  tags_data->timestamp);
		stripped_topic = strip_color (topic, -1, STRIP_ALL);
		set_topic (sess, topic, stripped_topic);
		g_free (stripped_topic);
	}
}

void
inbound_join (server *serv, char *chan, char *user, char *ip, char *account,
				  char *realname, const message_tags_data *tags_data)
{
	session *sess = find_channel (serv, chan);
	if (sess)
	{
		EMIT_SIGNAL_TIMESTAMP (XP_TE_JOIN, sess, user, chan, ip, account, 0,
									  tags_data->timestamp);
		userlist_add (sess, user, ip, account, realname, tags_data);
	}
}

void
inbound_kick (server *serv, char *chan, char *user, char *kicker, char *reason,
				  const message_tags_data *tags_data)
{
	session *sess = find_channel (serv, chan);
	if (sess)
	{
		EMIT_SIGNAL_TIMESTAMP (XP_TE_KICK, sess, kicker, user, chan, reason, 0,
									  tags_data->timestamp);
		userlist_remove (sess, user);
	}
}

void
inbound_part (server *serv, char *chan, char *user, char *ip, char *reason,
				  const message_tags_data *tags_data)
{
	session *sess = find_channel (serv, chan);
	if (sess)
	{
		if (*reason)
			EMIT_SIGNAL_TIMESTAMP (XP_TE_PARTREASON, sess, user, ip, chan, reason,
										  0, tags_data->timestamp);
		else
			EMIT_SIGNAL_TIMESTAMP (XP_TE_PART, sess, user, ip, chan, NULL, 0,
										  tags_data->timestamp);
		userlist_remove (sess, user);
	}
}

void
inbound_topictime (server *serv, char *chan, char *nick, time_t stamp,
						 const message_tags_data *tags_data)
{
	char *tim = ctime (&stamp);
	session *sess = find_channel (serv, chan);

	if (!sess)
		sess = serv->server_session;

	if (tim != NULL)
		tim[24] = 0;	/* get rid of the \n */

	EMIT_SIGNAL_TIMESTAMP (XP_TE_TOPICDATE, sess, chan, nick, tim, NULL, 0,
								  tags_data->timestamp);
}

void
inbound_quit (server *serv, char *nick, char *ip, char *reason,
				  const message_tags_data *tags_data)
{
	GSList *list = sess_list;
	session *sess;
	struct User *user;
	int was_on_front_session = FALSE;

	while (list)
	{
		sess = (session *) list->data;
		if (sess->server == serv)
		{
 			if (sess == current_sess)
 				was_on_front_session = TRUE;
			if ((user = userlist_find (sess, nick)))
			{
				EMIT_SIGNAL_TIMESTAMP (XP_TE_QUIT, sess, nick, reason, ip, NULL, 0,
											  tags_data->timestamp);
				userlist_remove_user (sess, user);
			} else if (sess->type == SESS_DIALOG && !serv->p_cmp (sess->channel, nick))
			{
				EMIT_SIGNAL_TIMESTAMP (XP_TE_QUIT, sess, nick, reason, ip, NULL, 0,
											  tags_data->timestamp);
			}
		}
		list = list->next;
	}

	notify_set_offline (serv, nick, was_on_front_session, tags_data);
}

void
inbound_account (server *serv, char *nick, char *account,
					  const message_tags_data *tags_data)
{
	session *sess = NULL;
	GSList *list;

	list = sess_list;
	while (list)
	{
		sess = list->data;
		if (sess->server == serv)
			userlist_set_account (sess, nick, account);
		list = list->next;
	}
}

void
inbound_ping_reply (session *sess, char *timestring, char *from,
						  const message_tags_data *tags_data)
{
	unsigned long tim, nowtim, dif;
	int lag = 0;
	char outbuf[64];

	if (strncmp (timestring, "LAG", 3) == 0)
	{
		timestring += 3;
		lag = 1;
	}

	tim = strtoul (timestring, NULL, 10);
	nowtim = make_ping_time ();
	dif = nowtim - tim;

	sess->server->ping_recv = time (0);

	if (lag)
	{
		sess->server->lag_sent = 0;
		sess->server->lag = dif;
		fe_set_lag (sess->server, dif);
		return;
	}

	if (atol (timestring) == 0)
	{
		if (sess->server->lag_sent)
			sess->server->lag_sent = 0;
		else
			EMIT_SIGNAL_TIMESTAMP (XP_TE_PINGREP, sess, from, "?", NULL, NULL, 0,
										  tags_data->timestamp);
	} else
	{
		g_snprintf (outbuf, sizeof (outbuf), "%ld.%03ld", dif / 1000, dif % 1000);
		EMIT_SIGNAL_TIMESTAMP (XP_TE_PINGREP, sess, from, outbuf, NULL, NULL, 0,
									  tags_data->timestamp);
	}
}

static session *
find_session_from_type (int type, server *serv)
{
	session *sess;
	GSList *list = sess_list;
	while (list)
	{
		sess = list->data;
		if (sess->type == type && serv == sess->server)
			return sess;
		list = list->next;
	}
	return NULL;
}

void
inbound_notice (server *serv, char *to, char *nick, char *msg, char *ip, int id,
					 const message_tags_data *tags_data)
{
	char *ptr = to;
	session *sess = 0;
	int server_notice = FALSE;

	if (is_channel (serv, ptr))
		sess = find_channel (serv, ptr);

	/* /notice [mode-prefix]#channel should end up in that channel */
	if (!sess && ptr[0] && strchr(serv->nick_prefixes, ptr[0]) != NULL)
	{
		ptr++;
		sess = find_channel (serv, ptr);
	}

	if (strcmp (nick, ip) == 0)
		server_notice = TRUE;

	if (!sess)
	{
		ptr = 0;
		if (prefs.hex_irc_notice_pos == 0)
		{
											/* paranoia check */
			if (msg[0] == '[' && (!serv->have_idmsg || id))
			{
				/* guess where chanserv meant to post this -sigh- */
				if (!g_ascii_strcasecmp (nick, "ChanServ") && !find_dialog (serv, nick))
				{
					char *dest = g_strdup (msg + 1);
					char *end = strchr (dest, ']');
					if (end)
					{
						*end = 0;
						sess = find_channel (serv, dest);
					}
					g_free (dest);
				}
			}
			if (!sess)
				sess = find_session_from_nick (nick, serv);
		} else if (prefs.hex_irc_notice_pos == 1)
		{
			int stype = server_notice ? SESS_SNOTICES : SESS_NOTICES;
			sess = find_session_from_type (stype, serv);
			if (!sess)
			{
				if (stype == SESS_NOTICES)
					sess = new_ircwindow (serv, "(notices)", SESS_NOTICES, 0);
				else
					sess = new_ircwindow (serv, "(snotices)", SESS_SNOTICES, 0);
				fe_set_channel (sess);
				fe_set_title (sess);
				fe_set_nonchannel (sess, FALSE);
				userlist_clear (sess);
				log_open_or_close (sess);
			}
			/* Avoid redundancy with some Undernet notices */
			if (!strncmp (msg, "*** Notice -- ", 14))
				msg += 14;
		} else
		{
			sess = serv->front_session;
		}

		if (!sess)
		{
			if (server_notice)	
				sess = serv->server_session;
			else
				sess = serv->front_session;
		}
	}

	if (msg[0] == '\001')
	{
		size_t len;

		msg++;
		if (!strncmp (msg, "PING", 4))
		{
			inbound_ping_reply (sess, msg + 5, nick, tags_data);
			return;
		}

		len = strlen(msg);
		if (msg[len - 1] == '\001')
			msg[len - 1] = '\000';
	}

	if (server_notice)
		EMIT_SIGNAL_TIMESTAMP (XP_TE_SERVNOTICE, sess, msg, nick, NULL, NULL, 0,
									  tags_data->timestamp);
	else if (ptr)
		EMIT_SIGNAL_TIMESTAMP (XP_TE_CHANNOTICE, sess, nick, to, msg, NULL, 0,
									  tags_data->timestamp);
	else
		EMIT_SIGNAL_TIMESTAMP (XP_TE_NOTICE, sess, nick, msg, NULL, NULL, 0,
									  tags_data->timestamp);
}

void
inbound_away (server *serv, char *nick, char *msg,
				  const message_tags_data *tags_data)
{
	struct away_msg *away = server_away_find_message (serv, nick);
	session *sess = NULL;
	GSList *list;

	if (away && !strcmp (msg, away->message))	/* Seen the msg before? */
	{
		if (prefs.hex_away_show_once && !serv->inside_whois)
			return;
	} else
	{
		server_away_save_message (serv, nick, msg);
	}

	if (prefs.hex_irc_whois_front)
		sess = serv->front_session;
	else
	{
		if (!serv->inside_whois)
			sess = find_session_from_nick (nick, serv);
		if (!sess)
			sess = serv->server_session;
	}

	/* possibly hide the output */
	if (!serv->inside_whois || !serv->skip_next_whois)
		EMIT_SIGNAL_TIMESTAMP (XP_TE_WHOIS5, sess, nick, msg, NULL, NULL, 0,
									  tags_data->timestamp);

	list = sess_list;
	while (list)
	{
		sess = list->data;
		if (sess->server == serv)
			userlist_set_away (sess, nick, TRUE);
		list = list->next;
	}
}

void
inbound_away_notify (server *serv, char *nick, char *reason,
							const message_tags_data *tags_data)
{
	session *sess = NULL;
	GSList *list;

	list = sess_list;
	while (list)
	{
		sess = list->data;
		if (sess->server == serv)
		{
			userlist_set_away (sess, nick, reason ? TRUE : FALSE);
			if (sess == serv->front_session && notify_is_in_list (serv, nick))
			{
				if (reason)
					EMIT_SIGNAL_TIMESTAMP (XP_TE_NOTIFYAWAY, sess, nick, reason, NULL,
												  NULL, 0, tags_data->timestamp);
				else
					EMIT_SIGNAL_TIMESTAMP (XP_TE_NOTIFYBACK, sess, nick, NULL, NULL, 
												  NULL, 0, tags_data->timestamp);
			}
		}
		list = list->next;
	}
}

int
inbound_nameslist_end (server *serv, char *chan,
							  const message_tags_data *tags_data)
{
	session *sess;
	GSList *list;

	if (!strcmp (chan, "*"))
	{
		list = sess_list;
		while (list)
		{
			sess = list->data;
			if (sess->server == serv)
			{
				sess->end_of_names = TRUE;
				sess->ignore_names = FALSE;
				fe_userlist_numbers (sess);
			}
			list = list->next;
		}
		return TRUE;
	}
	sess = find_channel (serv, chan);
	if (sess)
	{
		sess->end_of_names = TRUE;
		sess->ignore_names = FALSE;
		fe_userlist_numbers (sess);
		return TRUE;
	}
	return FALSE;
}

static gboolean
check_autojoin_channels (server *serv)
{
	int i = 0;
	session *sess;
	GSList *list = sess_list;
	GSList *sess_channels = NULL;			/* joined channels that are not in the favorites list */
	favchannel *fav;

	/* shouldn't really happen, the io tag is destroyed in server.c */
	if (!is_server (serv))
	{
		return FALSE;
	}

	/* If there's a session (i.e. this is a reconnect), autojoin to everything that was open previously. */
	while (list)
	{
		sess = list->data;

		if (sess->server == serv)
		{
			if (sess->willjoinchannel[0] != 0)
			{
				strcpy (sess->waitchannel, sess->willjoinchannel);
				sess->willjoinchannel[0] = 0;

				fav = servlist_favchan_find (serv->network, sess->waitchannel, NULL);	/* Is this channel in our favorites? */

				/* session->channelkey is initially unset for channels joined from the favorites. You have to fill them up manually from favorites settings. */
				if (fav)
				{
					/* session->channelkey is set if there was a key change during the session. In that case, use the session key, not the one from favorites. */
					if (fav->key && !strlen (sess->channelkey))
					{
						safe_strcpy (sess->channelkey, fav->key, sizeof (sess->channelkey));
					}
				}

				/* for easier checks, ensure that favchannel->key is just NULL when session->channelkey is empty i.e. '' */
				if (strlen (sess->channelkey))
				{
					sess_channels = servlist_favchan_listadd (sess_channels, sess->waitchannel, sess->channelkey);
				}
				else
				{
					sess_channels = servlist_favchan_listadd (sess_channels, sess->waitchannel, NULL);
				}
				i++;
			}
		}

		list = list->next;
	}

	if (sess_channels)
	{
		serv->p_join_list (serv, sess_channels);
		g_slist_free_full (sess_channels, (GDestroyNotify) servlist_favchan_free);
	}
	else
	{
		/* If there's no session, just autojoin to favorites. */
		if (serv->favlist)
		{
			serv->p_join_list (serv, serv->favlist);
			i++;

			/* FIXME this is not going to work and is not needed either. server_free() does the job already. */
			/* g_slist_free_full (serv->favlist, (GDestroyNotify) servlist_favchan_free); */
		}
	}

	serv->joindelay_tag = 0;
	fe_server_event (serv, FE_SE_LOGGEDIN, i);
	return FALSE;
}

void
inbound_next_nick (session *sess, char *nick, int error,
						 const message_tags_data *tags_data)
{
	char *newnick;
	server *serv = sess->server;
	ircnet *net;

	serv->nickcount++;

	switch (serv->nickcount)
	{
	case 2:
		newnick = prefs.hex_irc_nick2;
		net = serv->network;
		/* use network specific "Second choice"? */
		if (net && !(net->flags & FLAG_USE_GLOBAL) && net->nick2)
		{
			newnick = net->nick2;
		}
		serv->p_change_nick (serv, newnick);
		if (error)
		{
			EMIT_SIGNAL_TIMESTAMP (XP_TE_NICKERROR, sess, nick, newnick, NULL, NULL,
										  0, tags_data->timestamp);
		}
		else
		{
			EMIT_SIGNAL_TIMESTAMP (XP_TE_NICKCLASH, sess, nick, newnick, NULL, NULL,
										  0, tags_data->timestamp);
		}
		break;

	case 3:
		serv->p_change_nick (serv, prefs.hex_irc_nick3);
		if (error)
		{
			EMIT_SIGNAL_TIMESTAMP (XP_TE_NICKERROR, sess, nick, prefs.hex_irc_nick3,
										  NULL, NULL, 0, tags_data->timestamp);
		}
		else
		{
			EMIT_SIGNAL_TIMESTAMP (XP_TE_NICKCLASH, sess, nick, prefs.hex_irc_nick3,
										  NULL, NULL, 0, tags_data->timestamp);
		}
		break;

	default:
		EMIT_SIGNAL_TIMESTAMP (XP_TE_NICKFAIL, sess, NULL, NULL, NULL, NULL, 0,
									  tags_data->timestamp);
	}
}


static void
dns_addr_callback (GObject *obj, GAsyncResult *result, gpointer user_data)
{
	GResolver *resolver = G_RESOLVER(obj);
	session *sess = (session*)user_data;
	gchar *addr;

	g_return_if_fail (is_session(sess));

	addr = g_resolver_lookup_by_address_finish (resolver, result, NULL);
	if (addr)
		PrintTextf (sess, _("Resolved to %s"), addr);
	else
		PrintText (sess, _("Not found"));
}

static void
dns_name_callback (GObject *obj, GAsyncResult *result, gpointer user_data)
{
	GResolver *resolver = G_RESOLVER(obj);
	session *sess = (session*)user_data;
	GList* addrs;
	gchar* addr;
	GList* list;

	g_return_if_fail (is_session (sess));

	addrs = g_resolver_lookup_by_name_finish (resolver, result, NULL);
	if (addrs)
	{
		PrintText (sess, _("Resolved to:"));

		for (list = g_list_first (addrs); list; list = g_list_next (list))
		{
			addr = g_inet_address_to_string (list->data);
			PrintTextf (sess, "    %s", addr);
		}

		g_resolver_free_addresses (addrs);
	}
	else
		PrintText (sess, _("Not found"));
}

void
do_dns (session *sess, char *nick, char *host,
		const message_tags_data *tags_data)
{
	GResolver *res = g_resolver_get_default ();
	GInetAddress *addr;
	char *po;

	po = strrchr (host, '@');
	if (po)
		host = po + 1;

	if (nick)
		EMIT_SIGNAL_TIMESTAMP (XP_TE_RESOLVINGUSER, sess, nick, host, NULL, NULL, 0,
								tags_data->timestamp);

	PrintTextf (sess, _("Looking up %s..."), host);

	addr = g_inet_address_new_from_string (host);
	if (addr)
		g_resolver_lookup_by_address_async (res, addr, NULL, dns_addr_callback, sess);
	else
		g_resolver_lookup_by_name_async (res, host, NULL, dns_name_callback, sess);
}

static void
set_default_modes (server *serv)
{
	char modes[8];

	modes[0] = '+';
	modes[1] = '\0';

	if (prefs.hex_irc_wallops)
		strcat (modes, "w");
	if (prefs.hex_irc_servernotice)
		strcat (modes, "s");
	if (prefs.hex_irc_invisible)
		strcat (modes, "i");
	if (prefs.hex_irc_hidehost)
		strcat (modes, "x");

	if (modes[1] != '\0')
	{
		serv->p_mode (serv, serv->nick, modes);
	}
}

void
inbound_login_start (session *sess, char *nick, char *servname,
							const message_tags_data *tags_data)
{
	inbound_newnick (sess->server, sess->server->nick, nick, TRUE, tags_data);
	server_set_name (sess->server, servname);
	if (sess->type == SESS_SERVER)
		log_open_or_close (sess);
	/* reset our away status */
	if (sess->server->reconnect_away)
	{
		handle_command (sess->server->server_session, "away", FALSE);
		sess->server->reconnect_away = FALSE;
	}
}

static void
inbound_set_all_away_status (server *serv, char *nick, unsigned int status)
{
	GSList *list;
	session *sess;

	list = sess_list;
	while (list)
	{
		sess = list->data;
		if (sess->server == serv)
			userlist_set_away (sess, nick, status);
		list = list->next;
	}
}

void
inbound_uaway (server *serv, const message_tags_data *tags_data)
{
	serv->is_away = TRUE;
	serv->away_time = time (NULL);
	fe_set_away (serv);

	inbound_set_all_away_status (serv, serv->nick, 1);
}

void
inbound_uback (server *serv, const message_tags_data *tags_data)
{
	serv->is_away = FALSE;
	serv->reconnect_away = FALSE;
	fe_set_away (serv);

	inbound_set_all_away_status (serv, serv->nick, 0);
}

void
inbound_foundip (session *sess, char *ip, const message_tags_data *tags_data)
{
	struct in_addr addr;

	if (net_lookup_ipv4 (ip, &addr.s_addr))
	{
		sess->server->dcc_ip = addr.s_addr;
		EMIT_SIGNAL_TIMESTAMP (XP_TE_FOUNDIP, sess->server->server_session,
									  net_ip (ntohl (addr.s_addr)),
									  NULL, NULL, NULL, 0, tags_data->timestamp);
	}
}

void
inbound_user_info_start (session *sess, char *nick,
								 const message_tags_data *tags_data)
{
	/* set away to FALSE now, 301 may turn it back on */
	inbound_set_all_away_status (sess->server, nick, 0);
}

/* reporting new information found about this user. chan may be NULL.
 * away may be 0xff to indicate UNKNOWN. */

void
inbound_user_info (session *sess, char *chan, char *user, char *host,
						 char *servname, char *nick, char *realname,
						 char *account, unsigned int away,
						 const message_tags_data *tags_data)
{
	server *serv = sess->server;
	session *who_sess;
	GSList *list;
	char *uhost = NULL;

	if (user && host)
	{
		uhost = g_strdup_printf ("%s@%s", user, host);
	}

	if (chan)
	{
		who_sess = find_channel (serv, chan);
		if (who_sess)
			userlist_add_hostname (who_sess, nick, uhost, realname, servname, account, away);
		else
		{
			if (serv->doing_dns && nick && host)
				do_dns (sess, nick, host, tags_data);
		}
	}
	else
	{
		/* came from WHOIS, not channel specific */
		for (list = sess_list; list; list = list->next)
		{
			sess = list->data;
			if (sess->server != serv)
				continue;

			if (sess->type == SESS_CHANNEL)
			{
				userlist_add_hostname (sess, nick, uhost, realname, servname, account, away);
			}
			else if (sess->type == SESS_DIALOG && uhost && !serv->p_cmp (sess->channel, nick))
			{
				set_topic (sess, uhost, uhost);
			}
		}
	}

	g_free (uhost);
}

int
inbound_banlist (session *sess, time_t stamp, char *chan, char *mask, 
					  char *banner, int rplcode, const message_tags_data *tags_data)
{
	char *time_str = ctime (&stamp);
	server *serv = sess->server;
	char *nl;

	if (stamp <= 0 || time_str == NULL)
	{
		time_str = "";
	}
	else
	{
		if ((nl = strchr (time_str, '\n')))
			*nl = 0;
	}

	sess = find_channel (serv, chan);
	if (!sess)
	{
		sess = serv->front_session;
		goto nowindow;
	}

	if (!fe_add_ban_list (sess, mask, banner, time_str, rplcode))
	{
nowindow:

		EMIT_SIGNAL_TIMESTAMP (XP_TE_BANLIST, sess, chan, mask, banner, time_str,
									  0, tags_data->timestamp);
		return TRUE;
	}

	return TRUE;
}

/* execute 1 end-of-motd command */

static int
inbound_exec_eom_cmd (char *str, void *sess)
{
	char *cmd;

	cmd = command_insert_vars ((session*)sess, (str[0] == '/') ? str + 1 : str);
	handle_command ((session*)sess, cmd, TRUE);
	g_free (cmd);

	return 1;
}

static int
inbound_nickserv_login (server *serv)
{
	/* this could grow ugly, but let's hope there won't be new NickServ types */
	switch (serv->loginmethod)
	{
		case LOGIN_MSG_NICKSERV:
		case LOGIN_NICKSERV:
		case LOGIN_CHALLENGEAUTH:
#if 0
		case LOGIN_NS:
		case LOGIN_MSG_NS:
		case LOGIN_AUTH:
#endif
			return 1;
		default:
			return 0;
	}
}

void
inbound_login_end (session *sess, char *text, const message_tags_data *tags_data)
{
	GSList *cmdlist;
	commandentry *cmd;
	server *serv = sess->server;
	ircnet *net = serv->network;

	if (!serv->end_of_motd)
	{
		if (prefs.hex_dcc_ip_from_server && serv->use_who)
		{
			serv->skip_next_userhost = TRUE;
			serv->p_get_ip_uh (serv, serv->nick);	/* sends USERHOST mynick */
		}
		set_default_modes (serv);

		if (net)
		{
			/* there may be more than 1, separated by \n */

			cmdlist = net->commandlist;
			while (cmdlist)
			{
				cmd = cmdlist->data;
				inbound_exec_eom_cmd (cmd->command, sess);
				cmdlist = cmdlist->next;
			}
		}
		/* The previously ran commands can alter the state of the server */
		if (serv->network != net)
			return;

		/* send nickserv password */
		if (net && serv->password[0] && inbound_nickserv_login (serv))
		{
			serv->p_ns_identify (serv, serv->password);
		}

		/* wait for join if command or nickserv set */
		if (net && prefs.hex_irc_join_delay
			&& ((serv->password[0] && inbound_nickserv_login (serv))
				|| net->commandlist))
		{
			serv->joindelay_tag = fe_timeout_add_seconds (prefs.hex_irc_join_delay, check_autojoin_channels, serv);
		}
		else
		{
			check_autojoin_channels (serv);
		}

		if (serv->supports_watch || serv->supports_monitor)
		{
			notify_send_watches (serv);
		}

		serv->end_of_motd = TRUE;
	}

	if (prefs.hex_irc_skip_motd && !serv->motd_skipped)
	{
		serv->motd_skipped = TRUE;
		EMIT_SIGNAL_TIMESTAMP (XP_TE_MOTDSKIP, serv->server_session, NULL, NULL,
									  NULL, NULL, 0, tags_data->timestamp);
		return;
	}

	EMIT_SIGNAL_TIMESTAMP (XP_TE_MOTD, serv->server_session, text, NULL, NULL,
								  NULL, 0, tags_data->timestamp);
}

void
inbound_identified (server *serv)	/* 'MODE +e MYSELF' on freenode */
{
	if (serv->joindelay_tag)
	{
		/* stop waiting, just auto JOIN now */
		fe_timeout_remove (serv->joindelay_tag);
		serv->joindelay_tag = 0;
		check_autojoin_channels (serv);
	}
}

static const char *sasl_mechanisms[] =
{
	"PLAIN",
	"EXTERNAL",
	"SCRAM-SHA-1",
	"SCRAM-SHA-256",
	"SCRAM-SHA-512"
};

static void
inbound_toggle_caps (server *serv, const char *extensions_str, gboolean enable)
{
	char **extensions;
	gsize i;

	extensions = g_strsplit (extensions_str, " ", 0);

	for (i = 0; extensions[i]; i++)
	{
		const char *extension = extensions[i];

		if (!strcmp (extension, "solanum.chat/identify-msg"))
			serv->have_idmsg = enable;
		else if (!strcmp (extension, "multi-prefix"))
			serv->have_namesx = enable;
		else if (!strcmp (extension, "account-notify"))
			serv->have_accnotify = enable;
		else if (!strcmp (extension, "extended-join"))
			serv->have_extjoin = enable;
		else if (!strcmp (extension, "userhost-in-names"))
			serv->have_uhnames = enable;
		else if (!strcmp (extension, "server-time")
				|| !strcmp (extension, "znc.in/server-time")
				|| !strcmp (extension, "znc.in/server-time-iso"))
			serv->have_server_time = enable;
		else if (!strcmp (extension, "away-notify"))
			serv->have_awaynotify = enable;
		else if (!strcmp (extension, "account-tag"))
			serv->have_account_tag = enable;
		else if (!strcmp (extension, "message-tags"))
			serv->have_message_tags = enable;
		else if (!strcmp (extension, "echo-message"))
			serv->have_echo_message = enable;
		else if (!strcmp (extension, "sasl"))
		{
			serv->have_sasl = enable;
			if (enable)
			{
#ifdef USE_OPENSSL
				if (serv->loginmethod == LOGIN_SASLEXTERNAL)
					serv->sasl_mech = MECH_EXTERNAL;
				else if (serv->loginmethod == LOGIN_SASL_SCRAM_SHA_1)
					serv->sasl_mech = MECH_SCRAM_SHA_1;
				else if (serv->loginmethod == LOGIN_SASL_SCRAM_SHA_256)
					serv->sasl_mech = MECH_SCRAM_SHA_256;
				else if (serv->loginmethod == LOGIN_SASL_SCRAM_SHA_512)
					serv->sasl_mech = MECH_SCRAM_SHA_512;
#endif
				/* Mechanism either defaulted to PLAIN or server gave us list */
				tcp_sendf (serv, "AUTHENTICATE %s\r\n", sasl_mechanisms[serv->sasl_mech]);
			}
		}
	}

	g_strfreev (extensions);
}

void
inbound_cap_ack (server *serv, char *nick, char *extensions,
					  const message_tags_data *tags_data)
{
	if (extensions)
	{
		char **tokens = g_strsplit (extensions, " ", 0);
		int i;

		for (i = 0; tokens[i]; i++)
		{
			char **parts = g_strsplit (tokens[i], "=", 2);

			if (!g_strcmp0 (parts[0], "sts") && parts[1] && parts[1][0])
			{
				sts_handle_capability (serv, parts[1]);
			}

			g_strfreev (parts);
		}

		g_strfreev (tokens);
	}

	EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPACK, serv->server_session, nick, extensions,
								  NULL, NULL, 0, tags_data->timestamp);

	inbound_toggle_caps (serv, extensions, TRUE);
}

void
inbound_cap_new (server *serv, char *nick, char *extensions,
					 const message_tags_data *tags_data)
{
	if (extensions)
	{
		char **tokens = g_strsplit (extensions, " ", 0);
		int i;

		for (i = 0; tokens[i]; i++)
		{
			char **parts = g_strsplit (tokens[i], "=", 2);

			if (!g_strcmp0 (parts[0], "sts") && parts[1] && parts[1][0])
			{
				sts_handle_capability (serv, parts[1]);
			}

			g_strfreev (parts);
		}

		g_strfreev (tokens);
	}

	EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPACK, serv->server_session, nick, extensions,
								  NULL, NULL, 0, tags_data->timestamp);

	inbound_toggle_caps (serv, extensions, TRUE);
}

void
inbound_cap_del (server *serv, char *nick, char *extensions,
					 const message_tags_data *tags_data)
{
	if (extensions)
	{
		char **tokens = g_strsplit (extensions, " ", 0);
		int i;

		for (i = 0; tokens[i]; i++)
		{
			if (!g_strcmp0 (tokens[i], "sts") ||
				g_str_has_prefix (tokens[i], "sts="))
			{
				/* STS cannot be disabled via CAP DEL. */
				g_strfreev (tokens);
				return;
			}
		}

		g_strfreev (tokens);
	}

	EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPDEL, serv->server_session, nick, extensions,
								  NULL, NULL, 0, tags_data->timestamp);

	inbound_toggle_caps (serv, extensions, FALSE);
}

static const char * const supported_caps[] = {
	/* IRCv3.1 */
	"multi-prefix",
	"away-notify",
	"account-notify",
	"extended-join",
	/* "sasl", Handled manually */

	/* IRCv3.2 */
	"server-time",
	"userhost-in-names",
	"cap-notify",
	"chghost",
	"setname",
	"invite-notify",
	"account-tag",
	"extended-monitor",
	"standard-replies",
	"message-tags",
	"echo-message",

	/* ZNC */
	"znc.in/server-time-iso",
	"znc.in/server-time",

	/* Twitch */
	"twitch.tv/membership",

	/* Solanum */
	"solanum.chat/identify-msg",
};

static int
get_supported_mech (server *serv, const char *list)
{
	char **mechs = g_strsplit (list, ",", 0);
	gsize i;
	int ret = -1;

	for (i = 0; mechs[i]; ++i)
	{
#ifdef USE_OPENSSL
		if (serv->loginmethod == LOGIN_SASLEXTERNAL)
		{
			if (!strcmp (mechs[i], "EXTERNAL"))
			{
				ret = MECH_EXTERNAL;
				break;
			}
		}
		else if (serv->loginmethod == LOGIN_SASL_SCRAM_SHA_1)
		{
			if (!strcmp(mechs[i], "SCRAM-SHA-1"))
			{
				ret = MECH_SCRAM_SHA_1;
				break;
			}
		}
		else if (serv->loginmethod == LOGIN_SASL_SCRAM_SHA_256)
		{
			if (!strcmp(mechs[i], "SCRAM-SHA-256"))
			{
				ret = MECH_SCRAM_SHA_256;
				break;
			}
		}
		else if (serv->loginmethod == LOGIN_SASL_SCRAM_SHA_512)
		{
			if (!strcmp(mechs[i], "SCRAM-SHA-512"))
			{
				ret = MECH_SCRAM_SHA_512;
				break;
			}
        }
		else
#endif
		if (!strcmp (mechs[i], "PLAIN"))
		{
			ret = MECH_PLAIN;
			break;
		}
	}

	g_strfreev (mechs);
	return ret;
}

void
inbound_cap_ls (server *serv, char *nick, char *extensions_str,
					 const message_tags_data *tags_data)
{
	char buffer[500];	/* buffer for requesting capabilities and emitting the signal */
	gboolean want_cap = FALSE; /* format the CAP REQ string based on previous capabilities being requested or not */
	gboolean sts_upgrade_triggered = FALSE;
	char **extensions;
	int i;

	if (g_str_has_prefix (extensions_str, "* "))
	{
		serv->waiting_on_cap = TRUE;
		extensions_str += 2;
		extensions_str += extensions_str[0] == ':' ? 1 : 0;
	}
	else
	{
		serv->waiting_on_cap = FALSE;
	}

	EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPLIST, serv->server_session, nick,
								  extensions_str, NULL, NULL, 0, tags_data->timestamp);

	extensions = g_strsplit (extensions_str, " ", 0);

	strcpy (buffer, "CAP REQ :");

	for (i=0; extensions[i]; i++)
	{
		char *extension = extensions[i];
		char *value;
		gsize x;

		/* CAP 3.2 can provide values */
		if ((value = strchr (extension, '=')))
		{
			*value = '\0';
			value++;
		}

		if (!g_strcmp0 (extension, "sts"))
		{
			if (value)
			{
				sts_upgrade_triggered |= sts_handle_capability (serv, value);
			}
			else
			{
				PrintTextf (serv->server_session,
							_("Invalid STS capability token without a value; ignoring."));
			}
			continue;
		}

		/* if the SASL password is set AND auth mode is set to SASL, request SASL auth */
		if (!g_strcmp0 (extension, "sasl") &&
			(((serv->loginmethod == LOGIN_SASL
				|| serv->loginmethod == LOGIN_SASL_SCRAM_SHA_1
				|| serv->loginmethod == LOGIN_SASL_SCRAM_SHA_256
				|| serv->loginmethod == LOGIN_SASL_SCRAM_SHA_512)
					&& strlen (serv->password) != 0)
				|| serv->loginmethod == LOGIN_SASLEXTERNAL))
		{
			if (value)
			{
				int sasl_mech = get_supported_mech (serv, value);
				if (sasl_mech == -1) /* No supported mech */
					continue;
				serv->sasl_mech = sasl_mech;
			}
			want_cap = TRUE;
			serv->waiting_on_sasl = TRUE;
			g_strlcat (buffer, "sasl ", sizeof(buffer));
			continue;
		}

		for (x = 0; x < G_N_ELEMENTS(supported_caps); ++x)
		{
			if (!g_strcmp0 (extension, supported_caps[x]))
			{
				g_strlcat (buffer, extension, sizeof(buffer));
				g_strlcat (buffer, " ", sizeof(buffer));
				want_cap = TRUE;
			}
		}
	}

	g_strfreev (extensions);

	if (sts_upgrade_triggered)
	{
		return;
	}

	if (want_cap)
	{
		/* buffer + 9 = emit buffer without "CAP REQ :" */
		EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPREQ, serv->server_session,
									  buffer + 9, NULL, NULL, NULL, 0,
									  tags_data->timestamp);
		tcp_sendf (serv, "%s\r\n", g_strchomp (buffer));
	}
	if (!serv->waiting_on_sasl && !serv->waiting_on_cap)
	{
		/* if we use SASL, CAP END is dealt via raw numerics */
		serv->sent_capend = TRUE;
		tcp_send_len (serv, "CAP END\r\n", 9);
	}
}

void
inbound_cap_nak (server *serv, char *extensions_str, const message_tags_data *tags_data)
{
	char **extensions;
	int i;

	extensions = g_strsplit (extensions_str, " ", 0);
	for (i=0; extensions[i]; i++)
	{
		if (!g_strcmp0 (extensions[i], "sasl"))
			serv->waiting_on_sasl = FALSE;
	}

	if (!serv->waiting_on_cap && !serv->waiting_on_sasl && !serv->sent_capend)
	{
		serv->sent_capend = TRUE;
		tcp_send_len (serv, "CAP END\r\n", 9);
	}

	g_strfreev (extensions);
}

void
inbound_cap_list (server *serv, char *nick, char *extensions,
						const message_tags_data *tags_data)
{
	if (g_str_has_prefix (extensions, "* "))
	{
		extensions += 2;
		extensions += extensions[0] == ':' ? 1 : 0;
	}
	EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPACK, serv->server_session, nick, extensions,
								  NULL, NULL, 0, tags_data->timestamp);
}

static void
plain_authenticate (server *serv, char *user, char *password)
{
	char *pass = encode_sasl_pass_plain (user, password);

	if (pass == NULL)
	{
		/* something went wrong abort */
		tcp_sendf (serv, "AUTHENTICATE *\r\n");
		return;
	}

	/* long SASL passwords must be split into 400-byte chunks
	   https://ircv3.net/specs/extensions/sasl-3.1#the-authenticate-command */
	size_t pass_len = strlen (pass);
	if (pass_len <= 400)
		tcp_sendf (serv, "AUTHENTICATE %s\r\n", pass);
	else
	{
		size_t sent = 0;
		while (sent < pass_len)
		{
			char *pass_chunk = g_strndup (pass + sent, 400);
			tcp_sendf (serv, "AUTHENTICATE %s\r\n", pass_chunk);
			sent += 400;
			g_free (pass_chunk);
		}
	}
	if (pass_len % 400 == 0)
		tcp_sendf (serv, "AUTHENTICATE +\r\n");
}

#ifdef USE_OPENSSL
/*
 * Sends AUTHENTICATE messages to log in via SCRAM.
 */
static void
scram_authenticate (server *serv, const char *data, const char *digest,
					const char *user, const char *password)
{
	char *encoded, *decoded, *output;
	scram_status status;
	size_t output_len;
	gsize decoded_len;

	if (serv->scram_session == NULL)
	{
		serv->scram_session = scram_session_create (digest, user, password);

		if (serv->scram_session == NULL)
		{
			PrintTextf (serv->server_session, _("Could not create SCRAM session with digest %s"), digest);
			g_warning ("Could not create SCRAM session with digest %s", digest);
			tcp_sendf (serv, "AUTHENTICATE *\r\n");
			return;
		}
	}

	decoded = g_base64_decode (data, &decoded_len);
	status = scram_process (serv->scram_session, decoded, &output, &output_len);
	g_free (decoded);

	if (status == SCRAM_IN_PROGRESS)
	{
		encoded = g_base64_encode ((guchar *) output, output_len);
		tcp_sendf (serv, "AUTHENTICATE %s\r\n", encoded);
		g_free (encoded);
		g_free (output);
	}
	else if (status == SCRAM_SUCCESS)
	{
		tcp_sendf (serv, "AUTHENTICATE +\r\n");
		g_clear_pointer (&serv->scram_session, scram_session_free);
	}
	else if (status == SCRAM_ERROR)
	{
		tcp_sendf (serv, "AUTHENTICATE *\r\n");

		if (serv->scram_session->error != NULL)
		{
			PrintTextf (serv->server_session, _("SASL SCRAM authentication failed: %s"), serv->scram_session->error);
			g_info ("SASL SCRAM authentication failed: %s", serv->scram_session->error);
		}

		g_clear_pointer (&serv->scram_session, scram_session_free);
	}
}
#endif

void
inbound_sasl_authenticate (server *serv, char *data)
{
		ircnet *net = (ircnet*)serv->network;
		char *user;
		const char *mech = sasl_mechanisms[serv->sasl_mech];

		/* Got a list of supported mechanisms from outdated inspircd
		 * just ignore it as it goes against spec */
		if (strchr (data, ',') != NULL)
			return;

		if (net->user && !(net->flags & FLAG_USE_GLOBAL))
			user = net->user;
		else
			user = prefs.hex_irc_user_name;

		switch (serv->sasl_mech)
		{
		case MECH_PLAIN:
			plain_authenticate(serv, user, serv->password);
			break;
#ifdef USE_OPENSSL
		case MECH_EXTERNAL:
			tcp_sendf (serv, "AUTHENTICATE +\r\n");
			break;
		case MECH_SCRAM_SHA_1:
			scram_authenticate(serv, data, "SHA1", user, serv->password);
			return;
		case MECH_SCRAM_SHA_256:
			scram_authenticate(serv, data, "SHA256", user, serv->password);
			return;
		case MECH_SCRAM_SHA_512:
			scram_authenticate(serv, data, "SHA512", user, serv->password);
			return;
#endif
		}

		EMIT_SIGNAL_TIMESTAMP (XP_TE_SASLAUTH, serv->server_session, user, (char*)mech,
								NULL,	NULL,	0,	0);
}

void
inbound_sasl_error (server *serv)
{
#ifdef USE_OPENSSL
    g_clear_pointer (&serv->scram_session, scram_session_free);
#endif
	/* Just abort, not much we can do */
	tcp_sendf (serv, "AUTHENTICATE *\r\n");
}
