ScuttleBot
fix: refresh NAMES before returning user list girc's internal user tracking was missing bots that joined after the bridge. Now sends a NAMES command to Ergo before reading the user list, forcing an authoritative refresh from the server. Also adds RefreshNames method to bridge bot.
Commit
cca45cb0bf21809ec77ba76de788b25e05b90a11836bea47659f9f45faa765ea
Parent
4f3dcfeb8c16cf1…
3 files changed
+4
+1
+9
-1
+4
| --- internal/api/chat.go | ||
| +++ internal/api/chat.go | ||
| @@ -23,10 +23,11 @@ | ||
| 23 | 23 | Stats() bridge.Stats |
| 24 | 24 | TouchUser(channel, nick string) |
| 25 | 25 | Users(channel string) []string |
| 26 | 26 | UsersWithModes(channel string) []bridge.UserInfo |
| 27 | 27 | ChannelModes(channel string) string |
| 28 | + RefreshNames(channel string) | |
| 28 | 29 | } |
| 29 | 30 | |
| 30 | 31 | func (s *Server) handleJoinChannel(w http.ResponseWriter, r *http.Request) { |
| 31 | 32 | channel := "#" + r.PathValue("channel") |
| 32 | 33 | s.bridge.JoinChannel(channel) |
| @@ -110,10 +111,13 @@ | ||
| 110 | 111 | w.WriteHeader(http.StatusNoContent) |
| 111 | 112 | } |
| 112 | 113 | |
| 113 | 114 | func (s *Server) handleChannelUsers(w http.ResponseWriter, r *http.Request) { |
| 114 | 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 | |
| 115 | 119 | users := s.bridge.UsersWithModes(channel) |
| 116 | 120 | if users == nil { |
| 117 | 121 | users = []bridge.UserInfo{} |
| 118 | 122 | } |
| 119 | 123 | modes := s.bridge.ChannelModes(channel) |
| 120 | 124 |
| --- internal/api/chat.go | |
| +++ internal/api/chat.go | |
| @@ -23,10 +23,11 @@ | |
| 23 | Stats() bridge.Stats |
| 24 | TouchUser(channel, nick string) |
| 25 | Users(channel string) []string |
| 26 | UsersWithModes(channel string) []bridge.UserInfo |
| 27 | ChannelModes(channel string) string |
| 28 | } |
| 29 | |
| 30 | func (s *Server) handleJoinChannel(w http.ResponseWriter, r *http.Request) { |
| 31 | channel := "#" + r.PathValue("channel") |
| 32 | s.bridge.JoinChannel(channel) |
| @@ -110,10 +111,13 @@ | |
| 110 | w.WriteHeader(http.StatusNoContent) |
| 111 | } |
| 112 | |
| 113 | func (s *Server) handleChannelUsers(w http.ResponseWriter, r *http.Request) { |
| 114 | channel := "#" + r.PathValue("channel") |
| 115 | users := s.bridge.UsersWithModes(channel) |
| 116 | if users == nil { |
| 117 | users = []bridge.UserInfo{} |
| 118 | } |
| 119 | modes := s.bridge.ChannelModes(channel) |
| 120 |
| --- internal/api/chat.go | |
| +++ internal/api/chat.go | |
| @@ -23,10 +23,11 @@ | |
| 23 | Stats() bridge.Stats |
| 24 | TouchUser(channel, nick string) |
| 25 | Users(channel string) []string |
| 26 | UsersWithModes(channel string) []bridge.UserInfo |
| 27 | ChannelModes(channel string) string |
| 28 | RefreshNames(channel string) |
| 29 | } |
| 30 | |
| 31 | func (s *Server) handleJoinChannel(w http.ResponseWriter, r *http.Request) { |
| 32 | channel := "#" + r.PathValue("channel") |
| 33 | s.bridge.JoinChannel(channel) |
| @@ -110,10 +111,13 @@ | |
| 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_test.go | ||
| +++ internal/api/chat_test.go | ||
| @@ -35,10 +35,11 @@ | ||
| 35 | 35 | } |
| 36 | 36 | func (b *stubChatBridge) Stats() bridge.Stats { return bridge.Stats{} } |
| 37 | 37 | func (b *stubChatBridge) Users(string) []string { return nil } |
| 38 | 38 | func (b *stubChatBridge) UsersWithModes(string) []bridge.UserInfo { return nil } |
| 39 | 39 | func (b *stubChatBridge) ChannelModes(string) string { return "" } |
| 40 | +func (b *stubChatBridge) RefreshNames(string) {} | |
| 40 | 41 | func (b *stubChatBridge) TouchUser(channel, nick string) { |
| 41 | 42 | b.touched = append(b.touched, struct{ channel, nick string }{channel: channel, nick: nick}) |
| 42 | 43 | } |
| 43 | 44 | |
| 44 | 45 | func TestHandleChannelPresence(t *testing.T) { |
| 45 | 46 |
| --- internal/api/chat_test.go | |
| +++ internal/api/chat_test.go | |
| @@ -35,10 +35,11 @@ | |
| 35 | } |
| 36 | func (b *stubChatBridge) Stats() bridge.Stats { return bridge.Stats{} } |
| 37 | func (b *stubChatBridge) Users(string) []string { return nil } |
| 38 | func (b *stubChatBridge) UsersWithModes(string) []bridge.UserInfo { return nil } |
| 39 | func (b *stubChatBridge) ChannelModes(string) string { return "" } |
| 40 | func (b *stubChatBridge) TouchUser(channel, nick string) { |
| 41 | b.touched = append(b.touched, struct{ channel, nick string }{channel: channel, nick: nick}) |
| 42 | } |
| 43 | |
| 44 | func TestHandleChannelPresence(t *testing.T) { |
| 45 |
| --- internal/api/chat_test.go | |
| +++ internal/api/chat_test.go | |
| @@ -35,10 +35,11 @@ | |
| 35 | } |
| 36 | func (b *stubChatBridge) Stats() bridge.Stats { return bridge.Stats{} } |
| 37 | func (b *stubChatBridge) Users(string) []string { return nil } |
| 38 | func (b *stubChatBridge) UsersWithModes(string) []bridge.UserInfo { return nil } |
| 39 | func (b *stubChatBridge) ChannelModes(string) string { return "" } |
| 40 | func (b *stubChatBridge) RefreshNames(string) {} |
| 41 | func (b *stubChatBridge) TouchUser(channel, nick string) { |
| 42 | b.touched = append(b.touched, struct{ channel, nick string }{channel: channel, nick: nick}) |
| 43 | } |
| 44 | |
| 45 | func TestHandleChannelPresence(t *testing.T) { |
| 46 |
+9
-1
| --- internal/bots/bridge/bridge.go | ||
| +++ internal/bots/bridge/bridge.go | ||
| @@ -449,18 +449,26 @@ | ||
| 449 | 449 | b.webUsers[channel] = make(map[string]time.Time) |
| 450 | 450 | } |
| 451 | 451 | b.webUsers[channel][nick] = time.Now() |
| 452 | 452 | b.mu.Unlock() |
| 453 | 453 | } |
| 454 | + | |
| 455 | +// RefreshNames sends a NAMES command for a channel, forcing girc to | |
| 456 | +// update its user list from the server's authoritative response. | |
| 457 | +func (b *Bot) RefreshNames(channel string) { | |
| 458 | + if b.client != nil { | |
| 459 | + b.client.Cmd.SendRawf("NAMES %s", channel) | |
| 460 | + } | |
| 461 | +} | |
| 454 | 462 | |
| 455 | 463 | // Users returns the current nick list for a channel — IRC connections plus |
| 456 | 464 | // web UI users who have posted recently within the configured TTL. |
| 457 | 465 | func (b *Bot) Users(channel string) []string { |
| 458 | 466 | seen := make(map[string]bool) |
| 459 | 467 | var nicks []string |
| 460 | 468 | |
| 461 | - // IRC-connected nicks from NAMES — exclude the bridge bot itself. | |
| 469 | + // IRC-connected nicks from girc's state — exclude the bridge bot itself. | |
| 462 | 470 | if b.client != nil { |
| 463 | 471 | if ch := b.client.LookupChannel(channel); ch != nil { |
| 464 | 472 | for _, u := range ch.Users(b.client) { |
| 465 | 473 | if u.Nick == b.nick { |
| 466 | 474 | continue // skip the bridge bot |
| 467 | 475 |
| --- internal/bots/bridge/bridge.go | |
| +++ internal/bots/bridge/bridge.go | |
| @@ -449,18 +449,26 @@ | |
| 449 | b.webUsers[channel] = make(map[string]time.Time) |
| 450 | } |
| 451 | b.webUsers[channel][nick] = time.Now() |
| 452 | b.mu.Unlock() |
| 453 | } |
| 454 | |
| 455 | // Users returns the current nick list for a channel — IRC connections plus |
| 456 | // web UI users who have posted recently within the configured TTL. |
| 457 | func (b *Bot) Users(channel string) []string { |
| 458 | seen := make(map[string]bool) |
| 459 | var nicks []string |
| 460 | |
| 461 | // IRC-connected nicks from NAMES — exclude the bridge bot itself. |
| 462 | if b.client != nil { |
| 463 | if ch := b.client.LookupChannel(channel); ch != nil { |
| 464 | for _, u := range ch.Users(b.client) { |
| 465 | if u.Nick == b.nick { |
| 466 | continue // skip the bridge bot |
| 467 |
| --- internal/bots/bridge/bridge.go | |
| +++ internal/bots/bridge/bridge.go | |
| @@ -449,18 +449,26 @@ | |
| 449 | b.webUsers[channel] = make(map[string]time.Time) |
| 450 | } |
| 451 | b.webUsers[channel][nick] = time.Now() |
| 452 | b.mu.Unlock() |
| 453 | } |
| 454 | |
| 455 | // RefreshNames sends a NAMES command for a channel, forcing girc to |
| 456 | // update its user list from the server's authoritative response. |
| 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 | var nicks []string |
| 468 | |
| 469 | // IRC-connected nicks from girc's state — exclude the bridge bot itself. |
| 470 | if b.client != nil { |
| 471 | if ch := b.client.LookupChannel(channel); ch != nil { |
| 472 | for _, u := range ch.Users(b.client) { |
| 473 | if u.Nick == b.nick { |
| 474 | continue // skip the bridge bot |
| 475 |