X-Git-Url: https://git.distorted.org.uk/~mdw/ircbot/blobdiff_plain/732b11e0bed663e0596d26f3d073e4e09a2a58ee..b3d361abe62659d18e3fa9a1d3156482ea97a922:/bot.tcl diff --git a/bot.tcl b/bot.tcl index ec81dcd..bef0ead 100755 --- a/bot.tcl +++ b/bot.tcl @@ -2,7 +2,18 @@ set host chiark set port 6667 -set nick Blight +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 sendout {command args} { global sock @@ -18,7 +29,7 @@ proc sendout {command args} { } set args [lreplace $args 0 -1 $command] set string [join $args { }] - puts "-> $string" + puts "[clock seconds] -> $string" puts $sock $string } @@ -76,9 +87,12 @@ proc onread {args} { [regexp {^[&#+!]} [lindex $params 0]] && ![regexp {^!} [lindex $params 1]]} { # on-channel message, ignore + catch { + recordlastseen_p $prefix "talking on [lindex $params 0]" 1 + } return } - log "<- $org" + log "[clock seconds] <- $org" set procname msg_$command if {[catch { info body $procname }]} { return } if {[catch { @@ -89,6 +103,13 @@ proc onread {args} { } } +proc sendprivmsg {dest l} { + sendout [expr {[ischan $dest] ? "PRIVMSG" : "NOTICE"}] $dest $l +} +proc sendaction {dest what} { sendout PRIVMSG $dest "\001ACTION $what\001" } +proc msendprivmsg {dest ll} { foreach l $ll { sendprivmsg $dest $l } } +proc msendprivmsg_delayed {delay dest ll} { after $delay [list msendprivmsg $dest $ll] } + proc prefix_none {} { upvar 1 p p if {[string length $p]} { error "prefix specified" } @@ -104,26 +125,362 @@ proc check_nick {n} { if {[regexp {^[-0-9]} $n]} { error "bad nick start" } } +proc ischan {dest} { + return [regexp {^[&#+!]} $dest] +} + +proc irctolower {v} { + foreach {from to} [list "\\\[" "{" \ + "\\\]" "}" \ + "\\\\" "|" \ + "~" "^"] { + regsub -all $from $v $to v + } + return [string tolower $v] +} + proc prefix_nick {} { global nick upvar 1 p p upvar 1 n n if {![regexp {^([^!]+)!} $p dummy n]} { error "not from nick" } check_nick $n - if {"[string tolower $n]" == "$nick"} { error "from myself" } + if {"[irctolower $n]" == "[irctolower $nick]"} { error "from myself" } } +proc showintervalsecs {howlong} { + if {$howlong < 1000} { + return "${howlong}s" + } else { + if {$howlong < 1000000} { + set pfx k + set scale 1000 + } else { + set pfx M + set scale 1000000 + } + set value [expr "$howlong.0 / $scale"] + foreach {min format} {100 %.0f 10 %.1f 1 %.2f} { + if {$value < $min} continue + return [format "$format${pfx}s" $value] + } + } +} + +proc showinterval {howlong} { + if {$howlong <= 0} { + return {just now} + } else { + return "[showintervalsecs $howlong] ago" + } +} + +proc showtime {when} { + return [showinterval [expr {[clock seconds] - $when}]] +} + +proc def_msgproc {name argl body} { + proc msg_$name "varbase $argl" "\ + upvar #0 msg/\$varbase/dest d\n\ + upvar #0 msg/\$varbase/str s\n\ + upvar #0 msg/\$varbase/accum a\n\ +$body" +} + +def_msgproc begin {dest str} { + set d $dest + set s $str + set a {} +} + +def_msgproc append {str} { + set ns "$s$str" + if {[string length $s] && [string length $ns] > 65} { + msg__sendout $varbase + set s " [string trimleft $str]" + } else { + set s $ns + } +} + +def_msgproc finish {} { + msg__sendout $varbase + unset s + unset d + return $a +} + +def_msgproc _sendout {} { + lappend a [string trimright $s] + set s {} +} + +proc looking_whenwhere {when where} { + set str [showtime [expr {$when-1}]] + if {[string length $where]} { append str " on $where" } + return $str +} + +proc recordlastseen_n {n how here} { + global lastseen lookedfor + set lastseen([irctolower $n]) [list $n [clock seconds] $how] + if {!$here} return + upvar #0 lookedfor([irctolower $n]) lf + if {[info exists lf]} { + switch -exact [llength $lf] { + 0 { + set ml {} + } + 1 { + manyset [lindex $lf 0] when who where + set ml [list \ + "FYI, $who was looking for you [looking_whenwhere $when $where]."] + } + default { + msg_begin tosend $n "FYI, people have been looking for you:" + set i 0 + set fin "" + foreach e $lf { + incr i + if {$i == 1} { + msg_append tosend " " + } elseif {$i == [llength $lf]} { + msg_append tosend " and " + set fin . + } else { + msg_append tosend ", " + } + manyset $e when who where + msg_append tosend \ + "$who ([looking_whenwhere $when $where])$fin" + } + set ml [msg_finish tosend] + } + } + unset lf + msendprivmsg_delayed 1000 $n $ml + } +} + +proc recordlastseen_p {p how here} { + prefix_nick + recordlastseen_n $n $how $here +} + +proc chanmode_arg {} { + upvar 2 args cm_args + set rv [lindex $cm_args 0] + set cm_args [lreplace cm_args 0 0] + return $rv +} + +proc chanmode_o1 {m g p chan} { + global nick + prefix_nick + set who [chanmode_arg] + recordlastseen_n $n "being nice to $who" 1 + if {"[irctolower $who]" == "[irctolower $nick]"} { + sendprivmsg $n Thanks. + } +} + +proc chanmode_o0 {m g p chan} { + global nick chandeop + prefix_nick + set who [chanmode_arg] + recordlastseen_p $p "being mean to $who" 1 + if {"[irctolower $who]" == "[irctolower $nick]"} { + set chandeop($chan) [list [clock seconds] $p] + } +} + +proc msg_MODE {p c dest modelist args} { + if {![ischan $dest]} return + if {[regexp {^\-(.+)$} $modelist dummy modelist]} { + set give 0 + } elseif {[regexp {^\+(.+)$} $modelist dummy modelist]} { + set give 1 + } else { + error "invalid modelist" + } + foreach m [split $modelist] { + set procname chanmode_$m$give + if {[catch { info body $procname }]} { + recordlastseen_p $p "fiddling with $dest" 1 + } else { + $procname $m $give $p $dest + } + } +} + +proc msg_NICK {p c newnick} { + prefix_nick + recordlastseen_n $n "changing nicks to $newnick" 0 + recordlastseen_n $newnick "changing nicks from $n" 1 +} + +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_PRIVMSG {p c dest text} { prefix_nick - if {[regexp {^[&#+!]} $dest]} { - set what "!..." - set them it + if {[ischan $dest]} { + recordlastseen_n $n "invoking me in $dest" 1 + set output $dest + } else { + recordlastseen_n $n "talking to me" 1 + set output $n + } + + if {[catch { + regsub {^! *} $text {} text + set ucmd [ta_word] + set procname ucmd/[string tolower $ucmd] + if {[catch { info body $procname }]} { + error "unknown command; try help for help" + } + $procname $p $dest + } rv]} { + 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 l [split $val "\n"] { + sendprivmsg $td $l + } + } + foreach {td val} [list $n $priv_acts $output $pub_acts] { + foreach l [split $val "\n"] { + sendaction $td $l + } + } + } +} + +proc ta_nomore {} { + upvar 1 text text + if {[string length $text]} { error "too many parameters" } +} + +proc ta_word {} { + upvar 1 text text + if {![regexp {^([^ ]+) *(.*)} $text dummy firstword text]} { + error "too few parameters" + } + return $firstword +} + +proc ta_nick {} { + upvar 1 text text + set v [ta_word] + check_nick $v + return $v +} + +proc def_ucmd {cmdname body} { + proc ucmd/$cmdname {p dest} " upvar 1 text text\n$body" +} + +proc ucmdr {priv pub args} { + return -code return [concat [list $priv $pub] $args] +} + +proc ucmd_sendhelp {} { + 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 +Send commands to be by /msg, or say them in channel with ! in front.} {} +} + +def_ucmd help { ta_nomore; ucmd_sendhelp } + +def_ucmd ? { ta_nomore; ucmd_sendhelp } + +proc manyset {list args} { + foreach val $list var $args { + upvar 1 $var my + set my $val + } +} + +def_ucmd summon { + set target [ta_word] + ta_nomore + if { + [string length $target] > 8 || + [regexp {[^-0-9a-z]} $target] || + ![regexp {^[a-z]} $target] + } { error "invalid username" } + prefix_nick + + upvar #0 lastsummon($target) ls + set now [clock seconds] + if {[info exists ls]} { + set interval [expr {$now - $ls}] + if {$interval < 30} { + ucmdr {} \ + "Please be patient; $target was summoned only [showinterval $interval]." + } + } + regsub {^[^!]*!} $p {} path + if {[catch { + exec userv --timeout 3 $target irc-summon $n $path \ + [expr {[ischan $dest] ? "$dest" : ""}] \ + < /dev/null + } rv]} { + regsub -all "\n" $rv { / } rv + error $rv + } + if {[regexp {^problem (.*)} $rv dummy problem]} { + ucmdr {} "The user `$target' $problem." + } elseif {[regexp {^ok ([^ ]+) ([0-9]+)$} $rv dummy tty idlesince]} { + set idletime [expr {$now - $idlesince}] + set ls $now + ucmdr {} {} {} "invites $target ($tty[expr { + $idletime > 10 ? ", idle for [showintervalsecs $idletime]" : "" + }]) to [expr { + [ischan $dest] ? "join us here" : "talk to you" + }]." + } else { + error "unexpected response from userv service: $rv" + } +} + +proc md5sum {value} { exec md5sum << $value } + +def_ucmd seen { + global lastseen nick + prefix_nick + set ncase [ta_nick] + set nlower [irctolower $ncase] + ta_nomore + set now [clock seconds] + if {"$nlower" == "[irctolower $nick]"} { + error "I am not self-aware." + } elseif {![info exists lastseen($nlower)]} { + set rstr "I've never seen $ncase." } else { - set what "private messages" - set them them + manyset $lastseen($nlower) realnick time what + set howlong [expr {$now - $time}] + set string [showinterval $howlong] + set rstr "I last saw $realnick $string, $what." } - sendout PRIVMSG $n \ - "I will respond to $what at some point; for now I just log $them." + if {[ischan $dest]} { + set where $dest + } else { + set where {} + } + upvar #0 lookedfor($nlower) lf + if {[info exists lf]} { set oldvalue $lf } else { set oldvalue {} } + set lf [list [list $now $n $where]] + foreach v $oldvalue { + if {"[irctolower [lindex $v 1]]" == "[irctolower $n]"} continue + lappend lf $v + } + ucmdr {} $rstr } if {![info exists sock]} { @@ -132,11 +489,11 @@ 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 } -if {![regexp {tclsh} $argv0]} { - vwait terminate -} +#if {![regexp {tclsh} $argv0]} { +# vwait terminate +#}