/* 
   users.c -- handles:
     testing and enforcing bans and ignores
     adding and removing bans and ignores
     listing bans and ignores
     auto-linking bots
     sending and receiving a userfile from a bot
     listing users ('.whois' and '.match')
     reading the user file

   dprintf'ized, 9nov95
*/
/*
   This file is part of the eggdrop source code
   copyright (c) 1997 Robey Pointer
   and is distributed according to the GNU general public license.
   For full details, read the top of 'main.c' or the file called
   COPYING that was distributed with this code.
*/

#if HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include "eggdrop.h"
#include "users.h"
#include "chan.h"
#include "proto.h"

/* 
   bans:
   <banmask>:<expire-time>:[+<time-added>:<last-active>]:<user>:<encoded-desc>
     expire-time: timestamp when the ban was made, or 0 for permanent
       (if it starts with '+': when the ban will expire)
     time-added: when the ban was first created
     last-active: last time ban was enforced
     user: who placed the ban

   ignores:
   <ignoremask>:+<expire-time>:<user>[:<time-added>:<encoded-desc>]
     time-added: when the ignore was created
     user: who placed the ignore
*/


extern char botname[];
extern char botuser[];
extern char botuserhost[];
extern int serv;
extern struct dcc_t dcc[];
extern int dcc_total;
extern int noshare;
extern struct userrec *userlist,*lastuser,*banu,*ignu;
extern char cx_file[];
extern int cx_line;
extern char origbotname[];
extern char SBUF[];
extern struct chanset_t *chanset;

/* where the user records are stored */
char userfile[121]="";
/* how many minutes will bans last? */
int ban_time=60;
/* how many minutes will ignores last? */
int ignore_time=10;


/* is this nick!user@host being ignored? */
int match_ignore(uhost)
char *uhost;
{
  struct userrec *u; struct eggqueue *q; char host[UHOSTLEN],s[161];
  u=get_user_by_handle(userlist,IGNORE_NAME);
  if (u==NULL) return 0;
  q=u->host; while (q!=NULL) {
    strcpy(s,q->item); splitc(host,s,':');
    if (wild_match(host,uhost)) return 1;
    q=q->next;
  }
  return 0;
}

/* returns 1 if temporary ban, 2 if permban, 0 if not a ban at all */
int u_equals_ban(u,uhost)
struct userrec *u; char *uhost;
{
  struct eggqueue *q; char host[UHOSTLEN],s[256],*p;
  q=u->host; while (q!=NULL) {
    strcpy(s,q->item); splitc(host,s,':');
    if (strcasecmp(host,uhost)==0) {
      p=s; if (*p=='+') p++;
      if (atoi(p)==0) return 2;
      else return 1;
    }
    q=q->next;
  }
  return 0;   /* not equal */
}

int equals_ban(uhost)
char *uhost;
{
  struct userrec *u;
  u=get_user_by_handle(userlist,BAN_NAME);
  if (u==NULL) return 0;
  return u_equals_ban(u,uhost);
}

int equals_ignore(uhost)
char *uhost;
{
  struct userrec *u; struct eggqueue *q; char host[UHOSTLEN],s[256];
  u=get_user_by_handle(userlist,IGNORE_NAME);
  if (u==NULL) return 0;
  q=u->host; while (q!=NULL) {
    strcpy(s,q->item); splitc(host,s,':');
    if (strcasecmp(host,uhost)==0) {
      if (s[0]=='0') return 1;
      else return 2;
    }
    q=q->next;
  }
  return 0;   /* not equal */
}

int u_match_ban(u,uhost)
struct userrec *u; char *uhost;
{
  struct eggqueue *q; char host[UHOSTLEN],s[256];
  q=u->host; while (q!=NULL) {
    strcpy(s,q->item); splitc(host,s,':');
    if (wild_match(host,uhost)) return 1;
    q=q->next;
  }
  return 0;
}

int match_ban(uhost)
char *uhost;
{
  struct userrec *u;
  u=get_user_by_handle(userlist,BAN_NAME);
  if (u==NULL) return 0;
  return u_match_ban(u,uhost);
}

/* if any bans match this wildcard expression, refresh them on the channel */
void refresh_ban_kick(chan,user,nick)
char *user,*nick; struct chanset_t *chan;
{
  struct userrec *u; struct eggqueue *q;
  char host[UHOSTLEN],s[256],ts[21],s1[161],*p,new_expire=0;
  time_t expire_time,time_added=(time_t)0L,last_active=(time_t)0L;
  int cycle=0;
  u=get_user_by_handle(userlist,BAN_NAME);
  while (u!=NULL) {
    q=u->host; while ((q!=NULL) && (strcmp(q->item,"none")!=0)) {
      strcpy(s,q->item); splitc(host,s,':');
      if (wild_match(host,user)) {
	/* if this ban was placed in the last 60 seconds, it may not have */
	/* propagated yet -- or it could be a desync, which can't be solved */
	/* from here. :(  */
	if (q->stamp < time(NULL)-60) {
	  if (member_op(chan->name,nick))
	    add_mode(chan,'-','o',nick);   /* guess it can't hurt */
	  add_mode(chan,'+','b',host);
	  flush_mode(chan,QUICK);   /* do it IMMEDIATELY */
	  splitc(ts,s,':');
	  if (ts[0]=='+') {
	    strcpy(ts,&ts[1]); new_expire=1;
	  }
	  expire_time=(time_t)atol(ts);
	  if (s[0]=='+') {
	    /* strip off new timestamps */
	    strcpy(s,&s[1]);
	    splitc(ts,s,':'); time_added=(time_t)atol(ts);
	    splitc(ts,s,':'); last_active=(time_t)atol(ts);
	    /* (update last-active timestamp) */
	    sprintf(s1,"%s:%s%lu:+%lu:%lu:%s",host,new_expire?"+":"",
		    expire_time,time_added,time(NULL),s);	
	    chg_q(q,s1);
	  }
	  /* split off nick */
	  splitc(s1,s,':');
	  if (s[0]) {
	    /* ban reason stored */
	    p=strchr(s,'~'); while (p!=NULL) { *p=' '; p=strchr(s,'~'); }
	    p=strchr(s,'`'); while (p!=NULL) { *p=','; p=strchr(s,'`'); }
	    mprintf(serv,"KICK %s %s :banned: %s\n",chan->name,nick,s);
	  }
	  else mprintf(serv,"KICK %s %s :You are banned.\n",chan->name,nick);
	}
      }
      q=q->next;
    }
    cycle++;
    if (cycle==1) u=chan->bans;
    else u=NULL;
  }
}

