
# Clone detection from By-Tor, Ernst & Flash
# Version 3.6 for eggdrop 1.3.x, 1.4.x, 1.5.x
# 27-Sep-2000 Ernst <ernst@baschny.de>

# Ernst's eggdrop page:  http://www.baschny.de/eggdrop/

# 27-Sep-2000 Version 3.6 (by Ernst):
# - Now should also work without errors on eggdrop 1.5.x (untested!)
#   ("Tcl error [clone_remove]: called "clone_remove" with too many arguments.")

# 21-Aug-2000 Version 3.5 (by Ernst):
# - Tcl error in script for 'timer11847': invalid channel when logging in:
#   Bug fixed
# - Other scan's that were causing 'can't read "XXX": no such variable'
#   errors changed to lindex [split ..] 

# not released(?) Version 3.4B (by Ernst):
# Suggestions by John` <Johnny@technik.sth.ac.at>
# - Undernet doesn't allow fast-kick
# - Error when kicking without "fastkick" enabled
# - Changed 'scan' with 'lindex [split ...]'
# - Also checks for $nocloneflag when KICKING is done to avoid kicking ops
#   on same hosts as clones

# 8-May-1999 Version 3.3B (by Ernst):
# - Fixed error in .clones when channel has capital letters

# 17-Sep-1998 Version 3.2B (by Flash <flash@wurzel.ola.fr.eu.org>):
# - When starting or re starting the bot with clonecheck set to 0, the script
#   made the bot crash. It's now fixed.
# - Minor things in messages.

# 27-Jul-1998 Version 3.1B (by Flash):
# - Added a rare case : when someone rejoin after a split...

# New in version 3B:
# - Updated for eggdrop 1.3.x (Now also matches "channel flags")
# - Fast kick of all clones at once (if IRC networks allows it)
# - Console +1 also set for channel masters/ops
# - Logging now goes to loglevel 1 of each channel (and not global level 1)

# -- Installation/Usage -------------------------------------------------------

# - Copy it to your scripts/ directory
# - Change/Check anything in the "Configuration" part bellow
# - Install the script ('source scripts/clone_detect-3B.tcl' in bots config)
# - Rehash or restart the bot. Wait one minute for initial clone-checking
# - In bot's DCC/telnet, use following commands:
#    .console +1          - to be warned in bot when a clone joins/parts
#    .clones [#channel]   - gives a list of clones in specified channel
#    .clonecheck [on/off] - turns clonechecking on or off

# -- Configuration ------------------------------------------------------------

# "Fast-kick" - If your IRC server allows it, you can kick many people at once
# in just one command (I have tried it with 20 kicks on IRCnet). If you think
# this works on your irc network, set to "1", if not, set to "0".
# Here is what I got by testing on some networks:
#   Dalnet   - 0
#   Efnet    - 1
#   IRCnet   - 1
#   Undernet - 0
set fastkick 1

# Is the whole clone-checking on or off by default?
# (use .clonecheck on/off to change while bot is running)
set clonecheck 1

# Hosts to except from clone-checking, wildcards (?*) allowed:
# - hosts which more than one users can use at the same time
# - hosts of your "friends" which are neither masters nor bots
# - hosts of bots on the channel which have no +b flag
# - etc etc
# Note: Use HOST, the thing after the users "@"
#       e.g.  "yo!yaba@yeba.edu" has host "yeba.edu"
set noclonehost {
}

# People with those flags are never clones: (Separate the list with spaces)
# Channel and Global flags are matched
set nocloneflag "b c m"

# How many minutes will a clone-host banning last
set clonebantime 30

# If this number of clones is reached, bot kicks and bans
set maxclone 3

# NOTICE other ops (if opped) when ANY clone joins/parts the channel? (0 = no)
# (for paranoid people only!)
set cloneopnotice 0

# -- Script -------------------------------------------------------------------

# A little proc to turn console +1 when a +m or +o joins:
bind chon - * dcc_on
proc dcc_on { hand idx } {
  set cons1on 0
  foreach chan [channels] {
    if {[matchattr $hand m|m $chan] || [matchattr $hand o|o $chan]} {
      set cons1on 1
    }
  }
  if {$cons1on} { utimer 1 "console $idx -+1" }
  return 0
}

