diff --git a/handlers.go b/handlers.go index e92b713e..3f0cc61a 100644 --- a/handlers.go +++ b/handlers.go @@ -259,12 +259,24 @@ func (s *server) Connect() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - webhook := r.Context().Value("userinfo").(Values).Get("Webhook") - jid := r.Context().Value("userinfo").(Values).Get("Jid") - txtid := r.Context().Value("userinfo").(Values).Get("Id") - token := r.Context().Value("userinfo").(Values).Get("Token") + userInfo := r.Context().Value("userinfo").(Values) + webhook := userInfo.Get("Webhook") + jid := userInfo.Get("Jid") + txtid := userInfo.Get("Id") + token := userInfo.Get("Token") eventstring := "" + // Always prefer the DB jid over a stale in-memory cache entry so reconnect + // can find the whatsmeow device even after a process restart. + var dbJid string + if err := s.db.QueryRow("SELECT jid FROM users WHERE id = $1", txtid).Scan(&dbJid); err == nil && dbJid != "" { + jid = dbJid + if jid != userInfo.Get("Jid") { + v := updateUserInfo(userInfo, "Jid", jid) + userinfocache.Set(token, v, cache.NoExpiration) + } + } + // Decodes request BODY looking for events to subscribe decoder := json.NewDecoder(r.Body) var t connectStruct @@ -757,6 +769,47 @@ func (s *server) PairPhone() http.HandlerFunc { } } +// resolveSessionJID returns the best-known JID for a user. When logged in, the +// live whatsmeow store is authoritative — cache/DB often lag after QR pairing. +func (s *server) resolveSessionJID(ctx context.Context, txtid string, waClient *whatsmeow.Client, userInfo Values) string { + jid := userInfo.Get("Jid") + + if waClient != nil && waClient.Store != nil && waClient.IsLoggedIn() && waClient.Store.ID != nil { + storeJID := waClient.Store.ID.ToNonAD() + if storeJID.Server == types.HiddenUserServer { + timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + pn, err := getCachedPNForLID(timeoutCtx, waClient, storeJID) + if err == nil && !pn.IsEmpty() { + storeJID = pn.ToNonAD() + } + } + resolved := storeJID.String() + if resolved != "" { + jid = resolved + if resolved != userInfo.Get("Jid") { + token := userInfo.Get("Token") + if token != "" { + v := updateUserInfo(userInfo, "Jid", resolved) + userinfocache.Set(token, v, cache.NoExpiration) + } + query := s.db.Rebind("UPDATE users SET jid=? WHERE id=?") + if _, err := s.db.Exec(query, resolved, txtid); err != nil { + log.Warn().Err(err).Str("user_id", txtid).Msg("Failed to persist resolved JID") + } + } + } + } else if jid == "" { + var dbJid string + query := s.db.Rebind("SELECT jid FROM users WHERE id = ?") + if err := s.db.QueryRow(query, txtid).Scan(&dbJid); err == nil && dbJid != "" { + jid = dbJid + } + } + + return jid +} + // PasskeyResponse receives a WebAuthn response from the frontend and sends it to WhatsApp func (s *server) PasskeyResponse() http.HandlerFunc { type passkeyResponseStruct struct { @@ -882,8 +935,10 @@ func (s *server) GetStatus() http.HandlerFunc { txtid := userInfo.Get("Id") - isConnected := clientManager.GetWhatsmeowClient(txtid).IsConnected() - isLoggedIn := clientManager.GetWhatsmeowClient(txtid).IsLoggedIn() + waClient := clientManager.GetWhatsmeowClient(txtid) + isConnected := waClient != nil && waClient.IsConnected() + isLoggedIn := waClient != nil && waClient.IsLoggedIn() + jid := s.resolveSessionJID(r.Context(), txtid, waClient, userInfo) // Safe defaults so the response always contains every config field. proxyURL := "" @@ -953,7 +1008,7 @@ func (s *server) GetStatus() http.HandlerFunc { "connected": isConnected, "loggedIn": isLoggedIn, "token": userInfo.Get("Token"), - "jid": userInfo.Get("Jid"), + "jid": jid, "webhook": userInfo.Get("Webhook"), "events": userInfo.Get("Events"), "proxy_url": userInfo.Get("Proxy"), diff --git a/wmiau.go b/wmiau.go index 8f8bc9e8..a8fec072 100644 --- a/wmiau.go +++ b/wmiau.go @@ -406,6 +406,9 @@ func (s *server) connectOnStartup() { } func parseJID(arg string) (types.JID, bool) { + if arg == "" { + return types.JID{}, false + } if arg[0] == '+' { arg = arg[1:] } @@ -424,6 +427,127 @@ func parseJID(arg string) (types.JID, bool) { } } +// jidUserKey returns the phone/account part shared by users.jid and +// whatsmeow_device.jid even when formats differ (380...:8@ vs 380...@). +func jidUserKey(jid string) string { + if jid == "" { + return "" + } + userPart := strings.SplitN(jid, "@", 2)[0] + return strings.SplitN(userPart, ":", 2)[0] +} + +// jidLookupCandidates builds JID variants to try with sqlstore.GetDevice before +// falling back to account-key matching across all stored devices. +func jidLookupCandidates(textjid string) []types.JID { + jid, ok := parseJID(textjid) + if !ok { + return nil + } + + seen := make(map[string]struct{}) + var candidates []types.JID + add := func(candidate types.JID) { + if candidate.IsEmpty() { + return + } + key := candidate.String() + if _, exists := seen[key]; exists { + return + } + seen[key] = struct{}{} + candidates = append(candidates, candidate) + } + + add(jid) + add(jid.ToNonAD()) + + userOnly, _, _ := strings.Cut(jid.User, ":") + if userOnly != "" { + add(types.NewJID(userOnly, jid.Server)) + if jid.Server == types.DefaultUserServer || jid.Server == types.HiddenUserServer { + add(types.NewJID(userOnly, types.DefaultUserServer)) + } + } + + return candidates +} + +func canonicalStoreJID(deviceStore *store.Device) string { + if deviceStore == nil || deviceStore.ID == nil { + return "" + } + return deviceStore.ID.ToNonAD().String() +} + +// resolveDeviceStore loads an existing WhatsApp session from sqlstore. When +// users.jid does not exactly match whatsmeow_device.jid (common after LID/AD +// format changes), it falls back to matching by account key so reconnect does +// not create a fresh device and force QR scan. +func (s *server) resolveDeviceStore(ctx context.Context, textjid string) (*store.Device, string) { + if textjid != "" { + for _, candidate := range jidLookupCandidates(textjid) { + deviceStore, err := container.GetDevice(ctx, candidate) + if err != nil { + log.Error().Err(err).Str("jid", candidate.String()).Msg("Failed to get device") + continue + } + if resolved := canonicalStoreJID(deviceStore); resolved != "" { + if candidate.String() != textjid { + log.Info(). + Str("user_jid", textjid). + Str("store_jid", deviceStore.ID.String()). + Str("resolved_jid", resolved). + Msg("Resolved device by JID variant") + } + return deviceStore, resolved + } + } + + key := jidUserKey(textjid) + if key != "" { + allDevices, err := container.GetAllDevices(ctx) + if err != nil { + log.Error().Err(err).Msg("Failed to list devices from store") + } else { + for _, deviceStore := range allDevices { + if resolved := canonicalStoreJID(deviceStore); resolved != "" && jidUserKey(resolved) == key { + log.Info(). + Str("user_jid", textjid). + Str("store_jid", deviceStore.ID.String()). + Str("resolved_jid", resolved). + Msg("Resolved device by account key") + return deviceStore, resolved + } + } + } + } + + log.Warn().Str("jid", textjid).Msg("No store found for jid. Creating new device") + } else { + log.Warn().Msg("No jid found. Creating new device") + } + + return container.NewDevice(), "" +} + +func (s *server) syncUserJID(userID, token, oldJID, newJID string) { + if newJID == "" || newJID == oldJID { + return + } + if _, err := s.db.Exec(`UPDATE users SET jid=$1 WHERE id=$2`, newJID, userID); err != nil { + log.Warn().Err(err).Str("user_id", userID).Msg("Failed to sync jid from device store") + return + } + if token != "" { + if myuserinfo, found := userinfocache.Get(token); found { + v := updateUserInfo(myuserinfo, "Jid", newJID) + userinfocache.Set(token, v, cache.NoExpiration) + } + } + log.Info().Str("user_id", userID).Str("old_jid", oldJID).Str("resolved_jid", newJID).Msg("Synced user jid from whatsmeow store") +} + // getPlatformTypeEnum converts a platform type string to the corresponding DeviceProps enum // Returns DESKTOP as default if the string doesn't match any known type func getPlatformTypeEnum(platformType string) *waCompanionReg.DeviceProps_PlatformType { @@ -489,26 +613,10 @@ func (s *server) startClient(userID string, textjid string, token string, kill c const maxConnectionRetries = 3 const connectionRetryBaseWait = 5 * time.Second - var deviceStore *store.Device - var err error - - // First handle the device store initialization - if textjid != "" { - jid, _ := parseJID(textjid) - deviceStore, err = container.GetDevice(context.Background(), jid) - if err != nil { - log.Error().Err(err).Msg("Failed to get device") - deviceStore = container.NewDevice() - } - } else { - log.Warn().Msg("No jid found. Creating new device") - deviceStore = container.NewDevice() - } + deviceStore, resolvedJID := s.resolveDeviceStore(context.Background(), textjid) + s.syncUserJID(userID, token, textjid, resolvedJID) - if deviceStore == nil { - log.Warn().Msg("No store found. Creating new one") - deviceStore = container.NewDevice() - } + var err error clientLog := waLog.Stdout("Client", *waDebug, *colorOutput) @@ -860,11 +968,24 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { log.Error().Err(err).Msg(sqlStmt) return } + if mycli.WAClient.Store != nil && mycli.WAClient.Store.ID != nil { + connectedJID := mycli.WAClient.Store.ID.ToNonAD().String() + query := mycli.db.Rebind(`UPDATE users SET jid=? WHERE id=?`) + if _, err := mycli.db.Exec(query, connectedJID, mycli.userID); err != nil { + log.Warn().Err(err).Str("user_id", mycli.userID).Msg("Failed to persist JID on connect") + } else if myuserinfo, found := userinfocache.Get(mycli.token); found { + v := updateUserInfo(myuserinfo, "Jid", connectedJID) + userinfocache.Set(mycli.token, v, cache.NoExpiration) + } + } case *events.PairSuccess: log.Info().Str("userid", mycli.userID).Str("token", mycli.token).Str("ID", evt.ID.String()).Str("BusinessName", evt.BusinessName).Str("Platform", evt.Platform).Msg("QR Pair Success") - jid := evt.ID - sqlStmt := `UPDATE users SET jid=$1 WHERE id=$2` - _, err := mycli.db.Exec(sqlStmt, jid, mycli.userID) + jidStr := evt.ID.String() + if mycli.WAClient.Store != nil && mycli.WAClient.Store.ID != nil { + jidStr = mycli.WAClient.Store.ID.ToNonAD().String() + } + sqlStmt := mycli.db.Rebind(`UPDATE users SET jid=? WHERE id=?`) + _, err := mycli.db.Exec(sqlStmt, jidStr, mycli.userID) if err != nil { log.Error().Err(err).Msg(sqlStmt) return @@ -879,9 +1000,9 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { } else { txtid = myuserinfo.(Values).Get("Id") token := myuserinfo.(Values).Get("Token") - v := updateUserInfo(myuserinfo, "Jid", fmt.Sprintf("%s", jid)) + v := updateUserInfo(myuserinfo, "Jid", jidStr) userinfocache.Set(token, v, cache.NoExpiration) - log.Info().Str("jid", jid.String()).Str("userid", txtid).Str("token", token).Msg("User information set") + log.Info().Str("jid", jidStr).Str("userid", txtid).Str("token", token).Msg("User information set") } // Check if automatic history sync is enabled and trigger it after QR code is scanned