Tweak default access restrictions for "set unreviewed *".
[srvx.git] / src / chanserv.c
index b5e3d1e6a97e6f22691986a65947787b591ded43..f9381870bc3c4567810be2a52b13f7a07f4415d7 100644 (file)
@@ -1,11 +1,12 @@
 /* chanserv.c - Channel service bot
- * Copyright 2000-2004 srvx Development Team
+ * Copyright 2000-2006 srvx Development Team
  *
- * This program is free software; you can redistribute it and/or modify
+ * This file is part of srvx.
+ *
+ * srvx 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.  Important limitations are
- * listed in the COPYING file that accompanies this software.
+ * (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
@@ -13,7 +14,8 @@
  * 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, email srvx-maintainers@srvx.net.
+ * along with srvx; if not, write to the Free Software Foundation,
+ * Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA.
  */
 
 #include "chanserv.h"
 #define KEY_ADJUST_DELAY       "adjust_delay"
 #define KEY_CHAN_EXPIRE_FREQ   "chan_expire_freq"
 #define KEY_CHAN_EXPIRE_DELAY  "chan_expire_delay"
+#define KEY_DNR_EXPIRE_FREQ    "dnr_expire_freq"
 #define KEY_MAX_CHAN_USERS             "max_chan_users"
 #define KEY_MAX_CHAN_BANS      "max_chan_bans"
 #define KEY_NICK               "nick"
 #define KEY_OLD_CHANSERV_NAME  "old_chanserv_name"
-#define KEY_MAX_SWITCH_LOAD    "max_switch_load"
-#define KEY_SWITCH_TIMEOUT     "switch_timeout"
 #define KEY_8BALL_RESPONSES     "8ball"
 #define KEY_OLD_BAN_NAMES       "old_ban_names"
 #define KEY_REFRESH_PERIOD      "refresh_period"
@@ -52,6 +53,8 @@
 #define KEY_NETWORK_HELPER_EPITHET  "network_helper_epithet"
 #define KEY_SUPPORT_HELPER_EPITHET  "support_helper_epithet"
 #define KEY_NODELETE_LEVEL      "nodelete_level"
+#define KEY_MAX_USERINFO_LENGTH "max_userinfo_length"
+#define KEY_GIVEOWNERSHIP_PERIOD "giveownership_timeout"
 
 /* ChanServ database */
 #define KEY_CHANNELS           "channels"
 #define KEY_MAX                        "max"
 #define KEY_NOTES               "notes"
 #define KEY_TOPIC_MASK          "topic_mask"
+#define KEY_OWNER_TRANSFER      "owner_transfer"
 
 /* User data */
 #define KEY_LEVEL              "level"
 #define KEY_EXPIRES             "expires"
 #define KEY_TRIGGERED          "triggered"
 
-#define CHANNEL_DEFAULT_FLAGS   (CHANNEL_INFO_LINES)
+#define CHANNEL_DEFAULT_FLAGS   (CHANNEL_OFFCHANNEL | CHANNEL_UNREVIEWED)
+#define CHANNEL_PRESERVED_FLAGS (CHANNEL_UNREVIEWED)
 #define CHANNEL_DEFAULT_OPTIONS "lmoooanpcnat"
 
 /* Administrative messages */
@@ -131,6 +136,7 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_DNR_SEARCH_RESULTS", "The following do-not-registers were found:" },
     { "CSMSG_DNR_INFO", "$b%s$b is do-not-register (by $b%s$b): %s" },
     { "CSMSG_DNR_INFO_SET", "$b%s$b is do-not-register (set %s by $b%s$b): %s" },
+    { "CSMSG_DNR_INFO_SET_EXPIRES", "$b%s$b is do-not-register (set %s by $b%s$b; expires %s): %s" },
     { "CSMSG_MORE_DNRS", "%d more do-not-register entries skipped." },
     { "CSMSG_DNR_CHANNEL", "Only network staff may register $b%s$b." },
     { "CSMSG_DNR_CHANNEL_MOVE", "Only network staff may move $b%s$b." },
@@ -138,6 +144,8 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_NOREGISTER_CHANNEL", "$b%s$b has been added to the do-not-register list." },
     { "CSMSG_NO_SUCH_DNR", "$b%s$b is not in the do-not-register list." },
     { "CSMSG_DNR_REMOVED", "$b%s$b has been removed from the do-not-register list." },
+    { "CSMSG_DNR_BAD_ACTION", "$b%s$b is not a recognized do-not-register action." },
+    { "CSMSG_DNR_SEARCH_RESULTS", "The following do-not-registers were found:" },
 
 /* Channel unregistration */
     { "CSMSG_UNREG_SUCCESS", "$b%s$b has been unregistered." },
@@ -180,7 +188,7 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_DELETED_YOU", "Your $b%d$b access has been deleted from $b%s$b." },
 
 /* User management */
-    { "CSMSG_ADDED_USER", "Added new %s to the %s user list with access %d." },
+    { "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_DELETED_USERS", "Deleted accounts matching $b%s$b with access from $b%d$b to $b%d$b from the %s user list." },
@@ -192,7 +200,9 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_NO_SELF_CLVL", "You cannot change your own access." },
     { "CSMSG_NO_BUMP_ACCESS", "You cannot give users access greater than or equal to your own." },
     { "CSMSG_MULTIPLE_OWNERS", "There is more than one owner in %s; please use $bCLVL$b, $bDELOWNER$b and/or $bADDOWNER$b instead." },
+    { "CSMSG_TRANSFER_WAIT", "You must wait %s before you can give ownership of $b%s$b to someone else." },
     { "CSMSG_NO_TRANSFER_SELF", "You cannot give ownership to your own account." },
+    { "CSMSG_CONFIRM_GIVEOWNERSHIP", "To really give ownership to $b%1$s$b, you must use 'giveownership *%1$s %2$s'." },
     { "CSMSG_OWNERSHIP_GIVEN", "Ownership of $b%s$b has been transferred to account $b%s$b." },
 
 /* Ban management */
@@ -223,7 +233,7 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_TOPIC_SET", "Topic is now '%s'." },
     { "CSMSG_NO_TOPIC", "$b%s$b does not have a default topic." },
     { "CSMSG_TOPICMASK_CONFLICT1", "I do not know how to make that topic work with the current topic mask in $b%s$b, which is: %s" },
-    { "CSMSG_TOPICMASK_CONFLICT2", "Please make sure your topic at most %d characters and matches the topic mask pattern." },
+    { "CSMSG_TOPICMASK_CONFLICT2", "Please make sure your topic is at most %d characters and matches the topic mask pattern." },
     { "CSMSG_TOPIC_LOCKED", "The %s topic is locked." },
     { "CSMSG_MASK_BUT_NO_TOPIC", "Warning: $b%s$b does not have a default topic, but you just set the topic mask." },
     { "CSMSG_TOPIC_MISMATCH", "Warning: The default topic for $b%s$b does not match the topic mask; changing it anyway." },
@@ -234,9 +244,12 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_MODE_LOCKED", "Modes conflicting with $b%s$b are not allowed in %s." },
     { "CSMSG_CANNOT_SET", "That setting is above your current level, so you cannot change it." },
     { "CSMSG_OWNER_DEFAULTS", "You must have access 500 in %s to reset it to the default options." },
-    { "CSMSG_CONFIRM_DEFAULTS", "To reset %s's settings to the defaults, you muse use 'set defaults %s'." },
+    { "CSMSG_CONFIRM_DEFAULTS", "To reset %s's settings to the defaults, you must use 'set defaults %s'." },
     { "CSMSG_SETTINGS_DEFAULTED", "All settings for %s have been reset to default values." },
     { "CSMSG_BAD_SETLEVEL", "You cannot change any setting to above your level." },
+    { "CSMSG_BAD_GIVEVOICE", "You cannot change GiveVoice to above GiveOps (%d)." },
+    { "CSMSG_BAD_GIVEOPS", "You cannot change GiveOps to below GiveVoice (%d)." },
+    { "CSMSG_BAD_SETTERS", "You cannot change Setters to above your level." },
     { "CSMSG_INVALID_MODE_LOCK", "$b%s$b is an invalid mode lock." },
     { "CSMSG_INVALID_NUMERIC",   "$b%d$b is not a valid choice.  Choose one:" },
     { "CSMSG_SET_DEFAULT_TOPIC", "$bDefaultTopic$b %s" },
@@ -245,11 +258,12 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_SET_USERGREETING",  "$bUserGreeting$b %s" },
     { "CSMSG_SET_MODES",         "$bModes       $b %s" },
     { "CSMSG_SET_NODELETE",      "$bNoDelete    $b %s" },
-    { "CSMSG_SET_USERINFO",      "$bUserInfo    $b %s" },
-    { "CSMSG_SET_VOICE",         "$bVoice       $b %s" },
     { "CSMSG_SET_DYNLIMIT",      "$bDynLimit    $b %s" },
-    { "CSMSG_SET_TOPICSNARF",    "$bTopicSnarf  $b %s" },
-    { "CSMSG_SET_PEONINVITE",    "$bPeonInvite  $b %s" },
+    { "CSMSG_SET_OFFCHANNEL",    "$bOffChannel  $b %s" },
+    { "CSMSG_SET_USERINFO",      "$bUserInfo    $b %d" },
+    { "CSMSG_SET_GIVE_VOICE",    "$bGiveVoice   $b %d" },
+    { "CSMSG_SET_TOPICSNARF",    "$bTopicSnarf  $b %d" },
+    { "CSMSG_SET_INVITEME",      "$bInviteMe    $b %d" },
     { "CSMSG_SET_ENFOPS",        "$bEnfOps      $b %d" },
     { "CSMSG_SET_GIVE_OPS",      "$bGiveOps     $b %d" },
     { "CSMSG_SET_ENFMODES",      "$bEnfModes    $b %d" },
@@ -261,6 +275,7 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_SET_TOYS",          "$bToys        $b %d - %s" },
     { "CSMSG_SET_CTCPREACTION",  "$bCTCPReaction$b %d - %s" },
     { "CSMSG_SET_TOPICREFRESH",  "$bTopicRefresh$b %d - %s" },
+    { "CSMSG_SET_UNREVIEWED",    "$bUnreviewed  $b %s" },
     { "CSMSG_USET_NOAUTOOP",     "$bNoAutoOp    $b %s" },
     { "CSMSG_USET_NOAUTOVOICE",  "$bNoAutoVoice $b %s" },
     { "CSMSG_USET_AUTOINVITE",   "$bAutoInvite  $b %s" },
@@ -291,10 +306,13 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_CTCPREACTION_LONGBAN", "Long timed ban on disallowed CTCPs" },
 
     { "CSMSG_INVITED_USER", "Invited $b%s$b to join %s." },
-    { "CSMSG_INVITING_YOU", "$b%s$b invites you to join %s%s%s" },
-    { "CSMSG_ALREADY_PRESENT", "%s is $balready in %s$b." },
+    { "CSMSG_INVITING_YOU_REASON", "$b%s$b invites you to join %s: %s" },
+    { "CSMSG_INVITING_YOU", "$b%s$b invites you to join %s." },
+    { "CSMSG_ALREADY_PRESENT", "%s is already in $b%s$b." },
     { "CSMSG_YOU_ALREADY_PRESENT", "You are already in $b%s$b." },
     { "CSMSG_LOW_CHANNEL_ACCESS", "You lack sufficient access in %s to use this command." },
+    { "CSMSG_INFOLINE_TOO_LONG", "Your infoline may not exceed %u characters." },
+    { "CSMSG_BAD_INFOLINE", "You may not use the character \\%03o in your infoline." },
 
     { "CSMSG_KICK_DONE", "Kicked $b%s$b from %s." },
     { "CSMSG_NO_BANS", "No channel bans found on $b%s$b." },
