/* User, channel and server routines.

   Cygnus IRC Services - Copyright (c) 2001-2002 Darcy Grexton
   Contact: skold@habber.net, skold @ HabberNet

   See doc/LICENSE for licensing details.
 */

#include "../inc/services.h"

Channel *chanlist[HASHSIZE];
User *userlist[HASHSIZE];
Server *servlist[HASHSIZE];
Clone *clonelist[HASHSIZE];

static void flush_cmode (struct modedata *md);
static void flush_cmode_callback (Timeout *t);
static User *current_u;
static int next_index_u;
static Channel *current_c;
static int next_index_c;
static Server *current_s;
static int next_index_s;

/* Add a new user into memory. */
static User *new_user (const char *nick)
{
    User *user, **list;

    user = scalloc (sizeof (User), 1);

    if (!nick)
	nick = "";

    strscpy (user->nick, nick, NICKLEN);

    list = &userlist[NHASH(user->nick)];

    user->next = *list;

    if (*list)
	(*list)->prev = user;

    *list = user;

    usercnt++;
    userstat++;

    return user;
}

/* Find a user by nick. Return NULL if user could not be found. */
User *finduser (const char *nick)
{
    User *user;

    user = userlist[NHASH(nick)];
    while (user && stricmp (user->nick, nick))
	user = user->next;
    return user;
}

/* Return the first user in the hash */
User *firstuser ()
{
    next_index_u = 0;

    while (next_index_u < HASHSIZE && current_u == NULL)
	current_u = userlist[next_index_u++];

    return current_u;
}

/* Return the next user in the hash */       
User *nextuser ()
{
    if (current_u)
	current_u = current_u->next;

    if (!current_u && next_index_u < HASHSIZE)
	while (next_index_u < HASHSIZE && current_u == NULL)
	    current_u = userlist[next_index_u++];

    return current_u;
}

/* Add a new server into memory. */
Server *new_server (const char *name)
{
    Server *server, **list;

    server = scalloc (sizeof (Server), 1);

    if (!name)
	name = "";

    strscpy (server->name, name, HOSTLEN);

    list = &servlist[SVHASH(server->name)];

    server->next = *list;

    if (*list)
	(*list)->prev = server;

    *list = server;

    servcnt++;

    if (debuglevel > 1)
	debug ("new_server(): %s", server->name);

    return server;
}

/* Delete a server. */
void delete_server (Server *server)
{
    if (debuglevel > 1)
	debug ("delete_server(): %s", server->name);

    servcnt--;

    free (server->uplink);

    if (server->prev)
	server->prev->next = server->next;
    else
	servlist[SVHASH(server->name)] = server->next;

    if (server->next)
	server->next->prev = server->prev;

    free (server);
}

/* Find a server by name. Return NULL if user could not be found. */
Server *findserv (const char *name)
{
    Server *server;

    server = servlist[SVHASH(name)];

    if (!server)
	return NULL;

    return server;
}
    
/* Return the first server in the hash */
Server *firstserv ()
{
    next_index_s = 0;

    while (next_index_s < HASHSIZE && current_s == NULL)
	current_s = servlist[next_index_s++];

    return current_s;
}

/* Return the next server in the hash */
Server *nextserv ()
{
    if (current_s)
	current_s = current_s->next;

    if (!current_s && next_index_s < HASHSIZE)
	while (next_index_s < HASHSIZE && current_s == NULL)
	    current_s = servlist[next_index_s++];

    return current_s;
}

/* Add a new clone to the list */ 
static Clone *new_clone (const char *host)
{
    Clone *clone, **list;

    clone = scalloc (sizeof (Clone), 1);

    if (!host)
	host = "";

    strscpy (clone->host, host, HOSTLEN);
    list = &clonelist[CLHASH(clone->host)];
    clone->next = *list;

    if (*list)
	(*list)->prev = clone;

    *list = clone;
    return clone;
}

/* Find a Clone. Return NULL if clone could not be found. */
Clone *findclone (const char *host)
{
    Clone *clone;

    clone = clonelist[CLHASH(host)];
    while (clone && stricmp (clone->host, host))
	clone = clone->next;
    return clone;
}

/* Add a clone, creating the structure if needed. */
static void add_clone (User *u)
{
    Clone *clone;
    Trigger *trigger;
    Exception *exception;
    int limit, excepted = 0;

    if (!(clone = findclone (u->host)))
	clone = new_clone (u->host);

    clone->cnt++;

    /* Check if this host is excepted */
    if ((exception = findexcept (u->user, u->host)))
	excepted = 1;

    /* Check for a trigger for this host */
    if (!excepted)
    {
	if ((trigger = findtrig (u->user, u->host)))
	    limit = trigger->limit;
	else
	    limit = maxclones;

	if (clone->cnt > limit)
	{
	    /* If they've broke the max clone limit, akill them */
	    if (maxclonekills && clone->kills >= maxclonekills)
	    {
		AutoKill *akill;
		char *mask;

		mask = smalloc (strlen (u->user) + strlen (u->host) + 2);
		snprintf (mask, strlen (u->user) + strlen (u->host) + 2,
		    "*@%s", u->host);

		/* Ignore it if we already have it. (which we shouldn't) */
		if (is_akilled (mask))
		    return;

		if (akillcnt >= akill_size)
		{
		    if (akill_size < 8)
			akill_size = 8;
		    else if (akill_size >= 16384)
			akill_size = 32767;
		    else
			akill_size *= 2;

		    akills = srealloc (akills, sizeof (*akills) * akill_size);
		}

		akill = &akills[akillcnt];
		akill->mask = sstrdup (mask);
		akill->reason = sstrdup (RS_MAX_CLONES);
		akill->setter = sstrdup (me.name);
		akill->realname = 0;
		akill->set = time (NULL);
		akill->expires = clonekill_time;

		akillcnt++;

		if (ircdtype == BAHAMUT || ircdtype == PROMETHEUS)
		    send_cmd (me.name, "AKILL %s * 0 %s %ld :%s", u->host,
			s_RootServ, time (NULL), akill->reason);
		else
		    send_cmd (me.name, "%s %s * :%s", me.token ? "V" : "AKILL",
			u->host, akill->reason);

		free (mask);
	    }

	    kill_user (s_RootServ, u->nick, RS_MAX_CLONES);

	    if (maxclonekills)
		clone->kills++;
	}
    }
}

/* Remove a clone from a host, deleting it as needed */
static void del_clone (User *u)
{
    Clone *clone;

    if (!(clone = findclone (u->host)))
	return;

    clone->cnt--;

    if (clone->cnt <= 0)
    {
	if (clone->prev)
	    clone->prev->next = clone->next;
	else
	    clonelist[CLHASH(clone->host)] = clone->next;
	if (clone->next)
	    clone->next->prev = clone->prev;

	free (clone);
    }
}

/* Return the Channel structure corresponding to the named channel, or
   NULL if the channel was not found.
 */
Channel *findchan (const char *chan)
{
    Channel *c;

    c = chanlist[CHASH(chan)];

    while (c)
    {
	if (!stricmp (c->name, chan))
	    return c;

	c = c->next;
    }

    return NULL;
}

/* Return the first channel */
Channel *firstchan ()
{
    next_index_c = 0;

    while (next_index_c < HASHSIZE && current_c == NULL)
	current_c = chanlist[next_index_c++];

    return current_c;
}

/* Return the next channel */
Channel *nextchan ()
{
    if (current_c)
        current_c = current_c->next;

    if (!current_c && next_index_c < HASHSIZE)
	while (next_index_c < HASHSIZE && current_c == NULL)
	    current_c = chanlist[next_index_c++];

    return current_c;
}

/* Change a user's nick */
static void change_user_nick (User *user, const char *nick)
{
    User **list;

    if (user->prev)
	user->prev->next = user->next;
    else
	userlist[NHASH(user->nick)] = user->next;
    if (user->next)
	user->next->prev = user->prev;
    user->nick[1] = 0;
    strscpy (user->nick, nick, NICKLEN);
    list = &userlist[NHASH(user->nick)];
    user->next = *list;
    user->prev = NULL;
    if (*list)
	(*list)->prev = user;
    *list = user;
}

/* Handle a server NICK command.
	av[0] = nick
	If a new user:
	   av[1] = hop count
	   av[2] = signon time
	   DreamForge:
		av[3] = username
		av[4] = hostname
		av[5] = server
		av[6] = servicestamp
		av[7] = realname
	   Bahamut:
		av[3] = modes
		av[4] = username
		av[5] = hostname
		av[6] = server
		av[7] = servicestamp
		av[8] = realname
	   Unreal:
		av[3] = username
		av[4] = hostname
		av[5] = server
		av[6] = servicestamp
		av[7] = modes
		av[8] = vhost
		av[9] = realname
	Else:
	  av[1] = time of change
 */
void do_nick (const char *source, int8 ac, char **av)
{
    User *user;

    if (!*source)
    {
	/* This is a new user; create a User structure for it. */
	if (debuglevel > 1)
	    debug ("do_nick(): %s", av[0]);

	/* Check if this user matches any AKills */
	if (ircdtype == BAHAMUT || ircdtype == PROMETHEUS)
	{
	    if (check_akill (av[0], av[4], av[5], av[8]))
		return;
	}
	else
	{
	    if (check_akill (av[0], av[3], av[4], av[7]))
		return;
	}

	/* Allocate User structure and fill it in. */
	user = new_user (av[0]);
	user->timestamp	= atol (av[2]);

	if (ircdtype == BAHAMUT || ircdtype == PROMETHEUS)
	{
	    user->user = sstrdup (av[4]);
	    user->host = sstrdup (av[5]);
	    user->server = sstrdup (av[6]);
	    user->sstamp = atoi (av[7]);
	    user->real = sstrdup (av[8]);

	    av[1] = av[3];
	    do_umode (av[0], 2, av);
	}
	else if (ircdtype == UNREAL3 || ircdtype == UNREAL3_2)
	{
	    user->user = sstrdup (av[3]);
	    user->host = sstrdup (av[4]);
	    user->server = sstrdup (av[5]);
	    user->sstamp = atoi (av[6]);

	    if (fakehost_on == TRUE)
		user->host = sstrdup (av[8]);

	    user->real = sstrdup (av[9]);

	    av[1] = av[7];
	    do_umode (av[0], 2, av);
	}
	else
	{
	    user->user = sstrdup (av[3]);
	    user->host = sstrdup (av[4]);
	    user->server = sstrdup (av[5]);
	    user->sstamp = atoi (av[6]);
	    user->real = sstrdup (av[7]);
	}

	/* If they don't have a servicestamp, give them one. */
	if (!user->sstamp)
	{
	    sstamp++;

	    /* Check if the stamp is really high. We'll roll it over to 1
	       if it is. This isn't as high as it goes, but hey.
	     */
	    if (sstamp >= 2147473600)
	    {
		globops (s_RootServ, "Notice: Servicestamp rolling over to 1. You must be proud ;)");
		sstamp = 1;
	    }

	    user->sstamp = sstamp;

	    send_cmd (me.name, "%s %s +d %u", me.token ? "n" : "SVSMODE", user->nick, sstamp);
	}
	else
	{
	    /* If their stamp is higher than the one we have, set ours to theirs + 1000.
	       This should stop us from giving the same stamp to more than one person.
	     */
	    if (user->sstamp >= sstamp)
		sstamp += (user->sstamp + 1000);
	}

	/* Clone checking stuff */
	if (maxclones)
	    add_clone (user);
    }
    else
    {
	/* An old user changing nicks. */
	user = finduser (source);

	if (!user)
	{
	    if (debuglevel > 1)
		debug ("do_nick(): Got nickchange for nonexistant user %s->%s",
		    source, av[0]);
	    return;
	}

	/* Tell NickServ they've changed nicks */
	cancel_user (user);

	/* Update the timestamp. */
	user->timestamp = atol (av[1]);

	/* If they've identified to a nick, update the timestamp on that nick. */
	if (user->lastnick)
	{
	    NickInfo *ni = findnick (user->lastnick);

	    if (ni)
	        ni->timestamp = user->timestamp;
	}

	if (debuglevel > 1)
	    debug ("do_nick(): %s changed nick to %s", source, av[0]);

	/* If this is a nickname we were waiting for, reclaim it. */
	if (rs.wantnick)
	    if (!stricmp (source, rs.nick))
	    {
		send_cmd (s_RootServ, "%s %s", me.token ? "&" : "NICK",
		    rs.nick);
		strscpy (s_RootServ, rs.nick, sizeof (s_RootServ));
		rs.wantnick = 0;
	    }

	if (ns.wantnick)
	    if (!stricmp (source, ns.nick))
	    {
		send_cmd (s_NickServ, "%s %s", me.token ? "&" : "NICK",
		    ns.nick);
		strscpy (s_NickServ, ns.nick, sizeof (s_NickServ));
		ns.wantnick = 0;
	    }

	if (cs.wantnick)
	    if (!stricmp (source, cs.nick))
	    {
		send_cmd (s_ChanServ, "%s %s", me.token ? "&" : "NICK",
		    cs.nick);
		strscpy (s_ChanServ, cs.nick, sizeof (s_ChanServ));
		cs.wantnick = 0;
	    }

	if (ms.wantnick)
	    if (!stricmp (source, ms.nick))
	    {
		send_cmd (s_MemoServ, "%s %s", me.token ? "&" : "NICK",
		    ms.nick);
		strscpy (s_MemoServ, ms.nick, sizeof (s_MemoServ));
		ms.wantnick = 0;
	    }

	if (gn.wantnick)
	    if (!stricmp (source, gn.nick))
	    {
		send_cmd (s_GlobalNoticer, "%s %s", me.token ? "&" : "NICK",
		    gn.nick);
		strscpy (s_GlobalNoticer, gn.nick, sizeof (s_GlobalNoticer));
		gn.wantnick = 0;
	    }

	change_user_nick (user, av[0]);
    }

    if (!bursting && finduser (user->nick))
	validate_user (user);
}