# Opnotice proc, sends a notice to every op on a channel (if opped)
proc opnotice { chan text } {
	global botnick
	if {![botisop $chan]} { return 0 }
	if {$text == ""} { return 0 }
	# I set maxnicks to send the same NOTICE to maximal 10 nicks at the same
	# time. Rise it if you want. (I don't know what the maximum IRC-allowed is)
	set maxnicks 10
	set oplist ""
	set curnicks 0
	foreach thisnick [chanlist $chan] {
		# don't notice who's not op, the bot itself and other bots
		if {([isop $thisnick $chan]) && ($thisnick != $botnick) && (![matchattr [nick2hand $thisnick $chan] b])} {
			incr curnicks
			# Add next op
			lappend oplist $thisnick
			# Reached maxnicks?
			if {$curnicks == $maxnicks} {
				# Send the text to the current list
				regsub -all " " $oplist "," oplist
				puthelp "NOTICE $oplist :$text"
				# Clean up to begin next block
				set curnicks 0
				set oplist ""
			}
		}
	}
	if {$oplist != ""} {
		# still some left to process
		regsub -all " " $oplist "," oplist
		puthelp "NOTICE $oplist :$text"
	}
	return 1
}

bind dcc m|m clones dcc_clone_stats
bind dcc m|m clonescheck dcc_clonecheck
bind dcc m|m clonecheck dcc_clonecheck

# Kicks users matching $mask from $chan. Uses "fastkick", if enabled.
# Returns a list of kicked users (may not have been executed, because of lag)
proc kickall { mask chan reason } {
	global fastkick	botnick nocloneflag
	if {![validchan $chan]} { return "" }
	set kicked ""
	foreach thisnick [chanlist $chan] {
		if {$thisnick == $botnick} { continue }
		if {[string match "$mask" "$thisnick![getchanhost $thisnick $chan]"]} {
			set dontkick 0
			foreach exceptionflag $nocloneflag {
				if {[matchattr $hand $exceptionflag|$exceptionflag $chan]} {
					set dontkick 1
					break
				}
			}
			if {$dontkick} { continue }
			if {[botisop $chan]} {
			# We only can kick if we are op. But we want to return the list
			# of "kicked" even if we aren't
				if {$fastkick} {
					lappend knick $thisnick
					lappend kchan $chan
				} {
					putserv "KICK $chan $thisnick :$reason"
				}
			}
			lappend kicked $thisnick
		}
		if {$fastkick && [info exist knick]} {
			# Dump a KICK-line if it reaches 480 bytes
			if {[string length "KICK $knick $kchan :$reason"] > 480} {
				regsub -all " " $knick "," knick
				regsub -all " " $kchan "," kchan
				putserv "KICK $kchan $knick :$reason"
				unset knick
				unset kchan
			}
		}
	}
	if {$fastkick && [info exist knick]} {
		regsub -all " " $knick "," knick
		regsub -all " " $kchan "," kchan
		putserv "KICK $kchan $knick :$reason"
	}
	return "$kicked"
}

# Goes through the channel user list, marking clones. Called 10 secs after a
# rehash/restart and 1 min after the bot joins the chan	and after the bot is
# opped (to kick eventual unkicked clones)
proc set_clones { } {
	global clonelist botnick maxclone fastkick
	global noclonehost nocloneflag clonebantime
	if [array exists clonelist] { unset clonelist }
	foreach chan [channels] {
		set chan [string tolower $chan]
		set kicked ""
		foreach nick [chanlist $chan] {
			set hand [nick2hand $nick $chan]
			if {[lsearch -exact $kicked $nick] >= 0} {
				# This nick was already kicked
				continue
			}
      set host [string tolower [lindex [split [getchanhost $nick $chan] @] 1]]
			if [info exists clonelist($chan!$host)] {
				incr clonelist($chan!$host) 1
				if {$clonelist($chan!$host) >= $maxclone} {
					newchanban $chan "*!*@$host" $botnick "Clones \($clonelist($chan!$host)\)" $clonebantime
					set thiskicked [kickall "*!*@$host" $chan "Too many clones from $host"]
					set kicked [concat $kicked $thiskicked]
					putloglev 1 $chan "Max clones reached from $host in $chan: $thiskicked"
				}
				continue
			}
			set clonelist($chan!$host) 0
			if {$nick == $botnick} {unset clonelist($chan!$host)}
			foreach exceptionflag $nocloneflag {
				if {[matchattr $hand $exceptionflag|$exceptionflag $chan]} {
					if {[info exists clonelist($chan!$host)]} { unset clonelist($chan!$host) }
					break
				}
			}
			if {[info exists clonelist($chan!$host)]} {
				foreach exceptionhost $noclonehost {
					# remove it, if it is an exception
					if {[string match [string tolower $exceptionhost] [string tolower $host]]} {
						unset clonelist($chan!$host)
						break
					}
				}
			}
		}
	}
}
# (i love those 6-level identations...)

