/*
 * userchan.c -- part of channels.mod
 */
/*
 * Copyright (C) 1997 Robey Pointer
 * Copyright (C) 1999 - 2018 Eggheads Development Team
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

struct chanuserrec *get_chanrec(struct userrec *u, char *chname)
{
  struct chanuserrec *ch;

  for (ch = u->chanrec; ch; ch = ch->next)
    if (!rfc_casecmp(ch->channel, chname))
      return ch;
  return NULL;
}

static struct chanuserrec *add_chanrec(struct userrec *u, char *chname)
{
  struct chanuserrec *ch = NULL;

  if (findchan_by_dname(chname)) {
    ch = user_malloc(sizeof(struct chanuserrec));

    ch->next = u->chanrec;
    u->chanrec = ch;
    ch->info = NULL;
    ch->flags = 0;
    ch->flags_udef = 0;
    ch->laston = 0;
    strlcpy(ch->channel, chname, sizeof ch->channel);
    if (!noshare && !(u->flags & USER_UNSHARED))
      shareout(findchan_by_dname(chname), "+cr %s %s\n", u->handle, chname);
  }
  return ch;
}

static void add_chanrec_by_handle(struct userrec *bu, char *hand, char *chname)
{
  struct userrec *u;

  u = get_user_by_handle(bu, hand);
  if (!u)
    return;
  if (!get_chanrec(u, chname))
    add_chanrec(u, chname);
}

static void get_handle_chaninfo(char *handle, char *chname, char *s)
{
  struct userrec *u;
  struct chanuserrec *ch;

  u = get_user_by_handle(userlist, handle);
  if (u == NULL) {
    s[0] = 0;
    return;
  }
  ch = get_chanrec(u, chname);
  if (ch == NULL) {
    s[0] = 0;
    return;
  }
  if (ch->info == NULL) {
    s[0] = 0;
    return;
  }
  strcpy(s, ch->info);
  return;
}

static void set_handle_chaninfo(struct userrec *bu, char *handle,
                                char *chname, char *info)
{
  struct userrec *u;
  struct chanuserrec *ch;
  struct chanset_t *cst;

  u = get_user_by_handle(bu, handle);
  if (!u)
    return;
  ch = get_chanrec(u, chname);
  if (!ch) {
    add_chanrec_by_handle(bu, handle, chname);
    ch = get_chanrec(u, chname);
  }
  if (ch->info != NULL)
    nfree(ch->info);
  if (info && info[0]) {
    ch->info = (char *) user_malloc(strlen(info) + 1);
    strcpy(ch->info, info);
  } else
    ch->info = NULL;
  cst = findchan_by_dname(chname);
  if ((!noshare) && (bu == userlist) &&
      !(u->flags & (USER_UNSHARED | USER_BOT)) && share_greet) {
    shareout(cst, "chchinfo %s %s %s\n", handle, chname, info ? info : "");
  }
}

static void del_chanrec(struct userrec *u, char *chname)
{
  struct chanuserrec *ch = u->chanrec, *lst = NULL;

  while (ch) {
    if (!rfc_casecmp(chname, ch->channel)) {
      if (lst == NULL)
        u->chanrec = ch->next;
      else
        lst->next = ch->next;
      if (ch->info != NULL)
        nfree(ch->info);
      nfree(ch);
      if (!noshare && !(u->flags & USER_UNSHARED))
        shareout(findchan_by_dname(chname), "-cr %s %s\n", u->handle, chname);
      return;
    }
    lst = ch;
    ch = ch->next;
  }
}

static void set_handle_laston(char *chan, struct userrec *u, time_t n)
{
  struct chanuserrec *ch;

  if (!u)
    return;
  touch_laston(u, chan, n);
  ch = get_chanrec(u, chan);
  if (!ch)
    return;
  ch->laston = n;
}

/* Is this mask sticky?
 */
static int u_sticky_mask(maskrec *u, char *uhost)
{
  for (; u; u = u->next)
    if (!rfc_casecmp(u->mask, uhost))
      return (u->flags & MASKREC_STICKY);
  return 0;
}

/* Set sticky attribute for a mask.
 */
static int u_setsticky_mask(struct chanset_t *chan, maskrec *u, char *uhost,
                            int sticky, char *botcmd)
{
  int j;

  if (str_isdigit(uhost))
    j = atoi(uhost);
  else
    j = -1;

  while (u) {
    if (j >= 0)
      j--;

    if (!j || ((j < 0) && !rfc_casecmp(u->mask, uhost))) {
      if (sticky > 0)
        u->flags |= MASKREC_STICKY;
      else if (!sticky)
        u->flags &= ~MASKREC_STICKY;
      else /* We don't actually want to change, just skip over */
        return 0;
      if (!j)
        strcpy(uhost, u->mask);

      if (!noshare)
        shareout(chan, "%s %s %d %s\n", botcmd, uhost, sticky,
                 (chan) ? chan->dname : "");
      return 1;
    }

    u = u->next;
  }

  if (j >= 0)
    return -j;

  return 0;
}

/* Merge of u_equals_ban(), u_equals_exempt() and u_equals_invite().
 *
 * Returns:
 *   0       not a ban
 *   1       temporary ban
 *   2       perm ban
 */
static int u_equals_mask(maskrec *u, char *mask)
{
  for (; u; u = u->next)
    if (!rfc_casecmp(u->mask, mask)) {
      if (u->flags & MASKREC_PERM)
        return 2;
      else
        return 1;
    }
  return 0;
}

static int u_match_mask(maskrec *rec, char *mask)
{
  for (; rec; rec = rec->next)
    if (match_addr(rec->mask, mask))
      return 1;
  return 0;
}