int u_delban(u,who)
struct userrec *u; char *who;
{
  int i,j; struct eggqueue *q; char s[256],host[UHOSTLEN];
  context;
  i=0;
  if (atoi(who)) {
    j=atoi(who); q=u->host;
    while ((j>0) && (q!=NULL)) {
      if (strcmp(q->item,"none")!=0) j--;
      if (j>0) q=q->next;
    }
    if (q!=NULL) {
      strcpy(s,q->item); splitc(who,s,':');
      if (!who[0]) strcpy(who,s);
      u->host=del_q(q->item,u->host,&i);
    }
    else return j-atoi(who);
  }
  else {
    /* find matching host, if there is one */
    q=u->host; while ((q!=NULL) && (!i)) {
      strcpy(s,q->item); splitc(host,s,':');
      if (!host[0]) strcpy(host,s);
      if (strcasecmp(who,host)==0) u->host=del_q(q->item,u->host,&i);
      q=q->next;
    }
  }
  if (i) {
    if (!noshare) {
      /* distribute chan bans differently */
      if (strcasecmp(u->handle,BAN_NAME)==0)
	shareout("-ban %s\n",who);
      else
	shareout("-banchan %s %s\n",u->info,who);
    }
  }
  return i;
}

int delban(who)
char *who;
{
  struct userrec *u; int i;
  u=get_user_by_handle(userlist,BAN_NAME);
  if (u==NULL) return 0;
  i=u_delban(u,who);
  if (u->host==NULL) deluser(BAN_NAME);
  return i;
}

int delignore(ign)
char *ign;
{
  struct userrec *u; int i,j; struct eggqueue *q;
  char s[161],host[UHOSTLEN];
  context;
  u=get_user_by_handle(userlist,IGNORE_NAME); i=0;
  if (u==NULL) return 0;
  if (atoi(ign)) {
    j=atoi(ign)-1; q=u->host; 
    while (j>0) {
      if (q!=NULL) q=q->next;
      j--;
    }
    if (q!=NULL) {
      strcpy(s,q->item); splitc(ign,s,':');
      u->host=del_q(q->item,u->host,&i);
    }
  }
  else {
    /* find the matching host, if there is one */
    q=u->host; while ((q!=NULL) && (!i)) {
      strcpy(s,q->item); splitc(host,s,':'); context;
      if (strcasecmp(ign,host)==0) u->host=del_q(q->item,u->host,&i);
      q=q->next;
    }
  }
  if (i) {
    if (u->host == NULL) deluser(IGNORE_NAME);
    if (!noshare) shareout("-ignore %s\n",ign);
  }
  return i;
}

/* new method of creating bans */
void u_addban(u,ban,from,note,expire_time)
struct userrec *u; char *ban,*from,*note; time_t expire_time;
{
  char s[UHOSTLEN],host[UHOSTLEN],*p,oldnote[256]; time_t t,now=time(NULL);
  context;
  strcpy(host,ban);
  /* choke check: fix broken bans (must have '!' and '@') */
  if ((strchr(host,'!')==NULL) && (strchr(host,'@')==NULL))
    strcat(host,"!*@*");
  else if (strchr(host,'@')==NULL) strcat(host,"@*");
  else if (strchr(host,'!')==NULL) {
    p=strchr(host,'@'); strcpy(s,p); *p=0;
    strcat(host,"!*"); strcat(host,s);
  }
  sprintf(s,"%s!%s",botname,botuserhost);
  if (wild_match(host,s)) {
    putlog(LOG_MISC,"*","Wanted to ban myself: deflected.");
    return;
  }
  if (u_equals_ban(u,host)) u_delban(u,host);  /* remove old ban */
  /* new format: */
  sprintf(s,"%s:+%lu:+%lu:%lu:%s:",host,expire_time,now,now,from);
  if (note[0]) {
    strcpy(oldnote,note);
    /* remove spaces & commas */
    p=strchr(note,' '); while (p!=NULL) { *p='~'; p=strchr(note,' '); }
    p=strchr(note,','); while (p!=NULL) { *p='`'; p=strchr(note,','); }
    strcat(s,note);
  }
  else oldnote[0]=0;
  t=0L;
  if (expire_time != 0L) { t=(expire_time-now); if (t==0) t=1; }
  u->host=add_q(s,u->host);
  if (!noshare) {
    if (strcasecmp(u->handle,BAN_NAME)==0)
      shareout("+ban %s +%lu %s %s\n",host,t,from,oldnote);
    else
      shareout("+banchan %s +%lu %s %s %s\n",host,t,u->info,from,oldnote);
  }
  strcpy(note,oldnote);
}

void addban(ban,from,note,expire_time)
char *ban,*from,*note; time_t expire_time;
{
  struct userrec *u;
  u=get_user_by_handle(userlist,BAN_NAME);
  if (u==NULL) {
    userlist=adduser(userlist,BAN_NAME,"none","-",0);
    u=get_user_by_handle(userlist,BAN_NAME);
  }
  u_addban(u,ban,from,note,expire_time);
}

void addignore(ign,from,mnote,expire_time)
char *ign,*from,*mnote; time_t expire_time;
{
  struct userrec *u; char s[UHOSTLEN],oldnote[256],*p,note[81];
  time_t t,now;
  context;
  now=time(NULL); strcpy(note,mnote);
  if (equals_ignore(ign)) delignore(ign);  /* remove old ban */
  u=get_user_by_handle(userlist,IGNORE_NAME);
  sprintf(s,"%s:+%lu:%s:%lu:",ign,expire_time,from,now);
  if (note[0]) {
    strcpy(oldnote,note);
    /* remove spaces & commas */
    p=strchr(note,' '); while (p!=NULL) { *p='~'; p=strchr(note,' '); }
    p=strchr(note,','); while (p!=NULL) { *p='`'; p=strchr(note,','); }
    strcat(s,note);
  }
  else oldnote[0]=0;
  t=0L;
  if (expire_time != 0L) { t=(expire_time-now); if (t==0) t=1; }
  if (u==NULL) userlist=adduser(userlist,IGNORE_NAME,s,"-",0);
  else u->host=add_q(s,u->host);
  if (!noshare) shareout("+ignore %s +%lu %s %s\n",ign,t,from,oldnote);
  strcpy(note,oldnote);
}

