ScuttleBot

fix: reliable user list, shepherd voice, timestamp alignment User list: bypass girc's unreliable state tracker entirely. Parse RPL_NAMREPLY (353) directly into our own namesUsers map with mode prefixes (@, +). Periodic NAMES refresh clears and repopulates. All channel users now show reliably with correct modes. Shepherd voice: requests CS VOICE on all channels 3s after connect. Timestamps: use visibility:hidden instead of display:none so column alignment is preserved in columnar mode. Toggle button uses opacity 0.3/1.0 for clearer on/off state.

lmata 2026-04-06 00:39 trunk
Commit 75496910c87822470d36aaf1a83f82aa8d75f00b1609f3004f194ac1d7110c95
--- internal/api/ui/index.html
+++ internal/api/ui/index.html
@@ -142,11 +142,12 @@
142142
.chat-ch-name { font-weight:600; color:#58a6ff; }
143143
.stream-badge { font-size:11px; color:#8b949e; margin-left:auto; }
144144
.chat-msgs { flex:1; overflow-y:auto; padding:4px 8px; display:flex; flex-direction:column; gap:0; }
145145
.msg-row { font-size:13px; line-height:1.4; padding:1px 0; }
146146
.msg-time { color:#8b949e; font-size:11px; margin-right:6px; }
147
-.chat-msgs.hide-timestamps .msg-time { display:none; }
147
+.chat-msgs.hide-timestamps .msg-time { visibility:hidden; width:0; min-width:0; margin:0; overflow:hidden; }
148
+.chat-msgs.columnar.hide-timestamps .msg-time { visibility:hidden; min-width:36px; margin:0; }
148149
.msg-nick { font-weight:600; margin-right:6px; }
149150
.msg-grouped .msg-nick { display:none; }
150151
.msg-grouped .msg-time { display:none; }
151152
/* columnar layout mode */
152153
.chat-msgs.columnar .msg-row { display:flex; gap:6px; }
@@ -2311,18 +2312,18 @@
23112312
function toggleTimestamps() {
23122313
const el = document.getElementById('chat-msgs');
23132314
const hidden = el.classList.toggle('hide-timestamps');
23142315
localStorage.setItem('sb_hide_timestamps', hidden ? '1' : '0');
23152316
const btn = document.getElementById('chat-ts-toggle');
2316
- btn.style.color = hidden ? '#8b949e' : '';
2317
+ btn.style.opacity = hidden ? '0.3' : '1';
23172318
btn.title = hidden ? 'timestamps hidden — click to show' : 'timestamps shown — click to hide';
23182319
}
23192320
(function() {
23202321
const hidden = localStorage.getItem('sb_hide_timestamps') === '1';
23212322
if (hidden) document.getElementById('chat-msgs').classList.add('hide-timestamps');
23222323
const btn = document.getElementById('chat-ts-toggle');
2323
- if (hidden) { btn.style.color = '#8b949e'; btn.title = 'timestamps hidden — click to show'; }
2324
+ if (hidden) { btn.style.opacity = '0.3'; btn.title = 'timestamps hidden — click to show'; }
23242325
else { btn.title = 'timestamps shown — click to hide'; }
23252326
})();
23262327
23272328
// --- rich mode toggle ---
23282329
function isRichMode() { return localStorage.getItem('sb_rich_mode') === '1'; }
23292330
--- internal/api/ui/index.html
+++ internal/api/ui/index.html
@@ -142,11 +142,12 @@
142 .chat-ch-name { font-weight:600; color:#58a6ff; }
143 .stream-badge { font-size:11px; color:#8b949e; margin-left:auto; }
144 .chat-msgs { flex:1; overflow-y:auto; padding:4px 8px; display:flex; flex-direction:column; gap:0; }
145 .msg-row { font-size:13px; line-height:1.4; padding:1px 0; }
146 .msg-time { color:#8b949e; font-size:11px; margin-right:6px; }
147 .chat-msgs.hide-timestamps .msg-time { display:none; }
 
148 .msg-nick { font-weight:600; margin-right:6px; }
149 .msg-grouped .msg-nick { display:none; }
150 .msg-grouped .msg-time { display:none; }
151 /* columnar layout mode */
152 .chat-msgs.columnar .msg-row { display:flex; gap:6px; }
@@ -2311,18 +2312,18 @@
2311 function toggleTimestamps() {
2312 const el = document.getElementById('chat-msgs');
2313 const hidden = el.classList.toggle('hide-timestamps');
2314 localStorage.setItem('sb_hide_timestamps', hidden ? '1' : '0');
2315 const btn = document.getElementById('chat-ts-toggle');
2316 btn.style.color = hidden ? '#8b949e' : '';
2317 btn.title = hidden ? 'timestamps hidden — click to show' : 'timestamps shown — click to hide';
2318 }
2319 (function() {
2320 const hidden = localStorage.getItem('sb_hide_timestamps') === '1';
2321 if (hidden) document.getElementById('chat-msgs').classList.add('hide-timestamps');
2322 const btn = document.getElementById('chat-ts-toggle');
2323 if (hidden) { btn.style.color = '#8b949e'; btn.title = 'timestamps hidden — click to show'; }
2324 else { btn.title = 'timestamps shown — click to hide'; }
2325 })();
2326
2327 // --- rich mode toggle ---
2328 function isRichMode() { return localStorage.getItem('sb_rich_mode') === '1'; }
2329
--- internal/api/ui/index.html
+++ internal/api/ui/index.html
@@ -142,11 +142,12 @@
142 .chat-ch-name { font-weight:600; color:#58a6ff; }
143 .stream-badge { font-size:11px; color:#8b949e; margin-left:auto; }
144 .chat-msgs { flex:1; overflow-y:auto; padding:4px 8px; display:flex; flex-direction:column; gap:0; }
145 .msg-row { font-size:13px; line-height:1.4; padding:1px 0; }
146 .msg-time { color:#8b949e; font-size:11px; margin-right:6px; }
147 .chat-msgs.hide-timestamps .msg-time { visibility:hidden; width:0; min-width:0; margin:0; overflow:hidden; }
148 .chat-msgs.columnar.hide-timestamps .msg-time { visibility:hidden; min-width:36px; margin:0; }
149 .msg-nick { font-weight:600; margin-right:6px; }
150 .msg-grouped .msg-nick { display:none; }
151 .msg-grouped .msg-time { display:none; }
152 /* columnar layout mode */
153 .chat-msgs.columnar .msg-row { display:flex; gap:6px; }
@@ -2311,18 +2312,18 @@
2312 function toggleTimestamps() {
2313 const el = document.getElementById('chat-msgs');
2314 const hidden = el.classList.toggle('hide-timestamps');
2315 localStorage.setItem('sb_hide_timestamps', hidden ? '1' : '0');
2316 const btn = document.getElementById('chat-ts-toggle');
2317 btn.style.opacity = hidden ? '0.3' : '1';
2318 btn.title = hidden ? 'timestamps hidden — click to show' : 'timestamps shown — click to hide';
2319 }
2320 (function() {
2321 const hidden = localStorage.getItem('sb_hide_timestamps') === '1';
2322 if (hidden) document.getElementById('chat-msgs').classList.add('hide-timestamps');
2323 const btn = document.getElementById('chat-ts-toggle');
2324 if (hidden) { btn.style.opacity = '0.3'; btn.title = 'timestamps hidden — click to show'; }
2325 else { btn.title = 'timestamps shown — click to hide'; }
2326 })();
2327
2328 // --- rich mode toggle ---
2329 function isRichMode() { return localStorage.getItem('sb_rich_mode') === '1'; }
2330
--- internal/bots/bridge/bridge.go
+++ internal/bots/bridge/bridge.go
@@ -105,10 +105,14 @@
105105
msgTotal atomic.Int64
106106
107107
joinCh chan string
108108
client *girc.Client
109109
onUserJoin func(channel, nick string) // optional callback when a non-bridge user joins
110
+
111
+ // namesUsers is our own authoritative user list populated from RPL_NAMREPLY.
112
+ // channel → nick → mode prefix ("@", "+", or "")
113
+ namesUsers map[string]map[string]string
110114
111115
// RELAYMSG support detected from ISUPPORT.
112116
relaySep string // separator (e.g. "/"), empty if unsupported
113117
}
114118
@@ -140,10 +144,11 @@
140144
log: log,
141145
buffers: make(map[string]*ringBuf),
142146
subs: make(map[string]map[uint64]chan Message),
143147
joined: make(map[string]bool),
144148
joinCh: make(chan string, 32),
149
+ namesUsers: make(map[string]map[string]string),
145150
}
146151
}
147152
148153
// SetWebUserTTL updates how long bridge-posted HTTP nicks remain visible in
149154
// the channel user list after their last post.
@@ -243,10 +248,39 @@
243248
} else if b.onUserJoin != nil {
244249
// Another user joined — fire callback for on-join instructions.
245250
go b.onUserJoin(channel, nick)
246251
}
247252
})
253
+
254
+ // Parse RPL_NAMREPLY ourselves for a reliable user list.
255
+ c.Handlers.AddBg(girc.RPL_NAMREPLY, func(_ *girc.Client, e girc.Event) {
256
+ // Format: :server 353 bridge = #channel :@op +voice regular
257
+ if len(e.Params) < 4 {
258
+ return
259
+ }
260
+ channel := e.Params[2]
261
+ names := strings.Fields(e.Last())
262
+ b.mu.Lock()
263
+ if b.namesUsers[channel] == nil {
264
+ b.namesUsers[channel] = make(map[string]string)
265
+ }
266
+ for _, name := range names {
267
+ prefix := ""
268
+ nick := name
269
+ if strings.HasPrefix(name, "@") {
270
+ prefix = "@"
271
+ nick = name[1:]
272
+ } else if strings.HasPrefix(name, "+") {
273
+ prefix = "+"
274
+ nick = name[1:]
275
+ }
276
+ if nick != b.nick {
277
+ b.namesUsers[channel][nick] = prefix
278
+ }
279
+ }
280
+ b.mu.Unlock()
281
+ })
248282
249283
c.Handlers.AddBg(girc.PRIVMSG, func(_ *girc.Client, e girc.Event) {
250284
if len(e.Params) < 1 || e.Source == nil {
251285
return
252286
}
@@ -481,37 +515,38 @@
481515
channels := make([]string, 0, len(b.joined))
482516
for ch := range b.joined {
483517
channels = append(channels, ch)
484518
}
485519
b.mu.RUnlock()
520
+ // Clear stale data before refresh.
521
+ b.mu.Lock()
522
+ for _, ch := range channels {
523
+ b.namesUsers[ch] = make(map[string]string)
524
+ }
525
+ b.mu.Unlock()
486526
for _, ch := range channels {
487527
b.RefreshNames(ch)
488528
}
489529
}
490530
}
491531
}
492532
493
-// Users returns the current nick list for a channel — IRC connections plus
494
-// web UI users who have posted recently within the configured TTL.
533
+// Users returns the current nick list for a channel — from our NAMES cache
534
+// plus web UI users who have posted recently within the configured TTL.
495535
func (b *Bot) Users(channel string) []string {
496536
seen := make(map[string]bool)
497537
var nicks []string
498538
499
- // IRC-connected nicks from girc's state — exclude the bridge bot itself.
500
- if b.client != nil {
501
- if ch := b.client.LookupChannel(channel); ch != nil {
502
- for _, u := range ch.Users(b.client) {
503
- if u.Nick == b.nick {
504
- continue // skip the bridge bot
505
- }
506
- if !seen[u.Nick] {
507
- seen[u.Nick] = true
508
- nicks = append(nicks, u.Nick)
509
- }
510
- }
539
+ // IRC-connected nicks from our NAMES cache.
540
+ b.mu.RLock()
541
+ for nick := range b.namesUsers[channel] {
542
+ if !seen[nick] {
543
+ seen[nick] = true
544
+ nicks = append(nicks, nick)
511545
}
512546
}
547
+ b.mu.RUnlock()
513548
514549
// Web UI senders active within the configured TTL. Also prune expired nicks
515550
// so the bridge doesn't retain dead web-user entries forever.
516551
now := time.Now()
517552
b.mu.Lock()
@@ -540,44 +575,26 @@
540575
// UsersWithModes returns the current user list with mode info for a channel.
541576
func (b *Bot) UsersWithModes(channel string) []UserInfo {
542577
seen := make(map[string]bool)
543578
var users []UserInfo
544579
545
- if b.client != nil {
546
- if ch := b.client.LookupChannel(channel); ch != nil {
547
- for _, u := range ch.Users(b.client) {
548
- if u.Nick == b.nick {
549
- continue
550
- }
551
- if seen[u.Nick] {
552
- continue
553
- }
554
- seen[u.Nick] = true
555
- var modes []string
556
- if u.Perms != nil {
557
- if perms, ok := u.Perms.Lookup(channel); ok {
558
- if perms.Owner {
559
- modes = append(modes, "q")
560
- }
561
- if perms.Admin {
562
- modes = append(modes, "a")
563
- }
564
- if perms.Op {
565
- modes = append(modes, "o")
566
- }
567
- if perms.HalfOp {
568
- modes = append(modes, "h")
569
- }
570
- if perms.Voice {
571
- modes = append(modes, "v")
572
- }
573
- }
574
- }
575
- users = append(users, UserInfo{Nick: u.Nick, Modes: modes})
576
- }
577
- }
578
- }
580
+ // Use our NAMES cache for reliable user+mode data.
581
+ b.mu.RLock()
582
+ for nick, prefix := range b.namesUsers[channel] {
583
+ if seen[nick] {
584
+ continue
585
+ }
586
+ seen[nick] = true
587
+ var modes []string
588
+ if prefix == "@" {
589
+ modes = append(modes, "o")
590
+ } else if prefix == "+" {
591
+ modes = append(modes, "v")
592
+ }
593
+ users = append(users, UserInfo{Nick: nick, Modes: modes})
594
+ }
595
+ b.mu.RUnlock()
579596
580597
now := time.Now()
581598
b.mu.Lock()
582599
cutoff := now.Add(-b.webUserTTL)
583600
for nick, last := range b.webUsers[channel] {
584601
--- internal/bots/bridge/bridge.go
+++ internal/bots/bridge/bridge.go
@@ -105,10 +105,14 @@
105 msgTotal atomic.Int64
106
107 joinCh chan string
108 client *girc.Client
109 onUserJoin func(channel, nick string) // optional callback when a non-bridge user joins
 
 
 
 
110
111 // RELAYMSG support detected from ISUPPORT.
112 relaySep string // separator (e.g. "/"), empty if unsupported
113 }
114
@@ -140,10 +144,11 @@
140 log: log,
141 buffers: make(map[string]*ringBuf),
142 subs: make(map[string]map[uint64]chan Message),
143 joined: make(map[string]bool),
144 joinCh: make(chan string, 32),
 
145 }
146 }
147
148 // SetWebUserTTL updates how long bridge-posted HTTP nicks remain visible in
149 // the channel user list after their last post.
@@ -243,10 +248,39 @@
243 } else if b.onUserJoin != nil {
244 // Another user joined — fire callback for on-join instructions.
245 go b.onUserJoin(channel, nick)
246 }
247 })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
249 c.Handlers.AddBg(girc.PRIVMSG, func(_ *girc.Client, e girc.Event) {
250 if len(e.Params) < 1 || e.Source == nil {
251 return
252 }
@@ -481,37 +515,38 @@
481 channels := make([]string, 0, len(b.joined))
482 for ch := range b.joined {
483 channels = append(channels, ch)
484 }
485 b.mu.RUnlock()
 
 
 
 
 
 
486 for _, ch := range channels {
487 b.RefreshNames(ch)
488 }
489 }
490 }
491 }
492
493 // Users returns the current nick list for a channel — IRC connections plus
494 // web UI users who have posted recently within the configured TTL.
495 func (b *Bot) Users(channel string) []string {
496 seen := make(map[string]bool)
497 var nicks []string
498
499 // IRC-connected nicks from girc's state — exclude the bridge bot itself.
500 if b.client != nil {
501 if ch := b.client.LookupChannel(channel); ch != nil {
502 for _, u := range ch.Users(b.client) {
503 if u.Nick == b.nick {
504 continue // skip the bridge bot
505 }
506 if !seen[u.Nick] {
507 seen[u.Nick] = true
508 nicks = append(nicks, u.Nick)
509 }
510 }
511 }
512 }
 
513
514 // Web UI senders active within the configured TTL. Also prune expired nicks
515 // so the bridge doesn't retain dead web-user entries forever.
516 now := time.Now()
517 b.mu.Lock()
@@ -540,44 +575,26 @@
540 // UsersWithModes returns the current user list with mode info for a channel.
541 func (b *Bot) UsersWithModes(channel string) []UserInfo {
542 seen := make(map[string]bool)
543 var users []UserInfo
544
545 if b.client != nil {
546 if ch := b.client.LookupChannel(channel); ch != nil {
547 for _, u := range ch.Users(b.client) {
548 if u.Nick == b.nick {
549 continue
550 }
551 if seen[u.Nick] {
552 continue
553 }
554 seen[u.Nick] = true
555 var modes []string
556 if u.Perms != nil {
557 if perms, ok := u.Perms.Lookup(channel); ok {
558 if perms.Owner {
559 modes = append(modes, "q")
560 }
561 if perms.Admin {
562 modes = append(modes, "a")
563 }
564 if perms.Op {
565 modes = append(modes, "o")
566 }
567 if perms.HalfOp {
568 modes = append(modes, "h")
569 }
570 if perms.Voice {
571 modes = append(modes, "v")
572 }
573 }
574 }
575 users = append(users, UserInfo{Nick: u.Nick, Modes: modes})
576 }
577 }
578 }
579
580 now := time.Now()
581 b.mu.Lock()
582 cutoff := now.Add(-b.webUserTTL)
583 for nick, last := range b.webUsers[channel] {
584
--- internal/bots/bridge/bridge.go
+++ internal/bots/bridge/bridge.go
@@ -105,10 +105,14 @@
105 msgTotal atomic.Int64
106
107 joinCh chan string
108 client *girc.Client
109 onUserJoin func(channel, nick string) // optional callback when a non-bridge user joins
110
111 // namesUsers is our own authoritative user list populated from RPL_NAMREPLY.
112 // channel → nick → mode prefix ("@", "+", or "")
113 namesUsers map[string]map[string]string
114
115 // RELAYMSG support detected from ISUPPORT.
116 relaySep string // separator (e.g. "/"), empty if unsupported
117 }
118
@@ -140,10 +144,11 @@
144 log: log,
145 buffers: make(map[string]*ringBuf),
146 subs: make(map[string]map[uint64]chan Message),
147 joined: make(map[string]bool),
148 joinCh: make(chan string, 32),
149 namesUsers: make(map[string]map[string]string),
150 }
151 }
152
153 // SetWebUserTTL updates how long bridge-posted HTTP nicks remain visible in
154 // the channel user list after their last post.
@@ -243,10 +248,39 @@
248 } else if b.onUserJoin != nil {
249 // Another user joined — fire callback for on-join instructions.
250 go b.onUserJoin(channel, nick)
251 }
252 })
253
254 // Parse RPL_NAMREPLY ourselves for a reliable user list.
255 c.Handlers.AddBg(girc.RPL_NAMREPLY, func(_ *girc.Client, e girc.Event) {
256 // Format: :server 353 bridge = #channel :@op +voice regular
257 if len(e.Params) < 4 {
258 return
259 }
260 channel := e.Params[2]
261 names := strings.Fields(e.Last())
262 b.mu.Lock()
263 if b.namesUsers[channel] == nil {
264 b.namesUsers[channel] = make(map[string]string)
265 }
266 for _, name := range names {
267 prefix := ""
268 nick := name
269 if strings.HasPrefix(name, "@") {
270 prefix = "@"
271 nick = name[1:]
272 } else if strings.HasPrefix(name, "+") {
273 prefix = "+"
274 nick = name[1:]
275 }
276 if nick != b.nick {
277 b.namesUsers[channel][nick] = prefix
278 }
279 }
280 b.mu.Unlock()
281 })
282
283 c.Handlers.AddBg(girc.PRIVMSG, func(_ *girc.Client, e girc.Event) {
284 if len(e.Params) < 1 || e.Source == nil {
285 return
286 }
@@ -481,37 +515,38 @@
515 channels := make([]string, 0, len(b.joined))
516 for ch := range b.joined {
517 channels = append(channels, ch)
518 }
519 b.mu.RUnlock()
520 // Clear stale data before refresh.
521 b.mu.Lock()
522 for _, ch := range channels {
523 b.namesUsers[ch] = make(map[string]string)
524 }
525 b.mu.Unlock()
526 for _, ch := range channels {
527 b.RefreshNames(ch)
528 }
529 }
530 }
531 }
532
533 // Users returns the current nick list for a channel — from our NAMES cache
534 // plus web UI users who have posted recently within the configured TTL.
535 func (b *Bot) Users(channel string) []string {
536 seen := make(map[string]bool)
537 var nicks []string
538
539 // IRC-connected nicks from our NAMES cache.
540 b.mu.RLock()
541 for nick := range b.namesUsers[channel] {
542 if !seen[nick] {
543 seen[nick] = true
544 nicks = append(nicks, nick)
 
 
 
 
 
 
545 }
546 }
547 b.mu.RUnlock()
548
549 // Web UI senders active within the configured TTL. Also prune expired nicks
550 // so the bridge doesn't retain dead web-user entries forever.
551 now := time.Now()
552 b.mu.Lock()
@@ -540,44 +575,26 @@
575 // UsersWithModes returns the current user list with mode info for a channel.
576 func (b *Bot) UsersWithModes(channel string) []UserInfo {
577 seen := make(map[string]bool)
578 var users []UserInfo
579
580 // Use our NAMES cache for reliable user+mode data.
581 b.mu.RLock()
582 for nick, prefix := range b.namesUsers[channel] {
583 if seen[nick] {
584 continue
585 }
586 seen[nick] = true
587 var modes []string
588 if prefix == "@" {
589 modes = append(modes, "o")
590 } else if prefix == "+" {
591 modes = append(modes, "v")
592 }
593 users = append(users, UserInfo{Nick: nick, Modes: modes})
594 }
595 b.mu.RUnlock()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
596
597 now := time.Now()
598 b.mu.Lock()
599 cutoff := now.Add(-b.webUserTTL)
600 for nick, last := range b.webUsers[channel] {
601
--- internal/bots/shepherd/shepherd.go
+++ internal/bots/shepherd/shepherd.go
@@ -187,10 +187,20 @@
187187
c.Handlers.AddBg(girc.CONNECTED, func(cl *girc.Client, _ girc.Event) {
188188
cl.Cmd.Mode(cl.GetNick(), "+B")
189189
for _, ch := range b.cfg.Channels {
190190
cl.Cmd.Join(ch)
191191
}
192
+ // Request voice on all channels after a short delay (let JOINs complete).
193
+ go func() {
194
+ time.Sleep(3 * time.Second)
195
+ for _, ch := range b.cfg.Channels {
196
+ cl.Cmd.Message("ChanServ", "VOICE "+ch)
197
+ }
198
+ if b.cfg.ReportChannel != "" {
199
+ cl.Cmd.Message("ChanServ", "VOICE "+b.cfg.ReportChannel)
200
+ }
201
+ }()
192202
if b.cfg.ReportChannel != "" {
193203
cl.Cmd.Join(b.cfg.ReportChannel)
194204
}
195205
if b.log != nil {
196206
b.log.Info("shepherd connected", "channels", b.cfg.Channels)
197207
--- internal/bots/shepherd/shepherd.go
+++ internal/bots/shepherd/shepherd.go
@@ -187,10 +187,20 @@
187 c.Handlers.AddBg(girc.CONNECTED, func(cl *girc.Client, _ girc.Event) {
188 cl.Cmd.Mode(cl.GetNick(), "+B")
189 for _, ch := range b.cfg.Channels {
190 cl.Cmd.Join(ch)
191 }
 
 
 
 
 
 
 
 
 
 
192 if b.cfg.ReportChannel != "" {
193 cl.Cmd.Join(b.cfg.ReportChannel)
194 }
195 if b.log != nil {
196 b.log.Info("shepherd connected", "channels", b.cfg.Channels)
197
--- internal/bots/shepherd/shepherd.go
+++ internal/bots/shepherd/shepherd.go
@@ -187,10 +187,20 @@
187 c.Handlers.AddBg(girc.CONNECTED, func(cl *girc.Client, _ girc.Event) {
188 cl.Cmd.Mode(cl.GetNick(), "+B")
189 for _, ch := range b.cfg.Channels {
190 cl.Cmd.Join(ch)
191 }
192 // Request voice on all channels after a short delay (let JOINs complete).
193 go func() {
194 time.Sleep(3 * time.Second)
195 for _, ch := range b.cfg.Channels {
196 cl.Cmd.Message("ChanServ", "VOICE "+ch)
197 }
198 if b.cfg.ReportChannel != "" {
199 cl.Cmd.Message("ChanServ", "VOICE "+b.cfg.ReportChannel)
200 }
201 }()
202 if b.cfg.ReportChannel != "" {
203 cl.Cmd.Join(b.cfg.ReportChannel)
204 }
205 if b.log != nil {
206 b.log.Info("shepherd connected", "channels", b.cfg.Channels)
207

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button