diff --git a/commands.go b/commands.go index 4174639..b4a3b18 100644 --- a/commands.go +++ b/commands.go @@ -17,6 +17,7 @@ package main import ( + "context" "errors" "fmt" "math" @@ -254,7 +255,7 @@ func (handler *CommandHandler) CommandJoin(ce *CommandEvent) { handler.log.Debugln("%s successfully joined group %s", ce.User.MXID, jid) portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(jid)) if len(portal.MXID) > 0 { - portal.Sync(ce.User, whatsapp.Contact{Jid: portal.Key.JID}) + portal.Sync(ce.User, whatsapp.Contact{JID: portal.Key.JID}) ce.Reply("Successfully joined group \"%s\" and synced portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID) } else { err = portal.CreateMatrixRoom(ce.User) @@ -411,11 +412,11 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { ce.Reply("Unknown error while logging out: %v", err) return } - ce.User.Disconnect() ce.User.removeFromJIDMap() // TODO this causes a foreign key violation, which should be fixed //ce.User.JID = "" ce.User.SetSession(nil) + ce.User.DeleteConnection() ce.Reply("Logged out successfully.") } @@ -469,9 +470,10 @@ func (handler *CommandHandler) CommandDeleteSession(ce *CommandEvent) { ce.Reply("Nothing to purge: no session information stored and no active connection.") return } - ce.User.Disconnect() + //ce.User.JID = "" ce.User.removeFromJIDMap() ce.User.SetSession(nil) + ce.User.DeleteConnection() ce.Reply("Session information purged") } @@ -489,24 +491,21 @@ func (handler *CommandHandler) CommandReconnect(ce *CommandEvent) { } wasConnected := true - sess, err := ce.User.Conn.Disconnect() + err := ce.User.Conn.Disconnect() if err == whatsapp.ErrNotConnected { wasConnected = false } else if err != nil { ce.User.log.Warnln("Error while disconnecting:", err) - } else { - ce.User.SetSession(&sess) } - err = ce.User.Conn.Restore(true) + ctx := context.Background() + + err = ce.User.Conn.Restore(true, ctx) if err == whatsapp.ErrInvalidSession { if ce.User.Session != nil { ce.User.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...") - var sess whatsapp.Session - sess, err = ce.User.Conn.RestoreWithSession(*ce.User.Session) - if err == nil { - ce.User.SetSession(&sess) - } + ce.User.Conn.SetSession(*ce.User.Session) + err = ce.User.Conn.Restore(true, ctx) } else { ce.Reply("You are not logged in.") return @@ -520,17 +519,11 @@ func (handler *CommandHandler) CommandReconnect(ce *CommandEvent) { } if err != nil { ce.User.log.Warnln("Error while reconnecting:", err) - if errors.Is(err, whatsapp.ErrRestoreSessionTimeout) { - ce.Reply("Reconnection timed out. Is WhatsApp on your phone reachable?") - } else { - ce.Reply("Unknown error while reconnecting: %v", err) - } + ce.Reply("Unknown error while reconnecting: %v", err) ce.User.log.Debugln("Disconnecting due to failed session restore in reconnect command...") - sess, err = ce.User.Conn.Disconnect() + err = ce.User.Conn.Disconnect() if err != nil { ce.User.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err) - } else { - ce.User.SetSession(&sess) } return } @@ -553,7 +546,7 @@ func (handler *CommandHandler) CommandDeleteConnection(ce *CommandEvent) { ce.Reply("You don't have a WhatsApp connection.") return } - ce.User.Disconnect() + ce.User.DeleteConnection() ce.Reply("Successfully disconnected. Use the `reconnect` command to reconnect.") } @@ -564,7 +557,7 @@ func (handler *CommandHandler) CommandDisconnect(ce *CommandEvent) { ce.Reply("You don't have a WhatsApp connection.") return } - sess, err := ce.User.Conn.Disconnect() + err := ce.User.Conn.Disconnect() if err == whatsapp.ErrNotConnected { ce.Reply("You were not connected.") return @@ -572,8 +565,6 @@ func (handler *CommandHandler) CommandDisconnect(ce *CommandEvent) { ce.User.log.Warnln("Error while disconnecting:", err) ce.Reply("Unknown error while disconnecting: %v", err) return - } else { - ce.User.SetSession(&sess) } ce.User.bridge.Metrics.TrackConnectionState(ce.User.JID, false) ce.Reply("Successfully disconnected. Use the `reconnect` command to reconnect.") @@ -741,9 +732,9 @@ func formatContacts(contacts bool, input map[string]whatsapp.Contact) (result [] } if contacts { - result = append(result, fmt.Sprintf("* %s / %s - `%s`", contact.Name, contact.Notify, contact.Jid[:len(contact.Jid)-len(whatsapp.NewUserSuffix)])) + result = append(result, fmt.Sprintf("* %s / %s - `%s`", contact.Name, contact.Notify, contact.JID[:len(contact.JID)-len(whatsapp.NewUserSuffix)])) } else { - result = append(result, fmt.Sprintf("* %s - `%s`", contact.Name, contact.Jid)) + result = append(result, fmt.Sprintf("* %s - `%s`", contact.Name, contact.JID)) } } sort.Sort(sort.StringSlice(result)) @@ -875,11 +866,11 @@ func (handler *CommandHandler) CommandPM(ce *CommandEvent) { "To create a portal anyway, use `pm --force `.") return } - contact = whatsapp.Contact{Jid: jid} + contact = whatsapp.Contact{JID: jid} } - puppet := user.bridge.GetPuppetByJID(contact.Jid) + puppet := user.bridge.GetPuppetByJID(contact.JID) puppet.Sync(user, contact) - portal := user.bridge.GetPortalByJID(database.NewPortalKey(contact.Jid, user.JID)) + portal := user.bridge.GetPortalByJID(database.NewPortalKey(contact.JID, user.JID)) if len(portal.MXID) > 0 { err := portal.MainIntent().EnsureInvited(portal.MXID, user.MXID) if err != nil { diff --git a/config/bridge.go b/config/bridge.go index ea5e104..f55c997 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -165,8 +165,8 @@ type UsernameTemplateArgs struct { func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) (string, int8) { var buf bytes.Buffer - if index := strings.IndexRune(contact.Jid, '@'); index > 0 { - contact.Jid = "+" + contact.Jid[:index] + if index := strings.IndexRune(contact.JID, '@'); index > 0 { + contact.JID = "+" + contact.JID[:index] } bc.displaynameTemplate.Execute(&buf, contact) var quality int8 @@ -175,7 +175,7 @@ func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) (string, int8 quality = 3 case len(contact.Name) > 0 || len(contact.Short) > 0: quality = 2 - case len(contact.Jid) > 0: + case len(contact.JID) > 0: quality = 1 default: quality = 0 diff --git a/database/user.go b/database/user.go index 88367e0..2a52fcd 100644 --- a/database/user.go +++ b/database/user.go @@ -93,7 +93,7 @@ func (user *User) Scan(row Scannable) *User { if len(jid.String) > 0 && len(clientID.String) > 0 { user.JID = jid.String + whatsapp.NewUserSuffix user.Session = &whatsapp.Session{ - ClientId: clientID.String, + ClientID: clientID.String, ClientToken: clientToken.String, ServerToken: serverToken.String, EncKey: encKey, @@ -139,7 +139,7 @@ func (user *User) Insert() { _, err := user.db.Exec(`INSERT INTO "user" (mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, user.MXID, user.jidPtr(), user.ManagementRoom, user.LastConnection, - sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey) + sess.ClientID, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey) if err != nil { user.log.Warnfln("Failed to insert %s: %v", user.MXID, err) } @@ -158,7 +158,7 @@ func (user *User) Update() { sess := user.sessionUnptr() _, err := user.db.Exec(`UPDATE "user" SET jid=$1, management_room=$2, last_connection=$3, client_id=$4, client_token=$5, server_token=$6, enc_key=$7, mac_key=$8 WHERE mxid=$9`, user.jidPtr(), user.ManagementRoom, user.LastConnection, - sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, + sess.ClientID, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, user.MXID) if err != nil { user.log.Warnfln("Failed to update %s: %v", user.MXID, err) diff --git a/go.mod b/go.mod index fa035b8..0ee1e54 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,8 @@ require ( github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e gopkg.in/yaml.v2 v2.3.0 maunium.net/go/mauflag v1.0.0 - maunium.net/go/maulogger/v2 v2.1.1 + maunium.net/go/maulogger/v2 v2.2.2 maunium.net/go/mautrix v0.8.2 ) -replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.3.21 +replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.3.22-0.20210218211744-b9f35ff6257a diff --git a/go.sum b/go.sum index 379f950..22148fd 100644 --- a/go.sum +++ b/go.sum @@ -145,6 +145,8 @@ github.com/tulir/go-whatsapp v0.3.20 h1:nK92MgruqXwk+QlaAS39xhzHNbFvJIEgUIOUrN3i github.com/tulir/go-whatsapp v0.3.20/go.mod h1:U5+sm33vrv3wz62YyRM/VS7q2ObXkxU4Xqj/3KOmN9o= github.com/tulir/go-whatsapp v0.3.21 h1:2m7gUw4oHX4kIpMmP9VwCR7KEUK/PHhXLygPFGF9XfI= github.com/tulir/go-whatsapp v0.3.21/go.mod h1:U5+sm33vrv3wz62YyRM/VS7q2ObXkxU4Xqj/3KOmN9o= +github.com/tulir/go-whatsapp v0.3.22-0.20210218211744-b9f35ff6257a h1:8JSW6oIAgI1TtR7wkvhNpTYhjKWBxk/eFyB8qXeOfyg= +github.com/tulir/go-whatsapp v0.3.22-0.20210218211744-b9f35ff6257a/go.mod h1:rwwuTh1bKqhgrRvOBAr8hDqtuz8Cc1Quqw/0BeXb+/E= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -232,6 +234,10 @@ 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.1.1 h1:NAZNc6XUFJzgzfewCzVoGkxNAsblLCSSEdtDuIjP0XA= maunium.net/go/maulogger/v2 v2.1.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= +maunium.net/go/maulogger/v2 v2.2.1 h1:qwEDOdT7OhwqvFBXhSD0lqW2O2Oc/DbP/uv3zaai0W8= +maunium.net/go/maulogger/v2 v2.2.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= +maunium.net/go/maulogger/v2 v2.2.2 h1:NCw+7Be1GQFm8xXJ4M2C0Q8yLBTx3c5s7UZ4y1anZMU= +maunium.net/go/maulogger/v2 v2.2.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= maunium.net/go/mautrix v0.8.0-rc.3 h1:bb18oNxHUmeiJ0V63YTRVGMjgoeLwu+G40l4n42Z5GI= maunium.net/go/mautrix v0.8.0-rc.3/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY= maunium.net/go/mautrix v0.8.0-rc.4 h1:3JXoL2JJPE5nh/YSw9sv9dQA9ulma9yHTMOBMBY1xdo= diff --git a/main.go b/main.go index f16862b..693ce50 100644 --- a/main.go +++ b/main.go @@ -384,11 +384,9 @@ func (bridge *Bridge) Stop() { continue } bridge.Log.Debugln("Disconnecting", user.MXID) - sess, err := user.Conn.Disconnect() + err := user.Conn.Disconnect() if err != nil { bridge.Log.Errorfln("Error while disconnecting %s: %v", user.MXID, err) - } else { - user.SetSession(&sess) } } } diff --git a/portal.go b/portal.go index d1703ab..3799339 100644 --- a/portal.go +++ b/portal.go @@ -1771,7 +1771,7 @@ func (portal *Portal) convertGifToVideo(gif []byte) ([]byte, error) { "-pix_fmt", "yuv420p", "-c:v", "libx264", "-movflags", "+faststart", "-filter:v", "crop='floor(in_w/2)*2:floor(in_h/2)*2'", outputFileName) - vcLog := portal.log.Sub("VideoConverter").WithDefaultLevel(log.LevelWarn) + vcLog := portal.log.Sub("VideoConverter").Writer(log.LevelWarn) cmd.Stdout = vcLog cmd.Stderr = vcLog @@ -2319,7 +2319,7 @@ func (portal *Portal) HandleMatrixMeta(sender *User, evt *event.Event) { return } portal.Topic = content.Topic - resp, err = sender.Conn.UpdateGroupDescription(portal.Key.JID, content.Topic) + resp, err = sender.Conn.UpdateGroupDescription(sender.JID, portal.Key.JID, content.Topic) case *event.RoomAvatarEventContent: return } diff --git a/provisioning.go b/provisioning.go index b720bdb..0768883 100644 --- a/provisioning.go +++ b/provisioning.go @@ -125,7 +125,7 @@ func (prov *ProvisioningAPI) DeleteSession(w http.ResponseWriter, r *http.Reques }) return } - user.Disconnect() + user.DeleteConnection() user.SetSession(nil) jsonResponse(w, http.StatusOK, Response{true, "Session information purged"}) } @@ -139,7 +139,7 @@ func (prov *ProvisioningAPI) DeleteConnection(w http.ResponseWriter, r *http.Req }) return } - user.Disconnect() + user.DeleteConnection() jsonResponse(w, http.StatusOK, Response{true, "Disconnected from WhatsApp and connection deleted"}) } @@ -152,7 +152,7 @@ func (prov *ProvisioningAPI) Disconnect(w http.ResponseWriter, r *http.Request) }) return } - sess, err := user.Conn.Disconnect() + err := user.Conn.Disconnect() if err == whatsapp.ErrNotConnected { jsonResponse(w, http.StatusNotFound, Error{ Error: "You were not connected", @@ -166,8 +166,6 @@ func (prov *ProvisioningAPI) Disconnect(w http.ResponseWriter, r *http.Request) ErrCode: err.Error(), }) return - } else { - user.SetSession(&sess) } user.bridge.Metrics.TrackConnectionState(user.JID, false) jsonResponse(w, http.StatusOK, Response{true, "Disconnected from WhatsApp"}) @@ -190,25 +188,21 @@ func (prov *ProvisioningAPI) Reconnect(w http.ResponseWriter, r *http.Request) { user.log.Debugln("Received /reconnect request, disconnecting") wasConnected := true - sess, err := user.Conn.Disconnect() + err := user.Conn.Disconnect() if err == whatsapp.ErrNotConnected { wasConnected = false } else if err != nil { user.log.Warnln("Error while disconnecting:", err) - } else { - user.SetSession(&sess) } user.log.Debugln("Restoring session for /reconnect") - err = user.Conn.Restore(true) + err = user.Conn.Restore(true, r.Context()) user.log.Debugfln("Restore session for /reconnect responded with %v", err) if err == whatsapp.ErrInvalidSession { if user.Session != nil { user.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...") - sess, err = user.Conn.RestoreWithSession(*user.Session) - if err == nil { - user.SetSession(&sess) - } + user.Conn.SetSession(*user.Session) + err = user.Conn.Restore(true, r.Context()) } else { jsonResponse(w, http.StatusForbidden, Error{ Error: "You're not logged in", @@ -216,7 +210,8 @@ func (prov *ProvisioningAPI) Reconnect(w http.ResponseWriter, r *http.Request) { }) return } - } else if err == whatsapp.ErrLoginInProgress { + } + if err == whatsapp.ErrLoginInProgress { jsonResponse(w, http.StatusConflict, Error{ Error: "A login or reconnection is already in progress.", ErrCode: "login in progress", @@ -231,23 +226,14 @@ func (prov *ProvisioningAPI) Reconnect(w http.ResponseWriter, r *http.Request) { } if err != nil { user.log.Warnln("Error while reconnecting:", err) - if errors.Is(err, whatsapp.ErrRestoreSessionTimeout) { - jsonResponse(w, http.StatusForbidden, Error{ - Error: "Reconnection timed out. Is WhatsApp on your phone reachable?", - ErrCode: err.Error(), - }) - } else { - jsonResponse(w, http.StatusForbidden, Error{ - Error: fmt.Sprintf("Unknown error while reconnecting: %v", err), - ErrCode: err.Error(), - }) - } + jsonResponse(w, http.StatusInternalServerError, Error{ + Error: fmt.Sprintf("Unknown error while reconnecting: %v", err), + ErrCode: err.Error(), + }) user.log.Debugln("Disconnecting due to failed session restore in reconnect command...") - sess, err := user.Conn.Disconnect() + err = user.Conn.Disconnect() if err != nil { user.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err) - } else { - user.SetSession(&sess) } return } @@ -339,7 +325,7 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) { return } } - user.Disconnect() + user.DeleteConnection() } user.bridge.Metrics.TrackConnectionState(user.JID, false) @@ -407,7 +393,7 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) { }) user.log.Debugln("Starting login via provisioning API") - session, err := user.Conn.LoginWithRetry(qrChan, ctx, user.bridge.Config.Bridge.LoginQRRegenCount) + session, jid, err := user.Conn.Login(qrChan, ctx, user.bridge.Config.Bridge.LoginQRRegenCount) qrChan <- "stop" if err != nil { var msg string @@ -419,7 +405,7 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) { msg = "QR code scan timed out. Please try again." } else if errors.Is(err, whatsapp.ErrInvalidWebsocket) { msg = "WhatsApp connection error. Please try again." - user.Disconnect() + // TODO might need to make sure it reconnects? } else { msg = fmt.Sprintf("Unknown error while logging in: %v", err) } @@ -430,9 +416,9 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) { }) return } - user.log.Debugln("Successful login via provisioning API") + user.log.Debugln("Successful login as", jid, "via provisioning API") user.ConnectionErrors = 0 - user.JID = strings.Replace(user.Conn.Info.Wid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, 1) + user.JID = strings.Replace(jid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, 1) user.addToJIDMap() user.SetSession(&session) _ = c.WriteJSON(map[string]interface{}{ diff --git a/puppet.go b/puppet.go index 7c29460..2549e9e 100644 --- a/puppet.go +++ b/puppet.go @@ -291,8 +291,8 @@ func (puppet *Puppet) Sync(source *User, contact whatsapp.Contact) { puppet.log.Errorln("Failed to ensure registered:", err) } - if contact.Jid == source.JID { - contact.Notify = source.Conn.Info.Pushname + if contact.JID == source.JID { + contact.Notify = source.pushName } update := false diff --git a/user.go b/user.go index 5135a31..4c4b873 100644 --- a/user.go +++ b/user.go @@ -17,6 +17,7 @@ package main import ( + "context" "encoding/json" "errors" "fmt" @@ -61,6 +62,7 @@ type User struct { cleanDisconnection bool batteryWarningsSent int lastReconnection int64 + pushName string chatListReceived chan struct{} syncPortalsDone chan struct{} @@ -71,8 +73,9 @@ type User struct { syncStart chan struct{} syncWait sync.WaitGroup - mgmtCreateLock sync.Mutex - connLock sync.Mutex + mgmtCreateLock sync.Mutex + connLock sync.Mutex + cancelReconnect func() } func (bridge *Bridge) GetUserByMXID(userID id.UserID) *User { @@ -234,55 +237,56 @@ func (user *User) SetSession(session *whatsapp.Session) { func (user *User) Connect(evenIfNoSession bool) bool { user.connLock.Lock() - if user.Conn != nil && user.Conn.IsConnected() { + if user.Conn != nil { user.connLock.Unlock() - return true + if user.Conn.IsConnected() { + return true + } else { + return user.RestoreSession() + } } else if !evenIfNoSession && user.Session == nil { user.connLock.Unlock() return false } - if user.Conn != nil { - user.Disconnect() - } user.log.Debugln("Connecting to WhatsApp") timeout := time.Duration(user.bridge.Config.Bridge.ConnectionTimeout) if timeout == 0 { timeout = 20 } - conn, err := whatsapp.NewConnWithOptions(&whatsapp.Options{ + user.Conn = whatsapp.NewConn(&whatsapp.Options{ Timeout: timeout * time.Second, LongClientName: user.bridge.Config.WhatsApp.OSName, ShortClientName: user.bridge.Config.WhatsApp.BrowserName, ClientVersion: WAVersion, + Log: user.log.Sub("Conn"), + Handler: []whatsapp.Handler{user}, }) - if err != nil { - user.log.Errorln("Failed to connect to WhatsApp:", err) - user.sendMarkdownBridgeAlert("\u26a0 Failed to connect to WhatsApp server. " + - "This indicates a network problem on the bridge server. See bridge logs for more info.") - user.connLock.Unlock() - return false - } - user.Conn = conn - user.log.Debugln("WhatsApp connection successful") - user.Conn.AddHandler(user) user.connLock.Unlock() return user.RestoreSession() } -func (user *User) Disconnect() { - sess, err := user.Conn.Disconnect() +func (user *User) DeleteConnection() { + user.connLock.Lock() + if user.Conn == nil { + user.connLock.Unlock() + return + } + err := user.Conn.Disconnect() if err != nil && err != whatsapp.ErrNotConnected { user.log.Warnln("Error disconnecting: %v", err) } - user.SetSession(&sess) user.Conn.RemoveHandlers() user.Conn = nil user.bridge.Metrics.TrackConnectionState(user.JID, false) + user.connLock.Unlock() } func (user *User) RestoreSession() bool { if user.Session != nil { - sess, err := user.Conn.RestoreWithSession(*user.Session) + user.Conn.SetSession(*user.Session) + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + err := user.Conn.Restore(true, ctx) if err == whatsapp.ErrAlreadyLoggedIn { return true } else if err != nil { @@ -290,24 +294,23 @@ func (user *User) RestoreSession() bool { if errors.Is(err, whatsapp.ErrUnpaired) { user.sendMarkdownBridgeAlert("\u26a0 Failed to connect to WhatsApp: unpaired from phone. " + "To re-pair your phone, log in again.") - user.Disconnect() user.removeFromJIDMap() //user.JID = "" user.SetSession(nil) + user.DeleteConnection() return false } else { user.sendMarkdownBridgeAlert("\u26a0 Failed to connect to WhatsApp. Make sure WhatsApp " + "on your phone is reachable and use `reconnect` to try connecting again.") } user.log.Debugln("Disconnecting due to failed session restore...") - _, err := user.Conn.Disconnect() + err = user.Conn.Disconnect() if err != nil { user.log.Errorln("Failed to disconnect after failed session restore:", err) } return false } user.ConnectionErrors = 0 - user.SetSession(&sess) user.log.Debugln("Session restored successfully") user.PostLogin() } @@ -382,7 +385,7 @@ func (user *User) Login(ce *CommandEvent) { qrChan := make(chan string, 3) eventIDChan := make(chan id.EventID, 1) go user.loginQrChannel(ce, qrChan, eventIDChan) - session, err := user.Conn.LoginWithRetry(qrChan, nil, user.bridge.Config.Bridge.LoginQRRegenCount) + session, jid, err := user.Conn.Login(qrChan, nil, user.bridge.Config.Bridge.LoginQRRegenCount) qrChan <- "stop" if err != nil { var eventID id.EventID @@ -416,8 +419,9 @@ func (user *User) Login(ce *CommandEvent) { } // TODO there's a bit of duplication between this and the provisioning API login method // Also between the two logout methods (commands.go and provisioning.go) + user.log.Debugln("Successful login as", jid, "via command") user.ConnectionErrors = 0 - user.JID = strings.Replace(user.Conn.Info.Wid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, 1) + user.JID = strings.Replace(jid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, 1) user.addToJIDMap() user.SetSession(&session) ce.Reply("Successfully logged in, synchronizing chats...") @@ -499,31 +503,31 @@ func (user *User) sendMarkdownBridgeAlert(formatString string, args ...interface } } -func (user *User) postConnPing(conn *whatsapp.Conn) bool { - if user.Conn != conn { - user.log.Warnln("Connection changed before scheduled post-connection ping, canceling ping") - return false - } +func (user *User) postConnPing() bool { user.log.Debugln("Making post-connection ping") - err := conn.AdminTest() - if err != nil { - user.log.Errorfln("Post-connection ping failed: %v. Disconnecting and then reconnecting after a second", err) - sess, disconnectErr := conn.Disconnect() - if disconnectErr != nil { - user.log.Warnln("Error while disconnecting after failed post-connection ping:", disconnectErr) + var err error + for i := 0; ; i++ { + err = user.Conn.AdminTest() + if err == nil { + user.log.Debugln("Post-connection ping OK") + return true + } else if errors.Is(err, whatsapp.ErrConnectionTimeout) && i < 5 { + user.log.Warnfln("Post-connection ping timed out, sending new one") } else { - user.Session = &sess + break } - user.bridge.Metrics.TrackDisconnection(user.MXID) - go func() { - time.Sleep(1 * time.Second) - user.tryReconnect(fmt.Sprintf("Post-connection ping failed: %v", err)) - }() - return false - } else { - user.log.Debugln("Post-connection ping OK") - return true } + user.log.Errorfln("Post-connection ping failed: %v. Disconnecting and then reconnecting after a second", err) + disconnectErr := user.Conn.Disconnect() + if disconnectErr != nil { + user.log.Warnln("Error while disconnecting after failed post-connection ping:", disconnectErr) + } + user.bridge.Metrics.TrackDisconnection(user.MXID) + go func() { + time.Sleep(1 * time.Second) + user.tryReconnect(fmt.Sprintf("Post-connection ping failed: %v", err)) + }() + return false } func (user *User) intPostLogin(conn *whatsapp.Conn) { @@ -538,11 +542,11 @@ func (user *User) intPostLogin(conn *whatsapp.Conn) { user.log.Debugln("Chat list receive confirmation received in PostLogin") case <-time.After(time.Duration(user.bridge.Config.Bridge.ChatListWait) * time.Second): user.log.Warnln("Timed out waiting for chat list to arrive!") - user.postConnPing(conn) + user.postConnPing() return } - if !user.postConnPing(conn) { + if !user.postConnPing() { user.log.Debugln("Post-connection ping failed, unlocking processing of incoming messages.") return } @@ -557,16 +561,14 @@ func (user *User) intPostLogin(conn *whatsapp.Conn) { } } -type InfoGetter interface { +type NormalMessage interface { GetInfo() whatsapp.MessageInfo } func (user *User) HandleEvent(event interface{}) { switch v := event.(type) { - case whatsapp.TextMessage, whatsapp.ImageMessage, whatsapp.StickerMessage, whatsapp.VideoMessage, - whatsapp.AudioMessage, whatsapp.DocumentMessage, whatsapp.ContactMessage, whatsapp.StubMessage, - whatsapp.LocationMessage: - info := v.(InfoGetter).GetInfo() + case NormalMessage: + info := v.GetInfo() user.messageInput <- PortalMessage{info.RemoteJid, user, v, info.Timestamp} case whatsapp.MessageRevocation: user.messageInput <- PortalMessage{v.RemoteJid, user, v, 0} @@ -596,6 +598,8 @@ func (user *User) HandleEvent(event interface{}) { user.HandleCommand(v) case whatsapp.ChatUpdate: user.HandleChatUpdate(v) + case whatsapp.ConnInfo: + user.HandleConnInfo(v) case json.RawMessage: user.HandleJSONMessage(v) case *waProto.WebMessageInfo: @@ -614,11 +618,11 @@ func (user *User) HandleStreamEvent(evt whatsapp.StreamEvent) { if user.lastReconnection+60 > time.Now().Unix() { user.lastReconnection = 0 user.log.Infoln("Stream went to sleep soon after reconnection, making new post-connection ping in 20 seconds") - conn := user.Conn go func() { time.Sleep(20 * time.Second) // TODO if this happens during the post-login sync, it can get stuck forever - user.postConnPing(conn) + // TODO check if the above is still true + user.postConnPing() }() } } else { @@ -630,10 +634,10 @@ func (user *User) HandleChatList(chats []whatsapp.Chat) { user.log.Infoln("Chat list received") chatMap := make(map[string]whatsapp.Chat) for _, chat := range user.Conn.Store.Chats { - chatMap[chat.Jid] = chat + chatMap[chat.JID] = chat } for _, chat := range chats { - chatMap[chat.Jid] = chat + chatMap[chat.JID] = chat } select { case user.chatListReceived <- struct{}{}: @@ -655,14 +659,14 @@ func (user *User) syncPortals(chatMap map[string]whatsapp.Chat, createAll bool) for _, chat := range chatMap { ts, err := strconv.ParseUint(chat.LastMessageTime, 10, 64) if err != nil { - user.log.Warnfln("Non-integer last message time in %s: %s", chat.Jid, chat.LastMessageTime) + user.log.Warnfln("Non-integer last message time in %s: %s", chat.JID, chat.LastMessageTime) continue } - portal := user.GetPortalByJID(chat.Jid) + portal := user.GetPortalByJID(chat.JID) chats = append(chats, Chat{ Portal: portal, - Contact: user.Conn.Store.Contacts[chat.Jid], + Contact: user.Conn.Store.Contacts[chat.JID], LastMessageTime: ts, }) var inCommunity, ok bool @@ -777,7 +781,7 @@ func (user *User) UpdateDirectChats(chats map[id.UserID][]id.RoomID) { func (user *User) HandleContactList(contacts []whatsapp.Contact) { contactMap := make(map[string]whatsapp.Contact) for _, contact := range contacts { - contactMap[contact.Jid] = contact + contactMap[contact.JID] = contact } go user.syncPuppets(contactMap) } @@ -786,10 +790,20 @@ func (user *User) syncPuppets(contacts map[string]whatsapp.Contact) { if contacts == nil { contacts = user.Conn.Store.Contacts } + + _, hasSelf := contacts[user.JID] + if !hasSelf { + contacts[user.JID] = whatsapp.Contact{ + Name: user.pushName, + Notify: user.pushName, + JID: user.JID, + } + } + user.log.Infoln("Syncing puppet info from contacts") for jid, contact := range contacts { if strings.HasSuffix(jid, whatsapp.NewUserSuffix) { - puppet := user.bridge.GetPuppetByJID(contact.Jid) + puppet := user.bridge.GetPuppetByJID(contact.JID) puppet.Sync(user, contact) } } @@ -846,14 +860,18 @@ func (user *User) tryReconnect(msg string) { baseDelay = -baseDelay + 1 } delay := baseDelay - conn := user.Conn takeover := false + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + user.cancelReconnect = cancel for user.ConnectionErrors <= user.bridge.Config.Bridge.MaxConnectionAttempts { - if user.Conn != conn { - user.log.Debugln("Connection was recreated, aborting reconnection attempts") + select { + case <-ctx.Done(): + user.log.Debugln("tryReconnect context cancelled, aborting reconnection attempts") return + default: } - err := conn.Restore(takeover) + err := user.Conn.Restore(takeover, ctx) takeover = true if err == nil { user.ConnectionErrors = 0 @@ -863,14 +881,23 @@ func (user *User) tryReconnect(msg string) { user.PostLogin() return } else if errors.Is(err, whatsapp.ErrBadRequest) { - user.log.Infoln("Got init 400 error when trying to reconnect, resetting connection...") - sess, err := conn.Disconnect() + user.log.Warnln("Got init 400 error when trying to reconnect, resetting connection...") + err = user.Conn.Disconnect() if err != nil { user.log.Debugln("Error while disconnecting for connection reset:", err) } - user.SetSession(&sess) + } else if errors.Is(err, whatsapp.ErrUnpaired) { + user.log.Errorln("Got init 401 (unpaired) error when trying to reconnect, not retrying") + user.removeFromJIDMap() + //user.JID = "" + user.SetSession(nil) + user.DeleteConnection() + user.sendMarkdownBridgeAlert("\u26a0 Failed to reconnect to WhatsApp: unpaired from phone. " + + "To re-pair your phone, log in again.") + return + } else { + user.log.Errorln("Error while trying to reconnect after disconnection:", err) } - user.log.Errorln("Error while trying to reconnect after disconnection:", err) tries++ user.ConnectionErrors++ if user.ConnectionErrors <= user.bridge.Config.Bridge.MaxConnectionAttempts { @@ -930,10 +957,10 @@ func (user *User) handleMessageLoop() { func (user *User) HandleNewContact(contact whatsapp.Contact) { user.log.Debugfln("Contact message: %+v", contact) - if strings.HasSuffix(contact.Jid, whatsapp.OldUserSuffix) { - contact.Jid = strings.Replace(contact.Jid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, -1) + if strings.HasSuffix(contact.JID, whatsapp.OldUserSuffix) { + contact.JID = strings.Replace(contact.JID, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, -1) } - puppet := user.bridge.GetPuppetByJID(contact.Jid) + puppet := user.bridge.GetPuppetByJID(contact.JID) puppet.UpdateName(user, contact) } @@ -1176,11 +1203,23 @@ func (user *User) HandleChatUpdate(cmd whatsapp.ChatUpdate) { go portal.HandleWhatsAppInvite(cmd.Data.SenderJID, nil, cmd.Data.UserChange.JIDs) case whatsapp.ChatActionIntroduce: if cmd.Data.SenderJID != "unknown" { - go portal.Sync(user, whatsapp.Contact{Jid: portal.Key.JID}) + go portal.Sync(user, whatsapp.Contact{JID: portal.Key.JID}) } } } +func (user *User) HandleConnInfo(info whatsapp.ConnInfo) { + if user.Session != nil && info.Connected && len(info.ClientToken) > 0 { + user.log.Debugln("Received new tokens") + user.Session.ClientToken = info.ClientToken + user.Session.ServerToken = info.ServerToken + user.Session.Wid = info.WID + } + if len(info.PushName) > 0 { + user.pushName = info.PushName + } +} + func (user *User) HandleJSONMessage(message json.RawMessage) { if !json.Valid(message) { return