/* Handle a user modechange - This currently only cares about
   umode o, since we don't need any others.
 */
void do_umode (const char *source, int8 ac, char **av)
{
    User *user;
    char *s;
    int add = 1;

    if (stricmp (source, av[0]))
	return;

    user = finduser (source);

    if (!user)
    {
	if (debuglevel > 1)
	    debug ("MODE for nonexistant user %s: %s", source, av[1]);
	return;
    }

    if (debuglevel > 1)
	debug ("do_umode(): %s %s", av[0], av[1]);

    s = av[1]; 

    while (*s)
    {
	switch (*s++)
	{
	    case '+':
		add = 1;
		break;
	    case '-':
		add = 0;
		break;
	    case 'o':
		add ? (user->mode |= UMODE_o) : (user->mode &= ~UMODE_o);

		if (!add && (user->flags & UF_SRA))
		    user->flags &= ~UF_SRA;

		if (is_csop (user))
		{
		    if (add)
		    {
			user->mode |= UMODE_a;
			send_cmd (s_NickServ, "%s %s +a", me.token ? "n" : "SVSMODE", user->nick);
		    }
		    else
		    {
			user->mode &= ~UMODE_a;
			send_cmd (s_NickServ, "%s %s -a", me.token ? "n" : "SVSMODE", user->nick);
		    }
		}

		break;
	    case 'r':
		add ? (user->mode |= UMODE_r) : (user->mode &= ~UMODE_r);
		break;
	    case 'A':
		add ? (user->mode |= UMODE_A) : (user->mode &= ~UMODE_A);
		break;
	    case 'a':
		add ? (user->mode |= UMODE_a) : (user->mode &= ~UMODE_a);
		break;
	}
    }
}

/* Handle a channel modechange */
void do_cmode (const char *source, int8 ac, char **av)
{
    Channel *chan;
    ChanInfo *ci;
    User *u;
    struct c_userlist *cu;
    char *s;
    int8 add = 1, ulev;
    char *modestr = av[1];
    char *param;

    chan = findchan (av[0]);

    if (!chan)
    {
	if (debuglevel > 1)
	    debug ("MODE for nonexistant channel %s: %s", av[0], av[1]);
	return;
    }

    ci = cs_findchan (av[0]);

    if (debuglevel > 1)
	debug ("do_cmode(): %s %s", av[0], av[1]);

    s = modestr;
    ac -= 2;
    av += 2;

    while (*s)
    {
	switch (*s++)
	{
	    case '+':
		add = 1;
		break;
	    case '-':
		add = 0;
		break;

	    case 'k':
		if (--ac < 0)
		    break;

		if (chan->key)
		{
		    free (chan->key);
		    chan->key = NULL;
		}

		if (add)
		    chan->key = sstrdup (*av++);

		add ? (chan->mode |= CMODE_k) : (chan->mode &= ~CMODE_k);
		break;

	    case 'l':
		if (add)
		    chan->limit = atoi (*av++);
		else
		    chan->limit = 0;

		add ? (chan->mode |= CMODE_l) : (chan->mode &= ~CMODE_l);
		break;

	    case 'b':
		if (--ac < 0)
		    break;

		if (add)
		{
		    ++chan->bancnt;
		    chan->bans = srealloc (chan->bans, sizeof (char *) *chan->bancnt);
		    chan->bans[chan->bancnt - 1] = sstrdup (*av);
		}
		else
		{
		    char **b = chan->bans;
		    int i = 0;

		    while (i < chan->bancnt && strcmp (*b, *av))
		    {
			i++;
			b++;
		    }

		    if (i < chan->bancnt)
		    {
			chan->bancnt--;

			if (i < chan->bancnt)
			    memmove (b, b + 1, sizeof (char *) *(chan->bancnt - i));
		    }
		}

		av++;
		break;

	    case 'o':
		if (--ac < 0)
		    break;

		param = *av++;
		u = finduser (param);

		if (!u)
		{
		    /* Someone may have deopped us in the JoinChan.
		       If its one of our bots, Reop.
		     */
		    if (!stricmp (param, s_ChanServ))
			send_cmd (s_ChanServ, "%s %s +o %s", me.token ? "G" : "MODE", chan->name, s_ChanServ);

		    if (!stricmp (param, s_NickServ))
			send_cmd (s_NickServ, "%s %s +o %s", me.token ? "G" : "MODE", chan->name, s_NickServ);

		    if (!stricmp (param, s_MemoServ))
			send_cmd (s_MemoServ, "%s %s +o %s", me.token ? "G" : "MODE", chan->name, s_MemoServ);

		    if (!stricmp (param, s_RootServ))
			send_cmd (s_RootServ, "%s %s +o %s", me.token ? "G" : "MODE", chan->name, s_RootServ);

		    if (!stricmp (param, s_GlobalNoticer))
			send_cmd (s_GlobalNoticer, "%s %s +o %s", me.token ? "G" : "MODE", chan->name, s_GlobalNoticer);

		    break;
		}

		/* Check if they have access to be opped. */
		if (ci && add)
		{
		    /* If it's not secure, we'll leave ops alone. */
		    if (ci->flags & CF_SECURE)
		    {
			ulev = get_access (u, ci);

			/* Check for AOP or above */
			if (ulev < AOP)
			{
			    /* Deop them and break */
			    if (!bursting)
				send_cmode (s_ChanServ, chan->name, "-o", u->nick);

			    break;
			}
		    }
		}

		/* Find the user in the channel users list */
		for (cu = chan->users; cu; cu = cu->next)
		    if (cu->user == u)
			break;

		if (cu)
		    add ? (cu->mode |= CMODE_o) : (cu->mode &= ~CMODE_o);

		break;

	    case 'v':
		if (--ac < 0)
		    break;

		u = finduser (*av++);

		if (!u)
		    break;

		/* Check if they have access to be voiced. */
		if (ci && add)
		{
		    /* If SECURE or SecureVOP is on, devoice them. */
		    if ((ci->flags & CF_SECURE) && securevop_on == TRUE)
		    {
			ulev = get_access (u, ci);

			/* Check for VOP or above */
			if (ulev < VOP)
			{
			    /* Devoice them and break */
			    if (!bursting)
				send_cmode (s_ChanServ, chan->name, "-v", u->nick);

			    break;
			}
		    }
		}

		/* Find the user in the channel users list */
		for (cu = chan->users; cu; cu = cu->next)
		    if (cu->user == u)
			break;

		if (cu)
		    add ? (cu->mode |= CMODE_v) : (cu->mode &= ~CMODE_v);

		break;

	    case 't':
		add ? (chan->mode |= CMODE_t) : (chan->mode &= ~CMODE_t);
		break;

	    case 's':
		add ? (chan->mode |= CMODE_s) : (chan->mode &= ~CMODE_s);
		break;

	    case 'n':
		add ? (chan->mode |= CMODE_n) : (chan->mode &= ~CMODE_n);
		break;

	    case 'm':
		add ? (chan->mode |= CMODE_m) : (chan->mode &= ~CMODE_m);
		break;

	    case 'i':
		add ? (chan->mode |= CMODE_i) : (chan->mode &= ~CMODE_i);
		break;

	    case 'p':
		add ? (chan->mode |= CMODE_p) : (chan->mode &= ~CMODE_p);
		break;

	    case 'R':
		add ? (chan->mode |= CMODE_R) : (chan->mode &= ~CMODE_R);
		break;

	    case 'r':
		if (!ci && add && !bursting)
		    send_cmode (s_ChanServ, chan->name, "-r");
		else
		    add ? (chan->mode |= CMODE_r) : (chan->mode &= ~CMODE_r);
		break;

	    case 'c':
		add ? (chan->mode |= CMODE_c) : (chan->mode &= ~CMODE_c);
		break;

	    case 'O':
		add ? (chan->mode |= CMODE_O) : (chan->mode &= ~CMODE_O);
		break;

	    case 'h':
		if (--ac < 0)
		    break;

		if (!(ircdtype == UNREAL3 || ircdtype == UNREAL3_2))
		    break;

		u = finduser (*av++);

		if (!u)
		    break;

		/* Check if they have access to be halfopped. */
		if (ci && add)
		{
		    /* If it's not secure, we'll leave halfops alone. */
		    if (ci->flags & CF_SECURE)
		    {
			ulev = get_access (u, ci);

			/* Check for HOP or above */
			if (ulev < HOP)
			{
			    /* Deop them and break */
			    if (!bursting)
				send_cmode (s_ChanServ, chan->name, "-h", u->nick);
			    break;
			}
		    }
		}

		/* Find the user in the channel users list */
		for (cu = chan->users; cu; cu = cu->next)
		    if (cu->user == u)
			break;

		if (cu)
		    add ? (cu->mode |= CMODE_h) : (cu->mode &= ~CMODE_h);

		break;

	    case 'f':
		if (--ac < 0)
		    break;

		if (ircdtype == UNREAL3 || ircdtype == UNREAL3_2)
		{
		    if (chan->flood)
		    {   
			free (chan->flood);
			chan->flood = NULL;
		    }

		    if (add)
			chan->flood = sstrdup (*av++);
		}

		add ? (chan->mode |= CMODE_f) : (chan->mode &= ~CMODE_f);
		break;

	    case 'L':
		if (--ac < 0)
		    break;

		if (ircdtype == UNREAL3 || ircdtype == UNREAL3_2)
		{
		    if (chan->link)
		    {   
			free (chan->link);
			chan->link = NULL;
		    }

		    if (add)
			chan->link = sstrdup (*av++);
		}

		add ? (chan->mode |= CMODE_L) : (chan->mode &= ~CMODE_L);
		break;

	    case 'A':
		add ? (chan->mode |= CMODE_A) : (chan->mode &= ~CMODE_A);
		break;

	    case 'Q':
		add ? (chan->mode |= CMODE_Q) : (chan->mode &= ~CMODE_Q);
		break;

	    case 'S':
		add ? (chan->mode |= CMODE_S) : (chan->mode &= ~CMODE_S);
		break;

	    case 'K':
		add ? (chan->mode |= CMODE_K) : (chan->mode &= ~CMODE_K);
		break;

	    case 'V':
		add ? (chan->mode |= CMODE_V) : (chan->mode &= ~CMODE_V);
		break;

	    case 'H':
		add ? (chan->mode |= CMODE_H) : (chan->mode &= ~CMODE_H);
		break;

	    case 'G':
		add ? (chan->mode |= CMODE_G) : (chan->mode &= ~CMODE_G);
		break;

	    case 'C':
		add ? (chan->mode |= CMODE_C) : (chan->mode &= ~CMODE_C);
		break;

	    case 'u':
		add ? (chan->mode |= CMODE_u) : (chan->mode &= ~CMODE_u);
		break;

	    case 'z':
		add ? (chan->mode |= CMODE_z) : (chan->mode &= ~CMODE_z);
		break;

	    case 'N':
		add ? (chan->mode |= CMODE_N) : (chan->mode &= ~CMODE_N);
		break;
	}
    }

    /* If this channel is registered, check the modelock. */
    if (!bursting)
	if (ci)
	    check_modes (chan->name);
}

