####################################################
# by wiebe @ QuakeNet
#
#  helps to prevent the channel banlist from becoming full
#  various settings determine the script's behavior
#
#  +banlist enables the script, a check is done every banlist-delay minutes
#  if more than banlist-max channel bans are set, the bans in excess are removed
#  (the internal banlist is not touched).
#  in case the banlist becomes full and +banlist-lock is set,
#  banlist-lockmode is used to lock the channel (valid modes are imrD)
#  where +i at the least is recommended,
#  the channel remains locked until the next check (which is every banlist-delay minutes)
#  or bans are removed by other means (channel ops)
#
# flags
#
#  +banlist		enable/disable script)
#   banlist-delay	check every N minutes
#   banlist-max	        number of bans to leave set
#  +banlist-lock	enable/disable locking
#   banlist-lockmode	modes to set when locking (valid are imrD)
#   banlist-bot		used for sharing settings over the botnet, do not touch
#
# script can use: ircconsole.tcl
#
####################################################


####################################################
# flags
####################################################
setudef flag banlist
setudef flag banlist-lock
setudef int banlist-delay
setudef int banlist-max
setudef str banlist-lockmode
setudef str banlist-bot


####################################################
# banlist:time
####################################################
bind time -|- * banlist:time
proc banlist:time { n h d m y } {
  global banlistdb
  set t [clock seconds]
  foreach e [array names banlistdb] {
    set c [lindex [split $e ,] 0]; set b [join [lrange [split $e ,] 1 end]]
    set s $banlistdb($e)
    if {![validchan $c] || ![botonchan $c] || ![ischanban $b $c] || [isbansticky $b $c]} {
      unset banlistdb($e); continue
    }
    if {![botisop $c] && ![botishalfop $c]} { continue }
    if {![channel get $c dynamicbans]} { continue }
    set u [channel get $c ban-time]
    if {$u == 0} { continue }
    if {[expr ($t - $s) / 60] < $u} { continue }
    pushmode $c -b $b; unset banlistdb($e)
  }
  foreach c [channels] {
    set a [banlist:get $c]; set b [join [lrange [split [channel get $c banlist-bot]] 1 end]]
    if {$b != $a} { channel set $c banlist-bot "$t $a"; putallbots "BL S $c $t $a" }
    banlist:checkchan $c
  }
}


####################################################
# banlist:checkchan
####################################################
proc banlist:checkchan { c } {
  if {![validchan $c]} { return 0 }
  if {![channel get $c banlist]} { return 0 }
  if {![botisop $c] && ![botishalfop $c]} { return 0 }
# max-bans is wrong
  if {![info exists ::max-bans]} { return 0 }
  if {![string is digit ${::max-bans}]} { return 0 }
# get max setting, min 10
  set x [channel get $c banlist-max]
  if {$x < 10} { set x 10; channel set $c banlist-max $x }
  if {$x > ${::max-bans}} { return 0 }
# no ban checking needed, unlock if needed
  if {[llength [chanbans $c]] <= $x} { banlist:unlock $c; return 0 }
# delay check
  set d [channel get $c banlist-delay]
  if {$d < 1} { set d 1; channel set $c banlist-delay $d }
  if {[expr round(fmod([clock seconds] / 60,$d))] > 0} { banlist:lock $c; return 0 }
# remove bans
  set y [llength [chanbans $c]]
  foreach b [chanbans $c] {
    set b [lindex $b 0]
# on internal banlist and sticky
    if {[isban $b $c] && [isbansticky $b $c]} { continue }
    incr y -1
    pushmode $c -b $b
    if {$y == $x} { break }
  }
# unlock
  if {$y < ${::max-bans}} { banlist:unlock $c }
}


####################################################
# banlist:mode
####################################################
bind mode -|- "% +b" banlist:mode; bind mode -|- "% -b" banlist:mode
bind mode -|- "% +o" banlist:mode; bind mode -|- "% +h" banlist:mode
proc banlist:mode { n u h c m t } {
  if {![validchan $c]} { return 0 }
  if {![channel get $c banlist]} { return 0 }
  if {![botisop $c] && ![botishalfop $c]} { return 0 }
  if {![info exists ::max-bans]} { return 0 }
  if {![string is digit ${::max-bans}]} { return 0 }
  if {$m == "+b"} { if {${::max-bans} == [expr [llength [chanbans $c]] +1]} { banlist:lock $c }
  } elseif {$m == "-b"} { if {${::max-bans} == [llength [chanbans $c]]} { banlist:unlock $c }
  } elseif {[string match {+[oh]} $m] && [isbotnick $t]} { banlist:checkchan $c }
}


####################################################
# banlist:raw
####################################################
bind raw -|- "367" banlist:raw; bind raw -|- "368" banlist:raw
proc banlist:raw { s n t } {
  set t [lrange [split $t] 1 end]; set c [lindex $t 0]
  if {![validchan $c]} { return 0 }
  if {$n == 367} {
    global banlistdb
    set b [lindex $t 1]; set s [lindex $t 3];  set banlistdb($c,$b) $s
  } elseif {$n == 368} {
    if {![channel get $c banlist]} { return 0 }
    if {![botisop $c] && ![botishalfop $c]} { return 0 }
    banlist:checkchan $c
  }
  return 0
}


