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