/* Remove and free a User structure. */
void delete_user (User * user)
{
    struct u_chanlist *uc, *nextuc;
    char buf[BUFSIZE];

    *buf = 0;

    if (debuglevel > 1)
	debug ("delete_user(): %s", user->nick);

    usercnt--;
    userstat--;

    /* Remove a clone from their record */
    del_clone (user);

    /* Tell NickServ they've left */
    cancel_user (user);

    free (user->user);
    free (user->host);
    free (user->real);
    free (user->server);

    if (user->lastnick)
	free (user->lastnick);

    uc = user->chans;

    while (uc)
    {
	nextuc = uc->next;

	if (debuglevel > 1)
	{
	    strcat (buf, " ");
	    strcat (buf, uc->chan->name);
	}

	chan_deluser (user, uc->chan);

	free (uc);
	uc = nextuc;
    }

    if (user->idchancnt)
    {
	char **chans;
	int i;

	for (chans = user->idchans, i = 0; i < user->idchancnt; chans++, i++)
	    if (*chans)
		free (*chans);
    }

    if (*buf && debuglevel > 1)
	debug ("delete_user(): %s left%s", user->nick, buf);

    if (user->prev)
	user->prev->next = user->next;
    else
	userlist[NHASH(user->nick)] = user->next;

    if (user->next)
	user->next->prev = user->prev;

    free (user);
}

/* Handle a JOIN command.
      av[0] = channels to join
 */
void do_join (const char *source, int8 ac, char **av)
{
    User *user;
    Channel *c;
    char *s, *t;
    struct u_chanlist *uc, *nextuc;

    user = finduser (source);

    if (!user)
    {
	if (debuglevel > 1)
	    debug ("do_join(): Got JOIN to %s from nonexistant user %s",
		av[0], source);
	return;
    }

    t = av[0];

    while (*(s = t))
    {
	t = s + strcspn (s, ",");

	if (*t)
	    *t++ = 0;

        if (*s == '0')
        {
	    uc = user->chans;

	    while (uc)
	    {
		nextuc = uc->next;

		chan_deluser (user, uc->chan);

		free (uc);

		uc = nextuc;
	    }

	    if (debuglevel > 1)
		debug ("do_join(): %s parted all channels", source);

	    user->chans = NULL;
	    continue;
	}

	if (debuglevel > 1)
	    debug ("do_join(): %s joined %s", source, s);

	/* Various on-join ChanServ stuff. If this function
	   returns 1, the user was kicked for whatever reason.
	   otherwise everythings fine.
	 */
	if (!bursting)
	    if (cs_join (user, av[0], 0))
		continue;

	c = chan_adduser (user, s, 0);
	uc = smalloc (sizeof (*c));
	uc->next = user->chans; 
	uc->prev = NULL;
	if (user->chans)
	    user->chans->prev = uc;
	user->chans = uc;
	uc->chan = c;
    }
}

/* Handle a PART command */
void do_part (const char *source, int8 ac, char **av)
{
    User *user;
    char *s, *t;
    struct u_chanlist *c;
 
    user = finduser (source);

    if (!user)  
	return;

    t = av[0];

    while (*(s = t))
    {
	t = s + strcspn (s, ",");

	if (*t) 
	    *t++ = 0;

	if (debuglevel > 1)
	    debug ("do_part(): %s parted %s", source, s);

	for (c = user->chans; c && stricmp (s, c->chan->name) != 0; c = c->next);  

	if (c)
	{
	    chan_deluser (user, c->chan);

	    if (c->next)
		c->next->prev = c->prev;
	    if (c->prev)
		c->prev->next = c->next;
	    else
		user->chans = c->next;

	    free (c);
	}
	else
	    if (debuglevel > 1)
		debug ("do_part(): Got PART %s %s, but %s isn't on %s!", source, s, source, s);
    }
}

/* Handle a KICK command */
void do_kick (const char *source, int8 ac, char **av)
{
    User *user;
    char *s, *t;
    struct u_chanlist *c;   
        
    if (is_one_of_mine (av[1]))
    {
	/* Rejoin our channel if kicked */
	if (!stricmp (av[0], joinchan))
	{
	    if (ircdtype == BAHAMUT || ircdtype == PROMETHEUS)
		send_cmd (av[1], "SJOIN %ld %ld %s + :%s", time (NULL), time (NULL),
		    joinchan, av[1]);
	    else
		send_cmd (av[1], "%s %s", me.token ? "C" : "JOIN", joinchan);

	    send_cmd (av[1], "%s %s +o %s", me.token ? "G" : "MODE", joinchan, av[1]);
	}

	return;
    }

    t = av[1];

    while (*(s = t))
    {
	t = s + strcspn (s, ",");

	if (*t)
	    *t++ = 0;

	user = finduser (s);

	if (!user)
	{
	    if (debuglevel > 1)
		debug ("do_kick(): Got KICK for nonexistant user %s from %s",
		    user->nick, av[0]);
	    continue;
	}

	if (debuglevel > 1)
	    debug ("do_kick(): %s was kicked from %s", s, av[0]);

	for (c = user->chans; c && stricmp (av[0], c->chan->name) != 0; c = c->next);

	if (c) 
	{
	    chan_deluser (user, c->chan);

	    if (c->next)
		c->next->prev = c->prev;
	    if (c->prev)
		c->prev->next = c->next;
	    else
		user->chans = c->next;

	    free (c);
	}
    }

    if (!source)
	return;
}

