X-Git-Url: https://git.distorted.org.uk/~mdw/ircbot/blobdiff_plain/7ce72032518e164fbcef80ed0c3b20b91a1a9d26..7a70431a07263946551183efcf340fdf7a91befd:/bot.tcl diff --git a/bot.tcl b/bot.tcl index 54cda63..967b095 100755 --- a/bot.tcl +++ b/bot.tcl @@ -3,6 +3,40 @@ set host chiark set port 6667 if {![info exists nick]} { set nick Blight } +if {![info exists ownfullname]} { set ownfullname "here to Help" } +set ownmailaddr blight@chiark.greenend.org.uk + +if {![info exists globalsecret]} { + set gsfile [open /dev/urandom r] + fconfigure $gsfile -translation binary + set globalsecret [read $gsfile 32] + binary scan $globalsecret H* globalsecret + close $gsfile + unset gsfile +} + +proc try_except_finally {try except finally} { + global errorInfo errorCode + set er [catch { uplevel 1 $try } emsg] + if {$er} { + set ei $errorInfo + set ec $errorCode + if {[catch { uplevel 1 $except } emsg3]} { + append ei "\nALSO ERROR HANDLING ERROR:\n$emsg3" + } + } + set er2 [catch { uplevel 1 $finally } emsg2] + if {$er} { + if {$er2} { + append ei "\nALSO ERROR CLEANING UP:\n$emsg2" + } + return -code $er -errorinfo $ei -errorcode $ec $emsg + } elseif {$er2} { + return -code $er2 -errorinfo $errorInfo -errorcode $errorCode $emsg2 + } else { + return $emsg + } +} proc sendout {command args} { global sock @@ -46,14 +80,15 @@ proc bgerror {msg} { } proc onread {args} { - global sock + global sock nick if {[gets $sock line] == -1} { set terminate 1; return } -binary scan $line H* inhex; puts >$inhex< regsub -all "\[^ -\176\240-\376\]" $line ? line set org $line if {[regexp -nocase {^:([^ ]+) (.*)} $line dummy prefix remain]} { set line $remain + if {[regexp {^([^!]+)!} $prefix dummy maybenick] && + "[irctolower $maybenick]" == "[irctolower $nick]"} return } else { set prefix {} } @@ -135,7 +170,9 @@ proc prefix_nick {} { upvar 1 n n if {![regexp {^([^!]+)!} $p dummy n]} { error "not from nick" } check_nick $n - if {"[irctolower $n]" == "[irctolower $nick]"} { error "from myself" } + if {"[irctolower $n]" == "[irctolower $nick]"} { + error "from myself" {} {} + } } proc showintervalsecs {howlong} { @@ -303,15 +340,68 @@ proc msg_MODE {p c dest modelist args} { } } +proc process_kickpart {chan user} { + check_nick $user + if {![ischan $chan]} { error "not a channel" } + + upvar #0 nick_onchans($user) oc + set lc [irctolower $chan] + set oc [grep tc {"$tc" != "$lc"} $oc] +} + +proc msg_KICK {p c chans users comment} { + set chans [split $chans ,] + set users [split $users ,] + if {[llength $chans] > 1} { + foreach chan $chans user $users { process_kickpart $chan $user } + } else { + foreach user $users { process_kickpart [lindex $chans 0] $user } + } +} + +proc msg_KILL {p c user why} { + nick_forget $user +} + +set nick_arys {onchans username} + +proc nick_forget {n} { + global nick_arys + foreach ary $nick_arys { + upvar #0 nick_${ary}($n) av + catch { unset av } + } +} + proc msg_NICK {p c newnick} { + global nick_arys prefix_nick recordlastseen_n $n "changing nicks to $newnick" 0 recordlastseen_n $newnick "changing nicks from $n" 1 + foreach ary $nick_arys { + upvar #0 nick_${ary}($n) old + upvar #0 nick_${ary}($newnick) new + if {[info exists new]} { error "nick collision ?! $ary $n $newnick" } + if {[info exists old]} { set new $old; unset old } + } } -proc msg_JOIN {p c chan} { recordlastseen_p $p "joining $chan" 1 } -proc msg_PART {p c chan} { recordlastseen_p $p "leaving $chan" 1 } -proc msg_QUIT {p c why} { recordlastseen_p $p "leaving ($why)" 0 } +proc msg_JOIN {p c chan} { + prefix_nick + recordlastseen_n $n "joining $chan" 1 + upvar #0 nick_onchans($n) oc + lappend oc [irctolower $chan] +} +proc msg_PART {p c chan} { + prefix_nick + recordlastseen_n $n "leaving $chan" 1 + process_kickpart $chan $n +} +proc msg_QUIT {p c why} { + prefix_nick + recordlastseen_n $n "leaving ($why)" 0 + nick_forget $n +} proc msg_PRIVMSG {p c dest text} { prefix_nick @@ -335,19 +425,62 @@ proc msg_PRIVMSG {p c dest text} { sendprivmsg $n "error: $rv" } else { manyset $rv priv_msgs pub_msgs priv_acts pub_acts - foreach {td val} [list $n $priv_msgs $output $pub_msgs] { + foreach {td val} [list $n $priv_acts $output $pub_acts] { foreach l [split $val "\n"] { - sendprivmsg $td $l + sendaction $td $l } } - foreach {td val} [list $n $priv_acts $output $pub_acts] { + foreach {td val} [list $n $priv_msgs $output $pub_msgs] { foreach l [split $val "\n"] { - sendaction $td $l + sendprivmsg $td $l } } } } +proc msg_INVITE {p c n chan} { + after 1000 [list sendout JOIN $chan] +} + +proc grep {var predicate list} { + set o {} + upvar 1 $var v + foreach v $list { + if {[uplevel 1 [list expr $predicate]]} { lappend o $v } + } + return $o +} + +proc msg_353 {p c dest type chan nicklist} { + global names_chans nick_onchans + if {![info exists names_chans]} { set names_chans {} } + set chan [irctolower $chan] + lappend names_chans $chan + foreach n [array names nick_onchans] { + upvar #0 nick_onchans($n) oc + set oc [grep tc {"$tc" != "$chan"} $oc] + } + foreach n [split $nicklist { }] { + regsub {^[@+]} $n {} n + check_nick $n + if {![string length $n]} continue + upvar #0 nick_onchans($n) oc + lappend oc $chan + } +} + +proc msg_366 {p c args} { + global names_chans nick_onchans + if {[llength names_chans] > 1} { + foreach n [array names nick_onchans] { + upvar #0 nick_onchans($n) oc + set oc [grep tc {[lsearch -exact $tc $names_chans] >= 0} $oc] + if {![llength $oc]} { nick_forget $n } + } + } + unset names_chans +} + proc ta_nomore {} { upvar 1 text text if {[string length $text]} { error "too many parameters" } @@ -375,14 +508,45 @@ proc def_ucmd {cmdname body} { proc ucmdr {priv pub args} { return -code return [concat [list $priv $pub] $args] } - + +proc loadhelp {} { + global help_topics + + catch { unset help_topics } + set f [open helpinfos r] + set lno 0 + while {[gets $f l] >= 0} { + incr lno + if {[regexp {^#.*} $l]} { + } elseif {[regexp {^ *$} $l]} { + if {[info exists topic]} { + set help_topics($topic) [join $lines "\n"] + unset topic + unset lines + } + } elseif {[regexp {^!([-+._0-9a-z]*)$} $l dummy newtopic]} { + if {[info exists topic]} { error "help $newtopic while in $topic" } + set topic $newtopic + set lines {} + } elseif {[regexp {^[^!#]} $l]} { + set topic + lappend lines [string trimright $l] + } else { + error "eh ? $lno: $l" + } + } + if {[info exists topic]} { error "unfinished topic $topic" } +} + def_ucmd help { - ta_nomore - ucmdr \ -{Commands currently understood: -help get this list of commands -seen ask after someone (I'll tell them you asked) -summon invite a logged-on user onto IRC} {} + upvar #0 help_topics([irctolower [string trim $text]]) info + if {![info exists info]} { ucmdr "No help on $text, sorry." {} } + ucmdr $info {} +} + +def_ucmd ? { + global help_topics + ucmdr $help_topics() {} } proc manyset {list args} { @@ -392,14 +556,152 @@ proc manyset {list args} { } } -def_ucmd summon { - set target [ta_word] - ta_nomore +proc check_username {target} { if { [string length $target] > 8 || [regexp {[^-0-9a-z]} $target] || ![regexp {^[a-z]} $target] } { error "invalid username" } +} + +proc nickdb__head {} { + uplevel 1 { + set nl [irctolower $n] + upvar #0 nickdb($nl) ndbe + binary scan $nl H* nh + set nfn users/$nh + if {![info exists ndbe] && [file exists $nfn]} { + set f [file open $nfn r] + try_except_finally { set newval [read $f] } {} { close $f } + if {[llength $newval] % 2} { error "invalid length" } + set ndbe $newval + } + } +} + +proc def_nickdb {name arglist body} { + proc nickdb_$name $arglist "nickdb__head; $body" +} + +def_nickdb exists {n} { + return [info exists ndbe] +} + +def_nickdb delete {n} { + catch { unset ndbe } + file delete $nfn +} + +set default_settings {timeformat ks} + +def_nickdb set {n args} { + global default_settings + if {![info exists ndbe]} { set ndbe $default_settings } + foreach {key value} [concat $ndbe $args] { set a($key) $value } + set newval {} + foreach {key value} [array get a] { lappend newval $key $value } + set f [open $nfn.new w] + try_except_finally { + puts $f $newval + close $f + file rename -force $nfn.new $nfn + } { + catch { close $f } + } { + } + set ndbe $newval +} + +def_nickdb opt {n key} { + global default_settings + if {[info exists ndbe]} { + set l $ndbe + } else { + set l $default_settings + } + foreach {tkey value} $l { + if {"$tkey" == "$key"} { return $value } + } + error "unset setting $key" +} + +proc check_notonchan {} { + upvar 1 dest dest + if {[ischan $dest]} { error "that command must be sent privately" } +} + +proc nick_securitycheck {strict} { + upvar 1 n n + if {![nickdb_exists $n]} { error "you are unknown to me, use `register'." } + set wantu [nickdb_opt $n username] + if {![string length $wantu]} { + if {$strict} { + error "that feature is only available to secure users, sorry." + } else { + return + } + } + upvar #0 nick_username($n) nu + if {![info exists nu]} { + error "nick $n is secure, you must identify yourself first." + } + if {"$wantu" != "$nu"} { + error "you are the wrong user - the nick $n belongs to $wantu, not $nu" + } +} + +def_ucmd register { + prefix_nick + check_notonchan + set old [nickdb_exists $n] + if {$old} { nick_securitycheck 0 } + switch -exact [string tolower [string trim $text]] { + {} { + upvar #0 nick_username($n) nu + if {![info exists nu]} { + ucmdr {} \ + "You must identify yourself before using `register'. See `help identify'." + } + nickdb_set $n username $nu + ucmdr {} {} "makes a note of your username." {} + } + delete { + nickdb_delete $n + ucmdr {} {} "forgets your nickname." {} + } + insecure { + nickdb_set $n username {} + if {$old} { + ucmdr {} "Security is now disabled for your nickname !" + } else { + ucmdr {} "This is fine, but bear in mind that people will be able to mess with your settings. Channel management features need a secure registration." "makes an insecure registration for your nick." + } + } + } +} + +def_ucmd identpass { + set username [ta_word] + set passmd5 [md5sum [ta_word]] + ta_nomore + prefix_nick + check_isprivmsg + upvar #0 nick_onchans($n) onchans + if {![info exists onchans] || ![llength $onchans]} { + ucmdr "You must be on a channel with me to identify yourself." {} + } + check_username $username + exec userv --timeout 3 $username << "$passmd5\n" > /dev/null \ + irc-identpass $n + upvar #0 nick_username($n) rec_username + set rec_username $username + ucmdr "Pleased to see you, $username." {} +} + +def_ucmd summon { + set target [ta_word] + ta_nomore + check_username $target prefix_nick upvar #0 lastsummon($target) ls @@ -421,7 +723,7 @@ def_ucmd summon { error $rv } if {[regexp {^problem (.*)} $rv dummy problem]} { - ucmdr {} "$target $problem." + ucmdr {} "The user `$target' $problem." } elseif {[regexp {^ok ([^ ]+) ([0-9]+)$} $rv dummy tty idlesince]} { set idletime [expr {$now - $idlesince}] set ls $now @@ -435,6 +737,8 @@ def_ucmd summon { } } +proc md5sum {value} { exec md5sum << $value } + def_ucmd seen { global lastseen nick prefix_nick @@ -473,11 +777,13 @@ if {![info exists sock]} { #fconfigure $sock -translation binary fconfigure $sock -translation crlf - sendout USER guest 0 * "chiark testing bot" + sendout USER blight 0 * $ownfullname sendout NICK $nick fileevent $sock readable onread } +loadhelp + #if {![regexp {tclsh} $argv0]} { # vwait terminate #}