Add !votes command, fix vote tally timing, and improve Kosmi stability
- Add !votes command (IRC + Kosmi) showing per-session and all-time vote
breakdowns for the current game via new Jackbox API endpoints
(GET sessions/{id}/games, sessions/{id}/votes, games/{id})
- Fix vote tally broadcasting: remove debounce timer, announce tallies
only at game transitions or session end instead of after every vote
- Add !kreconnect IRC command to manually trigger Kosmi reconnection
- Add WebSocket ping/pong keepalive and write mutex to Kosmi client
for connection stability
- Add watchConnection() auto-reconnect on unexpected Kosmi disconnects
- Remove old 2025-10-31 chat summaries; add votes command design doc
Made-with: Cursor
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/42wim/matterbridge/bridge"
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/bridge/jackbox"
|
||||
"github.com/42wim/matterbridge/gateway/bridgemap"
|
||||
)
|
||||
|
||||
@@ -49,6 +50,131 @@ func (r *Router) handleEventGetChannelMembers(msg *config.Message) {
|
||||
}
|
||||
}
|
||||
|
||||
// handleEventReconnectKosmi handles a manual Kosmi reconnect request (e.g. from !kreconnect).
|
||||
// Returns true if the event was consumed and should not be routed further.
|
||||
func (r *Router) handleEventReconnectKosmi(msg *config.Message) bool {
|
||||
if msg.Event != config.EventReconnectKosmi {
|
||||
return false
|
||||
}
|
||||
|
||||
originChannel := msg.Channel
|
||||
originAccount := msg.Account
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.logger.Warn("!kreconnect: no Kosmi bridge found")
|
||||
return true
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (r *Router) handleEventVotesQuery(msg *config.Message) bool {
|
||||
if msg.Event != config.EventVotesQuery {
|
||||
return false
|
||||
}
|
||||
|
||||
client := r.JackboxManager.GetClient()
|
||||
if client == nil {
|
||||
r.logger.Warn("!votes: Jackbox client not available")
|
||||
return true
|
||||
}
|
||||
|
||||
session, err := client.GetActiveSession()
|
||||
if err != nil {
|
||||
r.logger.Warnf("!votes: failed to get active session: %v", err)
|
||||
return true
|
||||
}
|
||||
if session == nil {
|
||||
r.logger.Warn("!votes: no active session")
|
||||
return true
|
||||
}
|
||||
|
||||
games, err := client.GetSessionGames(session.ID)
|
||||
if err != nil {
|
||||
r.logger.Warnf("!votes: failed to get session games: %v", err)
|
||||
return true
|
||||
}
|
||||
|
||||
var playingGame *jackbox.SessionGame
|
||||
for i := range games {
|
||||
if games[i].Status == "playing" {
|
||||
playingGame = &games[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if playingGame == nil {
|
||||
r.logger.Warn("!votes: no game currently playing in session")
|
||||
return true
|
||||
}
|
||||
|
||||
r.logger.Infof("!votes: session=%d, playing game ID=%d (session_games.id=%d) title=%q",
|
||||
session.ID, playingGame.GameID, playingGame.ID, playingGame.Title)
|
||||
|
||||
votesResp, err := client.GetSessionVotes(session.ID)
|
||||
if err != nil {
|
||||
r.logger.Warnf("!votes: failed to get session votes: %v", err)
|
||||
return true
|
||||
}
|
||||
|
||||
var sessionUp, sessionDown, sessionNet int
|
||||
if votesResp != nil {
|
||||
r.logger.Infof("!votes: session votes response has %d entries", len(votesResp.Votes))
|
||||
for _, v := range votesResp.Votes {
|
||||
r.logger.Infof("!votes: vote entry game_id=%d title=%q up=%d down=%d net=%d",
|
||||
v.GameID, v.Title, v.Upvotes, v.Downvotes, v.NetScore)
|
||||
if v.GameID == playingGame.GameID {
|
||||
sessionUp = v.Upvotes
|
||||
sessionDown = v.Downvotes
|
||||
sessionNet = v.NetScore
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
r.logger.Info("!votes: session votes response is nil")
|
||||
}
|
||||
|
||||
game, err := client.GetGame(playingGame.GameID)
|
||||
if err != nil {
|
||||
r.logger.Warnf("!votes: failed to get game %d: %v", playingGame.GameID, err)
|
||||
return true
|
||||
}
|
||||
|
||||
var allTimeUp, allTimeDown, allTimeScore int
|
||||
if game != nil {
|
||||
allTimeUp = game.Upvotes
|
||||
allTimeDown = game.Downvotes
|
||||
allTimeScore = game.PopularityScore
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("🗳️ %s • Today: %d👍 %d👎 (Score: %d) • All-time: %d👍 %d👎 (Score: %d)",
|
||||
playingGame.Title, sessionUp, sessionDown, sessionNet, allTimeUp, allTimeDown, allTimeScore)
|
||||
|
||||
r.broadcastJackboxMessage(message)
|
||||
return true
|
||||
}
|
||||
|
||||
// handleEventRejoinChannels handles rejoining of channels.
|
||||
func (r *Router) handleEventRejoinChannels(msg *config.Message) {
|
||||
if msg.Event != config.EventRejoinChannels {
|
||||
|
||||
@@ -155,6 +155,12 @@ func (r *Router) getBridge(account string) *bridge.Bridge {
|
||||
func (r *Router) handleReceive() {
|
||||
for msg := range r.Message {
|
||||
msg := msg // scopelint
|
||||
if r.handleEventReconnectKosmi(&msg) {
|
||||
continue
|
||||
}
|
||||
if r.handleEventVotesQuery(&msg) {
|
||||
continue
|
||||
}
|
||||
r.handleEventGetChannelMembers(&msg)
|
||||
r.handleEventFailure(&msg)
|
||||
r.handleEventRejoinChannels(&msg)
|
||||
|
||||
Reference in New Issue
Block a user