/* grabs and translates the note from a ban (in host form) */
void getbannote(host,from,note)
char *host,*from,*note;
{
  char *p;
  /* scratch off ban and timestamps */
  splitc(NULL,host,':'); splitc(NULL,host,':');
  if (host[0]=='+') {
    splitc(NULL,host,':'); splitc(NULL,host,':');
  }
  if (host[0]) {
    splitc(from,host,':');
    /* fix spaces & commas */
    p=strchr(host,'~'); while (p!=NULL) { *p=' '; p=strchr(host,'~'); }
    p=strchr(host,'`'); while (p!=NULL) { *p=','; p=strchr(host,'`'); }
  }
  else from[0]=0;
  strcpy(note,host);
}

/* grabs and translates the note from an ignore (in host form) */
void getignorenote(host,from,note)
char *host,*from,*note;
{
  char *p;
  /* scratch off ignore and timestamp */
  splitc(NULL,host,':'); splitc(NULL,host,':');
  splitc(from,host,':');
  if (!from[0]) { strcpy(from,host); host[0]=0;}   /* old */
  if (host[0]) {
    splitc(NULL,host,':');  /* another timestamp */
    /* fix spaces & commas */
    p=strchr(host,'~'); while (p!=NULL) { *p=' '; p=strchr(host,'~'); }
    p=strchr(host,'`'); while (p!=NULL) { *p=','; p=strchr(host,'`'); }
  }
  strcpy(note,host);
}

/* take host entry from ban list and display it ban-style */
void display_ban(idx,number,host,chan,show_inact)
int idx,number,show_inact; char *host; struct chanset_t *chan;
{
  char ban[UHOSTLEN],ts[21],note[121],dates[81],from[81],s[41],*p;
  time_t expire_time,time_added,last_active,now;
  now=time(NULL);
  /* split off ban and expire-time */
  splitc(ban,host,':'); splitc(ts,host,':');
  if (ts[0]=='+') {
    /* new format */
    strcpy(ts,&ts[1]); expire_time=(time_t)atol(ts);
  }
  else {
    /* old format (ban originate time) */
    expire_time=(time_t)atol(ts);
    if (expire_time != 0L) expire_time+=(60*ban_time);
  }
  if (host[0]=='+') {
    /* extended format */
    strcpy(host,&host[1]);
    splitc(ts,host,':'); time_added=(time_t)atol(ts);
    splitc(ts,host,':'); last_active=(time_t)atol(ts);
    daysago(now,time_added,note);
    sprintf(dates,"Created %s",note);
    if (time_added < last_active) {
      strcat(dates,", last used ");
      daysago(now,last_active,note);
      strcat(dates,note);
    }
  }
  else { time_added=(time_t)0L; last_active=(time_t)0L; dates[0]=0; }
  splitc(from,host,':');
  strcpy(note,host);
  if (expire_time==0) strcpy(s,"(perm)");
  else {
    char s1[41];
    days(expire_time,now,s1);
    sprintf(s,"(expires %s)",s1);
  }
  if (note[0]) {
    /* fix spaces & commas */
    p=strchr(note,'~'); while (p!=NULL) { *p=' '; p=strchr(note,'~'); }
    p=strchr(note,'`'); while (p!=NULL) { *p=','; p=strchr(note,'`'); }
  }
  if (note[0]==' ') strcpy(note,&note[1]);
  if ((chan==NULL) || (isbanned(chan,ban))) {
    if (number>=0) {
      dprintf(idx,"  [%3d] %s %s\n",number,ban,s);
      dprintf(idx,"        %s: %s\n",from,note);
      if (dates[0]) dprintf(idx,"        %s\n",dates);
    }
    else {
      dprintf(idx,"BAN: %s %s\n",ban,s);
      dprintf(idx,"  %s: %s\n",from,note);
      if (dates[0]) dprintf(idx,"  %s\n",dates);
    }
  }
  else if (show_inact) {
    if (number>=0) {
      dprintf(idx,"! [%3d] %s %s\n",number,ban,s);
      dprintf(idx,"        %s: %s\n",from,note);
      if (dates[0]) dprintf(idx,"        %s\n",dates);
    }
    else {
      dprintf(idx,"BAN (inactive): %s %s\n",ban,s);
      dprintf(idx,"  %s: %s\n",from,note);
      if (dates[0]) dprintf(idx,"  %s\n",dates);
    }
  }
}

/* take host entry from ignore list and display it ignore-style */
void display_ignore(idx,number,host)
int idx,number; char *host;
{
  char ign[UHOSTLEN],ts[21],note[121],dates[81],from[81],s[41],*p;
  time_t expire_time,time_added,now;
  now=time(NULL);
  /* split off host and expire-time */
  splitc(ign,host,':'); splitc(ts,host,':');
  if (ts[0]=='+') {
    /* new format */
    strcpy(ts,&ts[1]); expire_time=(time_t)atol(ts);
  }
  else {
    /* old format (originate time) */
    expire_time=(time_t)atol(ts);
    if (expire_time != 0L) expire_time+=(60*ban_time);
  }
  splitc(from,host,':');
  if (!from[0]) { strcpy(from,host); host[0]=0; }    /* old */
  if (host[0]) {
    /* extended format */
    splitc(ts,host,':'); time_added=(time_t)atol(ts);
    daysago(now,time_added,note);
    sprintf(dates,"Started %s",note);
  }
  else { time_added=(time_t)0L; dates[0]=0; }
  strcpy(note,host);
  if (expire_time==0) strcpy(s,"(perm)");
  else {
    char s1[41];
    days(expire_time,now,s1);
    sprintf(s,"(expires %s)",s1);
  }
  if (note[0]) {
    /* fix spaces & commas */
    p=strchr(note,'~'); while (p!=NULL) { *p=' '; p=strchr(note,'~'); }
    p=strchr(note,'`'); while (p!=NULL) { *p=','; p=strchr(note,'`'); }
  }
  if (number>=0) {
    dprintf(idx,"  [%3d] %s %s\n",number,ign,s);
    if (note[0]) dprintf(idx,"        %s: %s\n",from,note);
    else dprintf(idx,"        placed by %s\n",from);
    if (dates[0]) dprintf(idx,"        %s\n",dates);
  }
  else {
    dprintf(idx,"IGNORE: %s %s\n",ign,s);
    if (note[0]) dprintf(idx,"  %s: %s\n",from,note);
    else dprintf(idx,"  placed by %s\n",from);
    if (dates[0]) dprintf(idx,"  %s\n",dates);
  }
}

