From bbc4da21b7e18f53d526f8db1cb871adffbc1175 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 15 Jul 2023 15:02:03 +0300 Subject: [PATCH] Add better bridge states --- bridgestate.go | 12 +++++- commands.go | 4 +- go.mod | 2 +- go.sum | 4 +- libgm/events/ready.go | 12 +++++- libgm/events/useralerts.go | 24 ------------ libgm/rpc.go | 7 ++-- libgm/useralert_handler.go | 22 +---------- user.go | 77 ++++++++++++++++++++++++++++++++++---- 9 files changed, 100 insertions(+), 64 deletions(-) diff --git a/bridgestate.go b/bridgestate.go index d81a9a5..1a837e9 100644 --- a/bridgestate.go +++ b/bridgestate.go @@ -26,10 +26,20 @@ const ( GMNotConnected status.BridgeStateErrorCode = "gm-not-connected" GMConnecting status.BridgeStateErrorCode = "gm-connecting" GMConnectionFailed status.BridgeStateErrorCode = "gm-connection-failed" + + GMBrowserInactive status.BridgeStateErrorCode = "gm-browser-inactive" + GMBrowserInactiveTimeout status.BridgeStateErrorCode = "gm-browser-inactive-timeout" + GMBrowserInactiveInactivity status.BridgeStateErrorCode = "gm-browser-inactive-inactivity" ) func init() { - status.BridgeStateHumanErrors.Update(status.BridgeStateErrorMap{}) + status.BridgeStateHumanErrors.Update(status.BridgeStateErrorMap{ + GMListenError: "Error polling messages from Google Messages server, the bridge will try to reconnect", + GMFatalError: "Google Messages login was invalidated, please re-link the bridge", + GMBrowserInactive: "Google Messages opened in another browser", + GMBrowserInactiveTimeout: "Google Messages disconnected due to timeout", + GMBrowserInactiveInactivity: "Google Messages disconnected due to inactivity", + }) } func (user *User) GetRemoteID() string { diff --git a/commands.go b/commands.go index d29dfd4..d497776 100644 --- a/commands.go +++ b/commands.go @@ -148,9 +148,7 @@ func fnDeleteSession(ce *WrappedCommandEvent) { ce.Reply("Nothing to purge: no session information stored and no active connection.") return } - ce.User.removeFromPhoneMap(status.BridgeState{StateEvent: status.StateLoggedOut}) - ce.User.DeleteConnection() - ce.User.DeleteSession() + ce.User.Logout(status.BridgeState{StateEvent: status.StateLoggedOut}) ce.Reply("Session information purged") } diff --git a/go.mod b/go.mod index ca181ec..9f01149 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e go.mau.fi/mautrix-gmessages/libgm v0.1.0 maunium.net/go/maulogger/v2 v2.4.1 - maunium.net/go/mautrix v0.15.4-0.20230711231757-65db706cd3ce + maunium.net/go/mautrix v0.15.4-0.20230714233218-82f817eff669 ) require ( diff --git a/go.sum b/go.sum index c5b0178..6c02e5f 100644 --- a/go.sum +++ b/go.sum @@ -81,5 +81,5 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8= maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho= -maunium.net/go/mautrix v0.15.4-0.20230711231757-65db706cd3ce h1:SYYPKkcJLI012g+p3jZ/Qnqm5VQd7kFZSSEHOtI4NZA= -maunium.net/go/mautrix v0.15.4-0.20230711231757-65db706cd3ce/go.mod h1:zLrQqdxJlLkurRCozTc9CL6FySkgZlO/kpCYxBILSLE= +maunium.net/go/mautrix v0.15.4-0.20230714233218-82f817eff669 h1:/7+suCGeh70hK0E7P3/GZLJ+8sMAC82ZBWYDoKSbpOE= +maunium.net/go/mautrix v0.15.4-0.20230714233218-82f817eff669/go.mod h1:zLrQqdxJlLkurRCozTc9CL6FySkgZlO/kpCYxBILSLE= diff --git a/libgm/events/ready.go b/libgm/events/ready.go index dc605c3..ece3e76 100644 --- a/libgm/events/ready.go +++ b/libgm/events/ready.go @@ -1,6 +1,7 @@ package events import ( + "fmt" "net/http" "go.mau.fi/mautrix-gmessages/libgm/binary" @@ -28,8 +29,17 @@ func NewAuthTokenRefreshed(token []byte) *AuthTokenRefreshed { } } +type HTTPError struct { + Action string + Resp *http.Response +} + +func (he HTTPError) Error() string { + return fmt.Sprintf("http %d while %s", he.Resp.StatusCode, he.Action) +} + type ListenFatalError struct { - Resp *http.Response + Error error } type ListenTemporaryError struct { diff --git a/libgm/events/useralerts.go b/libgm/events/useralerts.go index a00730b..989adfb 100644 --- a/libgm/events/useralerts.go +++ b/libgm/events/useralerts.go @@ -9,27 +9,3 @@ func NewBrowserActive(sessionId string) *BrowserActive { SessionId: sessionId, } } - -type MOBILE_BATTERY_RESTORED struct{} - -func NewMobileBatteryRestored() *MOBILE_BATTERY_RESTORED { - return &MOBILE_BATTERY_RESTORED{} -} - -type MOBILE_BATTERY_LOW struct{} - -func NewMobileBatteryLow() *MOBILE_BATTERY_LOW { - return &MOBILE_BATTERY_LOW{} -} - -type MOBILE_DATA_CONNECTION struct{} - -func NewMobileDataConnection() *MOBILE_DATA_CONNECTION { - return &MOBILE_DATA_CONNECTION{} -} - -type MOBILE_WIFI_CONNECTION struct{} - -func NewMobileWifiConnection() *MOBILE_WIFI_CONNECTION { - return &MOBILE_WIFI_CONNECTION{} -} diff --git a/libgm/rpc.go b/libgm/rpc.go index 9e261b5..5ab580e 100644 --- a/libgm/rpc.go +++ b/libgm/rpc.go @@ -41,6 +41,7 @@ func (r *RPC) ListenReceiveMessages(payload []byte) { err := r.client.refreshAuthToken() if err != nil { r.client.Logger.Err(err).Msg("Error refreshing auth token") + r.client.triggerEvent(&events.ListenFatalError{Error: fmt.Errorf("failed to refresh auth token: %w", err)}) return } } @@ -59,12 +60,12 @@ func (r *RPC) ListenReceiveMessages(payload []byte) { time.Sleep(5 * time.Second) continue } - if resp.StatusCode >= 400 && resp.StatusCode < 501 { + if resp.StatusCode >= 400 && resp.StatusCode < 500 { r.client.Logger.Error().Int("status_code", resp.StatusCode).Msg("Error making listen request") - r.client.triggerEvent(&events.ListenFatalError{Resp: resp}) + r.client.triggerEvent(&events.ListenFatalError{Error: events.HTTPError{Action: "polling", Resp: resp}}) return } else if resp.StatusCode >= 500 { - r.client.triggerEvent(&events.ListenTemporaryError{Error: fmt.Errorf("http %d while polling", resp.StatusCode)}) + r.client.triggerEvent(&events.ListenTemporaryError{Error: events.HTTPError{Action: "polling", Resp: resp}}) errored = true r.client.Logger.Debug().Int("statusCode", resp.StatusCode).Msg("5xx error in long polling, retrying in 5 seconds") time.Sleep(5 * time.Second) diff --git a/libgm/useralert_handler.go b/libgm/useralert_handler.go index 0f9ae27..2fc7562 100644 --- a/libgm/useralert_handler.go +++ b/libgm/useralert_handler.go @@ -34,27 +34,7 @@ func (c *Client) handleUserAlertEvent(res *pblite.Response, data *binary.UserAle } else { go c.handleClientReady(newSessionId) } - case binary.AlertType_MOBILE_BATTERY_LOW: - c.Logger.Info().Msg("[MOBILE_BATTERY_LOW] Mobile device is on low battery") - evt := events.NewMobileBatteryLow() - c.triggerEvent(evt) - - case binary.AlertType_MOBILE_BATTERY_RESTORED: - c.Logger.Info().Msg("[MOBILE_BATTERY_RESTORED] Mobile device has restored enough battery!") - evt := events.NewMobileBatteryRestored() - c.triggerEvent(evt) - - case binary.AlertType_MOBILE_DATA_CONNECTION: - c.Logger.Info().Msg("[MOBILE_DATA_CONNECTION] Mobile device is now using data connection") - evt := events.NewMobileDataConnection() - c.triggerEvent(evt) - - case binary.AlertType_MOBILE_WIFI_CONNECTION: - c.Logger.Info().Msg("[MOBILE_WIFI_CONNECTION] Mobile device is now using wifi connection") - evt := events.NewMobileWifiConnection() - c.triggerEvent(evt) - default: - c.Logger.Info().Any("data", data).Any("res", res).Msg("Got unknown alert type") + c.triggerEvent(data) } } diff --git a/user.go b/user.go index e66367b..6582f0a 100644 --- a/user.go +++ b/user.go @@ -67,6 +67,11 @@ type User struct { spaceMembershipChecked bool + longPollingError error + browserInactiveType status.BridgeStateErrorCode + batteryLow bool + mobileData bool + DoublePuppetIntent *appservice.IntentAPI hackyLoginCommand *WrappedCommandEvent @@ -514,21 +519,21 @@ func (user *User) HandleEvent(event interface{}) { user.hackyLoginCommandPrevEvent = user.sendQR(user.hackyLoginCommand, v.URL, user.hackyLoginCommandPrevEvent) } case *events.ListenFatalError: - user.BridgeState.Send(status.BridgeState{ - StateEvent: status.StateUnknownError, + user.Logout(status.BridgeState{ + StateEvent: status.StateBadCredentials, Error: GMFatalError, - Message: fmt.Sprintf("HTTP %d in long polling loop", v.Resp.StatusCode), + Info: map[string]any{"go_error": v.Error.Error()}, }) case *events.ListenTemporaryError: + user.longPollingError = v.Error user.BridgeState.Send(status.BridgeState{ StateEvent: status.StateTransientDisconnect, Error: GMListenError, - Message: v.Error.Error(), + Info: map[string]any{"go_error": v.Error.Error()}, }) case *events.ListenRecovered: - user.BridgeState.Send(status.BridgeState{ - StateEvent: status.StateConnected, - }) + user.longPollingError = nil + user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected}) case *events.PairSuccessful: user.hackyLoginCommand = nil user.hackyLoginCommandPrevEvent = "" @@ -550,7 +555,8 @@ func (user *User) HandleEvent(event interface{}) { portal := user.GetPortalByID(v.GetConversationID()) portal.messages <- PortalMessage{evt: v, source: user} case *events.ClientReady: - user.zlog.Trace().Any("data", v).Msg("Client is ready!") + user.browserInactiveType = "" + user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected}) go func() { for _, conv := range v.Conversations { user.syncConversation(conv) @@ -558,11 +564,66 @@ func (user *User) HandleEvent(event interface{}) { }() case *events.BrowserActive: user.zlog.Trace().Any("data", v).Msg("Browser active") + user.browserInactiveType = "" + case *binary.UserAlertEvent: + user.handleUserAlert(v) default: user.zlog.Trace().Any("data", v).Type("data_type", v).Msg("Unknown event") } } +func (user *User) handleUserAlert(v *binary.UserAlertEvent) { + user.zlog.Debug().Any("data", v).Msg("Got user alert event") + switch v.GetAlertType() { + case binary.AlertType_BROWSER_INACTIVE: + // TODO aggressively reactivate if configured to do so + user.browserInactiveType = GMBrowserInactive + case binary.AlertType_BROWSER_INACTIVE_FROM_TIMEOUT: + user.browserInactiveType = GMBrowserInactiveTimeout + case binary.AlertType_BROWSER_INACTIVE_FROM_INACTIVITY: + user.browserInactiveType = GMBrowserInactiveInactivity + case binary.AlertType_MOBILE_DATA_CONNECTION: + user.mobileData = true + case binary.AlertType_MOBILE_WIFI_CONNECTION: + user.mobileData = false + case binary.AlertType_MOBILE_BATTERY_LOW: + user.batteryLow = true + case binary.AlertType_MOBILE_BATTERY_RESTORED: + user.batteryLow = false + default: + return + } + user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected}) +} + +func (user *User) FillBridgeState(state status.BridgeState) status.BridgeState { + if state.Info == nil { + state.Info = make(map[string]any) + } + state.Info["battery_low"] = user.batteryLow + state.Info["mobile_data"] = user.mobileData + state.Info["browser_active"] = user.browserInactiveType == "" + if state.StateEvent == status.StateConnected { + if user.longPollingError != nil { + state.StateEvent = status.StateTransientDisconnect + state.Error = GMListenError + state.Info["go_error"] = user.longPollingError.Error() + } + if user.browserInactiveType != "" { + state.StateEvent = status.StateTransientDisconnect + state.Error = user.browserInactiveType + } + } + return state +} + +func (user *User) Logout(state status.BridgeState) { + user.removeFromPhoneMap(state) + // TODO invalidate token + user.DeleteConnection() + user.DeleteSession() +} + func (user *User) syncConversation(v *binary.Conversation) { updateType := v.GetStatus() portal := user.GetPortalByID(v.GetConversationID())