
# Clone detection Version 3A from By-Tor & Ernst
# 24/11/97 Ernst <ernst@studbox.uni-stuttgart.de>

# Ernst's eggdrop page:  http://www.sodre.net/ernst/eggdrop/

# Version 3A for eggdrop 1.1 and 1.2 ONLY	<- you are HERE
# Version 3B for eggdrop 1.3 ONLY

# New in version 3A:
# - Updated for eggdrop 1.1.x and 1.2.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-3A.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 - 1
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 {
	"bbs.cultura.com.br"
	"bbs.record.com.br"
	"bbsservo.servo.com.br"
	"infolink.com.br"
	"mais.sul.com.br"
	"vixnet.com.br"
	"www1.highway.com.br"
	"www.gol.com.br"
	"*.uni-stuttgart.de"
}

# 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 chan [lindex [console $idx] 0]
	if {[matchattr $hand m] || [matchattr $hand o]} { console $idx +1 }
	if {[matchchanattr $hand m $chan] || [matchchanattr $hand o $chan]} { 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 clones dcc_clone_stats
bind dcc M clonescheck dcc_clonecheck
bind dcc 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
	if {![validchan $chan]} { return "" }
	set kicked ""
	foreach thisnick [chanlist $chan] {
		if {$thisnick == $botnick} { continue }
		if {[string match "$mask" "$thisnick![getchanhost $thisnick $chan]"]} {
			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
			}
			scan [string tolower [getchanhost $nick $chan]] "%\[^@]@%s" host host
			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] || \
					[matchchanattr $hand $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] || \
			[matchchanattr $hand $exceptionflag $chan]}	{ return 0 }
	}
	scan [string tolower $uhost] "%\[^@]@%s" host host
	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] {
			scan [string tolower [getchanhost $nick2 $chan]] "%\[^@]@%s" host2 host2
			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 } {
	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
		}
	}
	scan [string tolower $uhost] "%\[^@]@%s" host host
	foreach exceptionflag $nocloneflag {
		if {[matchattr $hand $exceptionflag] || \
			[matchchanattr $hand $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] {
			scan [string tolower [getchanhost $n $chan]] "%\[^@]@%s" host2 host2
			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 \002on\002 already."
			return 0
		}
	} {
		if {$clonecheck == 1} {
			set clonecheck 0
			putdcc $idx "Clone-checking is now \002off\002."
			clonecheckload
			return 1
		} {
			putdcc $idx "Clone-checking was \002off\002 already."
			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:"
	foreach addy [array names clonelist] {
		if {$clonelist($addy) > 0} {
			scan $addy "%\[^!]!%s" chan host
			if {$chan != [string tolower $channel]} { continue }
			foreach nick [chanlist $chan] {
				scan [string tolower [getchanhost $nick $chan]] "%\[^@]@%s" host2 host2
				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
		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
		}
		set clone_loaded 1
		if {[array exists clonelist]} { unset clonelist }
	}
}

clonecheckload

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

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

unset clonestatus