void tell_bans(idx,show_inact,match)
int idx,show_inact; char *match;
{
  struct userrec *u; struct eggqueue *q; int k=1,cycle;
  char s[256],hst[UHOSTLEN],from[81],note[121]; struct chanset_t *chan;

  chan=findchan(dcc[idx].u.chat->con_chan);
  if (chan==NULL) chan=chanset;   /* pick arbitrary channel to view */
  if (chan==NULL) return;  /* i give up then. */
  if (show_inact)
    dprintf(idx,"Global bans:   (! = not active on %s)\n",chan->name);
  else dprintf(idx,"Global bans:\n");
  u=get_user_by_handle(userlist,BAN_NAME); cycle=0;
  if (u==NULL) { u=chan->bans; cycle++; }  /* skip to next cycle */
  while (u!=NULL) {
    if (cycle==1) {
      if (show_inact)
	dprintf(idx,"Channel bans for %s:   (! = not active, * = not placed by bot)\n",
		chan->name);
      else dprintf(idx,"Channel bans for %s:  (* = not placed by bot)\n",
		   chan->name);
    }
    q=u->host; while ((q!=NULL) && (strcasecmp(q->item,"none")!=0)) {
      strcpy(s,q->item); getbannote(s,from,note);
      strcpy(s,q->item); splitc(hst,s,':'); strcpy(s,q->item);
      if (match[0]) {
	if ((wild_match(match,hst)) || (wild_match(match,note)) ||
	    (wild_match(match,from)))
	  display_ban(idx,k,s,chan,1);
	k++;
      }
      else display_ban(idx,k++,s,chan,show_inact);
      q=q->next;
    }
    if (cycle==0) { u=chan->bans; cycle++; }
    else u=NULL;
  }
  tell_chanbans(chan,idx,k,match);
  if ((!show_inact) && (!match[0]))
    dprintf(idx,"Use '.bans all' to see the total list.\n");
}

/* list the ignores and how long they've been active */
void tell_ignores(idx,match)
int idx; char *match;
{
  struct userrec *u; struct eggqueue *q; int k=1;
  char s[256],hst[UHOSTLEN],from[81],note[121];
  u=get_user_by_handle(userlist,IGNORE_NAME);
  if (u==NULL) {
    dprintf(idx,"No ignores.\n");
    return;
  }
  q=u->host;
  if (q==NULL) dprintf(idx,"No ignores.\n");
  dprintf(idx,"Currently ignoring:\n");
  while ((q!=NULL) && (strcasecmp(q->item,"none")!=0)) {
    strcpy(s,q->item); getignorenote(s,from,note);
    strcpy(s,q->item); splitc(hst,s,':'); strcpy(s,q->item);
    if (match[0]) {
      if ((wild_match(match,hst)) || (wild_match(match,note)) ||
          (wild_match(match,from)))
        display_ignore(idx,k,s);
      k++;
    }
    else display_ignore(idx,k++,s);
    q=q->next;
  }
}

/* check for expired timed-ignores */
void check_expired_ignores()
{
  struct userrec *u; struct eggqueue *q; char s[UHOSTLEN],host[UHOSTLEN];
  time_t now,expire_time;
  context;
  u=get_user_by_handle(userlist,IGNORE_NAME); now=time(NULL);
  if (u==NULL) return;
  q=u->host; if (q==NULL) return;
  while (q!=NULL) {
    strcpy(s,q->item); splitc(host,s,':');
    if (s[0]=='+') {
      /* new-style */
      strcpy(s,&s[1]);
      expire_time=(time_t)atol(s);
    } 
    else {
      expire_time=(time_t)atol(s);
      if (expire_time != 0L) expire_time+=(60*ignore_time);
    }
    if ((expire_time!=0L) && (now >= expire_time)) {
      /* expired */
      putlog(LOG_MISC,"*","No longer ignoring %s (expired)",host);
      delignore(host); u=get_user_by_handle(userlist,IGNORE_NAME);
      if (u!=NULL) q=u->host;  /* start over, check for more */
    }
    if ((u!=NULL) && (q!=NULL)) q=q->next; else q=NULL;
  }
}

/* check for expired timed-bans */
void check_expired_bans()
{
  struct userrec *u; struct eggqueue *q; struct chanset_t *chan;
  char s[256],host[UHOSTLEN],note[81]; time_t now,ti; int expired;
  context;
  now=time(NULL);
  u=get_user_by_handle(userlist,BAN_NAME);
  if (u==NULL) q=NULL; else q=u->host;
  while ((q!=NULL) && (strcmp(q->item,"none")!=0)) {
    strcpy(s,q->item); splitc(host,s,':');
    splitc(note,s,':');
    if (note[0]=='+') {
      /* new style */
      strcpy(note,&note[1]); ti=(time_t)atol(note);
      expired = ((ti != 0L) && (ti <= now));   /* new style */
    }
    else {
      ti=(time_t)atol(note);
      expired = ((ti != 0L) && (now-ti >= 60*ban_time));
    }
    if (expired) {
      putlog(LOG_MISC,"*","No longer banning %s (expired)",host);
      chan=chanset; while (chan!=NULL) {
	add_mode(chan,'-','b',host); chan=chan->next;
      }
      delban(host); u=get_user_by_handle(userlist,BAN_NAME);
      if (u!=NULL) q=u->host;  /* start over, check for more */
      else q=NULL;
    }
    else q=q->next;
  }
  /* check for specific channel-domain bans expiring */
  chan=chanset; while (chan!=NULL) {
    q=chan->bans->host; while ((q!=NULL) && (strcmp(q->item,"none")!=0)) {
      strcpy(s,q->item); splitc(host,s,':');
      splitc(note,s,':');
      if (note[0]=='+') {
	/* new style */
	strcpy(note,&note[1]); ti=(time_t)atol(note);
	expired = ((ti != 0L) && (ti <= now));   /* new style */
      }
      else {
	ti=(time_t)atol(note);
	expired = ((ti != 0L) && (now-ti >= 60*ban_time));
      }
      if (expired) {
	putlog(LOG_MISC,chan->name,"No longer banning %s on %s (expired)",
	       host,chan->name);
	add_mode(chan,'-','b',host);
	u_delban(chan->bans,host); q=chan->bans->host;
      }
      else q=q->next;
    }
    chan=chan->next;
  }
}

/* theres an entire new function for this at the end of the file */
#ifndef BEL_NEWLINKS
/* see if there are any bots that need to be linked */
/* priority is: +sh (highest), +h, +a (lowest) */
/* only link ONE +h bot at a time */
/* only link ONE +a bot at a time, and only if the +h are unreachable,
   and then keep trying for the +h bot(s) */
void autolink_cycle(start)
char *start;
{
  struct userrec *u=userlist,*autc=NULL; static int cycle=0;
  int got_hub=0,got_alt=0,linked,ready=0,i;
  context;
  /* don't start a new cycle if some links are still pending */
  if (start==NULL) {
    for (i=0; i<dcc_total; i++) {
      if (dcc[i].type==DCC_BOT_NEW) return;
      if (dcc[i].type==DCC_FORK) if (dcc[i].u.fork->type==DCC_BOT) return;
    }
  }
  debug1("autolink: begin at: %s",start==NULL?"(null)":start);
  if (start==NULL) { ready=1; cycle=0; }   /* new run through the user list */
  while (u!=NULL) {
    if ((flags_eq(USER_BOT|BOT_HUB,u->flags)) ||
	(flags_eq(USER_BOT|BOT_ALT,u->flags))) {
      linked=0;
      for (i=0; i<dcc_total; i++) {
	if (strcasecmp(dcc[i].nick,u->handle)==0) {
	  if (dcc[i].type==DCC_BOT) linked=1;
	  if (dcc[i].type==DCC_BOT_NEW) linked=1;
	  if (dcc[i].type==DCC_FORK) if (dcc[i].u.fork->type==DCC_BOT)
	    linked=1;
	}
      }
      /* check for possibility that share-bot IS linked in, but not to me. */
      /* if +sh, drop that uplink, so i can link directly on the next round. */
      /* (if +sa, don't bother.)  */
      if ((!linked) && (in_chain(u->handle)) && (u->flags&BOT_SHARE) &&
	  (u->flags&BOT_HUB) && (strcasecmp(origbotname,u->handle)!=0)) {
	int q=nextbot(u->handle),qatr=get_attr_handle(dcc[q].nick);
	debug2("autolink: want +sh bot %s, unlinking %s",u->handle,
	       dcc[q].nick);
	putlog(LOG_BOTS,"*","Restructure to get sharebot %s",u->handle);
	if ((qatr & (BOT_HUB|BOT_SHARE)) == (BOT_HUB|BOT_SHARE)) {
	  /* ok, i can't be linked to both as-is, so make the other one move */
	  tandout("chat %s Bringing sharebot %s to me...\n",origbotname,
		  u->handle);
	  chatout("*** Bringing sharebot %s to me...\n",u->handle);
	  tprintf(dcc[q].sock,"unlink %s %s %s\n",origbotname,
		  lastbot(u->handle),u->handle);
	} else {
	  /* disconnect from botnet and reconnect there */
	  tprintf(dcc[q].sock,"bye\n");
	  tandout_but(q,"unlinked %s\n",dcc[q].nick);
	  tandout_but(q,"chat %s Disconnected %s (moving to %s)\n",
		      origbotname,dcc[q].nick,u->handle);
	  chatout("*** Disconnected %s (moving to %s)\n",dcc[q].nick,
		  u->handle);
	  cancel_user_xfer(q);
	  rembot(dcc[q].nick,dcc[q].nick); unvia(q,dcc[q].nick);
	  killsock(dcc[q].sock); lostdcc(q);
	}
	/* once unlinking is done, i'll try to autoconnect later */
      }
      /* is this a HUB we have linked? */
      if ((u->flags&BOT_HUB) && (linked)) {
	got_hub=1;
	if ((autc!=NULL) && (autc->flags&BOT_HUB) &&
	    !(autc->flags&BOT_SHARE)) {
	  /* great... already have a hub bot in line to be linked */
	  /* start over from there */
	  debug2("autolink: wanted hub %s, but %s is connected",autc->handle,
		 u->handle);
	  u=autc; linked=1; autc=NULL;
	}
      }
      /* is this an ALT we have linked? */
      if ((u->flags&BOT_ALT) && (linked)) got_alt=1;
      /* don't link anything if we have our +h bot */
      if ((got_hub) && !(u->flags&BOT_SHARE)) linked=1;
      /* if we've got an alt linked, don't link another one */
      if ((got_alt) && (u->flags&BOT_ALT) && !(u->flags&BOT_SHARE)) linked=1;
      if ((!linked) && (u->flags&BOT_ALT) && (in_chain(u->handle))) linked=1;
      /* don't try hubs on 2nd cycle */
      if ((u->flags&BOT_HUB) && (cycle==1)) linked=1;
      if ((!linked) && (ready)) {
	debug3("autolink: considering link to %s (+%s%c)",u->handle,
	       (u->flags&BOT_SHARE)?"s":"",
	       (u->flags&BOT_HUB)?'h':((u->flags&BOT_ALT)?'a':'?'));
	if (autc==NULL) autc=u;
	else if ((u->flags&BOT_HUB) && !(autc->flags&BOT_HUB) && (cycle==0))
	  autc=u;  /* hub priority over alt (except 2nd cycle) */
	else if ((u->flags&BOT_SHARE) && !(autc->flags&BOT_SHARE))
	  autc=u;  /* share bots get priority */
	if (autc==u) debug1("autolink: decided on %s",u->handle);
      }
      /* did we make it where we're supposed to start?  yay! */
      if (!ready) if (strcasecmp(u->handle,start)==0) { 
	ready=1; autc=NULL;
	/* if starting point is a +a bot, must be in 2nd cycle */
	if (u->flags&BOT_ALT) {
	  debug0("autolink: leap to cycle 2 (continuation)");
	  cycle=1;
	}
      }
    }
    if ((u->flags&USER_BOT) && (u->flags&BOT_REJECT))
      if (in_chain(u->handle)) {
	/* get rid of nasty reject bot */
	reject_bot(u->handle);
      }
    u=u->next;
    if ((u==NULL) && (autc==NULL) && (cycle==0) && (!got_hub)) {
      /* end of 1st run of userlist -- no succeses */
      /* try again, this time go for +a */
      cycle++; u=userlist;
      debug0("autolink: cycle 2 (no +h, looking for +a)");
    }
  }
  if ((got_hub) && (cycle==0)) {
    autc=NULL;
    debug0("autolink: have a hub, cycle 1 -- no auto-link");
  }
  if (((got_hub) || (got_alt)) && (cycle==1)) {
    autc=NULL;
    debug0("autolink: have hub/alt, cycle 2 -- no auto-link");
  }
  if (autc!=NULL) {
    debug1("autolink: trying %s",autc->handle);
    botlink("",-1,autc->handle);   /* try autoconnect */
  }
  else debug0("autolink: done trying");
}
#endif

