changed spamserv,chanserv and watchdog messages to privmsgs
[srvx.git] / src / chanserv.c
index 75122799b0d28a6e729cea46cbe3d51f9eb3652a..44f6d9d5f1488f88c0b24d06260e0fce960e1eb9 100644 (file)
@@ -43,6 +43,7 @@
 #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_MIN_TIME_BANS                      "min_time_bans"
 #define KEY_NICK                    "nick"
 #define KEY_OLD_CHANSERV_NAME       "old_chanserv_name"
 #define KEY_8BALL_RESPONSES         "8ball"
@@ -58,6 +59,7 @@
 #define KEY_MAX_USERINFO_LENGTH     "max_userinfo_length"
 #define KEY_GIVEOWNERSHIP_PERIOD    "giveownership_timeout"
 #define KEY_INVITED_INTERVAL           "invite_timeout"
+#define KEY_REVOKE_MODE_A           "revoke_mode_a"
 #define KEY_NEW_CHANNEL_AUTHED      "new_channel_authed_join"
 #define KEY_NEW_CHANNEL_UNAUTHED    "new_channel_unauthed_join"
 #define KEY_NEW_CHANNEL_MSG         "new_channel_message"
 #define KEY_REVOKED         "revoked"
 #define KEY_SUSPEND_EXPIRES "suspend_expires"
 #define KEY_SUSPEND_REASON  "suspend_reason"
+#define KEY_GIVEOWNERSHIP   "giveownership"
+#define KEY_STAFF_ISSUER    "staff_issuer"
+#define KEY_OLD_OWNER       "old_owner"
+#define KEY_TARGET          "target"
+#define KEY_TARGET_ACCESS   "target_access"
 #define KEY_VISITED         "visited"
 #define KEY_TOPIC           "topic"
 #define KEY_GREETING        "greeting"
 #define KEY_MAX_TIME        "max_time"
 #define KEY_NOTES           "notes"
 #define KEY_TOPIC_MASK      "topic_mask"
+#define KEY_ADVTOPIC_ENTRIES "adv_topic"
 #define KEY_OWNER_TRANSFER  "owner_transfer"
 #define KEY_EXPIRE          "expire"
 
@@ -148,6 +156,7 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_MUST_BE_OPPED", "You must be a channel operator in $b%s$b to register it." },
     { "CSMSG_PROXY_FORBIDDEN", "You may not register a channel for someone else." },
     { "CSMSG_OWN_TOO_MANY", "%s already owns enough channels (at least %d); use FORCE to override." },
+    { "CMSG_ALERT_REGISTERED" "%s registered to %s by %s." },
 
 /* Do-not-register channels */
     { "CSMSG_NOT_DNR", "$b%s$b is not a valid channel name or *account." },
@@ -170,6 +179,7 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_UNREG_NODELETE", "$b%s$b is protected from unregistration." },
     { "CSMSG_CHAN_SUSPENDED", "$b$C$b access to $b%s$b has been temporarily suspended (%s)." },
     { "CSMSG_CONFIRM_UNREG", "To confirm this unregistration, you must use 'unregister %s'." },
+    { "CMSG_ALERT_UNREGISTERED" "%s %s" },
 
 /* Channel moving */
     { "CSMSG_MOVE_SUCCESS", "Channel registration has been moved to $b%s$b." },
