chiark / gitweb /
Bot is operator and can do kill
[ircbot.git] / bot.tcl
1 # Actual IRC bot code
2
3 set helpfile helpinfos
4
5 source irccore.tcl
6 source parsecmd.tcl
7 source stdhelp.tcl
8
9 proc privmsg_unlogged {prefix ischan params} {
10     if {!$ischan ||
11         [regexp {^![a-z][-a-z]*[a-z]( .*)?$} [lindex $params 1]]} {
12         return 0
13     }
14     # on-channel message, ignore
15     set chan [lindex $params 0]
16     upvar #0 chan_lastactivity([irctolower $chan]) la
17     set la [clock seconds]
18     catch { recordlastseen_p $prefix "talking on $chan" 1 }
19     return 1
20 }
21
22 proc showintervalsecs {howlong abbrev} {
23     return [showintervalsecs/[opt timeformat] $howlong $abbrev]
24 }
25
26 proc formatsf {pfx value} {
27    foreach {min format} { 100 %.0f 10 %.1f 0 %.2f} {
28         set fval [format $format $value]
29         if {$fval < $min} continue
30         return [format "$fval${pfx}" $value]
31    }
32 }
33
34 proc showintervalsecs/beat {howlong abbrev} {
35     # We split in two to avoid overflow problems.
36     if {$howlong < 86 } {
37         # mB's
38         set pfx mB
39         return [format "%.0fmB" [expr {round($howlong * 1.157)*10} ]]
40     } else {
41         if {$howlong < 86400 } {
42             # B's
43             set pfx B
44             set value [expr {$howlong / 86.4}]
45         } else {
46             # kB's
47             set pfx kB
48             set value [expr {$howlong / 86400.0}]
49         }
50     }
51     return [formatsf $pfx $value]
52 }
53
54 proc showintervalsecs/ks {howlong abbrev} {
55     if {$howlong < 1000} {
56         return "${howlong}s"
57     } else {
58         if {$howlong < 1000000} {
59             set pfx ks
60             set scale 1000
61         } else {
62             set pfx Ms
63             set scale 1000000
64         }
65         set value [expr "$howlong.0 / $scale"]
66         return [formatsf $pfx $value]
67     }
68 }
69
70 proc format_qty {qty unit abbrev} {
71     set o $qty
72     if {$abbrev} {
73         append o [string range $unit 0 0]
74     } else {
75         append o " "
76         append o $unit
77         if {$qty != 1} { append o s }
78     }
79     return $o
80 }
81
82 proc showintervalsecs/hms {qty abbrev} {
83     set ul {second 60 minute 60 hour 24 day 7 week}
84     set remainv 0
85     while {[llength $ul] > 1 && $qty >= [set uv [lindex $ul 1]]} {
86         set remainu [lindex $ul 0]
87         set remainv [expr {$qty % $uv}]
88         set qty [expr {($qty-$remainv)/$uv}]
89         set ul [lreplace $ul 0 1]
90     }
91     set o [format_qty $qty [lindex $ul 0] $abbrev]
92     if {$remainv} {
93         if {!$abbrev} { append o " " }
94         append o [format_qty $remainv $remainu $abbrev]
95     }
96     return $o
97 }
98
99 proc showinterval {howlong} {
100     if {$howlong <= 0} {
101         return {just now}
102     } else {
103         return "[showintervalsecs $howlong 0] ago"
104     }
105 }
106
107 proc showtime {when} {
108     return [showinterval [expr {[clock seconds] - $when}]]
109 }
110
111 proc def_msgproc {name argl body} {
112     proc msg_$name "varbase $argl" "\
113     upvar #0 msg/\$varbase/dest d\n\
114     upvar #0 msg/\$varbase/str s\n\
115     upvar #0 msg/\$varbase/accum a\n\
116 $body"
117 }
118
119 def_msgproc begin {dest str} {
120     set d $dest
121     set s $str
122     set a {}
123 }
124
125 def_msgproc append {str} {
126     set ns "$s$str"
127     if {[string length $s] && [string length $ns] > 65} {
128         msg__sendout $varbase
129         set s " [string trimleft $str]"
130     } else {
131         set s $ns
132     }
133 }
134
135 def_msgproc finish {} {
136     msg__sendout $varbase
137     unset s
138     unset d
139     return $a
140 }
141
142 def_msgproc _sendout {} {
143     lappend a [string trimright $s]
144     set s {}
145 }
146
147 proc looking_whenwhere {when where} {
148     set str [showtime [expr {$when-1}]]
149     if {[string length $where]} { append str " on $where" }
150     return $str
151 }
152
153 proc recordlastseen_n {n how here} {
154     global lastseen lookedfor
155     set lastseen([irctolower $n]) [list $n [clock seconds] $how]
156     if {!$here} return
157     upvar #0 lookedfor([irctolower $n]) lf
158     if {[info exists lf]} {
159         switch -exact [llength $lf] {
160             0 {
161                 set ml {}
162             }
163             1 {
164                 manyset [lindex $lf 0] when who where
165                 set ml [list \
166  "FYI, $who was looking for you [looking_whenwhere $when $where]."]
167             }
168             default {
169                 msg_begin tosend $n "FYI, people have been looking for you:"
170                 set i 0
171                 set fin ""
172                 foreach e $lf {
173                     incr i
174                     if {$i == 1} {
175                         msg_append tosend " "
176                     } elseif {$i == [llength $lf]} {
177                         msg_append tosend " and "
178                         set fin .
179                     } else {
180                         msg_append tosend ", "
181                     }
182                     manyset $e when who where
183                     msg_append tosend \
184                             "$who ([looking_whenwhere $when $where])$fin"
185                 }
186                 set ml [msg_finish tosend]
187             }
188         }
189         unset lf
190         msendprivmsg_delayed 1000 $n $ml
191     }
192 }
193
194 proc note_topic {showoff whoby topic} {
195     set msg "FYI, $whoby has changed the topic on $showoff"
196     if {[string length $topic] < 160} {
197         append msg " to $topic"
198     } else {
199         append msg " but it is too long to reproduce here !"
200     }
201     set showoff [irctolower $showoff]
202     set tell [chandb_get $showoff topictell]
203     if {[lsearch -exact $tell *] >= 0} {
204         set tryspies [chandb_list]
205     } else {
206         set tryspies $tell
207     }
208     foreach spy $tryspies {
209         set see [chandb_get $spy topicsee]
210         if {[lsearch -exact $see $showoff] >= 0 || \
211                 ([lsearch -exact $see *] >= 0 && \
212                 [lsearch -exact $tell $spy] >= 0)} {
213             sendprivmsg $spy $msg
214         }
215     }
216 }
217
218 proc recordlastseen_p {p how here} {
219     prefix_nick
220     recordlastseen_n $n $how $here
221 }
222
223 proc chanmode_arg {} {
224     upvar 2 args cm_args
225     set rv [lindex $cm_args 0]
226     set cm_args [lreplace cm_args 0 0]
227     return $rv
228 }
229
230 proc chanmode_o1 {m g p chan} {
231     global nick chan_initialop
232     prefix_nick
233     set who [chanmode_arg]
234     recordlastseen_n $n "being nice to $who" 1
235     if {"[irctolower $who]" == "[irctolower $nick]"} {
236         set nlower [irctolower $n]
237         upvar #0 nick_unique($nlower) u
238         if {[chandb_exists $chan]} {
239             sendprivmsg $n Thanks.
240         } elseif {![info exists u]} {
241             sendprivmsg $n {Op me while not on the channel, why don't you ?}
242         } else {
243             set chan_initialop([irctolower $chan]) $u
244             sendprivmsg $n \
245  "Thanks. You can use `channel manager ...' to register this channel."
246             if {![nickdb_exists $n] || ![string length [nickdb_get $n username]]} {
247                 sendprivmsg $n \
248  "(But to do that you must register your nick securely first.)"
249             }
250         }
251     }
252 }
253
254 proc chanmode_o0 {m g p chan} {
255     global nick chandeop
256     prefix_nick
257     set who [chanmode_arg]
258     recordlastseen_p $p "being mean to $who" 1
259     if {"[irctolower $who]" == "[irctolower $nick]"} {
260         set chandeop($chan) [list [clock seconds] $p]
261     }
262 }
263
264 proc msg_MODE {p c dest modelist args} {
265     if {![ischan $dest]} return
266     if {[regexp {^\-(.+)$} $modelist dummy modelist]} {
267         set give 0
268     } elseif {[regexp {^\+(.+)$} $modelist dummy modelist]} {
269         set give 1
270     } else {
271         error "invalid modelist"
272     }
273     foreach m [split $modelist] {
274         set procname chanmode_$m$give
275         if {[catch { info body $procname }]} {
276             recordlastseen_p $p "fiddling with $dest" 1
277         } else {
278             $procname $m $give  $p $dest
279         }
280     }
281 }
282
283 proc leaving {lchan} {
284     foreach luser [array names nick_onchans] {
285         upvar #0 nick_onchans($luser) oc
286         set oc [grep tc {"$tc" != "$lchan"} $oc]
287     }
288     upvar #0 chan_nicks($lchan) nlist
289     unset nlist
290     upvar #0 chan_lastactivity($lchan) la
291     catch { unset la }
292 }
293
294 proc doleave {lchan} {
295     sendout PART $lchan
296     leaving $lchan
297 }
298
299 proc dojoin {lchan} {
300     global chan_nicks
301     sendout JOIN $lchan
302     set chan_nicks($lchan) {}
303 }
304
305 proc check_justme {lchan} {
306     global nick
307     upvar #0 chan_nicks($lchan) nlist
308     if {[llength $nlist] != 1} return
309     if {"[lindex $nlist 0]" != "[irctolower $nick]"} return
310     if {[chandb_exists $lchan]} {
311         set mode [chandb_get $lchan mode]
312         if {"$mode" != "*"} {
313             sendout MODE $lchan $mode
314         }
315         set topic [chandb_get $lchan topicset]
316         if {[string length $topic]} {
317             sendout TOPIC $lchan $topic
318         }
319     } else {
320         doleave $lchan
321     }
322 }
323
324 proc process_kickpart {chan user} {
325     global nick
326     check_nick $user
327     set luser [irctolower $user]
328     set lchan [irctolower $chan]
329     if {![ischan $chan]} { error "not a channel" }
330     if {"$luser" == "[irctolower $nick]"} {
331         leaving $lchan
332     } else {
333         upvar #0 nick_onchans($luser) oc
334         upvar #0 chan_nicks($lchan) nlist
335         set oc [grep tc {"$tc" != "$lchan"} $oc]
336         set nlist [grep tn {"$tn" != "$luser"} $nlist]
337         nick_case $user
338         if {![llength $oc]} {
339             nick_forget $luser
340         } else {
341             check_justme $lchan
342         }
343     }
344 }
345
346 proc msg_TOPIC {p c dest topic} {
347     prefix_nick
348     if {![ischan $dest]} return
349     recordlastseen_n $n "changing the topic on $dest" 1
350     note_topic [irctolower $dest] $n $topic
351 }
352
353 proc msg_KICK {p c chans users comment} {
354     set chans [split $chans ,]
355     set users [split $users ,]
356     if {[llength $chans] > 1} {
357         foreach chan $chans user $users { process_kickpart $chan $user }
358     } else {
359         foreach user $users { process_kickpart [lindex $chans 0] $user }
360     }
361 }
362
363 proc msg_KILL {p c user why} {
364     nick_forget $user
365 }
366
367 set nick_counter 0
368 set nick_arys {onchans username unique}
369 # nick_onchans($luser) -> [list ... $lchan ...]
370 # nick_username($luser) -> <securely known local username>
371 # nick_unique($luser) -> <counter>
372 # nick_case($luser) -> $user  (valid even if no longer visible)
373 # nick_markid($luser) -> <after id for marktime>
374
375 # chan_nicks($lchan) -> [list ... $luser ...]
376 # chan_lastactivity($lchan) -> [clock seconds]
377
378 proc lnick_forget {luser} {
379     global nick_arys chan_nicks
380     lnick_marktime_cancel $luser
381     foreach ary $nick_arys {
382         upvar #0 nick_${ary}($luser) av
383         catch { unset av }
384     }
385     foreach lch [array names chan_nicks] {
386         upvar #0 chan_nicks($lch) nlist
387         set nlist [grep tn {"$tn" != "$luser"} $nlist]
388         check_justme $lch
389     }
390 }
391
392 proc nick_forget {user} {
393     global nick_arys chan_nicks
394     lnick_forget [irctolower $user]
395     nick_case $user
396 }
397
398 proc nick_case {user} {
399     global nick_case
400     set nick_case([irctolower $user]) $user
401 }
402
403 proc msg_NICK {p c newnick} {
404     global nick_arys nick_case calling_nick
405     prefix_nick
406     recordlastseen_n $n "changing nicks to $newnick" 0
407     set calling_nick $newnick
408     recordlastseen_n $newnick "changing nicks from $n" 1
409     set luser [irctolower $n]
410     lnick_marktime_cancel $luser
411     set lusernew [irctolower $newnick]
412     foreach ary $nick_arys {
413         upvar #0 nick_${ary}($luser) old
414         upvar #0 nick_${ary}($lusernew) new
415         if {[info exists new]} { error "nick collision ?! $ary $n $newnick" }
416         if {[info exists old]} { set new $old; unset old }
417     }
418     upvar #0 nick_onchans($lusernew) oc
419     foreach ch $oc {
420         upvar #0 chan_nicks($ch) nlist
421         set nlist [grep tn {"$tn" != "$luser"} $nlist]
422         lappend nlist $lusernew
423     }
424     lnick_marktime_start $lusernew "Hi." 500
425     nick_case $newnick
426 }
427
428 proc nick_ishere {n} {
429     global nick_counter
430     upvar #0 nick_unique([irctolower $n]) u
431     if {![info exists u]} { set u [incr nick_counter].$n.[clock seconds] }
432     nick_case $n
433 }
434
435 proc msg_JOIN {p c chan} {
436     prefix_nick
437     recordlastseen_n $n "joining $chan" 1
438     set nl [irctolower $n]
439     set lchan [irctolower $chan]
440     upvar #0 nick_onchans($nl) oc
441     upvar #0 chan_nicks($lchan) nlist
442     if {![info exists oc]} {
443         global marktime_join_startdelay
444         lnick_marktime_start $nl "Welcome." $marktime_join_startdelay
445     }
446     lappend oc $lchan
447     lappend nlist $nl
448     nick_ishere $n
449 }
450 proc msg_PART {p c chan args} {
451     prefix_nick
452     set msg "leaving $chan"
453     if {[llength $args]} {
454         set why [lindex $args 0]
455         if {"[irctolower $why]" != "[irctolower $n]"} { append msg " ($why)" }
456     }
457     recordlastseen_n $n $msg 1
458     process_kickpart $chan $n
459 }
460 proc msg_QUIT {p c why} {
461     prefix_nick
462     recordlastseen_n $n "leaving ($why)" 0
463     nick_forget $n
464 }
465
466 proc msg_PRIVMSG {p c dest text} {
467     global errorCode
468     
469     prefix_nick
470     if {[ischan $dest]} {
471         recordlastseen_n $n "invoking me in $dest" 1
472         set output $dest
473     } else {
474         recordlastseen_n $n "talking to me" 1
475         set output $n
476     }
477     nick_case $n
478
479     execute_usercommand $p $c $n $output $dest $text
480 }
481
482 proc msg_INVITE {p c n chan} {
483     after 1000 [list dojoin [irctolower $chan]]
484 }
485
486 proc grep {var predicate list} {
487     set o {}
488     upvar 1 $var v
489     foreach v $list {
490         if {[uplevel 1 [list expr $predicate]]} { lappend o $v }
491     }
492     return $o
493 }
494
495 proc msg_353 {p c dest type chan nicklist} {
496     global names_chans nick_onchans
497     set lchan [irctolower $chan]
498     upvar #0 chan_nicks($lchan) nlist
499     lappend names_chans $lchan
500     if {![info exists nlist]} {
501         # We don't think we're on this channel, so ignore it !
502         # Unfortunately, because we don't get a reply to PART,
503         # we have to remember ourselves whether we're on a channel,
504         # and ignore stuff if we're not, to avoid races.  Feh.
505         return
506     }
507     set nlist_new {}
508     foreach user [split $nicklist { }] {
509         regsub {^[@+]} $user {} user
510         if {![string length $user]} continue
511         check_nick $user
512         set luser [irctolower $user]
513         upvar #0 nick_onchans($luser) oc
514         lappend oc $lchan
515         lappend nlist_new $luser
516         nick_ishere $user
517     }
518     set nlist $nlist_new
519 }
520
521 proc msg_366 {p c args} {
522     global names_chans nick_onchans
523     set lchan [irctolower $c]
524     foreach luser [array names nick_onchans] {
525         upvar #0 nick_onchans($luser) oc
526         if {[llength names_chans] > 1} {
527             set oc [grep tc {[lsearch -exact $tc $names_chans] >= 0} $oc]
528         }
529         if {![llength $oc]} { lnick_forget $n }
530     }
531     unset names_chans
532 }
533
534 proc check_username {target} {
535     if {
536         [string length $target] > 8 ||
537         [regexp {[^-0-9a-z]} $target] ||
538         ![regexp {^[a-z]} $target]
539     } { error "invalid username" }
540 }
541
542 proc somedb__head {} {
543     uplevel 1 {
544         set idl [irctolower $id]
545         upvar #0 ${nickchan}db($idl) ndbe
546         binary scan $idl H* idh
547         set idfn $fprefix$idh
548         if {![info exists iddbe] && [file exists $idfn]} {
549             set f [open $idfn r]
550             try_except_finally { set newval [read $f] } {} { close $f }
551             if {[llength $newval] % 2} { error "invalid length" }
552             set iddbe $newval
553         }
554     }
555 }
556
557 proc def_somedb {name arglist body} {
558     foreach {nickchan fprefix} {nick users/n chan chans/c} {
559         proc ${nickchan}db_$name $arglist \
560             "set nickchan $nickchan; set fprefix $fprefix; $body"
561     }
562 }
563
564 def_somedb list {} {
565     set list {}
566     foreach path [glob -nocomplain -path $fprefix *] {
567         binary scan $path "A[string length $fprefix]A*" afprefix thinghex
568         if {"$afprefix" != "$fprefix"} { error "wrong prefix $path $afprefix" }
569         lappend list [binary format H* $thinghex]
570     }
571     return $list
572 }
573
574 proc def_somedb_id {name arglist body} {
575     def_somedb $name [concat id $arglist] "somedb__head; $body"
576 }
577
578 def_somedb_id exists {} {
579     return [info exists iddbe]
580 }
581
582 def_somedb_id delete {} {
583     catch { unset iddbe }
584     file delete $idfn
585 }
586
587 set default_settings_nick {timeformat ks  marktime off}
588 set default_settings_chan {
589     autojoin 1
590     mode *
591     userinvite pub
592     topicset {}
593     topicsee {}
594     topictell {}
595 }
596
597 def_somedb_id set {args} {
598     upvar #0 default_settings_$nickchan def
599     if {![info exists iddbe]} { set iddbe $def }
600     foreach {key value} [concat $iddbe $args] { set a($key) $value }
601     set newval {}
602     foreach {key value} [array get a] { lappend newval $key $value }
603     set f [open $idfn.new w]
604     try_except_finally {
605         puts $f $newval
606         close $f
607         file rename -force $idfn.new $idfn
608     } {
609     } {
610         catch { close $f }
611     }
612     set iddbe $newval
613 }
614
615 def_somedb_id get {key} {
616     upvar #0 default_settings_$nickchan def
617     if {[info exists iddbe]} {
618         set l [concat $iddbe $def]
619     } else {
620         set l $def
621     }
622     foreach {tkey value} $l {
623         if {"$tkey" == "$key"} { return $value }
624     }
625     error "unset setting $key"
626 }
627
628 proc opt {key} {
629     global calling_nick
630     if {[info exists calling_nick]} { set n $calling_nick } { set n {} }
631     return [nickdb_get $n $key]
632 }
633
634 proc check_notonchan {} {
635     upvar 1 dest dest
636     if {[ischan $dest]} { usererror "That command must be sent privately." }
637 }
638
639 proc nick_securitycheck {strict} {
640     upvar 1 n n
641     if {![nickdb_exists $n]} {
642         usererror "You are unknown to me, use `register'."
643     }
644     set wantu [nickdb_get $n username]
645     if {![string length $wantu]} {
646         if {$strict} {
647             usererror "That feature is only available to secure users, sorry."
648         } else {
649             return
650         }
651     }
652     set luser [irctolower $n]
653     upvar #0 nick_username($luser) nu
654     if {![info exists nu]} {
655         usererror "Nick $n is secure, you must identify yourself first."
656     }
657     if {"$wantu" != "$nu"} {
658         usererror "You are the wrong user -\
659                 the nick $n belongs to $wantu, not $nu."
660     }
661 }
662
663 proc channel_ismanager {channel n} {
664     set mgrs [chandb_get $channel managers]
665     return [expr {[lsearch -exact [irctolower $mgrs] [irctolower $n]] >= 0}]
666 }
667
668 proc channel_securitycheck {channel} {
669     upvar n n
670     if {![channel_ismanager $channel $n]} {
671         usererror "You are not a manager of $channel."
672     }
673     nick_securitycheck 1
674 }
675
676 proc def_chancmd {name body} {
677     proc channel/$name {} \
678             "    upvar 1 target chan; upvar 1 n n; upvar 1 text text; $body"
679 }
680
681 proc ta_listop {findnow procvalue} {
682     # findnow and procvalue are code fragments which will be executed
683     # in the caller's level.  findnow should set ta_listop_ev to
684     # the current list, and procvalue should treat ta_listop_ev as
685     # a proposed value in the list and check and possibly modify
686     # (canonicalise?) it.  After ta_listop, ta_listop_ev will
687     # be the new value of the list.
688     upvar 1 ta_listop_ev exchg
689     upvar 1 text text
690     set opcode [ta_word]
691     switch -exact _$opcode {
692         _= { }
693         _+ - _- {
694             uplevel 1 $findnow
695             foreach item $exchg { set array($item) 1 }
696         }
697         default {
698             error "list change opcode must be one of + - ="
699         }
700     }
701     foreach exchg [split $text " "] {
702         if {![string length $exchg]} continue
703         uplevel 1 $procvalue
704         if {"$opcode" != "-"} {
705             set array($exchg) 1
706         } else {
707             catch { unset array($exchg) }
708         }
709     }
710     set exchg [lsort [array names array]]
711 }
712
713 def_chancmd manager {
714     ta_listop {
715         if {[chandb_exists $chan]} {
716             set ta_listop_ev [chandb_get $chan managers]
717         } else {
718             set ta_listop_ev [list [irctolower $n]]
719         }
720     } {
721         check_nick $ta_listop_ev
722         set ta_listop_ev [irctolower $ta_listop_ev]
723     }
724     if {[llength $ta_listop_ev]} {
725         chandb_set $chan managers $ta_listop_ev
726         ucmdr "Managers of $chan: $ta_listop_ev" {}
727     } else {
728         chandb_delete $chan
729         ucmdr {} {} "forgets about managing $chan." {}
730     }
731 }
732
733 def_chancmd autojoin {
734     set yesno [ta_word]
735     switch -exact [string tolower $yesno] {
736         no { set nv 0 }
737         yes { set nv 1 }
738         default { error "channel autojoin must be `yes' or `no' }
739     }
740     chandb_set $chan autojoin $nv
741     ucmdr [expr {$nv ? "I will join $chan when I'm restarted " : \
742             "I won't join $chan when I'm restarted "}] {}
743 }
744
745 def_chancmd userinvite {
746     set nv [string tolower [ta_word]]
747     switch -exact $nv {
748         pub { set txt "!invite will work for $chan, but it won't work by /msg" }
749         here { set txt "!invite and /msg invite will work, but only for users who are already on $chan." }
750         all { set txt "Any user will be able to invite themselves or anyone else to $chan." }
751         none { set txt "I will not invite anyone to $chan." }
752         default {
753             error "channel userinvite must be `pub', `here', `all' or `none'
754         }
755     }
756     chandb_set $chan userinvite $nv
757     ucmdr $txt {}
758 }
759
760 def_chancmd topic {
761     set what [ta_word]
762     switch -exact $what {
763         leave {
764             ta_nomore
765             chandb_set $chan topicset {}
766             ucmdr "I won't ever change the topic of $chan." {}
767         }
768         set {
769             set t [string trim $text]
770             if {![string length $t]} {
771                 error "you must specific the topic to set"
772             }
773             chandb_set $chan topicset $t
774             ucmdr "Whenever I'm alone on $chan, I'll set the topic to $t." {}
775         }
776         see - tell {
777             ta_listop {
778                 set ta_listop_ev [chandb_get $chan topic$what]
779             } {
780                 if {"$ta_listop_ev" != "*"} {
781                     if {![ischan $ta_listop_ev]} {
782                         error "bad channel \`$ta_listop_ev' in topic $what"
783                     }
784                     set ta_listop_ev [irctolower $ta_listop_ev]
785                 }
786             }
787             chandb_set $chan topic$what $ta_listop_ev
788             ucmdr "Topic $what list for $chan: $ta_listop_ev" {}
789         }
790         default {
791             usererror "Unknown channel topic subcommand - see help channel."
792         }
793     }
794 }
795
796 def_chancmd mode {
797     set mode [ta_word]
798     if {"$mode" != "*" && ![regexp {^(([-+][imnpst]+)+)$} $mode mode]} {
799         error {channel mode must be * or match ([-+][imnpst]+)+}
800     }
801     chandb_set $chan mode $mode
802     if {"$mode" == "*"} {
803         ucmdr "I won't ever change the mode of $chan." {}
804     } else {
805         ucmdr "Whenever I'm alone on $chan, I'll set the mode to $mode." {}
806     }
807 }
808
809 def_chancmd show {
810     if {[chandb_exists $chan]} {
811         set l "Settings for $chan: autojoin "
812         append l [lindex {no yes} [chandb_get $chan autojoin]]
813         append l ", mode " [chandb_get $chan mode]
814         append l ", userinvite " [chandb_get $chan userinvite] "."
815         append l "\nManagers: "
816         append l [join [chandb_get $chan managers] " "]
817         foreach {ts sep} {see "\n" tell "  "} {
818             set t [chandb_get $chan topic$ts]
819             append l $sep
820             if {[llength $t]} {
821                 append l "Topic $ts list: $t."
822             } else {
823                 append l "Topic $ts list is empty."
824             }
825         }
826         append l "\n"
827         set t [chandb_get $chan topicset]
828         if {[string length $t]} {
829             append l "Topic to set: $t"
830         } else {
831             append l "I will not change the topic."
832         }
833         ucmdr {} $l
834     } else {
835         ucmdr {} "The channel $chan is not managed."
836     }
837 }
838
839 proc channelmgr_monoop {} {
840     upvar 1 dest dest
841     upvar 1 text text
842     upvar 1 n n
843     upvar 1 p p
844     upvar 1 target target
845     global chan_nicks
846
847     prefix_nick
848
849     if {[ischan $dest]} { set target $dest }
850     if {[ta_anymore]} { set target [ta_word] }
851     ta_nomore
852     if {![info exists target]} {
853         usererror "You must specify, or invoke me on, the relevant channel."
854     }
855     if {![info exists chan_nicks([irctolower $target])]} {
856         usererror "I am not on $target."
857     }
858     if {![ischan $target]} { error "not a valid channel" }
859
860     if {![chandb_exists $target]} {
861         usererror "$target is not a managed channel."
862     }
863     channel_securitycheck $target
864 }
865
866 def_ucmd op {
867     channelmgr_monoop
868     sendout MODE $target +o $n
869 }
870
871 def_ucmd leave {
872     channelmgr_monoop
873     doleave $target
874 }
875
876 def_ucmd invite {
877     global chan_nicks errorCode errorInfo
878     prefix_nick
879     
880     if {[ischan $dest]} {
881         set target $dest
882         set onchan 1
883     } else {
884         set target [ta_word]
885         set onchan 0
886     }
887     set ltarget [irctolower $target]
888     if {![ischan $target]} { error "$target is not a channel" }
889     if {![info exists chan_nicks($ltarget)]} {
890         usererror "I am not on $target."
891     }
892     set ui [chandb_get $ltarget userinvite]
893     if {[catch {
894         if {"$ui" == "pub" && !$onchan} {
895             usererror "Invitations to $target must be made there with !invite."
896         }
897         if {"$ui" != "all"} {
898             if {[lsearch -exact $chan_nicks($ltarget) [irctolower $n]] < 0} {
899                 usererror "Invitations to $target may only be made\
900                         by a user on the channel."
901             }
902         }
903         if {"$ui" == "none"} {
904             usererror "Sorry, I've not been authorised\
905                     to invite people to $target."
906         }
907     } emsg]} {
908         if {"$errorCode" == "BLIGHT USER" && [channel_ismanager $target $n]} {
909             if {[catch {
910                 nick_securitycheck 1
911             } emsg2]} {
912                 if {"$errorCode" == "BLIGHT USER"} {
913                     usererror "$emsg2  Therefore you can't use your\
914                             channel manager privilege.  $emsg"
915                 } else {
916                     error $error $errorInfo $errorCode
917                 }
918             }
919         } else {
920             error $emsg $errorInfo $errorCode
921         }
922     }
923     if {![ta_anymore]} {
924         usererror "You have to say who to invite."
925     }
926     set invitees {}
927     while {[ta_anymore]} {
928         set invitee [ta_word]
929         check_nick $invitee
930         lappend invitees $invitee
931     }
932     foreach invitee $invitees {
933         sendout INVITE $invitee $ltarget
934     }
935     set who [lindex $invitees 0]
936     switch -exact llength $invitees {
937         0 { error "zero invitees" }
938         1 { }
939         2 { append who " and [lindex $invitees 1]" }
940         * {
941             set who [join [lreplace $invitees end end] ", "]
942             append who " and [lindex $invitees [llength $invitees]]"
943         }
944     }
945     ucmdr {} {} {} "invites $who to $target."
946 }
947
948 def_ucmd channel {
949     if {[ischan $dest]} { set target $dest }
950     if {![ta_anymore]} {
951         set subcmd show
952     } else {
953         set subcmd [ta_word]
954     }
955     if {[ischan $subcmd]} {
956         set target $subcmd
957         if {![ta_anymore]} {
958             set subcmd show
959         } else {
960             set subcmd [ta_word]
961         }
962     }
963     if {![info exists target]} { error "privately, you must specify a channel" }
964     set procname channel/$subcmd
965     if {"$subcmd" != "show"} {
966         if {[catch { info body $procname }]} {
967             usererror "unknown channel setting $subcmd."
968         }
969         prefix_nick
970         if {[chandb_exists $target]} {
971             channel_securitycheck $target
972         } else {
973             nick_securitycheck 1
974             upvar #0 chan_initialop([irctolower $target]) io
975             upvar #0 nick_unique([irctolower $n]) u
976             if {![info exists io]} {
977                 usererror "$target is not a managed channel."
978             }
979             if {"$io" != "$u"} {
980                 usererror "You are not the interim manager of $target."
981             }
982             if {"$subcmd" != "manager"} {
983                 usererror "Please use `channel manager' first."
984             }
985         }
986     }
987     channel/$subcmd
988 }
989
990 def_ucmd who {
991     if {[ta_anymore]} {
992         set target [ta_word]; ta_nomore
993         set myself 1
994     } else {
995         prefix_nick
996         set target $n
997         set myself [expr {"$target" != "$n"}]
998     }
999     set ltarget [irctolower $target]
1000     upvar #0 nick_case($ltarget) ctarget
1001     set nshow $target
1002     if {[info exists ctarget]} {
1003         upvar #0 nick_onchans($ltarget) oc
1004         upvar #0 nick_username($ltarget) nu
1005         if {[info exists oc]} { set nshow $ctarget }
1006     }
1007     if {![nickdb_exists $ltarget]} {
1008         set ol "$nshow is not a registered nick."
1009     } elseif {[string length [set username [nickdb_get $target username]]]} {
1010         set ol "The nick $nshow belongs to the user $username."
1011     } else {
1012         set ol "The nick $nshow is registered (but not to a username)."
1013     }
1014     if {![info exists ctarget] || ![info exists oc]} {
1015         if {$myself} {
1016             append ol "\nI can't see $nshow on anywhere."
1017         } else {
1018             append ol "\nYou aren't on any channels with me."
1019         }
1020     } elseif {![info exists nu]} {
1021         append ol "\n$nshow has not identified themselves."
1022     } elseif {![info exists username]} {
1023         append ol "\n$nshow has identified themselves as the user $nu."
1024     } elseif {"$nu" != "$username"} {
1025         append ol "\nHowever, $nshow is being used by the user $nu."
1026     } else {
1027         append ol "\n$nshow has identified themselves to me."
1028     }
1029     ucmdr {} $ol
1030 }
1031
1032 def_ucmd register {
1033     prefix_nick
1034     check_notonchan
1035     set old [nickdb_exists $n]
1036     if {$old} { nick_securitycheck 0 }
1037     set luser [irctolower $n]
1038     switch -exact [string tolower [string trim $text]] {
1039         {} {
1040             upvar #0 nick_username($luser) nu
1041             if {![info exists nu]} {
1042                 ucmdr {} \
1043  "You must identify yourself before using `register'.  See `help identify', or use `register insecure'."
1044             }
1045             nickdb_set $n username $nu
1046             ucmdr {} {} "makes a note of your username." {}
1047         }
1048         delete {
1049             nickdb_delete $n
1050             ucmdr {} {} "forgets your nickname." {}
1051         }
1052         insecure {
1053             nickdb_set $n username {}
1054             if {$old} {
1055                 ucmdr {} "Security is now disabled for your nickname !"
1056             } else {
1057                 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."
1058             }
1059         }
1060         default {
1061             error "you mean register / register delete / register insecure"
1062         }
1063     }
1064 }
1065
1066 proc timeformat_desc {tf} {
1067     switch -exact $tf {
1068         ks { return "Times will be displayed in seconds or kiloseconds." }
1069         hms { return "Times will be displayed in hours, minutes, etc." }
1070         beat { return "Times will be displayed in beats (1000B = 1d)." }
1071         default { error "invalid timeformat: $v" }
1072     }
1073 }
1074
1075 proc def_setting {opt show_body set_body} {
1076     proc set_show/$opt {} "
1077         upvar 1 n n
1078         set opt $opt
1079         $show_body"
1080     if {![string length $set_body]} return
1081     proc set_set/$opt {} "
1082         upvar 1 n n
1083         upvar 1 text text
1084         set opt $opt
1085         $set_body"
1086 }
1087
1088 def_setting timeformat {
1089     set tf [nickdb_get $n timeformat]
1090     return "$tf: [timeformat_desc $tf]"
1091 } {
1092     set tf [string tolower [ta_word]]
1093     ta_nomore
1094     set desc [timeformat_desc $tf]
1095     nickdb_set $n timeformat $tf
1096     ucmdr {} $desc
1097 }
1098
1099 proc marktime_desc {mt} {
1100     if {"$mt" == "off"} {
1101         return "I will not send you periodic messages."
1102     } elseif {"$mt" == "once"} {
1103         return "I will send you one informational message when I see you."
1104     } else {
1105         return "I'll send you a message every [showintervalsecs $mt 0]."
1106     }
1107 }
1108
1109 def_setting marktime {
1110     set mt [nickdb_get $n marktime]
1111     set p $mt
1112     if {[string match {[0-9]*} $mt]} { append p s }
1113     append p ": "
1114     append p [marktime_desc $mt]
1115     return $p
1116 } {
1117     global marktime_min
1118     set mt [string tolower [ta_word]]
1119     ta_nomore
1120
1121     if {"$mt" == "off" || "$mt" == "once"} {
1122     } elseif {[regexp {^([0-9]+)([a-z]+)$} $mt dummy value unit]} {
1123         switch -exact $unit {
1124             s { set u 1 }
1125             ks { set u 1000 }
1126             m { set u 60 }
1127             mb { set u 0.0864 }
1128             b { set u 86.4 }
1129             kb { set u 86400 }
1130             default { error "unknown unit of time $unit" }
1131         }
1132         if {$value > 86400*21/$u} { error "marktime interval too large" }
1133         set mt [expr {round($value*$u)}]
1134         if {$mt < $marktime_min} { error "marktime interval too small" }
1135     } else {
1136         error "invalid syntax for marktime"
1137     }
1138     nickdb_set $n marktime $mt
1139     lnick_marktime_start [irctolower $n] "So:" 500
1140     ucmdr {} [marktime_desc $mt]
1141 }
1142
1143 def_setting security {
1144     set s [nickdb_get $n username]
1145     if {[string length $s]} {
1146         return "Your nick, $n, is controlled by the user $s."
1147     } else {
1148         return "Your nick, $n, is not secure."
1149     }
1150 } {}
1151
1152 def_ucmd set {
1153     prefix_nick
1154     check_notonchan
1155     if {![nickdb_exists $n]} {
1156         ucmdr {} "You are unknown to me and so have no settings.  (Use `register'.)"
1157     }
1158     if {![ta_anymore]} {
1159         set ol {}
1160         foreach proc [lsort [info procs]] {
1161             if {![regexp {^set_show/(.*)$} $proc dummy opt]} continue
1162             lappend ol [format "%-10s %s" $opt [set_show/$opt]]
1163         }
1164         ucmdr {} [join $ol "\n"]
1165     } else {
1166         set opt [ta_word]
1167         if {[catch { info body set_show/$opt }]} {
1168             error "no setting $opt"
1169         }
1170         if {![ta_anymore]} {
1171             ucmdr {} "$opt [set_show/$opt]"
1172         } else {
1173             nick_securitycheck 0
1174             if {[catch { info body set_set/$opt }]} {
1175                 error "setting $opt cannot be set with `set'"
1176             }
1177             set_set/$opt
1178         }
1179     }
1180 }
1181
1182 def_ucmd identpass {
1183     set username [ta_word]
1184     set passmd5 [md5sum "[ta_word]\n"]
1185     ta_nomore
1186     prefix_nick
1187     check_notonchan
1188     set luser [irctolower $n]
1189     upvar #0 nick_onchans($luser) onchans
1190     if {![info exists onchans] || ![llength $onchans]} {
1191         ucmdr "You must be on a channel with me to identify yourself." {}
1192     }
1193     check_username $username
1194     exec userv --timeout 3 $username << "$passmd5\n" > /dev/null \
1195             irc-identpass $n
1196     upvar #0 nick_username($luser) rec_username
1197     set rec_username $username
1198     ucmdr "Pleased to see you, $username." {}
1199 }
1200
1201 def_ucmd kill {
1202     global nick
1203     prefix_nick
1204     set target [ta_nick]
1205     if {![nickdb_exists $target]} { error "$target is not a registered nick." }
1206     set wantu [nickdb_get $target username]
1207     if {![string length $wantu]} { error "$target is insecurely registred." }
1208     upvar #0 nick_username([irctolower $n]) nu
1209     if {![info exists nu]} { error "You must identify yourself first." }
1210     if {"$wantu" != "$nu"} {
1211         error "You are the wrong user, $nu - $target belongs to $wantu."
1212     }
1213     set reason "at request of user $nu"
1214     if {[ta_anymore]} { append reason "; $text" }
1215     sendout KILL $target $reason
1216 }
1217
1218 def_ucmd summon {
1219     set target [ta_word]
1220     ta_nomore
1221     check_username $target
1222     prefix_nick
1223
1224     upvar #0 lastsummon($target) ls
1225     set now [clock seconds]
1226     if {[info exists ls]} {
1227         set interval [expr {$now - $ls}]
1228         if {$interval < 30} {
1229             ucmdr {} \
1230  "Please be patient; $target was summoned only [showinterval $interval]."
1231         }
1232     }
1233     regsub {^[^!]*!} $p {} path
1234     if {[catch {
1235         exec userv --timeout 3 $target irc-summon $n $path \
1236                 [expr {[ischan $dest] ? "$dest" : ""}] \
1237                 < /dev/null
1238     } rv]} {
1239         regsub -all "\n" $rv { / } rv
1240         error $rv
1241     }
1242     if {[regexp {^problem (.*)} $rv dummy problem]} {
1243         ucmdr {} "The user `$target' $problem."
1244     } elseif {[regexp {^ok ([^ ]+) ([0-9]+)$} $rv dummy tty idlesince]} {
1245         set idletime [expr {$now - $idlesince}]
1246         set ls $now
1247         ucmdr {} {} {} "invites $target ($tty[expr {
1248             $idletime > 10 ? ", idle for [showintervalsecs $idletime 0]" : ""
1249         }]) to [expr {
1250             [ischan $dest] ? "join us here" : "talk to you"
1251         }]."
1252     } else {
1253         error "unexpected response from userv service: $rv"
1254     }
1255 }
1256
1257 proc md5sum {value} { exec md5sum << $value }
1258
1259 def_ucmd seen {
1260     global lastseen nick
1261     prefix_nick
1262     set ncase [ta_nick]
1263     set nlower [irctolower $ncase]
1264     ta_nomore
1265     set now [clock seconds]
1266     if {"$nlower" == "[irctolower $nick]"} {
1267         usererror "I am not self-aware."
1268     } elseif {![info exists lastseen($nlower)]} {
1269         set rstr "I've never seen $ncase."
1270     } else {
1271         manyset $lastseen($nlower) realnick time what
1272         set howlong [expr {$now - $time}]
1273         set string [showinterval $howlong]
1274         set rstr "I last saw $realnick $string, $what."
1275     }
1276     if {[ischan $dest]} {
1277         set where $dest
1278     } else {
1279         set where {}
1280     }
1281     upvar #0 lookedfor($nlower) lf
1282     if {[info exists lf]} { set oldvalue $lf } else { set oldvalue {} }
1283     set lf [list [list $now $n $where]]
1284     foreach v $oldvalue {
1285         if {"[irctolower [lindex $v 1]]" == "[irctolower $n]"} continue
1286         lappend lf $v
1287     }
1288     ucmdr {} $rstr
1289 }
1290
1291 proc lnick_marktime_cancel {luser} {
1292     upvar #0 nick_markid($luser) mi
1293     if {![info exists mi]} return
1294     catch { after cancel $mi }
1295     catch { unset mi }
1296 }
1297
1298 proc lnick_marktime_doafter {luser why ms} {
1299     lnick_marktime_cancel $luser
1300     upvar #0 nick_markid($luser) mi
1301     set mi [after $ms [list lnick_marktime_now $luser $why]]
1302 }
1303
1304 proc lnick_marktime_reset {luser} {
1305     set mt [nickdb_get $luser marktime]
1306     if {"$mt" == "off" || "$mt" == "once"} return
1307     lnick_marktime_doafter $luser "Time passes." [expr {$mt*1000}]
1308 }
1309
1310 proc lnick_marktime_start {luser why ms} {
1311     set mt [nickdb_get $luser marktime]
1312     if {"$mt" == "off"} {
1313         lnick_marktime_cancel $luser
1314     } else {
1315         lnick_marktime_doafter $luser $why $ms
1316     }
1317 }
1318
1319 proc lnick_marktime_now {luser why} {
1320     upvar #0 nick_onchans($luser) oc
1321     global calling_nick
1322     set calling_nick $luser
1323     sendprivmsg $luser [lnick_pingstring $why $oc ""]
1324     lnick_marktime_reset $luser
1325 }    
1326
1327 proc lnick_pingstring {why oc apstring} {
1328     global nick_onchans
1329     catch { exec uptime } uptime
1330     set nnicks [llength [array names nick_onchans]]
1331     if {[regexp \
1332  {^ *([0-9:apm]+) +up.*, +(\d+) users?, +load average: +([0-9., ]+) *$} \
1333             $uptime dummy time users load]} {
1334         regsub -all , $load {} load
1335         set uptime "$time  $nnicks/$users  $load"
1336     } else {
1337         append uptime ", $nnicks nicks"
1338     }
1339     if {[llength $oc]} {
1340         set best_la 0
1341         set activity quiet
1342         foreach ch $oc {
1343             upvar #0 chan_lastactivity($ch) la
1344             if {![info exists la]} continue
1345             if {$la <= $best_la} continue
1346             set since [showintervalsecs [expr {[clock seconds]-$la}] 1]
1347             set activity "$ch $since"
1348             set best_la $la
1349         }
1350     } else {
1351         set activity unseen
1352     }
1353     set str $why
1354     append str "  " $uptime "  " $activity
1355     if {[string length $apstring]} { append str "  " $apstring }
1356     return $str
1357 }
1358
1359 def_ucmd ping {
1360     if {[ischan $dest]} {
1361         set oc [irctolower $dest]
1362     } else {
1363         global nick_onchans
1364         prefix_nick
1365         set ln [irctolower $n]
1366         if {[info exists nick_onchans($ln)]} {
1367             set oc $nick_onchans($ln)
1368         } else {
1369             set oc {}
1370         }
1371         if {[llength $oc]} { lnick_marktime_reset $ln }
1372     }
1373     ucmdr {} [lnick_pingstring "Pong!" $oc $text]
1374 }
1375
1376 proc ensure_globalsecret {} {
1377     global globalsecret
1378     
1379     if {[info exists globalsecret]} return
1380     set gsfile [open /dev/urandom r]
1381     fconfigure $gsfile -translation binary
1382     set globalsecret [read $gsfile 32]
1383     binary scan $globalsecret H* globalsecret
1384     close $gsfile
1385     unset gsfile
1386 }
1387
1388 proc connected {} {
1389     global operuserpass
1390     if {[info exists operuserpass]} {
1391         eval sendout OPER $operuserpass
1392     }
1393     foreach chan [chandb_list] {
1394         if {[chandb_get $chan autojoin]} { dojoin $chan }
1395     }
1396 }
1397
1398 ensure_globalsecret
1399 loadhelp
1400 ensure_connecting