static int u_delban(struct chanset_t *c, char *who, int doit)
{
  int j, i = 0;
  maskrec *t;
  maskrec **u = (c) ? &c->bans : &global_bans;
  char temp[256];

  if (!strchr(who, '!') && str_isdigit(who)) {
    j = atoi(who);
    j--;
    for (; (*u) && j; u = &((*u)->next), j--);
    if (*u) {
      strlcpy(temp, (*u)->mask, sizeof temp);
      i = 1;
    } else
      return -j - 1;
  } else {
    /* Find matching host, if there is one */
    for (; *u && !i; u = &((*u)->next))
      if (!rfc_casecmp((*u)->mask, who)) {
        strlcpy(temp, who, sizeof temp);
        i = 1;
        break;
      }
    if (!*u)
      return 0;
  }
  if (i && doit) {
    if (!noshare) {
      char *mask = str_escape(temp, ':', '\\');

      if (mask) {
        /* Distribute chan bans differently */
        if (c)
          shareout(c, "-bc %s %s\n", c->dname, mask);
        else
          shareout(NULL, "-b %s\n", mask);
        nfree(mask);
      }
    }
    if (lastdeletedmask)
      nfree(lastdeletedmask);
    lastdeletedmask = nmalloc(strlen((*u)->mask) + 1);
    strcpy(lastdeletedmask, (*u)->mask);
    nfree((*u)->mask);
    if ((*u)->desc)
      nfree((*u)->desc);
    if ((*u)->user)
      nfree((*u)->user);
    t = *u;
    *u = (*u)->next;
    nfree(t);
  }
  return i;
}

static int u_delexempt(struct chanset_t *c, char *who, int doit)
{
  int j, i = 0;
  maskrec *t, **u = c ? &(c->exempts) : &global_exempts;
  char temp[256];

  if (!strchr(who, '!') && str_isdigit(who)) {
    j = atoi(who);
    j--;
    for (; (*u) && j; u = &((*u)->next), j--);
    if (*u) {
      strlcpy(temp, (*u)->mask, sizeof temp);
      i = 1;
    } else
      return -j - 1;
  } else {
    /* Find matching host, if there is one */
    for (; *u && !i; u = &((*u)->next))
      if (!rfc_casecmp((*u)->mask, who)) {
        strlcpy(temp, who, sizeof temp);
        i = 1;
        break;
      }
    if (!*u)
      return 0;
  }
  if (i && doit) {
    if (!noshare) {
      char *mask = str_escape(temp, ':', '\\');

      if (mask) {
        /* Distribute chan exempts differently */
        if (c)
          shareout(c, "-ec %s %s\n", c->dname, mask);
        else
          shareout(NULL, "-e %s\n", mask);
        nfree(mask);
      }
    }
    if (lastdeletedmask)
      nfree(lastdeletedmask);
    lastdeletedmask = nmalloc(strlen((*u)->mask) + 1);
    strcpy(lastdeletedmask, (*u)->mask);
    nfree((*u)->mask);
    if ((*u)->desc)
      nfree((*u)->desc);
    if ((*u)->user)
      nfree((*u)->user);
    t = *u;
    *u = (*u)->next;
    nfree(t);
  }
  return i;
}

static int u_delinvite(struct chanset_t *c, char *who, int doit)
{
  int j, i = 0;
  maskrec *t;
  maskrec **u = c ? &(c->invites) : &global_invites;
  char temp[256];

  if (!strchr(who, '!') && str_isdigit(who)) {
    j = atoi(who);
    j--;
    for (; (*u) && j; u = &((*u)->next), j--);
    if (*u) {
      strlcpy(temp, (*u)->mask, sizeof temp);
      i = 1;
    } else
      return -j - 1;
  } else {
    /* Find matching host, if there is one */
    for (; *u && !i; u = &((*u)->next))
      if (!rfc_casecmp((*u)->mask, who)) {
        strlcpy(temp, who, sizeof temp);
        i = 1;
        break;
      }
    if (!*u)
      return 0;
  }
  if (i && doit) {
    if (!noshare) {
      char *mask = str_escape(temp, ':', '\\');

      if (mask) {
        /* Distribute chan invites differently */
        if (c)
          shareout(c, "-invc %s %s\n", c->dname, mask);
        else
          shareout(NULL, "-inv %s\n", mask);
        nfree(mask);
      }
    }
    if (lastdeletedmask)
      nfree(lastdeletedmask);
    lastdeletedmask = nmalloc(strlen((*u)->mask) + 1);
    strcpy(lastdeletedmask, (*u)->mask);
    nfree((*u)->mask);
    if ((*u)->desc)
      nfree((*u)->desc);
    if ((*u)->user)
      nfree((*u)->user);
    t = *u;
    *u = (*u)->next;
    nfree(t);
  }
  return i;
}

/* Add a !* and/or @* if lacking in a usermask, copying to newmask */
void fix_broken_mask(char *newmask, const char *oldmask, size_t len)
{
  char *strat, *strbang;

  if (oldmask) {
    /* must have '!' and '@' */
    strat = strchr(oldmask, '@');
    strbang = strchr(oldmask, '!');
    if (strbang == NULL && strat == NULL)
      egg_snprintf(newmask, len, "%s!*@*", oldmask);
    else if (strat == NULL)
      egg_snprintf(newmask, len, "%s@*", oldmask);
    else if (strbang == NULL)
      egg_snprintf(newmask, len, "%.*s!*%s", (int)(strat - oldmask), oldmask, strat);
    else
      strlcpy(newmask, oldmask, len);
  }
}

/* Note: If first char of note is '*' it's a sticky ban.
 */
