chiark / gitweb /
bugfixes
[ircbot] / ledmodule.tcl
1 # maintains local list of users to userv-slurp config from
2 # each user provides list of
3 #    monitors
4 #    devicesets
5 #
6 # a monitor specifies
7 #    name
8 #    IRC channel(s)
9 #    nicks ignore totally
10 #    nicks ignore presence
11 #    nicks prefer speech
12 #    time for `a while ago'
13 #    time for `very-recently'
14 # syntax
15 #    nick ignore|nopresence|prefer <glob-pattern> [...]
16 #    times <very-recently> <a-while-ago>                  (default 120 450)
17 #      (affect subsequent `monitor' directives)
18 #    monitor <monname> <#chan>[,<#chan>...]
19 #      <monname> must start with <username>:
20 #
21 # a deviceset specifies
22 #    monitor
23 #    led-group
24 #    led states
25 # syntax
26 #    leds <led-group> <monname> <state>=<value>
27 # where state is one of
28 #    [pref]talk[now]  any non-ignored (with `pref', only any preferred)
29 #                     nick(s) spoke at least somewhat recently
30 #                     (with `now', only if they spoke very recently)
31 #    present          at least some non-nopresence nicks present
32 #    default          always matches
33 # where the first matching state wins; if none, no LEDs are set
34
35 set helpfile ledhelp
36
37 source irccore.tcl
38 source parsecmd.tcl
39 source stdhelp.tcl
40 source userv.tcl
41
42 defset errchan #$nick
43 defset retry_after 900000
44 defset chan_after 3000
45 defset chans_retry 3600000
46 defset debug_reset_after 86400000
47
48 defset debugusers {}
49
50 # variables
51 #
52 #   monitor/$monname(chans)           -> [list $chan1 $chan2 ...]
53 #   monitor/$monname(ignore)          -> [list $regexp ...]
54 #   monitor/$monname(prefer)          -> [list $regexp ...]
55 #   monitor/$monname(present-$chan)   -> [list $lnick ...]
56 #   monitor/$monname(last-talk)       -> $time_t
57 #   monitor/$monname(last-talkpref)   -> $time_t
58 #   monitor/$monname(time-recent)     -> $seconds
59 #   monitor/$monname(time-recentnow)  -> $seconds
60 #   monitor/$monname(talkchange)      -> [after ...]    or unset
61 #
62 #   deviceset/$username:$lno(monname)  -> $monname
63 #   deviceset/$username:$lno(group)    -> $led_group
64 #   deviceset/$username:$lno(username) -> $username
65 #   deviceset/$username:$lno(values)   -> $valuestring
66 #   deviceset/$username:$lno(states)   -> [list $state1 $value1 $state2 ...]
67 #   deviceset/$username:$lno(ochan)    -> [open remoteleds ... | r]  or unset
68 #   deviceset/$username:$lno(ichan)    -> fifo for remoteleds input  or unset
69 #   deviceset/$username:$lno(retry)    -> [after ... ]             or unset
70 #
71 #   onchans($chan)        [list mustleave]                 # in config_chane
72 #   onchans($chan)        [list idle]
73 #   onchans($chan)        [list forced]                    # for errchan
74 #   onchans($chan)        [list shortly [after ...]]       # do a NAMES
75
76 proc ldebug {facil m} {
77     global debugusers
78     # facil is
79     #    m$monname
80     #    d$deviceset
81     #    c$lchan
82     #    {}             for system stuff
83     if {![llength debugusers]} return
84     if {[regexp {[md]([^:]+)\:} $facil dummy username] &&
85         [lsearch -exact $debugusers $username]==-1} return
86
87     regsub {^(.)} $facil {\1 } cc
88     reporterr "DEBUG $cc $m"
89 }    
90
91 proc list_objs {vp} {
92     set l {}
93     foreach v [info globals] {
94         if {![regsub ^$vp/ $v {} v]} continue
95         lappend l $v
96     }
97     return $l
98 }
99
100 proc privmsg_unlogged {p ischan params} {
101     global errorInfo
102     if {!$ischan} { return 0 }
103
104     # on-channel message
105     if {[catch {
106         prefix_nick
107         foreach m [list_objs monitor] {
108             mon_speech $m [irctolower [lindex $params 0]] [irctolower $n]
109         }
110     } emsg]} {
111         log "processing error: $emsg\n$errorInfo"
112     }
113     return 1;
114 }
115
116 proc reporterr {m} {
117     global errchan
118     sendprivmsg $errchan $m
119 }
120
121 proc msg_PRIVMSG {p c dest text} {
122     global errchan
123     prefix_nick
124     execute_usercommand $p $c $n $errchan $dest $text
125 }
126
127 proc proc_mon {name argl body} {
128     proc mon_$name [concat m $argl] "
129     upvar #0 monitor/\$m mm
130     $body"
131 }
132
133 proc mon_nick_is {globlist ln} {
134     foreach gl $globlist {
135         if {[string match $gl $ln]} { return 1 }
136     }
137     return 0
138 }
139
140 proc_mon gotchanlist {ch nll} {
141     if {[lsearch -exact $mm(chans) $ch] == -1} return
142     set mm(present-$ch) $nll
143     mon_updateall $m
144 }
145
146 proc_mon speech {chan ln} {
147     if {[lsearch -exact $mm(chans) $chan] == -1} return
148     if {[mon_nick_is $mm(ignore) $ln]} return
149     set now [clock seconds]
150     set mm(last-talk) $now
151     if {[mon_nick_is $mm(prefer) $ln]} { set mm(last-talkpref) $now }
152     mon_updateall $m
153 }
154
155 proc_mon calcstate {} {
156     set s " default "
157     foreach ch $mm(chans) {
158         if {[llength $mm(present-$ch)]} { append s "present "; break }
159     }
160     set now [clock seconds]
161     set valid_until [expr {$now + 86400}]
162     set refresh_later 0
163     catch { after cancel $mm(talkchange) }
164     foreach p {{} pref} {
165         foreach t {{} now} {
166             set vu [expr {$mm(last-talk$p) + $mm(time-recent$t)}]
167             if {$vu < $now} continue
168             append s "${p}talk${t} "
169             set refresh_later 1
170             if {$vu < $valid_until} { set valid_until $vu }
171         }
172     }
173     regsub {^ default } $s { } ss
174     set ds [string trim $ss]
175     if {$refresh_later} {
176         set interval [expr {$valid_until - $now + 2}]
177         set ivms [expr {$interval*1000}]
178         set mm(talkchange) [after $ivms [list mon_updateall $m]]
179         ldebug m$m "until now+${interval}: $ds"
180     } else {
181         ldebug m$m "indefinitely: $ds"
182     }
183     return $s
184 }
185
186 proc_mon updateall {} {
187     set s [mon_calcstate $m]
188     foreach d [list_objs deviceset] {
189         upvar #0 deviceset/$d dd
190         if {[string compare $m $dd(monname)]} continue
191         dset_setbystate $d $s
192     }
193 }
194
195 proc_mon destroy {} {
196     ldebug m$m "destroying"
197     catch { after cancel $mm(talkchange) }
198     catch { unset mm }
199 }
200
201 proc proc_dset {name argl body} {
202     proc dset_$name [concat d $argl] "
203     upvar #0 deviceset/\$d dd
204     if {\[catch {
205         $body
206     } emsg\]==1} {
207         reporterr \"error on \$d: \$emsg\"
208     }"
209 }
210
211 proc timed_log {m} {
212     log "[clock seconds] $m"
213 }
214
215 proc_dset setbystate {s} {
216     foreach {sq v} $dd(states) {
217         if {![string match *$sq* $s]} continue
218         set lv $v; break
219     }
220     if {![info exists lv]} {
221         reporterr "no state for $d matching$s"
222         return
223     }
224     ldebug d$d "matches $sq: $v"
225     timed_log "->$d $lv"
226     set dd(values) "$sq=$lv"
227     puts $dd(ichan) $lv
228 }
229
230 proc_dset destroy {} {
231     ldebug d$d "destroying"
232     catch { after cancel $dd(retry) }
233     catch {
234         if {[info exists dd(ochan)]} { timed_log ">\$$d destroy" }
235         close $dd(ochan)
236         close $dd(ichan)
237     }
238     catch { unset dd }
239 }
240
241 proc modvar_save_copy {cv defv} {
242     upvar 1 m m
243     upvar 1 mm mm
244     upvar 1 save/$m save
245     if {[info exists save($cv)]} {
246         set mm($cv) $save($cv)
247     } else {
248         set mm($cv) $defv
249     }
250 }
251
252 proc reloaduser {username} {
253     check_username $username
254     ldebug u$username "reloading"
255     if {[catch {
256         set cfg [exec userv --timeout 3 $username irc-ledcontrol-config \
257                 < /dev/null]
258     } emsg]} {
259         regsub "\n" $emsg " // " emsg
260         reporterr "error reloading $username: $emsg"
261         return ""
262     }
263     foreach d [list_objs deviceset] {
264         if {![string match $username:* $d]} continue
265         dset_destroy $d
266     }
267     foreach m [list_objs monitor] {
268         if {![string match $username* $m]} continue
269         upvar #0 monitor/$m mm
270         foreach cv [array names mm] { set save/${m}($cv) $mm($cv) }
271     }
272     if {![string length $cfg]} {
273         file delete pwdb/$username
274         return "no config from $username"
275     } elseif {[catch {
276         exec userv --timeout 3 $username irc-ledcontrol-passwords \
277                 < /dev/null > pwdb/p$username
278     } emsg]} {
279         reporterr "error reading passwords for $username: $emsg"
280         return ""
281     } elseif {[catch {
282         ldebug u$username "parsing"
283         foreach cv {ignore nopresence prefer} { set cc($cv) {} }
284         set cc(time-recentnow) 120
285         set cc(time-recent) 450
286         set lno 0
287         foreach l [split $cfg "\n"] {
288             incr lno
289             set l [string trim $l]
290             if {[regexp {^\#} $l]} {
291             } elseif {[regexp {^nick\s+(ignore|nopresence|prefer)\s+(\S.*)$} \
292                     $l dummy kind globs]} {
293                 set cc($kind) {}
294                 foreach gl [split $globs " "] {
295                     if {![string length $gl]} continue
296                     string match $gl {}
297                     lappend cc($kind) $gl
298                 }
299             } elseif {[regexp {^times\s+(\d+)\s+(\d+)$} $l dummy r rnow]} {
300                 foreach cv {{} now} { set cc(time-recent$cv) [set r$cv] }
301             } elseif {[regexp {^monitor\s+(\S+)\s+(\S.*)$} $l dummy m cl]} {
302                 set cc(chans) {}
303                 if {![string match $username:* $m]} {
304                     error "monname must start with $username:"
305                 }
306                 check_monname $m
307                 foreach ch [split $cl " "] {
308                     if {![string length $ch]} continue
309                     check_chan $ch
310                     if {![ischan $ch]} { error "invalid channel $ch" }
311                     lappend cc(chans) [irctolower $ch]
312                     chan_shortly $ch
313                 }
314                 upvar #0 monitor/$m mm
315                 foreach cv [array names cc] { set mm($cv) $cc($cv) }
316                 foreach cv {{} pref} {
317                     modvar_save_copy last-talk$cv 0
318                 }
319                 foreach cv [array names mm(chans)] {
320                     modvar_save_copy present-$cv {}
321                 }
322                 ldebug m$m "created"
323             } elseif {[regexp \
324  {^leds\s+([0-9A-Za-z][-:/0-9A-Za-z]+)\s+(\S+)\s+(\S+.*)$} \
325                     $l dummy g m states]} {
326                 set d $username:$lno:$g
327                 set sl {}
328                 check_monname $m
329                 foreach sv [split $states " "] {
330                     if {![string length $sv]} continue
331                     if {![regexp \
332  {^((?:pref)?talk(?:now)?|present|default)\=([0-9a-z][,/+0-9A-Za-z]*)$} \
333                           $sv dummy lhs rhs]} {
334                         error "invalid state spec"
335                     }
336                     lappend sl $lhs $rhs
337                 }
338                 upvar #0 deviceset/$d dd
339                 set dd(monname) $m
340                 set dd(states) $sl
341                 set dd(group) $g
342                 set dd(values) startup
343                 set dd(username) $username
344                 dset_start $d
345                 ldebug d$d "created"
346             }
347         }
348     } emsg]} {
349         reporterr "setup error $username:$lno:$emsg"
350         return ""
351     } else {
352         return "reloaded $username"
353     }
354 }
355
356 proc check_monname {m} {
357     if {[regexp {[^-_+:.#0-9a-zA-Z]} $m badchar]} {
358         error "char $badchar not allowed in monnames"
359     }
360     if {![regexp {^[0-9a-zA-Z]} $m]} {
361         error "monname must start with alphanum"
362     }
363 }
364
365 proc_dset start {} {
366     catch { unset dd(retry) }
367     set username $dd(username)
368     ldebug d$d "starting"
369     if {[catch {
370         set cmdl [list remoteleds --pipe $dd(group) \
371                        --human --passfile-only pwdb/p$username]
372         timed_log "!-$d [join $cmdl " "]"
373         lappend cmdl < pwdb/fifo |& cat
374         catch { file delete pwdb/fifo }
375         exec mkfifo -m 0600 pwdb/fifo
376         set ichan [open pwdb/fifo r+]
377         set ochan [open |$cmdl r]
378         fconfigure $ichan -blocking 0 -buffering line
379         fconfigure $ochan -blocking 0 -buffering line
380         fileevent $ochan readable [list dset_rledout $d]
381         set dd(ichan) $ichan
382         set dd(ochan) $ochan
383     } emsg]} {
384         reporterr "remoteleds startup $d: $emsg"
385         catch { close $ichan }
386         catch { close $ochan }
387         dset_trylater $d
388     }
389 }
390
391 proc_dset rledout {} {
392     global errchan
393     while {[gets $dd(ochan) l] != -1} {
394         reporterr "remoteleds on $d: $dd(values): $l"
395     }
396     if {[fblocked $dd(ochan)]} return
397     timed_log ">\$$d failure";
398     catch { close $dd(ichan) }
399     catch { close $dd(ochan) }
400     unset dd(ichan)
401     unset dd(ochan)
402     reporterr "remoteleds on $d died"
403     dset_trylater $d
404 }
405
406 proc_dset trylater {} {
407     global retry_after
408     ldebug d$d "will try again later"
409     set dd(retry) [after $retry_after [list dset_start $d]]
410 }
411
412 proc config_change {} {
413     global onchans chans_retry errchan config_retry_after
414     ldebug {} "rechecking configuration etc"
415     foreach ch [array names onchans] {
416         manyset $onchans($ch) status after
417         if {"$status" == "shortly"} {
418             catch { after cancel $after }
419         }
420         set onchans($ch) mustleave
421     }
422     sendout JOIN $errchan
423     chan_shortly $errchan
424     foreach m [list_objs monitor] {
425         upvar #0 monitor/$m mm
426         foreach ch $mm(chans) {
427             sendout JOIN $ch
428             chan_shortly $ch
429         }
430     }
431     foreach ch [array names onchans] {
432         if {"[lindex $onchans($ch) 0]" != "mustleave"} continue
433         sendout PART $ch
434         unset onchans($ch)
435     }
436     catch { after cancel $config_retry_after }
437     set config_retry_after [after $chans_retry config_change]
438 }
439
440 proc chan_shortly {ch} {
441     global chan_after
442     upvar #0 onchans($ch) oc
443     if {[info exists oc]} {
444         manyset $oc status after
445         if {"$status" == "shortly"} {
446             ldebug c$ch "queued check already pending"
447             return
448         }
449     }
450     ldebug c$ch "queueing check"
451     set oc [list shortly [after $chan_after chan_sendnames $ch]]
452 }
453
454 proc msg_353 {p c dest type chan nicklist} {
455     set lchan [irctolower $chan]
456     set nll [irctolower $nicklist]
457     ldebug c$lchan "got names $nll"
458     foreach m [list_objs monitor] {
459         mon_gotchanlist $m $lchan $nll
460     }
461 }
462
463 proc chan_sendnames {ch} {
464     upvar #0 onchans($ch) oc
465     ldebug c$ch "asking for namelist"
466     sendout NAMES $ch
467     set oc idle
468 }
469
470 def_ucmd reload {
471     set username [ta_word]
472     ta_nomore
473     set m [reloaduser $username]
474     config_change
475     ucmdr {} $m
476 }
477
478 proc debug_reset {} {
479     global debugusers debug_cancelling
480     unset debug_cancelling
481     set debugusers {}
482     reporterr "debug mode timed out"
483 }
484
485 def_ucmd debug {
486     prefix_nick
487     global debugusers debug_cancelling debug_reset_after
488     if {![string length $text]} { error "must give list of usernames" }
489     llength $text
490     set debugusers $text
491     catch { after cancel $debug_cancelling }
492     set debug_cancelling [after $debug_reset_after debug_reset]
493     reporterr "debug enabled by $n: $debugusers"
494 }
495
496 def_ucmd nodebug {
497     prefix_nick
498     global debugusers debug_cancelling
499     set debugusers {}
500     catch { after cancel $debug_cancelling }
501     catch { unset debug_cancelling }
502     reporterr "debug disabled by $n"
503 }
504
505 def_ucmd who {
506     set r {}
507     foreach m [list_objs monitor] {
508         upvar #0 monitor/$m mm
509         lappend r "monitoring $mm(chans) for $m"
510     }
511     foreach d [list_objs deviceset] {
512         upvar #0 deviceset/$d dd
513         regexp {^[^:]*\:[^:]*} $dd(group) dest
514         lappend r "sending $dd(monname) to $dest"
515     }
516     ucmdr [join $r "\n"] {}
517 }
518
519 proc connected {} {
520     ldebug {} "connected"
521     foreach f [glob -nocomplain pwdb/p*] {
522         regexp {^pwdb/p(.*)$} $f dummy username
523         set m [reloaduser $username]
524     }
525     config_change
526 }
527
528 # JOIN PART
529 # KICK KILL QUIT
530
531 if {[catch {
532     loadhelp
533     ensure_connecting
534 } emsg]} {
535     fail "startup: $emsg"
536 }