/* erase old user list, switch to new one */
void finish_share(idx)
int idx;
{
  struct userrec *u; int i,j=0;
  for (i=0; i<dcc_total; i++)
    if ((strcasecmp(dcc[i].nick,dcc[idx].host)==0) &&
	(dcc[i].type==DCC_BOT))
      j=i;
  if (j==0) return;   /* oh well. */
  dcc[j].u.bot->status&=~STAT_GETTING;
  /* copy the bots over */
  u=dup_userlist(1);
  /* read the rest in */
  if (!readuserfile(dcc[idx].u.xfer->filename,&u)) {
    putlog(LOG_MISC,"*","CAN'T READ NEW USERFILE");
    return;
  }
  reaffirm_owners();   /* make sure my owners are +n */
  unlink(dcc[idx].u.xfer->filename);   /* done with you! */
  putlog(LOG_MISC,"*","Userlist transfer complete; switched over.");
  clear_userlist(userlist);
  userlist=u;
  clear_chanlist();
  lastuser=banu=ignu=NULL;
}

/* begin the user transfer process */
void start_sending_users(idx)
int idx;
{
  struct userrec *u; char s[161]; int i; struct eggqueue *q;
  context;
  sprintf(s,".share.user%lu",time(NULL));
  u=dup_userlist(0);  /* only non-bots */
  write_tmp_userfile(s,u);
  clear_userlist(u);
  i=raw_dcc_send(s,"*users","(users)",s);
  if (i>0) {   /* abort */
    unlink(s);
    tprintf(dcc[idx].sock,"error Can't send userfile to you (internal error)\n");
    dcc[idx].u.bot->status&=~STAT_SHARE;
    return;
  }
  dcc[idx].u.bot->status|=STAT_SENDING;
  i=dcc_total-1;
  strcpy(dcc[i].host,dcc[idx].nick);  /* store bot's nick */
  tprintf(dcc[idx].sock,"ufsend %lu %d %lu\n",ntohl(getmyip()),dcc[i].port,
	  dcc[i].u.xfer->length);
  /* start up a tbuf to queue outgoing changes for this bot until the */
  /* userlist is done transferring */
  new_tbuf(dcc[idx].nick);
  /* immediately, queue bot hostmasks & addresses (jump-start) */
  u=userlist; while (u!=NULL) {
    if (u->flags & USER_BOT) {
      /* send hostmasks */
      q=u->host; while (q!=NULL) {
	if (strcmp(q->item,"none")!=0) {
	  sprintf(s,"+bothost %s %s\n",u->handle,q->item);
	  q_tbuf(dcc[idx].nick,s);
	}
	q=q->next;
      }
      /* send address */
      sprintf(s,"chaddr %s %s\n",u->handle,u->info);
      q_tbuf(dcc[idx].nick,s);
    }
    u=u->next;
  }
  /* wish could unlink the file here to avoid possibly leaving it lying */
  /* around, but that messes up NFS clients. */
}

/* update a user's last signon, by host */
void update_laston(host)
char *host;
{
  struct userrec *u;
  u=get_user_by_host(host);
  if (u==NULL) return;
  u->laston=time(NULL);
}

/* return laston time */
void get_handle_laston(nick,n)
char *nick; time_t *n;
{
  struct userrec *u;
  u=get_user_by_handle(userlist,nick);
  if (u==NULL) *n=0L;
  else *n = u->laston;
}

void set_handle_laston(nick,n)
char *nick; time_t n;
{
  struct userrec *u;
  u=get_user_by_handle(userlist,nick);
  if (u==NULL) return;
  u->laston=n;
}

void set_handle_laston2(bu,nick,n)
struct userrec *bu; char *nick; time_t n;
{
  struct userrec *u;
  u=get_user_by_handle(bu,nick);
  if (u==NULL) return;
  u->laston=n;
}

/* since i was getting a ban list, i assume i'm chop */
/* recheck_bans makes sure that all who are 'banned' on the userlist are
   actually in fact banned on the channel */
void recheck_bans(chan)
struct chanset_t *chan;
{
  struct userrec *u; struct eggqueue *q; char s[256],host[UHOSTLEN]; int i;
  context;
  if (chan->stat&CHAN_DYNAMICBANS) return;
  for (i=0; i<2; i++) {
    if (i==0) u=get_user_by_handle(userlist,BAN_NAME);
    else u=chan->bans;
    if (u!=NULL) {
      q=u->host; while ((q!=NULL) && (strcmp(q->item,"none")!=0)) {
	strcpy(s,q->item); splitc(host,s,':');
	if (!host[0]) strcpy(host,s);
	if (!isbanned(chan,host)) add_mode(chan,'+','b',host);
	q=q->next;
      }
    }
  }
}

/* find info line for a user and display it if there is one */
void showinfo(chan,who,nick)
char *who,*nick; struct chanset_t *chan;
{
  char s[121];
  if (get_attr_handle(who) & USER_BOT) return;
  get_handle_info(who,s);
  if (s[0]=='@') strcpy(s,&s[1]);
  if (s[0]) mprintf(serv,"PRIVMSG %s :[%s] %s\n",chan->name,nick,s);
}