@@ -256,6 +266,7 @@ static const struct message_entry msgtab[] = {
     { "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." },
+    { "CSMSG_ADVTOPIC_INVALID_ID", "%d is an invalid advtopic id." },
 
     { "CSMSG_MODES_SET", "Channel modes are now $b%s$b." },
     { "CSMSG_DEFAULTED_MODES", "Channel modes for $b%s$b are set to their defaults." },
@@ -273,6 +284,7 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_INVALID_NUMERIC",   "$b%d$b is not a valid choice.  Choose one:" },
     { "CSMSG_SET_DEFAULT_TOPIC", "$bDefaultTopic$b %s" },
     { "CSMSG_SET_TOPICMASK",     "$bTopicMask   $b %s" },
+    { "CSMSG_SET_ADVTOPIC",      "$bAdvTopic    $b %s" },
     { "CSMSG_SET_GREETING",      "$bGreeting    $b %s" },
     { "CSMSG_SET_USERGREETING",  "$bUserGreeting$b %s" },
     { "CSMSG_SET_MODES",         "$bModes       $b %s" },
@@ -426,6 +438,11 @@ static const struct message_entry msgtab[] = {
     { "CSMSG_CHANNEL_REGISTERED", "$bRegistered:          $b%s ago." },
     { "CSMSG_CHANNEL_VISITED", "$bVisited:             $b%s ago." },
 
+    { "CSMSG_CHANNEL_OWNERSHIP_HISTORY", "Ownership transfer history for %s" },
+    { "CSMSG_CHANNEL_OWNERSHIP_STAFF_REASON", " from %s to %s (%d access) by %s on %s (Reason: %s)" },
+    { "CSMSG_CHANNEL_OWNERSHIP_STAFF", " from %s to %s (%d access) by %s on %s" },
+    { "CSMSG_CHANNEL_OWNERSHIP_NORMAL", " from %s to %s (%d access) on %s" },
+
     { "CSMSG_PEEK_INFO", "$b%s$b Status:" },
     { "CSMSG_PEEK_TOPIC", "$bTopic:          $b%s" },
     { "CSMSG_PEEK_MODES", "$bModes:          $b%s" },
@@ -533,6 +550,9 @@ static const struct message_entry msgtab[] = {
 DECLARE_LIST(dnrList, struct do_not_register *);
 DEFINE_LIST(dnrList, struct do_not_register *)
 
+#define chanserv_notice(target, format...) send_message(target , chanserv , ## format)
+#define chanserv_oper_message(format...) do { if(chanserv_conf.oper_channel) send_channel_message(chanserv_conf.oper_channel , chanserv , ## format); } while(0)
+
 static int eject_user(struct userNode *user, struct chanNode *channel, unsigned int argc, char *argv[], struct svccmd *cmd, int action);
 
 struct userNode *chanserv;
@@ -567,7 +587,10 @@ static struct
     unsigned int    max_owned;
     unsigned int    max_chan_users;
     unsigned int    max_chan_bans;
+    unsigned int    min_time_bans;
     unsigned int    max_userinfo_length;
+    
+    unsigned int    revoke_mode_a;
 
     struct string_list  *set_shows;
     struct string_list  *eightball;
@@ -583,6 +606,8 @@ static struct
     const char          *new_channel_authed;
     const char          *new_channel_unauthed;
     const char          *new_channel_msg;
+
+    struct chanNode    *oper_channel;
 } chanserv_conf;
 
 struct listData
@@ -1216,6 +1241,7 @@ register_channel(struct chanNode *cNode, char *registrar)
     struct chanData *channel;
     enum levelOption lvlOpt;
     enum charOption chOpt;
+    int i;
 
     channel = calloc(1, sizeof(struct chanData));
 
@@ -1232,6 +1258,8 @@ register_channel(struct chanNode *cNode, char *registrar)
         channel->lvlOpts[lvlOpt] = levelOptions[lvlOpt].default_value;
     for(chOpt = 0; chOpt < NUM_CHAR_OPTIONS; ++chOpt)
         channel->chOpts[chOpt] = charOptions[chOpt].default_value;
+    for(i = 0; i < MAXADVTOPICENTRIES; i++) 
+        channel->advtopic[i] = NULL;
 
     channel->prev = NULL;
     channel->next = channelList;
@@ -1423,6 +1451,7 @@ unregister_channel(struct chanData *channel, const char *reason)
 {
     struct mod_chanmode change;
     char msgbuf[MAXLEN];
+    int i;
 
     /* After channel unregistration, the following must be cleaned
        up:
@@ -1438,11 +1467,13 @@ 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);
+    if(off_channel > 0 || chanserv_conf.revoke_mode_a) {
+        mod_chanmode_init(&change);
+        if(off_channel > 0)
+            change.modes_clear |= MODE_REGISTERED;
+        if(chanserv_conf.revoke_mode_a)
+            change.modes_clear |= MODE_ACCESS;
+        mod_chanmode_announce(chanserv, channel->channel, &change);
     }
 
     while(channel->users)
@@ -1456,6 +1487,11 @@ unregister_channel(struct chanData *channel, const char *reason)
     free(channel->greeting);
     free(channel->user_greeting);
     free(channel->topic_mask);
+    
+    for(i = 0; i < MAXADVTOPICENTRIES; i++) {
+        if(channel->advtopic[i]) 
+            free(channel->advtopic[i]);
+    }
 
     if(channel->prev)
         channel->prev->next = channel->next;
@@ -1488,10 +1524,9 @@ unregister_channel(struct chanData *channel, const char *reason)
     channel->channel->channel_info = NULL;
 
     dict_delete(channel->notes);
-    sprintf(msgbuf, "%s %s", channel->channel->name, reason);
     if(!IsSuspended(channel))
         DelChannelUser(chanserv, channel->channel, msgbuf, 0);
-    global_message(MESSAGE_RECIPIENT_OPERS | MESSAGE_RECIPIENT_HELPERS, msgbuf);
+    chanserv_oper_message(CSMSGL_CHANNEL_UNREGISTERED, channel->channel->name, reason);
     UnlockChannel(channel->channel);
     free(channel);
     registered_channels--;
@@ -2242,8 +2277,7 @@ static CHANSERV_FUNC(cmd_register)
     else
         reply("CSMSG_REG_SUCCESS", channel->name);
 
-    sprintf(reason, "%s registered to %s by %s.", channel->name, handle->handle, user->handle_info->handle);
-    global_message(MESSAGE_RECIPIENT_OPERS | MESSAGE_RECIPIENT_HELPERS, reason);
+    chanserv_oper_message(CSMSG_ALERT_REGISTERED, channel->name, handle->handle, user->handle_info->handle);
     return 1;
 }
 
@@ -3449,7 +3483,7 @@ eject_user(struct userNode *user, struct chanNode *channel, unsigned int argc, c
         {
             duration = ParseInterval(argv[2]);
 
-            if(duration < 15)
+            if(duration < chanserv_conf.min_time_bans)
             {
                 reply("CSMSG_DURATION_TOO_LOW");
                 free(ban);
@@ -4140,6 +4174,8 @@ cmd_list_users(struct userNode *user, struct chanNode *channel, unsigned int arg
         ary[1] = uData->handle->handle;
         if(uData->present)
             ary[2] = "Here";
+        else if(HANDLE_FLAGGED(uData->handle, NETWORK))
+             ary[2] = "Here";
         else if(!uData->seen)
             ary[2] = "Never";
         else
@@ -4147,6 +4183,12 @@ cmd_list_users(struct userNode *user, struct chanNode *channel, unsigned int arg
         ary[2] = strdup(ary[2]);
         if(IsUserSuspended(uData))
             ary[3] = "Suspended";
+        else if(HANDLE_FLAGGED(uData->handle, OPER))
+            ary[3] = "Operator";
+        else if(HANDLE_FLAGGED(uData->handle, HELPING))
+            ary[3] = "Staff";
+        else if(HANDLE_FLAGGED(uData->handle, NETWORK))
+            ary[3] = "Network";
         else if(HANDLE_FLAGGED(uData->handle, FROZEN))
             ary[3] = "Vacation";
         else if(HANDLE_FLAGGED(uData->handle, BOT))
@@ -4308,8 +4350,34 @@ bad_topic(struct chanNode *channel, struct userNode *user, const char *new_topic
     struct chanData *cData = channel->channel_info;
     if(check_user_level(channel, user, lvlEnfTopic, 1, 0))
         return 0;
-    if(cData->topic_mask)
-        return !match_ircglob(new_topic, cData->topic_mask);
+    if(cData->topic_mask) 
+    {
+        if(cData->flags & CHANNEL_ADVTOPIC) 
+        {
+            //this implementation is a little bit dirty but i haven't found a better, faster solution, yet...
+            char topicmask[TOPICLEN];
+            int skipnum, topicpos = 0;
+            char *ptr = cData->topic_mask;
+            for(;*ptr;ptr++) { //replace all the %[0-9]* variables with *
+                switch(*ptr) {
+                case '%':
+                    for(skipnum = 0; isdigit(ptr[1]) && skipnum < 3; ptr++, skipnum++) {} //skip up to 3 numbers
+                    if(skipnum)
+                        topicmask[topicpos++] = '*';
+                    else
+                        topicmask[topicpos++] = *ptr;
+                    break;
+                default:
+                    topicmask[topicpos++] = *ptr;
+                    break;
+                }
+            }
+            topicmask[topicpos] = 0;
+            return !match_ircglob(new_topic, topicmask);
+        }
+        else
+            return !match_ircglob(new_topic, cData->topic_mask);
+    }
     else if(cData->topic)
         return irccasecmp(new_topic, cData->topic);
     else
@@ -4347,30 +4415,96 @@ static CHANSERV_FUNC(cmd_topic)
             char new_topic[TOPICLEN+1], tchar;
             int pos=0, starpos=-1, dpos=0, len;
 
-            while((tchar = topic_mask[pos++]) && (dpos <= TOPICLEN))
+            if(cData->flags & CHANNEL_ADVTOPIC) 
             {
-                switch(tchar)
+                //first check if there is a leading 'modifier id'
+                int advtopic_index = 0;
+                char numbuf[4];
+                int numpos;
+                for(; topic[pos]; pos++) 
                 {
-                case '*':
-                    if(starpos != -1)
-                        goto bad_mask;
-                    len = strlen(topic);
-                    if((dpos + len) > TOPICLEN)
-                        len = TOPICLEN + 1 - dpos;
-                    memcpy(new_topic+dpos, topic, len);
-                    dpos += len;
-                    starpos = pos;
-                    break;
-                case '\\': tchar = topic_mask[pos++]; /* and fall through */
-                default: new_topic[dpos++] = tchar; break;
+                    if(topic[pos] == ' ') 
+                    {
+                        //leading number found, cut off and store value in advtopic_index
+                        topic[pos] = 0;
+                        advtopic_index = atoi(topic) - 1; //no zerobase
+                        topic = &topic[pos+1];
+                        /* If they say "!topic 2 *", unset advtopic id 2. */
+                        if((topic[0] == '*') && (topic[1] == 0))
+                            topic[0] = 0;
+                    }
+                    if(!isdigit(topic[pos]))
+                        break;
+                }
+                if(advtopic_index < 0 || advtopic_index >= MAXADVTOPICENTRIES)
+                {
+                    //invalid id!
+                    reply("CSMSG_ADVTOPIC_INVALID_ID", advtopic_index+1);
+                    return 0;
+                }
+                if(cData->advtopic[advtopic_index])
+                    free(cData->advtopic[advtopic_index]);
+                cData->advtopic[advtopic_index] = (topic[0] ? strdup(topic) : NULL);
+                char *ptr = topic_mask;
+                while(*ptr && (dpos <= TOPICLEN))
+                {
+                    switch(*ptr)
+                    {
+                    case '%':
+                        ptr++;
+                        for(numpos = 0; isdigit(*ptr) && numpos < 3; ptr++) {
+                            numbuf[numpos++] = *ptr;
+                        }
+                        numbuf[numpos] = 0;
+                        if(!numpos || (advtopic_index = atoi(numbuf)) <= 0 || advtopic_index > MAXADVTOPICENTRIES) {
+                            ptr -= numpos+1;
+                            new_topic[dpos++] = *ptr; //is % again
+                            break;
+                        }
+                        advtopic_index--; //no zero base
+                        if(!cData->advtopic[advtopic_index])
+                            break; //just leave it empty
+                        len = strlen(cData->advtopic[advtopic_index]);
+                        if((dpos + len) > TOPICLEN)
+                            len = TOPICLEN + 1 - dpos;
+                        memcpy(new_topic+dpos, cData->advtopic[advtopic_index], len);
+                        dpos += len;
+                        break;
+                    case '\\': 
+                        ptr++; /* and fall through */
+                        if(!*ptr) break;
+                    default:
+                        new_topic[dpos++] = *ptr;
+                        ptr++;
+                        break;
+                    }
+                }
+            } else {
+                while((tchar = topic_mask[pos++]) && (dpos <= TOPICLEN))
+                {
+                    switch(tchar)
+                    {
+                    case '*':
+                        if(starpos != -1)
+                            goto bad_mask;
+                        len = strlen(topic);
+                        if((dpos + len) > TOPICLEN)
+                            len = TOPICLEN + 1 - dpos;
+                        memcpy(new_topic+dpos, topic, len);
+                        dpos += len;
+                        starpos = pos;
+                        break;
+                    case '\\': tchar = topic_mask[pos++]; /* and fall through */
+                    default: new_topic[dpos++] = tchar; break;
+                    }
+                }
+                if((dpos > TOPICLEN) || tchar)
+                {
+                bad_mask:
+                    reply("CSMSG_TOPICMASK_CONFLICT1", channel->name, topic_mask);
+                    reply("CSMSG_TOPICMASK_CONFLICT2", TOPICLEN);
+                    return 0;
                 }
-            }
-            if((dpos > TOPICLEN) || tchar)
-            {
-            bad_mask:
-                reply("CSMSG_TOPICMASK_CONFLICT1", channel->name, topic_mask);
-                reply("CSMSG_TOPICMASK_CONFLICT2", TOPICLEN);
-                return 0;
             }
             new_topic[dpos] = 0;
             SetChannelTopic(channel, chanserv, new_topic, 1);
@@ -4604,6 +4738,30 @@ show_suspension_info(struct svccmd *cmd, struct userNode *user, struct suspended
     }
 }
 
