Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 62 additions & 7 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Comment on lines +802 to +807

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The hardcoded $1 placeholder in the fallback SELECT query will fail on SQLite. Use s.db.Rebind with ? placeholders to maintain database compatibility.

	} 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 {
Expand Down Expand Up @@ -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 := ""
Expand Down Expand Up @@ -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"),
Expand Down
169 changes: 145 additions & 24 deletions wmiau.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:]
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading