Commit | Line | Data |
---|---|---|
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 | ||
35 | set helpfile ledhelp | |
36 | ||
37 | source irccore.tcl | |
38 | source parsecmd.tcl | |
39 | source stdhelp.tcl | |
b52676c0 | 40 | source userv.tcl |
6b33d29a | 41 | |
cdbc7569 IJ |
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 | ||
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 |
76 | proc 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 |
cdbc7569 IJ |
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 | ||
6b33d29a IJ |
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 | ||
b52676c0 IJ |
100 | proc 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 | ||
116 | proc reporterr {m} { | |
117 | global errchan | |
118 | sendprivmsg $errchan $m | |
119 | } | |
120 | ||
b52676c0 IJ |
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 | ||
6b33d29a IJ |
127 | proc proc_mon {name argl body} { |
128 | proc mon_$name [concat m $argl] " | |
129 | upvar #0 monitor/\$m mm | |
130 | $body" | |
131 | } | |
132 | ||
b52676c0 IJ |
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 | ||
cdbc7569 IJ |
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 | ||
6b33d29a | 146 | proc_mon speech {chan ln} { |
b52676c0 | 147 | if {[lsearch -exact $mm(chans) $chan] == -1} return |
6b33d29a IJ |
148 | if {[mon_nick_is $mm(ignore) $ln]} return |
149 | set now [clock seconds] | |
150 | set mm(last-talk) $now | |
b52676c0 | 151 | if {[mon_nick_is $mm(prefer) $ln]} { set mm(last-talkpref) $now } |
6b33d29a IJ |
152 | mon_updateall $m |
153 | } | |
154 | ||
155 | proc_mon calcstate {} { | |
cdbc7569 IJ |
156 | set s " default " |
157 | foreach ch $mm(chans) { | |
158 | if {[llength $mm(present-$ch)]} { append s "present "; break } | |
159 | } | |
6b33d29a | 160 | set now [clock seconds] |
cdbc7569 IJ |
161 | set valid_until [expr {$now + 86400}] |
162 | set refresh_later 0 | |
163 | catch { after cancel $mm(talkchange) } | |
b52676c0 | 164 | foreach p {{} pref} { |
6b33d29a | 165 | foreach t {{} now} { |
cdbc7569 IJ |
166 | set vu [expr {$mm(last-talk$p) + $mm(time-recent$t)}] |
167 | if {$vu < $now} continue | |
6b33d29a | 168 | append s "${p}talk${t} " |
cdbc7569 IJ |
169 | set refresh_later 1 |
170 | if {$vu < $valid_until} { set valid_until $vu } | |
6b33d29a IJ |
171 | } |
172 | } | |
cdbc7569 IJ |
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 | } | |
6b33d29a IJ |
183 | return $s |
184 | } | |
185 | ||
186 | proc_mon updateall {} { | |
187 | set s [mon_calcstate $m] | |
b52676c0 | 188 | foreach d [list_objs deviceset] { |
6b33d29a IJ |
189 | upvar #0 deviceset/$d dd |
190 | if {[string compare $m $dd(monname)]} continue | |
b52676c0 | 191 | dset_setbystate $d $s |
6b33d29a IJ |
192 | } |
193 | } | |
194 | ||
195 | proc_mon destroy {} { | |
cdbc7569 IJ |
196 | ldebug m$m "destroying" |
197 | catch { after cancel $mm(talkchange) } | |
6b33d29a IJ |
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 | |
b52676c0 | 204 | if {\[catch { |
6b33d29a | 205 | $body |
cdbc7569 | 206 | } emsg\]==1} { |
6b33d29a IJ |
207 | reporterr \"error on \$d: \$emsg\" |
208 | }" | |
209 | } | |
210 | ||
b52676c0 IJ |
211 | proc timed_log {m} { |
212 | log "[clock seconds] $m" | |
213 | } | |
214 | ||
6b33d29a | 215 | proc_dset setbystate {s} { |
cdbc7569 | 216 | foreach {sq v} $dd(states) { |
6b33d29a IJ |
217 | if {![string match *$sq* $s]} continue |
218 | set lv $v; break | |
219 | } | |
cdbc7569 IJ |
220 | if {![info exists lv]} { |
221 | reporterr "no state for $d matching$s" | |
222 | return | |
223 | } | |
224 | ldebug d$d "matches $sq: $v" | |
b52676c0 | 225 | timed_log "->$d $lv" |
cdbc7569 IJ |
226 | set dd(values) "$sq=$lv" |
227 | puts $dd(ichan) $lv | |
6b33d29a IJ |
228 | } |
229 | ||
b52676c0 | 230 | proc_dset destroy {} { |
cdbc7569 | 231 | ldebug d$d "destroying" |
6b33d29a | 232 | catch { after cancel $dd(retry) } |
b52676c0 | 233 | catch { |
cdbc7569 IJ |
234 | if {[info exists dd(ochan)]} { timed_log ">\$$d destroy" } |
235 | close $dd(ochan) | |
236 | close $dd(ichan) | |
b52676c0 | 237 | } |
6b33d29a IJ |
238 | catch { unset dd } |
239 | } | |
240 | ||
cdbc7569 IJ |
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 | ||
6b33d29a IJ |
252 | proc reloaduser {username} { |
253 | check_username $username | |
cdbc7569 | 254 | ldebug u$username "reloading" |
6b33d29a IJ |
255 | if {[catch { |
256 | set cfg [exec userv --timeout 3 $username irc-ledcontrol-config \ | |
257 | < /dev/null] | |
6b33d29a | 258 | } emsg]} { |
b52676c0 | 259 | regsub "\n" $emsg " // " emsg |
6b33d29a | 260 | reporterr "error reloading $username: $emsg" |
b52676c0 | 261 | return "" |
6b33d29a | 262 | } |
b52676c0 | 263 | foreach d [list_objs deviceset] { |
6b33d29a IJ |
264 | if {![string match $username:* $d]} continue |
265 | dset_destroy $d | |
266 | } | |
b52676c0 | 267 | foreach m [list_objs monitor] { |
6b33d29a | 268 | if {![string match $username* $m]} continue |
cdbc7569 IJ |
269 | upvar #0 monitor/$m mm |
270 | foreach cv [array names mm] { set save/${m}($cv) $mm($cv) } | |
6b33d29a IJ |
271 | } |
272 | if {![string length $cfg]} { | |
cdbc7569 | 273 | file delete pwdb/$username |
b52676c0 IJ |
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 "" | |
6b33d29a | 281 | } elseif {[catch { |
cdbc7569 | 282 | ldebug u$username "parsing" |
6b33d29a IJ |
283 | foreach cv {ignore nopresence prefer} { set cc($cv) {} } |
284 | set cc(time-recentnow) 120 | |
285 | set cc(time-recent) 450 | |
b52676c0 | 286 | set lno 0 |
6b33d29a | 287 | foreach l [split $cfg "\n"] { |
b52676c0 | 288 | incr lno |
6b33d29a IJ |
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 | } | |
b52676c0 | 306 | check_monname $m |
6b33d29a IJ |
307 | foreach ch [split $cl " "] { |
308 | if {![string length $ch]} continue | |
b52676c0 | 309 | check_chan $ch |
6b33d29a IJ |
310 | if {![ischan $ch]} { error "invalid channel $ch" } |
311 | lappend cc(chans) [irctolower $ch] | |
b52676c0 | 312 | chan_shortly $ch |
6b33d29a IJ |
313 | } |
314 | upvar #0 monitor/$m mm | |
315 | foreach cv [array names cc] { set mm($cv) $cc($cv) } | |
cdbc7569 IJ |
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" | |
6b33d29a IJ |
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 {} | |
b52676c0 | 328 | check_monname $m |
6b33d29a IJ |
329 | foreach sv [split $states " "] { |
330 | if {![string length $sv]} continue | |
331 | if {![regexp \ | |
cdbc7569 IJ |
332 | {^((?:pref)?talk(?:now)?|present|default)\=([0-9a-z][,/+0-9A-Za-z]*)$} \ |
333 | $sv dummy lhs rhs]} { | |
6b33d29a IJ |
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 | |
cdbc7569 | 342 | set dd(values) startup |
b52676c0 | 343 | set dd(username) $username |
6b33d29a | 344 | dset_start $d |
cdbc7569 | 345 | ldebug d$d "created" |
6b33d29a IJ |
346 | } |
347 | } | |
348 | } emsg]} { | |
b52676c0 IJ |
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" | |
6b33d29a IJ |
362 | } |
363 | } | |
364 | ||
365 | proc_dset start {} { | |
366 | catch { unset dd(retry) } | |
b52676c0 | 367 | set username $dd(username) |
cdbc7569 | 368 | ldebug d$d "starting" |
6b33d29a | 369 | if {[catch { |
b52676c0 | 370 | set cmdl [list remoteleds --pipe $dd(group) \ |
cdbc7569 | 371 | --human --passfile-only pwdb/p$username] |
b52676c0 | 372 | timed_log "!-$d [join $cmdl " "]" |
cdbc7569 IJ |
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 | |
6b33d29a IJ |
383 | } emsg]} { |
384 | reporterr "remoteleds startup $d: $emsg" | |
cdbc7569 IJ |
385 | catch { close $ichan } |
386 | catch { close $ochan } | |
6b33d29a IJ |
387 | dset_trylater $d |
388 | } | |
389 | } | |
390 | ||
391 | proc_dset rledout {} { | |
b52676c0 | 392 | global errchan |
cdbc7569 IJ |
393 | while {[gets $dd(ochan) l] != -1} { |
394 | reporterr "remoteleds on $d: $dd(values): $l" | |
b52676c0 | 395 | } |
cdbc7569 IJ |
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) | |
6b33d29a IJ |
402 | reporterr "remoteleds on $d died" |
403 | dset_trylater $d | |
404 | } | |
405 | ||
406 | proc_dset trylater {} { | |
b52676c0 | 407 | global retry_after |
cdbc7569 | 408 | ldebug d$d "will try again later" |
b52676c0 | 409 | set dd(retry) [after $retry_after [list dset_start $d]] |
6b33d29a IJ |
410 | } |
411 | ||
412 | proc config_change {} { | |
cdbc7569 IJ |
413 | global onchans chans_retry errchan config_retry_after |
414 | ldebug {} "rechecking configuration etc" | |
6b33d29a IJ |
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 | } | |
6b33d29a | 422 | sendout JOIN $errchan |
b52676c0 | 423 | chan_shortly $errchan |
6b33d29a IJ |
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 | } | |
cdbc7569 IJ |
436 | catch { after cancel $config_retry_after } |
437 | set config_retry_after [after $chans_retry config_change] | |
6b33d29a IJ |
438 | } |
439 | ||
440 | proc chan_shortly {ch} { | |
441 | global chan_after | |
442 | upvar #0 onchans($ch) oc | |
b52676c0 IJ |
443 | if {[info exists oc]} { |
444 | manyset $oc status after | |
cdbc7569 IJ |
445 | if {"$status" == "shortly"} { |
446 | ldebug c$ch "queued check already pending" | |
447 | return | |
448 | } | |
b52676c0 | 449 | } |
cdbc7569 | 450 | ldebug c$ch "queueing check" |
6b33d29a IJ |
451 | set oc [list shortly [after $chan_after chan_sendnames $ch]] |
452 | } | |
453 | ||
cdbc7569 IJ |
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 | ||
6b33d29a IJ |
463 | proc chan_sendnames {ch} { |
464 | upvar #0 onchans($ch) oc | |
cdbc7569 | 465 | ldebug c$ch "asking for namelist" |
6b33d29a IJ |
466 | sendout NAMES $ch |
467 | set oc idle | |
468 | } | |
469 | ||
b52676c0 | 470 | def_ucmd reload { |
6b33d29a IJ |
471 | set username [ta_word] |
472 | ta_nomore | |
b52676c0 | 473 | set m [reloaduser $username] |
6b33d29a | 474 | config_change |
b52676c0 | 475 | ucmdr {} $m |
6b33d29a IJ |
476 | } |
477 | ||
cdbc7569 IJ |
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 | ||
b52676c0 IJ |
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 | } | |
cdbc7569 | 516 | ucmdr [join $r "\n"] {} |
b52676c0 IJ |
517 | } |
518 | ||
519 | proc connected {} { | |
cdbc7569 | 520 | ldebug {} "connected" |
6b33d29a IJ |
521 | foreach f [glob -nocomplain pwdb/p*] { |
522 | regexp {^pwdb/p(.*)$} $f dummy username | |
b52676c0 | 523 | set m [reloaduser $username] |
6b33d29a IJ |
524 | } |
525 | config_change | |
526 | } | |
527 | ||
6b33d29a IJ |
528 | # JOIN PART |
529 | # KICK KILL QUIT | |
b52676c0 IJ |
530 | |
531 | if {[catch { | |
532 | loadhelp | |
533 | ensure_connecting | |
534 | } emsg]} { | |
535 | fail "startup: $emsg" | |
536 | } |