chiark / gitweb /
Merge up from trunk. cvs up -j branchpoint-2001-10-09-tell -j mergeup-1-2001-10...
[ircbot] / 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 defset marktime_min 300
10 defset marktime_join_startdelay 5000
11
12 proc privmsg_unlogged {prefix ischan params} {
13     if {!$ischan ||
14         [regexp {^![a-z][-a-z]*[a-z]( .*)?$} [lindex $params 1]]} {
15         return 0
16     }
17     # on-channel message, ignore
18     set chan [lindex $params 0]
19     upvar #0 chan_lastactivity([irctolower $chan]) la
20     set la [clock seconds]
21     catch_logged { recordlastseen_p $prefix "talking on $chan" 2 }
22     return 1
23 }
24
25 proc showintervalsecs {howlong abbrev} {
26     return [showintervalsecs/[opt timeformat] $howlong $abbrev]
27 }
28
29 proc formatsf {pfx value} {
30    foreach {min format} { 100 %.0f 10 %.1f 0 %.2f} {
31         set fval [format $format $value]
32         if {$fval < $min} continue
33         return [format "$fval${pfx}" $value]
34    }
35 }
36
37 proc showintervalsecs/beat {howlong abbrev} {
38     # We split in two to avoid overflow problems.
39     if {$howlong < 86 } {
40         # mB's
41         set pfx mB
42         return [format "%.0fmB" [expr {round($howlong * 1.157)*10} ]]
43     } else {
44         if {$howlong < 86400 } {
45             # B's
46             set pfx B
47             set value [expr {$howlong / 86.4}]
48         } else {
49             # kB's
50             set pfx kB
51             set value [expr {$howlong / 86400.0}]
52         }
53     }
54     return [formatsf $pfx $value]
55 }
56
57 proc showintervalsecs/ks {howlong abbrev} {
58     if {$howlong < 1000} {
59         return "${howlong}s"
60     } else {
61         if {$howlong < 1000000} {
62             set pfx ks
63             set scale 1000
64         } else {
65             set pfx Ms
66             set scale 1000000
67         }
68         set value [expr "$howlong.0 / $scale"]
69         return [formatsf $pfx $value]
70     }
71 }
72
73 proc format_qty {qty unit abbrev} {
74     set o $qty
75     if {$abbrev} {
76         append o [string range $unit 0 0]
77     } else {
78         append o " "
79         append o $unit
80         if {$qty != 1} { append o s }
81     }
82     return $o
83 }
84
85 proc showintervalsecs/hms {qty abbrev} {
86     set ul {second 60 minute 60 hour 24 day 7 week}
87     set remainv 0
88     while {[llength $ul] > 1 && $qty >= [set uv [lindex $ul 1]]} {
89         set remainu [lindex $ul 0]
90         set remainv [expr {$qty % $uv}]
91         set qty [expr {($qty-$remainv)/$uv}]
92         set ul [lreplace $ul 0 1]
93     }
94     set o [format_qty $qty [lindex $ul 0] $abbrev]
95     if {$remainv} {
96         if {!$abbrev} { append o " " }
97         append o [format_qty $remainv $remainu $abbrev]
98     }
99     return $o
100 }
101
102 proc showinterval {howlong} {
103     if {$howlong <= 0} {
104         return {just now}
105     } else {
106         return "[showintervalsecs $howlong 0] ago"
107     }
108 }
109
110 proc showtime {when} {
111     return [showinterval [expr {[clock seconds] - $when}]]
112 }
113
114 proc parse_interval {specified min} {
115     if {![regexp {^([0-9]+)([a-z]+)$} $specified dummy value unit]} {
116         error "invalid syntax for interval"
117     }
118     switch -exact $unit {
119         s { set u 1 }
120         ks { set u 1000 }
121         m { set u 60 }
122         h { set u 3600 }
123         mb { set u 0.0864 }
124         b { set u 86.4 }
125         kb { set u 86400 }
126         default { error "unknown unit of time $unit" }
127     }
128     if {$value > 86400*21/$u} { error "interval too large" }
129     set result [expr {round($value*$u)}]
130     if {$result < $min} { error "interval too small (<${min}s)" }
131     return $result
132 }
133
134 proc def_msgproc {name argl body} {
135     proc msg_$name "varbase $argl" "\
136     upvar #0 msg/\$varbase/dest d\n\
137     upvar #0 msg/\$varbase/str s\n\
138     upvar #0 msg/\$varbase/accum a\n\
139 $body"
140 }
141
142 def_msgproc begin {dest str} {
143     set d $dest
144     set s $str
145     set a {}
146 }
147
148 def_msgproc append {str} {
149     set ns "$s$str"
150     if {[string length $s] && [string length $ns] > 65} {
151         msg__sendout $varbase
152         set s " [string trimleft $str]"
153     } else {
154         set s $ns
155     }
156 }
157
158 def_msgproc finish {} {
159     msg__sendout $varbase
160     unset s
161     unset d
162     return $a
163 }
164
165 def_msgproc _sendout {} {
166     lappend a [string trimright $s]
167     set s {}
168 }
169
170 proc looking_whenwhere {when where} {
171     set str [showtime [expr {$when-1}]]
172     if {[string length $where]} { append str " on $where" }
173     return $str
174 }
175
176 proc tell_getcstate {} {
177     # uses nl from caller's context
178     # imports telling (as the nick_telling) and u
179     # sets stt, telling_when
180     uplevel 1 {
181         upvar #0 nick_telling($nl) telling
182         upvar #0 nick_unique($nl) u
183
184         if {[info exists telling]} {
185             manyset $telling u_last stt telling_when
186             if {![info exists u] || "$u_last" != "$u"} {
187                 set stt undelivered
188             }
189         } else {
190             set stt undelivered
191             set telling_when 0
192         }
193     }
194 }
195
196 proc tell_event {nl event} {
197     # For `act' we *haven't* yet done the 750ms delay; we implement
198     # that here.  Also, here we turn `talk' into `talk' now and `act'
199     # later.  We also support the psuedo-event `none'.  The del msg
200     # and new msg events are handled by the command procedures, not here.
201     global calling_nick
202     if {[info exists calling_nick]} { set save $calling_nick }
203     set r DELAYED
204     switch -exact $event {
205         none { }
206         talk {
207             tell_event_core $nl talk
208             tell_event $nl act
209         }
210         act {
211             after 750 [list tell_event_core $nl $event]
212         }
213         ident - msgsarrive {
214             tell_event_core $nl $event
215         }
216         tellme {
217             set r [tell_event_core $nl TELLME]
218         }
219         default {
220             error "tell_event $nl $event"
221         }
222     }
223     if {[info exists save]} { set calling_nick $save }
224     return $r
225 }
226
227 proc tell_event_core {nl event} {
228     global tell_event_teventi errorInfo
229     set tell_event_teventi "*$event* $nl"
230     if {[catch {
231         tell_event_core1 $nl $event
232     } emsg]} {
233         log_intern "tell event error" "$emsg >$errorInfo<"
234         set emsg ERROR
235     } else {
236         log_intern "tell event" "done $tell_event_teventi $emsg"
237     }
238     return $emsg
239 }
240
241 proc tell_event_core1 {nl event} {
242     # event is `talk', `act', `ident', `msgsarrive' or `TELLME'
243     # When user talks we actually get talk now and act later
244     global calling_nick
245     set calling_nick $nl
246     set iml [msgsdb_get $nl inbound]
247     if {![llength $iml]} { return nomsgs }
248
249     set now [clock seconds]
250     tell_getcstate
251     set ago [expr {$now - $telling_when}]
252
253     # Now we have the components of a telling state
254     #   u     - nick_unique (unset if not visible)
255     #   stt   - state: undelivered, mentioned, passede
256     #   ago   - how long ago since we did anything
257
258     # We compute an evstate to dispatch on as follows:
259
260     # evstate is string of letters
261     #   current state
262     #      u   UNDELIVERED (MESSAGES)
263     #      m   MENTIONED
264     #      p   PASSED
265     #   event
266     #      t   talk
267     #      a   act
268     #      i   ident
269     #      m   msgsarrive
270     #      T   tellme (user command)
271     #   security level and timing
272     #      ii  Insecure
273     #      ss  Secure and soon (before interval)
274     #      sl  Secure and late (after interval)
275     #   current identification
276     #      i   Identified
277     #      u   Unidentified
278     #   reliability and timing
279     #      uu  Unreliable
280     #      rv  Remind, very soon (before within-interval)
281     #      rs  Remind, soon (between)
282     #      rl  Remind, late (after every-interval)
283     #      ps  Pester, soon (before interval)
284     #      pl  Pester, late (after interval)
285
286     set evstate {}
287
288     append evstate [string range $stt 0 0]
289     append evstate [string range $event 0 0]
290
291     manyset [nickdb_get_sec_effective $nl] sec secwhen
292     switch -exact $sec {
293         insecure { append evstate ii }
294         secure { append evstate [expr {$ago<$secwhen ? "ss" : "sl"}] }
295         default { append evstate "#$sec#" }
296     }
297
298     upvar #0 nick_username($nl) nu
299     if {[info exists nu] && "$nu" == "[nickdb_get_username $nl]"} {
300         append evstate i
301     } else {
302         append evstate u
303     }
304     
305     manyset [nickdb_get $nl tellrel] rel relint relwithin
306     switch -exact $rel {
307         unreliable { append evstate uu }
308         remind { append evstate [expr {
309             $ago<$relwithin ? "rv" : $ago<$relint ? "rs" : "rl"
310         }]}
311         pester { append evstate [expr {$ago<$relint ? "ps" : "pl"}] }
312         default { append evstate "#$rel#" }
313     }
314
315     global tell_event_teventi
316     set tell_event_teventi "$evstate $ago $nl"
317     switch -glob $evstate {
318         pt???rv {
319             # consider delivered:
320             #  (very recently passed, and the user talks)
321             set ndel [tell_delete_msgs {} $nl]
322             sendprivmsg $nl \
323  "I'm assuming you got the $ndel message(s) I just passed on."
324             return delivered
325         }
326         pm????? {
327             # oops, messages passed are now out of date
328             catch_restoreei { unset telling }
329             return reset
330         }
331         ?m????? {
332             # ignore new msgs if we haven't passed yet
333             return ignorenew
334         }
335         ut????? - mt????? -
336         pt???uu - pt???rs - pt???rl - pt???p? {
337             # ignore (any other `talk's) - act handles these
338             return ignoretalk
339         }
340         ui????? -
341         uaii?uu - uaii??l - uas?iuu - uas?i?l -
342         mi????? - pa????l -
343         ?Tii??? - ?Ts?i?? {
344             # pass and then stuff
345             if {[llength $iml] == 3} {
346                 manyset $iml sender sentwhen msg
347                 sendprivmsg $nl \
348  "$sender asked me, [showinterval [expr {$now-$sentwhen}]],\
349  to tell you: $msg"
350             } else {
351                 sendprivmsg $nl \
352  "I have [expr {[llength $iml]/3}] messages for you:"
353                 while {[llength $iml] >= 3} {
354                     manyset [lrange $iml 0 2] sender sentwhen msg
355                     set iml [lrange $iml 3 end]
356                     sendprivmsg $nl \
357  " [showintervalsecs [expr {$now-$sentwhen}] 1] <$sender> $msg"
358                 }
359             }
360             if {"$rel" == "unreliable"} {
361                 tell_delete_msgs {} $nl
362                 return toldunreliable
363             }
364             set stt passed
365             set re passed
366         }
367         uaslu?? {
368             sendprivmsg $nl {You have messages (so identify yourself please).}
369             set stt mentioned
370             set re mentioned
371         }
372         ?Ts?u?? {
373             sendprivmsg $nl {You must identify yourself to see your messages.}
374             return ignoreuitm
375         }
376         masl??? {
377             sendprivmsg $nl {Don't forget about your messages.}
378             return remind
379         }
380         pi????? {
381             return ignorepi
382         }
383         mass??? - pa????v - pa????s -
384         uaii??v - uaii??s -
385         uas?i?v - uas?i?s -
386         uassu?? {
387             # too soon
388             return ignoresoon
389         }
390         * {
391             error "tell_event_core nl=$nl evstate=$evstate ?"
392         }
393     }
394     if {![info exists u]} {
395         set telling [list {} undelivered $now]
396     } else {
397         set telling [list $u $stt $now]
398     }
399     return $re
400 }
401
402 proc recordlastseen_n {n how here} {
403     # here is:
404     #   0 - nick was seen leaving (or changing to another nicks or some such)
405     #   1 - nick was seen doing something else
406     #   2 - nick was seen talking on channel
407     global lastseen lookedfor
408     set nl [irctolower $n]
409     set now [clock seconds]
410     set lastseen($nl) [list $n $now $how]
411
412     if {!$here} return
413
414     tell_event $nl [lindex {none act talk} $here]
415
416     upvar #0 lookedfor($nl) lf
417     if {[info exists lf]} {
418         switch -exact [llength $lf] {
419             0 {
420                 set ml {}
421             }
422             1 {
423                 manyset [lindex $lf 0] when who where
424                 set ml [list \
425  "FYI, $who was looking for you [looking_whenwhere $when $where]."]
426             }
427             default {
428                 msg_begin tosend $n "FYI, people have been looking for you:"
429                 set i 0
430                 set fin ""
431                 foreach e $lf {
432                     incr i
433                     if {$i == 1} {
434                         msg_append tosend " "
435                     } elseif {$i == [llength $lf]} {
436                         msg_append tosend " and "
437                         set fin .
438                     } else {
439                         msg_append tosend ", "
440                     }
441                     manyset $e when who where
442                     msg_append tosend \
443                             "$who ([looking_whenwhere $when $where])$fin"
444                 }
445                 set ml [msg_finish tosend]
446             }
447         }
448         unset lf
449         msendprivmsg_delayed 1000 $n $ml
450     }
451 }
452
453 proc note_topic {showoff whoby topic} {
454     set msg "FYI, $whoby has changed the topic on $showoff"
455     if {[string length $topic] < 160} {
456         append msg " to $topic"
457     } else {
458         append msg " but it is too long to reproduce here !"
459     }
460     set showoff [irctolower $showoff]
461     set tell [chandb_get $showoff topictell]
462     if {[lsearch -exact $tell *] >= 0} {
463         set tryspies [chandb_list]
464     } else {
465         set tryspies $tell
466     }
467     foreach spy $tryspies {
468         set see [chandb_get $spy topicsee]
469         if {[lsearch -exact $see $showoff] >= 0 || \
470                 ([lsearch -exact $see *] >= 0 && \
471                 [lsearch -exact $tell $spy] >= 0)} {
472             sendprivmsg $spy $msg
473         }
474     }
475 }
476
477 proc recordlastseen_p {p how here} {
478     prefix_nick
479     recordlastseen_n $n $how $here
480 }
481
482 proc chanmode_arg {} {
483     upvar 2 args cm_args
484     set rv [lindex $cm_args 0]
485     set cm_args [lreplace cm_args 0 0]
486     return $rv
487 }
488
489 proc chanmode_o1 {m g p chan} {
490     global nick chan_initialop
491     prefix_nick
492     set who [chanmode_arg]
493     recordlastseen_n $n "being nice to $who" 1
494     if {"[irctolower $who]" == "[irctolower $nick]"} {
495         set nlower [irctolower $n]
496         upvar #0 nick_unique($nlower) u
497         if {[chandb_exists $chan]} {
498             sendprivmsg $n Thanks.
499         } elseif {![info exists u]} {
500             sendprivmsg $n {Op me while not on the channel, why don't you ?}
501         } else {
502             set chan_initialop([irctolower $chan]) $u
503             sendprivmsg $n \
504  "Thanks.  You can use `channel manager ...' to register this channel."
505             if {![string length [nickdb_get_username $n username]]} {
506                 sendprivmsg $n \
507  "(But to do that you must register your nick securely first.)"
508             }
509         }
510     }
511 }
512
513 proc chanmode_o0 {m g p chan} {
514     global nick chandeop
515     prefix_nick
516     set who [chanmode_arg]
517     recordlastseen_p $p "being mean to $who" 1
518     if {"[irctolower $who]" == "[irctolower $nick]"} {
519         set chandeop($chan) [list [clock seconds] $p]
520     }
521 }
522
523 proc msg_MODE {p c dest modelist args} {
524     if {![ischan $dest]} return
525     if {[regexp {^\-(.+)$} $modelist dummy modelist]} {
526         set give 0
527     } elseif {[regexp {^\+(.+)$} $modelist dummy modelist]} {
528         set give 1
529     } else {
530         error "invalid modelist"
531     }
532     foreach m [split $modelist] {
533         set procname chanmode_$m$give
534         if {[catch { info body $procname }]} {
535             recordlastseen_p $p "fiddling with $dest" 1
536         } else {
537             $procname $m $give  $p $dest
538         }
539     }
540 }
541
542 proc leaving {lchan} {
543     foreach luser [array names nick_onchans] {
544         upvar #0 nick_onchans($luser) oc
545         set oc [grep tc {"$tc" != "$lchan"} $oc]
546     }
547     upvar #0 chan_nicks($lchan) nlist
548     unset nlist
549     upvar #0 chan_lastactivity($lchan) la
550     catch { unset la }
551 }
552
553 proc doleave {lchan} {
554     sendout PART $lchan
555     leaving $lchan
556 }
557
558 proc dojoin {lchan} {
559     global chan_nicks
560     sendout JOIN $lchan
561     set chan_nicks($lchan) {}
562 }
563
564 proc check_justme {lchan} {
565     global nick
566     upvar #0 chan_nicks($lchan) nlist
567     if {[llength $nlist] != 1} return
568     if {"[lindex $nlist 0]" != "[irctolower $nick]"} return
569     if {[chandb_exists $lchan]} {
570         set mode [chandb_get $lchan mode]
571         if {"$mode" != "*"} {
572             sendout MODE $lchan $mode
573         }
574         set topic [chandb_get $lchan topicset]
575         if {[string length $topic]} {
576             sendout TOPIC $lchan $topic
577         }
578     } else {
579         doleave $lchan
580     }
581 }
582
583 proc process_kickpart {chan user} {
584     global nick
585     check_nick $user
586     set luser [irctolower $user]
587     set lchan [irctolower $chan]
588     if {![ischan $chan]} { error "not a channel" }
589     if {"$luser" == "[irctolower $nick]"} {
590         leaving $lchan
591     } else {
592         upvar #0 nick_onchans($luser) oc
593         upvar #0 chan_nicks($lchan) nlist
594         set oc [grep tc {"$tc" != "$lchan"} $oc]
595         set nlist [grep tn {"$tn" != "$luser"} $nlist]
596         nick_case $user
597         if {![llength $oc]} {
598             nick_forget $luser
599         } else {
600             check_justme $lchan
601         }
602     }
603 }
604
605 proc msg_TOPIC {p c dest topic} {
606     prefix_nick
607     if {![ischan $dest]} return
608     recordlastseen_n $n "changing the topic on $dest" 1
609     note_topic [irctolower $dest] $n $topic
610 }
611
612 proc msg_KICK {p c chans users comment} {
613     set chans [split $chans ,]
614     set users [split $users ,]
615     if {[llength $chans] > 1} {
616         foreach chan $chans user $users { process_kickpart $chan $user }
617     } else {
618         foreach user $users { process_kickpart [lindex $chans 0] $user }
619     }
620 }
621
622 proc msg_KILL {p c user why} {
623     nick_forget $user
624 }
625
626 set nick_counter 0
627 set nick_arys {onchans username unique}
628 # nick_onchans($luser) -> [list ... $lchan ...]
629 # nick_username($luser) -> <securely known local username>
630 # nick_unique($luser) -> <includes-counter>
631 # nick_case($luser) -> $user  (valid even if no longer visible)
632 # nick_markid($luser) -> <after id for marktime>
633 # nick_telling($luser) -> <unique> mentioned|passed <when>
634
635 # chan_nicks($lchan) -> [list ... $luser ...]
636 # chan_lastactivity($lchan) -> [clock seconds]
637
638 proc lnick_forget {luser} {
639     global nick_arys chan_nicks
640     lnick_marktime_cancel $luser
641     foreach ary $nick_arys {
642         upvar #0 nick_${ary}($luser) av
643         catch { unset av }
644     }
645     foreach lch [array names chan_nicks] {
646         upvar #0 chan_nicks($lch) nlist
647         set nlist [grep tn {"$tn" != "$luser"} $nlist]
648         check_justme $lch
649     }
650 }
651
652 proc nick_forget {user} {
653     global nick_arys chan_nicks
654     lnick_forget [irctolower $user]
655     nick_case $user
656 }
657
658 proc nick_case {user} {
659     global nick_case
660     set nick_case([irctolower $user]) $user
661 }
662
663 proc msg_NICK {p c newnick} {
664     global nick_arys nick_case calling_nick
665     prefix_nick
666     recordlastseen_n $n "changing nicks to $newnick" 0
667     set calling_nick $newnick
668     recordlastseen_n $newnick "changing nicks from $n" 1
669     set luser [irctolower $n]
670     lnick_marktime_cancel $luser
671     set lusernew [irctolower $newnick]
672     foreach ary $nick_arys {
673         upvar #0 nick_${ary}($luser) old
674         upvar #0 nick_${ary}($lusernew) new
675         if {[info exists new]} { error "nick collision ?! $ary $n $newnick" }
676         if {[info exists old]} { set new $old; unset old }
677     }
678     upvar #0 nick_onchans($lusernew) oc
679     foreach ch $oc {
680         upvar #0 chan_nicks($ch) nlist
681         set nlist [grep tn {"$tn" != "$luser"} $nlist]
682         lappend nlist $lusernew
683     }
684     lnick_marktime_start $lusernew "Hi." 500 1
685     nick_case $newnick
686 }
687
688 proc nick_ishere {n} {
689     global nick_counter
690     upvar #0 nick_unique([irctolower $n]) u
691     if {![info exists u]} { set u [incr nick_counter].$n.[clock seconds] }
692     nick_case $n
693 }
694
695 proc msg_JOIN {p c chan} {
696     prefix_nick
697     nick_ishere $n
698     recordlastseen_n $n "joining $chan" 1
699     set nl [irctolower $n]
700     set lchan [irctolower $chan]
701     upvar #0 nick_onchans($nl) oc
702     upvar #0 chan_nicks($lchan) nlist
703     if {![info exists oc]} {
704         global marktime_join_startdelay
705         lnick_marktime_start $nl "Welcome." $marktime_join_startdelay 1
706     }
707     lappend oc $lchan
708     lappend nlist $nl
709 }
710 proc msg_PART {p c chan args} {
711     prefix_nick
712     set msg "leaving $chan"
713     if {[llength $args]} {
714         set why [lindex $args 0]
715         if {"[irctolower $why]" != "[irctolower $n]"} { append msg " ($why)" }
716     }
717     recordlastseen_n $n $msg 1
718     process_kickpart $chan $n
719 }
720 proc msg_QUIT {p c why} {
721     prefix_nick
722     recordlastseen_n $n "leaving ($why)" 0
723     nick_forget $n
724 }
725
726 proc msg_PRIVMSG {p c dest text} {
727     global errorCode
728     
729     prefix_nick
730     if {[ischan $dest]} {
731         recordlastseen_n $n "invoking me in $dest" 1
732         set output $dest
733     } else {
734         recordlastseen_n $n "talking to me" 1
735         set output $n
736     }
737     nick_case $n
738
739     execute_usercommand $p $c $n $output $dest $text
740 }
741
742 proc msg_INVITE {p c n chan} {
743     after 1000 [list dojoin [irctolower $chan]]
744 }
745
746 proc grep {var predicate list} {
747     set o {}
748     upvar 1 $var v
749     foreach v $list {
750         if {[uplevel 1 [list expr $predicate]]} { lappend o $v }
751     }
752     return $o
753 }
754
755 proc msg_353 {p c dest type chan nicklist} {
756     global names_chans nick_onchans
757     set lchan [irctolower $chan]
758     upvar #0 chan_nicks($lchan) nlist
759     lappend names_chans $lchan
760     if {![info exists nlist]} {
761         # We don't think we're on this channel, so ignore it !
762         # Unfortunately, because we don't get a reply to PART,
763         # we have to remember ourselves whether we're on a channel,
764         # and ignore stuff if we're not, to avoid races.  Feh.
765         return
766     }
767     set nlist_new {}
768     foreach user [split $nicklist { }] {
769         regsub {^[@+]} $user {} user
770         if {![string length $user]} continue
771         check_nick $user
772         set luser [irctolower $user]
773         upvar #0 nick_onchans($luser) oc
774         lappend oc $lchan
775         lappend nlist_new $luser
776         nick_ishere $user
777     }
778     set nlist $nlist_new
779 }
780
781 proc msg_366 {p c args} {
782     global names_chans nick_onchans
783     set lchan [irctolower $c]
784     foreach luser [array names nick_onchans] {
785         upvar #0 nick_onchans($luser) oc
786         if {[llength names_chans] > 1} {
787             set oc [grep tc {[lsearch -exact $tc $names_chans] >= 0} $oc]
788         }
789         if {![llength $oc]} { lnick_forget $n }
790     }
791     unset names_chans
792 }
793
794 proc check_username {target} {
795     if {
796         [string length $target] > 8 ||
797         [regexp {[^-0-9a-z]} $target] ||
798         ![regexp {^[a-z]} $target]
799     } { error "invalid username" }
800 }
801
802 proc somedb__head {} {
803     uplevel 1 {
804         set idl [irctolower $id]
805         upvar #0 ${nickchan}db($idl) ndbe
806         binary scan $idl H* idh
807         set idfn $fprefix$idh
808         if {![info exists iddbe] && [file exists $idfn]} {
809             set f [open $idfn r]
810             try_except_finally { set newval [read $f] } {} { close $f }
811             if {[llength $newval] % 2} { error "invalid length" }
812             set iddbe $newval
813         }
814     }
815 }
816
817 proc def_somedb {name arglist body} {
818     foreach {nickchan fprefix} {
819         nick users/n
820         chan chans/c
821         msgs users/m
822     } {
823         proc ${nickchan}db_$name $arglist \
824             "set nickchan $nickchan; set fprefix $fprefix; $body"
825     }
826 }
827
828 def_somedb list {} {
829     set list {}
830     foreach path [glob -nocomplain -path $fprefix *] {
831         binary scan $path "A[string length $fprefix]A*" afprefix thinghex
832         if {"$afprefix" != "$fprefix"} { error "wrong prefix $path $afprefix" }
833         lappend list [binary format H* $thinghex]
834     }
835     return $list
836 }
837
838 proc def_somedb_id {name arglist body} {
839     def_somedb $name [concat id $arglist] "somedb__head; $body"
840 }
841
842 def_somedb_id exists {} {
843     return [info exists iddbe]
844 }
845
846 def_somedb_id delete {} {
847     catch { unset iddbe }
848     file delete $idfn
849 }
850
851 set default_settings_nick {
852     timeformat ks
853     marktime off
854     tellsec {secure 600}
855     tellrel {remind 3600 30}
856 }
857
858 set default_settings_chan {
859     autojoin 1
860     mode *
861     userinvite pub
862     topicset {}
863     topicsee {}
864     topictell {}
865 }
866
867 set default_settings_msgs {
868     inbound {}
869     outbound {}
870 }
871 # inbound -> [<nick> <time_t> <message>] ...
872 # outbound -> [<nick> <time_t(earliest)> <count>] ...
873 #   neither are sorted particularly; only one entry per recipient in
874 #   output; both sender and recipient are cased
875
876 def_somedb_id set {args} {
877     upvar #0 default_settings_$nickchan def
878     if {![info exists iddbe]} { set iddbe $def }
879     foreach {key value} [concat $iddbe $args] { set a($key) $value }
880     set newval {}
881     foreach {key value} [array get a] { lappend newval $key $value }
882     set f [open $idfn.new w]
883     try_except_finally {
884         puts $f $newval
885         close $f
886         file rename -force $idfn.new $idfn
887     } {
888     } {
889         catch { close $f }
890     }
891     set iddbe $newval
892 }
893
894 def_somedb_id get {key} {
895     upvar #0 default_settings_$nickchan def
896     if {[info exists iddbe]} {
897         set l [concat $iddbe $def]
898     } else {
899         set l $def
900     }
901     foreach {tkey value} $l {
902         if {"$tkey" == "$key"} { return $value }
903     }
904     error "unset setting $key"
905 }
906
907 proc opt {key} {
908     global calling_nick
909     if {[info exists calling_nick]} { set n $calling_nick } { set n {} }
910     return [nickdb_get $n $key]
911 }
912
913 proc check_notonchan {} {
914     upvar 1 dest dest
915     if {[ischan $dest]} { usererror "That command must be sent privately." }
916 }
917
918 proc nick_securitycheck {strict} {
919     upvar 1 n n
920     if {![nickdb_exists $n]} {
921         usererror "You are unknown to me, use `register'."
922     }
923     set wantu [nickdb_get $n username]
924     if {![string length $wantu]} {
925         if {$strict} {
926             usererror "That feature is only available to secure users, sorry."
927         } else {
928             return
929         }
930     }
931     set luser [irctolower $n]
932     upvar #0 nick_username($luser) nu
933     if {![info exists nu]} {
934         usererror "Nick $n is secure, you must identify yourself first."
935     }
936     if {"$wantu" != "$nu"} {
937         usererror "You are the wrong user -\
938                 the nick $n belongs to $wantu, not $nu."
939     }
940 }
941
942 proc channel_ismanager {channel n} {
943     set mgrs [chandb_get $channel managers]
944     return [expr {[lsearch -exact [irctolower $mgrs] [irctolower $n]] >= 0}]
945 }
946
947 proc channel_securitycheck {channel} {
948     upvar n n
949     if {![channel_ismanager $channel $n]} {
950         usererror "You are not a manager of $channel."
951     }
952     nick_securitycheck 1
953 }
954
955 proc def_chancmd {name body} {
956     proc channel/$name {} \
957             "    upvar 1 target chan; upvar 1 n n; upvar 1 text text; $body"
958 }
959
960 proc ta_listop {findnow procvalue} {
961     # findnow and procvalue are code fragments which will be executed
962     # in the caller's level.  findnow should set ta_listop_ev to
963     # the current list, and procvalue should treat ta_listop_ev as
964     # a proposed value in the list and check and possibly modify
965     # (canonicalise?) it.  After ta_listop, ta_listop_ev will
966     # be the new value of the list.
967     upvar 1 ta_listop_ev exchg
968     upvar 1 text text
969     set opcode [ta_word]
970     switch -exact _$opcode {
971         _= { }
972         _+ - _- {
973             uplevel 1 $findnow
974             foreach item $exchg { set array($item) 1 }
975         }
976         default {
977             error "list change opcode must be one of + - ="
978         }
979     }
980     foreach exchg [split $text " "] {
981         if {![string length $exchg]} continue
982         uplevel 1 $procvalue
983         if {"$opcode" != "-"} {
984             set array($exchg) 1
985         } else {
986             catch { unset array($exchg) }
987         }
988     }
989     set exchg [lsort [array names array]]
990 }
991
992 def_chancmd manager {
993     ta_listop {
994         if {[chandb_exists $chan]} {
995             set ta_listop_ev [chandb_get $chan managers]
996         } else {
997             set ta_listop_ev [list [irctolower $n]]
998         }
999     } {
1000         check_nick $ta_listop_ev
1001         set ta_listop_ev [irctolower $ta_listop_ev]
1002     }
1003     if {[llength $ta_listop_ev]} {
1004         chandb_set $chan managers $ta_listop_ev
1005         ucmdr "Managers of $chan: $ta_listop_ev" {}
1006     } else {
1007         chandb_delete $chan
1008         ucmdr {} {} "forgets about managing $chan." {}
1009     }
1010 }
1011
1012 def_chancmd autojoin {
1013     set yesno [ta_word]
1014     switch -exact [string tolower $yesno] {
1015         no { set nv 0 }
1016         yes { set nv 1 }
1017         default { error "channel autojoin must be `yes' or `no' }
1018     }
1019     chandb_set $chan autojoin $nv
1020     ucmdr [expr {$nv ? "I will join $chan when I'm restarted " : \
1021             "I won't join $chan when I'm restarted "}] {}
1022 }
1023
1024 def_chancmd userinvite {
1025     set nv [string tolower [ta_word]]
1026     switch -exact $nv {
1027         pub { set txt "!invite will work for $chan, but it won't work by /msg" }
1028         here { set txt "!invite and /msg invite will work, but only for users who are already on $chan." }
1029         all { set txt "Any user will be able to invite themselves or anyone else to $chan." }
1030         none { set txt "I will not invite anyone to $chan." }
1031         default {
1032             error "channel userinvite must be `pub', `here', `all' or `none'
1033         }
1034     }
1035     chandb_set $chan userinvite $nv
1036     ucmdr $txt {}
1037 }
1038
1039 def_chancmd topic {
1040     set what [ta_word]
1041     switch -exact $what {
1042         leave {
1043             ta_nomore
1044             chandb_set $chan topicset {}
1045             ucmdr "I won't ever change the topic of $chan." {}
1046         }
1047         set {
1048             set t [string trim $text]
1049             if {![string length $t]} {
1050                 error "you must specific the topic to set"
1051             }
1052             chandb_set $chan topicset $t
1053             ucmdr "Whenever I'm alone on $chan, I'll set the topic to $t." {}
1054         }
1055         see - tell {
1056             ta_listop {
1057                 set ta_listop_ev [chandb_get $chan topic$what]
1058             } {
1059                 if {"$ta_listop_ev" != "*"} {
1060                     if {![ischan $ta_listop_ev]} {
1061                         error "bad channel \`$ta_listop_ev' in topic $what"
1062                     }
1063                     set ta_listop_ev [irctolower $ta_listop_ev]
1064                 }
1065             }
1066             chandb_set $chan topic$what $ta_listop_ev
1067             ucmdr "Topic $what list for $chan: $ta_listop_ev" {}
1068         }
1069         default {
1070             usererror "Unknown channel topic subcommand - see help channel."
1071         }
1072     }
1073 }
1074
1075 def_chancmd mode {
1076     set mode [ta_word]
1077     if {"$mode" != "*" && ![regexp {^(([-+][imnpst]+)+)$} $mode mode]} {
1078         error {channel mode must be * or match ([-+][imnpst]+)+}
1079     }
1080     chandb_set $chan mode $mode
1081     if {"$mode" == "*"} {
1082         ucmdr "I won't ever change the mode of $chan." {}
1083     } else {
1084         ucmdr "Whenever I'm alone on $chan, I'll set the mode to $mode." {}
1085     }
1086 }
1087
1088 def_chancmd show {
1089     if {[chandb_exists $chan]} {
1090         set l "Settings for $chan: autojoin "
1091         append l [lindex {no yes} [chandb_get $chan autojoin]]
1092         append l ", mode " [chandb_get $chan mode]
1093         append l ", userinvite " [chandb_get $chan userinvite] "."
1094         append l "\nManagers: "
1095         append l [join [chandb_get $chan managers] " "]
1096         foreach {ts sep} {see "\n" tell "  "} {
1097             set t [chandb_get $chan topic$ts]
1098             append l $sep
1099             if {[llength $t]} {
1100                 append l "Topic $ts list: $t."
1101             } else {
1102                 append l "Topic $ts list is empty."
1103             }
1104         }
1105         append l "\n"
1106         set t [chandb_get $chan topicset]
1107         if {[string length $t]} {
1108             append l "Topic to set: $t"
1109         } else {
1110             append l "I will not change the topic."
1111         }
1112         ucmdr {} $l
1113     } else {
1114         ucmdr {} "The channel $chan is not managed."
1115     }
1116 }
1117
1118 proc channelmgr_monoop {} {
1119     upvar 1 dest dest
1120     upvar 1 text text
1121     upvar 1 n n
1122     upvar 1 p p
1123     upvar 1 target target
1124     global chan_nicks
1125
1126     prefix_nick
1127
1128     if {[ischan $dest]} { set target $dest }
1129     if {[ta_anymore]} { set target [ta_word] }
1130     ta_nomore
1131     if {![info exists target]} {
1132         usererror "You must specify, or invoke me on, the relevant channel."
1133     }
1134     if {![info exists chan_nicks([irctolower $target])]} {
1135         usererror "I am not on $target."
1136     }
1137     if {![ischan $target]} { error "not a valid channel" }
1138
1139     if {![chandb_exists $target]} {
1140         usererror "$target is not a managed channel."
1141     }
1142     channel_securitycheck $target
1143 }
1144
1145 def_ucmd op {
1146     channelmgr_monoop
1147     sendout MODE $target +o $n
1148 }
1149
1150 def_ucmd leave {
1151     channelmgr_monoop
1152     doleave $target
1153 }
1154
1155 def_ucmd invite {
1156     global chan_nicks errorCode errorInfo
1157     prefix_nick
1158     
1159     if {[ischan $dest]} {
1160         set target $dest
1161         set onchan 1
1162     } else {
1163         set target [ta_word]
1164         set onchan 0
1165     }
1166     set ltarget [irctolower $target]
1167     if {![ischan $target]} { error "$target is not a channel" }
1168     if {![info exists chan_nicks($ltarget)]} {
1169         usererror "I am not on $target."
1170     }
1171     set ui [chandb_get $ltarget userinvite]
1172     if {[catch {
1173         if {"$ui" == "pub" && !$onchan} {
1174             usererror "Invitations to $target must be made there with !invite."
1175         }
1176         if {"$ui" != "all"} {
1177             if {[lsearch -exact $chan_nicks($ltarget) [irctolower $n]] < 0} {
1178                 usererror "Invitations to $target may only be made\
1179                         by a user on the channel."
1180             }
1181         }
1182         if {"$ui" == "none"} {
1183             usererror "Sorry, I've not been authorised\
1184                     to invite people to $target."
1185         }
1186     } emsg]} {
1187         if {"$errorCode" == "BLIGHT USER" && [channel_ismanager $target $n]} {
1188             if {[catch {
1189                 nick_securitycheck 1
1190             } emsg2]} {
1191                 if {"$errorCode" == "BLIGHT USER"} {
1192                     usererror "$emsg2  Therefore you can't use your\
1193                             channel manager privilege.  $emsg"
1194                 } else {
1195                     error $error $errorInfo $errorCode
1196                 }
1197             }
1198         } else {
1199             error $emsg $errorInfo $errorCode
1200         }
1201     }
1202     if {![ta_anymore]} {
1203         usererror "You have to say who to invite."
1204     }
1205     set invitees {}
1206     while {[ta_anymore]} {
1207         set invitee [ta_nick]
1208         lappend invitees $invitee
1209     }
1210     foreach invitee $invitees {
1211         sendout INVITE $invitee $ltarget
1212     }
1213     set who [lindex $invitees 0]
1214     switch -exact llength $invitees {
1215         0 { error "zero invitees" }
1216         1 { }
1217         2 { append who " and [lindex $invitees 1]" }
1218         * {
1219             set who [join [lreplace $invitees end end] ", "]
1220             append who " and [lindex $invitees [llength $invitees]]"
1221         }
1222     }
1223     ucmdr {} {} {} "invites $who to $target."
1224 }
1225
1226 def_ucmd channel {
1227     if {[ischan $dest]} { set target $dest }
1228     if {![ta_anymore]} {
1229         set subcmd show
1230     } else {
1231         set subcmd [ta_word]
1232     }
1233     if {[ischan $subcmd]} {
1234         set target $subcmd
1235         if {![ta_anymore]} {
1236             set subcmd show
1237         } else {
1238             set subcmd [ta_word]
1239         }
1240     }
1241     if {![info exists target]} { error "privately, you must specify a channel" }
1242     set procname channel/$subcmd
1243     if {"$subcmd" != "show"} {
1244         if {[catch { info body $procname }]} {
1245             usererror "unknown channel setting $subcmd."
1246         }
1247         prefix_nick
1248         if {[chandb_exists $target]} {
1249             channel_securitycheck $target
1250         } else {
1251             nick_securitycheck 1
1252             upvar #0 chan_initialop([irctolower $target]) io
1253             upvar #0 nick_unique([irctolower $n]) u
1254             if {![info exists io]} {
1255                 usererror "$target is not a managed channel."
1256             }
1257             if {"$io" != "$u"} {
1258                 usererror "You are not the interim manager of $target."
1259             }
1260             if {"$subcmd" != "manager"} {
1261                 usererror "Please use `channel manager' first."
1262             }
1263         }
1264     }
1265     channel/$subcmd
1266 }
1267
1268 proc nickdb_get_username {n} {
1269     if {![nickdb_exists $n]} { return "" }
1270     return [nickdb_get $n username]
1271 }
1272
1273 proc nickdb_get_sec_effective {n} {
1274     set l [nickdb_get $n tellsec]
1275     set u [nickdb_get_username $n]
1276     if {"[lindex $l 0]" == "secure" && ![string length $u]} { set l insecure }
1277     return $l
1278 }
1279
1280 proc tell_peernicks {text} {
1281     global errorInfo errorCode
1282     set text [irctolower [string trim $text]]
1283     set senders [split $text " "]
1284     foreach sender $senders {
1285         if {[catch { check_nick $sender } emsg]} {
1286             error "invalid nick `$sender': $emsg" $errorInfo $errorCode
1287         }
1288     }
1289     return $senders
1290 }
1291
1292 proc msgsdb_set_maydelete {n key l otherkey} {
1293     msgsdb_set $n $key $l
1294     if {[llength $l]} return
1295     if {[llength [msgsdb_get $n $otherkey]]} return
1296     msgsdb_delete $n
1297 }
1298
1299 proc tell_delete_msgs {lsenders lrecip} {
1300     set ninbound {}
1301     set ndel 0
1302     foreach {s t m} [msgsdb_get $lrecip inbound] {
1303         if {[llength $lsenders]} {
1304             if {[lsearch -exact $lsenders [irctolower $s]] == -1} {
1305                 lappend ninbound $s $t $m
1306                 continue
1307             }
1308         }
1309         set rsenders($s) 1
1310         incr ndel
1311     }
1312     msgsdb_set_maydelete $lrecip inbound $ninbound outbound
1313     if {![llength $ninbound]} {
1314         upvar #0 nick_telling($lrecip) telling
1315         catch { unset telling }
1316     }
1317     foreach s [array names rsenders] {
1318         set noutbound {}
1319         foreach {r t c} [msgsdb_get $s outbound] {
1320             if {"[irctolower $r]" == "$lrecip"} continue
1321             lappend noutbound $r $t $c
1322         }
1323         msgsdb_set_maydelete $s outbound $noutbound inbound
1324     }
1325     return $ndel
1326 }
1327
1328 def_ucmd untell {
1329     prefix_nick
1330     check_notonchan
1331     nick_securitycheck 0
1332     set recipients [tell_peernicks $text]
1333     if {![llength $recipients]} {
1334         usererror "You must say which recipients' messages from you to forget."
1335     }
1336     set ndel 0
1337     foreach recip $recipients {
1338         incr ndel [tell_delete_msgs [irctolower $n] $recip]
1339     }
1340     ucmdr "Removed $ndel as yet undelivered message(s)." {}
1341 }
1342
1343 def_ucmd delmsg {
1344     global errorInfo errorCode
1345     prefix_nick
1346     set nl [irctolower $n]
1347     check_notonchan
1348     manyset [nickdb_get_sec_effective $n] sec secwhen
1349     switch -exact $sec {
1350         insecure { }
1351         refuse - mailto {
1352             usererror \
1353  "There are no messages to delete\
1354  because your message disposition prevents them from being left."
1355         }
1356         secure {
1357             nick_securitycheck 1
1358         }
1359         default {
1360             error "delmsg sec $sec"
1361         }
1362     }
1363     if {![llength [msgsdb_get $n inbound]]} {
1364         ucmdr "No incoming messages to delete." {}
1365     }
1366     tell_getcstate
1367     if {![info exists u]} {
1368         usererror \
1369  "I can't delete your messages unless I can see you on a channel with me.\
1370   Otherwise I might delete a message I hadn't told you about yet."
1371     }
1372     if {"$stt" != "passed"} {
1373         set telling [list $u undelivered 0]
1374         usererror \
1375  "There are message(s) you may not yet have seen;\
1376  I'll deliver them to you now.\
1377   If you actually want to delete them, just tell me `delmsg' again."
1378     }
1379     set senders [tell_peernicks $text]
1380     set ndel [tell_delete_msgs [irctolower $senders] [irctolower $n]]
1381     if {!$ndel} {
1382         if {[llength $senders]} {
1383             ucmdr "No relevant incoming messages to delete." {}
1384         }
1385     }
1386     switch -exact [llength $senders] {
1387         0 { ucmdr {} {} "deletes your $ndel message(s)." }
1388         1 { ucmdr {} {} "deletes your $ndel message(s) from $senders." }
1389         default {
1390             ucmdr {} {} "deletes your $ndel message(s) from\
1391  [lreplace $senders end end] and [lindex $senders end]."
1392         }
1393     }
1394 }
1395
1396 def_ucmd tellme {
1397     prefix_nick
1398     ta_nomore
1399     check_notonchan
1400     switch -exact [tell_event [irctolower $n] tellme] {
1401         ERROR - INVALID { ucmdr {} {is ill.  Help!} }
1402         nomsgs { ucmdr {You have no messages.} {} }
1403         default { }
1404     }
1405 }
1406
1407 def_ucmd tell {
1408     global nick_case ownmailaddr ownfullname
1409     
1410     prefix_nick
1411     set target [ta_nick]
1412     if {![string length $text]} { error "tell them what?" }
1413     if {[string length $text] > 400} { error "message too long" }
1414
1415     set ltarget [irctolower $target]
1416     set ctarget $target
1417     if {[info exists nick_case($ltarget)]} { set ctarget $nick_case($ltarget) }
1418
1419     manyset [nickdb_get_sec_effective $target] sec mailtoint mailwhy
1420     manyset [nickdb_get $target tellrel] rel relint relwithin
1421     switch -exact $sec {
1422         insecure - secure {
1423             set now [clock seconds]
1424             set inbound [msgsdb_get $ltarget inbound]
1425             lappend inbound $n $now $text
1426             msgsdb_set $ltarget inbound $inbound
1427
1428             set outbound [msgsdb_get $n outbound]
1429             set noutbound {}
1430             set found 0
1431             foreach {recip time count} $outbound {
1432                 if {"[irctolower $recip]" == "$ltarget"} {
1433                     incr count
1434                     set recip $ctarget
1435                     set found 1
1436                 }
1437                 lappend noutbound $recip $time $count
1438             }
1439             if {!$found} {
1440                 lappend noutbound $ctarget $now 1
1441             }
1442             msgsdb_set $n outbound $noutbound
1443             set msg "OK, I'll tell $ctarget"
1444             if {$found} { append msg " that too" }
1445             append msg ", "
1446             if {"$sec" != "secure"} {
1447                 switch -exact $rel {
1448                     unreliable { append msg "neither reliably nor securely" }
1449                     remind { append msg "pretty reliably, but not securely" }
1450                     pester { append msg "reliably but not securely" }
1451                 }
1452             } else {
1453                 switch -exact $rel {
1454                     unreliable { append msg "securely but not reliably" }
1455                     remind { append msg "securely and pretty reliably" }
1456                     pester { append msg "reliably and securely" }
1457                 }
1458             }
1459             append msg .
1460             tell_event $ltarget msgsarrive
1461             ucmdr $msg {}
1462         }
1463         mailto {
1464             set fmtmsg [exec fmt << " $text"]
1465             exec /usr/sbin/sendmail -odb -oi -t -oee -f $mailwhy \
1466                     > /dev/null << \
1467  "From: $ownmailaddr ($ownfullname)
1468 To: $mailtoint
1469 Subject: IRC tell from $n
1470
1471 $n asked me[expr {[ischan $dest] ? " on $dest" : ""}] to tell you:
1472 [exec fmt << " $text"]
1473
1474 (This message was for your nick $ctarget; your account $mailwhy
1475  arranged for it to be forwarded to $mailtoint.)
1476 "
1477             ucmdr \
1478  "I've mailed $ctarget, which is what they prefer." \
1479                 {}
1480         }
1481         refuse {
1482             usererror "Sorry, $ctarget does not want me to take messages."
1483         }
1484         default {
1485             error "bad tellsec $sec"
1486         }
1487     }
1488 }
1489
1490 def_ucmd who {
1491     if {[ta_anymore]} {
1492         set target [ta_nick]; ta_nomore
1493         set myself 1
1494     } else {
1495         prefix_nick
1496         set target $n
1497         set myself [expr {"$target" != "$n"}]
1498     }
1499     set ltarget [irctolower $target]
1500     upvar #0 nick_case($ltarget) ctarget
1501     set nshow $target
1502     if {[info exists ctarget]} {
1503         upvar #0 nick_onchans($ltarget) oc
1504         upvar #0 nick_username($ltarget) nu
1505         if {[info exists oc]} { set nshow $ctarget }
1506     }
1507     if {![nickdb_exists $ltarget]} {
1508         set ol "$nshow is not a registered nick."
1509     } elseif {[string length [set username [nickdb_get $target username]]]} {
1510         set ol "The nick $nshow belongs to the user $username."
1511     } else {
1512         set ol "The nick $nshow is registered (but not to a username)."
1513     }
1514     if {![info exists ctarget] || ![info exists oc]} {
1515         if {$myself} {
1516             append ol "\nI can't see $nshow on anywhere."
1517         } else {
1518             append ol "\nYou aren't on any channels with me."
1519         }
1520     } elseif {![info exists nu]} {
1521         append ol "\n$nshow has not identified themselves."
1522     } elseif {![info exists username]} {
1523         append ol "\n$nshow has identified themselves as the user $nu."
1524     } elseif {"$nu" != "$username"} {
1525         append ol "\nHowever, $nshow is being used by the user $nu."
1526     } else {
1527         append ol "\n$nshow has identified themselves to me."
1528     }
1529     ucmdr {} $ol
1530 }
1531
1532 def_ucmd register {
1533     prefix_nick
1534     check_notonchan
1535     set old [nickdb_exists $n]
1536     if {$old} { nick_securitycheck 0 }
1537     set luser [irctolower $n]
1538     switch -exact [string tolower [string trim $text]] {
1539         {} {
1540             upvar #0 nick_username($luser) nu
1541             if {![info exists nu]} {
1542                 ucmdr {} \
1543  "You must identify yourself before using `register'.  See `help identify', or use `register insecure'."
1544             }
1545             nickdb_set $n username $nu
1546             ucmdr {} {} "makes a note of your username." {}
1547         }
1548         delete {
1549             nickdb_delete $n
1550             ucmdr {} {} "forgets your nickname." {}
1551         }
1552         insecure {
1553             nickdb_set $n username {}
1554             if {$old} {
1555                 ucmdr {} "Security is now disabled for your nickname !"
1556             } else {
1557                 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."
1558             }
1559         }
1560         default {
1561             error "you mean register / register delete / register insecure"
1562         }
1563     }
1564 }
1565
1566 proc timeformat_desc {tf} {
1567     switch -exact $tf {
1568         ks { return "Times will be displayed in seconds or kiloseconds." }
1569         hms { return "Times will be displayed in hours, minutes, etc." }
1570         beat { return "Times will be displayed in beats (1000B = 1d)." }
1571         default { error "invalid timeformat: $v" }
1572     }
1573 }
1574
1575 set settings {}
1576 proc def_setting {opt show_body set_body} {
1577     global settings
1578     lappend settings $opt
1579     proc set_show/$opt {} "
1580         upvar 1 n n
1581         set opt $opt
1582         $show_body"
1583     if {![string length $set_body]} return
1584     proc set_set/$opt {} "
1585         upvar 1 n n
1586         upvar 1 text text
1587         set opt $opt
1588         $set_body"
1589 }
1590
1591 proc tellme_sec_desc {v n} {
1592     manyset $v sec mailtoint
1593     switch -exact $sec {
1594         insecure {
1595             return "I'll tell you your messages whenever I see you."
1596         }
1597         secure {
1598             if {[string length [nickdb_get_username $n]]} {
1599                 return \
1600  "I'll keep the bodies of your messages private until you identify yourself, reminding you every [showintervalsecs $mailtoint 1]."
1601             } else {
1602                 return \
1603  "I'll tell you your messages whenever I see you.\
1604   (Secure message delivery is enabled, but your nick is not registered\
1605  securely.  See `help register'.)"
1606             }
1607         }
1608         refuse {
1609             return "I shan't accept messages for you."
1610         }
1611         mailto {
1612             return "I'll forward your messages by email to $mailtoint."
1613         }
1614         default {
1615             error "bad tellsec $sec"
1616         }
1617     }
1618 }
1619
1620 proc tellme_rel_desc {v n} {
1621     manyset $v rel every within
1622     switch -exact $rel {
1623         unreliable {
1624             return "As soon as I've told you message(s), I'll forget them\
1625  - note that this means messages can get lost !"
1626         }
1627         pester {
1628             set u {}
1629         }
1630         remind {
1631             set u ", or talk on channel within [showintervalsecs $within 1] of me having told you"
1632         }
1633         default {
1634             error "bad tellrel $rel"
1635         }
1636     }
1637     return "After delivering messages, I'll remind you every\
1638  [showintervalsecs $every 1] until you say delmsg$u."
1639 }
1640
1641 def_setting timeformat {
1642     set tf [nickdb_get $n timeformat]
1643     return "$tf: [timeformat_desc $tf]"
1644 } {
1645     set tf [string tolower [ta_word]]
1646     ta_nomore
1647     set desc [timeformat_desc $tf]
1648     nickdb_set $n timeformat $tf
1649     ucmdr {} $desc
1650 }
1651
1652 proc marktime_desc {mt} {
1653     if {"$mt" == "off"} {
1654         return "I will not send you periodic messages."
1655     } elseif {"$mt" == "once"} {
1656         return "I will send you one informational message when I see you."
1657     } else {
1658         return "I'll send you a message every [showintervalsecs $mt 0]."
1659     }
1660 }
1661
1662 def_setting marktime {
1663     set mt [nickdb_get $n marktime]
1664     set p $mt
1665     if {[string match {[0-9]*} $mt]} { append p s }
1666     append p ": "
1667     append p [marktime_desc $mt]
1668     return $p
1669 } {
1670     global marktime_min
1671     set mt [string tolower [ta_word]]
1672     ta_nomore
1673
1674     if {"$mt" == "off" || "$mt" == "once"} {
1675     } else {
1676         set mt [parse_interval $mt $marktime_min]
1677     }
1678     nickdb_set $n marktime $mt
1679     lnick_marktime_start [irctolower $n] "So:" 500 0
1680     ucmdr {} [marktime_desc $mt]
1681 }
1682
1683 def_setting security {
1684     set s [nickdb_get $n username]
1685     if {[string length $s]} {
1686         return "Your nick, $n, is controlled by the user $s."
1687     } else {
1688         return "Your nick, $n, is not secure."
1689     }
1690 } {}
1691
1692 proc tellme_setting_sec_simple {} {
1693     uplevel 1 {
1694         ta_nomore
1695         set sr sec
1696         set v $setting
1697     }
1698 }
1699
1700 proc tellme_setting_neednomsgs {} {
1701     uplevel 1 {
1702         if {[llength [msgsdb_get $n inbound]]} {
1703             usererror "You must delete the incoming messages you have, first."
1704         }
1705     }
1706 }
1707
1708 def_setting tellme {
1709     set secv [nickdb_get $n tellsec]
1710     set ms [tellme_sec_desc $secv $n]
1711     manyset $secv sec
1712     switch -exact $sec {
1713         insecure - secure {
1714             set mr [tellme_rel_desc [nickdb_get $n tellrel] $n]
1715             return "$ms  $mr"
1716         }
1717         refuse - mailto {
1718             return $ms
1719         }
1720     }
1721 } {
1722     set setting [string tolower [ta_word]]
1723     switch -exact $setting {
1724         insecure {
1725             tellme_setting_sec_simple
1726         }
1727         secure {
1728             set every [ta_interval_optional 60 600]
1729             ta_nomore
1730             set sr sec
1731             set v [list secure $every]
1732         }
1733         refuse {
1734             tellme_setting_neednomsgs
1735             tellme_setting_sec_simple
1736         }
1737         mailto {
1738             tellme_setting_neednomsgs
1739             
1740             if {![string length [set u [nickdb_get_username $n]]]} {
1741                 usererror \
1742  "Sorry, you must register securely to have your messages mailed\
1743  (to prevent the use of this feature for spamming).  See `help register'."
1744             }
1745             set sr sec
1746             set v [list mailto [ta_word] $u]
1747         }
1748         unreliable - pester - remind {
1749             manyset [nickdb_get $n tellsec] sec
1750             switch -exact $sec {
1751                 refuse - mailto {
1752                     usererror \
1753  "You can't change your message delivery conditions when\
1754  your message disposition prevents messages from being left."
1755                 }
1756             }
1757             set sr rel
1758             set v $setting
1759             if {"$setting" != "unreliable"} {
1760                 set every [ta_interval_optional 300 3600]
1761                 lappend v $every
1762             }
1763             if {"$setting" == "remind"} {
1764                 set within [ta_interval_optional 5 30]
1765                 if {$within > $every} {
1766                     error "remind interval must be at least time to respond"
1767                 }
1768                 lappend v $within
1769             }
1770             ta_nomore
1771         }
1772         default {
1773             error "invalid tellme setting $setting"
1774         }
1775     }
1776     nickdb_set $n tell$sr $v
1777     upvar #0 nick_telling([irctolower $n]) telling
1778     catch { unset telling }
1779     ucmdr [tellme_${sr}_desc $v $n] {}
1780 }
1781
1782 proc lnick_checktold {luser} {
1783     set ml [msgsdb_get $luser outbound]
1784     if {![llength $ml]} return
1785     set is1 [expr {[llength $ml]==3}]
1786     set m1 "FYI, I haven't yet delivered your"
1787     set ol {}
1788     set now [clock seconds]
1789     while {[llength $ml]} {
1790         manyset $ml r t n
1791         set ml [lreplace $ml 0 2]
1792         set td [expr {$now-$t}]
1793         if {$n == 1} {
1794             set iv [showinterval $td]
1795             set ifo "$r, $iv"
1796             set if1 "message to $r, $iv."
1797         } else {
1798             set iv [showintervalsecs $td 0]
1799             set ifo "$r, $n messages, oldest $iv"
1800             set if1 "$n messages to $r, oldest $iv."
1801         }
1802         if {$is1} {
1803             sendprivmsg $luser "$m1 $if1"
1804             return
1805         } else {
1806             lappend ol " to $ifo[expr {[llength $ml] ? ";" : "."}]"
1807         }
1808     }
1809     sendprivmsg $luser "$m1 messages:"
1810     msendprivmsg $luser $ol
1811 }
1812
1813 def_ucmd set {
1814     global settings
1815     prefix_nick
1816     check_notonchan
1817     if {![nickdb_exists $n]} {
1818         ucmdr {} "You are unknown to me and so have no settings.  (Use `register'.)"
1819     }
1820     if {![ta_anymore]} {
1821         set ol {}
1822         foreach opt $settings {
1823             lappend ol [format "%-10s %s" $opt [set_show/$opt]]
1824         }
1825         ucmdr {} [join $ol "\n"]
1826     } else {
1827         set opt [ta_word]
1828         if {[catch { info body set_show/$opt }]} {
1829             error "no setting $opt"
1830         }
1831         if {![ta_anymore]} {
1832             ucmdr {} "$opt: [set_show/$opt]"
1833         } else {
1834             nick_securitycheck 0
1835             if {[catch { info body set_set/$opt }]} {
1836                 error "setting $opt cannot be set with `set'"
1837             }
1838             set_set/$opt
1839         }
1840     }
1841 }
1842
1843 def_ucmd identpass {
1844     prefix_nick
1845     check_notonchan
1846     set luser [irctolower $n]
1847     set username [ta_word]
1848     set passmd5 [md5sum "[ta_word]\n"]
1849     ta_nomore
1850     upvar #0 nick_onchans($luser) onchans
1851     if {![info exists onchans] || ![llength $onchans]} {
1852         ucmdr "You must be on a channel with me to identify yourself." {}
1853     }
1854     check_username $username
1855     exec userv --timeout 3 $username << "$passmd5\n" > /dev/null \
1856             irc-identpass $n
1857     upvar #0 nick_username($luser) rec_username
1858     set rec_username $username
1859     after 50 [list tell_event $luser ident]
1860     ucmdr "Pleased to see you, $username." {}
1861 }
1862
1863 def_ucmd kill {
1864     global nick
1865     prefix_nick
1866     set target [ta_nick]
1867     if {![nickdb_exists $target]} { error "$target is not a registered nick." }
1868     set wantu [nickdb_get $target username]
1869     if {![string length $wantu]} { error "$target is insecurely registred." }
1870     upvar #0 nick_username([irctolower $n]) nu
1871     if {![info exists nu]} { error "You must identify yourself first." }
1872     if {"$wantu" != "$nu"} {
1873         error "You are the wrong user, $nu - $target belongs to $wantu."
1874     }
1875     set reason "at request of user $nu"
1876     if {[ta_anymore]} { append reason "; $text" }
1877     sendout KILL $target $reason
1878 }
1879
1880 def_ucmd summon {
1881     set target [ta_word]
1882     ta_nomore
1883     # fixme would be nice if the rest of the text was passed on instead
1884     check_username $target
1885     prefix_nick
1886
1887     upvar #0 lastsummon($target) ls
1888     set now [clock seconds]
1889     if {[info exists ls]} {
1890         set interval [expr {$now - $ls}]
1891         if {$interval < 30} {
1892             ucmdr {} \
1893  "Please be patient; $target was summoned only [showinterval $interval]."
1894         }
1895     }
1896     regsub {^[^!]*!} $p {} path
1897     if {[catch {
1898         exec userv --timeout 3 $target irc-summon $n $path \
1899                 [expr {[ischan $dest] ? "$dest" : ""}] \
1900                 < /dev/null
1901     } rv]} {
1902         regsub -all "\n" $rv { / } rv
1903         error $rv
1904     }
1905     if {[regexp {^problem (.*)} $rv dummy problem]} {
1906         ucmdr {} "The user `$target' $problem."
1907     } elseif {[regexp {^ok ([^ ]+) ([0-9]+)$} $rv dummy tty idlesince]} {
1908         set idletime [expr {$now - $idlesince}]
1909         set ls $now
1910         ucmdr {} {} {} "invites $target ($tty[expr {
1911             $idletime > 10 ? ", idle for [showintervalsecs $idletime 0]" : ""
1912         }]) to [expr {
1913             [ischan $dest] ? "join us here" : "talk to you"
1914         }]."
1915     } else {
1916         error "unexpected response from userv service: $rv"
1917     }
1918 }
1919
1920 proc md5sum {value} { exec md5sum << $value }
1921
1922 def_ucmd seen {
1923     global lastseen nick
1924     prefix_nick
1925     set ncase [ta_nick]
1926     set nlower [irctolower $ncase]
1927     ta_nomore
1928     set now [clock seconds]
1929     if {"$nlower" == "[irctolower $nick]"} {
1930         usererror "I am not self-aware."
1931     } elseif {![info exists lastseen($nlower)]} {
1932         set rstr "I've never seen $ncase."
1933     } else {
1934         manyset $lastseen($nlower) realnick time what
1935         set howlong [expr {$now - $time}]
1936         set string [showinterval $howlong]
1937         set rstr "I last saw $realnick $string, $what."
1938     }
1939     if {[ischan $dest]} {
1940         set where $dest
1941     } else {
1942         set where {}
1943     }
1944     upvar #0 lookedfor($nlower) lf
1945     if {[info exists lf]} { set oldvalue $lf } else { set oldvalue {} }
1946     set lf [list [list $now $n $where]]
1947     foreach v $oldvalue {
1948         if {"[irctolower [lindex $v 1]]" == "[irctolower $n]"} continue
1949         lappend lf $v
1950     }
1951     ucmdr {} $rstr
1952 }
1953
1954 proc lnick_marktime_cancel {luser} {
1955     upvar #0 nick_markid($luser) mi
1956     if {![info exists mi]} return
1957     catch { after cancel $mi }
1958     catch { unset mi }
1959 }
1960
1961 proc lnick_marktime_doafter {luser why ms mentiontold} {
1962     lnick_marktime_cancel $luser
1963     upvar #0 nick_markid($luser) mi
1964     set mi [after $ms [list lnick_marktime_now $luser $why 0]]
1965 }
1966
1967 proc lnick_marktime_reset {luser} {
1968     set mt [nickdb_get $luser marktime]
1969     if {"$mt" == "off" || "$mt" == "once"} return
1970     lnick_marktime_doafter $luser "Time passes." [expr {$mt*1000}] 0
1971 }
1972
1973 proc lnick_marktime_start {luser why ms mentiontold} {
1974     set mt [nickdb_get $luser marktime]
1975     if {"$mt" == "off"} {
1976         lnick_marktime_cancel $luser
1977         if {$mentiontold} { after $ms [list lnick_checktold $luser] }
1978     } else {
1979         lnick_marktime_doafter $luser $why $ms $mentiontold
1980     }
1981 }
1982
1983 proc lnick_marktime_now {luser why mentiontold} {
1984     upvar #0 nick_onchans($luser) oc
1985     global calling_nick
1986     set calling_nick $luser
1987     sendprivmsg $luser [lnick_pingstring $why $oc ""]
1988     if {$mentiontold} { after 150 [list lnick_checktold $luser] }
1989     lnick_marktime_reset $luser
1990 }    
1991
1992 proc lnick_pingstring {why oc apstring} {
1993     global nick_onchans
1994     catch { exec uptime } uptime
1995     set nnicks [llength [array names nick_onchans]]
1996     if {[regexp \
1997  {^ *([0-9:apm]+) +up.*, +(\d+) users?, +load average: +([0-9., ]+) *$} \
1998             $uptime dummy time users load]} {
1999         regsub -all , $load {} load
2000         set uptime "$time  $nnicks/$users  $load"
2001     } else {
2002         append uptime ", $nnicks nicks"
2003     }
2004     if {[llength $oc]} {
2005         set best_la 0
2006         set activity quiet
2007         foreach ch $oc {
2008             upvar #0 chan_lastactivity($ch) la
2009             if {![info exists la]} continue
2010             if {$la <= $best_la} continue
2011             set since [showintervalsecs [expr {[clock seconds]-$la}] 1]
2012             set activity "$ch $since"
2013             set best_la $la
2014         }
2015     } else {
2016         set activity unseen
2017     }
2018     set str $why
2019     append str "  " $uptime "  " $activity
2020     if {[string length $apstring]} { append str "  " $apstring }
2021     return $str
2022 }
2023
2024 def_ucmd ping {
2025     prefix_nick
2026     set ln [irctolower $n]
2027     if {[ischan $dest]} {
2028         set oc [irctolower $dest]
2029     } else {
2030         global nick_onchans
2031         if {[info exists nick_onchans($ln)]} {
2032             set oc $nick_onchans($ln)
2033         } else {
2034             set oc {}
2035         }
2036         if {[llength $oc]} { lnick_marktime_reset $ln }
2037     }
2038     after 150 [list lnick_checktold $ln]
2039     ucmdr {} [lnick_pingstring "Pong!" $oc $text]
2040 }
2041
2042 proc ensure_globalsecret {} {
2043     global globalsecret
2044     
2045     if {[info exists globalsecret]} return
2046     set gsfile [open /dev/urandom r]
2047     fconfigure $gsfile -translation binary
2048     set globalsecret [read $gsfile 32]
2049     binary scan $globalsecret H* globalsecret
2050     close $gsfile
2051     unset gsfile
2052 }
2053
2054 proc connected {} {
2055     global operuserpass
2056     if {[info exists operuserpass]} {
2057         eval sendout OPER $operuserpass
2058     }
2059     foreach chan [chandb_list] {
2060         if {[chandb_get $chan autojoin]} { dojoin $chan }
2061     }
2062 }
2063
2064 ensure_globalsecret
2065 loadhelp
2066 ensure_connecting