static int u_addban(struct chanset_t *chan, char *ban, char *from, char *note,
                    time_t expire_time, int flags)
{
  char host[1024], s[1024];
  maskrec *p = NULL, *l, **u = chan ? &chan->bans : &global_bans;
  module_entry *me;

  /* Choke check: fix broken bans (must have '!' and '@') */
  fix_broken_mask(host, ban, sizeof host);

  if ((me = module_find("server", 0, 0)) && me->funcs) {
    simple_sprintf(s, "%s!%s", me->funcs[SERVER_BOTNAME],
                   me->funcs[SERVER_BOTUSERHOST]);
    if (match_addr(host, s)) {
      putlog(LOG_MISC, "*", IRC_IBANNEDME);
      return 0;
    }
  }
  if (expire_time == now)
    return 1;

  for (l = *u; l; l = l->next)
    if (!rfc_casecmp(l->mask, host)) {
      p = l;
      break;
    }

  /* It shouldn't expire and be sticky also */
  if (note[0] == '*') {
    flags |= MASKREC_STICKY;
    note++;
  }
  if ((expire_time == 0L) || (flags & MASKREC_PERM)) {
    flags |= MASKREC_PERM;
    expire_time = 0L;
  }

  if (p == NULL) {
    p = user_malloc(sizeof(maskrec));
    p->next = *u;
    *u = p;
  } else {
    nfree(p->mask);
    nfree(p->user);
    nfree(p->desc);
  }
  p->expire = expire_time;
  p->added = now;
  p->lastactive = 0;
  p->flags = flags;
  p->mask = user_malloc(strlen(host) + 1);
  strcpy(p->mask, host);
  p->user = user_malloc(strlen(from) + 1);
  strcpy(p->user, from);
  p->desc = user_malloc(strlen(note) + 1);
  strcpy(p->desc, note);
  if (!noshare) {
    char *mask = str_escape(host, ':', '\\');

    if (mask) {
      if (!chan)
        shareout(NULL, "+b %s %li %s%s %s %s\n", mask, expire_time - now,
                 (flags & MASKREC_STICKY) ? "s" : "",
                 (flags & MASKREC_PERM) ? "p" : "-", from, note);
      else
        shareout(chan, "+bc %s %li %s %s%s %s %s\n", mask, expire_time - now,
                 chan->dname, (flags & MASKREC_STICKY) ? "s" : "",
                 (flags & MASKREC_PERM) ? "p" : "-", from, note);
      nfree(mask);
    }
  }
  return 1;
}

/* Note: If first char of note is '*' it's a sticky invite.
 */
static int u_addinvite(struct chanset_t *chan, char *invite, char *from,
                       char *note, time_t expire_time, int flags)
{
  char host[1024];
  maskrec *p = NULL, *l, **u = chan ? &chan->invites : &global_invites;

  /* Choke check: fix broken invites (must have '!' and '@') */
  fix_broken_mask(host, invite, sizeof host);

  for (l = *u; l; l = l->next)
    if (!rfc_casecmp(l->mask, host)) {
      p = l;
      break;
    }

  /* It shouldn't expire and be sticky also */
  if (note[0] == '*') {
    flags |= MASKREC_STICKY;
    note++;
  }
  if ((expire_time == 0L) || (flags & MASKREC_PERM)) {
    flags |= MASKREC_PERM;
    expire_time = 0L;
  }

  if (p == NULL) {
    p = user_malloc(sizeof(maskrec));
    p->next = *u;
    *u = p;
  } else {
    nfree(p->mask);
    nfree(p->user);
    nfree(p->desc);
  }
  p->expire = expire_time;
  p->added = now;
  p->lastactive = 0;
  p->flags = flags;
  p->mask = user_malloc(strlen(host) + 1);
  strcpy(p->mask, host);
  p->user = user_malloc(strlen(from) + 1);
  strcpy(p->user, from);
  p->desc = user_malloc(strlen(note) + 1);
  strcpy(p->desc, note);
  if (!noshare) {
    char *mask = str_escape(host, ':', '\\');

    if (mask) {
      if (!chan)
        shareout(NULL, "+inv %s %li %s%s %s %s\n", mask, expire_time - now,
                 (flags & MASKREC_STICKY) ? "s" : "",
                 (flags & MASKREC_PERM) ? "p" : "-", from, note);
      else
        shareout(chan, "+invc %s %li %s %s%s %s %s\n", mask, expire_time - now,
                 chan->dname, (flags & MASKREC_STICKY) ? "s" : "",
                 (flags & MASKREC_PERM) ? "p" : "-", from, note);
      nfree(mask);
    }
  }
  return 1;
}

/* Note: If first char of note is '*' it's a sticky exempt.
 */
static int u_addexempt(struct chanset_t *chan, char *exempt, char *from,
                       char *note, time_t expire_time, int flags)
{
  char host[1024];
  maskrec *p = NULL, *l, **u = chan ? &chan->exempts : &global_exempts;

  /* Choke check: fix broken exempts (must have '!' and '@') */
  fix_broken_mask(host, exempt, sizeof host);

  for (l = *u; l; l = l->next)
    if (!rfc_casecmp(l->mask, host)) {
      p = l;
      break;
    }

  /* It shouldn't expire and be sticky also */
  if (note[0] == '*') {
    flags |= MASKREC_STICKY;
    note++;
  }
  if ((expire_time == 0L) || (flags & MASKREC_PERM)) {
    flags |= MASKREC_PERM;
    expire_time = 0L;
  }

  if (p == NULL) {
    p = user_malloc(sizeof(maskrec));
    p->next = *u;
    *u = p;
  } else {
    nfree(p->mask);
    nfree(p->user);
    nfree(p->desc);
  }
  p->expire = expire_time;
  p->added = now;
  p->lastactive = 0;
  p->flags = flags;
  p->mask = user_malloc(strlen(host) + 1);
  strcpy(p->mask, host);
  p->user = user_malloc(strlen(from) + 1);
  strcpy(p->user, from);
  p->desc = user_malloc(strlen(note) + 1);
  strcpy(p->desc, note);
  if (!noshare) {
    char *mask = str_escape(host, ':', '\\');

    if (mask) {
      if (!chan)
        shareout(NULL, "+e %s %li %s%s %s %s\n", mask, expire_time - now,
                 (flags & MASKREC_STICKY) ? "s" : "",
                 (flags & MASKREC_PERM) ? "p" : "-", from, note);
      else
        shareout(chan, "+ec %s %li %s %s%s %s %s\n", mask, expire_time - now,
                 chan->dname, (flags & MASKREC_STICKY) ? "s" : "",
                 (flags & MASKREC_PERM) ? "p" : "-", from, note);
      nfree(mask);
    }
  }
  return 1;
}