####################################################
# banlist:unlock
####################################################
proc banlist:unlock { c } {
  global banlistdb
  if {![info exists ::max-bans] } { return 0 }
  if {![string is digit ${::max-bans}]} { return 0 }
  if {![channel get $c banlist-lock]} { return 0 }
  set l [channel get $c banlist-lockmode]
  if {$l == ""} { return 0 }
  set x [lindex [split [getchanmode $c]] 0]
  if {![info exists banlistdb($c,m)]} { return 0 }
  foreach m [split $l ""] {
    if {![string match *$m* $x]} { continue }
    if {![string match {[imrD]} $m]} { continue }
    if {![string match *$m* $banlistdb($c,m)]} { continue }
    pushmode $c -$m
  }
  set banlistdb($c,m) ""
}


####################################################
# banlist:lock
####################################################
proc banlist:lock { c } {
  global banlistdb
  if {![info exists ::max-bans]} { return 0 }
  if {![string is digit ${::max-bans}]} { return 0 }
  if {![channel get $c banlist-lock]} { return 0 }
  set l [channel get $c banlist-lockmode]
  if {$l == ""} { return 0 }
  set x [lindex [split [getchanmode $c]] 0]; set d 0
  foreach m [split $l ""] {
    if {[string match *$m* $x]} { continue }
    if {![string match {[imrD]} $m]} { continue }
    if {[string match *$m* [lindex [split [getchanmode $c]] 0]]} { continue }
    if {![info exists banlistdb($c,m)]} { set banlistdb($c,m) "" }
    pushmode $c +$m; set d 1; append banlistdb($c,m) $m
  }
  if {$d} {
    putloglev k $c "$c: channel ban/ignore list is full"
    if {[info procs ircconsole] != ""} { ircconsole $c h BANLIST "channel ban/ignore list is full" }
  }
}


####################################################
# banlist:lower
####################################################
proc banlist:lower { t } {
  global rfc-compliant
  if {[info exists rfc-compliant] && ${rfc-compliant} == 1} { set t [string map "\\{ \[ \\} \] ~ ^ \\\\ |" $t] }
  set t [string tolower $t]
  return $t
}


####################################################
# banlist:get
####################################################
proc banlist:get { c } {
  if {![validchan $c]} { return "" }
  lappend l "" -lock -delay -max -lockmode
  foreach e $l { lappend o [lindex [split [channel get $c banlist$e]] 0] }
  return [join $o]
}


####################################################
# banlist:link
####################################################
bind link -|- * banlist:link
proc banlist:link { b v } { if {[islinked $b] && [string equal -nocase ${::botnet-nick} $v]} { putbot $b "BL ?" } }


####################################################
# banlist:bot
####################################################
# bot BL "?|Y|B|S <chan> <ts> <status> <lock> <delay> <max> <lockmode>"
# ?=banlist?, Y=yes, B=burst when linking, S=setting changed, or informing botnet of change after B
bind bot -|- BL banlist:bot
proc banlist:bot { b c t } {
  if {![islinked $b]} { return 0 }
  if {![string equal -nocase $c BL]} { return 0 }
  set t [split $t]; set s [string tolower [lindex $t 0]]; set c [lindex $t 1]; set n [lindex $t 2]
  if {$s == "?"} {
    putbot $b "BL Y"
  } elseif {$s == "y"} {
    set n [clock seconds]
    foreach c [channels] {
      set x [split [channel get $c banlist-bot]]; set d [join [lrange $x 1 end]]; set x [lindex $x 0]
      set a [banlist:get $c]
      if {![string is digit $x] || $d != $a} { set x $n; channel set $c banlist-bot "$x $a" }
      putbot $b "BL B $c $x $a"
    }
  } elseif {[string match {[bs]} $s]} {
    if {[llength $t] != 8} { return 0 }
    if {![validchan $c] && (![string match *g* [botattr $b]] || [catch {channel add $c +inactive}])} { return 0 }
    set x [split [channel get $c banlist-bot]]; set d [join [lrange $x 1 end]]; set x [lindex $x 0]
    if {![string is digit $x]} { set x 0 }
    if {$s == "b"} {
      # equal timestamps, the greater bot wins (ab smaller than xy)
      if {$x == $n} { if {$b > ${::botnet-nick}} { return 0 } } elseif {$x > $n} { return 0 }
    }
    set p 3; set l [list "" -lock]
    foreach e $l { if {[lindex $t $p]} { set v + } else { set v - }; channel set $c ${v}banlist$e; incr p }
    set l [list delay max lockmode]
    foreach e $l { channel set $c banlist-$e [lindex $t $p]; incr p }
    set t [join [lrange $t 3 end]]; channel set $c banlist-bot "$n $t"
    if {$s != "b" || $d == $t} { return 0 }
    putallbots "BL S $c $n $t"    
  }
}


####################################################
# set info for script.tcl
####################################################
set ::scriptdb(banlist) {
  "helps to prevent the channel banlist from becoming full, various settings determine the script's behavior"
  "+banlist enables the script, a check is done every banlist-delay minutes, if more than banlist-max channel bans are set, the bans in excess are removed (the internal banlist is not touched)."
  "in case the banlist becomes full and +banlist-lock is set, banlist-lockmode is used to lock the channel (valid modes are imrD) where +i at the least is recommended, the channel remains locked until the next check (which is every banlist-delay minutes). banlist-bot is used for sharing settings over the botnet, do not touch."
}