void tell_user(idx,u,master)
int idx; struct userrec *u; int master;
{
  char s[81],s1[81]; struct eggqueue *q; time_t now; int n;
  if (strcmp(u->handle,BAN_NAME)==0) return;
  if (strcmp(u->handle,IGNORE_NAME)==0) return;
  flags2str(u->flags,s);
  n=num_notes(u->handle);
  if (u->laston == 0L) strcpy(s1,"never");
  else {
    now=time(NULL)-(u->laston); strcpy(s1,ctime(&(u->laston)));
    if (now>86400) { s1[7]=0; strcpy(&s1[11],&s1[4]); strcpy(s1,&s1[8]); }
    else { s1[16]=0; strcpy(s1,&s1[11]); }
  }
  dprintf(idx,"%-10s%-5s%-25s%5d %s\n",u->handle,u->pass[0]=='-'?"no":"yes",
	  s,n,s1);
  s[0]=0; q=u->host;
  while (q!=NULL) {
    if (!s[0]) sprintf(s,"          %s",q->item);
    else {
      if (strlen(s)+strlen(q->item)+2>60) {
	dprintf(idx,"%s\n",s);
	sprintf(s,"          %s",q->item);
      }
      else {
	strcat(s,", "); strcat(s,q->item);
      }
    }
    q=q->next;
  }
  if (s[0]) dprintf(idx,"%s\n",s);
  if ((master) && (u->comment!=NULL))
    dprintf(idx,"          COMMENT: %s\n",u->comment);
  if (u->email!=NULL) dprintf(idx,"          EMAIL: %s\n",u->email);
  if (u->flags&USER_BOT) {
    if (u->info!=NULL) dprintf(idx,"          ADDRESS: %s\n",u->info);
  }
  else {
    if (u->info!=NULL) dprintf(idx,"          INFO: %s\n",u->info);
  }
}

/* show user by ident */
void tell_user_ident(idx,id,master)
int idx; char *id; int master;
{
  struct userrec *u;
  u=get_user_by_handle(userlist,id);
  if (u==NULL) u=get_user_by_host(id);
  if (u==NULL) {
    dprintf(idx,"Can't find anyone matching that.\n");
    return;
  }
  dprintf(idx,"HANDLE    PASS FLAGS                    NOTES LAST\n");
  tell_user(idx,u,master);
}

/* match string: wildcard to match nickname or hostmasks */
/*               +attr to find all with attr */
void tell_users_match(idx,mtch,start,limit,master)
int idx; char *mtch; int start,limit,master;
{
  struct userrec *u=userlist; int fnd=0,cnt; struct eggqueue *q;
  char s[UHOSTLEN];
  dprintf(idx,"*** Matching '%s':\n",mtch); cnt=0;
  dprintf(idx,"HANDLE    PASS FLAGS                    NOTES LAST\n");
  if (start>1) dprintf(idx,"(skipping first %d)\n",start-1);
  while (u!=NULL) {
    if (mtch[0]=='+') {
      fnd=str2flags(&mtch[1]);
      if (((u->flags&fnd)==fnd) && (fnd!=0)) {
	cnt++;
	if ((cnt<=limit) && (cnt>=start)) tell_user(idx,u,master);
	if (cnt==limit+1)
	  dprintf(idx,"(more than %d matches; list truncated)\n",limit);
      }
    }
    else if (wild_match(mtch,u->handle)) {
      cnt++;
      if ((cnt<=limit) && (cnt>=start)) tell_user(idx,u,master);
      if (cnt==limit+1)
	dprintf(idx,"(more than %d matches; list truncated)\n",limit);
    }
    else {
      fnd=0; q=u->host; while (q!=NULL) {
	if ((wild_match(mtch,q->item)) && (!fnd)) {
	  cnt++; fnd=1;
	  if ((cnt<=limit) && (cnt>=start)) {
	    if (strcmp(u->handle,BAN_NAME)==0) {
	      strcpy(s,q->item); fnd=0;
	      display_ban(idx,-1,s,NULL,1);
	    }
	    else if (strcmp(u->handle,IGNORE_NAME)==0) {
	      strcpy(s,q->item); fnd=0;
	      display_ignore(idx,-1,s);
	    }
	    else tell_user(idx,u,master);
	  }
	  if (cnt==limit+1)
	    dprintf(idx,"(more than %d matches; list truncated)\n",limit);
	}
	q=q->next;
      }
    }
    u=u->next;
  }
  dprintf(idx,"--- Found %d match%s.\n",cnt,cnt==1?"":"es");
}

/*
   tagged lines in the user file:
   #  (comment)
   ;  (comment)
   -  hostmask(s)
   +  email
   *  dcc directory
   =  comment
   :  info line
   .  xtra (Tcl)
   !  channel-specific
*/
      
