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