/* Take host entry from ban list and display it ban-style.
 */
static void display_ban(int idx, int number, maskrec *ban,
                        struct chanset_t *chan, int show_inact)
{
  char dates[81], s[41];

  if (ban->added) {
    daysago(now, ban->added, s);
    sprintf(dates, "%s %s", MODES_CREATED, s);
    if (ban->added < ban->lastactive) {
      int len = strlen(dates);
      daysago(now, ban->lastactive, s);
      egg_snprintf(dates + len, (sizeof dates) - len, ", %s %s", MODES_LASTUSED, s); /* Concatenation */
    }
  } else
    dates[0] = 0;
  if (ban->flags & MASKREC_PERM)
    strcpy(s, "(perm)");
  else {
    char s1[29];

    days(ban->expire, now, s1);
    sprintf(s, "(expires %s)", s1);
  }
  if (ban->flags & MASKREC_STICKY)
    strcat(s, " (sticky)");
  if (!chan || ischanban(chan, ban->mask)) {
    if (number >= 0)
      dprintf(idx, "  [%3d] %s %s\n", number, ban->mask, s);
    else
      dprintf(idx, "BAN: %s %s\n", ban->mask, s);
  } else if (show_inact) {
    if (number >= 0)
      dprintf(idx, "! [%3d] %s %s\n", number, ban->mask, s);
    else
      dprintf(idx, "BAN (%s): %s %s\n", MODES_INACTIVE, ban->mask, s);
  } else
    return;
  dprintf(idx, "        %s: %s\n", ban->user, ban->desc);
  if (dates[0])
    dprintf(idx, "        %s\n", dates);
}

/* Take host entry from exempt list and display it ban-style.
 */
static void display_exempt(int idx, int number, maskrec *exempt,
                           struct chanset_t *chan, int show_inact)
{
  char dates[81], s[41];

  if (exempt->added) {
    daysago(now, exempt->added, s);
    sprintf(dates, "%s %s", MODES_CREATED, s);
    if (exempt->added < exempt->lastactive) {
      int len = strlen(dates);
      daysago(now, exempt->lastactive, s);
      egg_snprintf(dates + len, (sizeof dates) - len, ", %s %s", MODES_LASTUSED, s); /* Concatenation */
    }
  } else
    dates[0] = 0;
  if (exempt->flags & MASKREC_PERM)
    strcpy(s, "(perm)");
  else {
    char s1[29];

    days(exempt->expire, now, s1);
    sprintf(s, "(expires %s)", s1);
  }
  if (exempt->flags & MASKREC_STICKY)
    strcat(s, " (sticky)");
  if (!chan || ischanexempt(chan, exempt->mask)) {
    if (number >= 0)
      dprintf(idx, "  [%3d] %s %s\n", number, exempt->mask, s);
    else
      dprintf(idx, "EXEMPT: %s %s\n", exempt->mask, s);
  } else if (show_inact) {
    if (number >= 0)
      dprintf(idx, "! [%3d] %s %s\n", number, exempt->mask, s);
    else
      dprintf(idx, "EXEMPT (%s): %s %s\n", MODES_INACTIVE, exempt->mask, s);
  } else
    return;
  dprintf(idx, "        %s: %s\n", exempt->user, exempt->desc);
  if (dates[0])
    dprintf(idx, "        %s\n", dates);
}

/* Take host entry from invite list and display it ban-style.
 */
static void display_invite(int idx, int number, maskrec *invite,
                           struct chanset_t *chan, int show_inact)
{
  char dates[81], s[41];

  if (invite->added) {
    daysago(now, invite->added, s);
    sprintf(dates, "%s %s", MODES_CREATED, s);
    if (invite->added < invite->lastactive) {
      int len = strlen(dates);
      daysago(now, invite->lastactive, s);
      egg_snprintf(dates + len, (sizeof dates) - len, ", %s %s", MODES_LASTUSED, s); /* Concatenation */
    }
  } else
    dates[0] = 0;
  if (invite->flags & MASKREC_PERM)
    strcpy(s, "(perm)");
  else {
    char s1[29];

    days(invite->expire, now, s1);
    sprintf(s, "(expires %s)", s1);
  }
  if (invite->flags & MASKREC_STICKY)
    strcat(s, " (sticky)");
  if (!chan || ischaninvite(chan, invite->mask)) {
    if (number >= 0)
      dprintf(idx, "  [%3d] %s %s\n", number, invite->mask, s);
    else
      dprintf(idx, "INVITE: %s %s\n", invite->mask, s);
  } else if (show_inact) {
    if (number >= 0)
      dprintf(idx, "! [%3d] %s %s\n", number, invite->mask, s);
    else
      dprintf(idx, "INVITE (%s): %s %s\n", MODES_INACTIVE, invite->mask, s);
  } else
    return;
  dprintf(idx, "        %s: %s\n", invite->user, invite->desc);
  if (dates[0])
    dprintf(idx, "        %s\n", dates);
}

