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