+static void
+show_giveownership_info(struct svccmd *cmd, struct userNode *user, struct giveownership *giveownership)
+{
+    char buf[MAXLEN];
+    const char *fmt = "%a %b %d %H:%M %Y";
+    strftime(buf, sizeof(buf), fmt, localtime(&giveownership->issued));
+
+    if(giveownership->staff_issuer)
+    {
+        if(giveownership->reason)
+            reply("CSMSG_CHANNEL_OWNERSHIP_STAFF_REASON", giveownership->old_owner,
+                  giveownership->target, giveownership->target_access,
+                  giveownership->staff_issuer, buf, giveownership->reason);
+        else
+            reply("CSMSG_CHANNEL_OWNERSHIP_STAFF", giveownership->old_owner,
+                  giveownership->target, giveownership->target_access,
+                  giveownership->staff_issuer, buf);
+    }
+    else
+    {
+        reply("CSMSG_CHANNEL_OWNERSHIP_NORMAL", giveownership->old_owner, giveownership->target, giveownership->target_access, buf);
+    }
+}
+
 static CHANSERV_FUNC(cmd_info)
 {
     char modes[MAXLEN], buffer[INTERVALLEN];
@@ -4670,6 +4828,14 @@ static CHANSERV_FUNC(cmd_info)
         reply("CSMSG_CHANNEL_SUSPENDED", channel->name);
         show_suspension_info(cmd, user, cData->suspended);
     }
