Merge remote branch 'upstream/master'
authorroot <root@mail.pk910.de>
Wed, 9 Nov 2011 15:27:40 +0000 (16:27 +0100)
committerroot <root@mail.pk910.de>
Wed, 9 Nov 2011 15:27:40 +0000 (16:27 +0100)
Conflicts:
src/chanserv.c

1  2 
src/chanserv.c

diff --combined src/chanserv.c
index fa1827fa6e59d68c9455901faf960246d406c11f,2ba265f1ab6cc778baceed3c9fae8cb4c50a534c..11b2c74e5677e7a1292e62cb4044dc0f602dd946
  #include "conf.h"
  #include "global.h"
  #include "modcmd.h"
 -#include "opserv.h" /* for opserv_bad_channel() */
 +#include "opserv.h" /* for opserv_bad_channel() and devnull management */
  #include "nickserv.h" /* for oper_outranks() */
  #include "saxdb.h"
 +#include "spamserv.h"
  #include "timeq.h"
  
  #define CHANSERV_CONF_NAME  "services/chanserv"
  #define KEY_NODELETE_LEVEL          "nodelete_level"
  #define KEY_MAX_USERINFO_LENGTH     "max_userinfo_length"
  #define KEY_GIVEOWNERSHIP_PERIOD    "giveownership_timeout"
 +#define KEY_INVITED_INTERVAL          "invite_timeout"
 +#define KEY_NEW_CHANNEL_AUTHED      "new_channel_authed_join"
 +#define KEY_NEW_CHANNEL_UNAUTHED    "new_channel_unauthed_join"
 +#define KEY_NEW_CHANNEL_MSG         "new_channel_message"
  
  /* ChanServ database */
  #define KEY_CHANNELS                "channels"
  #define KEY_USERS           "users"
  #define KEY_BANS            "bans"
  #define KEY_MAX             "max"
 +#define KEY_MAX_TIME        "max_time"
  #define KEY_NOTES           "notes"
  #define KEY_TOPIC_MASK      "topic_mask"
  #define KEY_OWNER_TRANSFER  "owner_transfer"
 +#define KEY_EXPIRE          "expire"
  
  /* User data */
  #define KEY_LEVEL   "level"
  #define KEY_INFO    "info"
  #define KEY_SEEN    "seen"
  
 +/* Votes */
 +#define KEY_VOTE    "vote"
 +#define KEY_VOTE_START    "votestart"
 +#define KEY_VOTE_OPTIONS    "voptions"
 +#define KEY_VOTE_OPTION_NAME    "voptionname"
 +#define KEY_VOTE_VOTED    "vvoted"
 +#define KEY_VOTE_VOTEDFOR    "vvotefor"
 +#define KEY_VOTE_OPTION_ID    "voptionid"
 +#define KEY_VOTE_OPTION_VOTED    "voptionvoted"
 +
  /* Ban data */
  #define KEY_OWNER       "owner"
  #define KEY_REASON      "reason"
@@@ -209,7 -192,7 +209,7 @@@ static const struct message_entry msgta
  /* User management */
      { "CSMSG_ADDED_USER", "Added %s to the %s user list with access %d." },
      { "CSMSG_DELETED_USER", "Deleted %s (with access %d) from the %s user list." },
 -    { "CSMSG_BAD_RANGE", "Invalid access range; minimum (%d) must be greater than maximum (%d)." },
 +    { "CSMSG_BAD_RANGE", "Invalid access range; minimum (%d) must be lower than maximum (%d)." },
      { "CSMSG_DELETED_USERS", "Deleted accounts matching $b%s$b with access from $b%d$b to $b%d$b from the %s user list." },
      { "CSMSG_TRIMMED_USERS", "Trimmed $b%d users$b with access from %d to %d from the %s user list who were inactive for at least %s." },
      { "CSMSG_INCORRECT_ACCESS", "%s has access $b%d$b, not %s." },
      { "CSMSG_SET_USERINFO",      "$bUserInfo    $b %d" },
      { "CSMSG_SET_GIVE_VOICE",    "$bGiveVoice   $b %d" },
      { "CSMSG_SET_TOPICSNARF",    "$bTopicSnarf  $b %d" },
 +    { "CSMSG_SET_VOTE",          "$bVote        $b %d" },
      { "CSMSG_SET_INVITEME",      "$bInviteMe    $b %d" },
      { "CSMSG_SET_ENFOPS",        "$bEnfOps      $b %d" },
      { "CSMSG_SET_GIVE_OPS",      "$bGiveOps     $b %d" },
      { "CSMSG_SET_CTCPREACTION",  "$bCTCPReaction$b %d - %s" },
      { "CSMSG_SET_TOPICREFRESH",  "$bTopicRefresh$b %d - %s" },
      { "CSMSG_SET_UNREVIEWED",    "$bUnreviewed  $b %s" },
 +    { "CSMSG_SET_EXPIRE",        "$bExpire      $b %s" },
 +    { "CSMSG_SET_EXPIRE_OFF",    "$bExpire      $b off" },
      { "CSMSG_USET_NOAUTOOP",     "$bNoAutoOp    $b %s" },
      { "CSMSG_USET_NOAUTOVOICE",  "$bNoAutoVoice $b %s" },
      { "CSMSG_USET_AUTOINVITE",   "$bAutoInvite  $b %s" },
      { "CSMSG_ACCESS_SEARCH_HEADER", "%s users from level %d to %d matching %s:" },
      { "CSMSG_INVALID_ACCESS", "$b%s$b is an invalid access level." },
      { "CSMSG_CHANGED_ACCESS", "%s now has access $b%d$b in %s." },
 +    { "CSMSG_TOTAL_USERS", "There are $b%d$b users in %s." },
  
  /* Channel note list */
      { "CSMSG_NOTELIST_HEADER", "Notes for $b%s$b:" },
      { "CSMSG_UC_H_TITLE", "network helper" },
      { "CSMSG_LC_H_TITLE", "support helper" },
      { "CSMSG_LAME_SMURF_TARGET", "%s is an IRC operator." },
 +    { "CSMSG_MYACCESS_COUNT", "%s has access in $b%d$b channels and is owner of $b%d$b channel(s)." },
 +      { "CSMSG_MYACCESS_COUNT_1", "%s has access in $b%d$b channel and is owner of $b%d$b channel(s)." },
 +
  
  /* Seen information */
      { "CSMSG_NEVER_SEEN", "%s has never been seen in $b%s$b." },
      { "CSMSG_CHANNEL_MODES", "$bMode Lock:           $b%s" },
      { "CSMSG_CHANNEL_NOTE", "$b%s:%*s$b%s" },
      { "CSMSG_CHANNEL_MAX", "$bRecord Visitors:     $b%i" },
 +    { "CSMSG_CHANNEL_MAX_TIME", "$bRecord Visitors:     $b%i (%s ago.)" },
      { "CSMSG_CHANNEL_OWNER", "$bOwner:               $b%s" },
      { "CSMSG_CHANNEL_BANS", "$bBan Count:           $b%i" },
      { "CSMSG_CHANNEL_USERS", "$bTotal User Count:    $b%i" },
      { "CSMSG_HUGGLES_HIM", "\001ACTION huggles %s\001" },
      { "CSMSG_HUGGLES_YOU", "\001ACTION huggles you\001" },
  
 +/* Vote */
 +    { "CSMSG_ADDVOTE_DONE", "Vote added. Use $baddoption$b to add at least 2 vote options and then $bstartvote$b to start the voting." },
 +    { "CSMSG_ADDVOTE_FULL", "There is already a vote in this channel. Use $bdelvote$b to delete it." },
 +    { "CSMSG_DELVOTE_DONE", "Vote deleted." },
 +    { "CSMSG_NO_VOTE", "There is no vote in this channel." },
 +    { "CSMSG_ADDOPTION_DONE", "Vote option added with id %i (%i - %i)." },
 +    { "CSMSG_DELOPTION_DONE", "Vote option deleted." },
 +    { "CSMSG_DELOPTION_NONE", "Vote option does not exist." },
 +    { "CSMSG_VOTE_NEED_OPTIONS", "There must be at least 2 options in a vote." },
 +    { "CSMSG_VOTE_NOT_STARTED", "The vote is not started. Use $bstartvote$b to allow voting." },
 +    { "CSMSG_VOTE_QUESTION", "Question:   %s" },
 +    { "CSMSG_VOTE_OPTION", "$b%i$b:   %s ($b%i$b votes)" },
 +    { "CSMSG_VOTERES_QUESTION", "Question:   %s" },
 +    { "CSMSG_VOTERES_OPTION", "\ 2%i\ 2:   %s (\ 2%i\ 2 votes)" },
 +    { "CSMSG_STARTVOTE_TOP", "\ 2%s\ 2 has started a voting:" },
 +    { "CSMSG_STARTVOTE_QUESTION", "\ 2Question:\ 2 %s" },
 +    { "CSMSG_STARTVOTE_OPTION", "\ 2%i:\ 2  %s" },
 +    { "CSMSG_STARTVOTE_ACCESS", "All channel users with at least \ 2%i\ 2 access can vote." },
 +    { "CSMSG_STARTVOTE_HOWTO", "To vote for an option, use \ 2vote ID\ 2. To see the available options and the current votes, use \ 2vote\ 2 without parameters." },
 +    { "CSMSG_STARTVOTE_RUNNING", "The vote is already running." },
 +    { "CSMSG_VOTE_VOTED", "You have already voted." },
 +    { "CSMSG_VOTE_INVALID", "Vote option does not exist." },
 +    { "CSMSG_VOTE_DONE", "You voted for $b%s$b." },
 +    { "CSMSG_ENDVOTE_DONE", "The vote has been finished." },
 +    { "CSMSG_ENDVOTE_STOPPED", "The vote is not running." },
 +
  /* Other things */
      { "CSMSG_EVENT_SEARCH_RESULTS", "The following channel events were found:" },
      { NULL, NULL }