/* Handle a QUIT command */
void do_quit (const char *source, int8 ac, char **av)
{    
    User *user;

    user = finduser (source);

    if (!user)
    {
	if (debuglevel > 1)
	    debug ("do_quit(): Got QUIT for nonexistant user %s", user->nick);
	return;
    }

    if (debuglevel > 1)
	debug ("do_quit(): %s QUIT", source);

    /* If this is a nickname we were waiting for, reclaim it. */
    if (rs.wantnick)
	if (!stricmp (source, rs.nick))
	{
	    send_cmd (s_RootServ, "%s %s", me.token ? "&" : "NICK", rs.nick);
	    strscpy (s_RootServ, rs.nick, sizeof (s_RootServ));
	    rs.wantnick = 0;
	}

    if (ns.wantnick)
	if (!stricmp (source, ns.nick))
	{
	    send_cmd (s_NickServ, "%s %s", me.token ? "&" : "NICK", ns.nick);
	    strscpy (s_NickServ, ns.nick, sizeof (s_NickServ));
	    ns.wantnick = 0;
	}

    if (cs.wantnick)
	if (!stricmp (source, cs.nick))
	{
	    send_cmd (s_ChanServ, "%s %s", me.token ? "&" : "NICK", cs.nick);
	    strscpy (s_ChanServ, cs.nick, sizeof (s_ChanServ));
	    cs.wantnick = 0;
	}

    if (ms.wantnick)
	if (!stricmp (source, ms.nick))
	{
	    send_cmd (s_MemoServ, "%s %s", me.token ? "&" : "NICK", ms.nick);
	    strscpy (s_MemoServ, ms.nick, sizeof (s_MemoServ));
	    ms.wantnick = 0;
	}

    if (gn.wantnick)
	if (!stricmp (source, gn.nick))
	{
	    send_cmd (s_GlobalNoticer, "%s %s", me.token ? "&" : "NICK",
		gn.nick);
	    strscpy (s_GlobalNoticer, gn.nick, sizeof (s_GlobalNoticer));
	    gn.wantnick = 0;
	}

    delete_user (user);
}

/* Add a user to a channel, creating the channel as necessary. */
Channel *chan_adduser (User *user, const char *chan, int32 modes)
{
    Channel *c = findchan (chan);
    ChanInfo *ci = cs_findchan (chan);
    Channel **list;
    NickInfo *ni;
    int ulev = 0, newchan = !c;
    struct c_userlist *u;

    if (newchan)
    {
	/* Allocate pre-cleared memory */
	c = scalloc (sizeof (Channel), 1);
	strscpy (c->name, chan, sizeof (c->name));
	list = &chanlist[CHASH(c->name)];
	c->next = *list;

	if (*list)
	    (*list)->prev = c;

	*list = c;

	if (debuglevel > 1) 
	    debug ("chan_adduser(): Creating new channel %s", chan);

	chancnt++;

	/* Check if this channel is registered, and do appropriate things. */
	restore_channel (c);
    }

    u = smalloc (sizeof (struct c_userlist));
    u->next = c->users;
    u->prev = NULL;

    if (c->users)
	c->users->prev = u;

    c->users = u;
    u->user = user;
    u->mode = modes;

    c->usercnt++;

    /* Check their ChanServ access, and perform any nessecary actions. */
    if (!bursting && ci)
    {
	/* Get their access. */
	ulev = get_access (user, ci);

	if (!ulev)
	{
	    if (debuglevel > 1)
		debug ("chan_adduser(): %s added to %s", user->nick, c->name);

	    /* If they don't have any access, but VOPALL is on, voice them. */
	    if (ci->flags & CF_VOPALL)
	    {
		if (!(u->mode & CMODE_v))
		{
		    send_cmode (s_ChanServ, c->name, "+v", user->nick);
		    u->mode |= CMODE_v;
		}
	    }
	    else
	    {
		if (u->mode & CMODE_v)
		{
		    u->mode &= ~CMODE_v;
		    send_cmode (s_ChanServ, c->name, "-v", user->nick);
		}
	    }

	    if (ircdtype == UNREAL3 || ircdtype == UNREAL3_2)
	    {
		if (u->mode & CMODE_h)
		{
		    u->mode &= ~CMODE_h;
		    send_cmode (s_ChanServ, c->name, "-h", user->nick);
		}

		if (u->mode & CMODE_q)
		{
		    u->mode &= ~CMODE_q;
		    send_cmode (s_ChanServ, c->name, "-q", user->nick);
		}

		if (u->mode & CMODE_a)
		{
		    u->mode &= ~CMODE_a;
		    send_cmode (s_ChanServ, c->name, "-a", user->nick);
		}
	    }

	    if (u->mode & CMODE_o)
	    {
		u->mode &= ~CMODE_o;
		send_cmode (s_ChanServ, c->name, "-o", user->nick);
	    }

	    return c;
	}

	ni = findnick (user->nick);

	if (!ni)
	{
	    /* If their nick isn't registered, well, they shouldn't be in here! :P */
	    return c;
	}

	/* Update the last used timestamp */
	ci->lastused = time (NULL);

	/* VOP, +v */
	if (ulev == VOP)
	{
	    if (u->mode & CMODE_o)
	    {
		send_cmode (s_ChanServ, c->name, "-o", user->nick);
		u->mode &= ~CMODE_o;
	    }

	    if (ircdtype == UNREAL3 || ircdtype == UNREAL3_2)
	    {
		if (u->mode & CMODE_h)
		{
		    send_cmode (s_ChanServ, c->name, "-h", user->nick);
		    u->mode &= ~CMODE_h;
		}
	    }

	    if (!(u->mode & CMODE_v) && !(ni->flags & NF_NEVEROP))
	    {
		send_cmode (s_ChanServ, c->name, "+v", user->nick);
		u->mode |= CMODE_v;
	    }
	}

	if (ircdtype == UNREAL3 || ircdtype == UNREAL3_2)
	{
	    /* HOP, +h */
	    if (ulev == HOP)
	    {
		if (u->mode & CMODE_o)
		{
		    send_cmode (s_ChanServ, c->name, "-o", user->nick);
		    u->mode &= ~CMODE_o;
		}

		if (!(u->mode & CMODE_h) && !(ni->flags & NF_NEVEROP))
		{
		    send_cmode (s_ChanServ, c->name, "+h", user->nick);
		    u->mode |= CMODE_h;
		}
	    }
	}

	/* AOP, SOP, Founder, +o */
	if (ulev >= AOP)
	{
	    if (!(ni->flags & NF_NEVEROP))
	    {
		send_cmode (s_ChanServ, c->name, "+o", user->nick);
		u->mode |= CMODE_o;
	    }
	}
    }

    if (debuglevel > 1)
	debug ("chan_adduser(): %s added to %s", user->nick, c->name);

    return c;
}