+    
+    if(cData->giveownership && ((uData && (uData->access >= UL_COOWNER)) || IsStaff(user)))
+    {
+        struct giveownership *giveownership;
+        reply("CSMSG_CHANNEL_OWNERSHIP_HISTORY", channel->name);
+        for(giveownership = cData->giveownership; giveownership; giveownership = giveownership->previous)
+            show_giveownership_info(cmd, user, giveownership);
+    }
     return 1;
 }
 
@@ -5752,6 +5918,11 @@ static MODCMD_FUNC(chan_opt_dynlimit)
     CHANNEL_BINARY_OPTION("CSMSG_SET_DYNLIMIT", CHANNEL_DYNAMIC_LIMIT);
 }
 
+static MODCMD_FUNC(chan_opt_advtopic)
+{
+    CHANNEL_BINARY_OPTION("CSMSG_SET_ADVTOPIC", CHANNEL_ADVTOPIC);
+}
+
 static MODCMD_FUNC(chan_opt_offchannel)
 {
     struct chanData *cData = channel->channel_info;
@@ -6303,13 +6474,21 @@ static CHANSERV_FUNC(cmd_giveownership)
     struct chanData *cData = channel->channel_info;
     struct do_not_register *dnr;
     const char *confirm;
-    unsigned int force;
-    unsigned short co_access;
-    char reason[MAXLEN];
+    struct giveownership *giveownership;
+    unsigned int force, override;
+    unsigned short co_access, new_owner_old_access;
+    char reason[MAXLEN], transfer_reason[MAXLEN];
 
     REQUIRE_PARAMS(2);
     curr_user = GetChannelAccess(cData, user->handle_info);
     force = IsHelping(user) && (argc > 2) && !irccasecmp(argv[2], "force");
+
+    struct userData *uData = _GetChannelUser(channel->channel_info, user->handle_info, 1, 0);
+    override = ((cmd->effective_flags & MODCMD_REQUIRE_CHANUSER)
+                && (uData->access > 500)
+                && (!(uData = _GetChannelUser(channel->channel_info, user->handle_info, 0, 0))
+                    || uData->access < 500));
+
     if(!curr_user || (curr_user->access != UL_OWNER))
     {
         struct userData *owner = NULL;
@@ -6377,6 +6556,7 @@ static CHANSERV_FUNC(cmd_giveownership)
             return 0;
         }
     }
