chiark / gitweb /
other events
[ircbot.git] / 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 1500
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     global nick
142     if {[lsearch -exact $mm(chans) $ch] == -1} return
143     set l {}
144     foreach nl $nll {
145         if {![string compare $nl [irctolower $nick]]} continue
146         if {[mon_nick_is $mm(nopresence) $nl]} continue
147         lappend $l $nl
148     }
149     set mm(present-$ch) $l
150     mon_updateall $m
151 }
152
153 proc_mon speech {chan ln} {
154     if {[lsearch -exact $mm(chans) $chan] == -1} return
155     if {[mon_nick_is $mm(ignore) $ln]} return
156     set now [clock seconds]
157     set mm(last-talk) $now
158     if {[mon_nick_is $mm(prefer) $ln]} { set mm(last-talkpref) $now }
159     mon_updateall $m
160 }
161
162 proc_mon calcstate {} {
163     set s " default "
164     foreach ch $mm(chans) {
165         if {[llength $mm(present-$ch)]} { append s "present "; break }
166     }
167     set now [clock seconds]
168     set valid_until [expr {$now + 86400}]
169     set refresh_later 0
170     catch { after cancel $mm(talkchange) }
171     foreach p {{} pref} {
172         foreach t {{} now} {
173             set vu [expr {$mm(last-talk$p) + $mm(time-recent$t)}]
174             if {$vu < $now} continue
175             append s "${p}talk${t} "
176             set refresh_later 1
177             if {$vu < $valid_until} { set valid_until $vu }
178         }
179     }
180     regsub {^ default } $s { } ss
181     set ds [string trim $ss]
182     if {$refresh_later} {
183         set interval [expr {$valid_until - $now + 2}]
184         set ivms [expr {$interval*1000}]
185         set mm(talkchange) [after $ivms [list mon_updateall $m]]
186         ldebug m$m "until now+${interval}: $ds"
187     } else {
188         ldebug m$m "indefinitely: $ds"
189     }
190     return $s
191 }
192
193 proc_mon updateall {} {
194     set s [mon_calcstate $m]
195     foreach d [list_objs deviceset] {
196         upvar #0 deviceset/$d dd
197         if {[string compare $m $dd(monname)]} continue
198         dset_setbystate $d $s
199     }
200 }
201
202 proc_mon destroy {} {
203     ldebug m$m "destroying"
204     catch { after cancel $mm(talkchange) }
205     catch { unset mm }
206 }
207
208 proc proc_dset {name argl body} {
209     proc dset_$name [concat d $argl] "
210     upvar #0 deviceset/\$d dd
211     if {\[catch {
212         $body
213     } emsg\]==1} {
214         reporterr \"error on \$d: \$emsg\"
215     }"
216 }
217
218 proc timed_log {m} {
219     log "[clock seconds] $m"
220 }
221
222 proc_dset setbystate {s} {
223     foreach {sq v} $dd(states) {
224         if {![string match *$sq* $s]} continue
225         set lv $v; break
226     }
227     if {![info exists lv]} {
228         reporterr "no state for $d matching$s"
229         return
230     }
231     ldebug d$d "matches $sq: $v"
232     timed_log "->$d $lv"
233     set dd(values) "$sq=$lv"
234     puts $dd(ichan) $lv
235 }
236
237 proc_dset destroy {} {
238     ldebug d$d "destroying"
239     catch { after cancel $dd(retry) }
240     catch {
241         if {[info exists dd(ochan)]} { timed_log ">\$$d destroy" }
242         close $dd(ochan)
243         close $dd(ichan)
244     }
245     catch { unset dd }
246 }
247
248 proc modvar_save_copy {cv defv} {
249     upvar 1 m m
250     upvar 1 mm mm
251     upvar 1 save/$m save
252     if {[info exists save($cv)]} {
253         set mm($cv) $save($cv)
254     } else {
255         set mm($cv) $defv
256     }
257 }
258
259 proc reloaduser {username} {
260     check_username $username
261     ldebug u$username "reloading"
262     if {[catch {
263         set cfg [exec userv --timeout 3 $username irc-ledcontrol-config \
264                 < /dev/null]
265     } emsg]} {
266         regsub "\n" $emsg " // " emsg
267         reporterr "error reloading $username: $emsg"
268         return ""
269     }
270     foreach d [list_objs deviceset] {
271         if {![string match $username:* $d]} continue
272         dset_destroy $d
273     }
274     foreach m [list_objs monitor] {
275         if {![string match $username* $m]} continue
276         upvar #0 monitor/$m mm
277         foreach cv [array names mm] { set save/${m}($cv) $mm($cv) }
278     }
279     if {![string length $cfg]} {
280         file delete pwdb/$username
281         return "no config from $username"
282     } elseif {[catch {
283         exec userv --timeout 3 $username irc-ledcontrol-passwords \
284                 < /dev/null > pwdb/p$username
285     } emsg]} {
286         reporterr "error reading passwords for $username: $emsg"
287         return ""
288     } elseif {[catch {
289         ldebug u$username "parsing"
290         foreach cv {ignore nopresence prefer} { set cc($cv) {} }
291         set cc(time-recentnow) 120
292         set cc(time-recent) 450
293         set lno 0
294         foreach l [split $cfg "\n"] {
295             incr lno
296             set l [string trim $l]
297             if {[regexp {^\#} $l]} {
298             } elseif {[regexp {^nick\s+(ignore|nopresence|prefer)\s+(\S.*)$} \
299                     $l dummy kind globs]} {
300                 set cc($kind) {}
301                 foreach gl [split $globs " "] {
302                     if {![string length $gl]} continue
303                     string match $gl {}
304                     lappend cc($kind) $gl
305                 }
306             } elseif {[regexp {^times\s+(\d+)\s+(\d+)$} $l dummy r rnow]} {
307                 foreach cv {{} now} { set cc(time-recent$cv) [set r$cv] }
308             } elseif {[regexp {^monitor\s+(\S+)\s+(\S.*)$} $l dummy m cl]} {
309                 set cc(chans) {}
310                 if {![string match $username:* $m]} {
311                     error "monname must start with $username:"
312                 }
313                 check_monname $m
314                 foreach ch [split $cl " "] {
315                     if {![string length $ch]} continue
316                     check_chan $ch
317                     if {![ischan $ch]} { error "invalid channel $ch" }
318                     lappend cc(chans) [irctolower $ch]
319                     chan_shortly $ch
320                 }
321                 upvar #0 monitor/$m mm
322                 foreach cv [array names cc] { set mm($cv) $cc($cv) }
323                 foreach cv {{} pref} {
324                     modvar_save_copy last-talk$cv 0
325                 }
326                 foreach cv [array names mm(chans)] {
327                     modvar_save_copy present-$cv {}
328                 }
329                 ldebug m$m "created"
330             } elseif {[regexp \
331  {^leds\s+([0-9A-Za-z][-:/0-9A-Za-z]+)\s+(\S+)\s+(\S+.*)$} \
332                     $l dummy g m states]} {
333                 set d $username:$lno:$g
334                 set sl {}
335                 check_monname $m
336                 foreach sv [split $states " "] {
337                     if {![string length $sv]} continue
338                     if {![regexp \
339  {^((?:pref)?talk(?:now)?|present|default)\=([0-9a-z][,/+0-9A-Za-z]*)$} \
340                           $sv dummy lhs rhs]} {
341                         error "invalid state spec"
342                     }
343                     lappend sl $lhs $rhs
344                 }
345                 upvar #0 deviceset/$d dd
346                 set dd(monname) $m
347                 set dd(states) $sl
348                 set dd(group) $g
349                 set dd(values) startup
350                 set dd(username) $username
351                 dset_start $d
352                 ldebug d$d "created"
353             }
354         }
355     } emsg]} {
356         reporterr "setup error $username:$lno:$emsg"
357         return ""
358     } else {
359         return "reloaded $username"
360     }
361 }
362
363 proc check_monname {m} {
364     if {[regexp {[^-_+:.#0-9a-zA-Z]} $m badchar]} {
365         error "char $badchar not allowed in monnames"
366     }
367     if {![regexp {^[0-9a-zA-Z]} $m]} {
368         error "monname must start with alphanum"
369     }
370 }
371
372 proc_dset start {} {
373     catch { unset dd(retry) }
374     set username $dd(username)
375     ldebug d$d "starting"
376     if {[catch {
377         set cmdl [list remoteleds --pipe $dd(group) \
378                        --human --passfile-only pwdb/p$username]
379         timed_log "!-$d [join $cmdl " "]"
380         lappend cmdl < pwdb/fifo |& cat
381         catch { file delete pwdb/fifo }
382         exec mkfifo -m 0600 pwdb/fifo
383         set ichan [open pwdb/fifo r+]
384         set ochan [open |$cmdl r]
385         fconfigure $ichan -blocking 0 -buffering line
386         fconfigure $ochan -blocking 0 -buffering line
387         fileevent $ochan readable [list dset_rledout $d]
388         set dd(ichan) $ichan
389         set dd(ochan) $ochan
390     } emsg]} {
391         reporterr "remoteleds startup $d: $emsg"
392         catch { close $ichan }
393         catch { close $ochan }
394         dset_trylater $d
395     }
396 }
397
398 proc_dset rledout {} {
399     global errchan
400     while {[gets $dd(ochan) l] != -1} {
401         reporterr "remoteleds on $d: $dd(values): $l"
402     }
403     if {[fblocked $dd(ochan)]} return
404     timed_log ">\$$d failure";
405     catch { close $dd(ichan) }
406     catch { close $dd(ochan) }
407     unset dd(ichan)
408     unset dd(ochan)
409     reporterr "remoteleds on $d died"
410     dset_trylater $d
411 }
412
413 proc_dset trylater {} {
414     global retry_after
415     ldebug d$d "will try again later"
416     set dd(retry) [after $retry_after [list dset_start $d]]
417 }
418
419 proc config_change {} {
420     global onchans chans_retry errchan config_retry_after
421     ldebug {} "rechecking configuration etc"
422     foreach ch [array names onchans] {
423         manyset $onchans($ch) status after
424         if {"$status" == "shortly"} {
425             catch { after cancel $after }
426         }
427         set onchans($ch) mustleave
428     }
429     sendout JOIN $errchan
430     chan_shortly $errchan
431     foreach m [list_objs monitor] {
432         upvar #0 monitor/$m mm
433         foreach ch $mm(chans) {
434             sendout JOIN $ch
435             chan_shortly $ch
436         }
437     }
438     foreach ch [array names onchans] {
439         if {"[lindex $onchans($ch) 0]" != "mustleave"} continue
440         sendout PART $ch
441         unset onchans($ch)
442     }
443     catch { after cancel $config_retry_after }
444     set config_retry_after [after $chans_retry config_change]
445 }
446
447 proc allchans_shortly {} {
448     global chan_after onchans shortly_alling
449     if {[info exists shortly_alling]} {
450         ldebug {} "global check already pending"
451         return
452     }
453     foreach ch [array names onchans] {
454         manyset $onchans($ch) status after
455         if {"$status" != "shortly"} continue
456         set idle
457     }
458     set shortly_alling [after $chan_after allchans_sendnames]
459 }
460
461 proc allchans_sendnames {} {
462     global shortly_alling
463     unset shortly_alling
464     ldebug {} "asking for global namelist"
465     sendout NAMES
466 }
467
468 proc chan_shortly {ch} {
469     global chan_after shortly_alling
470     set ch [irctolower $ch]
471     if {[info exists shortly_alling]} {
472         ldebug c$ch "global check already pending"
473         return
474     }
475     upvar #0 onchans($ch) oc
476     if {[info exists oc]} {
477         manyset $oc status after
478         if {"$status" == "shortly"} {
479             ldebug c$ch "queued check already pending"
480             return
481         }
482     }
483     ldebug c$ch "queueing check"
484     set oc [list shortly [after $chan_after chan_sendnames $ch]]
485 }
486
487 proc msg_353 {p c dest type chan nicklist} {
488     set lchan [irctolower $chan]
489     set nll [irctolower $nicklist]
490     regsub -all {[=@*]} $nll {} nll
491     ldebug c$lchan "got names $nll"
492     foreach m [list_objs monitor] {
493         mon_gotchanlist $m $lchan $nll
494     }
495 }
496
497 proc chan_sendnames {ch} {
498     upvar #0 onchans($ch) oc
499     ldebug c$ch "asking for namelist"
500     sendout NAMES $ch
501     set oc idle
502 }
503
504 def_ucmd reload {
505     set username [ta_word]
506     ta_nomore
507     set m [reloaduser $username]
508     config_change
509     ucmdr {} $m
510 }
511
512 proc debug_reset {} {
513     global debugusers debug_cancelling
514     unset debug_cancelling
515     set debugusers {}
516     reporterr "debug mode timed out"
517 }
518
519 def_ucmd debug {
520     prefix_nick
521     global debugusers debug_cancelling debug_reset_after
522     if {![string length $text]} { error "must give list of usernames" }
523     llength $text
524     set debugusers $text
525     catch { after cancel $debug_cancelling }
526     set debug_cancelling [after $debug_reset_after debug_reset]
527     reporterr "debug enabled by $n: $debugusers"
528 }
529
530 def_ucmd nodebug {
531     prefix_nick
532     global debugusers debug_cancelling
533     set debugusers {}
534     catch { after cancel $debug_cancelling }
535     catch { unset debug_cancelling }
536     reporterr "debug disabled by $n"
537 }
538
539 def_ucmd who {
540     set r {}
541     foreach m [list_objs monitor] {
542         upvar #0 monitor/$m mm
543         lappend r "monitoring $mm(chans) for $m"
544     }
545     foreach d [list_objs deviceset] {
546         upvar #0 deviceset/$d dd
547         regexp {^[^:]*\:[^:]*} $dd(group) dest
548         lappend r "sending $dd(monname) to $dest"
549     }
550     ucmdr [join $r "\n"] {}
551 }
552
553 proc connected {} {
554     ldebug {} "connected"
555     foreach f [glob -nocomplain pwdb/p*] {
556         regexp {^pwdb/p(.*)$} $f dummy username
557         set m [reloaduser $username]
558     }
559     config_change
560 }
561
562 proc msg_JOIN {p c chan} { chan_shortly $chan }
563 proc msg_PART {p c chan} { chan_shortly $chan }
564 proc msg_KILL {p c user why} { allchans_shortly }
565 proc msg_QUIT {p c why} { allchans_shortly }
566 proc msg_KICK {p c chans users comment} {
567     if {[llength $chans] > 1} {
568         allchans_shortly
569     } else {
570         chan_shortly [lindex $chans 0]
571     }
572 }
573
574 if {[catch {
575     loadhelp
576     ensure_connecting
577 } emsg]} {
578     fail "startup: $emsg"
579 }