From 23747d4917d10f508c5dab90aae15ef249a4b366 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 17 May 2019 23:53:57 +0300 Subject: [PATCH] Add automatic connection retries --- commands.go | 5 ++++ config/bridge.go | 12 +++++++-- config/config.go | 1 + user.go | 69 +++++++++++++++++++++++++++++++++++++++--------- 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/commands.go b/commands.go index 74ce7a2..b50ad09 100644 --- a/commands.go +++ b/commands.go @@ -182,6 +182,9 @@ func (handler *CommandHandler) CommandReconnect(ce *CommandEvent) { ce.Reply("You are not logged in.") return } + } else if err == whatsapp.ErrLoginInProgress { + ce.Reply("A login or reconnection is already in progress.") + return } if err != nil { ce.User.log.Warnln("Error while reconnecting:", err) @@ -190,6 +193,7 @@ func (handler *CommandHandler) CommandReconnect(ce *CommandEvent) { ce.Reply("You were already connected.") } else { ce.User.Connected = true + ce.User.ConnectionErrors = 0 ce.Reply("You were already connected, but the bridge hadn't noticed. Fixed that now.") } } else if err.Error() == "restore session connection timed out" { @@ -200,6 +204,7 @@ func (handler *CommandHandler) CommandReconnect(ce *CommandEvent) { return } ce.User.Connected = true + ce.User.ConnectionErrors = 0 ce.Reply("Reconnected successfully.") } diff --git a/config/bridge.go b/config/bridge.go index d4da810..765347b 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -33,7 +33,9 @@ type BridgeConfig struct { UsernameTemplate string `yaml:"username_template"` DisplaynameTemplate string `yaml:"displayname_template"` - ConnectionTimeout int `yaml:"connection_timeout"` + ConnectionTimeout int `yaml:"connection_timeout"` + MaxConnectionAttempts int `yaml:"max_connection_attempts"` + ReportConnectionRetry bool `yaml:"report_connection_retry"` CommandPrefix string `yaml:"command_prefix"` @@ -43,6 +45,12 @@ type BridgeConfig struct { displaynameTemplate *template.Template `yaml:"-"` } +func (bc *BridgeConfig) setDefaults() { + bc.ConnectionTimeout = 20 + bc.MaxConnectionAttempts = 3 + bc.ReportConnectionRetry = true +} + type umBridgeConfig BridgeConfig func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { @@ -61,7 +69,7 @@ func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { } type UsernameTemplateArgs struct { - UserID string + UserID string } func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) (string, int8) { diff --git a/config/config.go b/config/config.go index f572742..20eff6d 100644 --- a/config/config.go +++ b/config/config.go @@ -64,6 +64,7 @@ type Config struct { func (config *Config) setDefaults() { config.AppService.Database.MaxOpenConns = 20 config.AppService.Database.MaxIdleConns = 2 + config.Bridge.setDefaults() } func Load(path string) (*Config, error) { diff --git a/user.go b/user.go index fd14117..8b8ed5a 100644 --- a/user.go +++ b/user.go @@ -44,6 +44,8 @@ type User struct { Admin bool Whitelisted bool Connected bool + + ConnectionErrors int } func (bridge *Bridge) GetUserByMXID(userID types.MatrixUserID) *User { @@ -182,6 +184,7 @@ func (user *User) RestoreSession() bool { return false } user.Connected = true + user.ConnectionErrors = 0 user.SetSession(&sess) user.log.Debugln("Session restored successfully") } @@ -236,6 +239,7 @@ func (user *User) Login(ce *CommandEvent) { return } user.Connected = true + user.ConnectionErrors = 0 user.JID = strings.Replace(user.Conn.Info.Wid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, 1) user.SetSession(&session) ce.Reply("Successfully logged in. Now, you may ask for `sync [--create]`.") @@ -243,22 +247,63 @@ func (user *User) Login(ce *CommandEvent) { func (user *User) HandleError(err error) { user.log.Errorln("WhatsApp error:", err) - closed, ok := err.(*whatsapp.ErrConnectionClosed) - if ok { - if closed.Code != 1000 { - msg := fmt.Sprintf("\u26a0 Your WhatsApp connection was closed with websocket status code %d.\n\n"+ - "Use the `reconnect` command to reconnect.", closed.Code) - _, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, mautrix.EventMessage, format.RenderMarkdown(msg)) + var msg string + if closed, ok := err.(*whatsapp.ErrConnectionClosed); ok { + user.Connected = false + if closed.Code == 1000 { + // Normal closure + return } + user.ConnectionErrors++ + msg = fmt.Sprintf("Your WhatsApp connection was closed with websocket status code %d", closed.Code) + } else if failed, ok := err.(*whatsapp.ErrConnectionFailed); ok { user.Connected = false + user.ConnectionErrors++ + msg = fmt.Sprintf("Your WhatsApp connection failed: %v", failed.Err) + } else { + // Unknown error, probably mostly harmless + return } - failed, ok := err.(*whatsapp.ErrConnectionFailed) - if ok { - msg := fmt.Sprintf("\u26a0 Your WhatsApp connection failed: %v.\n\n"+ - "Use the `reconnect` command to reconnect.", failed.Err) - _, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, mautrix.EventMessage, format.RenderMarkdown(msg)) - user.Connected = false + if user.ConnectionErrors > user.bridge.Config.Bridge.MaxConnectionAttempts { + content := format.RenderMarkdown(fmt.Sprintf("%s. Use the `reconnect` command to reconnect.", msg)) + _, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, mautrix.EventMessage, content) + return } + if user.bridge.Config.Bridge.ReportConnectionRetry { + _, _ = user.bridge.Bot.SendNotice(user.ManagementRoom, fmt.Sprintf("%s. Reconnecting...", msg)) + // Don't want the same error to be repeated + msg = "" + } + tries := 0 + for user.ConnectionErrors <= user.bridge.Config.Bridge.MaxConnectionAttempts { + err = user.Conn.Restore() + if err == nil { + user.ConnectionErrors = 0 + user.Connected = true + _, _ = user.bridge.Bot.SendNotice(user.ManagementRoom, "Reconnected successfully") + return + } + user.log.Errorln("Error while trying to reconnect after disconnection:", err) + tries++ + user.ConnectionErrors++ + if user.ConnectionErrors <= user.bridge.Config.Bridge.MaxConnectionAttempts { + if user.bridge.Config.Bridge.ReportConnectionRetry { + _, _ = user.bridge.Bot.SendNotice(user.ManagementRoom, + fmt.Sprintf("Reconnection attempt failed: %v. Retrying in 10 seconds...", err)) + } + time.Sleep(10 * time.Second) + } + } + + if user.bridge.Config.Bridge.ReportConnectionRetry { + msg = fmt.Sprintf("%d reconnection attempts failed. Use the `reconnect` command to try to reconnect manually.", tries) + } else { + msg = fmt.Sprintf("\u26a0 %sAdditionally, %d reconnection attempts failed. "+ + "Use the `reconnect` command to try to reconnect.", msg, tries) + } + + content := format.RenderMarkdown(msg) + _, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, mautrix.EventMessage, content) } func (user *User) HandleJSONParseError(err error) {