+    new_owner_old_access = new_owner->access;
     if(new_owner->access >= UL_COOWNER)
         co_access = new_owner->access;
     else
@@ -6385,19 +6565,46 @@ static CHANSERV_FUNC(cmd_giveownership)
     if(curr_user)
         curr_user->access = co_access;
     cData->ownerTransfer = now;
+    giveownership = calloc(1, sizeof(*giveownership));
+    giveownership->issued = now;
+    giveownership->old_owner = curr_user->handle->handle;
+    giveownership->target = new_owner_hi->handle;
+    giveownership->target_access = new_owner_old_access;
+    if(override)
+    {
+        if(argc > (2 + force))
+        {
+            unsplit_string(argv + 2 + force, argc - 2 - force, transfer_reason);
+            giveownership->reason = strdup(transfer_reason);
+        }
+        giveownership->staff_issuer = strdup(user->handle_info->handle);
+    }
+
+    giveownership->previous = channel->channel_info->giveownership;
+    channel->channel_info->giveownership = giveownership;
     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);
     return 1;
 }
 
+static void
+chanserv_expire_user_suspension(void *data)
+{
+    struct userData *target = data;
+
+       target->expires = 0;
+       target->flags &= ~USER_SUSPENDED;       
+}
+
 static CHANSERV_FUNC(cmd_suspend)
 {
     struct handle_info *hi;
     struct userData *actor, *real_actor, *target;
     unsigned int override = 0;
+    time_t expiry;
 
-    REQUIRE_PARAMS(2);
+    REQUIRE_PARAMS(3);
     if(!(hi = modcmd_get_handle_info(user, argv[1]))) return 0;
     actor = GetChannelUser(channel->channel_info, user->handle_info);
     real_actor = GetChannelAccess(channel->channel_info, user->handle_info);
@@ -6423,6 +6630,24 @@ static CHANSERV_FUNC(cmd_suspend)
     }
     if(!real_actor || target->access >= real_actor->access)
         override = CMD_LOG_OVERRIDE;