static void tell_bans(int idx, int show_inact, char *match)
{
  int k = 1;
  char *chname;
  struct chanset_t *chan = NULL;
  maskrec *u;

  /* Was a channel given? */
  if (match[0]) {
    chname = newsplit(&match);
    if (chname[0] && (strchr(CHANMETA, chname[0]))) {
      chan = findchan_by_dname(chname);
      if (!chan) {
        dprintf(idx, "%s.\n", CHAN_NOSUCH);
        return;
      }
    } else
      match = chname;
  }

  /* don't return here, we want to show global bans even if no chan */
  if (!chan && !(chan = findchan_by_dname(dcc[idx].u.chat->con_chan)) &&
      !(chan = chanset))
    chan = NULL;

  if (chan && show_inact)
    dprintf(idx, "%s:   (! = %s %s)\n", BANS_GLOBAL,
            MODES_NOTACTIVE, chan->dname);
  else
    dprintf(idx, "%s:\n", BANS_GLOBAL);
  for (u = global_bans; u; u = u->next) {
    if (match[0]) {
      if ((match_addr(match, u->mask)) ||
          (wild_match(match, u->desc)) || (wild_match(match, u->user)))
        display_ban(idx, k, u, chan, 1);
      k++;
    } else
      display_ban(idx, k++, u, chan, show_inact);
  }
  if (chan) {
    if (show_inact)
      dprintf(idx, "%s %s:   (! = %s, * = %s)\n",
              BANS_BYCHANNEL, chan->dname, MODES_NOTACTIVE2, MODES_NOTBYBOT);
    else
      dprintf(idx, "%s %s:  (* = %s)\n",
              BANS_BYCHANNEL, chan->dname, MODES_NOTBYBOT);
    for (u = chan->bans; u; u = u->next) {
      if (match[0]) {
        if ((match_addr(match, u->mask)) ||
            (wild_match(match, u->desc)) || (wild_match(match, u->user)))
          display_ban(idx, k, u, chan, 1);
        k++;
      } else
        display_ban(idx, k++, u, chan, show_inact);
    }
    if (chan->status & CHAN_ACTIVE) {
      masklist *b;
      char s[UHOSTLEN], *s1, *s2, fill[256];
      int min, sec;

      for (b = chan->channel.ban; b && b->mask[0]; b = b->next) {
        if ((!u_equals_mask(global_bans, b->mask)) &&
            (!u_equals_mask(chan->bans, b->mask))) {
          strcpy(s, b->who);
          s2 = s;
          s1 = splitnick(&s2);
          if (s1[0])
            sprintf(fill, "%s (%s!%s)", b->mask, s1, s2);
          else
            sprintf(fill, "%s (server %s)", b->mask, s2);
          if (b->timer != 0) {
            min = (now - b->timer) / 60;
            sec = (now - b->timer) - (min * 60);
            sprintf(s, " (active %02d:%02d)", min, sec);
            strcat(fill, s);
          }
          if ((!match[0]) || (match_addr(match, b->mask)))
            dprintf(idx, "* [%3d] %s\n", k, fill);
          k++;
        }
      }
    }
  }
  if (k == 1)
    dprintf(idx, "(There are no bans, permanent or otherwise.)\n");
  if ((!show_inact) && (!match[0]))
    dprintf(idx, "%s.\n", BANS_USEBANSALL);
}

static void tell_exempts(int idx, int show_inact, char *match)
{
  int k = 1;
  char *chname;
  struct chanset_t *chan = NULL;
  maskrec *u;

  /* Was a channel given? */
  if (match[0]) {
    chname = newsplit(&match);
    if (chname[0] && strchr(CHANMETA, chname[0])) {
      chan = findchan_by_dname(chname);
      if (!chan) {
        dprintf(idx, "%s.\n", CHAN_NOSUCH);
        return;
      }
    } else
      match = chname;
  }

  /* don't return here, we want to show global exempts even if no chan */
  if (!chan && !(chan = findchan_by_dname(dcc[idx].u.chat->con_chan)) &&
      !(chan = chanset))
    chan = NULL;

  if (chan && show_inact)
    dprintf(idx, "%s:   (! = %s %s)\n", EXEMPTS_GLOBAL,
            MODES_NOTACTIVE, chan->dname);
  else
    dprintf(idx, "%s:\n", EXEMPTS_GLOBAL);
  for (u = global_exempts; u; u = u->next) {
    if (match[0]) {
      if ((match_addr(match, u->mask)) ||
          (wild_match(match, u->desc)) || (wild_match(match, u->user)))
        display_exempt(idx, k, u, chan, 1);
      k++;
    } else
      display_exempt(idx, k++, u, chan, show_inact);
  }
  if (chan) {
    if (show_inact)
      dprintf(idx, "%s %s:   (! = %s, * = %s)\n", EXEMPTS_BYCHANNEL,
              chan->dname, MODES_NOTACTIVE2, MODES_NOTBYBOT);
    else
      dprintf(idx, "%s %s:  (* = %s)\n",
              EXEMPTS_BYCHANNEL, chan->dname, MODES_NOTBYBOT);
    for (u = chan->exempts; u; u = u->next) {
      if (match[0]) {
        if ((match_addr(match, u->mask)) ||
            (wild_match(match, u->desc)) || (wild_match(match, u->user)))
          display_exempt(idx, k, u, chan, 1);
        k++;
      } else
        display_exempt(idx, k++, u, chan, show_inact);
    }
    if (chan->status & CHAN_ACTIVE) {
      masklist *e;
      char s[UHOSTLEN], *s1, *s2, fill[256];
      int min, sec;

      for (e = chan->channel.exempt; e && e->mask[0]; e = e->next) {
        if ((!u_equals_mask(global_exempts, e->mask)) &&
            (!u_equals_mask(chan->exempts, e->mask))) {
          strcpy(s, e->who);
          s2 = s;
          s1 = splitnick(&s2);
          if (s1[0])
            sprintf(fill, "%s (%s!%s)", e->mask, s1, s2);
          else
            sprintf(fill, "%s (server %s)", e->mask, s2);
          if (e->timer != 0) {
            min = (now - e->timer) / 60;
            sec = (now - e->timer) - (min * 60);
            sprintf(s, " (active %02d:%02d)", min, sec);
            strcat(fill, s);
          }
          if ((!match[0]) || (match_addr(match, e->mask)))
            dprintf(idx, "* [%3d] %s\n", k, fill);
          k++;
        }
      }
    }
  }
  if (k == 1)
    dprintf(idx, "(There are no ban exempts, permanent or otherwise.)\n");
  if ((!show_inact) && (!match[0]))
    dprintf(idx, "%s.\n", EXEMPTS_USEEXEMPTSALL);
}