int readuserfile(file,ret)
char *file; struct userrec **ret;
{
  char *p,s[181],lasthand[181],host[181],attr[181],pass[181],code[181];
  FILE *f; unsigned int flags; struct userrec *bu; int convpw=0;
  char ignored[512];
  context;
  bu=(*ret); ignored[0]=0;
  if (bu==userlist) {
    clear_chanlist(); lastuser=banu=ignu=NULL;
  }
  lasthand[0]=0; f=fopen(file,"r");
  if (f==NULL) return 0;
  noshare=1;
  context;
  /* read opening comment */
  fgets(s,180,f);
  if (s[1]<'2') {
    convpw=1;
    putlog(LOG_MISC,"*","* Old userfile (unencrypted passwords)");
    putlog(LOG_MISC,"*","* Encrypting as I load ...");
  }
  if (s[1]>'2') fatal("Don't understand userfile encoding!");
  while (!feof(f)) {
    fgets(s,180,f);
    if (!feof(f)) {
      rmspace(s);
      if ((s[0]!='#') && (s[0]!=';') && (s[0])) {
	nsplit(code,s); rmspace(code); rmspace(s);
	if (strcasecmp(code,"-")==0) {
	  if (lasthand[0]) {
	    p=strchr(s,','); while (p!=NULL) {
	      splitc(code,s,','); rmspace(code); rmspace(s);
	      if (code[0]) addhost_by_handle2(bu,lasthand,code);
	      p=strchr(s,',');
	    }
	    /* channel bans are never stacked with , */
	    if (s[0]) {
	      if ((lasthand[0]=='#') || (lasthand[0]=='+'))
		restore_chanban(lasthand,s);
	      else addhost_by_handle2(bu,lasthand,s);
	    }
	  }
	}
	else if (strcasecmp(code,"+")==0) {
	  if (lasthand[0]) {
	    set_handle_email(bu,lasthand,s);
	  }
	}
	else if (strcasecmp(code,"*")==0) {
	  if (lasthand[0]) {
	    set_handle_dccdir(bu,lasthand,s);
	  }
	}
	else if (strcasecmp(code,"=")==0) {
	  if (lasthand[0]) {
	    set_handle_comment(bu,lasthand,s);
	  }
	}
	else if (strcasecmp(code,":")==0) {
	  if (lasthand[0]) {
	    set_handle_info(bu,lasthand,s);
	  }
	}
	else if (strcasecmp(code,".")==0) {
	  if (lasthand[0]) {
	    add_handle_xtra(bu,lasthand,s);
	  }
	}
	else if (strcasecmp(code,"!")==0) {
	  /* ! #chan laston flags */
	  /* not implemented yet */
	  putlog(LOG_MISC,"*","* Channel-specific user data for '%s'! %s",
		 lasthand,"(not supported yet)");
	}
	else if (strncmp(code,"::",2)==0) {
	  /* channel-specific bans */
	  strcpy(lasthand,&code[2]);
	  if (!defined_channel(lasthand)) {
	    strcat(ignored,lasthand); strcat(ignored," ");
	    lasthand[0]=0;
	  }
	}
	else {
	  if (convpw) {
	    nsplit(host,s); rmspace(host); rmspace(s);  /* unused */
	  }
	  nsplit(pass,s); rmspace(pass); rmspace(s);
	  nsplit(attr,s); rmspace(attr); rmspace(s);
	  if ((!s[0]) || (!attr[0]) || (!pass[0]) ||
	      ((!host[0]) && convpw)) {
	    putlog(LOG_MISC,"*","* Corrupt user record '%s'!",code);
	    lasthand[0]=0;
	  }
	  else if (is_user2(bu,code)) {
	    putlog(LOG_MISC,"*","* Duplicate user record '%s'!",code);
	    lasthand[0]=0;
	  }
	  else {
	    flags=str2flags(attr); strcpy(lasthand,code);
	    if (convpw) {
	      if (strcasecmp(host,"$placeholder$")==0) host[0]=0;
	      if (strcasecmp(host,"none")==0) host[0]=0;
	    }
	    else host[0]=0;
	    if (strlen(code)>9) code[9]=0;
	    if (strlen(pass)>20) {
	      putlog(LOG_MISC,"*","* Corrupted password for %s; reset.",
		     code);
	      strcpy(pass,"-");
	    }
	    if (convpw) {
	      if (strcmp(pass,"nopass")==0) strcpy(pass,"-");
	      else if (!(flags & USER_BOT)) encrypt_pass(pass,pass);
	    }
	    bu=adduser(bu,code,host,pass,flags);
	    set_handle_laston2(bu,code,atol(s));
	  }
	}
      }
    }
  }
  noshare=0;
  context;
  fclose(f);
  (*ret)=bu;
  if (ignored[0]) {
    putlog(LOG_MISC,"*","Ignored bans for channel(s): %s",ignored);
  }
  return 1;
}

#ifdef BEL_NEWLINKS
/* New methodology - cycle through list 3 times */
/* 1st time scan for +sh bots and link if none connected */
/* 2nd time scan for +h bots */
/* 3rd time scan for +a/+h bots */
void autolink_cycle(start)
char *start;
{
  struct userrec *u=userlist,*autc=NULL; static int cycle=0;
  int got_hub=0,got_alt=0,got_shared=0,linked,ready=0,i;
  context;
  /* don't start a new cycle if some links are still pending */
  if (start==NULL) {
    for (i=0; i<dcc_total; i++) {
      if (dcc[i].type==DCC_BOT_NEW) return;
      if (dcc[i].type==DCC_FORK) if (dcc[i].u.fork->type==DCC_BOT) return;
    }
  }
  debug1("autolink: begin at: %s",start==NULL?"(null)":start);
  if (start==NULL) { ready=1; cycle=0; }   /* new run through the user list */
  while (u!=NULL) {
    if ((flags_eq(USER_BOT|BOT_HUB,u->flags)) ||
        (flags_eq(USER_BOT|BOT_ALT,u->flags))) {
      linked=0;
      for (i=0; i<dcc_total; i++) {
        if (strcasecmp(dcc[i].nick,u->handle)==0) {
          if (dcc[i].type==DCC_BOT) linked=1;
          if (dcc[i].type==DCC_BOT_NEW) linked=1;
          if (dcc[i].type==DCC_FORK) if (dcc[i].u.fork->type==DCC_BOT)
            linked=1;
        }
       }

      if (flags_eq(BOT_HUB|BOT_SHARE,u->flags)) {
        if (linked)
           got_shared = 1;
        else if ((cycle == 0) && ready && !autc)
           autc = u;
      }
      else if (u->flags&BOT_HUB && cycle > 0) {
         if (linked)
           got_hub = 1;
         else if ((cycle==1) && ready && !autc)
           autc = u;
      }
      else if (u->flags&BOT_ALT && cycle == 2) {
        if (linked)
           got_alt = 1;
        else if (!in_chain(u->handle) && ready && !autc)
            autc=u;
       }

       /* did we make it where we're supposed to start?  yay! */
       if (!ready) if (strcasecmp(u->handle,start)==0) {
        ready=1; autc=NULL;
        /* if starting point is a +h bot, must be in 2nd cycle */
        if ((u->flags&BOT_HUB)&&(!(u->flags&BOT_SHARE))) {
          debug0("autolink: leap to cycle 2 (continuation)");
          cycle=1;
        }
        /* if starting point is a +a bot, must be in 3rd cycle */
        if (u->flags&BOT_ALT) {
          debug0("autolink: leap to cycle 2 (continuation)");
          cycle=2;
        }
       }
     }
     if ((u->flags&USER_BOT) && (u->flags&BOT_REJECT))
      if (in_chain(u->handle)) {
        /* get rid of nasty reject bot */
        reject_bot(u->handle);
       }
     u=u->next;
    if ((u==NULL) && (autc==NULL)) {
      if ((cycle==0) && (!got_shared)) {
         debug0("autolink: cycle 2 (no +sh, looking for +h)");
         cycle++; u= userlist;
      }
      else if ((cycle==1) && (!(got_shared||got_hub))) {
        cycle++; u=userlist;
        debug0("autolink: cycle 3 (no +h, looking for +a)");
      }
     }
   }
  if ((got_shared || got_hub) && (cycle==1)) {
     autc=NULL;
    debug0("autolink: have a hub, cycle 2 -- no auto-link");
   }
  if ((got_shared || got_hub || got_alt) && (cycle==2)) {
     autc=NULL;
    debug0("autolink: have hub/alt, cycle 3 -- no auto-link");
  }
  if (autc!=NULL) {
    debug1("autolink: trying %s",autc->handle);
    botlink("",-1,autc->handle);   /* try autoconnect */
  }
  else debug0("autolink: done trying");
}
#endif
