Add better bridge states

This commit is contained in:
Tulir Asokan 2023-07-15 15:02:03 +03:00
parent 7860fb64c8
commit bbc4da21b7
9 changed files with 100 additions and 64 deletions

View file

@ -26,10 +26,20 @@ const (
GMNotConnected status.BridgeStateErrorCode = "gm-not-connected" GMNotConnected status.BridgeStateErrorCode = "gm-not-connected"
GMConnecting status.BridgeStateErrorCode = "gm-connecting" GMConnecting status.BridgeStateErrorCode = "gm-connecting"
GMConnectionFailed status.BridgeStateErrorCode = "gm-connection-failed" 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() { 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 { func (user *User) GetRemoteID() string {

View file

@ -148,9 +148,7 @@ func fnDeleteSession(ce *WrappedCommandEvent) {
ce.Reply("Nothing to purge: no session information stored and no active connection.") ce.Reply("Nothing to purge: no session information stored and no active connection.")
return return
} }
ce.User.removeFromPhoneMap(status.BridgeState{StateEvent: status.StateLoggedOut}) ce.User.Logout(status.BridgeState{StateEvent: status.StateLoggedOut})
ce.User.DeleteConnection()
ce.User.DeleteSession()
ce.Reply("Session information purged") ce.Reply("Session information purged")
} }

2
go.mod
View file

@ -9,7 +9,7 @@ require (
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
go.mau.fi/mautrix-gmessages/libgm v0.1.0 go.mau.fi/mautrix-gmessages/libgm v0.1.0
maunium.net/go/maulogger/v2 v2.4.1 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 ( require (

4
go.sum
View file

@ -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/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 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho= 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.20230714233218-82f817eff669 h1:/7+suCGeh70hK0E7P3/GZLJ+8sMAC82ZBWYDoKSbpOE=
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/go.mod h1:zLrQqdxJlLkurRCozTc9CL6FySkgZlO/kpCYxBILSLE=

View file

@ -1,6 +1,7 @@
package events package events
import ( import (
"fmt"
"net/http" "net/http"
"go.mau.fi/mautrix-gmessages/libgm/binary" "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 { type ListenFatalError struct {
Resp *http.Response Error error
} }
type ListenTemporaryError struct { type ListenTemporaryError struct {

View file

@ -9,27 +9,3 @@ func NewBrowserActive(sessionId string) *BrowserActive {
SessionId: sessionId, 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{}
}

View file

@ -41,6 +41,7 @@ func (r *RPC) ListenReceiveMessages(payload []byte) {
err := r.client.refreshAuthToken() err := r.client.refreshAuthToken()
if err != nil { if err != nil {
r.client.Logger.Err(err).Msg("Error refreshing auth token") 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 return
} }
} }
@ -59,12 +60,12 @@ func (r *RPC) ListenReceiveMessages(payload []byte) {
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
continue 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.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 return
} else if resp.StatusCode >= 500 { } 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 errored = true
r.client.Logger.Debug().Int("statusCode", resp.StatusCode).Msg("5xx error in long polling, retrying in 5 seconds") r.client.Logger.Debug().Int("statusCode", resp.StatusCode).Msg("5xx error in long polling, retrying in 5 seconds")
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)

View file

@ -34,27 +34,7 @@ func (c *Client) handleUserAlertEvent(res *pblite.Response, data *binary.UserAle
} else { } else {
go c.handleClientReady(newSessionId) 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: default:
c.Logger.Info().Any("data", data).Any("res", res).Msg("Got unknown alert type") c.triggerEvent(data)
} }
} }

77
user.go
View file

@ -67,6 +67,11 @@ type User struct {
spaceMembershipChecked bool spaceMembershipChecked bool
longPollingError error
browserInactiveType status.BridgeStateErrorCode
batteryLow bool
mobileData bool
DoublePuppetIntent *appservice.IntentAPI DoublePuppetIntent *appservice.IntentAPI
hackyLoginCommand *WrappedCommandEvent hackyLoginCommand *WrappedCommandEvent
@ -514,21 +519,21 @@ func (user *User) HandleEvent(event interface{}) {
user.hackyLoginCommandPrevEvent = user.sendQR(user.hackyLoginCommand, v.URL, user.hackyLoginCommandPrevEvent) user.hackyLoginCommandPrevEvent = user.sendQR(user.hackyLoginCommand, v.URL, user.hackyLoginCommandPrevEvent)
} }
case *events.ListenFatalError: case *events.ListenFatalError:
user.BridgeState.Send(status.BridgeState{ user.Logout(status.BridgeState{
StateEvent: status.StateUnknownError, StateEvent: status.StateBadCredentials,
Error: GMFatalError, 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: case *events.ListenTemporaryError:
user.longPollingError = v.Error
user.BridgeState.Send(status.BridgeState{ user.BridgeState.Send(status.BridgeState{
StateEvent: status.StateTransientDisconnect, StateEvent: status.StateTransientDisconnect,
Error: GMListenError, Error: GMListenError,
Message: v.Error.Error(), Info: map[string]any{"go_error": v.Error.Error()},
}) })
case *events.ListenRecovered: case *events.ListenRecovered:
user.BridgeState.Send(status.BridgeState{ user.longPollingError = nil
StateEvent: status.StateConnected, user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
})
case *events.PairSuccessful: case *events.PairSuccessful:
user.hackyLoginCommand = nil user.hackyLoginCommand = nil
user.hackyLoginCommandPrevEvent = "" user.hackyLoginCommandPrevEvent = ""
@ -550,7 +555,8 @@ func (user *User) HandleEvent(event interface{}) {
portal := user.GetPortalByID(v.GetConversationID()) portal := user.GetPortalByID(v.GetConversationID())
portal.messages <- PortalMessage{evt: v, source: user} portal.messages <- PortalMessage{evt: v, source: user}
case *events.ClientReady: 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() { go func() {
for _, conv := range v.Conversations { for _, conv := range v.Conversations {
user.syncConversation(conv) user.syncConversation(conv)
@ -558,11 +564,66 @@ func (user *User) HandleEvent(event interface{}) {
}() }()
case *events.BrowserActive: case *events.BrowserActive:
user.zlog.Trace().Any("data", v).Msg("Browser active") user.zlog.Trace().Any("data", v).Msg("Browser active")
user.browserInactiveType = ""
case *binary.UserAlertEvent:
user.handleUserAlert(v)
default: default:
user.zlog.Trace().Any("data", v).Type("data_type", v).Msg("Unknown event") 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) { func (user *User) syncConversation(v *binary.Conversation) {
updateType := v.GetStatus() updateType := v.GetStatus()
portal := user.GetPortalByID(v.GetConversationID()) portal := user.GetPortalByID(v.GetConversationID())