/* Remove a user from a channel, deleting the channel as necessary. */
void chan_deluser (User *user, Channel *c)
{
    struct c_userlist *u;

    if (debuglevel > 1)
	debug ("chan_deluser(): %s %s", user->nick, c->name);

    for (u = c->users; u && u->user != user; u = u->next);

    if (!u)
    {
	if (debuglevel > 1)
	    debug ("chan_deluser(): %s not in %s!", user->nick, c->name);
    }
    else
    {
	if (u->next)
	    u->next->prev = u->prev;
	if (u->prev)
	    u->prev->next = u->next;
	else
	    c->users = u->next;

	free (u);

	c->usercnt--;
    }

    if (!c->users)
    {
	if (debuglevel > 1)
	    debug ("chan_deluser(): Deleting empty channel %s", c->name);

	chancnt--;

	if (c->key)
	    free (c->key);

	if (ircdtype == UNREAL3 || ircdtype == UNREAL3_2)
	{
	    if (c->flood)
		free (c->flood);

	    if (c->link)
		free (c->link);
	}

	if (c->next)
	    c->next->prev = c->prev;
	if (c->prev)
	    c->prev->next = c->next;
	else
	    chanlist[CHASH(c->name)] = c->next;

	free (c);

	return;
    }
}

/* Change a channel's stored topic. */
void do_topic (const char *source, int8 ac, char **av)
{
    Channel *c = findchan (av[0]);
    ChanInfo *ci = cs_findchan (av[0]);

    if (!c)
	return;

    /* If this channel is frozen, ignore topic changes. We usurped the topic
       fields for freeze info.
     */
    if (ci && (ci->flags & CF_FROZEN))
	return;

    /* Check topiclock first */
    if (ci && ci->topiclock)
    {
	User *u;
	int ulev;

	/* If the source is a server, make note of the topic
	   and bail. We'll set it later if needed.
	 */
	if (ircdtype == BAHAMUT || ircdtype == PROMETHEUS || ircdtype == UNREAL3 || ircdtype == UNREAL3_2)
	{
	    if (strchr (source, '.'))
	    {
		strscpy (c->topicsetter, av[1], NICKLEN);
		strscpy (c->topic, av[3], TOPICLEN);
		c->topictime = atol (av[2]);
		return;
	    }
	}

	u = finduser (source);

	ulev = get_access (u, ci);

	if (ulev < ci->topiclock)
	{
	    /* Topiclocked, and not enough access. Restore topic. */
	    strscpy (c->topicsetter, ci->topicsetter, NICKLEN);
	    strscpy (c->topic, ci->topic, TOPICLEN);
	    c->topictime = ci->topictime;

	    send_cmd (s_ChanServ, "%s %s %s %lu :%s", me.token ? ")" : "TOPIC", c->name,
		c->topicsetter, c->topictime, c->topic);

	    return;
	}
    }

    if (ircdtype == BAHAMUT || ircdtype == PROMETHEUS || ircdtype == UNREAL3 || ircdtype == UNREAL3_2)
    {
	/* If the source is a server, make note of the topic
	   and bail. We'll set it later if needed.
	 */
	if (strchr (source, '.'))
	{
	    strscpy (c->topicsetter, av[1], NICKLEN);
	    strscpy (c->topic, av[3], TOPICLEN);
	    c->topictime = atol (av[2]);
	    return;
	}
    }

    /* Store the topic info in the channel structure */
    strscpy (c->topicsetter, av[1], NICKLEN);
    strscpy (c->topic, av[3], TOPICLEN);
    c->topictime = atol (av[2]);

    /* Store the topic info in ChanInfo if registered */
    if (ci)
    {
	strscpy (ci->topicsetter, av[1], NICKLEN);
	strscpy (ci->topic, av[3], TOPICLEN);
	ci->topictime = atol (av[2]);
    }
}

/* Handle a KILL command.
      av[0] = nick being killed
      av[1] = reason
 */
void do_kill (const char *source, int8 ac, char **av)
{
    User *user;

    user = finduser (av[0]);

    if (!user)
    {
	if (debuglevel > 1)
	    debug ("do_kill(): Got kill for nonexistant user %s", av[0]);
	return;
    }

    if (debuglevel > 1)
	debug ("do_kill(): %s Killed", av[0]);

    delete_user (user);
}

/* Remove a user from the network. */
void kill_user (const char *source, const char *user, const char *reason)
{
    char *av[2];
    char buf[BUFSIZE];

    if (!user || !*user)
	return;

    if (!source || !*source)
	source = me.name;

    if (!reason)
	reason = "Killed";

    snprintf (buf, sizeof (buf), "%s (%s)", source, reason);

    av[0] = sstrdup (user);
    av[1] = buf;

    send_cmd (source, "%s %s :%s", me.token ? "." : "KILL", user, av[1]);
    do_kill (source, 2, av);
    free (av[0]);
}

/* Handle an SJOIN command. - Borrowed from IRCServices
        Bahamut no-SSJOIN format:
            av[0] = TS3 timestamp
            av[1] = TS3 timestamp
            av[2] = channel
            av[3] = channel modes
            av[4] = limit / key (depends on modes in av[3])
            av[5] = limit / key (depends on modes in av[3]) 
            av[ac-1] = nickname(s), with modes, joining channel
        Bahamut SSJOIN format (server source) / Unreal SJOIN2 format:
            av[0] = TS3 timestamp - channel creation time
            av[1] = channel
            av[2] = modes (limit/key in av[3]/av[4] as needed)
            av[ac-1] = users
            (Note that Unreal may omit the modes if there aren't any.)
        Bahamut SSJOIN format (client source):
            av[0] = TS3 timestamp
            av[1] = channel
 */