+       if(!strcmp(argv[2], "0"))
+        expiry = 0;
+    else
+    {
+        unsigned int duration;
+        if(!(duration = ParseInterval(argv[2])))
+        {
+            reply("MSG_INVALID_DURATION", argv[2]);
+            return 0;
+        }
+        expiry = now + duration;
+    }
+
+       target->expires = expiry;
+
+       if(target->expires)
+               timeq_add(target->expires, chanserv_expire_user_suspension, target);
+
     target->flags |= USER_SUSPENDED;
     reply("CSMSG_USER_SUSPENDED", hi->handle, channel->name);
     return 1 | override;
@@ -6455,6 +6680,7 @@ static CHANSERV_FUNC(cmd_unsuspend)
     }
     if(!real_actor || target->access >= real_actor->access)
         override = CMD_LOG_OVERRIDE;
+    timeq_del(target->expires, chanserv_expire_user_suspension, target, 0);
     target->flags &= ~USER_SUSPENDED;
     scan_user_presence(target, NULL);
     reply("CSMSG_USER_UNSUSPENDED", hi->handle, channel->name);
@@ -7659,12 +7885,16 @@ chanserv_conf_read(void)
     chanserv_conf.dnr_expire_frequency = str ? ParseInterval(str) : 3600;
     str = database_get_data(conf_node, KEY_INVITED_INTERVAL, RECDB_QSTRING);
     chanserv_conf.invited_timeout = str ? ParseInterval(str) : 600*2;
+    str = database_get_data(conf_node, KEY_REVOKE_MODE_A, RECDB_QSTRING);
+    chanserv_conf.revoke_mode_a = str ? atoi(str) : 1;
     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_MIN_TIME_BANS, RECDB_QSTRING);
+    chanserv_conf.min_time_bans = str ? atoi(str) : 5;
     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);
@@ -7755,6 +7985,16 @@ chanserv_conf_read(void)
     chanserv_conf.old_ban_names = strlist;
     str = database_get_data(conf_node, "off_channel", RECDB_QSTRING);
     off_channel = str ? atoi(str) : 0;
+
+    str = database_get_data(conf_node, "oper_chan", RECDB_QSTRING);
+       if(str)
+       {
+               chanserv_conf.oper_channel = AddChannel(str, now, "+tinms", NULL);
+       }
+       else
+       {
+               chanserv_conf.oper_channel = NULL;
+       }
 }
 
 static void