# Someone joining, check if this host already has someone here
proc clone_detect { nick uhost hand chan } {
	global clonelist maxclone botnick
	global nocloneflag noclonehost clonebantime cloneopnotice
	set chan [string tolower $chan]
	# It is me joining! Let's rescan the channels list for clones in 1 min!
	if {$nick == $botnick} {
		# previous timer exists, kill it, start a new one
		foreach this [timers] {
			if {[string compare [lindex $this 1] "set_clones"] == 0} {
				killtimer [lindex $this 2]
			}
		}
		timer 1 set_clones
		return 0
	}
	# Someone joining. Check if bot needs to update cloneslist first
	foreach this [timers] {
		if {[string compare [lindex $this 1] "set_clones"] == 0} {
			# Just relax, I need to refresh the clones-list first...
			return 0
		}
	}
	foreach exceptionflag $nocloneflag {
		if {[matchattr $hand $exceptionflag|$exceptionflag $chan]} {
			return 0
		}
	}
  set host [string tolower [lindex [split $uhost @] 1]]
	foreach exceptionhost $noclonehost {
		# if it is an exception do nothing
		if {[string match [string tolower $exceptionhost] $host]} {return 0}
	}
	if [info exists clonelist($chan!$host)] {
		incr clonelist($chan!$host) 1
		if {$clonelist($chan!$host) >= $maxclone} {
			newchanban $chan "*!*@$host" $botnick "Clones \($clonelist($chan!$host)\)" $clonebantime
			set thiskicked [kickall "*!*@$host" $chan "Too many clones from $host"]
			putloglev 1 $chan "Max clones reached from $host in $chan: $thiskicked"
			return 1
		}
		foreach nick2 [chanlist $chan] {
      set host2 [string tolower [lindex [split [getchanhost $nick2 $chan] @] 1]]
			if {$host == $host2 && $nick != $nick2} { lappend clonenicks $nick2 }
		}
		regsub -all " " $clonenicks ", " clonenicks
		if {$cloneopnotice} {
			opnotice $chan "Clone IN: $nick \($uhost\) is a clone to $clonenicks in $chan"
		}
		putloglev 1 $chan "Clone IN $chan: $nick \($uhost\) is clone \#$clonelist($chan!$host) \($clonenicks\)"
		return 1
	}
	set clonelist($chan!$host) 0
	return 0
}

proc clone_remove { nick uhost hand chan { rest "" } } {
	global clonelist botnick cloneopnotice nocloneflag noclonehost
	set chan [string tolower $chan]
	if {$nick == $botnick} {return 0}
	foreach this [timers] {
		if {[string compare [lindex $this 1] "set_clones"] == 0} {
			# Just relax, I need to refresh the clones-list first...
			return 0
		}
	}
  set host [string tolower [lindex [split $uhost @] 1]]
	foreach exceptionflag $nocloneflag {
		if {[matchattr $hand $exceptionflag|$exceptionflag $chan]} {
			return 0
		}
	}
	foreach exceptionhost $noclonehost {
		# if it is an exception do nothing
		if {[string match [string tolower $exceptionhost] [string tolower $host]]} {return 0}
	}
	if ![info exists clonelist($chan!$host)] {return 0}
	if {$clonelist($chan!$host) < 0} {
		putloglev 1 $chan "Negative number of clones listed?? Something is wrong with the script!"
		return 0
	} elseif {$clonelist($chan!$host) == 0} {
		unset clonelist($chan!$host)
		return 1
	} elseif {$clonelist($chan!$host) > 0} {
		incr clonelist($chan!$host) -1
		foreach n [chanlist $chan] {
      set host2 [string tolower [lindex [split [getchanhost $n $chan] @] 1]]
			if {($host == $host2) && ($n != $nick)} { lappend clonenicks $n }
		}
		regsub -all " " $clonenicks ", " clonenicks
		putloglev 1 $chan "Clone OUT $chan: $nick \($uhost\) left \($clonenicks remaining\)"
		if {$cloneopnotice} {
			opnotice $chan "Clone OUT: $nick \($uhost\) left $chan \($clonenicks remaining\)"
		}
		return 1
	}
}