static void tell_invites(int idx, int show_inact, char *match)
{
  int k = 1;
  char *chname;
  struct chanset_t *chan = NULL;
  maskrec *u;

  /* Was a channel given? */
  if (match[0]) {
    chname = newsplit(&match);
    if (chname[0] && strchr(CHANMETA, chname[0])) {
      chan = findchan_by_dname(chname);
      if (!chan) {
        dprintf(idx, "%s.\n", CHAN_NOSUCH);
        return;
      }
    } else
      match = chname;
  }

  /* don't return here, we want to show global invites even if no chan */
  if (!chan && !(chan = findchan_by_dname(dcc[idx].u.chat->con_chan)) &&
      !(chan = chanset))
    chan = NULL;

  if (chan && show_inact)
    dprintf(idx, "%s:   (! = %s %s)\n", INVITES_GLOBAL,
            MODES_NOTACTIVE, chan->dname);
  else
    dprintf(idx, "%s:\n", INVITES_GLOBAL);
  for (u = global_invites; u; u = u->next) {
    if (match[0]) {
      if ((match_addr(match, u->mask)) ||
          (wild_match(match, u->desc)) || (wild_match(match, u->user)))
        display_invite(idx, k, u, chan, 1);
      k++;
    } else
      display_invite(idx, k++, u, chan, show_inact);
  }
  if (chan) {
    if (show_inact)
      dprintf(idx, "%s %s:   (! = %s, * = %s)\n", INVITES_BYCHANNEL,
              chan->dname, MODES_NOTACTIVE2, MODES_NOTBYBOT);
    else
      dprintf(idx, "%s %s:  (* = %s)\n",
              INVITES_BYCHANNEL, chan->dname, MODES_NOTBYBOT);
    for (u = chan->invites; u; u = u->next) {
      if (match[0]) {
        if ((match_addr(match, u->mask)) ||
            (wild_match(match, u->desc)) || (wild_match(match, u->user)))
          display_invite(idx, k, u, chan, 1);
        k++;
      } else
        display_invite(idx, k++, u, chan, show_inact);
    }
    if (chan->status & CHAN_ACTIVE) {
      masklist *i;
      char s[UHOSTLEN], *s1, *s2, fill[256];
      int min, sec;

      for (i = chan->channel.invite; i && i->mask[0]; i = i->next) {
        if ((!u_equals_mask(global_invites, i->mask)) &&
            (!u_equals_mask(chan->invites, i->mask))) {
          strcpy(s, i->who);
          s2 = s;
          s1 = splitnick(&s2);
          if (s1[0])
            sprintf(fill, "%s (%s!%s)", i->mask, s1, s2);
          else
            sprintf(fill, "%s (server %s)", i->mask, s2);
          if (i->timer != 0) {
            min = (now - i->timer) / 60;
            sec = (now - i->timer) - (min * 60);
            sprintf(s, " (active %02d:%02d)", min, sec);
            strcat(fill, s);
          }
          if ((!match[0]) || (match_addr(match, i->mask)))
            dprintf(idx, "* [%3d] %s\n", k, fill);
          k++;
        }
      }
    }
  }
  if (k == 1)
    dprintf(idx, "(There are no invites, permanent or otherwise.)\n");
  if ((!show_inact) && (!match[0]))
    dprintf(idx, "%s.\n", INVITES_USEINVITESALL);
}

/* Write the ban lists and the ignore list to a file.
 */
static int write_bans(FILE *f, int idx)
{
  struct chanset_t *chan;
  maskrec *b;
  char *mask;
  long expire, added;

  if (global_bans)
    if (fprintf(f, BAN_NAME " - -\n") == EOF)   /* Daemus */
      return 0;
  for (b = global_bans; b; b = b->next) {
    mask = str_escape(b->mask, ':', '\\');
    expire = b->expire;
    added = b->added;
    if (!mask ||
        fprintf(f, "- %s:%s%lu%s:+%lu:%lu:%s:%s\n", mask,
                (b->flags & MASKREC_PERM) ? "+" : "", expire,
                (b->flags & MASKREC_STICKY) ? "*" : "", added,
                (long) b->lastactive, b->user ? b->user : botnetnick,
                b->desc ? b->desc : "requested") == EOF) {
      if (mask)
        nfree(mask);
      return 0;
    }
    nfree(mask);
  }
  for (chan = chanset; chan; chan = chan->next)
    if ((idx < 0) || (chan->status & CHAN_SHARED)) {
      struct flag_record fr = { FR_CHAN | FR_GLOBAL | FR_BOT, 0, 0, 0, 0, 0 };

      if (idx >= 0)
        get_user_flagrec(dcc[idx].user, &fr, chan->dname);
      else
        fr.chan = BOT_SHARE;
      if ((fr.chan & BOT_SHARE) || (fr.bot & BOT_GLOBAL)) {
        if (fprintf(f, "::%s bans\n", chan->dname) == EOF)
          return 0;
        for (b = chan->bans; b; b = b->next) {
          mask = str_escape(b->mask, ':', '\\');
          expire = b->expire;
          added = b->added;
          if (!mask ||
              fprintf(f, "- %s:%s%lu%s:+%lu:%lu:%s:%s\n", mask,
                      (b->flags & MASKREC_PERM) ? "+" : "", expire,
                      (b->flags & MASKREC_STICKY) ? "*" : "", added,
                      (long) b->lastactive, b->user ? b->user : botnetnick,
                      b->desc ? b->desc : "requested") == EOF) {
            if (mask)
              nfree(mask);
            return 0;
          }
          nfree(mask);
        }
      }
    }
  return 1;
}

/* Write the exemptlists to a file.
 */