@@ -7839,7 +8079,7 @@ user_read_helper(const char *key, struct record_data *rd, struct chanData *chan)
 {
     struct handle_info *handle;
     struct userData *uData;
-    char *seen, *inf, *flags, *voted, *votefor;
+    char *seen, *inf, *flags, *voted, *votefor, *expires;
     unsigned long last_seen;
     unsigned short access_level;
 
@@ -7860,6 +8100,7 @@ user_read_helper(const char *key, struct record_data *rd, struct chanData *chan)
     seen = database_get_data(rd->d.object, KEY_SEEN, RECDB_QSTRING);
     last_seen = seen ? strtoul(seen, NULL, 0) : now;
     flags = database_get_data(rd->d.object, KEY_FLAGS, RECDB_QSTRING);
+    expires = database_get_data(rd->d.object, KEY_EXPIRES, RECDB_QSTRING);
     voted = database_get_data(rd->d.object, KEY_VOTE_VOTED, RECDB_QSTRING);
     votefor = database_get_data(rd->d.object, KEY_VOTE_VOTEDFOR, RECDB_QSTRING);
     handle = get_handle_info(key);
@@ -7871,6 +8112,15 @@ user_read_helper(const char *key, struct record_data *rd, struct chanData *chan)
 
     uData = add_channel_user(chan, handle, access_level, last_seen, inf);
     uData->flags = flags ? strtoul(flags, NULL, 0) : 0;
+       uData->expires = expires ? (signed)strtoul(expires, NULL, 0) : 0;
+
+       if((uData->flags & USER_SUSPENDED) && uData->expires)
+       {
+         if(uData->expires > now)
+                 timeq_add(uData->expires, chanserv_expire_user_suspension, uData);
+         else
+                 uData->flags &= ~USER_SUSPENDED;
+       }
     if(chan->vote) {
         uData->voted = voted ? strtoul(voted, NULL, 0) : 0;
         uData->votefor = votefor ? strtoul(votefor, NULL, 0) : 0;
@@ -7936,10 +8186,36 @@ chanserv_read_suspended(dict_t obj)
     return suspended;
 }
 
+static struct giveownership *
+chanserv_read_giveownership(dict_t obj)
+{
+    struct giveownership *giveownership = calloc(1, sizeof(*giveownership));
+    char *str;
+    dict_t previous;
+
+    str = database_get_data(obj, KEY_STAFF_ISSUER, RECDB_QSTRING);
+    giveownership->staff_issuer = str ? strdup(str) : NULL;
+
+    giveownership->old_owner = strdup(database_get_data(obj, KEY_OLD_OWNER, RECDB_QSTRING));
+
+    giveownership->target = strdup(database_get_data(obj, KEY_TARGET, RECDB_QSTRING));
+    giveownership->target_access = atoi(database_get_data(obj, KEY_TARGET_ACCESS, RECDB_QSTRING));
+
+    str = database_get_data(obj, KEY_REASON, RECDB_QSTRING);
+    giveownership->reason = str ? strdup(str) : NULL;
+    str = database_get_data(obj, KEY_ISSUED, RECDB_QSTRING);
+    giveownership->issued = str ? (time_t)strtoul(str, NULL, 0) : 0;
+
+    previous = database_get_data(obj, KEY_PREVIOUS, RECDB_OBJECT);
+    giveownership->previous = previous ? chanserv_read_giveownership(previous) : NULL;
+    return giveownership;
+}
+
 static int
 chanserv_channel_read(const char *key, struct record_data *hir)
 {
     struct suspended *suspended;
+    struct giveownership *giveownership;
     struct mod_chanmode *modes;
     struct chanNode *cNode;
     struct chanData *cData;
@@ -8078,6 +8354,12 @@ chanserv_channel_read(const char *key, struct record_data *hir)
             cData->flags &= ~CHANNEL_SUSPENDED;
     }
 
+    if((obj = database_get_data(hir->d.object, KEY_GIVEOWNERSHIP, RECDB_OBJECT)))
+    {
+        giveownership = chanserv_read_giveownership(obj);
+        cData->giveownership = giveownership;
+    }
+
     if((!off_channel || !IsOffChannel(cData)) && !IsSuspended(cData)) {
         struct mod_chanmode change;
         mod_chanmode_init(&change);
@@ -8120,6 +8402,16 @@ chanserv_channel_read(const char *key, struct record_data *hir)
         }
     }
 