void do_sjoin (const char *source, int8 ac, char **av)
{
    User *user;
    Channel *c = NULL;
    struct u_chanlist *uc;
    char *t, *nick, *channel;
    int joins = 0;

    if (isdigit (av[1][0]))
    {
	memmove (&av[0], &av[1], sizeof (char *) * (ac-1));
	ac--;
    }

    channel = av[1];

    if (ac >= 3)
	t = av[ac-1];
    else
	t = (char *)source;

    while (*(nick=t))
    {
	int32 modes = 0, thismode;

        t = nick + strcspn (nick, " ");

	if (*t)
	    *t++ = 0;

	if (ircdtype == UNREAL3 || ircdtype == UNREAL3_2)
	{
	    if (*nick == '&' || *nick == '"')
	    {
		char *avtmp[3];

		avtmp[0] = channel;
		avtmp[1] = (*nick=='&') ? "+b" : "+e";
		avtmp[2] = nick + 1;

		do_cmode (source, 3, avtmp);
		continue;
	    }
	}

	do
	{
	    thismode = prefix_to_flag (*nick);
	    modes |= thismode;
	} while (thismode && nick++);

	user = finduser (nick);

	if (!user)
	{
	    if (debuglevel > 1)
		debug ("do_sjoin(): Got SJOIN to %s for nonexistant user %s",
		    channel, nick);
	    continue;
	}

	if (debuglevel > 1)
	    debug ("do_sjoin(): %s joined %s", nick, channel);

	/* Various on-join ChanServ stuff. If this function
	   returns 1, the user was kicked for whatever reason.
	   otherwise everythings fine.
	 */
	if (!bursting)
	    if (cs_join (user, channel, 0))
		continue;

	c = chan_adduser (user, channel, modes);
	joins++;

	uc = smalloc (sizeof (*uc));
	uc->next = user->chans;
	uc->prev = NULL;

	if (user->chans)
	    user->chans->prev = uc;

	user->chans = uc;
	uc->chan = c;
    }

    /* Did anyone actually join the channel? */
    if (c)
	if (ac > 3)
	    do_cmode (source, ac-2, av+1);
}

/* Stack channel modes to be applied to a channel. Borrowed from IRCServices. */
void send_cmode (const char *sender, ...)
{
    va_list args;
    const char *channel, *modes;
    struct modedata *md;
    int which = -1, add; 
    int32 flag;
    int i;
    char c, *s;

    if (!sender)
    {
	for (i = 0; i < 3; i++)
	{
	    if (modedata[i].used)
		flush_cmode (&modedata[i]);
	}

	return;
    }

    va_start (args, sender);
    channel = va_arg (args, const char *);
    modes = va_arg (args, const char *);

    for (i = 0; i < 3; i++)
    {
	if (modedata[i].used && !stricmp (modedata[i].channel, channel))
	{
	    if (stricmp (modedata[i].sender, sender))
		flush_cmode (&modedata[i]);

	    which = i;
	    break;
	}
    }

    if (which < 0)
    {
	for (i = 0; i < 3; i++)
	{
	    if (!modedata[i].used)
	    {
		which = i;
		modedata[which].last_add = -1;
		break;
	    }
	}
    }

    if (which < 0)
    {
	int oldest = 0;
	time_t oldest_time = modedata[0].used;

	for (i = 1; i < 3; i++)
	{
	    if (modedata[i].used < oldest_time)
	    {
		oldest_time = modedata[i].used;
		oldest = i;
	    }
	}

        flush_cmode (&modedata[oldest]);
        which = oldest;
        modedata[which].last_add = -1;
    }

    md = &modedata[which];
    strscpy (md->sender, sender, NICKLEN);
    strscpy (md->channel, channel, CHANNELLEN);

    add = -1;

    while ((c = *modes++))
    {
	if (c == '+')
	{
	    add = 1;
	    continue;
	}
	else if (c == '-')
	{
	    add = 0;
	    continue;
        }
	else if (add < 0)
            continue;

        switch (c)
	{
	    case 'k':
	    case 'l':
	    case 'o':
	    case 'v':
	    case 'h':
	    case 'a':
	    case 'q':
	    case 'b':
		if (md->nparams >= MAXMODES || md->paramslen >= MAXPARAMSLEN)
		{
		    flush_cmode (&modedata[which]);
		    strscpy (md->sender, sender, NICKLEN);
		    strscpy (md->channel, channel, CHANNELLEN);
		    md->used = time (NULL);
		}

		s = md->opmodes + strlen (md->opmodes);

		if (add != md->last_add)
		{
		    *s++ = add ? '+' : '-';
		    md->last_add = add;
		}

		*s++ = c;

		if (!add && c == 'l')
		    break;

		s = va_arg (args, char *);

		md->paramslen += snprintf (md->params + md->paramslen,
		    MAXPARAMSLEN + 1 - md->paramslen, "%s%s",
		    md->paramslen ? " " : "", s);

		md->nparams++;
		break;

	    default:
		flag = mode_to_flag (c);

		if (add)
		{
		    md->binmodes_on |= flag;
		    md->binmodes_off &= ~flag;
		}
		else
		{
		    md->binmodes_off |= flag;
		    md->binmodes_on &= ~flag;
		}
	}
    }

    va_end (args);
    md->used = time (NULL);

    if (!md->timeout)
    {
	md->timeout = add_timeout_ms (MODESTACK_WAIT, flush_cmode_callback, 0);
	md->timeout->data = md;
    }
}

/* Flush stacked and waiting cmodes. */            
static void flush_cmode (struct modedata *md)
{
    char buf[BUFSIZE];
    int len = 0;
    char lastc = 0;

    if (md->timeout)
	del_timeout (md->timeout);

    if (!md->binmodes_on && !md->binmodes_off && !*md->opmodes)
    {
	memset (md, 0, sizeof (*md));
	md->last_add = -1;
	return;
    }

    if (md->binmodes_off)
    {
	len += snprintf (buf + len, sizeof (buf) - len, "-%s",
	    flags_to_string (md->binmodes_off));
	lastc = '-';
    }

    if (md->binmodes_on)
    {
	len += snprintf (buf + len, sizeof (buf) - len, "+%s",
	    flags_to_string (md->binmodes_on));
	lastc = '+';
    }

    if (*md->opmodes)
    {
	if (*md->opmodes == lastc)
	    memmove (md->opmodes, md->opmodes + 1, strlen (md->opmodes + 1) + 1);

	len += snprintf (buf + len, sizeof (buf) - len, "%s", md->opmodes);
    }

    if (md->paramslen)
	snprintf (buf + len, sizeof (buf) - len, " %s", md->params);

    send_cmd (md->sender, "%s %s %s", me.token ? "G" : "MODE", md->channel, buf);

    memset (md, 0, sizeof (*md));
    md->last_add = -1;
}
     
static void flush_cmode_callback (Timeout *t)
{
    flush_cmode ((struct modedata *)t->data);
}
