Add connection resilience and reconnect commands for Kosmi and Jackbox
Kosmi WebSocket would silently die after hours/days with no reconnection. Jackbox WebSocket failed to reconnect after API server restarts (stale JWT) and leaked heartbeat goroutines on each reconnect cycle. Kosmi changes: - Add WebSocket ping/pong keepalive (30s ping, 90s read deadline) - Send EventFailure on unexpected disconnect to trigger gateway reconnectBridge() - Add intentionalDisconnect flag to prevent false failure events on clean shutdown - Fix Disconnect() to be safe for reconnect cycles Jackbox changes: - Add read deadline (90s) to detect stale connections - Fix heartbeat goroutine leak via per-connection listenDone channel - Re-authenticate for fresh JWT before each reconnect attempt - Add Manager.Reconnect() for on-demand teardown and rebuild IRC commands: - !kreconnect - reconnect Kosmi bridge - !jreconnect - reconnect Jackbox WebSocket - !reconnect - reconnect all services (Kosmi + Jackbox) Made-with: Cursor
This commit is contained in:
@@ -57,26 +57,12 @@ func (r *Router) handleEventReconnectKosmi(msg *config.Message) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
originChannel := msg.Channel
|
||||
originAccount := msg.Account
|
||||
r.sendConfirmation(msg, "Reconnecting Kosmi...")
|
||||
|
||||
for _, gw := range r.Gateways {
|
||||
for _, br := range gw.Bridges {
|
||||
if br.Protocol == "kosmi" {
|
||||
r.logger.Infof("Reconnecting Kosmi bridge %s (requested via !kreconnect)", br.Account)
|
||||
|
||||
// Send confirmation to the IRC channel that requested it
|
||||
if originAccount != "" && originChannel != "" {
|
||||
if ircBr, ok := gw.Bridges[originAccount]; ok {
|
||||
ircBr.Send(config.Message{
|
||||
Text: "Reconnecting Kosmi...",
|
||||
Channel: originChannel,
|
||||
Username: "system",
|
||||
Account: originAccount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
go gw.reconnectBridge(br)
|
||||
return true
|
||||
}
|
||||
@@ -87,6 +73,79 @@ func (r *Router) handleEventReconnectKosmi(msg *config.Message) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// handleEventReconnectJackbox handles a manual Jackbox reconnect request (e.g. from !jreconnect).
|
||||
// Returns true if the event was consumed.
|
||||
func (r *Router) handleEventReconnectJackbox(msg *config.Message) bool {
|
||||
if msg.Event != config.EventReconnectJackbox {
|
||||
return false
|
||||
}
|
||||
|
||||
r.sendConfirmation(msg, "Reconnecting Jackbox...")
|
||||
go func() {
|
||||
if r.JackboxManager == nil || !r.JackboxManager.IsEnabled() {
|
||||
r.logger.Warn("!jreconnect: Jackbox integration is not enabled")
|
||||
return
|
||||
}
|
||||
r.logger.Info("Reconnecting Jackbox WebSocket (requested via !jreconnect)")
|
||||
if err := r.JackboxManager.Reconnect(); err != nil {
|
||||
r.logger.Errorf("Jackbox reconnection failed: %v", err)
|
||||
}
|
||||
}()
|
||||
return true
|
||||
}
|
||||
|
||||
// handleEventReconnectAll handles a general reconnect request (e.g. from !reconnect).
|
||||
// It reconnects all non-IRC bridges (Kosmi, Jackbox).
|
||||
// Returns true if the event was consumed.
|
||||
func (r *Router) handleEventReconnectAll(msg *config.Message) bool {
|
||||
if msg.Event != config.EventReconnectAll {
|
||||
return false
|
||||
}
|
||||
|
||||
r.sendConfirmation(msg, "Reconnecting all services...")
|
||||
|
||||
// Reconnect Kosmi bridges
|
||||
for _, gw := range r.Gateways {
|
||||
for _, br := range gw.Bridges {
|
||||
if br.Protocol == "kosmi" {
|
||||
r.logger.Infof("Reconnecting Kosmi bridge %s (requested via !reconnect)", br.Account)
|
||||
go gw.reconnectBridge(br)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reconnect Jackbox
|
||||
if r.JackboxManager != nil && r.JackboxManager.IsEnabled() {
|
||||
go func() {
|
||||
r.logger.Info("Reconnecting Jackbox WebSocket (requested via !reconnect)")
|
||||
if err := r.JackboxManager.Reconnect(); err != nil {
|
||||
r.logger.Errorf("Jackbox reconnection failed: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// sendConfirmation sends a short confirmation message back to the IRC channel
|
||||
// that originated a command event.
|
||||
func (r *Router) sendConfirmation(msg *config.Message, text string) {
|
||||
if msg.Account == "" || msg.Channel == "" {
|
||||
return
|
||||
}
|
||||
for _, gw := range r.Gateways {
|
||||
if ircBr, ok := gw.Bridges[msg.Account]; ok {
|
||||
ircBr.Send(config.Message{
|
||||
Text: text,
|
||||
Channel: msg.Channel,
|
||||
Username: "system",
|
||||
Account: msg.Account,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleEventVotesQuery handles a !votes command by fetching vote data for the
|
||||
// currently playing game and broadcasting the result to all bridges.
|
||||
// Returns true if the event was consumed.
|
||||
|
||||
Reference in New Issue
Block a user