ScuttleBot

fix: periodic NAMES refresh for consistent user list girc's internal user tracking missed bots that join after the bridge. Instead of relying on JOIN event tracking alone, the bridge now sends NAMES to all joined channels every 30s, forcing girc to sync with Ergo's authoritative user list. First refresh fires 30s after connect to let all bots join first.

lmata 2026-04-05 23:46 trunk
Commit 64a41509344d2f4157b529ecdeb6fcbf5addeb953bf2bad558ca092d4b17b6c0
--- internal/api/chat.go
+++ internal/api/chat.go
@@ -111,13 +111,10 @@
111111
w.WriteHeader(http.StatusNoContent)
112112
}
113113
114114
func (s *Server) handleChannelUsers(w http.ResponseWriter, r *http.Request) {
115115
channel := "#" + r.PathValue("channel")
116
- // Refresh girc's user list from the server before returning.
117
- s.bridge.RefreshNames(channel)
118
- time.Sleep(200 * time.Millisecond) // give girc time to process NAMES reply
119116
users := s.bridge.UsersWithModes(channel)
120117
if users == nil {
121118
users = []bridge.UserInfo{}
122119
}
123120
modes := s.bridge.ChannelModes(channel)
124121
--- internal/api/chat.go
+++ internal/api/chat.go
@@ -111,13 +111,10 @@
111 w.WriteHeader(http.StatusNoContent)
112 }
113
114 func (s *Server) handleChannelUsers(w http.ResponseWriter, r *http.Request) {
115 channel := "#" + r.PathValue("channel")
116 // Refresh girc's user list from the server before returning.
117 s.bridge.RefreshNames(channel)
118 time.Sleep(200 * time.Millisecond) // give girc time to process NAMES reply
119 users := s.bridge.UsersWithModes(channel)
120 if users == nil {
121 users = []bridge.UserInfo{}
122 }
123 modes := s.bridge.ChannelModes(channel)
124
--- internal/api/chat.go
+++ internal/api/chat.go
@@ -111,13 +111,10 @@
111 w.WriteHeader(http.StatusNoContent)
112 }
113
114 func (s *Server) handleChannelUsers(w http.ResponseWriter, r *http.Request) {
115 channel := "#" + r.PathValue("channel")
 
 
 
116 users := s.bridge.UsersWithModes(channel)
117 if users == nil {
118 users = []bridge.UserInfo{}
119 }
120 modes := s.bridge.ChannelModes(channel)
121
--- internal/bots/bridge/bridge.go
+++ internal/bots/bridge/bridge.go
@@ -286,10 +286,11 @@
286286
errCh <- err
287287
}
288288
}()
289289
290290
go b.joinLoop(ctx, c)
291
+ go b.namesRefreshLoop(ctx)
291292
292293
select {
293294
case <-ctx.Done():
294295
c.Close()
295296
return nil
@@ -457,10 +458,39 @@
457458
func (b *Bot) RefreshNames(channel string) {
458459
if b.client != nil {
459460
b.client.Cmd.SendRawf("NAMES %s", channel)
460461
}
461462
}
463
+
464
+// namesRefreshLoop periodically sends NAMES for all joined channels so
465
+// girc's user tracking stays in sync with the server.
466
+func (b *Bot) namesRefreshLoop(ctx context.Context) {
467
+ // Wait for initial connection and bot joins to settle.
468
+ select {
469
+ case <-ctx.Done():
470
+ return
471
+ case <-time.After(30 * time.Second):
472
+ }
473
+ ticker := time.NewTicker(30 * time.Second)
474
+ defer ticker.Stop()
475
+ for {
476
+ select {
477
+ case <-ctx.Done():
478
+ return
479
+ case <-ticker.C:
480
+ b.mu.RLock()
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
+}
462492
463493
// Users returns the current nick list for a channel — IRC connections plus
464494
// web UI users who have posted recently within the configured TTL.
465495
func (b *Bot) Users(channel string) []string {
466496
seen := make(map[string]bool)
467497
--- internal/bots/bridge/bridge.go
+++ internal/bots/bridge/bridge.go
@@ -286,10 +286,11 @@
286 errCh <- err
287 }
288 }()
289
290 go b.joinLoop(ctx, c)
 
291
292 select {
293 case <-ctx.Done():
294 c.Close()
295 return nil
@@ -457,10 +458,39 @@
457 func (b *Bot) RefreshNames(channel string) {
458 if b.client != nil {
459 b.client.Cmd.SendRawf("NAMES %s", channel)
460 }
461 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
463 // Users returns the current nick list for a channel — IRC connections plus
464 // web UI users who have posted recently within the configured TTL.
465 func (b *Bot) Users(channel string) []string {
466 seen := make(map[string]bool)
467
--- internal/bots/bridge/bridge.go
+++ internal/bots/bridge/bridge.go
@@ -286,10 +286,11 @@
286 errCh <- err
287 }
288 }()
289
290 go b.joinLoop(ctx, c)
291 go b.namesRefreshLoop(ctx)
292
293 select {
294 case <-ctx.Done():
295 c.Close()
296 return nil
@@ -457,10 +458,39 @@
458 func (b *Bot) RefreshNames(channel string) {
459 if b.client != nil {
460 b.client.Cmd.SendRawf("NAMES %s", channel)
461 }
462 }
463
464 // namesRefreshLoop periodically sends NAMES for all joined channels so
465 // girc's user tracking stays in sync with the server.
466 func (b *Bot) namesRefreshLoop(ctx context.Context) {
467 // Wait for initial connection and bot joins to settle.
468 select {
469 case <-ctx.Done():
470 return
471 case <-time.After(30 * time.Second):
472 }
473 ticker := time.NewTicker(30 * time.Second)
474 defer ticker.Stop()
475 for {
476 select {
477 case <-ctx.Done():
478 return
479 case <-ticker.C:
480 b.mu.RLock()
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

Keyboard Shortcuts

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