@@ -334,8 +352,8 @@ static const struct message_entry msgtab[] = {
 
 /* Access information */
     { "CSMSG_IS_CHANSERV", "$b$C$b is the $bchannel service bot$b." },
-    { "CSMSG_ACCESS_SELF_ONLY", "You may only see the list of infolines for yourself (by using $b%s$b with no arguments)." },
-    { "CSMSG_SQUAT_ACCESS", "You do not have access to any channels." },
+    { "CSMSG_MYACCESS_SELF_ONLY", "You may only see the list of infolines for yourself (by using $b%s$b with no arguments)." },
+    { "CSMSG_SQUAT_ACCESS", "$b%s$b does not have access to any channels." },
     { "CSMSG_INFOLINE_LIST", "Showing all channel entries for account $b%s$b:" },
     { "CSMSG_USER_NO_ACCESS", "%s lacks access to %s." },
     { "CSMSG_USER_HAS_ACCESS", "%s has access $b%d$b in %s." },
@@ -343,6 +361,9 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_HELPER_HAS_ACCESS", "%s has access $b%d$b in %s and has $bsecurity override$b enabled." },
     { "CSMSG_LAZY_SMURF_TARGET", "%s is %s ($bIRCOp$b; not logged in)." },
     { "CSMSG_SMURF_TARGET", "%s is %s ($b%s$b)." },
+    { "CSMSG_OPERATOR_TITLE", "IRC operator" },
+    { "CSMSG_UC_H_TITLE", "network helper" },
+    { "CSMSG_LC_H_TITLE", "support helper" },
     { "CSMSG_LAME_SMURF_TARGET", "%s is an IRC operator." },
 
 /* Seen information */
@@ -390,6 +411,7 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_NETWORK_SERVERS", "$bServers:             $b%i" },
     { "CSMSG_NETWORK_USERS",   "$bTotal Users:         $b%i" },
     { "CSMSG_NETWORK_BANS",    "$bTotal Ban Count:     $b%i" },
+    { "CSMSG_NETWORK_CHANUSERS", "$bTotal User Count:    $b%i" },
     { "CSMSG_NETWORK_OPERS",   "$bIRC Operators:       $b%i" },
     { "CSMSG_NETWORK_CHANNELS","$bRegistered Channels: $b%i" },
     { "CSMSG_SERVICES_UPTIME", "$bServices Uptime:     $b%s" },
@@ -408,6 +430,7 @@ static const struct message_entry msgtab[] = {
 
 /* Channel configuration */
     { "CSMSG_INVALID_OPTION", "$b%s$b is not a valid %s option." },
+    { "CSMSG_INVALID_CFLAG", "$b%s$b is not a recognized channel flag." },
     { "CSMSG_CHANNEL_OPTIONS", "Channel Options:" },
     { "CSMSG_GREETING_TOO_LONG", "Your greeting ($b%d$b characters) must be shorter than $b%d$b characters." },
 
@@ -421,9 +444,9 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_WUT_RESPONSE", "wut" },
     { "CSMSG_BAD_NUMBER", "$b%s$b is an invalid number.  Please use a number greater than 1 with this command." },
     { "CSMSG_BAD_DIE_FORMAT", "I do not understand $b%s$b.  Please use either a single number or standard 4d6+3 format." },
-    { "CSMSG_BAD_DICE_COUNT", "%d is too many dice.  Please use at most %d." },
-    { "CSMSG_DICE_ROLL", "The total is $b%d$b from rolling %dd%d+%d." },
-    { "CSMSG_DIE_ROLL", "A $b%d$b shows on the %d-sided die." },
+    { "CSMSG_BAD_DICE_COUNT", "%lu is too many dice.  Please use at most %lu." },
+    { "CSMSG_DICE_ROLL", "The total is $b%lu$b from rolling %lud%lu+%lu." },
+    { "CSMSG_DIE_ROLL", "A $b%lu$b shows on the %lu-sided die." },
     { "CSMSG_HUGGLES_HIM", "\001ACTION huggles %s\001" },
     { "CSMSG_HUGGLES_YOU", "\001ACTION huggles you\001" },
 
@@ -461,6 +484,7 @@ static int eject_user(struct userNode *user, struct chanNode *channel, unsigned
 
 struct userNode *chanserv;
 dict_t note_types;
+int off_channel;
 static dict_t plain_dnrs, mask_dnrs, handle_dnrs;
 static struct log_type *CS_LOG;
 
@@ -471,6 +495,7 @@ static struct
 
     unsigned long      db_backup_frequency;
     unsigned long      channel_expire_frequency;
+    unsigned long      dnr_expire_frequency;
 
     long               info_delay;
     unsigned int       adjust_delay;
@@ -482,10 +507,12 @@ static struct
 
     unsigned int       greeting_length;
     unsigned int        refresh_period;
+    unsigned int        giveownership_period;
 
     unsigned int        max_owned;
     unsigned int       max_chan_users;
     unsigned int       max_chan_bans;
+    unsigned int        max_userinfo_length;
 
     struct string_list  *set_shows;
     struct string_list  *eightball;
@@ -567,14 +594,20 @@ static const struct {
     char *db_name;
     unsigned short default_value;
     unsigned int old_idx;
+    unsigned int old_flag;
+    unsigned short flag_value;
 } levelOptions[] = {
-    { "CSMSG_SET_GIVE_OPS", "giveops", 200, 2 },
-    { "CSMSG_SET_ENFOPS", "enfops", 300, 1 },
-    { "CSMSG_SET_ENFMODES", "enfmodes", 200, 3 },
-    { "CSMSG_SET_ENFTOPIC", "enftopic", 200, 4 },
-    { "CSMSG_SET_PUBCMD", "pubcmd", 0, 5 },
-    { "CSMSG_SET_SETTERS", "setters", 400, 7 },
-    { "CSMSG_SET_CTCPUSERS", "ctcpusers", 0, 9 }
+    { "CSMSG_SET_GIVE_VOICE", "givevoice", 100, ~0, CHANNEL_VOICE_ALL, 0 },
+    { "CSMSG_SET_GIVE_OPS", "giveops", 200, 2, 0, 0 },
+    { "CSMSG_SET_ENFOPS", "enfops", 300, 1, 0, 0 },
+    { "CSMSG_SET_ENFMODES", "enfmodes", 200, 3, 0, 0 },
+    { "CSMSG_SET_ENFTOPIC", "enftopic", 200, 4, 0, 0 },
+    { "CSMSG_SET_PUBCMD", "pubcmd", 0, 5, 0, 0 },
+    { "CSMSG_SET_SETTERS", "setters", 400, 7, 0, 0 },
+    { "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 }
 };
 
 struct charOptionValues {
@@ -630,7 +663,7 @@ user_level_from_name(const char *name, unsigned short clamp_level)
 {
     unsigned int level = 0, ii;
     if(isdigit(name[0]))
-        level = atoi(name);
+        level = strtoul(name, NULL, 10);
     else for(ii = 0; (ii < ArrayLength(accessLevels)) && !level; ++ii)
         if(!irccasecmp(name, accessLevels[ii].name))
             level = accessLevels[ii].level;
@@ -765,7 +798,7 @@ scan_user_presence(struct userData *uData, struct userNode *user)
 }
 
 static void
-chanserv_ctcp_check(struct userNode *user, struct chanNode *channel, char *text, UNUSED_ARG(struct userNode *bot))
+chanserv_ctcp_check(struct userNode *user, struct chanNode *channel, const char *text, UNUSED_ARG(struct userNode *bot))
 {
     unsigned int eflags, argc;
     char *argv[4];
@@ -782,7 +815,7 @@ chanserv_ctcp_check(struct userNode *user, struct chanNode *channel, char *text,
         return;
     /* We need to enforce against them; do so. */
     eflags = 0;
-    argv[0] = text;
+    argv[0] = (char*)text;
     argv[1] = user->nick;
     argc = 2;
     if(GetUserMode(channel, user))
@@ -991,10 +1024,10 @@ mode_lock_violated(const struct mod_chanmode *orig, const struct mod_chanmode *c
         return 1;
     if(orig->modes_clear & change->modes_set)
         return 1;
-    if((orig->modes_set & MODE_KEY)
+    if((orig->modes_set & MODE_KEY) && (change->modes_set & MODE_KEY)
        && strcmp(orig->new_key, change->new_key))
         return 1;
-    if((orig->modes_set & MODE_LIMIT)
+    if((orig->modes_set & MODE_LIMIT) && (change->modes_set & MODE_LIMIT)
        && (orig->new_limit != change->new_limit))
         return 1;
     return 0;
@@ -1059,6 +1092,7 @@ register_channel(struct chanNode *cNode, char *registrar)
     channel->registered = now;
     channel->visited = now;
     channel->limitAdjusted = now;
+    channel->ownerTransfer = now;
     channel->flags = CHANNEL_DEFAULT_FLAGS;
     for(lvlOpt = 0; lvlOpt < NUM_LEVEL_OPTIONS; ++lvlOpt)
         channel->lvlOpts[lvlOpt] = levelOptions[lvlOpt].default_value;
@@ -1174,13 +1208,13 @@ add_channel_ban(struct chanData *channel, const char *mask, char *owner, time_t
         if(irccasecmp(mask + l1 - l2, old_name))
             continue;
         new_mask = alloca(MAXLEN);
-        sprintf(new_mask, "%.*s%s", l1-l2, mask, hidden_host_suffix);
+        sprintf(new_mask, "%.*s%s", (int)(l1-l2), mask, hidden_host_suffix);
         mask = new_mask;
     }
     safestrncpy(bd->mask, mask, sizeof(bd->mask));
     if(owner)
         safestrncpy(bd->owner, owner, sizeof(bd->owner));
-    bd->reason = reason ? strdup(reason) : NULL;
+    bd->reason = strdup(reason);
 
     if(expires)
        timeq_add(expires, expire_ban, bd);
@@ -1229,15 +1263,14 @@ expire_ban(void *data)
         struct mod_chanmode change;
         unsigned int ii;
         bans = bd->channel->channel->banlist;
-        change.modes_set = change.modes_clear = 0;
-        change.argc = 0;
+        mod_chanmode_init(&change);
         for(ii=0; ii<bans.used; ii++)
         {
             if(!strcmp(bans.list[ii]->ban, bd->mask))
            {
                 change.argc = 1;
                 change.args[0].mode = MODE_REMOVE|MODE_BAN;
-                change.args[0].hostmask = bd->mask;
+                change.args[0].u.hostmask = bd->mask;
                 mod_chanmode_announce(chanserv, bd->channel->channel, &change);
                 break;
             }
@@ -1252,6 +1285,7 @@ static void chanserv_expire_suspension(void *data);
 static void
 unregister_channel(struct chanData *channel, const char *reason)
 {
+    struct mod_chanmode change;
     char msgbuf[MAXLEN];
 
     /* After channel unregistration, the following must be cleaned
@@ -1268,22 +1302,32 @@ unregister_channel(struct chanData *channel, const char *reason)
 
     timeq_del(0, NULL, channel, TIMEQ_IGNORE_FUNC | TIMEQ_IGNORE_WHEN);
 
+    if(off_channel > 0)
+    {
+      mod_chanmode_init(&change);
+      change.modes_clear |= MODE_REGISTERED;
+      mod_chanmode_announce(chanserv, channel->channel, &change);
+    }
+
     while(channel->users)
        del_channel_user(channel->users, 0);
 
     while(channel->bans)
        del_channel_ban(channel->bans);
 
-    if(channel->topic) free(channel->topic);
-    if(channel->registrar) free(channel->registrar);
-    if(channel->greeting) free(channel->greeting);
-    if(channel->user_greeting) free(channel->user_greeting);
-    if(channel->topic_mask) free(channel->topic_mask);
+    free(channel->topic);
+    free(channel->registrar);
+    free(channel->greeting);
+    free(channel->user_greeting);
+    free(channel->topic_mask);
 
-    if(channel->prev) channel->prev->next = channel->next;
-    else channelList = channel->next;
+    if(channel->prev)
+        channel->prev->next = channel->next;
+    else
+        channelList = channel->next;
 
-    if(channel->next) channel->next->prev = channel->prev;
+    if(channel->next)
+        channel->next->prev = channel->prev;
 
     if(channel->suspended)
     {
@@ -1305,8 +1349,7 @@ unregister_channel(struct chanData *channel, const char *reason)
     }
     channel->channel->channel_info = NULL;
 
-    if(channel->notes)
-        dict_delete(channel->notes);
+    dict_delete(channel->notes);
     sprintf(msgbuf, "%s %s", channel->channel->name, reason);
     if(!IsSuspended(channel))
         DelChannelUser(chanserv, channel->channel, msgbuf, 0);
@@ -1323,7 +1366,7 @@ expire_channels(UNUSED_ARG(void *data))
     struct userData *user;
     char delay[INTERVALLEN], reason[INTERVALLEN + 64];
 
-    intervalString(delay, chanserv_conf.channel_expire_delay);
+    intervalString(delay, chanserv_conf.channel_expire_delay, NULL);
     sprintf(reason, "Channel registration automatically expired after %s of disuse.", delay);
 
     for(channel = channelList; channel; channel = next)
@@ -1351,6 +1394,38 @@ expire_channels(UNUSED_ARG(void *data))
        timeq_add(now + chanserv_conf.channel_expire_frequency, expire_channels, NULL);
 }
 
+static void
+expire_dnrs(UNUSED_ARG(void *data))
+{
+    dict_iterator_t it;
+    struct do_not_register *dnr;
+
+    for(it = dict_first(handle_dnrs); it; it = iter_next(it))
+    {
+        dnr = iter_data(it);
+        if(!dnr->expires || dnr->expires > now)
+            continue;
+        dict_remove(handle_dnrs, dnr->chan_name + 1);
+    }
+    for(it = dict_first(plain_dnrs); it; it = iter_next(it))
+    {
+        dnr = iter_data(it);
+        if(!dnr->expires || dnr->expires > now)
+            continue;
+        dict_remove(plain_dnrs, dnr->chan_name);
+    }
+    for(it = dict_first(mask_dnrs); it; it = iter_next(it))
+    {
+        dnr = iter_data(it);
+        if(!dnr->expires || dnr->expires > now)
+            continue;
+        dict_remove(mask_dnrs, dnr->chan_name);
+    }
+
+    if(chanserv_conf.dnr_expire_frequency)
+        timeq_add(now + chanserv_conf.dnr_expire_frequency, expire_dnrs, NULL);
+}
+
 static int
 protect_user(const struct userNode *victim, const struct userNode *aggressor, struct chanData *channel)
 {
@@ -1431,13 +1506,14 @@ validate_deop(struct userNode *user, struct chanNode *channel, struct userNode *
 }
 
 static struct do_not_register *
-chanserv_add_dnr(const char *chan_name, const char *setter, const char *reason)
+chanserv_add_dnr(const char *chan_name, const char *setter, time_t expires, const char *reason)
 {
     struct do_not_register *dnr = calloc(1, sizeof(*dnr)+strlen(reason));
     safestrncpy(dnr->chan_name, chan_name, sizeof(dnr->chan_name));
     safestrncpy(dnr->setter, setter, sizeof(dnr->setter));
     strcpy(dnr->reason, reason);
     dnr->set = now;
+    dnr->expires = expires;
     if(dnr->chan_name[0] == '*')
         dict_insert(handle_dnrs, dnr->chan_name+1, dnr);
     else if(strpbrk(dnr->chan_name, "*?"))
@@ -1448,44 +1524,80 @@ chanserv_add_dnr(const char *chan_name, const char *setter, const char *reason)
 }
 
 static struct dnrList
-chanserv_find_dnrs(const char *chan_name, struct handle_info *handle)
+chanserv_find_dnrs(const char *chan_name, const char *handle, unsigned int max)
 {
     struct dnrList list;
-    dict_iterator_t it;
+    dict_iterator_t it, next;
     struct do_not_register *dnr;
 
     dnrList_init(&list);
-    if(handle && (dnr = dict_find(handle_dnrs, handle->handle, NULL)))
-        dnrList_append(&list, dnr);
+
+    if(handle && (dnr = dict_find(handle_dnrs, handle, NULL)))
+    {
+        if(dnr->expires && dnr->expires <= now)
+            dict_remove(handle_dnrs, handle);
+        else if(list.used < max)
+            dnrList_append(&list, dnr);
+    }
+
     if(chan_name && (dnr = dict_find(plain_dnrs, chan_name, NULL)))
-        dnrList_append(&list, dnr);
+    {
+        if(dnr->expires && dnr->expires <= now)
+            dict_remove(plain_dnrs, chan_name);
+        else if(list.used < max)
+            dnrList_append(&list, dnr);
+    }
+
     if(chan_name)
-        for(it = dict_first(mask_dnrs); it; it = iter_next(it))
-            if(match_ircglob(chan_name, iter_key(it)))
-                dnrList_append(&list, iter_data(it));
+    {
+        for(it = dict_first(mask_dnrs); it && list.used < max; it = next)
+        {
+            next = iter_next(it);
+            if(!match_ircglob(chan_name, iter_key(it)))
+                continue;
+            dnr = iter_data(it);
+            if(dnr->expires && dnr->expires <= now)
+                dict_remove(mask_dnrs, iter_key(it));
+            else
+                dnrList_append(&list, dnr);
+        }
+    }
+
     return list;
 }
 
+static int dnr_print_func(struct do_not_register *dnr, void *extra)
+{
+    struct userNode *user;
+    char buf1[INTERVALLEN];
+    char buf2[INTERVALLEN];
+
+    user = extra;
+    if(dnr->set)
+        strftime(buf1, sizeof(buf1), "%d %b %Y", localtime(&dnr->set));
+    if(dnr->expires)
+    {
+        strftime(buf2, sizeof(buf2), "%d %b %Y", localtime(&dnr->expires));
+        send_message(user, chanserv, "CSMSG_DNR_INFO_SET_EXPIRES", dnr->chan_name, buf1, dnr->setter, buf2, dnr->reason);
+    }
+    else if(dnr->set)
+    {
+        send_message(user, chanserv, "CSMSG_DNR_INFO_SET", dnr->chan_name, buf1, dnr->setter, dnr->reason);
+    }
+    else
+        send_message(user, chanserv, "CSMSG_DNR_INFO", dnr->chan_name, dnr->setter, dnr->reason);
+    return 0;
+}
+
 static unsigned int
-chanserv_show_dnrs(struct userNode *user, struct svccmd *cmd, const char *chan_name, struct handle_info *handle)
+chanserv_show_dnrs(struct userNode *user, struct svccmd *cmd, const char *chan_name, const char *handle)
 {
     struct dnrList list;
-    struct do_not_register *dnr;
     unsigned int ii;
-    char buf[INTERVALLEN];
 
-    list = chanserv_find_dnrs(chan_name, handle);
+    list = chanserv_find_dnrs(chan_name, handle, UINT_MAX);
     for(ii = 0; (ii < list.used) && (ii < 10); ++ii)
-    {
-        dnr = list.list[ii];
-        if(dnr->set)
-        {
-            strftime(buf, sizeof(buf), "%Y %b %d", localtime(&dnr->set));
-            reply("CSMSG_DNR_INFO_SET", dnr->chan_name, buf, dnr->setter, dnr->reason);
-        }
-        else
-            reply("CSMSG_DNR_INFO", dnr->chan_name, dnr->setter, dnr->reason);
-    }
+        dnr_print_func(list.list[ii], user);
     if(ii < list.used)
         reply("CSMSG_MORE_DNRS", list.used - ii);
     free(list.list);
@@ -1495,63 +1607,49 @@ chanserv_show_dnrs(struct userNode *user, struct svccmd *cmd, const char *chan_n
 struct do_not_register *
 chanserv_is_dnr(const char *chan_name, struct handle_info *handle)
 {
+    struct dnrList list;
     struct do_not_register *dnr;
-    dict_iterator_t it;
 
-    if(handle && (dnr = dict_find(handle_dnrs, handle->handle, NULL)))
-        return dnr;
-    if(chan_name)
+    list = chanserv_find_dnrs(chan_name, handle->handle, 1);
+    dnr = list.used ? list.list[0] : NULL;
+    free(list.list);
+    return dnr;
+}
+
+static unsigned int send_dnrs(struct userNode *user, dict_t dict)
+{
+    struct do_not_register *dnr;
+    dict_iterator_t it, next;
+    unsigned int matches = 0;
+
+    for(it = dict_first(dict); it; it = next)
     {
-        if((dnr = dict_find(plain_dnrs, chan_name, NULL)))
-            return dnr;
-        for(it = dict_first(mask_dnrs); it; it = iter_next(it))
-            if(match_ircglob(chan_name, iter_key(it)))
-                return iter_data(it);
+        dnr = iter_data(it);
+        next = iter_next(it);
+        if(dnr->expires && dnr->expires <= now)
+        {
+            dict_remove(dict, iter_key(it));
+            continue;
+        }
+        dnr_print_func(dnr, user);
+        matches++;
     }
-    return NULL;
+
+    return matches;
 }
 
 static CHANSERV_FUNC(cmd_noregister)
 {
     const char *target;
-    struct do_not_register *dnr;
-    char buf[INTERVALLEN];
+    time_t expiry, duration;
     unsigned int matches;
 
     if(argc < 2)
     {
-        dict_iterator_t it;
-
         reply("CSMSG_DNR_SEARCH_RESULTS");
-        matches = 0;
-        for(it = dict_first(handle_dnrs); it; it = iter_next(it))
-        {
-            dnr = iter_data(it);
-            if(dnr->set)
-                reply("CSMSG_DNR_INFO_SET", dnr->chan_name, intervalString(buf, now - dnr->set), dnr->setter, dnr->reason);
-            else
-                reply("CSMSG_DNR_INFO", dnr->chan_name, dnr->setter, dnr->reason);
-            matches++;
-        }
-        for(it = dict_first(plain_dnrs); it; it = iter_next(it))
-        {
-            dnr = iter_data(it);
-            if(dnr->set)
-                reply("CSMSG_DNR_INFO_SET", dnr->chan_name, intervalString(buf, now - dnr->set), dnr->setter, dnr->reason);
-            else
-                reply("CSMSG_DNR_INFO", dnr->chan_name, dnr->setter, dnr->reason);
-            matches++;
-        }
-        for(it = dict_first(mask_dnrs); it; it = iter_next(it))
-        {
-            dnr = iter_data(it);
-            if(dnr->set)
-                reply("CSMSG_DNR_INFO_SET", dnr->chan_name, intervalString(buf, now - dnr->set), dnr->setter, dnr->reason);
-            else
-                reply("CSMSG_DNR_INFO", dnr->chan_name, dnr->setter, dnr->reason);
-            matches++;
-        }
-
+        matches = send_dnrs(user, handle_dnrs);
+        matches += send_dnrs(user, plain_dnrs);
+        matches += send_dnrs(user, mask_dnrs);
         if(matches)
             reply("MSG_MATCH_COUNT", matches);
         else
@@ -1569,20 +1667,36 @@ static CHANSERV_FUNC(cmd_noregister)
 
     if(argc > 2)
     {
-        const char *reason = unsplit_string(argv + 2, argc - 2, NULL);
+        if(argc == 3)
+        {
+            reply("MSG_INVALID_DURATION", argv[2]);
+            return 0;
+        }
+
+        if(!strcmp(argv[2], "0"))
+            expiry = 0;
+        else if((duration = ParseInterval(argv[2])))
+            expiry = now + duration;
+        else
+        {
+            reply("MSG_INVALID_DURATION", argv[2]);
+            return 0;
+        }
+
+        const char *reason = unsplit_string(argv + 3, argc - 3, NULL);
         if((*target == '*') && !get_handle_info(target + 1))
         {
             reply("MSG_HANDLE_UNKNOWN", target + 1);
             return 0;
         }
-        chanserv_add_dnr(target, user->handle_info->handle, reason);
+        chanserv_add_dnr(target, user->handle_info->handle, expiry, reason);
         reply("CSMSG_NOREGISTER_CHANNEL", target);
         return 1;
     }
 
     reply("CSMSG_DNR_SEARCH_RESULTS");
     if(*target == '*')
-        matches = chanserv_show_dnrs(user, cmd, NULL, get_handle_info(target + 1));
+        matches = chanserv_show_dnrs(user, cmd, NULL, target + 1);
     else
         matches = chanserv_show_dnrs(user, cmd, target, NULL);
     if(!matches)
@@ -1594,26 +1708,255 @@ static CHANSERV_FUNC(cmd_allowregister)
 {
     const char *chan_name = argv[1];
 
-    if((chan_name[0] == '*') && dict_find(handle_dnrs, chan_name+1, NULL))
+    if(((chan_name[0] == '*') && dict_remove(handle_dnrs, chan_name+1))
+       || dict_remove(plain_dnrs, chan_name)
+       || dict_remove(mask_dnrs, chan_name))
     {
-        dict_remove(handle_dnrs, chan_name+1);
         reply("CSMSG_DNR_REMOVED", chan_name);
+        return 1;
     }
-    else if(dict_find(plain_dnrs, chan_name, NULL))
+    reply("CSMSG_NO_SUCH_DNR", chan_name);
+    return 0;
+}
+
+struct dnr_search {
+    struct userNode *source;
+    char *chan_mask;
+    char *setter_mask;
+    char *reason_mask;
+    time_t min_set, max_set;
+    time_t min_expires, max_expires;
+    unsigned int limit;
+};
+
+static int
+dnr_search_matches(const struct do_not_register *dnr, const struct dnr_search *search)
+{
+    return !((dnr->set < search->min_set)
+             || (dnr->set > search->max_set)
+             || (dnr->expires && ((dnr->expires < search->min_expires)
+                                  || (dnr->expires > search->max_expires)))
+             || (search->chan_mask
+                 && !match_ircglob(search->chan_mask, dnr->chan_name))
+             || (search->setter_mask
+                 && !match_ircglob(search->setter_mask, dnr->setter))
+             || (search->reason_mask
+                 && !match_ircglob(search->reason_mask, dnr->reason)));
+}
+
+static struct dnr_search *
+dnr_search_create(struct userNode *user, struct svccmd *cmd, unsigned int argc, char *argv[])
+{
+    struct dnr_search *discrim;
+    unsigned int ii;
+
+    discrim = calloc(1, sizeof(*discrim));
+    discrim->source = user;
+    discrim->chan_mask = NULL;
+    discrim->setter_mask = NULL;
+    discrim->reason_mask = NULL;
+    discrim->max_set = INT_MAX;
+    discrim->max_expires = INT_MAX;
+    discrim->limit = 50;
+
+    for(ii=0; ii<argc; ++ii)
     {
-        dict_remove(plain_dnrs, chan_name);
-        reply("CSMSG_DNR_REMOVED", chan_name);
+        if(ii == argc - 1)
+        {
+            reply("MSG_MISSING_PARAMS", argv[ii]);
+            goto fail;
+        }
+        else if(0 == irccasecmp(argv[ii], "channel"))
+        {
+            discrim->chan_mask = argv[++ii];
+        }
+        else if(0 == irccasecmp(argv[ii], "setter"))
+        {
+            discrim->setter_mask = argv[++ii];
+        }
+        else if(0 == irccasecmp(argv[ii], "reason"))
+        {
+            discrim->reason_mask = argv[++ii];
+        }
+        else if(0 == irccasecmp(argv[ii], "limit"))
+        {
+            discrim->limit = strtoul(argv[++ii], NULL, 0);
+        }
+        else if(0 == irccasecmp(argv[ii], "set"))
+        {
+            const char *cmp = argv[++ii];
+            if(cmp[0] == '<') {
+                if(cmp[1] == '=')
+                    discrim->min_set = now - ParseInterval(cmp + 2);
+                else
+                    discrim->min_set = now - (ParseInterval(cmp + 1) - 1);
+            } else if(cmp[0] == '=') {
+                discrim->min_set = discrim->max_set = now - ParseInterval(cmp + 1);
+            } else if(cmp[0] == '>') {
+                if(cmp[1] == '=')
+                    discrim->max_set = now - ParseInterval(cmp + 2);
+                else
+                    discrim->max_set = now - (ParseInterval(cmp + 1) - 1);
+            } else {
+                discrim->max_set = now - (ParseInterval(cmp) - 1);
+            }
+        }
+        else if(0 == irccasecmp(argv[ii], "expires"))
+        {
+            const char *cmp = argv[++ii];
+            if(cmp[0] == '<') {
+                if(cmp[1] == '=')
+                    discrim->max_expires = now + ParseInterval(cmp + 2);
+                else
+                    discrim->max_expires = now + (ParseInterval(cmp + 1) - 1);
+            } else if(cmp[0] == '=') {
+                discrim->min_expires = discrim->max_expires = now + ParseInterval(cmp + 1);
+            } else if(cmp[0] == '>') {
+                if(cmp[1] == '=')
+                    discrim->min_expires = now + ParseInterval(cmp + 2);
+                else
+                    discrim->min_expires = now + (ParseInterval(cmp + 1) - 1);
+            } else {
+                discrim->min_expires = now + (ParseInterval(cmp) - 1);
+            }
+        }
+        else
+        {
+            reply("MSG_INVALID_CRITERIA", argv[ii]);
+            goto fail;
+        }
     }
-    else if(dict_find(mask_dnrs, chan_name, NULL))
+    return discrim;
+
+  fail:
+    free(discrim);
+    return NULL;
+}
+
+typedef int (*dnr_search_func)(struct do_not_register *match, void *extra);
+
+static unsigned int
+dnr_search(struct dnr_search *discrim, dnr_search_func dsf, void *data)
+{
+    struct do_not_register *dnr;
+    dict_iterator_t next;
+    dict_iterator_t it;
+    unsigned int count;
+    int target_fixed;
+
+    /* Initialize local variables. */
+    count = 0;
+    target_fixed = 0;
+    if(discrim->chan_mask)
     {
-        dict_remove(mask_dnrs, chan_name);
-        reply("CSMSG_DNR_REMOVED", chan_name);
+        int shift = (discrim->chan_mask[0] == '\\' && discrim->chan_mask[1] == '*') ? 2 : 0;
+        if('\0' == discrim->chan_mask[shift + strcspn(discrim->chan_mask+shift, "*?")])
+            target_fixed = 1;
+    }
+
+    if(target_fixed && discrim->chan_mask[0] == '\\' && discrim->chan_mask[1] == '*')
+    {
+        /* Check against account-based DNRs. */
+        dnr = dict_find(handle_dnrs, discrim->chan_mask + 2, NULL);
+        if(dnr && dnr_search_matches(dnr, discrim) && (count++ < discrim->limit))
+            dsf(dnr, data);
+    }
+    else if(target_fixed)
+    {
+        /* Check against channel-based DNRs. */
+        dnr = dict_find(plain_dnrs, discrim->chan_mask, NULL);
+        if(dnr && dnr_search_matches(dnr, discrim) && (count++ < discrim->limit))
+            dsf(dnr, data);
     }
     else
     {
-        reply("CSMSG_NO_SUCH_DNR", chan_name);
+        /* Exhaustively search account DNRs. */
+        for(it = dict_first(handle_dnrs); it; it = next)
+        {
+            next = iter_next(it);
+            dnr = iter_data(it);
+            if(dnr_search_matches(dnr, discrim) && (count++ < discrim->limit) && dsf(dnr, data))
+                break;
+        }
+
+        /* Do the same for channel DNRs. */
+        for(it = dict_first(plain_dnrs); it; it = next)
+        {
+            next = iter_next(it);
+            dnr = iter_data(it);
+            if(dnr_search_matches(dnr, discrim) && (count++ < discrim->limit) && dsf(dnr, data))
+                break;
+        }
+
+        /* Do the same for wildcarded channel DNRs. */
+        for(it = dict_first(mask_dnrs); it; it = next)
+        {
+            next = iter_next(it);
+            dnr = iter_data(it);
+            if(dnr_search_matches(dnr, discrim) && (count++ < discrim->limit) && dsf(dnr, data))
+                break;
+        }
+    }
+    return count;
+}
+
+static int
+dnr_remove_func(struct do_not_register *match, void *extra)
+{
+    struct userNode *user;
+    char *chan_name;
+
+    chan_name = alloca(strlen(match->chan_name) + 1);
+    strcpy(chan_name, match->chan_name);
+    user = extra;
+    if(((chan_name[0] == '*') && dict_remove(handle_dnrs, chan_name+1))
+       || dict_remove(plain_dnrs, chan_name)
+       || dict_remove(mask_dnrs, chan_name))
+    {
+        send_message(user, chanserv, "CSMSG_DNR_REMOVED", chan_name);
+    }
+    return 0;
+}
+
+static MODCMD_FUNC(cmd_dnrsearch)
+{
+    struct dnr_search *discrim;
+    dnr_search_func action;
+    struct svccmd *subcmd;
+    unsigned int matches;
+    char buf[MAXLEN];
+
+    sprintf(buf, "dnrsearch %s", argv[1]);
+    subcmd = dict_find(cmd->parent->commands, buf, NULL);
+    if(!subcmd)
+    {
+        reply("CSMSG_DNR_BAD_ACTION", argv[1]);
         return 0;
     }
+    if(!svccmd_can_invoke(user, cmd->parent->bot, subcmd, channel, SVCCMD_NOISY))
+        return 0;
+    if(!irccasecmp(argv[1], "print"))
+        action = dnr_print_func;
+    else if(!irccasecmp(argv[1], "remove"))
+        action = dnr_remove_func;
+    else
+    {
+        reply("CSMSG_DNR_BAD_ACTION", argv[1]);
+        return 0;
+    }
+
+    discrim = dnr_search_create(user, cmd, argc-2, argv+2);
+    if(!discrim)
+        return 0;
+
+    if(action == dnr_print_func)
+        reply("CSMSG_DNR_SEARCH_RESULTS");
+    matches = dnr_search(discrim, action, user);
+    if(matches)
+        reply("MSG_MATCH_COUNT", matches);
+    else
+        reply("MSG_NO_MATCHES");
+    free(discrim);
     return 1;
 }
 
@@ -1631,7 +1974,6 @@ chanserv_get_owned_count(struct handle_info *hi)
 
 static CHANSERV_FUNC(cmd_register)
 {
-    struct mod_chanmode *change;
     struct handle_info *handle;
     struct chanData *cData;
     struct modeNode *mn;
@@ -1654,7 +1996,8 @@ static CHANSERV_FUNC(cmd_register)
             return 0;
         }
 
-        if(!IsHelping(user) && (!(mn = GetUserMode(channel, user)) || !(mn->modes & MODE_CHANOP)))
+        if(!IsHelping(user)
+           && (!(mn = GetUserMode(channel, user)) || !(mn->modes & MODE_CHANOP)))
         {
             reply("CSMSG_MUST_BE_OPPED", channel->name);
             return 0;
@@ -1704,7 +2047,7 @@ static CHANSERV_FUNC(cmd_register)
         if(!IsHelping(user))
             reply("CSMSG_DNR_CHANNEL", chan_name);
         else
-            chanserv_show_dnrs(user, cmd, chan_name, handle);
+            chanserv_show_dnrs(user, cmd, chan_name, handle->handle);
         return 0;
     }
 
@@ -1720,12 +2063,21 @@ static CHANSERV_FUNC(cmd_register)
     cData = register_channel(channel, user->handle_info->handle);
     scan_user_presence(add_channel_user(cData, handle, UL_OWNER, 0, NULL), NULL);
     cData->modes = chanserv_conf.default_modes;
-    change = mod_chanmode_dup(&cData->modes, 1);
-    change->args[change->argc].mode = MODE_CHANOP;
-    change->args[change->argc].member = AddChannelUser(chanserv, channel);
-    change->argc++;
-    mod_chanmode_announce(chanserv, channel, change);
-    mod_chanmode_free(change);
+    if(off_channel > 0)
+        cData->modes.modes_set |= MODE_REGISTERED;
+    if (IsOffChannel(cData))
+    {
+        mod_chanmode_announce(chanserv, channel, &cData->modes);
+    }
+    else
+    {
+        struct mod_chanmode *change = mod_chanmode_dup(&cData->modes, 1);
+        change->args[change->argc].mode = MODE_CHANOP;
+        change->args[change->argc].u.member = AddChannelUser(chanserv, channel);
+        change->argc++;
+        mod_chanmode_announce(chanserv, channel, change);
+        mod_chanmode_free(change);
+    }
 
     /* Initialize the channel's max user record. */
     cData->max = channel->members.used;
@@ -1810,6 +2162,7 @@ static CHANSERV_FUNC(cmd_unregister)
 
 static CHANSERV_FUNC(cmd_move)
 {
+    struct mod_chanmode change;
     struct chanNode *target;
     struct modeNode *mn;
     struct userData *uData;
@@ -1845,16 +2198,16 @@ static CHANSERV_FUNC(cmd_move)
                 if(!IsHelping(user))
                     reply("CSMSG_DNR_CHANNEL_MOVE", argv[1]);
                 else
-                    chanserv_show_dnrs(user, cmd, argv[1], uData->handle);
+                    chanserv_show_dnrs(user, cmd, argv[1], uData->handle->handle);
                 return 0;
             }
         }
     }
 
+    mod_chanmode_init(&change);
     if(!(target = GetChannel(argv[1])))
     {
         target = AddChannel(argv[1], now, NULL, NULL);
-        LockChannel(target);
         if(!IsSuspended(channel->channel_info))
             AddChannelUser(chanserv, target);
     }
@@ -1871,11 +2224,20 @@ static CHANSERV_FUNC(cmd_move)
     }
     else if(!IsSuspended(channel->channel_info))
     {
-        struct mod_chanmode change;
-        change.modes_set = change.modes_clear = 0;
         change.argc = 1;
         change.args[0].mode = MODE_CHANOP;
-        change.args[0].member = AddChannelUser(chanserv, target);
+        change.args[0].u.member = AddChannelUser(chanserv, target);
+        mod_chanmode_announce(chanserv, target, &change);
+    }
+
+    if(off_channel > 0)
+    {
+        /* Clear MODE_REGISTERED from old channel, add it to new. */
+        change.argc = 0;
+        change.modes_clear = MODE_REGISTERED;
+        mod_chanmode_announce(chanserv, channel, &change);
+        change.modes_clear = 0;
+        change.modes_set = MODE_REGISTERED;
         mod_chanmode_announce(chanserv, target, &change);
     }
 
@@ -1896,6 +2258,7 @@ static CHANSERV_FUNC(cmd_move)
        DelChannelUser(chanserv, channel, reason2, 0);
     }
     UnlockChannel(channel);
+    LockChannel(target);
     global_message(MESSAGE_RECIPIENT_OPERS | MESSAGE_RECIPIENT_HELPERS, reason);
     return 1;
 }
@@ -2057,8 +2420,20 @@ merge_bans(struct chanData *source, struct chanData *target)
 static void
 merge_data(struct chanData *source, struct chanData *target)
 {
+    /* Use more recent visited and owner-transfer time; use older
+     * registered time.  Bitwise or may_opchan.  Use higher max.
+     * Do not touch last_refresh, ban count or user counts.
+     */
     if(source->visited > target->visited)
        target->visited = source->visited;
+    if(source->registered < target->registered)
+        target->registered = source->registered;
+    if(source->ownerTransfer > target->ownerTransfer)
+        target->ownerTransfer = source->ownerTransfer;
+    if(source->may_opchan)
+        target->may_opchan = 1;
+    if(source->max > target->max)
+        target->max = source->max;
 }
 
 static void
@@ -2133,10 +2508,10 @@ static CHANSERV_FUNC(cmd_opchan)
         return 0;
     }
     channel->channel_info->may_opchan = 0;
-    change.modes_set = change.modes_clear = 0;
+    mod_chanmode_init(&change);
     change.argc = 1;
     change.args[0].mode = MODE_CHANOP;
-    change.args[0].member = GetUserMode(channel, chanserv);
+    change.args[0].u.member = GetUserMode(channel, chanserv);
     mod_chanmode_announce(chanserv, channel, &change);
     reply("CSMSG_OPCHAN_DONE", channel->name);
     return 1;
@@ -2145,9 +2520,9 @@ static CHANSERV_FUNC(cmd_opchan)
 static CHANSERV_FUNC(cmd_adduser)
 {
     struct userData *actee;
-    struct userData *actor;
+    struct userData *actor, *real_actor;
     struct handle_info *handle;
-    unsigned short access;
+    unsigned short access, override = 0;
 
     REQUIRE_PARAMS(3);
 
@@ -2165,12 +2540,18 @@ static CHANSERV_FUNC(cmd_adduser)
     }
 
     actor = GetChannelUser(channel->channel_info, user->handle_info);
+    real_actor = GetChannelAccess(channel->channel_info, user->handle_info);
+
     if(actor->access <= access)
     {
        reply("CSMSG_NO_BUMP_ACCESS");
        return 0;
     }
 
+    /* Trying to add someone with equal/more access? */
+    if (!real_actor || real_actor->access <= access)
+        override = CMD_LOG_OVERRIDE;
+
     if(!(handle = modcmd_get_handle_info(user, argv[1])))
         return 0;
 
@@ -2183,20 +2564,21 @@ static CHANSERV_FUNC(cmd_adduser)
     actee = add_channel_user(channel->channel_info, handle, access, 0, NULL);
     scan_user_presence(actee, NULL);
     reply("CSMSG_ADDED_USER", handle->handle, channel->name, access);
-    return 1;
+    return 1 | override;
 }
 
 static CHANSERV_FUNC(cmd_clvl)
 {
     struct handle_info *handle;
     struct userData *victim;
-    struct userData *actor;
-    unsigned short new_access;
+    struct userData *actor, *real_actor;
+    unsigned short new_access, override = 0;
     int privileged = IsHelping(user) && ((user->handle_info->opserv_level >= chanserv_conf.nodelete_level) || !IsProtected(channel->channel_info));
 
     REQUIRE_PARAMS(3);
 
     actor = GetChannelUser(channel->channel_info, user->handle_info);
+    real_actor = GetChannelAccess(channel->channel_info, user->handle_info);
 
     if(!(handle = modcmd_get_handle_info(user, argv[1])))
         return 0;
@@ -2233,22 +2615,33 @@ static CHANSERV_FUNC(cmd_clvl)
        return 0;
     }
 
+    /* Trying to clvl a equal/higher user? */
+    if(!real_actor || (real_actor->access <= victim->access && handle != user->handle_info))
+        override = CMD_LOG_OVERRIDE;
+    /* Trying to clvl someone to equal/higher access? */
+    if(!real_actor || new_access >= real_actor->access)
+        override = CMD_LOG_OVERRIDE;
+    /* Helpers clvling themselves get caught by the "clvl someone to equal/higher access" check.
+     * If they lower their own access it's not a big problem.
+     */
+
     victim->access = new_access;
     reply("CSMSG_CHANGED_ACCESS", handle->handle, new_access, channel->name);
-    return 1;
+    return 1 | override;
 }
 
 static CHANSERV_FUNC(cmd_deluser)
 {
     struct handle_info *handle;
     struct userData *victim;
-    struct userData *actor;
-    unsigned short access;
+    struct userData *actor, *real_actor;
+    unsigned short access, override = 0;
     char *chan_name;
 
     REQUIRE_PARAMS(2);
 
     actor = GetChannelUser(channel->channel_info, user->handle_info);
+    real_actor = GetChannelAccess(channel->channel_info, user->handle_info);
 
     if(!(handle = modcmd_get_handle_info(user, argv[argc-1])))
         return 0;
@@ -2284,19 +2677,27 @@ static CHANSERV_FUNC(cmd_deluser)
        return 0;
     }
 
+    /* If people delete themselves it is an override, but they
+     * could've used deleteme so we don't log it as an override
+     */
+    if(!real_actor || (real_actor->access <= victim->access && real_actor != victim))
+        override = CMD_LOG_OVERRIDE;
+
     chan_name = strdup(channel->name);
     del_channel_user(victim, 1);
     reply("CSMSG_DELETED_USER", handle->handle, access, chan_name);
     free(chan_name);
-    return 1;
+    return 1 | override;
 }
 
 static int
 cmd_mdel_user(struct userNode *user, struct chanNode *channel, unsigned short min_access, unsigned short max_access, char *mask, struct svccmd *cmd)
 {
-    struct userData *actor, *uData, *next;
+    struct userData *actor, *real_actor, *uData, *next;
+    unsigned int override = 0;
 
     actor = GetChannelUser(channel->channel_info, user->handle_info);
+    real_actor = GetChannelAccess(channel->channel_info, user->handle_info);
 
     if(min_access > max_access)
     {
@@ -2310,6 +2711,9 @@ cmd_mdel_user(struct userNode *user, struct chanNode *channel, unsigned short mi
        return 0;
     }
 
+    if(!real_actor || real_actor->access <= max_access)
+        override = CMD_LOG_OVERRIDE;
+
     for(uData = channel->channel_info->users; uData; uData = next)
     {
        next = uData->next;
@@ -2321,7 +2725,7 @@ cmd_mdel_user(struct userNode *user, struct chanNode *channel, unsigned short mi
     }
 
     reply("CSMSG_DELETED_USERS", mask, min_access, max_access, channel->name);
-    return 1;
+    return 1 | override;
 }
 
 static CHANSERV_FUNC(cmd_mdelowner)
@@ -2370,27 +2774,27 @@ cmd_trim_bans(struct userNode *user, struct chanNode *channel, unsigned long dur
         count++;
     }
 
-    intervalString(interval, duration);
+    intervalString(interval, duration, user->handle_info);
     send_message(user, chanserv, "CSMSG_TRIMMED_BANS", count, channel->name, interval);
     return 1;
 }
 
 static int
-cmd_trim_users(struct userNode *user, struct chanNode *channel, unsigned short min_access, unsigned short max_access, unsigned long duration)
+cmd_trim_users(struct userNode *user, struct chanNode *channel, unsigned short min_access, unsigned short max_access, unsigned long duration, int vacation)
 {
     struct userData *actor, *uData, *next;
     char interval[INTERVALLEN];
     unsigned int count;
     time_t limit;
 
-    actor = GetChannelUser(channel->channel_info, user->handle_info);
+    actor = GetChannelAccess(channel->channel_info, user->handle_info);
     if(min_access > max_access)
     {
         send_message(user, chanserv, "CSMSG_BAD_RANGE", min_access, max_access);
         return 0;
     }
 
-    if((actor->access <= max_access) && !IsHelping(user))
+    if(!actor || actor->access <= max_access)
     {
        send_message(user, chanserv, "CSMSG_NO_ACCESS");
        return 0;
@@ -2402,18 +2806,25 @@ cmd_trim_users(struct userNode *user, struct chanNode *channel, unsigned short m
     {
        next = uData->next;
 
-       if((uData->seen > limit) || uData->present)
+       if((uData->seen > limit)
+           || uData->present
+           || (HANDLE_FLAGGED(uData->handle, FROZEN) && !vacation))
            continue;
 
        if(((uData->access >= min_access) && (uData->access <= max_access))
-           || (max_access && (uData->access < actor->access)))
+           || (!max_access && (uData->access < actor->access)))
        {
            del_channel_user(uData, 1);
            count++;
        }
     }
 
-    send_message(user, chanserv, "CSMSG_TRIMMED_USERS", count, min_access, max_access, channel->name, intervalString(interval, duration));
+    if(!max_access)
+    {
+        min_access = 1;
+        max_access = (actor->access > UL_OWNER) ? UL_OWNER : (actor->access - 1);
+    }
+    send_message(user, chanserv, "CSMSG_TRIMMED_USERS", count, min_access, max_access, channel->name, intervalString(interval, duration, user->handle_info));
     return 1;
 }
 
@@ -2421,9 +2832,11 @@ static CHANSERV_FUNC(cmd_trim)
 {
     unsigned long duration;
     unsigned short min_level, max_level;
+    int vacation;
 
     REQUIRE_PARAMS(3);
 
+    vacation = argc > 3 && !strcmp(argv[3], "vacation");
     duration = ParseInterval(argv[2]);
     if(duration < 60)
     {
@@ -2438,17 +2851,17 @@ static CHANSERV_FUNC(cmd_trim)
     }
     else if(!irccasecmp(argv[1], "users"))
     {
-       cmd_trim_users(user, channel, 0, 0, duration);
+       cmd_trim_users(user, channel, 0, 0, duration, vacation);
        return 1;
     }
     else if(parse_level_range(&min_level, &max_level, argv[1]))
     {
-       cmd_trim_users(user, channel, min_level, max_level, duration);
+       cmd_trim_users(user, channel, min_level, max_level, duration, vacation);
        return 1;
     }
     else if((min_level = user_level_from_name(argv[1], UL_OWNER)))
     {
-       cmd_trim_users(user, channel, min_level, min_level, duration);
+       cmd_trim_users(user, channel, min_level, min_level, duration, vacation);
        return 1;
     }
     else
@@ -2466,10 +2879,10 @@ static CHANSERV_FUNC(cmd_up)
     struct userData *uData;
     const char *errmsg;
 
-    change.modes_set = change.modes_clear = 0;
+    mod_chanmode_init(&change);
     change.argc = 1;
-    change.args[0].member = GetUserMode(channel, user);
-    if(!change.args[0].member)
+    change.args[0].u.member = GetUserMode(channel, user);
+    if(!change.args[0].u.member)
     {
         if(argc)
             reply("MSG_CHANNEL_ABSENT", channel->name);
@@ -2488,12 +2901,18 @@ static CHANSERV_FUNC(cmd_up)
         change.args[0].mode = MODE_CHANOP;
         errmsg = "CSMSG_ALREADY_OPPED";
     }
-    else
+    else if(uData->access >= channel->channel_info->lvlOpts[lvlGiveVoice])
     {
         change.args[0].mode = MODE_VOICE;
         errmsg = "CSMSG_ALREADY_VOICED";
     }
-    change.args[0].mode &= ~change.args[0].member->modes;
+    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)
@@ -2508,24 +2927,24 @@ static CHANSERV_FUNC(cmd_down)
 {
     struct mod_chanmode change;
 
-    change.modes_set = change.modes_clear = 0;
+    mod_chanmode_init(&change);
     change.argc = 1;
-    change.args[0].member = GetUserMode(channel, user);
-    if(!change.args[0].member)
+    change.args[0].u.member = GetUserMode(channel, user);
+    if(!change.args[0].u.member)
     {
         if(argc)
             reply("MSG_CHANNEL_ABSENT", channel->name);
        return 0;
     }
 
-    if(!change.args[0].member->modes)
+    if(!change.args[0].u.member->modes)
     {
         if(argc)
             reply("CSMSG_ALREADY_DOWN", channel->name);
        return 0;
     }
 
-    change.args[0].mode = MODE_REMOVE | change.args[0].member->modes;
+    change.args[0].mode = MODE_REMOVE | change.args[0].u.member->modes;
     modcmd_chanmode_announce(&change);
     return 1;
 }
@@ -2574,8 +2993,8 @@ modify_users(struct userNode *user, struct chanNode *channel, unsigned int argc,
        if(!(victim = GetUserH(argv[ii])))
             continue;
         change->args[valid].mode = mode;
-        change->args[valid].member = GetUserMode(channel, victim);
-        if(!change->args[valid].member)
+        change->args[valid].u.member = GetUserMode(channel, victim);
+        if(!change->args[valid].u.member)
             continue;
         if(validate && !validate(user, channel, victim))
            continue;
@@ -2615,7 +3034,7 @@ static CHANSERV_FUNC(cmd_devoice)
 }
 
 static int
-bad_channel_ban(struct chanNode *channel, struct userNode *user, const char *ban, int *victimCount, struct modeNode **victims)
+bad_channel_ban(struct chanNode *channel, struct userNode *user, const char *ban, unsigned int *victimCount, struct modeNode **victims)
 {
     unsigned int ii;
 
@@ -2628,7 +3047,7 @@ bad_channel_ban(struct chanNode *channel, struct userNode *user, const char *ban
         if(IsService(mn->user))
             continue;
 
-        if(!user_matches_glob(mn->user, ban, 1))
+        if(!user_matches_glob(mn->user, ban, MATCH_USENICK | MATCH_VISIBLE))
             continue;
 
         if(protect_user(mn->user, user, channel->channel_info))
@@ -2773,7 +3192,8 @@ eject_user(struct userNode *user, struct chanNode *channel, unsigned int argc, c
                         free(bData->reason);
                    bData->reason = strdup(reason);
                     safestrncpy(bData->owner, (user->handle_info ? user->handle_info->handle : user->nick), sizeof(bData->owner));
-                   reply("CSMSG_REASON_CHANGE", ban);
+                    if(cmd)
+                        reply("CSMSG_REASON_CHANGE", ban);
                    if(!bData->expires)
                         goto post_add_ban;
                }
@@ -2804,15 +3224,20 @@ eject_user(struct userNode *user, struct chanNode *channel, unsigned int argc, c
                        if(bData->expires)
                            timeq_add(bData->expires, expire_ban, bData);
 
-                       if(duration)
-                           reply("CSMSG_BAN_EXTENDED", ban, intervalString(interval, duration));
+                        if(!cmd)
+                        {
+                            /* automated kickban */
+                        }
+                       else if(duration)
+                           reply("CSMSG_BAN_EXTENDED", ban, intervalString(interval, duration, user->handle_info));
                        else
                            reply("CSMSG_BAN_ADDED", name, channel->name);
 
                        goto post_add_ban;
                    }
                }
-               reply("CSMSG_REDUNDANT_BAN", name, channel->name);
+                if(cmd)
+                    reply("CSMSG_REDUNDANT_BAN", name, channel->name);
 
                free(ban);
                return 0;
@@ -2847,7 +3272,7 @@ eject_user(struct userNode *user, struct chanNode *channel, unsigned int argc, c
             if(irccasecmp(ban + l1 - l2, old_name))
                 continue;
             new_mask = malloc(MAXLEN);
-            sprintf(new_mask, "%.*s%s", l1-l2, ban, hidden_host_suffix);
+            sprintf(new_mask, "%.*s%s", (int)(l1-l2), ban, hidden_host_suffix);
             free(ban);
             name = ban = new_mask;
         }
@@ -2861,7 +3286,8 @@ eject_user(struct userNode *user, struct chanNode *channel, unsigned int argc, c
 
        if(channel->banlist.used >= MAXBANS)
        {
-           reply("CSMSG_BANLIST_FULL", channel->name);
+            if(cmd)
+                reply("CSMSG_BANLIST_FULL", channel->name);
            free(ban);
            return 0;
        }
@@ -2871,15 +3297,15 @@ eject_user(struct userNode *user, struct chanNode *channel, unsigned int argc, c
         for(n = 0; n < victimCount; ++n)
         {
             change->args[n].mode = MODE_REMOVE|MODE_CHANOP|MODE_VOICE;
-            change->args[n].member = victims[n];
+            change->args[n].u.member = victims[n];
         }
         if(!exists)
         {
             change->args[n].mode = MODE_BAN;
-            change->args[n++].hostmask = ban;
+            change->args[n++].u.hostmask = ban;
         }
         change->argc = n;
-        if (cmd)
+        if(cmd)
             modcmd_chanmode_announce(change);
         else
             mod_chanmode_announce(chanserv, channel, change);
@@ -2887,7 +3313,8 @@ eject_user(struct userNode *user, struct chanNode *channel, unsigned int argc, c
 
         if(exists && (action == ACTION_BAN))
        {
-            reply("CSMSG_REDUNDANT_BAN", name, channel->name);
+            if(cmd)
+                reply("CSMSG_REDUNDANT_BAN", name, channel->name);
             free(ban);
             return 0;
         }
@@ -2896,7 +3323,7 @@ eject_user(struct userNode *user, struct chanNode *channel, unsigned int argc, c
     if(action & ACTION_KICK)
     {
         char kick_reason[MAXLEN];
-       sprintf(kick_reason, "%s (%s)", reason, user->nick);
+       sprintf(kick_reason, "(%s) %s", user->nick, reason);
 
        for(n = 0; n < victimCount; n++)
            KickChannelUser(victims[n]->user, channel, chanserv, kick_reason);
@@ -2909,7 +3336,7 @@ eject_user(struct userNode *user, struct chanNode *channel, unsigned int argc, c
     else if(action & ACTION_ADD_BAN)
     {
        if(duration)
-           reply("CSMSG_TIMED_BAN_ADDED", name, channel->name, intervalString(interval, duration));
+           reply("CSMSG_TIMED_BAN_ADDED", name, channel->name, intervalString(interval, duration, user->handle_info));
        else
            reply("CSMSG_BAN_ADDED", name, channel->name);
     }
@@ -2961,7 +3388,8 @@ find_matching_bans(struct banList *bans, struct userNode *actee, const char *mas
     {
         for(ii = count = 0; ii < bans->used; ++ii)
         {
-            match[ii] = user_matches_glob(actee, bans->list[ii]->ban, 1);
+            match[ii] = user_matches_glob(actee, bans->list[ii]->ban,
+                                          MATCH_USENICK | MATCH_VISIBLE);
             if(match[ii])
                 count++;
         }
@@ -2983,8 +3411,9 @@ find_matching_bans(struct banList *bans, struct userNode *actee, const char *mas
         if(!match[ii])
             continue;
         change->args[count].mode = MODE_REMOVE | MODE_BAN;
-        change->args[count++].hostmask = bans->list[ii]->ban;
+        change->args[count++].u.hostmask = strdup(bans->list[ii]->ban);
     }
+    assert(count == change->argc);
     return change;
 }
 
@@ -3017,7 +3446,11 @@ unban_user(struct userNode *user, struct chanNode *channel, unsigned int argc, c
         change = find_matching_bans(&channel->banlist, actee, mask);
         if(change)
         {
+            unsigned int ii;
+
             modcmd_chanmode_announce(change);
+            for(ii = 0; ii < change->argc; ++ii)
+                free((char*)change->args[ii].u.hostmask);
             mod_chanmode_free(change);
             acted = 1;
         }
@@ -3031,7 +3464,8 @@ unban_user(struct userNode *user, struct chanNode *channel, unsigned int argc, c
        while(ban)
        {
            if(actee)
-               for( ; ban && !user_matches_glob(actee, ban->mask, 1);
+               for( ; ban && !user_matches_glob(actee, ban->mask,
+                                                 MATCH_USENICK | MATCH_VISIBLE);
                     ban = ban->next);
            else
                for( ; ban && !match_ircglobs(mask, ban->mask);
@@ -3094,9 +3528,11 @@ static CHANSERV_FUNC(cmd_unbanall)
     for(ii=0; ii<channel->banlist.used; ii++)
     {
         change->args[ii].mode = MODE_REMOVE | MODE_BAN;
-        change->args[ii].hostmask = channel->banlist.list[ii]->ban;
+        change->args[ii].u.hostmask = strdup(channel->banlist.list[ii]->ban);
     }
     modcmd_chanmode_announce(change);
+    for(ii = 0; ii < change->argc; ++ii)
+        free((char*)change->args[ii].u.hostmask);
     mod_chanmode_free(change);
     reply("CSMSG_BANS_REMOVED", channel->name);
     return 1;
@@ -3105,6 +3541,7 @@ static CHANSERV_FUNC(cmd_unbanall)
 static CHANSERV_FUNC(cmd_open)
 {
     struct mod_chanmode *change;
+    unsigned int ii;
 
     change = find_matching_bans(&channel->banlist, user, NULL);
     if(!change)
@@ -3115,65 +3552,79 @@ static CHANSERV_FUNC(cmd_open)
         change->modes_clear &= ~channel->channel_info->modes.modes_set;
     modcmd_chanmode_announce(change);
     reply("CSMSG_CHANNEL_OPENED", channel->name);
+    for(ii = 0; ii < change->argc; ++ii)
+        free((char*)change->args[ii].u.hostmask);
     mod_chanmode_free(change);
     return 1;
 }
 
-static CHANSERV_FUNC(cmd_access)
+static CHANSERV_FUNC(cmd_myaccess)
 {
-    struct userNode *target;
+    static struct string_buffer sbuf;
     struct handle_info *target_handle;
     struct userData *uData;
-    int helping;
-    char prefix[MAXLEN];
 
-    if(!channel)
+    if(argc < 2)
+        target_handle = user->handle_info;
+    else if(!IsHelping(user))
     {
-        struct userData *uData;
-        const char *chanName;
-        int hide = 0;
+        reply("CSMSG_MYACCESS_SELF_ONLY", argv[0]);
+        return 0;
+    }
+    else if(!(target_handle = modcmd_get_handle_info(user, argv[1])))
+        return 0;
 
-        target_handle = user->handle_info;
-        if(!target_handle)
-        {
-            reply("MSG_AUTHENTICATE");
-            return 0;
-        }
-        if(argc > 1)
-        {
-            if(!IsHelping(user))
-            {
-                reply("CSMSG_ACCESS_SELF_ONLY", argv[0]);
-                return 0;
-            }
+    if(!target_handle->channels)
+    {
+        reply("CSMSG_SQUAT_ACCESS", target_handle->handle);
+        return 1;
+    }
 
-            if(!(target_handle = modcmd_get_handle_info(user, argv[1])))
-                return 0;
-            hide = 1;
-        }
-        if(!target_handle->channels)
-        {
-            reply("CSMSG_SQUAT_ACCESS");
-            return 1;
-        }
-        reply("CSMSG_INFOLINE_LIST", target_handle->handle);
-        for(uData = target_handle->channels; uData; uData = uData->u_next)
-        {
-            struct chanData *cData = uData->channel;
+    reply("CSMSG_INFOLINE_LIST", target_handle->handle);
+    for(uData = target_handle->channels; uData; uData = uData->u_next)
+    {
+        struct chanData *cData = uData->channel;
 
-            if(uData->access > UL_OWNER)
-                continue;
-            if(IsProtected(cData) && hide && !GetTrueChannelAccess(cData, user->handle_info))
-                continue;
-            chanName = cData->channel->name;
-            if(uData->info)
-                send_message_type(4, user, cmd->parent->bot, "[%s (%d)] %s", chanName, uData->access, uData->info);
-            else
-                send_message_type(4, user, cmd->parent->bot, "[%s (%d)]", chanName, uData->access);
+        if(uData->access > UL_OWNER)
+            continue;
+        if(IsProtected(cData)
+           && (target_handle != user->handle_info)
+           && !GetTrueChannelAccess(cData, user->handle_info))
+            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, ',');
+        if(IsUserSuspended(uData))
+            string_buffer_append(&sbuf, 's');
+        if(IsUserAutoOp(uData))
+        {
+            if(uData->access >= cData->lvlOpts[lvlGiveOps])
+                string_buffer_append(&sbuf, 'o');
+            else if(uData->access >= cData->lvlOpts[lvlGiveVoice])
+                string_buffer_append(&sbuf, 'v');
         }
-        return 1;
+        if(IsUserAutoInvite(uData) && (uData->access >= cData->lvlOpts[lvlInviteMe]))
+            string_buffer_append(&sbuf, 'i');
+        if(uData->info)
+            string_buffer_append_printf(&sbuf, ")] %s", uData->info);
+        else
+            string_buffer_append_string(&sbuf, ")]");
+        string_buffer_append(&sbuf, '\0');
+        send_message_type(4, user, cmd->parent->bot, "%s", sbuf.list);
     }
 
+    return 1;
+}
+
+static CHANSERV_FUNC(cmd_access)
+{
+    struct userNode *target;
+    struct handle_info *target_handle;
+    struct userData *uData;
+    int helping;
+    char prefix[MAXLEN];
+
     if(argc < 2)
     {
        target = user;
@@ -3227,17 +3678,17 @@ static CHANSERV_FUNC(cmd_access)
         if(IsOper(target))
         {
             epithet = chanserv_conf.irc_operator_epithet;
-            type = "IRCOp";
+            type = user_find_message(user, "CSMSG_OPERATOR_TITLE");
         }
         else if(IsNetworkHelper(target))
         {
             epithet = chanserv_conf.network_helper_epithet;
-            type = "network helper";
+            type = user_find_message(user, "CSMSG_UC_H_TITLE");
         }
         else if(IsSupportHelper(target))
         {
             epithet = chanserv_conf.support_helper_epithet;
-            type = "support helper";
+            type = user_find_message(user, "CSMSG_LC_H_TITLE");
         }
         if(epithet)
         {
@@ -3309,9 +3760,9 @@ zoot_list(struct listData *list)
     tmp_table.flags = list->table.flags;
     list->table.contents[0][0] = " ";
     highest = list->highest;
-    if (list->lowest != 0)
+    if(list->lowest != 0)
         lowest = list->lowest;
-    else if (highest < 100)
+    else if(highest < 100)
         lowest = 1;
     else
         lowest = highest - 100;
@@ -3382,14 +3833,15 @@ cmd_list_users(struct userNode *user, struct chanNode *channel, unsigned int arg
     lData.lowest = lowest;
     lData.highest = highest;
     lData.search = (argc > 1) ? argv[1] : NULL;
-    send_list = zoot_list;
+    send_list = def_list;
+    (void)zoot_list; /* since it doesn't show user levels */
 
     if(user->handle_info)
     {
        switch(user->handle_info->userlist_style)
        {
        case HI_STYLE_DEF: send_list = def_list; break;
-        case HI_STYLE_ZOOT: send_list = zoot_list; break;
+        case HI_STYLE_ZOOT: send_list = def_list; break;
        }
     }
 
@@ -3429,7 +3881,7 @@ cmd_list_users(struct userNode *user, struct chanNode *channel, unsigned int arg
         else if(!uData->seen)
             ary[2] = "Never";
         else
-            ary[2] = intervalString(seen, now - uData->seen);
+            ary[2] = intervalString(seen, now - uData->seen, user->handle_info);
         ary[2] = strdup(ary[2]);
         if(IsUserSuspended(uData))
             ary[3] = "Suspended";
@@ -3481,23 +3933,37 @@ static CHANSERV_FUNC(cmd_plist)
 
 static CHANSERV_FUNC(cmd_bans)
 {
+    struct userNode *search_u = NULL;
     struct helpfile_table tbl;
-    unsigned int matches = 0, timed = 0, ii;
+    unsigned int matches = 0, timed = 0, search_wilds = 0, ii;
     char t_buffer[INTERVALLEN], e_buffer[INTERVALLEN], *search;
     const char *msg_never, *triggered, *expires;
     struct banData *ban, **bans;
 
-    if(argc > 1)
-       search = argv[1];
-    else
+    if(argc < 2)
         search = NULL;
+    else if(strchr(search = argv[1], '!'))
+    {
+       search = argv[1];
+        search_wilds = search[strcspn(search, "?*")];
+    }
+    else if(!(search_u = GetUserH(search)))
+        reply("MSG_NICK_UNKNOWN", search);
 
     bans = alloca(channel->channel_info->banCount * sizeof(struct banData *));
 
     for(ban = channel->channel_info->bans; ban; ban = ban->next)
     {
-       if(search && !match_ircglobs(search, ban->mask))
-           continue;
+        if(search_u)
+        {
+            if(!user_matches_glob(search_u, ban->mask, MATCH_USENICK | MATCH_VISIBLE))
+                continue;
+        }
+       else if(search)
+        {
+            if(search_wilds ? !match_ircglobs(search, ban->mask) : !match_ircglob(search, ban->mask))
+                continue;
+        }
        bans[matches++] = ban;
        if(ban->expires)
             timed = 1;
@@ -3523,6 +3989,8 @@ static CHANSERV_FUNC(cmd_bans)
     {
         table_send(cmd->parent->bot, user->nick, 0, NULL, tbl);
        reply("MSG_NONE");
+        free(tbl.contents[0]);
+        free(tbl.contents);
        return 0;
     }
 
@@ -3534,12 +4002,12 @@ static CHANSERV_FUNC(cmd_bans)
        if(!timed)
            expires = "";
        else if(ban->expires)
-           expires = intervalString(e_buffer, ban->expires - now);
+           expires = intervalString(e_buffer, ban->expires - now, user->handle_info);
        else
            expires = msg_never;
 
        if(ban->triggered)
-           triggered = intervalString(t_buffer, now - ban->triggered);
+           triggered = intervalString(t_buffer, now - ban->triggered, user->handle_info);
        else
            triggered = msg_never;
 
@@ -3649,7 +4117,7 @@ static CHANSERV_FUNC(cmd_topic)
     else
         SetChannelTopic(channel, chanserv, topic, 1);
 
-    if(cData->flags & CHANNEL_TOPIC_SNARF)
+    if(check_user_level(channel, user, lvlTopicSnarf, 1, 0))
     {
         /* Grab the topic and save it as the default topic. */
         free(cData->topic);
@@ -3661,8 +4129,10 @@ static CHANSERV_FUNC(cmd_topic)
 
 static CHANSERV_FUNC(cmd_mode)
 {
+    struct userData *uData;
     struct mod_chanmode *change;
-    
+    short base_oplevel;
+
     if(argc < 2)
     {
         change = &channel->channel_info->modes;
@@ -3674,7 +4144,14 @@ static CHANSERV_FUNC(cmd_mode)
        return 1;
     }
 
-    change = mod_chanmode_parse(channel, argv+1, argc-1, MCP_KEY_FREE);
+    uData = GetChannelUser(channel->channel_info, user->handle_info);
+    if (!uData)
+        base_oplevel = MAXOPLEVEL;
+    else if (uData->access >= UL_OWNER)
+        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, base_oplevel);
     if(!change)
     {
        reply("MSG_INVALID_MODES", unsplit_string(argv+1, argc-1, NULL));
@@ -3722,8 +4199,13 @@ static CHANSERV_FUNC(cmd_invite)
 
     if(user != invite)
     {
-       char *reason = (argc > 2) ? unsplit_string(argv + 2, argc - 2, NULL) : "";
-       send_message(invite, chanserv, "CSMSG_INVITING_YOU", user->nick, channel->name, (argc > 2) ? ": " : ".", reason);
+        if(argc > 2)
+        {
+            char *reason = unsplit_string(argv + 2, argc - 2, NULL);
+            send_message(invite, chanserv, "CSMSG_INVITING_YOU_REASON", user->nick, channel->name, reason);
+        }
+        else
+            send_message(invite, chanserv, "CSMSG_INVITING_YOU", user->nick, channel->name);
     }
     irc_invite(chanserv, invite, channel);
     if(argc > 1)
@@ -3734,17 +4216,13 @@ static CHANSERV_FUNC(cmd_invite)
 
 static CHANSERV_FUNC(cmd_inviteme)
 {
-    struct userData *uData;
-
     if(GetUserMode(channel, user))
     {
        reply("CSMSG_YOU_ALREADY_PRESENT", channel->name);
        return 0;
     }
     if(channel->channel_info
-       && !(channel->channel_info->flags & CHANNEL_PEON_INVITE)
-       && (uData = GetChannelUser(channel->channel_info, user->handle_info))
-       && (uData->access < channel->channel_info->lvlOpts[lvlGiveOps]))
+       && !check_user_level(channel, user, lvlInviteMe, 1, 0))
     {
         reply("CSMSG_LOW_CHANNEL_ACCESS", channel->name);
         return 0;
@@ -3772,34 +4250,34 @@ show_suspension_info(struct svccmd *cmd, struct userNode *user, struct suspended
         reply("CSMSG_CHANNEL_SUSPENDED_0", suspended->suspender, suspended->reason);
         break;
     case 1: /* no issue time, expires in future */
-        intervalString(buf1, suspended->expires-now);
+        intervalString(buf1, suspended->expires-now, user->handle_info);
         reply("CSMSG_CHANNEL_SUSPENDED_1", suspended->suspender, buf1, suspended->reason);
         break;
     case 2: /* no issue time, expired */
-        intervalString(buf1, now-suspended->expires);
+        intervalString(buf1, now-suspended->expires, user->handle_info);
         reply("CSMSG_CHANNEL_SUSPENDED_2", suspended->suspender, buf1, suspended->reason);
         break;
     case 3: /* no issue time, revoked */
-        intervalString(buf1, now-suspended->revoked);
+        intervalString(buf1, now-suspended->revoked, user->handle_info);
         reply("CSMSG_CHANNEL_SUSPENDED_3", suspended->suspender, buf1, suspended->reason);
         break;
     case 4: /* issue time set, indefinite expiration */
-        intervalString(buf1, now-suspended->issued);
+        intervalString(buf1, now-suspended->issued, user->handle_info);
         reply("CSMSG_CHANNEL_SUSPENDED_4", buf1, suspended->suspender, suspended->reason);
         break;
     case 5: /* issue time set, expires in future */
-        intervalString(buf1, now-suspended->issued);
-        intervalString(buf2, suspended->expires-now);
+        intervalString(buf1, now-suspended->issued, user->handle_info);
+        intervalString(buf2, suspended->expires-now, user->handle_info);
         reply("CSMSG_CHANNEL_SUSPENDED_5", buf1, suspended->suspender, buf2, suspended->reason);
         break;
     case 6: /* issue time set, expired */
-        intervalString(buf1, now-suspended->issued);
-        intervalString(buf2, now-suspended->expires);
+        intervalString(buf1, now-suspended->issued, user->handle_info);
+        intervalString(buf2, now-suspended->expires, user->handle_info);
         reply("CSMSG_CHANNEL_SUSPENDED_6", buf1, suspended->suspender, buf2, suspended->reason);
         break;
     case 7: /* issue time set, revoked */
-        intervalString(buf1, now-suspended->issued);
-        intervalString(buf2, now-suspended->revoked);
+        intervalString(buf1, now-suspended->issued, user->handle_info);
+        intervalString(buf2, now-suspended->revoked, user->handle_info);
         reply("CSMSG_CHANNEL_SUSPENDED_7", buf1, suspended->suspender, buf2, suspended->reason);
         break;
     default:
@@ -3847,10 +4325,11 @@ static CHANSERV_FUNC(cmd_info)
             reply("CSMSG_CHANNEL_OWNER", owner->handle->handle);
     reply("CSMSG_CHANNEL_USERS", cData->userCount);
     reply("CSMSG_CHANNEL_BANS", cData->banCount);
-    reply("CSMSG_CHANNEL_VISITED", intervalString(buffer, now - cData->visited));
-    reply("CSMSG_CHANNEL_REGISTERED", intervalString(buffer, now - cData->registered));
+    reply("CSMSG_CHANNEL_VISITED", intervalString(buffer, now - cData->visited, user->handle_info));
 
     privileged = IsStaff(user);
+    if(privileged)
+        reply("CSMSG_CHANNEL_REGISTERED", intervalString(buffer, now - cData->registered, user->handle_info));
     if(((uData && uData->access >= UL_COOWNER) || privileged) && cData->registrar)
         reply("CSMSG_CHANNEL_REGISTRAR", cData->registrar);
 
@@ -3884,9 +4363,9 @@ static CHANSERV_FUNC(cmd_netinfo)
     reply("CSMSG_NETWORK_OPERS", curr_opers.used);
     reply("CSMSG_NETWORK_CHANNELS", registered_channels);
     reply("CSMSG_NETWORK_BANS", banCount);
-    reply("CSMSG_CHANNEL_USERS", userCount);
-    reply("CSMSG_SERVICES_UPTIME", intervalString(interval, time(NULL) - boot_time));
-    reply("CSMSG_BURST_LENGTH",intervalString(interval, burst_length));
+    reply("CSMSG_NETWORK_CHANUSERS", userCount);
+    reply("CSMSG_SERVICES_UPTIME", intervalString(interval, time(NULL) - boot_time, user->handle_info));
+    reply("CSMSG_BURST_LENGTH", intervalString(interval, burst_length, user->handle_info));
     return 1;
 }
 
@@ -3985,10 +4464,12 @@ static CHANSERV_FUNC(cmd_peek)
 static MODCMD_FUNC(cmd_wipeinfo)
 {
     struct handle_info *victim;
-    struct userData *ud, *actor;
+    struct userData *ud, *actor, *real_actor;
+    unsigned int override = 0;
 
     REQUIRE_PARAMS(2);
     actor = GetChannelUser(channel->channel_info, user->handle_info);
+    real_actor = GetChannelAccess(channel->channel_info, user->handle_info);
     if(!(victim = modcmd_get_handle_info(user, argv[1])))
         return 0;
     if(!(ud = GetTrueChannelAccess(channel->channel_info, victim)))
@@ -4001,11 +4482,13 @@ static MODCMD_FUNC(cmd_wipeinfo)
         reply("MSG_USER_OUTRANKED", victim->handle);
         return 0;
     }
+    if((ud != real_actor) && (!real_actor || (ud->access >= real_actor->access)))
+        override = CMD_LOG_OVERRIDE;
     if(ud->info)
         free(ud->info);
     ud->info = NULL;
     reply("CSMSG_WIPED_INFO_LINE", argv[1], channel->name);
-    return 1;
+    return 1 | override;
 }
 
 static CHANSERV_FUNC(cmd_resync)
@@ -4019,37 +4502,40 @@ static CHANSERV_FUNC(cmd_resync)
     {
         struct modeNode *mn = channel->members.list[ii];
         struct userData *uData;
+
         if(IsService(mn->user))
+            continue;
+
+        uData = GetChannelAccess(cData, mn->user->handle_info);
+        if(!cData->lvlOpts[lvlGiveOps]
+           || (uData && uData->access >= cData->lvlOpts[lvlGiveOps]))
         {
-            /* must not change modes for this user */
-        }
-        else if(!(uData = GetChannelAccess(cData, mn->user->handle_info)))
-        {
-            if(mn->modes)
+            if(!(mn->modes & MODE_CHANOP))
             {
-                changes->args[used].mode = MODE_REMOVE | mn->modes;
-                changes->args[used++].member = mn;
+                changes->args[used].mode = MODE_CHANOP;
+                changes->args[used++].u.member = mn;
             }
         }
-        else if(uData->access < cData->lvlOpts[lvlGiveOps])
+        else if(!cData->lvlOpts[lvlGiveVoice]
+                || (uData && uData->access >= cData->lvlOpts[lvlGiveVoice]))
         {
             if(mn->modes & MODE_CHANOP)
             {
                 changes->args[used].mode = MODE_REMOVE | (mn->modes & ~MODE_VOICE);
-                changes->args[used++].member = mn;
+                changes->args[used++].u.member = mn;
             }
             if(!(mn->modes & MODE_VOICE))
             {
                 changes->args[used].mode = MODE_VOICE;
-                changes->args[used++].member = mn;
+                changes->args[used++].u.member = mn;
             }
         }
         else
         {
-            if(!(mn->modes & MODE_CHANOP))
+            if(mn->modes)
             {
-                changes->args[used].mode = MODE_CHANOP;
-                changes->args[used++].member = mn;
+                changes->args[used].mode = MODE_REMOVE | mn->modes;
+                changes->args[used++].u.member = mn;
             }
         }
     }
@@ -4089,7 +4575,7 @@ static CHANSERV_FUNC(cmd_seen)
     if(uData->present)
        reply("CSMSG_USER_PRESENT", handle->handle);
     else if(uData->seen)
-        reply("CSMSG_USER_SEEN", handle->handle, channel->name, intervalString(seen, now - uData->seen));
+        reply("CSMSG_USER_SEEN", handle->handle, channel->name, intervalString(seen, now - uData->seen, user->handle_info));
     else
         reply("CSMSG_NEVER_SEEN", handle->handle, channel->name);
 
@@ -4112,7 +4598,7 @@ static MODCMD_FUNC(cmd_names)
         targData = GetTrueChannelAccess(channel->channel_info, targ->handle_info);
         if(!targData)
             continue;
-        if(pos + strlen(targ->nick) + strlen(targ->handle_info->handle) + 6 > sizeof(buf))
+        if(pos + strlen(targ->nick) + strlen(targ->handle_info->handle) + 8 > sizeof(buf))
         {
             buf[pos] = 0;
             reply("CSMSG_CHANNEL_NAMES", channel->name, buf);
@@ -4156,7 +4642,7 @@ note_type_settable_by_user(struct chanNode *channel, struct note_type *ntype, st
     case NOTE_SET_CHANNEL_SETTER:
         return check_user_level(channel, user, lvlSetters, 1, 0);
     case NOTE_SET_PRIVILEGED: default:
-        return IsHelping(user);
+        return IsHelping(user) && (user->handle_info->opserv_level >= ntype->set_access.min_opserv);
     }
 }
 
@@ -4271,12 +4757,14 @@ static CHANSERV_FUNC(cmd_events)
     unsigned int matches, limit;
 
     limit = (argc > 1) ? atoi(argv[1]) : 10;
-    if(limit < 1 || limit > 200) limit = 10;
+    if(limit < 1 || limit > 200)
+        limit = 10;
 
     memset(&discrim, 0, sizeof(discrim));
     discrim.masks.bot = chanserv;
     discrim.masks.channel_name = channel->name;
-    if(argc > 2) discrim.masks.command = argv[2];
+    if(argc > 2)
+        discrim.masks.command = argv[2];
     discrim.limit = limit;
     discrim.max_time = INT_MAX;
     discrim.severities = 1 << LOG_COMMAND;
@@ -4304,11 +4792,11 @@ static CHANSERV_FUNC(cmd_say)
     {
         REQUIRE_PARAMS(3);
         msg = unsplit_string(argv + 2, argc - 2, NULL);
-        send_target_message(1, argv[1], cmd->parent->bot, "%s", msg);
+        send_target_message(5, argv[1], cmd->parent->bot, "%s", msg);
     }
     else
     {
-        reply("You must specify the name of a channel or user.");
+        reply("MSG_NOT_TARGET_NAME");
         return 0;
     }
     return 1;
@@ -4327,11 +4815,11 @@ static CHANSERV_FUNC(cmd_emote)
     else if(GetUserH(argv[1]))
     {
         msg = unsplit_string(argv + 2, argc - 2, NULL);
-        send_target_message(1, argv[1], cmd->parent->bot, "\001ACTION %s\001", msg);
+        send_target_message(5, argv[1], cmd->parent->bot, "\001ACTION %s\001", msg);
     }
     else
     {
-        reply("You must specify the name of a channel or user.");
+        reply("MSG_NOT_TARGET_NAME");
         return 0;
     }
     return 1;
@@ -4356,18 +4844,21 @@ chanserv_expire_suspension(void *data)
 {
     struct suspended *suspended = data;
     struct chanNode *channel;
-    struct mod_chanmode change;
 
     if(!suspended->expires || (now < suspended->expires))
         suspended->revoked = now;
     channel = suspended->cData->channel;
     suspended->cData->channel = channel;
     suspended->cData->flags &= ~CHANNEL_SUSPENDED;
-    change.modes_set = change.modes_clear = 0;
-    change.argc = 1;
-    change.args[0].mode = MODE_CHANOP;
-    change.args[0].member = AddChannelUser(chanserv, channel);
-    mod_chanmode_announce(chanserv, channel, &change);
+    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);
+    }
 }
 
 static CHANSERV_FUNC(cmd_csuspend)
@@ -4519,6 +5010,8 @@ chanserv_search_create(struct userNode *user, unsigned int argc, char *argv[])
                search->flags |= CHANNEL_NODELETE;
            else if(!irccasecmp(argv[i], "suspended"))
                search->flags |= CHANNEL_SUSPENDED;
+            else if(!irccasecmp(argv[i], "unreviewed"))
+                search->flags |= CHANNEL_UNREVIEWED;
            else
            {
                send_message(user, chanserv, "CSMSG_INVALID_CFLAG", argv[i]);
@@ -4641,7 +5134,7 @@ static CHANSERV_FUNC(cmd_unvisited)
             limit = atoi(argv[2]);
     }
 
-    intervalString(buffer, interval);
+    intervalString(buffer, interval, user->handle_info);
     reply("CSMSG_UNVISITED_HEADER", limit, buffer);
 
     for(cData = channelList; cData && matches < limit; cData = cData->next)
@@ -4649,7 +5142,7 @@ static CHANSERV_FUNC(cmd_unvisited)
        if((now - cData->visited) < interval)
             continue;
 
-       intervalString(buffer, now - cData->visited);
+       intervalString(buffer, now - cData->visited, user->handle_info);
        reply("CSMSG_UNVISITED_DATA", cData->channel->name, buffer);
        matches++;
     }
@@ -4788,7 +5281,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)))
+       else if(!(new_modes = mod_chanmode_parse(channel, argv+1, argc-1, MCP_KEY_FREE|MCP_REGISTERED, 0)))
        {
             reply("CSMSG_INVALID_MODE_LOCK", unsplit_string(argv+1, argc-1, NULL));
             return 0;
@@ -4865,34 +5358,111 @@ static MODCMD_FUNC(chan_opt_nodelete)
     CHANNEL_BINARY_OPTION("CSMSG_SET_NODELETE", CHANNEL_NODELETE);
 }
 
-static MODCMD_FUNC(chan_opt_userinfo)
-{
-    CHANNEL_BINARY_OPTION("CSMSG_SET_USERINFO", CHANNEL_INFO_LINES);
-}
-
-static MODCMD_FUNC(chan_opt_voice)
-{
-    CHANNEL_BINARY_OPTION("CSMSG_SET_VOICE", CHANNEL_VOICE_ALL);
-}
-
 static MODCMD_FUNC(chan_opt_dynlimit)
 {
     CHANNEL_BINARY_OPTION("CSMSG_SET_DYNLIMIT", CHANNEL_DYNAMIC_LIMIT);
 }
 
-static MODCMD_FUNC(chan_opt_topicsnarf)
+static MODCMD_FUNC(chan_opt_offchannel)
 {
-    if((argc > 0) && !check_user_level(channel, user, lvlEnfTopic, 1, 0))
+    struct chanData *cData = channel->channel_info;
+    int value;
+
+    if(argc > 1)
     {
-        reply("CSMSG_TOPIC_LOCKED", channel->name);
-        return 0;
+       /* Set flag according to value. */
+       if(enabled_string(argv[1]))
+       {
+            if(!IsOffChannel(cData))
+                DelChannelUser(chanserv, channel, "Going off-channel.", 0);
+           cData->flags |= CHANNEL_OFFCHANNEL;
+           value = 1;
+       }
+       else if(disabled_string(argv[1]))
+       {
+            if(IsOffChannel(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);
+            }
+           cData->flags &= ~CHANNEL_OFFCHANNEL;
+           value = 0;
+       }
+       else
+       {
+           reply("MSG_INVALID_BINARY", argv[1]);
+           return 0;
+       }
+    }
+    else
+    {
+       /* Find current option value. */
+       value = (cData->flags & CHANNEL_OFFCHANNEL) ? 1 : 0;
     }
-    CHANNEL_BINARY_OPTION("CSMSG_SET_TOPICSNARF", CHANNEL_TOPIC_SNARF);
+
+    if(value)
+        reply("CSMSG_SET_OFFCHANNEL", user_find_message(user, "MSG_ON"));
+    else
+        reply("CSMSG_SET_OFFCHANNEL", user_find_message(user, "MSG_OFF"));
+    return 1;
 }
 
-static MODCMD_FUNC(chan_opt_peoninvite)
+static MODCMD_FUNC(chan_opt_unreviewed)
 {
-    CHANNEL_BINARY_OPTION("CSMSG_SET_PEONINVITE", CHANNEL_PEON_INVITE);
+    struct chanData *cData = channel->channel_info;
+    int value = (cData->flags & CHANNEL_UNREVIEWED) ? 1 : 0;
+
+    if(argc > 1)
+    {
+        int new_value;
+
+        /* The two directions can have different ACLs. */
+        if(enabled_string(argv[1]))
+            new_value = 1;
+        else if(disabled_string(argv[1]))
+            new_value = 0;
+        else
+       {
+           reply("MSG_INVALID_BINARY", argv[1]);
+           return 0;
+       }
+
+        if (new_value != value)
+        {
+            struct svccmd *subcmd;
+            char subcmd_name[32];
+
+            snprintf(subcmd_name, sizeof(subcmd_name), "%s %s", argv[0], (new_value ? "on" : "off"));
+            subcmd = dict_find(cmd->parent->commands, subcmd_name, NULL);
+            if(!subcmd)
+            {
+                reply("MSG_COMMAND_DISABLED", subcmd_name);
+                return 0;
+            }
+            else if(!svccmd_can_invoke(user, cmd->parent->bot, subcmd, channel, SVCCMD_NOISY))
+                return 0;
+
+            if (new_value)
+                cData->flags |= CHANNEL_UNREVIEWED;
+            else
+            {
+                free(cData->registrar);
+                cData->registrar = strdup(user->handle_info->handle);
+                cData->flags &= ~CHANNEL_UNREVIEWED;
+            }
+            value = new_value;
+        }
+    }
+
+    if(value)
+        reply("CSMSG_SET_UNREVIEWED", user_find_message(user, "MSG_ON"));
+    else
+        reply("CSMSG_SET_UNREVIEWED", user_find_message(user, "MSG_OFF"));
+    return 1;
 }
 
 static MODCMD_FUNC(chan_opt_defaults)
@@ -4916,7 +5486,8 @@ static MODCMD_FUNC(chan_opt_defaults)
         reply("CSMSG_CONFIRM_DEFAULTS", channel->name, confirm);
         return 0;
     }
-    cData->flags = CHANNEL_DEFAULT_FLAGS;
+    cData->flags = (CHANNEL_DEFAULT_FLAGS & ~CHANNEL_PRESERVED_FLAGS)
+        | (cData->flags & CHANNEL_PRESERVED_FLAGS);
     cData->modes = chanserv_conf.default_modes;
     for(lvlOpt = 0; lvlOpt < NUM_LEVEL_OPTIONS; ++lvlOpt)
         cData->lvlOpts[lvlOpt] = levelOptions[lvlOpt].default_value;
@@ -4941,17 +5512,47 @@ channel_level_option(enum levelOption option, struct userNode *user, struct chan
             return 0;
         }
         value = user_level_from_name(argv[1], UL_OWNER+1);
-        if(!value && !isdigit(argv[1][0]))
+        if(!value && strcmp(argv[1], "0"))
        {
            reply("CSMSG_INVALID_ACCESS", argv[1]);
             return 0;
         }
         uData = GetChannelUser(cData, user->handle_info);
-        if(!uData || (uData->access < value))
+        if(!uData || ((uData->access < UL_OWNER) && (value > uData->access)))
         {
             reply("CSMSG_BAD_SETLEVEL");
             return 0;
         }
+        switch(option)
+        {
+        case lvlGiveVoice:
+            if(value > cData->lvlOpts[lvlGiveOps])
+            {
+                reply("CSMSG_BAD_GIVEVOICE", cData->lvlOpts[lvlGiveOps]);
+                return 0;
+            }
+            break;
+        case lvlGiveOps:
+            if(value < cData->lvlOpts[lvlGiveVoice])
+            {
+                reply("CSMSG_BAD_GIVEOPS", cData->lvlOpts[lvlGiveVoice]);
+                return 0;
+            }
+            break;
+        case lvlSetters:
+            /* This test only applies to owners, since non-owners
+             * trying to set an option to above their level get caught
+             * by the CSMSG_BAD_SETLEVEL test above.
+             */
+            if(value > uData->access)
+            {
+                reply("CSMSG_BAD_SETTERS");
+                return 0;
+            }
+            break;
+        default:
+            break;
+        }
         cData->lvlOpts[option] = value;
     }
     reply(levelOptions[option].format_name, cData->lvlOpts[option]);
@@ -4993,6 +5594,26 @@ static MODCMD_FUNC(chan_opt_ctcpusers)
     return channel_level_option(lvlCTCPUsers, CSFUNC_ARGS);
 }
 
+static MODCMD_FUNC(chan_opt_userinfo)
+{
+    return channel_level_option(lvlUserInfo, CSFUNC_ARGS);
+}
+
+static MODCMD_FUNC(chan_opt_givevoice)
+{
+    return channel_level_option(lvlGiveVoice, CSFUNC_ARGS);
+}
+
+static MODCMD_FUNC(chan_opt_topicsnarf)
+{
+    return channel_level_option(lvlTopicSnarf, CSFUNC_ARGS);
+}
+
+static MODCMD_FUNC(chan_opt_inviteme)
+{
+    return channel_level_option(lvlInviteMe, CSFUNC_ARGS);
+}
+
 static int
 channel_multiple_option(enum charOption option, struct userNode *user, struct chanNode *channel, int argc, char *argv[], struct svccmd *cmd)
 {
@@ -5115,6 +5736,8 @@ static CHANSERV_FUNC(cmd_set)
         return 0;
     }
 
+    argv[0] = "";
+    argv[1] = buf;
     return subcmd->command->func(user, channel, argc - 1, argv + 1, subcmd);
 }
 
@@ -5191,7 +5814,19 @@ static MODCMD_FUNC(user_opt_info)
 
     if(argc > 1)
     {
+        size_t bp;
         infoline = unsplit_string(argv + 1, argc - 1, NULL);
+        if(strlen(infoline) > chanserv_conf.max_userinfo_length)
+        {
+            reply("CSMSG_INFOLINE_TOO_LONG", chanserv_conf.max_userinfo_length);
+            return 0;
+        }
+        bp = strcspn(infoline, "\001");
+        if(infoline[bp])
+        {
+            reply("CSMSG_BAD_INFOLINE", infoline[bp]);
+            return 0;
+        }
         if(uData->info)
             free(uData->info);
         if(infoline[0] == '*' && infoline[1] == 0)
@@ -5264,9 +5899,12 @@ static CHANSERV_FUNC(cmd_uset)
 static CHANSERV_FUNC(cmd_giveownership)
 {
     struct handle_info *new_owner_hi;
-    struct userData *new_owner, *curr_user;
+    struct userData *new_owner;
+    struct userData *curr_user;
+    struct userData *invoker;
     struct chanData *cData = channel->channel_info;
     struct do_not_register *dnr;
+    const char *confirm;
     unsigned int force;
     unsigned short co_access;
     char reason[MAXLEN];
@@ -5290,6 +5928,14 @@ static CHANSERV_FUNC(cmd_giveownership)
             }
             owner = curr_user;
         }
+        curr_user = owner;
+    }
+    else if(!force && (now < (time_t)(cData->ownerTransfer + chanserv_conf.giveownership_period)))
+    {
+        char delay[INTERVALLEN];
+        intervalString(delay, cData->ownerTransfer + chanserv_conf.giveownership_period - now, user->handle_info);
+        reply("CSMSG_TRANSFER_WAIT", delay, channel->name);
+        return 0;
     }
     if(!(new_owner_hi = modcmd_get_handle_info(user, argv[1])))
         return 0;
@@ -5301,8 +5947,15 @@ static CHANSERV_FUNC(cmd_giveownership)
     new_owner = GetChannelAccess(cData, new_owner_hi);
     if(!new_owner)
     {
-        reply("CSMSG_NO_CHAN_USER", new_owner_hi->handle, channel->name);
-        return 0;
+        if(force)
+        {
+            new_owner = add_channel_user(cData, new_owner_hi, UL_COOWNER, 0, NULL);
+        }
+        else
+        {
+            reply("CSMSG_NO_CHAN_USER", new_owner_hi->handle, channel->name);
+            return 0;
+        }
     }
     if((chanserv_get_owned_count(new_owner_hi) >= chanserv_conf.max_owned) && !force)
     {
@@ -5313,9 +5966,19 @@ static CHANSERV_FUNC(cmd_giveownership)
         if(!IsHelping(user))
             reply("CSMSG_DNR_ACCOUNT", new_owner_hi->handle);
         else
-            chanserv_show_dnrs(user, cmd, NULL, new_owner_hi);
+            chanserv_show_dnrs(user, cmd, NULL, new_owner_hi->handle);
         return 0;
     }
+    invoker = GetChannelUser(cData, user->handle_info);
+    if(invoker->access <= UL_OWNER)
+    {
+        confirm = make_confirmation_string(curr_user);
+        if((argc < 3) || strcmp(argv[2], confirm))
+        {
+            reply("CSMSG_CONFIRM_GIVEOWNERSHIP", new_owner_hi->handle, confirm);
+            return 0;
+        }
+    }
     if(new_owner->access >= UL_COOWNER)
         co_access = new_owner->access;
     else
@@ -5323,6 +5986,7 @@ static CHANSERV_FUNC(cmd_giveownership)
     new_owner->access = UL_OWNER;
     if(curr_user)
         curr_user->access = co_access;
+    cData->ownerTransfer = now;
     reply("CSMSG_OWNERSHIP_GIVEN", channel->name, new_owner_hi->handle);
     sprintf(reason, "%s ownership transferred to %s by %s.", channel->name, new_owner_hi->handle, user->handle_info->handle);
     global_message(MESSAGE_RECIPIENT_OPERS | MESSAGE_RECIPIENT_HELPERS, reason);
@@ -5332,11 +5996,13 @@ static CHANSERV_FUNC(cmd_giveownership)
 static CHANSERV_FUNC(cmd_suspend)
 {
     struct handle_info *hi;
-    struct userData *self, *target;
+    struct userData *self, *real_self, *target;
+    unsigned int override = 0;
 
     REQUIRE_PARAMS(2);
     if(!(hi = modcmd_get_handle_info(user, argv[1]))) return 0;
     self = GetChannelUser(channel->channel_info, user->handle_info);
+    real_self = GetChannelAccess(channel->channel_info, user->handle_info);
     if(!(target = GetTrueChannelAccess(channel->channel_info, hi)))
     {
         reply("CSMSG_NO_CHAN_USER", hi->handle, channel->name);
@@ -5357,19 +6023,23 @@ static CHANSERV_FUNC(cmd_suspend)
         target->present = 0;
         target->seen = now;
     }
+    if(!real_self || target->access >= real_self->access)
+        override = CMD_LOG_OVERRIDE;
     target->flags |= USER_SUSPENDED;
     reply("CSMSG_USER_SUSPENDED", hi->handle, channel->name);
-    return 1;
+    return 1 | override;
 }
 
 static CHANSERV_FUNC(cmd_unsuspend)
 {
     struct handle_info *hi;
-    struct userData *self, *target;
+    struct userData *self, *real_self, *target;
+    unsigned int override = 0;
 
     REQUIRE_PARAMS(2);
     if(!(hi = modcmd_get_handle_info(user, argv[1]))) return 0;
     self = GetChannelUser(channel->channel_info, user->handle_info);
+    real_self = GetChannelAccess(channel->channel_info, user->handle_info);
     if(!(target = GetTrueChannelAccess(channel->channel_info, hi)))
     {
         reply("CSMSG_NO_CHAN_USER", hi->handle, channel->name);
@@ -5385,9 +6055,12 @@ static CHANSERV_FUNC(cmd_unsuspend)
         reply("CSMSG_NOT_SUSPENDED", hi->handle);
         return 0;
     }
+    if(!real_self || target->access >= real_self->access)
+        override = CMD_LOG_OVERRIDE;
     target->flags &= ~USER_SUSPENDED;
+    scan_user_presence(target, NULL);
     reply("CSMSG_USER_UNSUSPENDED", hi->handle, channel->name);
-    return 1;
+    return 1 | override;
 }
 
 static MODCMD_FUNC(cmd_deleteme)
@@ -5517,10 +6190,12 @@ static CHANSERV_FUNC(cmd_d)
     const char *fmt;
 
     REQUIRE_PARAMS(2);
-    if((count = strtoul(argv[1], &sep, 10)) <= 1)
+    if((count = strtoul(argv[1], &sep, 10)) < 1)
         goto no_dice;
     if(sep[0] == 0)
     {
+        if(count == 1)
+            goto no_dice;
         sides = count;
         count = 1;
         modifier = 0;
@@ -5563,7 +6238,7 @@ static CHANSERV_FUNC(cmd_d)
         sprintf(response, fmt, total, sides);
     }
     if(channel)
-        send_target_message(5, channel->name, cmd->parent->bot, "$b%s$b: %s", user->nick, response);
+        send_channel_message(channel, cmd->parent->bot, "$b%s$b: %s", user->nick, response);
     else
         send_message_type(4, user, cmd->parent->bot, "%s", response);
     return 1;
@@ -5571,20 +6246,11 @@ static CHANSERV_FUNC(cmd_d)
 
 static CHANSERV_FUNC(cmd_huggle)
 {
-    char response[MAXLEN];
-    const char *fmt;
     /* CTCP must be via PRIVMSG, never notice */
     if(channel)
-    {
-        fmt = user_find_message(user, "CSMSG_HUGGLES_HIM");
-        sprintf(response, fmt, user->nick);
-        irc_privmsg(cmd->parent->bot, channel->name, response);
-    }
+        send_target_message(1, channel->name, cmd->parent->bot, "CSMSG_HUGGLES_HIM", user->nick);
     else
-    {
-        fmt = user_find_message(user, "CSMSG_HUGGLES_YOU");
-        irc_privmsg(cmd->parent->bot, user->nick, fmt);
-    }
+        send_target_message(1, user->nick, cmd->parent->bot, "CSMSG_HUGGLES_YOU");
     return 1;
 }
 
@@ -5609,10 +6275,9 @@ chanserv_adjust_limit(void *data)
             return;
     }
 
+    mod_chanmode_init(&change);
     change.modes_set = MODE_LIMIT;
-    change.modes_clear = 0;
     change.new_limit = limit;
-    change.argc = 0;
     mod_chanmode_announce(chanserv, channel, &change);
 }
 
@@ -5668,7 +6333,7 @@ handle_join(struct modeNode *mNode)
         unsigned int ii;
         for(ii = 0; ii < channel->banlist.used; ii++)
         {
-            if(user_matches_glob(user, channel->banlist.list[ii]->ban, 1))
+            if(user_matches_glob(user, channel->banlist.list[ii]->ban, MATCH_USENICK))
             {
                 /* Riding a netburst.  Naughty. */
                 KickChannelUser(user, channel, chanserv, "User from far side of netsplit should have been banned - bye.");
@@ -5677,26 +6342,28 @@ handle_join(struct modeNode *mNode)
         }
     }
 
-    change.modes_set = change.modes_clear = 0;
+    mod_chanmode_init(&change);
     change.argc = 1;
     if(channel->banlist.used < MAXBANS)
     {
         /* Not joining through a ban. */
         for(bData = cData->bans;
-                bData && !user_matches_glob(user, bData->mask, 1);
-                bData = bData->next);
+            bData && !user_matches_glob(user, bData->mask, MATCH_USENICK);
+            bData = bData->next);
 
         if(bData)
         {
             char kick_reason[MAXLEN];
-            sprintf(kick_reason, "%s (%s)", bData->reason, bData->owner);
+            sprintf(kick_reason, "(%s) %s", bData->owner, bData->reason);
 
             bData->triggered = now;
             if(bData != cData->bans)
             {
                 /* Shuffle the ban to the head of the list. */
-                if(bData->next) bData->next->prev = bData->prev;
-                if(bData->prev) bData->prev->next = bData->next;
+                if(bData->next)
+                    bData->next->prev = bData->prev;
+                if(bData->prev)
+                    bData->prev->next = bData->next;
 
                 bData->prev = NULL;
                 bData->next = cData->bans;
@@ -5707,7 +6374,7 @@ handle_join(struct modeNode *mNode)
             }
 
             change.args[0].mode = MODE_BAN;
-            change.args[0].hostmask = bData->mask;
+            change.args[0].u.hostmask = bData->mask;
             mod_chanmode_announce(chanserv, channel, &change);
             KickChannelUser(user, channel, chanserv, kick_reason);
             return 1;
@@ -5731,9 +6398,13 @@ handle_join(struct modeNode *mNode)
         timeq_add(now + chanserv_conf.adjust_delay, chanserv_adjust_limit, cData);
     }
 
-    if(cData->lvlOpts[lvlGiveOps] == 0)
+    if(channel->join_flooded)
+    {
+        /* don't automatically give ops or voice during a join flood */
+    }
+    else if(cData->lvlOpts[lvlGiveOps] == 0)
         modes |= MODE_CHANOP;
-    else if((cData->flags & CHANNEL_VOICE_ALL) && !channel->join_flooded)
+    else if(cData->lvlOpts[lvlGiveVoice] == 0)
         modes |= MODE_VOICE;
 
     greeting = cData->greeting;
@@ -5760,38 +6431,42 @@ handle_join(struct modeNode *mNode)
             /* Ops and above were handled by the above case. */
             if(IsUserAutoOp(uData))
             {
-                if(uData->access < cData->lvlOpts[lvlGiveOps])
-                    modes |= MODE_VOICE;
-                else
+                if(uData->access >= cData->lvlOpts[lvlGiveOps])
                     modes |= MODE_CHANOP;
+                else if(uData->access >= cData->lvlOpts[lvlGiveVoice])
+                    modes |= MODE_VOICE;
             }
             if(uData->access >= UL_PRESENT)
                 cData->visited = now;
-
-            uData->seen = now;
-            uData->present = 1;
-
             if(cData->user_greeting)
                 greeting = cData->user_greeting;
             if(uData->info
-               && (cData->flags & CHANNEL_INFO_LINES)
+               && (uData->access >= cData->lvlOpts[lvlUserInfo])
                && ((now - uData->seen) >= chanserv_conf.info_delay)
                && !uData->present)
                 info = 1;
+            uData->seen = now;
+            uData->present = 1;
         }
     }
+
+    /* If user joining normally (not during burst), apply op or voice,
+     * and send greeting/userinfo as appropriate.
+     */
     if(!user->uplink->burst)
     {
         if(modes)
         {
+            if(modes & MODE_CHANOP)
+                modes &= ~MODE_VOICE;
             change.args[0].mode = modes;
-            change.args[0].member = mNode;
+            change.args[0].u.member = mNode;
             mod_chanmode_announce(chanserv, channel, &change);
         }
-        if(greeting && !user->uplink->burst)
+        if(greeting)
             send_message_type(4, user, chanserv, "(%s) %s", channel->name, greeting);
         if(uData && info)
-            send_target_message(4, channel->name, chanserv, "[%s] %s", user->nick, uData->info);
+            send_target_message(5, channel->name, chanserv, "[%s] %s", user->nick, uData->info);
     }
     return 0;
 }
@@ -5806,7 +6481,7 @@ handle_auth(struct userNode *user, UNUSED_ARG(struct handle_info *old_handle))
     if(!user->handle_info)
        return;
 
-    change.modes_set = change.modes_clear = 0;
+    mod_chanmode_init(&change);
     change.argc = 1;
     for(channel = user->handle_info->channels; channel; channel = channel->u_next)
     {
@@ -5822,8 +6497,9 @@ handle_auth(struct userNode *user, UNUSED_ARG(struct handle_info *old_handle))
         {
             if(!IsUserSuspended(channel)
                && IsUserAutoInvite(channel)
-               && (cn->modes & (MODE_KEY | MODE_INVITEONLY))
-               && !self->burst)
+               && (channel->access >= channel->channel->lvlOpts[lvlInviteMe])
+               && !self->burst
+               && !user->uplink->burst)
                 irc_invite(chanserv, user, cn);
             continue;
         }
@@ -5835,10 +6511,13 @@ handle_auth(struct userNode *user, UNUSED_ARG(struct handle_info *old_handle))
         {
             if(channel->access >= cn->channel_info->lvlOpts[lvlGiveOps])
                 change.args[0].mode = MODE_CHANOP;
-            else
+            else if(channel->access >= cn->channel_info->lvlOpts[lvlGiveVoice])
                 change.args[0].mode = MODE_VOICE;
-            change.args[0].member = mn;
-            mod_chanmode_announce(chanserv, cn, &change);
+            else
+                change.args[0].mode = 0;
+            change.args[0].u.member = mn;
+            if(change.args[0].mode)
+                mod_chanmode_announce(chanserv, cn, &change);
         }
 
        channel->seen = now;
@@ -5851,20 +6530,21 @@ handle_auth(struct userNode *user, UNUSED_ARG(struct handle_info *old_handle))
         struct banData *ban;
 
         if((user->channels.list[ii]->modes & (MODE_CHANOP|MODE_VOICE))
-           || !channel->channel_info)
+           || !channel->channel_info
+           || IsSuspended(channel->channel_info))
             continue;
         for(jj = 0; jj < channel->banlist.used; ++jj)
-            if(user_matches_glob(user, channel->banlist.list[jj]->ban, 1))
+            if(user_matches_glob(user, channel->banlist.list[jj]->ban, MATCH_USENICK))
                 break;
         if(jj < channel->banlist.used)
             continue;
         for(ban = channel->channel_info->bans; ban; ban = ban->next)
         {
             char kick_reason[MAXLEN];
-            if(!user_matches_glob(user, ban->mask, 1))
+            if(!user_matches_glob(user, ban->mask, MATCH_USENICK | MATCH_VISIBLE))
                 continue;
             change.args[0].mode = MODE_BAN;
-            change.args[0].hostmask = ban->mask;
+            change.args[0].u.hostmask = ban->mask;
             mod_chanmode_announce(chanserv, channel, &change);
             sprintf(kick_reason, "(%s) %s", ban->owner, ban->reason);
             KickChannelUser(user, channel, chanserv, kick_reason);
@@ -5887,49 +6567,54 @@ handle_auth(struct userNode *user, UNUSED_ARG(struct handle_info *old_handle))
 }
 
 static void
-handle_part(struct userNode *user, struct chanNode *channel, UNUSED_ARG(const char *reason))
+handle_part(struct modeNode *mn, UNUSED_ARG(const char *reason))
 {
     struct chanData *cData;
     struct userData *uData;
 
-    cData = channel->channel_info;
-    if(!cData || IsSuspended(cData) || IsLocal(user))
+    cData = mn->channel->channel_info;
+    if(!cData || IsSuspended(cData) || IsLocal(mn->user))
         return;
 
-    if((cData->flags & CHANNEL_DYNAMIC_LIMIT) && !channel->join_flooded)
+    if((cData->flags & CHANNEL_DYNAMIC_LIMIT) && !mn->channel->join_flooded)
     {
        /* Allow for a bit of padding so that the limit doesn't
           track the user count exactly, which could get annoying. */
-       if((channel->limit - channel->members.used) > chanserv_conf.adjust_threshold + 5)
+       if((mn->channel->limit - mn->channel->members.used) > chanserv_conf.adjust_threshold + 5)
        {
            timeq_del(0, chanserv_adjust_limit, cData, TIMEQ_IGNORE_WHEN);
            timeq_add(now + chanserv_conf.adjust_delay, chanserv_adjust_limit, cData);
        }
     }
 
-    if((uData = GetTrueChannelAccess(cData, user->handle_info)))
-       scan_user_presence(uData, user);
+    if((uData = GetTrueChannelAccess(cData, mn->user->handle_info)))
+    {
+       scan_user_presence(uData, mn->user);
+        uData->seen = now;
+        if (uData->access >= UL_PRESENT)
+            cData->visited = now;
+    }
 
-    if(IsHelping(user) && IsSupportHelper(user))
+    if(IsHelping(mn->user) && IsSupportHelper(mn->user))
     {
         unsigned int ii, jj;
         for(ii = 0; ii < chanserv_conf.support_channels.used; ++ii)
         {
-            for(jj = 0; jj < user->channels.used; ++jj)
-                if(user->channels.list[jj]->channel == chanserv_conf.support_channels.list[ii])
+            for(jj = 0; jj < mn->user->channels.used; ++jj)
+                if(mn->user->channels.list[jj]->channel == chanserv_conf.support_channels.list[ii])
                     break;
-            if(jj < user->channels.used)
+            if(jj < mn->user->channels.used)
                 break;
         }
         if(ii == chanserv_conf.support_channels.used)
-            HANDLE_CLEAR_FLAG(user->handle_info, HELPING);
+            HANDLE_CLEAR_FLAG(mn->user->handle_info, HELPING);
     }
 }
 
 static void
 handle_kick(struct userNode *kicker, struct userNode *victim, struct chanNode *channel)
 {
-    char *reason = "CSMSG_USER_PROTECTED";
+    struct userData *uData;
 
     if(!channel->channel_info || !kicker || IsService(kicker)
        || (kicker == victim) || IsSuspended(channel->channel_info)
@@ -5937,7 +6622,13 @@ handle_kick(struct userNode *kicker, struct userNode *victim, struct chanNode *c
         return;
 
     if(protect_user(victim, kicker, channel->channel_info))
+    {
+        const char *reason = user_find_message(kicker, "CSMSG_USER_PROTECTED");
        KickChannelUser(kicker, channel, chanserv, reason);
+    }
+
+    if((uData = GetTrueChannelAccess(channel->channel_info, victim->handle_info)))
+        uData->seen = now;
 }
 
 static int
@@ -5945,7 +6636,8 @@ handle_topic(struct userNode *user, struct chanNode *channel, const char *old_to
 {
     struct chanData *cData;
 
-    if(!channel->channel_info || !user || IsSuspended(channel->channel_info) || IsService(user)) return 0;
+    if(!channel->channel_info || !user || IsSuspended(channel->channel_info) || IsService(user))
+        return 0;
 
     cData = channel->channel_info;
     if(bad_topic(channel, user, channel->topic))
@@ -5958,7 +6650,7 @@ handle_topic(struct userNode *user, struct chanNode *channel, const char *old_to
         return 1;
     }
     /* With topicsnarf, grab the topic and save it as the default topic. */
-    if(cData->flags & CHANNEL_TOPIC_SNARF)
+    if(check_user_level(channel, user, lvlTopicSnarf, 0, 0))
     {
         free(cData->topic);
         cData->topic = strdup(channel->topic);
@@ -5980,8 +6672,7 @@ handle_mode(struct chanNode *channel, struct userNode *user, const struct mod_ch
        && mode_lock_violated(&channel->channel_info->modes, change))
     {
         char correct[MAXLEN];
-        bounce = mod_chanmode_alloc(change->argc + 1);
-        *bounce = channel->channel_info->modes;
+        bounce = mod_chanmode_dup(&channel->channel_info->modes, change->argc + 1);
         mod_chanmode_format(&channel->channel_info->modes, correct);
         send_message(user, chanserv, "CSMSG_MODE_LOCKED", correct, channel->name);
     }
@@ -5989,7 +6680,7 @@ handle_mode(struct chanNode *channel, struct userNode *user, const struct mod_ch
     {
         if((change->args[ii].mode & (MODE_REMOVE|MODE_CHANOP)) == (MODE_REMOVE|MODE_CHANOP))
         {
-            const struct userNode *victim = change->args[ii].member->user;
+            const struct userNode *victim = change->args[ii].u.member->user;
             if(!protect_user(victim, user, channel->channel_info))
                 continue;
             if(!bounce)
@@ -5997,43 +6688,47 @@ handle_mode(struct chanNode *channel, struct userNode *user, const struct mod_ch
             if(!deopped)
             {
                 bounce->args[bnc].mode = MODE_REMOVE | MODE_CHANOP;
-                bounce->args[bnc].member = GetUserMode(channel, user);
-                if(bounce->args[bnc].member)
+                bounce->args[bnc].u.member = GetUserMode(channel, user);
+                if(bounce->args[bnc].u.member)
                     bnc++;
+                deopped = 1;
             }
             bounce->args[bnc].mode = MODE_CHANOP;
-            bounce->args[bnc].member = change->args[ii].member;
+            bounce->args[bnc].u.member = change->args[ii].u.member;
             bnc++;
             send_message(user, chanserv, "CSMSG_USER_PROTECTED", victim->nick);
         }
         else if(change->args[ii].mode & MODE_CHANOP)
         {
-            const struct userNode *victim = change->args[ii].member->user;
+            const struct userNode *victim = change->args[ii].u.member->user;
             if(IsService(victim) || validate_op(user, channel, (struct userNode*)victim))
                 continue;
             if(!bounce)
                 bounce = mod_chanmode_alloc(change->argc + 1 - ii);
             bounce->args[bnc].mode = MODE_REMOVE | MODE_CHANOP;
-            bounce->args[bnc].member = change->args[ii].member;
+            bounce->args[bnc].u.member = change->args[ii].u.member;
             bnc++;
         }
-        else if(change->args[ii].mode & MODE_BAN)
+        else if((change->args[ii].mode & (MODE_REMOVE | MODE_BAN)) == MODE_BAN)
         {
-            const char *ban = change->args[ii].hostmask;
+            const char *ban = change->args[ii].u.hostmask;
             if(!bad_channel_ban(channel, user, ban, NULL, NULL))
                 continue;
             if(!bounce)
                 bounce = mod_chanmode_alloc(change->argc + 1 - ii);
             bounce->args[bnc].mode = MODE_REMOVE | MODE_BAN;
-            bounce->args[bnc].hostmask = ban;
+            bounce->args[bnc].u.hostmask = strdup(ban);
             bnc++;
             send_message(user, chanserv, "CSMSG_MASK_PROTECTED", ban);
         }
     }
     if(bounce)
     {
-        if((bounce->argc = bnc))
+        if((bounce->argc = bnc) || bounce->modes_set || bounce->modes_clear)
             mod_chanmode_announce(chanserv, channel, bounce);
+        for(ii = 0; ii < change->argc; ++ii)
+            if(bounce->args[ii].mode == (MODE_REMOVE | MODE_BAN))
+                free((char*)bounce->args[ii].u.hostmask);
         mod_chanmode_free(bounce);
     }
 }
@@ -6047,7 +6742,7 @@ handle_nick_change(struct userNode *user, UNUSED_ARG(const char *old_nick))
     unsigned int ii, jj;
     char kick_reason[MAXLEN];
 
-    change.modes_set = change.modes_clear = 0;
+    mod_chanmode_init(&change);
     change.argc = 1;
     change.args[0].mode = MODE_BAN;
     for(ii = 0; ii < user->channels.used; ++ii)
@@ -6061,7 +6756,7 @@ handle_nick_change(struct userNode *user, UNUSED_ARG(const char *old_nick))
             continue;
         /* Look for a matching ban already on the channel. */
         for(jj = 0; jj < channel->banlist.used; ++jj)
-            if(user_matches_glob(user, channel->banlist.list[jj]->ban, 1))
+            if(user_matches_glob(user, channel->banlist.list[jj]->ban, MATCH_USENICK))
                 break;
         /* Need not act if we found one. */
         if(jj < channel->banlist.used)
@@ -6069,9 +6764,9 @@ handle_nick_change(struct userNode *user, UNUSED_ARG(const char *old_nick))
         /* Look for a matching ban in this channel. */
         for(bData = channel->channel_info->bans; bData; bData = bData->next)
         {
-            if(!user_matches_glob(user, bData->mask, 1))
+            if(!user_matches_glob(user, bData->mask, MATCH_USENICK | MATCH_VISIBLE))
                 continue;
-            change.args[0].hostmask = bData->mask;
+            change.args[0].u.hostmask = bData->mask;
             mod_chanmode_announce(chanserv, channel, &change);
             sprintf(kick_reason, "(%s) %s", bData->owner, bData->reason);
             KickChannelUser(user, channel, chanserv, kick_reason);
@@ -6174,7 +6869,7 @@ chanserv_conf_read(void)
     str = database_get_data(conf_node, KEY_INFO_DELAY, RECDB_QSTRING);
     chanserv_conf.info_delay = str ? ParseInterval(str) : 180;
     str = database_get_data(conf_node, KEY_MAX_GREETLEN, RECDB_QSTRING);
-    chanserv_conf.greeting_length = str ? atoi(str) : 120;
+    chanserv_conf.greeting_length = str ? atoi(str) : 200;
     str = database_get_data(conf_node, KEY_ADJUST_THRESHOLD, RECDB_QSTRING);
     chanserv_conf.adjust_threshold = str ? atoi(str) : 15;
     str = database_get_data(conf_node, KEY_ADJUST_DELAY, RECDB_QSTRING);
@@ -6183,17 +6878,23 @@ chanserv_conf_read(void)
     chanserv_conf.channel_expire_frequency = str ? ParseInterval(str) : 86400;
     str = database_get_data(conf_node, KEY_CHAN_EXPIRE_DELAY, RECDB_QSTRING);
     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_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.max_chan_users = str ? atoi(str) : 512;
     str = database_get_data(conf_node, KEY_MAX_CHAN_BANS, RECDB_QSTRING);
     chanserv_conf.max_chan_bans = str ? atoi(str) : 512;
+    str = database_get_data(conf_node, KEY_MAX_USERINFO_LENGTH, RECDB_QSTRING);
+    chanserv_conf.max_userinfo_length = str ? atoi(str) : 400;
     str = database_get_data(conf_node, KEY_NICK, RECDB_QSTRING);
     if(chanserv && str)
         NickChange(chanserv, str, 0);
     str = database_get_data(conf_node, KEY_REFRESH_PERIOD, RECDB_QSTRING);
     chanserv_conf.refresh_period = str ? ParseInterval(str) : 3*60*60;
+    str = database_get_data(conf_node, KEY_GIVEOWNERSHIP_PERIOD, RECDB_QSTRING);
+    chanserv_conf.giveownership_period = str ? ParseInterval(str) : 0;
     str = database_get_data(conf_node, KEY_CTCP_SHORT_BAN_DURATION, RECDB_QSTRING);
     chanserv_conf.ctcp_short_ban_duration = str ? str : "3m";
     str = database_get_data(conf_node, KEY_CTCP_LONG_BAN_DURATION, RECDB_QSTRING);
@@ -6211,7 +6912,8 @@ chanserv_conf_read(void)
         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)) && (change->argc < 2))
+    if((change = mod_chanmode_parse(NULL, modes, ii, MCP_KEY_FREE, 0))
+       && (change->argc < 2))
     {
         chanserv_conf.default_modes = *change;
         mod_chanmode_free(change);
@@ -6223,14 +6925,18 @@ chanserv_conf_read(void)
     else
     {
         static const char *list[] = {
-            /* multiple choice options */
+            /* free form text */
             "DefaultTopic", "TopicMask", "Greeting", "UserGreeting", "Modes",
-            "PubCmd", "GiveOps", "EnfOps", "EnfModes", "EnfTopic", "Protect",
-            "Toys", "Setters", "TopicRefresh", "CtcpUsers", "CtcpReaction",
+            /* options based on user level */
+            "PubCmd", "InviteMe", "UserInfo", "GiveVoice", "GiveOps", "EnfOps",
+            "EnfModes", "EnfTopic", "TopicSnarf", "Setters", "CtcpUsers",
+            /* multiple choice options */
+            "CtcpReaction", "Protect", "Toys", "TopicRefresh",
             /* binary options */
-            "Voice", "UserInfo", "DynLimit", "TopicSnarf", "PeonInvite", "NoDelete",
+            "DynLimit", "NoDelete",
             /* delimiter */
-            NULL };
+            NULL
+        };
         unsigned int ii;
         strlist = alloc_string_list(ArrayLength(list)-1);
         for(ii=0; list[ii]; ii++)
@@ -6258,11 +6964,13 @@ chanserv_conf_read(void)
     chanserv_conf.eightball = strlist;
     free_string_list(chanserv_conf.old_ban_names);
     strlist = database_get_data(conf_node, KEY_OLD_BAN_NAMES, RECDB_STRING_LIST);
-    if (strlist)
+    if(strlist)
         strlist = string_list_copy(strlist);
     else
         strlist = alloc_string_list(2);
     chanserv_conf.old_ban_names = strlist;
+    str = database_get_data(conf_node, "off_channel", RECDB_QSTRING);
+    off_channel = str ? atoi(str) : 0;
 }
 
 static void
@@ -6377,6 +7085,8 @@ ban_read_helper(const char *key, struct record_data *rd, struct chanData *chan)
     s_expires = database_get_data(rd->d.object, KEY_EXPIRES, RECDB_QSTRING);
     owner = database_get_data(rd->d.object, KEY_OWNER, RECDB_QSTRING);
     reason = database_get_data(rd->d.object, KEY_REASON, RECDB_QSTRING);
+    if (!reason || !owner)
+        return;
 
     set_time = set ? (time_t)strtoul(set, NULL, 0) : now;
     triggered_time = triggered ? (time_t)strtoul(triggered, NULL, 0) : 0;
@@ -6387,7 +7097,7 @@ ban_read_helper(const char *key, struct record_data *rd, struct chanData *chan)
     else
         expires_time = 0;
 
-    if(expires_time && (expires_time < now))
+    if(!reason || (expires_time && (expires_time < now)))
         return;
 
     bData = add_channel_ban(chan, key, owner, set_time, triggered_time, expires_time, reason);
@@ -6447,20 +7157,30 @@ chanserv_channel_read(const char *key, struct record_data *hir)
     {
         enum levelOption lvlOpt;
         enum charOption chOpt;
+
+        if((str = database_get_data(obj, KEY_FLAGS, RECDB_QSTRING)))
+            cData->flags = atoi(str);
+
         for(lvlOpt = 0; lvlOpt < NUM_LEVEL_OPTIONS; ++lvlOpt)
         {
-            if(!(str = database_get_data(obj, levelOptions[lvlOpt].db_name, RECDB_QSTRING)))
-                continue;
-            cData->lvlOpts[lvlOpt] = user_level_from_name(str, UL_OWNER+1);
+            str = database_get_data(obj, levelOptions[lvlOpt].db_name, RECDB_QSTRING);
+            if(str)
+                cData->lvlOpts[lvlOpt] = user_level_from_name(str, UL_OWNER+1);
+            else if(levelOptions[lvlOpt].old_flag)
+            {
+                if(cData->flags & levelOptions[lvlOpt].old_flag)
+                    cData->lvlOpts[lvlOpt] = levelOptions[lvlOpt].flag_value;
+                else
+                    cData->lvlOpts[lvlOpt] = levelOptions[lvlOpt].default_value;
+            }
         }
+
         for(chOpt = 0; chOpt < NUM_CHAR_OPTIONS; ++chOpt)
         {
             if(!(str = database_get_data(obj, charOptions[chOpt].db_name, RECDB_QSTRING)))
                 continue;
             cData->chOpts[chOpt] = str[0];
         }
-        if((str = database_get_data(obj, KEY_FLAGS, RECDB_QSTRING)))
-            cData->flags = atoi(str);
     }
     else if((str = database_get_data(channel, KEY_FLAGS, RECDB_QSTRING)))
     {
@@ -6473,7 +7193,14 @@ chanserv_channel_read(const char *key, struct record_data *hir)
         for(lvlOpt = 0; lvlOpt < NUM_LEVEL_OPTIONS; ++lvlOpt)
         {
             unsigned short lvl;
-            switch(((count <= levelOptions[lvlOpt].old_idx) ? str : CHANNEL_DEFAULT_OPTIONS)[levelOptions[lvlOpt].old_idx])
+            if(levelOptions[lvlOpt].old_flag)
+            {
+                if(cData->flags & levelOptions[lvlOpt].old_flag)
+                    lvl = levelOptions[lvlOpt].flag_value;
+                else
+                    lvl = levelOptions[lvlOpt].default_value;
+            }
+            else switch(((count <= levelOptions[lvlOpt].old_idx) ? str : CHANNEL_DEFAULT_OPTIONS)[levelOptions[lvlOpt].old_idx])
             {
             case 'c': lvl = UL_COOWNER; break;
             case 'm': lvl = UL_MASTER; break;
@@ -6497,14 +7224,14 @@ chanserv_channel_read(const char *key, struct record_data *hir)
         /* We could use suspended->expires and suspended->revoked to
          * set the CHANNEL_SUSPENDED flag, but we don't. */
     }
-    else if(cData->flags & CHANNEL_SUSPENDED)
+    else if(IsSuspended(cData) && (str = database_get_data(hir->d.object, KEY_SUSPENDER, RECDB_QSTRING)))
     {
         suspended = calloc(1, sizeof(*suspended));
         suspended->issued = 0;
         suspended->revoked = 0;
+        suspended->suspender = strdup(str);
         str = database_get_data(hir->d.object, KEY_SUSPEND_EXPIRES, RECDB_QSTRING);
         suspended->expires = str ? atoi(str) : 0;
-        suspended->suspender = strdup(database_get_data(hir->d.object, KEY_SUSPENDER, RECDB_QSTRING));
         str = database_get_data(hir->d.object, KEY_SUSPEND_REASON, RECDB_QSTRING);
         suspended->reason = strdup(str ? str : "No reason");
         suspended->previous = NULL;
@@ -6512,33 +7239,33 @@ chanserv_channel_read(const char *key, struct record_data *hir)
         suspended->cData = cData;
     }
     else
-        suspended = NULL;
-
-    if((cData->flags & CHANNEL_SUSPENDED)
-       && suspended->expires
-       && (suspended->expires <= now))
     {
         cData->flags &= ~CHANNEL_SUSPENDED;
+        suspended = NULL; /* to squelch a warning */
     }
 
-    if(!(cData->flags & CHANNEL_SUSPENDED))
-    {
+    if(IsSuspended(cData)) {
+        if(suspended->expires > now)
+            timeq_add(suspended->expires, chanserv_expire_suspension, suspended);
+        else if(suspended->expires)
+            cData->flags &= ~CHANNEL_SUSPENDED;
+    }
+
+    if((!off_channel || !IsOffChannel(cData)) && !IsSuspended(cData)) {
         struct mod_chanmode change;
-        change.modes_set = change.modes_clear = 0;
+        mod_chanmode_init(&change);
         change.argc = 1;
         change.args[0].mode = MODE_CHANOP;
-        change.args[0].member = AddChannelUser(chanserv, cNode);
+        change.args[0].u.member = AddChannelUser(chanserv, cNode);
         mod_chanmode_announce(chanserv, cNode, &change);
     }
-    else if(suspended->expires > now)
-    {
-        timeq_add(suspended->expires, chanserv_expire_suspension, suspended);
-    }
 
     str = database_get_data(channel, KEY_REGISTERED, RECDB_QSTRING);
     cData->registered = str ? (time_t)strtoul(str, NULL, 0) : now;
     str = database_get_data(channel, KEY_VISITED, RECDB_QSTRING);
     cData->visited = str ? (time_t)strtoul(str, NULL, 0) : now;
+    str = database_get_data(channel, KEY_OWNER_TRANSFER, RECDB_QSTRING);
+    cData->ownerTransfer = str ? (time_t)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_GREETING, RECDB_QSTRING);
@@ -6550,14 +7277,16 @@ chanserv_channel_read(const char *key, struct record_data *hir)
     str = database_get_data(channel, KEY_TOPIC, RECDB_QSTRING);
     cData->topic = str ? strdup(str) : NULL;
 
-    if((str = database_get_data(channel, KEY_MODES, RECDB_QSTRING))
+    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))) {
+       && (modes = mod_chanmode_parse(cNode, argv, argc, MCP_KEY_FREE, 0))) {
         cData->modes = *modes;
+       if(off_channel > 0)
+          cData->modes.modes_set |= MODE_REGISTERED;
         if(cData->modes.argc > 1)
             cData->modes.argc = 1;
-        if(!IsSuspended(cData))
-            mod_chanmode_announce(chanserv, cNode, &cData->modes);
+        mod_chanmode_announce(chanserv, cNode, &cData->modes);
         mod_chanmode_free(modes);
     }
 
@@ -6611,6 +7340,7 @@ chanserv_dnr_read(const char *key, struct record_data *hir)
 {
     const char *setter, *reason, *str;
     struct do_not_register *dnr;
+    time_t expiry;
 
     setter = database_get_data(hir->d.object, KEY_DNR_SETTER, RECDB_QSTRING);
     if(!setter)
@@ -6624,7 +7354,11 @@ chanserv_dnr_read(const char *key, struct record_data *hir)
         log_module(CS_LOG, LOG_ERROR, "Missing reason for DNR %s.", key);
         return;
     }
-    dnr = chanserv_add_dnr(key, setter, reason);
+    str = database_get_data(hir->d.object, KEY_EXPIRES, RECDB_QSTRING);
+    expiry = str ? (time_t)strtoul(str, NULL, 0) : 0;
+    if(expiry && expiry <= now)
+        return;
+    dnr = chanserv_add_dnr(key, setter, expiry, reason);
     if(!dnr)
         return;
     str = database_get_data(hir->d.object, KEY_DNR_SET, RECDB_QSTRING);
@@ -6779,6 +7513,8 @@ chanserv_write_channel(struct saxdb_context *ctx, struct chanData *channel)
         saxdb_end_record(ctx);
     }
 
+    if(channel->ownerTransfer)
+        saxdb_write_int(ctx, KEY_OWNER_TRANSFER, channel->ownerTransfer);
     saxdb_write_int(ctx, KEY_VISITED, high_present ? now : channel->visited);
     saxdb_end_record(ctx);
 }
@@ -6816,14 +7552,22 @@ static void
 write_dnrs_helper(struct saxdb_context *ctx, struct dict *dnrs)
 {
     struct do_not_register *dnr;
-    dict_iterator_t it;
+    dict_iterator_t it, next;
 
-    for(it = dict_first(dnrs); it; it = iter_next(it))
+    for(it = dict_first(dnrs); it; it = next)
     {
+        next = iter_next(it);
         dnr = iter_data(it);
+        if(dnr->expires && dnr->expires <= now)
+        {
+            dict_remove(dnrs, iter_key(it));
+            continue;
+        }
         saxdb_start_record(ctx, dnr->chan_name, 0);
         if(dnr->set)
             saxdb_write_int(ctx, KEY_DNR_SET, dnr->set);
+        if(dnr->expires)
+            saxdb_write_int(ctx, KEY_EXPIRES, dnr->expires);
         saxdb_write_string(ctx, KEY_DNR_SETTER, dnr->setter);
         saxdb_write_string(ctx, KEY_DNR_REASON, dnr->reason);
         saxdb_end_record(ctx);
@@ -6894,17 +7638,19 @@ init_chanserv(const char *nick)
     CS_LOG = log_register_type("ChanServ", "file:chanserv.log");
     conf_register_reload(chanserv_conf_read);
 
-    reg_server_link_func(handle_server_link);
-
-    reg_new_channel_func(handle_new_channel);
-    reg_join_func(handle_join);
-    reg_part_func(handle_part);
-    reg_kick_func(handle_kick);
-    reg_topic_func(handle_topic);
-    reg_mode_change_func(handle_mode);
-    reg_nick_change_func(handle_nick_change);
+    if(nick)
+    {
+        reg_server_link_func(handle_server_link);
+        reg_new_channel_func(handle_new_channel);
+        reg_join_func(handle_join);
+        reg_part_func(handle_part);
+        reg_kick_func(handle_kick);
+        reg_topic_func(handle_topic);
+        reg_mode_change_func(handle_mode);
+        reg_nick_change_func(handle_nick_change);
+        reg_auth_func(handle_auth);
+    }
 
-    reg_auth_func(handle_auth);
     reg_handle_rename_func(handle_rename);
     reg_unreg_func(handle_unreg);
 
@@ -6920,6 +7666,9 @@ init_chanserv(const char *nick)
     DEFINE_COMMAND(register, 1, MODCMD_REQUIRE_AUTHED, "flags", "+acceptchan,+helping", NULL);
     DEFINE_COMMAND(noregister, 1, MODCMD_REQUIRE_AUTHED, "flags", "+helping", NULL);
     DEFINE_COMMAND(allowregister, 2, 0, "template", "noregister", NULL);
+    DEFINE_COMMAND(dnrsearch, 3, 0, "template", "noregister", NULL);
+    modcmd_register(chanserv_module, "dnrsearch print", NULL, 0, 0, NULL);
+    modcmd_register(chanserv_module, "dnrsearch remove", NULL, 0, 0, NULL);
     DEFINE_COMMAND(move, 1, MODCMD_REQUIRE_AUTHED|MODCMD_REQUIRE_REGCHAN, "template", "register", NULL);
     DEFINE_COMMAND(csuspend, 2, MODCMD_REQUIRE_AUTHED|MODCMD_REQUIRE_REGCHAN, "flags", "+helping", NULL);
     DEFINE_COMMAND(cunsuspend, 1, MODCMD_REQUIRE_AUTHED|MODCMD_REQUIRE_REGCHAN, "flags", "+helping", NULL);
@@ -6942,7 +7691,7 @@ init_chanserv(const char *nick)
     DEFINE_COMMAND(mdelpeon, 2, MODCMD_REQUIRE_CHANUSER, "access", "master", NULL);
 
     DEFINE_COMMAND(trim, 3, MODCMD_REQUIRE_CHANUSER, "access", "master", NULL);
-    DEFINE_COMMAND(opchan, 1, MODCMD_REQUIRE_REGCHAN, "access", "peon", NULL);
+    DEFINE_COMMAND(opchan, 1, MODCMD_REQUIRE_REGCHAN|MODCMD_NEVER_CSUSPEND, "access", "1", NULL);
     DEFINE_COMMAND(clvl, 3, MODCMD_REQUIRE_CHANUSER, "access", "master", NULL);
     DEFINE_COMMAND(giveownership, 2, MODCMD_REQUIRE_CHANUSER, "access", "owner", "flags", "+loghostmask", NULL);
 
@@ -6964,7 +7713,7 @@ init_chanserv(const char *nick)
     DEFINE_COMMAND(open, 1, MODCMD_REQUIRE_CHANUSER, "template", "op", NULL);
     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", "peon", NULL);
+    DEFINE_COMMAND(inviteme, 1, MODCMD_REQUIRE_CHANNEL, "access", "1", 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);
@@ -6974,12 +7723,13 @@ init_chanserv(const char *nick)
     DEFINE_COMMAND(addban, 2, MODCMD_REQUIRE_REGCHAN, "access", "250", NULL);
     DEFINE_COMMAND(addtimedban, 3, MODCMD_REQUIRE_REGCHAN, "access", "250", NULL);
     DEFINE_COMMAND(delban, 2, MODCMD_REQUIRE_REGCHAN, "access", "250", NULL);
-    DEFINE_COMMAND(uset, 1, MODCMD_REQUIRE_CHANUSER, "access", "peon", NULL);
+    DEFINE_COMMAND(uset, 1, MODCMD_REQUIRE_CHANUSER, "access", "1", NULL);
 
-    DEFINE_COMMAND(bans, 1, MODCMD_REQUIRE_REGCHAN, "access", "peon", "flags", "+nolog", NULL);
+    DEFINE_COMMAND(bans, 1, MODCMD_REQUIRE_REGCHAN, "access", "1", "flags", "+nolog", NULL);
     DEFINE_COMMAND(peek, 1, MODCMD_REQUIRE_REGCHAN, "access", "op", "flags", "+nolog", NULL);
 
-    DEFINE_COMMAND(access, 1, 0, "flags", "+nolog,+acceptchan", NULL);
+    DEFINE_COMMAND(myaccess, 1, MODCMD_REQUIRE_AUTHED, NULL);
+    DEFINE_COMMAND(access, 1, MODCMD_REQUIRE_REGCHAN, "flags", "+nolog,+joinable", NULL);
     DEFINE_COMMAND(users, 1, MODCMD_REQUIRE_REGCHAN, "flags", "+nolog,+joinable", NULL);
     DEFINE_COMMAND(wlist, 1, MODCMD_REQUIRE_REGCHAN, "flags", "+nolog,+joinable", NULL);
     DEFINE_COMMAND(clist, 1, MODCMD_REQUIRE_REGCHAN, "flags", "+nolog,+joinable", NULL);
@@ -7023,7 +7773,7 @@ init_chanserv(const char *nick)
     DEFINE_CHANNEL_OPTION(enfmodes);
     DEFINE_CHANNEL_OPTION(enftopic);
     DEFINE_CHANNEL_OPTION(pubcmd);
-    DEFINE_CHANNEL_OPTION(voice);
+    DEFINE_CHANNEL_OPTION(givevoice);
     DEFINE_CHANNEL_OPTION(userinfo);
     DEFINE_CHANNEL_OPTION(dynlimit);
     DEFINE_CHANNEL_OPTION(topicsnarf);
@@ -7033,7 +7783,12 @@ init_chanserv(const char *nick)
     DEFINE_CHANNEL_OPTION(topicrefresh);
     DEFINE_CHANNEL_OPTION(ctcpusers);
     DEFINE_CHANNEL_OPTION(ctcpreaction);
-    DEFINE_CHANNEL_OPTION(peoninvite);
+    DEFINE_CHANNEL_OPTION(inviteme);
+    DEFINE_CHANNEL_OPTION(unreviewed);
+    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)
+        DEFINE_CHANNEL_OPTION(offchannel);
     modcmd_register(chanserv_module, "set defaults", chan_opt_defaults, 1, 0, "access", "owner", NULL);
 
     /* Alias set topic to set defaulttopic for compatibility. */
@@ -7051,8 +7806,9 @@ init_chanserv(const char *nick)
     dict_set_free_data(note_types, chanserv_deref_note_type);
     if(nick)
     {
-        chanserv = AddService(nick, "Channel Services");
-        service_register(chanserv, '!');
+        const char *modes = conf_get_data("services/chanserv/modes", RECDB_QSTRING);
+        chanserv = AddLocalUser(nick, nick, NULL, "Channel Services", modes);
+        service_register(chanserv)->trigger = '!';
         reg_chanmsg_func('\001', chanserv, chanserv_ctcp_check);
     }
     saxdb_register("ChanServ", chanserv_saxdb_read, chanserv_saxdb_write);
@@ -7060,13 +7816,16 @@ init_chanserv(const char *nick)
     if(chanserv_conf.channel_expire_frequency)
        timeq_add(now + chanserv_conf.channel_expire_frequency, expire_channels, NULL);
 
+    if(chanserv_conf.dnr_expire_frequency)
+        timeq_add(now + chanserv_conf.dnr_expire_frequency, expire_dnrs, NULL);
+
     if(chanserv_conf.refresh_period)
     {
         time_t next_refresh;
         next_refresh = (now + chanserv_conf.refresh_period - 1) / chanserv_conf.refresh_period * chanserv_conf.refresh_period;
         timeq_add(next_refresh, chanserv_refresh_topics, NULL);
     }
-    
+
     reg_exit_func(chanserv_db_cleanup);
     message_register_table(msgtab);
 }