static int write_exempts(FILE *f, int idx)
{
  struct chanset_t *chan;
  maskrec *e;
  char *mask;
  long expire, added;

  if (global_exempts)
    if (fprintf(f, EXEMPT_NAME " - -\n") == EOF)        /* Daemus */
      return 0;
  for (e = global_exempts; e; e = e->next) {
    mask = str_escape(e->mask, ':', '\\');
    expire = e->expire;
    added = e->added;
    if (!mask ||
        fprintf(f, "%s %s:%s%lu%s:+%lu:%lu:%s:%s\n", "%", mask,
                (e->flags & MASKREC_PERM) ? "+" : "", expire,
                (e->flags & MASKREC_STICKY) ? "*" : "", added,
                (long) e->lastactive, e->user ? e->user : botnetnick,
                e->desc ? e->desc : "requested") == EOF) {
      if (mask)
        nfree(mask);
      return 0;
    }
    nfree(mask);
  }
  for (chan = chanset; chan; chan = chan->next)
    if ((idx < 0) || (chan->status & CHAN_SHARED)) {
      struct flag_record fr = { FR_CHAN | FR_GLOBAL | FR_BOT, 0, 0, 0, 0, 0 };

      if (idx >= 0)
        get_user_flagrec(dcc[idx].user, &fr, chan->dname);
      else
        fr.chan = BOT_SHARE;
      if ((fr.chan & BOT_SHARE) || (fr.bot & BOT_GLOBAL)) {
        if (fprintf(f, "&&%s exempts\n", chan->dname) == EOF)
          return 0;
        for (e = chan->exempts; e; e = e->next) {
          mask = str_escape(e->mask, ':', '\\');
          expire = e->expire;
          added = e->added;
          if (!mask ||
              fprintf(f, "%s %s:%s%lu%s:+%lu:%lu:%s:%s\n", "%", mask,
                      (e->flags & MASKREC_PERM) ? "+" : "", expire,
                      (e->flags & MASKREC_STICKY) ? "*" : "", added,
                      (long) e->lastactive, e->user ? e->user : botnetnick,
                      e->desc ? e->desc : "requested") == EOF) {
            if (mask)
              nfree(mask);
            return 0;
          }
          nfree(mask);
        }
      }
    }
  return 1;
}

/* Write the invitelists to a file.
 */
static int write_invites(FILE *f, int idx)
{
  struct chanset_t *chan;
  maskrec *ir;
  char *mask;
  long expire, added;

  if (global_invites)
    if (fprintf(f, INVITE_NAME " - -\n") == EOF)        /* Daemus */
      return 0;
  for (ir = global_invites; ir; ir = ir->next) {
    mask = str_escape(ir->mask, ':', '\\');
    expire = ir->expire;
    added = ir->added;
    if (!mask ||
        fprintf(f, "@ %s:%s%lu%s:+%lu:%lu:%s:%s\n", mask,
                (ir->flags & MASKREC_PERM) ? "+" : "", expire,
                (ir->flags & MASKREC_STICKY) ? "*" : "", added,
                (long) ir->lastactive, ir->user ? ir->user : botnetnick,
                ir->desc ? ir->desc : "requested") == EOF) {
      if (mask)
        nfree(mask);
      return 0;
    }
    nfree(mask);
  }
  for (chan = chanset; chan; chan = chan->next)
    if ((idx < 0) || (chan->status & CHAN_SHARED)) {
      struct flag_record fr = { FR_CHAN | FR_GLOBAL | FR_BOT, 0, 0, 0, 0, 0 };

      if (idx >= 0)
        get_user_flagrec(dcc[idx].user, &fr, chan->dname);
      else
        fr.chan = BOT_SHARE;
      if ((fr.chan & BOT_SHARE) || (fr.bot & BOT_GLOBAL)) {
        if (fprintf(f, "$$%s invites\n", chan->dname) == EOF)
          return 0;
        for (ir = chan->invites; ir; ir = ir->next) {
          mask = str_escape(ir->mask, ':', '\\');
          expire = ir->expire;
          added = ir->added;
          if (!mask ||
              fprintf(f, "@ %s:%s%lu%s:+%lu:%lu:%s:%s\n", mask,
                      (ir->flags & MASKREC_PERM) ? "+" : "", expire,
                      (ir->flags & MASKREC_STICKY) ? "*" : "", added,
                      (long) ir->lastactive, ir->user ? ir->user : botnetnick,
                      ir->desc ? ir->desc : "requested") == EOF) {
            if (mask)
              nfree(mask);
            return 0;
          }
          nfree(mask);
        }
      }
    }
  return 1;
}

static void channels_writeuserfile(void)
{
  char s[1024];
  FILE *f;
  int ret = 0;

  simple_sprintf(s, "%s~new", userfile);
  f = fopen(s, "a");
  if (f) {
    ret = write_bans(f, -1);
    ret += write_exempts(f, -1);
    ret += write_invites(f, -1);
    fclose(f);
  }
  if (ret < 3)
    putlog(LOG_MISC, "*", USERF_ERRWRITE);
  write_channels();
}

/* Expire mask originally set by `who' on `chan'?
 *
 * We might not want to expire masks in all cases, as other bots
 * often tend to immediately reset masks they've listed in their
 * internal ban list, making it quite senseless for us to remove
 * them in the first place.
 *
 * Returns 1 if a mask on `chan' by `who' may be expired and 0 if
 * not.
 */
static int expired_mask(struct chanset_t *chan, char *who)
{
  memberlist *m, *m2;
  char buf[UHOSTLEN], *snick, *sfrom;
  struct userrec *u;

  /* Always expire masks, regardless of who set it? */
  if (force_expire)
    return 1;

  strlcpy(buf, who, sizeof buf);
  sfrom = buf;
  snick = splitnick(&sfrom);

  if (!snick[0])
    return 1;

  m = ismember(chan, snick);
  if (!m)
    for (m2 = chan->channel.member; m2 && m2->nick[0]; m2 = m2->next)
      if (!egg_strcasecmp(sfrom, m2->userhost)) {
        m = m2;
        break;
      }

  if (!m || !chan_hasop(m) || !rfc_casecmp(m->nick, botname))
    return 1;

  /* At this point we know the person/bot who set the mask is currently
   * present in the channel and has op.
   */

  if (m->user)
    u = m->user;
  else {
    simple_sprintf(buf, "%s!%s", m->nick, m->userhost);
    u = get_user_by_host(buf);
  }
  /* Do not expire masks set by bots. */
  if (u && u->flags & USER_BOT)
    return 0;
  else
    return 1;
}