proc clone_kick { nick host hand chan knick rest } {
	clone_remove $knick [getchanhost $knick $chan] [nick2hand $knick $chan] $chan
}

proc clone_quit { nick host hand chan rest } {
	clone_remove $nick $host $hand $chan
}

proc dcc_clonecheck { hand idx onoff } {
	global clonecheck
	set choice [string tolower $onoff]
	if {$onoff == ""} {
		if {$clonecheck} { set status "on" } { set status "off" }
		putdcc $idx "Clone-checking currently \002${status}\002."
		return 0
	}
	if {($choice != "on") && ($choice != "off")} {
		putdcc $idx "Usage: '.clonecheck \[on/off\]'"
		return 0
	}
	if {$choice == "on"} {
		if {$clonecheck == 0} {
			set clonecheck 1
			putdcc $idx "Clone-checking is now \002on\002."
			clonecheckload
			return 1
		} {
			putdcc $idx "Clone-checking was already \002on\002."
			return 0
		}
	} {
		if {$clonecheck == 1} {
			set clonecheck 0
			putdcc $idx "Clone-checking is now \002off\002."
			clonecheckload
			return 1
		} {
			putdcc $idx "Clone-checking was already \002off\002."
			return 0
		}
	}
}

proc dcc_clone_stats { hand idx chan } {
	global clonecheck
	if {$clonecheck == 0} {
		putdcc $idx "Sorry, clone-checking currently off. Use \002.clonecheck on\002 to turn it on."
		return 0
	}
	global clonelist
	if {$chan == ""} {
		set channel [lindex [console $idx] 0]
	} {
		regsub -nocase -all \[{}] $chan "" chan
		set channel [lindex $chan 0]
	}
	putdcc $idx "Clones in $channel:"
  set channel [string tolower $channel]
	foreach addy [array names clonelist] {
		if {$clonelist($addy) > 0} {
			scan $addy "%\[^!]!%s" chan host
			if {$chan != [string tolower $channel]} { continue }
			foreach nick [chanlist $chan] {
        set host2 [string tolower [lindex [split [getchanhost $nick $chan] @] 1]]
				if {$host == $host2} { lappend clonenicks $nick	}
			}
		}
		if [info exists clonenicks] {
			regsub -all " " $clonenicks ", " clonenicks
			set clones($host) $clonenicks
			unset clonenicks
		}
	}
	if [array exists clones] {
		set counter 0
		foreach addy [array names clones] {
			incr counter
			putdcc $idx " $counter. $addy \($clones($addy)): $clonelist($channel!$addy)"
		}
		unset counter
	} { putdcc $idx "No clones detected in $channel" }
	return 1
}

proc clonecheckload {} {
	global clonecheck clonelist clone_loaded
	
	if {$clonecheck} {
	# When activated, make all binds, and refresh clones-array
		bind join - * clone_detect
		bind part - * clone_remove
		bind sign - * clone_quit
		bind kick - * clone_kick
		bind splt - * clone_remove
		bind rejn - * clone_detect
		set clone_loaded 1
		# begins in 10 secs (not ON rehash, cause chan-userlist is not ok yet)
		# if bot was restarted, set_clones is called 1 min after bot joins chan
		# (see clone_detect)
		utimer 10 set_clones
	} {
	# When not activated, remove all bindings and clean the clonelist from mem
		if {$clone_loaded} {
			unbind join - * clone_detect
			unbind part - * clone_remove
			unbind sign - * clone_quit
			unbind kick - * clone_kick
			unbind splt - * clone_remove
			unbind rejn - * clone_detect
		}
		set clone_loaded 1
		if {[array exists clonelist]} { unset clonelist }
	}
}

global clone_loaded
set clone_loaded 0
clonecheckload

if {$clonecheck} { set clonestatus "on" } { set clonestatus "off" }

putlog "- Clone detection v3.5 \($clonestatus\) by By-Tor, Ernst & Flash."
putlog "  Max clones allowed: $maxclone. Banning for $clonebantime mins."

unset clonestatus
