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