X-Git-Url: https://git.distorted.org.uk/~mdw/ircbot/blobdiff_plain/e6cc22dcd98951a1208d81f375f587d32c38a1d7..c362e1729f5517438d2d96c9793cf5754cdb3f3e:/bot.tcl diff --git a/bot.tcl b/bot.tcl index f3db5c1..b187113 100755 --- a/bot.tcl +++ b/bot.tcl @@ -15,6 +15,36 @@ if {![info exists globalsecret]} { unset gsfile } +proc manyset {list args} { + foreach val $list var $args { + upvar 1 $var my + set my $val + } +} + +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 if {[llength $args]} { @@ -57,15 +87,24 @@ proc bgerror {msg} { } proc onread {args} { - global sock nick + global sock nick calling_nick errorInfo errorCode if {[gets $sock line] == -1} { set terminate 1; return } regsub -all "\[^ -\176\240-\376\]" $line ? line set org $line + + set ei $errorInfo + set ec $errorCode + catch { unset calling_nick } + set errorInfo $ei + set errorCode $ec + if {[regexp -nocase {^:([^ ]+) (.*)} $line dummy prefix remain]} { set line $remain - if {[regexp {^([^!]+)!} $prefix dummy maybenick] && - "[irctolower $maybenick]" == "[irctolower $nick]"} return + if {[regexp {^([^!]+)!} $prefix dummy maybenick]} { + set calling_nick $maybenick + if {"[irctolower $maybenick]" == "[irctolower $nick]"} return + } } else { set prefix {} } @@ -106,7 +145,9 @@ proc onread {args} { } proc sendprivmsg {dest l} { - sendout [expr {[ischan $dest] ? "PRIVMSG" : "NOTICE"}] $dest $l + foreach v [split $l "\n"] { + sendout [expr {[ischan $dest] ? "PRIVMSG" : "NOTICE"}] $dest $v + } } proc sendaction {dest what} { sendout PRIVMSG $dest "\001ACTION $what\001" } proc msendprivmsg {dest ll} { foreach l $ll { sendprivmsg $dest $l } } @@ -153,6 +194,10 @@ proc prefix_nick {} { } proc showintervalsecs {howlong} { + return [showintervalsecs/[opt timeformat] $howlong] +} + +proc showintervalsecs/ks {howlong} { if {$howlong < 1000} { return "${howlong}s" } else { @@ -171,6 +216,31 @@ proc showintervalsecs {howlong} { } } +proc format_qty {qty unit} { + set o $qty + append o " " + append o $unit + if {$qty != 1} { append o s } + return $o +} + +proc showintervalsecs/hms {qty} { + set ul {second 60 minute 60 hour 24 day 7 week} + set remainv 0 + while {[llength $ul] > 1 && $qty >= [set uv [lindex $ul 1]]} { + set remainu [lindex $ul 0] + set remainv [expr {$qty % $uv}] + set qty [expr {($qty-$remainv)/$uv}] + set ul [lreplace $ul 0 1] + } + set o [format_qty $qty [lindex $ul 0]] + if {$remainv} { + append o " " + append o [format_qty $remainv $remainu] + } + return $o +} + proc showinterval {howlong} { if {$howlong <= 0} { return {just now} @@ -279,12 +349,26 @@ proc chanmode_arg {} { } proc chanmode_o1 {m g p chan} { - global nick + global nick chan_initialop prefix_nick set who [chanmode_arg] recordlastseen_n $n "being nice to $who" 1 if {"[irctolower $who]" == "[irctolower $nick]"} { - sendprivmsg $n Thanks. + set nl [irctolower $n] + upvar #0 nick_unique($n) u + if {[chandb_exists $chan]} { + sendprivmsg $n Thanks. + } elseif {![info exists u]} { + sendprivmsg $n {Op me while not on the channel, why don't you ?} + } else { + set chan_initialop([irctolower $chan]) $u + sendprivmsg $n \ + "Thanks. You can use `channel manager ...' to register this channel." + if {![nickdb_exists $n] || ![string length [nickdb_get $n username]]} { + sendprivmsg $n \ + "(But to do that you must register your nick securely first.)" + } + } } } @@ -317,13 +401,26 @@ proc msg_MODE {p c dest modelist args} { } } +proc channel_noone_seen {chan} { + global nick_onchans + foreach n [array names nick_onchans] { + upvar #0 nick_onchans($n) oc + set oc [grep tc {"$tc" != "$chan"} $oc] + } +} + proc process_kickpart {chan user} { + global nick check_nick $user if {![ischan $chan]} { error "not a channel" } - + if {"[irctolower $user]" == "[irctolower $nick]"} { + channel_noone_seen $chan + } upvar #0 nick_onchans($user) oc set lc [irctolower $chan] set oc [grep tc {"$tc" != "$lc"} $oc] + if {![llength $oc]} { nick_forget $user } + nick_case $user } proc msg_KICK {p c chans users comment} { @@ -340,7 +437,8 @@ proc msg_KILL {p c user why} { nick_forget $user } -set nick_arys {onchans username} +set nick_counter 0 +set nick_arys {onchans username unique} proc nick_forget {n} { global nick_arys @@ -348,10 +446,16 @@ proc nick_forget {n} { upvar #0 nick_${ary}($n) av catch { unset av } } + nick_case $n +} + +proc nick_case {n} { + global nick_case + set nick_case([irctolower $n]) $n } proc msg_NICK {p c newnick} { - global nick_arys + global nick_arys nick_case prefix_nick recordlastseen_n $n "changing nicks to $newnick" 0 recordlastseen_n $newnick "changing nicks from $n" 1 @@ -361,6 +465,14 @@ proc msg_NICK {p c newnick} { if {[info exists new]} { error "nick collision ?! $ary $n $newnick" } if {[info exists old]} { set new $old; unset old } } + nick_case $newnick +} + +proc nick_ishere {n} { + global nick_counter + upvar #0 nick_unique($n) u + if {![info exists u]} { set u [incr nick_counter].$n.[clock seconds] } + nick_case $n } proc msg_JOIN {p c chan} { @@ -368,6 +480,7 @@ proc msg_JOIN {p c chan} { recordlastseen_n $n "joining $chan" 1 upvar #0 nick_onchans($n) oc lappend oc [irctolower $chan] + nick_ishere $n } proc msg_PART {p c chan} { prefix_nick @@ -389,6 +502,7 @@ proc msg_PRIVMSG {p c dest text} { recordlastseen_n $n "talking to me" 1 set output $n } + nick_case $n if {[catch { regsub {^! *} $text {} text @@ -402,14 +516,14 @@ 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 } } } @@ -433,16 +547,14 @@ proc msg_353 {p c dest type chan nicklist} { 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] - } + channel_noone_seen $chan foreach n [split $nicklist { }] { regsub {^[@+]} $n {} n - check_nick $n if {![string length $n]} continue + check_nick $n upvar #0 nick_onchans($n) oc lappend oc $chan + nick_ishere $n } } @@ -458,6 +570,11 @@ proc msg_366 {p c args} { unset names_chans } +proc ta_anymore {} { + upvar 1 text text + return [expr {!![string length $text]}] +} + proc ta_nomore {} { upvar 1 text text if {[string length $text]} { error "too many parameters" } @@ -491,33 +608,39 @@ proc loadhelp {} { 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 + try_except_finally { + 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" } - } 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" } + } {} { + close $f } - if {[info exists topic]} { error "unfinished topic $topic" } } def_ucmd help { upvar #0 help_topics([irctolower [string trim $text]]) info - if {![info exists info]} { error "no help on $text, sorry." {} } + if {![info exists info]} { ucmdr "No help on $text, sorry." {} } ucmdr $info {} } @@ -526,21 +649,390 @@ def_ucmd ? { ucmdr $help_topics() {} } -proc manyset {list args} { - foreach val $list var $args { - upvar 1 $var my - set my $val +proc check_username {target} { + if { + [string length $target] > 8 || + [regexp {[^-0-9a-z]} $target] || + ![regexp {^[a-z]} $target] + } { error "invalid username" } +} + +proc somedb__head {} { + uplevel 1 { + set idl [irctolower $id] + upvar #0 ${nickchan}db($idl) ndbe + binary scan $idl H* idh + set idfn $fprefix$idh + if {![info exists iddbe] && [file exists $idfn]} { + set f [open $idfn r] + try_except_finally { set newval [read $f] } {} { close $f } + if {[llength $newval] % 2} { error "invalid length" } + set iddbe $newval + } + } +} + +proc def_somedb {name arglist body} { + foreach {nickchan fprefix} {nick users/n chan chans/c} { + proc ${nickchan}db_$name $arglist \ + "set nickchan $nickchan; set fprefix $fprefix; somedb__head; $body" + } +} + +def_somedb exists {id} { + return [info exists iddbe] +} + +def_somedb delete {id} { + catch { unset iddbe } + file delete $idfn +} + +set default_settings_nick {timeformat ks} +set default_settings_chan {autojoin 1} + +def_somedb set {id args} { + upvar #0 default_settings_$nickchan def + if {![info exists iddbe]} { set iddbe $def } + foreach {key value} [concat $iddbe $args] { set a($key) $value } + set newval {} + foreach {key value} [array get a] { lappend newval $key $value } + set f [open $idfn.new w] + try_except_finally { + puts $f $newval + close $f + file rename -force $idfn.new $idfn + } { + } { + catch { close $f } + } + set iddbe $newval +} + +def_somedb get {id key} { + upvar #0 default_settings_$nickchan def + if {[info exists iddbe]} { + set l $iddbe + } else { + set l $def + } + foreach {tkey value} $l { + if {"$tkey" == "$key"} { return $value } } + error "unset setting $key" +} + +proc opt {key} { + global calling_nick + if {[info exists calling_nick]} { set n $calling_nick } { set n {} } + return [nickdb_get $n $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_get $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" + } +} + +proc channel_securitycheck {channel n} { + # You must also call `nick_securitycheck 1' + set mgrs [chandb_get $channel managers] + if {[lsearch -exact [irctolower $mgrs] [irctolower $n]] < 0} { + error "you are not a manager of $channel" + } +} + +proc def_chancmd {name body} { + proc channel/$name {} \ + " upvar 1 target chan; upvar 1 n n; upvar 1 text text; $body" +} + +def_chancmd manager { + set opcode [ta_word] + switch -exact _$opcode { + _= { set ml {} } + _+ - _- { + if {[chandb_exists $chan]} { + set ml [chandb_get $chan managers] + } else { + set ml [list [irctolower $n]] + } + } + default { + error "`channel manager' opcode must be one of + - =" + } + } + foreach nn [split $text " "] { + if {![string length $nn]} continue + check_nick $nn + set nn [irctolower $nn] + if {"$opcode" != "-"} { + lappend ml $nn + } else { + set ml [grep nq {"$nq" != "$nn"} $ml] + } + } + if {[llength $ml]} { + chandb_set $chan managers $ml + ucmdr "Managers of $chan: $ml" {} + } else { + chandb_delete $chan + ucmdr {} {} "forgets about managing $chan." {} + } +} + +def_chancmd autojoin { + set yesno [ta_word] + switch -exact [string tolower $yesno] { + no { set nv 0 } + yes { set nv 1 } + default { error "channel autojoin must be `yes' or `no' } + } + chandb_set $chan autojoin $nv +} + +def_chancmd show { + if {[chandb_exists $chan]} { + set l "Settings for $chan: autojoin " + append l [lindex {no yes} [chandb_get $chan autojoin]] + append l "\nManagers: " + append l [join [chandb_get $chan managers] " "] + ucmdr {} $l + } else { + ucmdr {} "The channel $chan is not managed." + } +} + +def_ucmd op { + if {[ischan $dest]} { set target $dest } + if {[ta_anymore]} { set target [ta_word] } + ta_nomore + if {![info exists target]} { error "you must specify, or !... on, the channel" } + if {![ischan $target]} { error "not a valid channel" } + if {![chandb_exists $target]} { error "$target is not a managed channel." } + prefix_nick + nick_securitycheck 1 + channel_securitycheck $target $n + sendout MODE $target +o $n +} + +def_ucmd channel { + if {[ischan $dest]} { set target $dest } + if {![ta_anymore]} { + set subcmd show + } else { + set subcmd [ta_word] + } + if {[ischan $subcmd]} { + set target $subcmd + if {![ta_anymore]} { + set subcmd show + } else { + set subcmd [ta_word] + } + } + if {![info exists target]} { error "privately, you must specify a channel" } + set procname channel/$subcmd + if {"$subcmd" != "show"} { + if {[catch { info body $procname }]} { error "unknown channel setting $subcmd" } + prefix_nick + nick_securitycheck 1 + if {[chandb_exists $target]} { + channel_securitycheck $target $n + } else { + upvar #0 chan_initialop([irctolower $target]) io + upvar #0 nick_unique($n) u + if {![info exists io]} { error "$target is not a managed channel" } + if {"$io" != "$u"} { error "you are not the interim manager of $target" } + if {"$subcmd" != "manager"} { error "use `channel manager' first" } + } + } + channel/$subcmd +} + +def_ucmd who { + if {[ta_anymore]} { + set target [ta_word]; ta_nomore + set myself 1 + } else { + prefix_nick + set target $n + set myself [expr {"$target" != "$n"}] + } + upvar #0 nick_case([irctolower $target]) nc + set nshow $target + if {[info exists nc]} { + upvar #0 nick_onchans($nc) oc + upvar #0 nick_username($nc) nu + if {[info exists oc]} { set nshow $nc } + } + if {![nickdb_exists $target]} { + set ol "$nshow is not a registered nick." + } elseif {[string length [set username [nickdb_get $target username]]]} { + set ol "The nick $nshow belongs to the user $username." + } else { + set ol "The nick $nshow is registered (but not to a username)." + } + if {![info exists nc] || ![info exists oc]} { + if {$myself} { + append ol "\nI can't see $nshow on anywhere." + } else { + append ol "\nYou aren't on any channels with me." + } + } elseif {![info exists nu]} { + append ol "\n$nshow has not identified themselves." + } elseif {![info exists username]} { + append ol "\n$nshow has identified themselves as the user $nu." + } elseif {"$nu" != "$username"} { + append ol "\nHowever, $nshow is being used by the user $nu." + } else { + append ol "\n$nshow has identified themselves to me." + } + ucmdr {} $ol +} + +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', or use `register insecure'." + } + 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." + } + } + } +} + +proc timeformat_desc {tf} { + switch -exact $tf { + ks { return "Times will be displayed in seconds or kiloseconds." } + hms { return "Times will be displayed in hours, minutes, etc." } + default { error "invalid timeformat: $v" } + } +} + +proc def_setting {opt show_body set_body} { + proc set_show/$opt {} " + upvar 1 n n + set opt $opt + $show_body" + if {![string length $set_body]} return + proc set_set/$opt {} " + upvar 1 n n + upvar 1 text text + set opt $opt + $set_body" +} + +def_setting timeformat { + set tf [nickdb_get $n timeformat] + return "$tf: [timeformat_desc $tf]" +} { + set tf [string tolower [ta_word]] + ta_nomore + set desc [timeformat_desc $tf] + nickdb_set $n timeformat $tf + ucmdr {} $desc +} + +def_setting security { + set s [nickdb_get $n username] + if {[string length $s]} { + return "Your nick, $n, is controlled by the user $s." + } else { + return "Your nick, $n, is not secure." + } +} {} + +def_ucmd set { + prefix_nick + check_notonchan + if {![nickdb_exists $n]} { + ucmdr {} "You are unknown to me and so have no settings. (Use `register'.)" + } + if {![ta_anymore]} { + set ol {} + foreach proc [lsort [info procs]] { + if {![regexp {^set_show/(.*)$} $proc dummy opt]} continue + lappend ol [format "%-10s %s" $opt [set_show/$opt]] + } + ucmdr {} [join $ol "\n"] + } else { + set opt [ta_word] + if {[catch { info body set_show/$opt }]} { + error "no setting $opt" + } + if {![ta_anymore]} { + ucmdr {} "$opt [set_show/$opt]" + } else { + nick_securitycheck 0 + if {[catch { info body set_set/$opt }]} { + error "setting $opt cannot be set with `set'" + } + set_set/$opt + } + } +} + +def_ucmd identpass { + set username [ta_word] + set passmd5 [md5sum "[ta_word]\n"] + ta_nomore + prefix_nick + check_notonchan + 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 - if { - [string length $target] > 8 || - [regexp {[^-0-9a-z]} $target] || - ![regexp {^[a-z]} $target] - } { error "invalid username" } + check_username $target prefix_nick upvar #0 lastsummon($target) ls