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