From 30752fa48b418f8c194408c1f75b6f90b67845fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Mon, 2 Feb 2026 19:34:57 +0100 Subject: [PATCH] Improve Discord bridge state handling --- pkg/connector/bridge_state.go | 127 +++++++++++++++++++++++++++++++++ pkg/connector/client.go | 13 +++- pkg/connector/handlediscord.go | 18 +++-- pkg/connector/login_generic.go | 5 ++ 4 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 pkg/connector/bridge_state.go diff --git a/pkg/connector/bridge_state.go b/pkg/connector/bridge_state.go new file mode 100644 index 0000000..d1ccad1 --- /dev/null +++ b/pkg/connector/bridge_state.go @@ -0,0 +1,127 @@ +// mautrix-discord - A Matrix-Discord puppeting bridge. +// Copyright (C) 2026 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package connector + +import ( + "time" + + "maunium.net/go/mautrix/bridgev2/status" +) + +const ( + DiscordNotLoggedIn status.BridgeStateErrorCode = "discord-not-logged-in" + DiscordInvalidAuth status.BridgeStateErrorCode = "discord-invalid-auth" + DiscordDisconnected status.BridgeStateErrorCode = "discord-disconnected" + DiscordConnectFailed status.BridgeStateErrorCode = "discord-connect-failed" +) + +const discordDisconnectDebounce = 7 * time.Second + +func init() { + status.BridgeStateHumanErrors.Update(status.BridgeStateErrorMap{ + DiscordNotLoggedIn: "You're not logged into Discord. Relogin to continue using the bridge.", + DiscordInvalidAuth: "You were logged out of Discord. Relogin to continue using the bridge.", + DiscordDisconnected: "Disconnected from Discord. Trying to reconnect.", + DiscordConnectFailed: "Connecting to Discord failed.", + }) +} + +func (d *DiscordClient) resetBridgeStateTracking() { + d.bridgeStateLock.Lock() + if d.disconnectTimer != nil { + d.disconnectTimer.Stop() + d.disconnectTimer = nil + } + d.invalidAuthDetected = false + d.bridgeStateLock.Unlock() +} + +func (d *DiscordClient) markConnected() { + if d.UserLogin == nil { + return + } + d.bridgeStateLock.Lock() + if d.disconnectTimer != nil { + d.disconnectTimer.Stop() + d.disconnectTimer = nil + } + d.invalidAuthDetected = false + d.bridgeStateLock.Unlock() + d.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected}) +} + +func (d *DiscordClient) markInvalidAuth(message string) { + if d.UserLogin == nil { + return + } + d.bridgeStateLock.Lock() + d.invalidAuthDetected = true + if d.disconnectTimer != nil { + d.disconnectTimer.Stop() + d.disconnectTimer = nil + } + d.bridgeStateLock.Unlock() + d.UserLogin.BridgeState.Send(status.BridgeState{ + StateEvent: status.StateBadCredentials, + Error: DiscordInvalidAuth, + Message: message, + }) +} + +func (d *DiscordClient) scheduleTransientDisconnect(message string) { + if d.UserLogin == nil { + return + } + d.bridgeStateLock.Lock() + if d.invalidAuthDetected { + d.bridgeStateLock.Unlock() + return + } + if d.disconnectTimer != nil { + d.disconnectTimer.Stop() + } + login := d.UserLogin + d.disconnectTimer = time.AfterFunc(discordDisconnectDebounce, func() { + d.bridgeStateLock.Lock() + d.disconnectTimer = nil + invalidAuth := d.invalidAuthDetected + d.bridgeStateLock.Unlock() + if invalidAuth { + return + } + login.BridgeState.Send(status.BridgeState{ + StateEvent: status.StateTransientDisconnect, + Error: DiscordDisconnected, + Message: message, + }) + }) + d.bridgeStateLock.Unlock() +} + +func (d *DiscordClient) sendConnectFailure(err error) { + if d.UserLogin == nil || err == nil { + return + } + d.UserLogin.BridgeState.Send(status.BridgeState{ + StateEvent: status.StateUnknownError, + Error: DiscordConnectFailed, + Message: err.Error(), + Info: map[string]any{ + "go_error": err.Error(), + }, + }) +} diff --git a/pkg/connector/client.go b/pkg/connector/client.go index a6d2cae..2dd4b8d 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -48,6 +48,10 @@ type DiscordClient struct { markedOpened map[string]time.Time markedOpenedLock sync.Mutex + + bridgeStateLock sync.Mutex + disconnectTimer *time.Timer + invalidAuthDetected bool } func (d *DiscordConnector) LoadUserLogin(ctx context.Context, login *bridgev2.UserLogin) error { @@ -95,6 +99,7 @@ func (d *DiscordClient) SetUp(ctx context.Context, meta *discordid.UserLoginMeta } d.markedOpened = make(map[string]time.Time) + d.resetBridgeStateTracking() } func (d *DiscordClient) Connect(ctx context.Context) { @@ -104,7 +109,7 @@ func (d *DiscordClient) Connect(ctx context.Context) { log.Error().Msg("No session present") d.UserLogin.BridgeState.Send(status.BridgeState{ StateEvent: status.StateBadCredentials, - Error: "discord-not-logged-in", + Error: DiscordNotLoggedIn, }) return } @@ -135,12 +140,16 @@ func (cl *DiscordClient) connect(ctx context.Context) error { } if err != nil { log.Err(err).Msg("Failed to connect to Discord") + cl.sendConnectFailure(err) return err } // Ensure that we actually have a user. if !cl.IsLoggedIn() { - return fmt.Errorf("unknown identity even after connecting to Discord") + err := fmt.Errorf("unknown identity even after connecting to Discord") + log.Err(err).Msg("No Discord user available after connecting") + cl.sendConnectFailure(err) + return err } user := cl.Session.State.User log.Info().Str("user_id", user.ID).Str("user_username", user.Username).Msg("Connected to Discord") diff --git a/pkg/connector/handlediscord.go b/pkg/connector/handlediscord.go index 7525579..b9d6136 100644 --- a/pkg/connector/handlediscord.go +++ b/pkg/connector/handlediscord.go @@ -25,7 +25,6 @@ import ( "github.com/rs/zerolog" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/networkid" - "maunium.net/go/mautrix/bridgev2/status" "go.mau.fi/mautrix-discord/pkg/discordid" ) @@ -196,16 +195,21 @@ func (d *DiscordClient) handleDiscordEvent(rawEvt any) { Logger() switch evt := rawEvt.(type) { + case *discordgo.Connect: + log.Info().Msg("Discord gateway connected") + d.markConnected() + case *discordgo.Disconnect: + log.Info().Msg("Discord gateway disconnected") + d.scheduleTransientDisconnect("") + case *discordgo.InvalidAuth: + log.Warn().Msg("Discord gateway reported invalid auth") + d.markInvalidAuth("You have been logged out of Discord, please reconnect") case *discordgo.Ready: log.Info().Msg("Received READY dispatch from discordgo") - d.UserLogin.BridgeState.Send(status.BridgeState{ - StateEvent: status.StateConnected, - }) + d.markConnected() case *discordgo.Resumed: log.Info().Msg("Received RESUMED dispatch from discordgo") - d.UserLogin.BridgeState.Send(status.BridgeState{ - StateEvent: status.StateConnected, - }) + d.markConnected() case *discordgo.MessageCreate: if evt.Author == nil { log.Trace().Int("message_type", int(evt.Message.Type)). diff --git a/pkg/connector/login_generic.go b/pkg/connector/login_generic.go index 95a82ef..a04db97 100644 --- a/pkg/connector/login_generic.go +++ b/pkg/connector/login_generic.go @@ -97,6 +97,11 @@ func (dl *DiscordGenericLogin) FinalizeCreatingLogin(ctx context.Context, token Str("user_username", user.Username). Msg("Logged in to Discord") + // We already opened the gateway session before creating the UserLogin, + // which means the initial READY/CONNECT event was dropped. Send Connected + // here so infra gets login status for new logins. + client.markConnected() + return ul, nil }