/* Check for expired timed-bans.
 */
static void check_expired_bans(void)
{
  maskrec *u, *u2;
  struct chanset_t *chan;
  masklist *b;

  for (u = global_bans; u; u = u2) {
    u2 = u->next;
    if (!(u->flags & MASKREC_PERM) && (now >= u->expire)) {
      putlog(LOG_MISC, "*", "%s %s (%s)", BANS_NOLONGER, u->mask, MISC_EXPIRED);
      for (chan = chanset; chan; chan = chan->next)
        for (b = chan->channel.ban; b->mask[0]; b = b->next)
          if (!rfc_casecmp(b->mask, u->mask) &&
              expired_mask(chan, b->who) && b->timer != now) {
            add_mode(chan, '-', 'b', u->mask);
            b->timer = now;
          }
      u_delban(NULL, u->mask, 1);
    }
  }
  /* Check for specific channel-domain bans expiring */
  for (chan = chanset; chan; chan = chan->next) {
    for (u = chan->bans; u; u = u2) {
      u2 = u->next;
      if (!(u->flags & MASKREC_PERM) && (now >= u->expire)) {
        putlog(LOG_MISC, "*", "%s %s %s %s (%s)", BANS_NOLONGER,
               u->mask, MISC_ONLOCALE, chan->dname, MISC_EXPIRED);
        for (b = chan->channel.ban; b->mask[0]; b = b->next)
          if (!rfc_casecmp(b->mask, u->mask) &&
              expired_mask(chan, b->who) && b->timer != now) {
            add_mode(chan, '-', 'b', u->mask);
            b->timer = now;
          }
        u_delban(chan, u->mask, 1);
      }
    }
  }
}

/* Check for expired timed-exemptions
 */
static void check_expired_exempts(void)
{
  maskrec *u, *u2;
  struct chanset_t *chan;
  masklist *b, *e;
  int match;

  if (!use_exempts)
    return;
  for (u = global_exempts; u; u = u2) {
    u2 = u->next;
    if (!(u->flags & MASKREC_PERM) && (now >= u->expire)) {
      putlog(LOG_MISC, "*", "%s %s (%s)", EXEMPTS_NOLONGER,
             u->mask, MISC_EXPIRED);
      for (chan = chanset; chan; chan = chan->next) {
        match = 0;
        b = chan->channel.ban;
        while (b->mask[0] && !match) {
          if (mask_match(b->mask, u->mask))
            match = 1;
          else
            b = b->next;
        }
        if (match)
          putlog(LOG_MISC, chan->dname,
                 "Exempt not expired on channel %s. Ban still set!",
                 chan->dname);
        else
          for (e = chan->channel.exempt; e->mask[0]; e = e->next)
            if (!rfc_casecmp(e->mask, u->mask) &&
                expired_mask(chan, e->who) && e->timer != now) {
              add_mode(chan, '-', 'e', u->mask);
              e->timer = now;
            }
      }
      u_delexempt(NULL, u->mask, 1);
    }
  }
  /* Check for specific channel-domain exempts expiring */
  for (chan = chanset; chan; chan = chan->next) {
    for (u = chan->exempts; u; u = u2) {
      u2 = u->next;
      if (!(u->flags & MASKREC_PERM) && (now >= u->expire)) {
        match = 0;
        b = chan->channel.ban;
        while (b->mask[0] && !match) {
          if (mask_match(b->mask, u->mask))
            match = 1;
          else
            b = b->next;
        }
        if (match)
          putlog(LOG_MISC, chan->dname,
                 "Exempt not expired on channel %s. Ban still set!",
                 chan->dname);
        else {
          putlog(LOG_MISC, "*", "%s %s %s %s (%s)", EXEMPTS_NOLONGER,
                 u->mask, MISC_ONLOCALE, chan->dname, MISC_EXPIRED);
          for (e = chan->channel.exempt; e->mask[0]; e = e->next)
            if (!rfc_casecmp(e->mask, u->mask) &&
                expired_mask(chan, e->who) && e->timer != now) {
              add_mode(chan, '-', 'e', u->mask);
              e->timer = now;
            }
          u_delexempt(chan, u->mask, 1);
        }
      }
    }
  }
}

/* Check for expired timed-invites.
 */
static void check_expired_invites(void)
{
  maskrec *u, *u2;
  struct chanset_t *chan;
  masklist *b;

  if (!use_invites)
    return;
  for (u = global_invites; u; u = u2) {
    u2 = u->next;
    if (!(u->flags & MASKREC_PERM) && (now >= u->expire)) {
      putlog(LOG_MISC, "*", "%s %s (%s)", INVITES_NOLONGER,
             u->mask, MISC_EXPIRED);
      for (chan = chanset; chan; chan = chan->next)
        if (!(chan->channel.mode & CHANINV))
          for (b = chan->channel.invite; b->mask[0]; b = b->next)
            if (!rfc_casecmp(b->mask, u->mask) &&
                expired_mask(chan, b->who) && b->timer != now) {
              add_mode(chan, '-', 'I', u->mask);
              b->timer = now;
            }
      u_delinvite(NULL, u->mask, 1);
    }
  }
  /* Check for specific channel-domain invites expiring */
  for (chan = chanset; chan; chan = chan->next) {
    for (u = chan->invites; u; u = u2) {
      u2 = u->next;
      if (!(u->flags & MASKREC_PERM) && (now >= u->expire)) {
        putlog(LOG_MISC, "*", "%s %s %s %s (%s)", INVITES_NOLONGER,
               u->mask, MISC_ONLOCALE, chan->dname, MISC_EXPIRED);
        if (!(chan->channel.mode & CHANINV))
          for (b = chan->channel.invite; b->mask[0]; b = b->next)
            if (!rfc_casecmp(b->mask, u->mask) &&
                expired_mask(chan, b->who) && b->timer != now) {
              add_mode(chan, '-', 'I', u->mask);
              b->timer = now;
            }
        u_delinvite(chan, u->mask, 1);
      }
    }
  }
}