+    obj = database_get_data(channel, KEY_ADVTOPIC_ENTRIES, RECDB_OBJECT);
+    for(it = dict_first(obj); it; it = iter_next(it))
+    {
+        struct record_data *rd = iter_data(it);
+        if(rd->type != RECDB_QSTRING) continue;
+        int advtopic_index = atoi(iter_key(it));
+        if(advtopic_index < 0 || advtopic_index >= MAXADVTOPICENTRIES) continue;
+        cData->advtopic[advtopic_index] = (rd ? strdup(rd->d.qstring) : NULL);
+    }
+
     if(!IsSuspended(cData)
        && (str = database_get_data(channel, KEY_MODES, RECDB_QSTRING))
        && (argc = split_line(str, 0, ArrayLength(argv), argv))
@@ -8246,6 +8538,8 @@ chanserv_write_users(struct saxdb_context *ctx, struct userData *uData)
         saxdb_write_int(ctx, KEY_SEEN, uData->seen);
         if(uData->flags)
             saxdb_write_int(ctx, KEY_FLAGS, uData->flags);
+        if(uData->expires)
+            saxdb_write_int(ctx, KEY_EXPIRES, uData->expires);
         if(uData->channel->vote && uData->voted)
             saxdb_write_int(ctx, KEY_VOTE_VOTED, uData->voted);
         if(uData->channel->vote && uData->votefor)
@@ -8298,6 +8592,27 @@ chanserv_write_suspended(struct saxdb_context *ctx, const char *name, struct sus
     saxdb_end_record(ctx);
 }
 
+static void
+chanserv_write_giveownership(struct saxdb_context *ctx, const char *name, struct giveownership *giveownership)
+{
+    saxdb_start_record(ctx, name, 0);
+    if(giveownership->staff_issuer)
+      saxdb_write_string(ctx, KEY_STAFF_ISSUER, giveownership->staff_issuer);
+    if(giveownership->old_owner)
+      saxdb_write_string(ctx, KEY_OLD_OWNER, giveownership->old_owner);
+    if(giveownership->target)
+      saxdb_write_string(ctx, KEY_TARGET, giveownership->target);
+    if(giveownership->target_access)
+      saxdb_write_int(ctx, KEY_TARGET_ACCESS, giveownership->target_access);
+    if(giveownership->reason)
+      saxdb_write_string(ctx, KEY_REASON, giveownership->reason);
+    if(giveownership->issued)
+        saxdb_write_int(ctx, KEY_ISSUED, giveownership->issued);
+    if(giveownership->previous)
+        chanserv_write_giveownership(ctx, KEY_PREVIOUS, giveownership->previous);
+    saxdb_end_record(ctx);
+}
+
 static void
 chanserv_write_channel(struct saxdb_context *ctx, struct chanData *channel)
 {
@@ -8324,6 +8639,8 @@ chanserv_write_channel(struct saxdb_context *ctx, struct chanData *channel)
         saxdb_write_string(ctx, KEY_TOPIC_MASK, channel->topic_mask);
     if(channel->suspended)
         chanserv_write_suspended(ctx, "suspended", channel->suspended);
+    if(channel->giveownership)
+        chanserv_write_giveownership(ctx, "giveownership", channel->giveownership);
     if(channel->expiry)
         saxdb_write_int(ctx, KEY_EXPIRE, channel->expiry);
 
@@ -8369,6 +8686,16 @@ chanserv_write_channel(struct saxdb_context *ctx, struct chanData *channel)
     high_present = chanserv_write_users(ctx, channel->users);
     chanserv_write_bans(ctx, channel->bans);
 
+    if(channel->flags & CHANNEL_ADVTOPIC) {
+        saxdb_start_record(ctx, KEY_ADVTOPIC_ENTRIES, 0);
+        int advtopic_index;
+        for(advtopic_index = 0; advtopic_index < MAXADVTOPICENTRIES; advtopic_index++) {
+            if(channel->advtopic[advtopic_index])
+                saxdb_write_string(ctx, strtab(advtopic_index), channel->advtopic[advtopic_index]);
+        }
+        saxdb_end_record(ctx);
+    }
+
     if(dict_size(channel->notes))
     {
         dict_iterator_t it;
@@ -8674,6 +9001,7 @@ init_chanserv(const char *nick)
     DEFINE_CHANNEL_OPTION(ctcpusers);
     DEFINE_CHANNEL_OPTION(ctcpreaction);
     DEFINE_CHANNEL_OPTION(inviteme);
+    DEFINE_CHANNEL_OPTION(advtopic);
     DEFINE_CHANNEL_OPTION(unreviewed);
     modcmd_register(chanserv_module, "set expire", chan_opt_expire, 1, 0, "flags", "+helping", NULL);
     modcmd_register(chanserv_module, "set unreviewed on", NULL, 0, 0, "flags", "+helping", NULL);