@@@ -549,8 -498,6 +549,8 @@@ static struc
      unsigned long   db_backup_frequency;
      unsigned long   channel_expire_frequency;
      unsigned long   dnr_expire_frequency;
 +    
 +    unsigned long   invited_timeout;
  
      unsigned long   info_delay;
      unsigned long   adjust_delay;
      const char          *irc_operator_epithet;
      const char          *network_helper_epithet;
      const char          *support_helper_epithet;
 +
 +    const char          *new_channel_authed;
 +    const char          *new_channel_unauthed;
 +    const char          *new_channel_msg;
  } chanserv_conf;
  
  struct listData
      struct helpfile_table table;
  };
  
 +struct ChanUser
 +{
 +      struct userNode *user;
 +    struct chanNode *chan;
 +};
 +
  enum note_access_type
  {
      NOTE_SET_CHANNEL_ACCESS,
@@@ -672,8 -609,7 +672,8 @@@ static const struct 
      { "CSMSG_SET_CTCPUSERS", "ctcpusers", 0, 9, 0, 0 },
      { "CSMSG_SET_USERINFO", "userinfo", 1, ~0, CHANNEL_INFO_LINES, 1 },
      { "CSMSG_SET_INVITEME", "inviteme", 1, ~0, CHANNEL_PEON_INVITE, 200 },
 -    { "CSMSG_SET_TOPICSNARF", "topicsnarf", 501, ~0, CHANNEL_TOPIC_SNARF, 1 }
 +    { "CSMSG_SET_TOPICSNARF", "topicsnarf", 501, ~0, CHANNEL_TOPIC_SNARF, 1 },
 +    { "CSMSG_SET_VOTE", "vote", 100, ~0, 0, 0 }
  };
  
  struct charOptionValues {
@@@ -720,9 -656,9 +720,9 @@@ struct chanData *channelList
  static struct module *chanserv_module;
  static unsigned int userCount;
  
 -#define GetChannelUser(channel, handle) _GetChannelUser(channel, handle, 1, 0)
  #define GetChannelAccess(channel, handle) _GetChannelUser(channel, handle, 0, 0)
  #define GetTrueChannelAccess(channel, handle) _GetChannelUser(channel, handle, 0, 1)
 +static void unregister_channel(struct chanData *channel, const char *reason);
  
  unsigned short
  user_level_from_name(const char *name, unsigned short clamp_level)
@@@ -914,15 -850,6 +914,15 @@@ chanserv_create_note_type(const char *n
      return ntype;
  }
  
 +static void
 +free_vote_options(void *data)
 +{
 +    struct vote_option *vOpt = data;
 +    free(vOpt->name);
 +    free(vOpt->option_str);
 +    free(vOpt);
 +}
 +
  static void
  chanserv_deref_note_type(void *data)
  {
@@@ -1090,65 -1017,6 +1090,65 @@@ static MODCMD_FUNC(cmd_removenote) 
      return 1;
  }
  
 +static void
 +chanserv_expire_channel(void *data)
 +{
 +    struct chanData *channel = data;
 +    char reason[MAXLEN];
 +    sprintf(reason, "channel expired.");
 +    channel->expiry = 0;
 +    spamserv_cs_unregister(NULL, channel->channel, expire, NULL);
 +    unregister_channel(channel, reason);
 +}
 +
 +static MODCMD_FUNC(chan_opt_expire)
 +{
 +    struct chanData *cData = channel->channel_info;
 +    unsigned long value = cData->expiry;
 +
 +    if(argc > 1)
 +    {
 +        if((!IsOper(user) || !user->handle_info || (user->handle_info->opserv_level < chanserv_conf.nodelete_level)))
 +        {
 +            reply("MSG_SETTING_PRIVILEGED", argv[0]);
 +            return 0;
 +        }
 +        unsigned long expiry,duration;
 +
 +        /* The two directions can have different ACLs. */
 +        if(!strcmp(argv[1], "0"))
 +            expiry = 0;
 +        else if((duration = ParseInterval(argv[1])))
 +            expiry = now + duration;
 +        else
 +        {
 +            reply("MSG_INVALID_DURATION", argv[1]);
 +            return 0;
 +        }
 +
 +        if (expiry != value)
 +        {
 +            if(value) {
 +                //unset old timer
 +                timeq_del(value, chanserv_expire_channel, cData, 0);
 +            }
 +            value = expiry;
 +            cData->expiry = value;
 +            if(value > 0) {
 +                //New timer!
 +                timeq_add(expiry, chanserv_expire_channel, cData);
 +            }
 +        }
 +    }
 +
 +    if(cData->expiry > now) {
 +        char expirestr[INTERVALLEN];
 +        reply("CSMSG_SET_EXPIRE", intervalString(expirestr, cData->expiry - now, user->handle_info));
 +    } else
 +        reply("CSMSG_SET_EXPIRE_OFF");
 +    return 1;
 +}
 +
  static int
  mode_lock_violated(const struct mod_chanmode *orig, const struct mod_chanmode *change)
  {
@@@ -1244,8 -1112,6 +1244,8 @@@ register_channel(struct chanNode *cNode
      channel->channel = cNode;
      LockChannel(cNode);
      cNode->channel_info = channel;
 +    
 +    channel->vote = NULL;
  
      return channel;
  }
@@@ -1283,6 -1149,8 +1283,6 @@@ add_channel_user(struct chanData *chann
      return ud;
  }
  
 -static void unregister_channel(struct chanData *channel, const char *reason);
 -
  void
  del_channel_user(struct userData *user, int do_gc)
  {
  
      free(user->info);
      free(user);
 -    if(do_gc && !channel->users && !IsProtected(channel))
 +    if(do_gc && !channel->users && !IsProtected(channel)) {
 +        spamserv_cs_unregister(NULL, channel->channel, lost_all_users, NULL);
          unregister_channel(channel, "lost all users.");
 +    }
  }
  
  static void expire_ban(void *data);
  
 -static struct banData*
 +struct banData*
  add_channel_ban(struct chanData *channel, const char *mask, char *owner, unsigned long set, unsigned long triggered, unsigned long expires, char *reason)
  {
      struct banData *bd;
@@@ -1483,8 -1349,6 +1483,8 @@@ unregister_channel(struct chanData *cha
          if(cNode)
              cNode->channel_info = NULL;
      }
 +    if(channel->expiry)
 +        timeq_del(channel->expiry, chanserv_expire_channel, channel, 0);
      channel->channel->channel_info = NULL;
  
      dict_delete(channel->notes);
  }
  
  static void
- expire_channels(UNUSED_ARG(void *data))
+ expire_channels(void *data)
  {
      struct chanData *channel, *next;
      struct userData *user;
  
          /* Unregister the channel */
          log_module(CS_LOG, LOG_INFO, "(%s) Channel registration expired.", channel->channel->name);
 +        spamserv_cs_unregister(NULL, channel->channel, expire, NULL);
          unregister_channel(channel, "registration expired.");
      }
  
-     if(chanserv_conf.channel_expire_frequency)
+     if(chanserv_conf.channel_expire_frequency && !data)
          timeq_add(now + chanserv_conf.channel_expire_frequency, expire_channels, NULL);
  }
  
@@@ -2235,7 -2098,6 +2235,7 @@@ static CHANSERV_FUNC(cmd_register
  
      /* Initialize the channel's max user record. */
      cData->max = channel->members.used;
 +    cData->max_time = 0;
  
      if(handle != user->handle_info)
          reply("CSMSG_PROXY_SUCCESS", handle->handle, channel->name);
@@@ -2285,7 -2147,7 +2285,7 @@@ static CHANSERV_FUNC(cmd_unregister
          return 0;
      }
  
 -    if(IsProtected(cData))
 +    if(IsProtected(cData) && !IsOper(user))
      {
          reply("CSMSG_UNREG_NODELETE", channel->name);
          return 0;
      sprintf(reason, "unregistered by %s.", user->handle_info->handle);
      name = strdup(channel->name);
      unregister_channel(cData, reason);
 +    spamserv_cs_unregister(user, channel, manually, "unregistered");
      reply("CSMSG_UNREG_SUCCESS", name);
      free(name);
      return 1;
  }
  
 +static void
 +ss_cs_join_channel(struct chanNode *channel, int spamserv_join)
 +{
 +    extern struct userNode *spamserv;
 +    struct mod_chanmode *change;
 +
 +    if(spamserv && spamserv_join && get_chanInfo(channel->name))
 +    {
 +        change = mod_chanmode_alloc(2);
 +        change->argc = 2;
 +        change->args[0].mode = MODE_CHANOP;
 +        change->args[0].u.member = AddChannelUser(chanserv, channel);
 +        change->args[1].mode = MODE_CHANOP;
 +        change->args[1].u.member = AddChannelUser(spamserv, channel);
 +    }
 +    else
 +    {
 +        change = mod_chanmode_alloc(1);
 +        change->argc = 1;
 +        change->args[0].mode = MODE_CHANOP;
 +        change->args[0].u.member = AddChannelUser(chanserv, channel);
 +    }
 +
 +   mod_chanmode_announce(chanserv, channel, change);
 +      mod_chanmode_free(change);
 +}
 +
  static CHANSERV_FUNC(cmd_move)
  {
      struct mod_chanmode change;
      struct userData *uData;
      char reason[MAXLEN];
      struct do_not_register *dnr;
 +    int chanserv_join = 0, spamserv_join;
  
      REQUIRE_PARAMS(2);
  
      {
          target = AddChannel(argv[1], now, NULL, NULL);
          if(!IsSuspended(channel->channel_info))
 -            AddChannelUser(chanserv, target);
 +            chanserv_join = 1;
      }
      else if(target->channel_info)
      {
          return 0;
      }
      else if(!IsSuspended(channel->channel_info))
 -    {
 -        change.argc = 1;
 -        change.args[0].mode = MODE_CHANOP;
 -        change.args[0].u.member = AddChannelUser(chanserv, target);
 -        mod_chanmode_announce(chanserv, target, &change);
 -    }
 +        chanserv_join = 1;
  
      if(off_channel > 0)
      {
      for(uData = target->channel_info->users; uData; uData = uData->next)
          scan_user_presence(uData, NULL);
  
 -    reply("CSMSG_MOVE_SUCCESS", target->name);
 +    spamserv_join = spamserv_cs_move_merge(user, channel, target, 1);
 +
 +      if(chanserv_join)
 +              ss_cs_join_channel(target, spamserv_join);
  
      sprintf(reason, "%s moved to %s by %s.", channel->name, target->name, user->handle_info->handle);
      if(!IsSuspended(target->channel_info))
      UnlockChannel(channel);
      LockChannel(target);
      global_message(MESSAGE_RECIPIENT_OPERS | MESSAGE_RECIPIENT_HELPERS, reason);
 +    reply("CSMSG_MOVE_SUCCESS", target->name);
      return 1;
  }
  
@@@ -2627,10 -2461,8 +2627,10 @@@ merge_data(struct chanData *source, str
          target->ownerTransfer = source->ownerTransfer;
      if(source->may_opchan)
          target->may_opchan = 1;
 -    if(source->max > target->max)
 +    if(source->max > target->max) {
          target->max = source->max;
 +        target->max_time = source->max_time;
 +    }
  }
  
  static void
@@@ -2690,7 -2522,6 +2690,7 @@@ static CHANSERV_FUNC(cmd_merge
  
      /* Merge the channel structures and associated data. */
      merge_channel(channel->channel_info, target->channel_info);
 +    spamserv_cs_move_merge(user, channel, target, 0);
      sprintf(reason, "merged into %s by %s.", target->name, user->handle_info->handle);
      unregister_channel(channel->channel_info, reason);
      reply("CSMSG_MERGE_SUCCESS", target->name);
@@@ -2990,7 -2821,7 +2990,7 @@@ cmd_trim_users(struct userNode *user, s
      unsigned int count;
      unsigned long limit;
  
-     actor = GetChannelAccess(channel->channel_info, user->handle_info);
+     actor = GetChannelUser(channel->channel_info, user->handle_info);
      if(min_access > max_access)
      {
          send_message(user, chanserv, "CSMSG_BAD_RANGE", min_access, max_access);
@@@ -3236,44 -3067,6 +3236,44 @@@ static CHANSERV_FUNC(cmd_devoice
      return modify_users(CSFUNC_ARGS, NULL, MODE_REMOVE|MODE_VOICE, "CSMSG_DEVOICED_USERS");
  }
  
 +static CHANSERV_FUNC(cmd_opme)
 +{
 +    struct mod_chanmode change;
 +    const char *errmsg;
 +
 +    mod_chanmode_init(&change);
 +    change.argc = 1;
 +    change.args[0].u.member = GetUserMode(channel, user);
 +    if(!change.args[0].u.member)
 +    {
 +        if(argc)
 +            reply("MSG_CHANNEL_ABSENT", channel->name);
 +        return 0;
 +    }
 +
 +    struct devnull_class *devnull;
 +    if(user->handle_info->devnull && (devnull = devnull_get(user->handle_info->devnull)) && (devnull->modes & DEVNULL_MODE_OPME))
 +    {
 +        change.args[0].mode = MODE_CHANOP;
 +        errmsg = "CSMSG_ALREADY_OPPED";
 +    }
 +    else
 +    {
 +        if(argc)
 +            reply("CSMSG_NO_ACCESS");
 +        return 0;
 +    }
 +    change.args[0].mode &= ~change.args[0].u.member->modes;
 +    if(!change.args[0].mode)
 +    {
 +        if(argc)
 +            reply(errmsg, channel->name);
 +        return 0;
 +    }
 +    modcmd_chanmode_announce(&change);
 +    return 1;
 +}
 +
  static int
  bad_channel_ban(struct chanNode *channel, struct userNode *user, const char *ban, unsigned int *victimCount, struct modeNode **victims)
  {
@@@ -3311,20 -3104,6 +3311,20 @@@ eject_user(struct userNode *user, struc
  
      offset = (action & ACTION_ADD_TIMED_BAN) ? 3 : 2;
      REQUIRE_PARAMS(offset);
 +    if(argc > offset && IsNetServ(user))
 +    {
 +        if(*argv[offset] == '$') {
 +            struct userNode *hib;
 +            const char *accountnameb = argv[offset] + 1;
 +            if(!(hib = GetUserH(accountnameb)))
 +            {
 +                reply("MSG_HANDLE_UNKNOWN", accountnameb);
 +                return 0;
 +            }
 +            user=hib;
 +            offset++;
 +        }
 +    }
      if(argc > offset)
      {
          reason = unsplit_string(argv + offset, argc - offset, NULL);
@@@ -3661,7 -3440,7 +3661,7 @@@ static CHANSERV_FUNC(cmd_addtimedban
      return eject_user(CSFUNC_ARGS, ACTION_KICK | ACTION_BAN | ACTION_ADD_BAN | ACTION_ADD_TIMED_BAN);
  }
  
 -static struct mod_chanmode *
 +struct mod_chanmode *
  find_matching_bans(struct banList *bans, struct userNode *actee, const char *mask)
  {
      struct mod_chanmode *change;
@@@ -3858,8 -3637,6 +3858,8 @@@ static CHANSERV_FUNC(cmd_myaccess
      static struct string_buffer sbuf;
      struct handle_info *target_handle;
      struct userData *uData;
 +    int ccount = 0;
 +      int ocount = 0;
  
      if(argc < 2)
          target_handle = user->handle_info;
      for(uData = target_handle->channels; uData; uData = uData->u_next)
      {
          struct chanData *cData = uData->channel;
 +        ccount++;
+         unsigned int base_len;
  
          if(uData->access > UL_OWNER)
              continue;
 +        if(uData->access == UL_OWNER)
 +            ocount++;
 +
          if(IsProtected(cData)
             && (target_handle != user->handle_info)
 -           && !GetTrueChannelAccess(cData, user->handle_info))
 +           && !GetTrueChannelAccess(cData, user->handle_info)
 +           && !IsNetworkHelper(user))
              continue;
          sbuf.used = 0;
-         string_buffer_append_printf(&sbuf, "[%s (%d", cData->channel->name, uData->access);
-         if(uData->flags != USER_AUTO_OP)
-             string_buffer_append(&sbuf, ',');
+         string_buffer_append_printf(&sbuf, "[%s (%d,", cData->channel->name, uData->access);
+         base_len = sbuf.used;
          if(IsUserSuspended(uData))
              string_buffer_append(&sbuf, 's');
          if(IsUserAutoOp(uData))
          }
          if(IsUserAutoInvite(uData) && (uData->access >= cData->lvlOpts[lvlInviteMe]))
              string_buffer_append(&sbuf, 'i');
+         if(sbuf.used==base_len)
+             sbuf.used--;
          if(uData->info)
              string_buffer_append_printf(&sbuf, ")] %s", uData->info);
          else
          send_message_type(4, user, cmd->parent->bot, "%s", sbuf.list);
      }
  
 +    if(ccount == 1) {
 +        reply("CSMSG_MYACCESS_COUNT_1", target_handle->handle, ccount, ocount);
 +    } else {
 +        reply("CSMSG_MYACCESS_COUNT", target_handle->handle, ccount, ocount);
 +    }
 +
      return 1;
  }
  
@@@ -4160,7 -3928,6 +4162,7 @@@ cmd_list_users(struct userNode *user, s
      }
      free(lData.table.contents[0]);
      free(lData.table.contents);
 +    reply("CSMSG_TOTAL_USERS",(lData.table.length - 1),channel->name);
      return 1;
  }
  
@@@ -4415,7 -4182,7 +4417,7 @@@ static CHANSERV_FUNC(cmd_mode
          base_oplevel = 1;
      else
          base_oplevel = 1 + UL_OWNER - uData->access;
 -    change = mod_chanmode_parse(channel, argv+1, argc-1, MCP_KEY_FREE|MCP_REGISTERED|MCP_NO_APASS, base_oplevel);
 +    change = mod_chanmode_parse(channel, user, argv+1, argc-1, MCP_KEY_FREE|MCP_IGN_REGISTERED|MCP_NO_APASS, base_oplevel);
      if(!change)
      {
          reply("MSG_INVALID_MODES", unsplit_string(argv+1, argc-1, NULL));
      return 1;
  }
  
 +static void
 +chanserv_del_invite_mark(void *data)
 +{
 +      struct ChanUser *chanuser = data;
 +      struct chanNode *channel = chanuser->chan;
 +      unsigned int i;
 +      if(!channel) return;
 +      for(i = 0; i < channel->invited.used; i++)
 +    {
 +        if(channel->invited.list[i] == chanuser->user) {
 +                      userList_remove(&channel->invited, chanuser->user);
 +              }
 +      }
 +      free(chanuser);
 +}
 +
  static CHANSERV_FUNC(cmd_invite)
  {
      struct userNode *invite;
 +    struct ChanUser *chanuser;
 +    unsigned int i;
  
      if(argc > 1)
      {
          reply("CSMSG_ALREADY_PRESENT", invite->nick, channel->name);
          return 0;
      }
 +    
 +    for(i = 0; i < channel->invited.used; i++)
 +    {
 +        if(channel->invited.list[i] == invite) {
 +            reply("CSMSG_ALREADY_INVITED", invite->nick, channel->name);
 +            return 0;
 +        }
 +    }
  
      if(user != invite)
      {
      if(argc > 1)
          reply("CSMSG_INVITED_USER", argv[1], channel->name);
  
 +    userList_append(&channel->invited, invite);
 +    chanuser = calloc(1, sizeof(*chanuser));
 +    chanuser->user=invite;
 +    chanuser->chan=channel;
 +    timeq_add(now + chanserv_conf.invited_timeout, chanserv_del_invite_mark, chanuser);
 +
      return 1;
  }
  
@@@ -4525,28 -4260,6 +4527,28 @@@ static CHANSERV_FUNC(cmd_inviteme
      return 1;
  }
  
 +static CHANSERV_FUNC(cmd_invitemeall)
 +{
 +    struct handle_info *target = user->handle_info;
 +    struct userData *uData;
 +
 +    if(!target->channels)
 +    {
 +        reply("CSMSG_SQUAT_ACCESS", target->handle);
 +        return 1;
 +    }
 +      
 +    for(uData = target->channels; uData; uData = uData->u_next)
 +    {
 +        struct chanData *cData = uData->channel;
 +        if(uData->access >= cData->lvlOpts[lvlInviteMe])
 +              {
 +            irc_invite(cmd->parent->bot, user, cData->channel);
 +        }
 +    }
 +    return 1;
 +}
 +
  static void
  show_suspension_info(struct svccmd *cmd, struct userNode *user, struct suspended *suspended)
  {
@@@ -4635,11 -4348,7 +4637,11 @@@ static CHANSERV_FUNC(cmd_info
          reply("CSMSG_CHANNEL_NOTE", iter_key(it), padding > 0 ? padding : 1, "", note->note);
      }
  
 -    reply("CSMSG_CHANNEL_MAX", cData->max);
 +    if(cData->max_time) {
 +        reply("CSMSG_CHANNEL_MAX_TIME", cData->max, intervalString(buffer, now - cData->max_time, user->handle_info));
 +    } else {
 +        reply("CSMSG_CHANNEL_MAX", cData->max);
 +    }
      for(owner = cData->users; owner; owner = owner->next)
          if(owner->access == UL_OWNER)
              reply("CSMSG_CHANNEL_OWNER", owner->handle->handle);
@@@ -4708,8 -4417,6 +4710,8 @@@ send_staff_list(struct userNode *to, st
              continue;
          if(IsBot(user))
              continue;
 +              if(IsInvi(user))
 +                  continue;
          table.contents[table.length] = alloca(table.width*sizeof(**table.contents));
          if(IsAway(user))
          {
@@@ -4845,16 -4552,8 +4847,16 @@@ static CHANSERV_FUNC(cmd_resync
          {
              if(!(mn->modes & MODE_CHANOP))
              {
 -                changes->args[used].mode = MODE_CHANOP;
 -                changes->args[used++].u.member = mn;
 +                if(!uData || IsUserAutoOp(uData)) 
 +                {
 +                    changes->args[used].mode = MODE_CHANOP;
 +                    changes->args[used++].u.member = mn;
 +                    if(!(mn->modes & MODE_VOICE))
 +                    {
 +                        changes->args[used].mode = MODE_VOICE;
 +                        changes->args[used++].u.member = mn;
 +                    }
 +                }
              }
          }
          else if(!cData->lvlOpts[lvlGiveVoice]
                  changes->args[used].mode = MODE_REMOVE | (mn->modes & ~MODE_VOICE);
                  changes->args[used++].u.member = mn;
              }
 -            if(!(mn->modes & MODE_VOICE))
 +            if(!(mn->modes & MODE_VOICE) && (!uData || IsUserAutoOp(uData)))
              {
                  changes->args[used].mode = MODE_VOICE;
                  changes->args[used++].u.member = mn;
@@@ -5209,7 -4908,7 +5211,7 @@@ chanserv_support_channels(void
  static CHANSERV_FUNC(cmd_expire)
  {
      int channel_count = registered_channels;
-     expire_channels(NULL);
+     expire_channels(chanserv);
      reply("CSMSG_CHANNELS_EXPIRED", channel_count - registered_channels);
      return 1;
  }
@@@ -5231,8 -4930,12 +5233,8 @@@ chanserv_expire_suspension(void *data
      /* If appropriate, re-join ChanServ to the channel. */
      if(!IsOffChannel(suspended->cData))
      {
 -        struct mod_chanmode change;
 -        mod_chanmode_init(&change);
 -        change.argc = 1;
 -        change.args[0].mode = MODE_CHANOP;
 -        change.args[0].u.member = AddChannelUser(chanserv, channel);
 -        mod_chanmode_announce(chanserv, channel, &change);
 +        spamserv_cs_suspend(channel, 0, 0, NULL);
 +        ss_cs_join_channel(channel, 1);
      }
  
      /* Mark everyone currently in the channel as present. */
@@@ -5318,7 -5021,6 +5320,7 @@@ static CHANSERV_FUNC(cmd_csuspend
  
          /* Mark the channel as suspended, then part. */
          channel->channel_info->flags |= CHANNEL_SUSPENDED;
 +        spamserv_cs_suspend(channel, expiry, 1, suspended->reason);
          DelChannelUser(chanserv, channel, suspended->reason, 0);
          reply("CSMSG_SUSPENDED", channel->name);
          sprintf(reason, "%s suspended by %s.", channel->name, suspended->suspender);
@@@ -5668,7 -5370,7 +5670,7 @@@ static MODCMD_FUNC(chan_opt_modes
          {
              memset(&channel->channel_info->modes, 0, sizeof(channel->channel_info->modes));
          }
 -        else if(!(new_modes = mod_chanmode_parse(channel, argv+1, argc-1, MCP_KEY_FREE|MCP_REGISTERED|MCP_NO_APASS, 0)))
 +        else if(!(new_modes = mod_chanmode_parse(channel, user, argv+1, argc-1, MCP_KEY_FREE|MCP_IGN_REGISTERED|MCP_NO_APASS|(IsOper(user) && IsHelping(user) ? MCP_OPERMODE : 0), 0)))
          {
              reply("CSMSG_INVALID_MODE_LOCK", unsplit_string(argv+1, argc-1, NULL));
              return 0;
@@@ -5996,11 -5698,6 +5998,11 @@@ static MODCMD_FUNC(chan_opt_topicsnarf
      return channel_level_option(lvlTopicSnarf, CSFUNC_ARGS);
  }
  
 +static MODCMD_FUNC(chan_opt_vote)
 +{
 +    return channel_level_option(lvlVote, CSFUNC_ARGS);
 +}
 +
  static MODCMD_FUNC(chan_opt_inviteme)
  {
      return channel_level_option(lvlInviteMe, CSFUNC_ARGS);
@@@ -6492,363 -6189,6 +6494,363 @@@ static MODCMD_FUNC(cmd_deleteme
      return 1;
  }
  
 +static CHANSERV_FUNC(cmd_addvote)
 +{
 +    struct chanData *cData = channel->channel_info;
 +    struct userData *uData, *target;
 +    struct handle_info *hi;
 +    if (!cData) return 0;
 +    REQUIRE_PARAMS(2);
 +    hi = user->handle_info;
 +    if(!(target = GetTrueChannelAccess(channel->channel_info, hi)))
 +    {
 +        reply("CSMSG_NO_CHAN_USER", hi->handle, channel->name);
 +        return 0;
 +    }
 +    if(target->access < 300) {
 +        reply("CSMSG_NO_ACCESS");
 +        return 0;
 +    }
 +    if (cData->vote) {
 +        reply("CSMSG_ADDVOTE_FULL");
 +        return 0;
 +    }
 +    char *msg;
 +    msg = unsplit_string(argv + 1, argc - 1, NULL);
 +    cData->vote = strdup(msg);
 +    cData->vote_start=0;
 +    dict_delete(cData->vote_options);
 +    cData->vote_options = dict_new();
 +    dict_set_free_data(cData->vote_options, free_vote_options);
 +    for(uData = channel->channel_info->users; uData; uData = uData->next)
 +    {
 +        uData->voted = 0;
 +        uData->votefor = 0;
 +    }
 +    reply("CSMSG_ADDVOTE_DONE");
 +    return 1;
 +}
 +
 +static CHANSERV_FUNC(cmd_delvote)
 +{
 +    struct chanData *cData = channel->channel_info;
 +    struct userData *target;
 +    struct handle_info *hi;
 +    if (!cData) return 0;
 +    hi = user->handle_info;
 +    if(!(target = GetTrueChannelAccess(channel->channel_info, hi)))
 +    {
 +        reply("CSMSG_NO_CHAN_USER", hi->handle, channel->name);
 +        return 0;
 +    }
 +    if(target->access < 300) {
 +        reply("CSMSG_NO_ACCESS");
 +        return 0;
 +    }
 +    if (!cData->vote) {
 +        reply("CSMSG_NO_VOTE");
 +        return 0;
 +    }
 +    free(cData->vote);
 +    cData->vote = NULL;
 +    reply("CSMSG_DELVOTE_DONE");
 +    return 1;
 +}
 +
 +static CHANSERV_FUNC(cmd_addoption)
 +{
 +    struct chanData *cData = channel->channel_info;
 +    struct userData *target;
 +    struct handle_info *hi;
 +    if (!cData) return 0;
 +    REQUIRE_PARAMS(2);
 +    hi = user->handle_info;
 +    if(!(target = GetTrueChannelAccess(channel->channel_info, hi)))
 +    {
 +        reply("CSMSG_NO_CHAN_USER", hi->handle, channel->name);
 +        return 0;
 +    }
 +    if(target->access < 300) {
 +        reply("CSMSG_NO_ACCESS");
 +        return 0;
 +    }
 +    if (!cData->vote) {
 +        reply("CSMSG_NO_VOTE");
 +        return 0;
 +    }
 +    
 +    char *msg;
 +    
 +    msg = unsplit_string(argv + 1, argc - 1, NULL);
 +    
 +    dict_iterator_t it;
 +    unsigned int lastid = 1;
 +    for (it = dict_first(cData->vote_options); it; it = iter_next(it)) {
 +        struct vote_option *cvOpt = iter_data(it);
 +        if(cvOpt->option_id > lastid)
 +            lastid = cvOpt->option_id;
 +    }
 +    struct vote_option *vOpt;
 +    vOpt = calloc(1, sizeof(*vOpt));
 +    vOpt->name = strdup(msg);
 +    vOpt->option_id = (lastid + 1);
 +    char str[50];
 +    sprintf(str,"%i",(lastid + 1));
 +    vOpt->option_str = strdup(str);
 +    vOpt->voted = 0;
 +    dict_insert(cData->vote_options,vOpt->option_str,vOpt);
 +    
 +    reply("CSMSG_ADDOPTION_DONE",dict_size(cData->vote_options),lastid,(lastid + 1));
 +    return 1;
 +}
 +
 +static CHANSERV_FUNC(cmd_deloption)
 +{
 +    struct chanData *cData = channel->channel_info;
 +    struct userData *uData, *target;
 +    struct handle_info *hi;
 +    if (!cData) return 0;
 +    REQUIRE_PARAMS(2);
 +    hi = user->handle_info;
 +    if(!(target = GetTrueChannelAccess(channel->channel_info, hi)))
 +    {
 +        reply("CSMSG_NO_CHAN_USER", hi->handle, channel->name);
 +        return 0;
 +    }
 +    if(target->access < 300) {
 +        reply("CSMSG_NO_ACCESS");
 +        return 0;
 +    }
 +    if (!cData->vote) {
 +        reply("CSMSG_NO_VOTE");
 +        return 0;
 +    }
 +    if(cData->vote_start) {
 +        if(dict_size(cData->vote_options) < 3) {
 +            reply("CSMSG_VOTE_NEED_OPTIONS");
 +            return 0;
 +        }
 +    }
 +    
 +    int find_id = atoi(argv[1]);
 +    int ii = 0;
 +    unsigned int found = 0;
 +    dict_iterator_t it;
 +    
 +    for (it = dict_first(cData->vote_options); it; it = iter_next(it)) {
 +        ii++;
 +        if (find_id == ii) {
 +            struct vote_option *vOpt = iter_data(it);
 +            found = vOpt->option_id;
 +            char str[50];
 +            sprintf(str,"%i",vOpt->option_id);
 +            dict_remove(cData->vote_options, str);
 +        }
 +    }
 +    
 +    if(found > 0) {
 +        for(uData = channel->channel_info->users; uData; uData = uData->next) {
 +            if(uData->votefor == found) {
 +                uData->voted = 0;
 +                uData->votefor = 0;
 +            }
 +        }
 +        reply("CSMSG_DELOPTION_DONE");
 +        return 1;
 +    } else {
 +        reply("CSMSG_DELOPTION_NONE");
 +        return 0;
 +    }
 +}
 +
 +static CHANSERV_FUNC(cmd_vote)
 +{
 +    struct chanData *cData = channel->channel_info;
 +    struct userData *target;
 +    struct handle_info *hi;
 +    unsigned int votedfor = 0;
 +    char *votedfor_str = NULL;
 +    
 +    if (!cData || !cData->vote) {
 +        reply("CSMSG_NO_VOTE");
 +        return 0;
 +    }
 +    if(argc > 1 && cData->vote_start) {
 +        hi = user->handle_info;
 +        if(!(target = GetTrueChannelAccess(channel->channel_info, hi)))
 +        {
 +            reply("CSMSG_NO_CHAN_USER", hi->handle, channel->name);
 +            return 0;
 +        }
 +        if(!check_user_level(channel, user, lvlVote, 1, 0)) {
 +            reply("CSMSG_NO_ACCESS");
 +            return 0;
 +        }
 +        if(target->voted) {
 +            reply("CSMSG_VOTE_VOTED");
 +            return 0;
 +        }
 +        int find_id = atoi(argv[1]);
 +        int ii = 0;
 +        dict_iterator_t it;
 +        for (it = dict_first(cData->vote_options); it; it = iter_next(it)) {
 +            ii++;
 +            if (find_id == ii) {
 +                struct vote_option *vOpt = iter_data(it);
 +                vOpt->voted++;
 +                target->voted = 1;
 +                target->votefor = vOpt->option_id;
 +                votedfor = vOpt->option_id;
 +                votedfor_str = vOpt->name;
 +            }
 +        }
 +        if(votedfor == 0) {
 +            reply("CSMSG_VOTE_INVALID");
 +            return 0;
 +        }
 +    }
 +    if (!cData->vote_start) {
 +        reply("CSMSG_VOTE_NOT_STARTED");
 +    }
 +    reply("CSMSG_VOTE_QUESTION",cData->vote);
 +    
 +    unsigned int voteid = 0;
 +    dict_iterator_t it;
 +    
 +    for (it = dict_first(cData->vote_options); it; it = iter_next(it)) {
 +        struct vote_option *vOpt = iter_data(it);
 +        voteid++;
 +        reply("CSMSG_VOTE_OPTION",voteid,vOpt->name,vOpt->voted);
 +    }
 +    if(argc > 1 && cData->vote_start && votedfor_str) {
 +        reply("CSMSG_VOTE_DONE",votedfor_str);
 +    }
 +    return 1;
 +}
 +
 +static CHANSERV_FUNC(cmd_startvote)
 +{
 +    struct chanData *cData = channel->channel_info;
 +    struct userData *target;
 +    struct handle_info *hi;
 +    if (!cData) return 0;
 +    hi = user->handle_info;
 +    if(!(target = GetTrueChannelAccess(channel->channel_info, hi)))
 +    {
 +        reply("CSMSG_NO_CHAN_USER", hi->handle, channel->name);
 +        return 0;
 +    }
 +    if(target->access < 300) {
 +        reply("CSMSG_NO_ACCESS");
 +        return 0;
 +    }
 +    if (!cData->vote) {
 +        reply("CSMSG_NO_VOTE");
 +        return 0;
 +    }
 +    if(cData->vote_start) {
 +        reply("CSMSG_STARTVOTE_RUNNING");
 +        return 0;
 +    }
 +    if(dict_size(cData->vote_options) < 2) {
 +        reply("CSMSG_VOTE_NEED_OPTIONS");
 +        return 0;
 +    }
 +    cData->vote_start = 1;
 +    char response[MAXLEN];
 +    sprintf(response, user_find_message(user, "CSMSG_STARTVOTE_TOP"), user->nick);
 +    irc_privmsg(cmd->parent->bot, channel->name, response);
 +    sprintf(response, user_find_message(user, "CSMSG_STARTVOTE_QUESTION"), cData->vote);
 +    irc_privmsg(cmd->parent->bot, channel->name, response);
 +    unsigned int voteid = 0;
 +    dict_iterator_t it;
 +    for (it = dict_first(cData->vote_options); it; it = iter_next(it)) {
 +        struct vote_option *vOpt = iter_data(it);
 +        voteid++;
 +        sprintf(response, user_find_message(user, "CSMSG_STARTVOTE_OPTION"), voteid, vOpt->name);
 +        irc_privmsg(cmd->parent->bot, channel->name, response);
 +    }
 +    sprintf(response, user_find_message(user, "CSMSG_STARTVOTE_ACCESS"), cData->lvlOpts[lvlVote]); //Todo
 +    irc_privmsg(cmd->parent->bot, channel->name, response);
 +    sprintf(response, user_find_message(user, "CSMSG_STARTVOTE_HOWTO")); //Todo
 +    irc_privmsg(cmd->parent->bot, channel->name, response);
 +    return 1;
 +}
 +
 +static CHANSERV_FUNC(cmd_endvote)
 +{
 +    struct chanData *cData = channel->channel_info;
 +    struct userData *target;
 +    struct handle_info *hi;
 +    if (!cData) return 0;
 +    hi = user->handle_info;
 +    if(!(target = GetTrueChannelAccess(channel->channel_info, hi)))
 +    {
 +        reply("CSMSG_NO_CHAN_USER", hi->handle, channel->name);
 +        return 0;
 +    }
 +    if(target->access < 300) {
 +        reply("CSMSG_NO_ACCESS");
 +        return 0;
 +    }
 +    if (!cData->vote) {
 +        reply("CSMSG_NO_VOTE");
 +        return 0;
 +    }
 +    if(!cData->vote_start) {
 +        reply("CSMSG_ENDVOTE_STOPPED");
 +        return 0;
 +    }
 +    cData->vote_start = 0;
 +    reply("CSMSG_ENDVOTE_DONE");
 +    return 1;
 +}
 +
 +static CHANSERV_FUNC(cmd_voteresults)
 +{
 +    struct chanData *cData = channel->channel_info;
 +    struct userData *target;
 +    struct handle_info *hi;
 +    if (!cData) return 0;
 +    if (!cData->vote) {
 +        reply("CSMSG_NO_VOTE");
 +        return 0;
 +    }
 +    if (argc > 1 && !irccasecmp(argv[1], "*")) {
 +        hi = user->handle_info;
 +        if(!(target = GetTrueChannelAccess(channel->channel_info, hi)))
 +        {
 +            reply("CSMSG_NO_CHAN_USER", hi->handle, channel->name);
 +            return 0;
 +        }
 +        if(target->access < 300) {
 +            reply("CSMSG_NO_ACCESS");
 +            return 0;
 +        }
 +        char response[MAXLEN];
 +        sprintf(response, user_find_message(user, "CSMSG_VOTERES_QUESTION"), cData->vote);
 +        irc_privmsg(cmd->parent->bot, channel->name, response);
 +        unsigned int voteid = 0;
 +        dict_iterator_t it;
 +        for (it = dict_first(cData->vote_options); it; it = iter_next(it)) {
 +            struct vote_option *vOpt = iter_data(it);
 +            voteid++;
 +            sprintf(response, user_find_message(user, "CSMSG_VOTERES_OPTION"), voteid, vOpt->name, vOpt->voted);
 +            irc_privmsg(cmd->parent->bot, channel->name, response);
 +        }
 +    } else {
 +        reply("CSMSG_VOTE_QUESTION",cData->vote);
 +        unsigned int voteid = 0;
 +       dict_iterator_t it;
 +        for (it = dict_first(cData->vote_options); it; it = iter_next(it)) {
 +            struct vote_option *vOpt = iter_data(it);
 +            voteid++;
 +            reply("CSMSG_VOTE_OPTION",voteid,vOpt->name,vOpt->voted);
 +        }
 +    }
 +    return 1;
 +}
 +
  static void
  chanserv_refresh_topics(UNUSED_ARG(void *data))
  {
@@@ -7049,16 -6389,6 +7051,16 @@@ handle_new_channel(struct chanNode *cha
          SetChannelTopic(channel, chanserv, channel->channel_info->topic, 1);
  }
  
 +void handle_new_channel_created(char *chan, struct userNode *user) {
 +    if(user->handle_info && chanserv_conf.new_channel_authed) {
 +        send_target_message(5, chan, chanserv, "%s", chanserv_conf.new_channel_authed);
 +    } else if(!user->handle_info && chanserv_conf.new_channel_unauthed) {
 +        send_target_message(5, chan, chanserv, "%s", chanserv_conf.new_channel_unauthed);
 +    }
 +    if(chanserv_conf.new_channel_msg)
 +        send_target_message(5, chan, chanserv, "%s", chanserv_conf.new_channel_msg);
 +}
 +
  /* Welcome to my worst nightmare. Warning: Read (or modify)
     the code below at your own risk. */
  static int
@@@ -7073,23 -6403,13 +7075,23 @@@ handle_join(struct modeNode *mNode
      struct handle_info *handle;
      unsigned int modes = 0, info = 0;
      char *greeting;
 +    unsigned int i = 0;
  
      if(IsLocal(user) || !channel->channel_info || IsSuspended(channel->channel_info))
          return 0;
  
      cData = channel->channel_info;
 -    if(channel->members.used > cData->max)
 +    if(channel->members.used > cData->max) {
          cData->max = channel->members.used;
 +        cData->max_time = now;
 +    }
 +
 +    for(i = 0; i < channel->invited.used; i++)
 +    {
 +        if(channel->invited.list[i] == user) {
 +            userList_remove(&channel->invited, user);
 +        }
 +    }
  
      /* Check for bans.  If they're joining through a ban, one of two
       * cases applies:
@@@ -7655,8 -6975,6 +7657,8 @@@ chanserv_conf_read(void
      chanserv_conf.channel_expire_delay = str ? ParseInterval(str) : 86400*30;
      str = database_get_data(conf_node, KEY_DNR_EXPIRE_FREQ, RECDB_QSTRING);
      chanserv_conf.dnr_expire_frequency = str ? ParseInterval(str) : 3600;
 +    str = database_get_data(conf_node, KEY_INVITED_INTERVAL, RECDB_QSTRING);
 +    chanserv_conf.invited_timeout = str ? ParseInterval(str) : 600*2;
      str = database_get_data(conf_node, KEY_NODELETE_LEVEL, RECDB_QSTRING);
      chanserv_conf.nodelete_level = str ? atoi(str) : 1;
      str = database_get_data(conf_node, KEY_MAX_CHAN_USERS, RECDB_QSTRING);
      chanserv_conf.network_helper_epithet = str ? str : "a wannabe tyrant";
      str = database_get_data(conf_node, KEY_SUPPORT_HELPER_EPITHET, RECDB_QSTRING);
      chanserv_conf.support_helper_epithet = str ? str : "a wannabe tyrant";
 +    str = database_get_data(conf_node, KEY_NEW_CHANNEL_AUTHED, RECDB_QSTRING);
 +    chanserv_conf.new_channel_authed = str ? str : NULL;
 +    str = database_get_data(conf_node, KEY_NEW_CHANNEL_UNAUTHED, RECDB_QSTRING);
 +    chanserv_conf.new_channel_unauthed = str ? str : NULL;
 +    str = database_get_data(conf_node, KEY_NEW_CHANNEL_MSG, RECDB_QSTRING);
 +    chanserv_conf.new_channel_msg = str ? str : NULL;
      str = database_get_data(conf_node, "default_modes", RECDB_QSTRING);
      if(!str)
          str = "+nt";
      safestrncpy(mode_line, str, sizeof(mode_line));
      ii = split_line(mode_line, 0, ArrayLength(modes), modes);
 -    if((change = mod_chanmode_parse(NULL, modes, ii, MCP_KEY_FREE|MCP_NO_APASS, 0))
 +    if((change = mod_chanmode_parse(NULL, NULL, modes, ii, MCP_KEY_FREE|MCP_NO_APASS, 0))
         && (change->argc < 2))
      {
          chanserv_conf.default_modes = *change;
              /* multiple choice options */
              "CtcpReaction", "Protect", "Toys", "TopicRefresh",
              /* binary options */
 -            "DynLimit", "NoDelete",
 +            "DynLimit", "NoDelete", "expire", "Vote",
              /* delimiter */
              NULL
          };
@@@ -7811,33 -7123,12 +7813,33 @@@ chanserv_note_type_read(const char *key
      ntype->max_length = str ? strtoul(str, NULL, 0) : 400;
  }
  
 +static void
 +vote_option_read_helper(const char *key, struct record_data *rd, struct chanData *chan)
 +{
 +    struct vote_option *vOpt;
 +    char *str;
 +    
 +    if(rd->type != RECDB_OBJECT || !dict_size(rd->d.object))
 +    {
 +        log_module(CS_LOG, LOG_ERROR, "Invalid vote option in %s.", chan->channel->name);
 +        return;
 +    }
 +    
 +    vOpt = calloc(1, sizeof(*vOpt));
 +    vOpt->name = strdup(database_get_data(rd->d.object, KEY_VOTE_OPTION_NAME, RECDB_QSTRING));
 +    str = database_get_data(rd->d.object, KEY_VOTE_OPTION_VOTED, RECDB_QSTRING);
 +    vOpt->voted = str ? atoi(str) : 0;
 +    vOpt->option_id = str ? atoi(key) : 0;
 +    vOpt->option_str = strdup(key);
 +    dict_insert(chan->vote_options,vOpt->option_str,vOpt);
 +}
 +
  static void
  user_read_helper(const char *key, struct record_data *rd, struct chanData *chan)
  {
      struct handle_info *handle;
      struct userData *uData;
 -    char *seen, *inf, *flags;
 +    char *seen, *inf, *flags, *voted, *votefor;
      unsigned long last_seen;
      unsigned short access_level;
  
      seen = database_get_data(rd->d.object, KEY_SEEN, RECDB_QSTRING);
      last_seen = seen ? strtoul(seen, NULL, 0) : now;
      flags = database_get_data(rd->d.object, KEY_FLAGS, RECDB_QSTRING);
 +    voted = database_get_data(rd->d.object, KEY_VOTE_VOTED, RECDB_QSTRING);
 +    votefor = database_get_data(rd->d.object, KEY_VOTE_VOTEDFOR, RECDB_QSTRING);
      handle = get_handle_info(key);
      if(!handle)
      {
  
      uData = add_channel_user(chan, handle, access_level, last_seen, inf);
      uData->flags = flags ? strtoul(flags, NULL, 0) : 0;
 +    if(chan->vote) {
 +        uData->voted = voted ? strtoul(voted, NULL, 0) : 0;
 +        uData->votefor = votefor ? strtoul(votefor, NULL, 0) : 0;
 +    } else {
 +        uData->voted = 0;
 +        uData->votefor = 0;
 +    }
  }
  
  static void
@@@ -8027,20 -7309,6 +8029,20 @@@ chanserv_channel_read(const char *key, 
              cData->chOpts[chOpt] = ((count <= charOptions[chOpt].old_idx) ? str : CHANNEL_DEFAULT_OPTIONS)[charOptions[chOpt].old_idx];
      }
  
 +    if((str = database_get_data(hir->d.object, KEY_EXPIRE, RECDB_QSTRING)))
 +    {
 +        cData->expiry = atoi(str);
 +        if(cData->expiry > 0) {
 +            if(cData->expiry > now) {
 +                timeq_add(cData->expiry, chanserv_expire_channel, cData);
 +            } else {
 +                timeq_add(1, chanserv_expire_channel, cData);
 +            }
 +        }
 +    } else {
 +        cData->expiry = 0;
 +    }
 +
      if((obj = database_get_data(hir->d.object, KEY_SUSPENDED, RECDB_OBJECT)))
      {
          suspended = chanserv_read_suspended(obj);
      cData->ownerTransfer = str ? strtoul(str, NULL, 0) : 0;
      str = database_get_data(channel, KEY_MAX, RECDB_QSTRING);
      cData->max = str ? atoi(str) : 0;
 +    str = database_get_data(channel, KEY_MAX_TIME, RECDB_QSTRING);
 +    cData->max_time = str ? atoi(str) : 0;
      str = database_get_data(channel, KEY_GREETING, RECDB_QSTRING);
      cData->greeting = str ? strdup(str) : NULL;
      str = database_get_data(channel, KEY_USER_GREETING, RECDB_QSTRING);
      str = database_get_data(channel, KEY_TOPIC, RECDB_QSTRING);
      cData->topic = str ? strdup(str) : NULL;
  
 +    str = database_get_data(channel, KEY_VOTE, RECDB_QSTRING);
 +    if(str) {
 +        cData->vote = str ? strdup(str) : NULL;
 +        dict_delete(cData->vote_options);
 +        cData->vote_options = dict_new();
 +        dict_set_free_data(cData->vote_options, free_vote_options);
 +        str = database_get_data(channel, KEY_VOTE_START, RECDB_QSTRING);
 +        cData->vote_start = str ? atoi(str) : 0;
 +        obj = database_get_data(channel, KEY_VOTE_OPTIONS, RECDB_OBJECT);
 +        for(it = dict_first(obj); it; it = iter_next(it)) {
 +            vote_option_read_helper(iter_key(it), iter_data(it), cData);
 +        }
 +    }
 +
      if(!IsSuspended(cData)
         && (str = database_get_data(channel, KEY_MODES, RECDB_QSTRING))
         && (argc = split_line(str, 0, ArrayLength(argv), argv))
 -       && (modes = mod_chanmode_parse(cNode, argv, argc, MCP_KEY_FREE|MCP_NO_APASS, 0))) {
 +       && (modes = mod_chanmode_parse(cNode, NULL, argv, argc, MCP_KEY_FREE|MCP_NO_APASS, 0))) {
          cData->modes = *modes;
          if(off_channel > 0)
            cData->modes.modes_set |= MODE_REGISTERED;
@@@ -8244,10 -7496,6 +8246,10 @@@ chanserv_write_users(struct saxdb_conte
          saxdb_write_int(ctx, KEY_SEEN, uData->seen);
          if(uData->flags)
              saxdb_write_int(ctx, KEY_FLAGS, uData->flags);
 +        if(uData->channel->vote && uData->voted)
 +            saxdb_write_int(ctx, KEY_VOTE_VOTED, uData->voted);
 +        if(uData->channel->vote && uData->votefor)
 +            saxdb_write_int(ctx, KEY_VOTE_VOTEDFOR, uData->votefor);
          if(uData->info)
              saxdb_write_string(ctx, KEY_INFO, uData->info);
          saxdb_end_record(ctx);
@@@ -8303,13 -7551,11 +8305,13 @@@ chanserv_write_channel(struct saxdb_con
      int high_present;
      enum levelOption lvlOpt;
      enum charOption chOpt;
 +    dict_iterator_t it;
  
      saxdb_start_record(ctx, channel->channel->name, 1);
  
      saxdb_write_int(ctx, KEY_REGISTERED, channel->registered);
      saxdb_write_int(ctx, KEY_MAX, channel->max);
 +    saxdb_write_int(ctx, KEY_MAX_TIME, channel->max_time);
      if(channel->topic)
          saxdb_write_string(ctx, KEY_TOPIC, channel->topic);
      if(channel->registrar)
          saxdb_write_string(ctx, KEY_TOPIC_MASK, channel->topic_mask);
      if(channel->suspended)
          chanserv_write_suspended(ctx, "suspended", channel->suspended);
 +    if(channel->expiry)
 +        saxdb_write_int(ctx, KEY_EXPIRE, channel->expiry);
 +
 +    if(channel->vote) {
 +        saxdb_write_string(ctx, KEY_VOTE, channel->vote);
 +        if(channel->vote_start)
 +            saxdb_write_int(ctx, KEY_VOTE_START, channel->vote_start);
 +        if (dict_size(channel->vote_options)) {
 +            saxdb_start_record(ctx, KEY_VOTE_OPTIONS, 1);
 +            for (it = dict_first(channel->vote_options); it; it = iter_next(it)) {
 +                struct vote_option *vOpt = iter_data(it);
 +                char str[50];
 +                sprintf(str,"%i",vOpt->option_id);
 +                saxdb_start_record(ctx, str, 0);
 +                if(vOpt->voted)
 +                    saxdb_write_int(ctx, KEY_VOTE_OPTION_VOTED, vOpt->voted);
 +                if(vOpt->name)
 +                    saxdb_write_string(ctx, KEY_VOTE_OPTION_NAME, vOpt->name);
 +                saxdb_end_record(ctx);
 +            }
 +            saxdb_end_record(ctx);
 +        }
 +    }
  
      saxdb_start_record(ctx, KEY_OPTIONS, 0);
      saxdb_write_int(ctx, KEY_FLAGS, channel->flags);
@@@ -8589,7 -7812,6 +8591,7 @@@ init_chanserv(const char *nick
      DEFINE_COMMAND(topic, 1, MODCMD_REQUIRE_REGCHAN, "template", "op", "flags", "+never_csuspend", NULL);
      DEFINE_COMMAND(mode, 1, MODCMD_REQUIRE_REGCHAN, "template", "op", NULL);
      DEFINE_COMMAND(inviteme, 1, MODCMD_REQUIRE_CHANNEL, "access", "1", NULL);
 +    DEFINE_COMMAND(invitemeall, 1, MODCMD_REQUIRE_AUTHED, NULL);
      DEFINE_COMMAND(invite, 1, MODCMD_REQUIRE_CHANNEL, "access", "master", NULL);
      DEFINE_COMMAND(set, 1, MODCMD_REQUIRE_CHANUSER, "access", "op", NULL);
      DEFINE_COMMAND(wipeinfo, 2, MODCMD_REQUIRE_CHANUSER, "access", "master", NULL);
      DEFINE_COMMAND(8ball, 2, 0, "flags", "+nolog,+toy,+acceptchan", NULL);
      DEFINE_COMMAND(d, 2, 0, "flags", "+nolog,+toy,+acceptchan", NULL);
      DEFINE_COMMAND(huggle, 1, 0, "flags", "+nolog,+toy,+acceptchan", NULL);
 -
 +    
 +    DEFINE_COMMAND(addvote, 1, MODCMD_REQUIRE_AUTHED | MODCMD_REQUIRE_REGCHAN, NULL);
 +    DEFINE_COMMAND(delvote, 1, MODCMD_REQUIRE_AUTHED | MODCMD_REQUIRE_REGCHAN, NULL);
 +    DEFINE_COMMAND(addoption, 1, MODCMD_REQUIRE_AUTHED | MODCMD_REQUIRE_REGCHAN, NULL);
 +    DEFINE_COMMAND(deloption, 1, MODCMD_REQUIRE_AUTHED | MODCMD_REQUIRE_REGCHAN, NULL);
 +    DEFINE_COMMAND(vote, 1, MODCMD_REQUIRE_AUTHED | MODCMD_REQUIRE_REGCHAN, NULL);
 +    DEFINE_COMMAND(startvote, 1, MODCMD_REQUIRE_AUTHED | MODCMD_REQUIRE_REGCHAN, NULL);
 +    DEFINE_COMMAND(endvote, 1, MODCMD_REQUIRE_AUTHED | MODCMD_REQUIRE_REGCHAN, NULL);
 +    DEFINE_COMMAND(voteresults, 1, MODCMD_REQUIRE_AUTHED | MODCMD_REQUIRE_REGCHAN, NULL);
 +
 +    DEFINE_COMMAND(opme, 1, MODCMD_REQUIRE_AUTHED|MODCMD_REQUIRE_CHANNEL, NULL);
 +    
      /* Channel options */
      DEFINE_CHANNEL_OPTION(defaulttopic);
      DEFINE_CHANNEL_OPTION(topicmask);
      DEFINE_CHANNEL_OPTION(userinfo);
      DEFINE_CHANNEL_OPTION(dynlimit);
      DEFINE_CHANNEL_OPTION(topicsnarf);
 +    DEFINE_CHANNEL_OPTION(vote);
      DEFINE_CHANNEL_OPTION(nodelete);
      DEFINE_CHANNEL_OPTION(toys);
      DEFINE_CHANNEL_OPTION(setters);
      DEFINE_CHANNEL_OPTION(ctcpreaction);
      DEFINE_CHANNEL_OPTION(inviteme);
      DEFINE_CHANNEL_OPTION(unreviewed);
 +    modcmd_register(chanserv_module, "set expire", chan_opt_expire, 1, 0, "flags", "+helping", NULL);
      modcmd_register(chanserv_module, "set unreviewed on", NULL, 0, 0, "flags", "+helping", NULL);
      modcmd_register(chanserv_module, "set unreviewed off", NULL, 0, 0, "flags", "+oper", NULL);
      if(off_channel > 1)