// Copyright (c) 2021 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package whatsmeow import ( "errors" "go.mau.fi/whatsmeow/appstate" waBinary "go.mau.fi/whatsmeow/binary" "go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" ) func (cli *Client) handleEncryptNotification(node *waBinary.Node) { from := node.AttrGetter().JID("from") if from == types.ServerJID { count := node.GetChildByTag("count") ag := count.AttrGetter() otksLeft := ag.Int("value") if !ag.OK() { cli.Log.Warnf("Didn't get number of OTKs left in encryption notification %s", node.XMLString()) return } cli.Log.Infof("Got prekey count from server: %s", node.XMLString()) if otksLeft < MinPreKeyCount { cli.uploadPreKeys() } } else if _, ok := node.GetOptionalChildByTag("identity"); ok { cli.Log.Debugf("Got identity change for %s: %s, deleting all identities/sessions for that number", from, node.XMLString()) err := cli.Store.Identities.DeleteAllIdentities(from.User) if err != nil { cli.Log.Warnf("Failed to delete all identities of %s from store after identity change: %v", from, err) } err = cli.Store.Sessions.DeleteAllSessions(from.User) if err != nil { cli.Log.Warnf("Failed to delete all sessions of %s from store after identity change: %v", from, err) } ts := node.AttrGetter().UnixTime("t") cli.dispatchEvent(&events.IdentityChange{JID: from, Timestamp: ts}) } else { cli.Log.Debugf("Got unknown encryption notification from server: %s", node.XMLString()) } } func (cli *Client) handleAppStateNotification(node *waBinary.Node) { for _, collection := range node.GetChildrenByTag("collection") { ag := collection.AttrGetter() name := appstate.WAPatchName(ag.String("name")) version := ag.Uint64("version") cli.Log.Debugf("Got server sync notification that app state %s has updated to version %d", name, version) err := cli.FetchAppState(name, false, false) if errors.Is(err, ErrIQDisconnected) || errors.Is(err, ErrNotConnected) { // There are some app state changes right before a remote logout, so stop syncing if we're disconnected. cli.Log.Debugf("Failed to sync app state after notification: %v, not trying to sync other states", err) return } else if err != nil { cli.Log.Errorf("Failed to sync app state after notification: %v", err) } } } func (cli *Client) handlePictureNotification(node *waBinary.Node) { ts := node.AttrGetter().UnixTime("t") for _, child := range node.GetChildren() { ag := child.AttrGetter() var evt events.Picture evt.Timestamp = ts evt.JID = ag.JID("jid") evt.Author = ag.OptionalJIDOrEmpty("author") if child.Tag == "delete" { evt.Remove = true } else if child.Tag == "add" { evt.PictureID = ag.String("id") } else if child.Tag == "set" { // TODO sometimes there's a hash and no ID? evt.PictureID = ag.String("id") } else { continue } if !ag.OK() { cli.Log.Debugf("Ignoring picture change notification with unexpected attributes: %v", ag.Error()) continue } cli.dispatchEvent(&evt) } } func (cli *Client) handleDeviceNotification(node *waBinary.Node) { cli.userDevicesCacheLock.Lock() defer cli.userDevicesCacheLock.Unlock() ag := node.AttrGetter() from := ag.JID("from") cached, ok := cli.userDevicesCache[from] if !ok { cli.Log.Debugf("No device list cached for %s, ignoring device list notification", from) return } cachedParticipantHash := participantListHashV2(cached) for _, child := range node.GetChildren() { if child.Tag != "add" && child.Tag != "remove" { cli.Log.Debugf("Unknown device list change tag %s", child.Tag) continue } cag := child.AttrGetter() deviceHash := cag.String("device_hash") deviceChild, _ := child.GetOptionalChildByTag("device") changedDeviceJID := deviceChild.AttrGetter().JID("jid") switch child.Tag { case "add": cached = append(cached, changedDeviceJID) case "remove": for i, jid := range cached { if jid == changedDeviceJID { cached = append(cached[:i], cached[i+1:]...) } } case "update": // ??? } newParticipantHash := participantListHashV2(cached) if newParticipantHash == deviceHash { cli.Log.Debugf("%s's device list hash changed from %s to %s (%s). New hash matches", from, cachedParticipantHash, deviceHash, child.Tag) cli.userDevicesCache[from] = cached } else { cli.Log.Warnf("%s's device list hash changed from %s to %s (%s). New hash doesn't match (%s)", from, cachedParticipantHash, deviceHash, child.Tag, newParticipantHash) delete(cli.userDevicesCache, from) } } } func (cli *Client) handleOwnDevicesNotification(node *waBinary.Node) { cli.userDevicesCacheLock.Lock() defer cli.userDevicesCacheLock.Unlock() ownID := cli.getOwnID().ToNonAD() if ownID.IsEmpty() { cli.Log.Debugf("Ignoring own device change notification, session was deleted") return } cached, ok := cli.userDevicesCache[ownID] if !ok { cli.Log.Debugf("Ignoring own device change notification, device list not cached") return } oldHash := participantListHashV2(cached) expectedNewHash := node.AttrGetter().String("dhash") var newDeviceList []types.JID for _, child := range node.GetChildren() { jid := child.AttrGetter().JID("jid") if child.Tag == "device" && !jid.IsEmpty() { jid.AD = true newDeviceList = append(newDeviceList, jid) } } newHash := participantListHashV2(newDeviceList) if newHash != expectedNewHash { cli.Log.Debugf("Received own device list change notification %s -> %s, but expected hash was %s", oldHash, newHash, expectedNewHash) delete(cli.userDevicesCache, ownID) } else { cli.Log.Debugf("Received own device list change notification %s -> %s", oldHash, newHash) cli.userDevicesCache[ownID] = newDeviceList } } func (cli *Client) handleAccountSyncNotification(node *waBinary.Node) { for _, child := range node.GetChildren() { switch child.Tag { case "privacy": cli.handlePrivacySettingsNotification(&child) case "devices": cli.handleOwnDevicesNotification(&child) default: cli.Log.Debugf("Unhandled account sync item %s", child.Tag) } } } func (cli *Client) handlePrivacyTokenNotification(node *waBinary.Node) { ownID := cli.getOwnID().ToNonAD() if ownID.IsEmpty() { cli.Log.Debugf("Ignoring privacy token notification, session was deleted") return } tokens := node.GetChildByTag("tokens") if tokens.Tag != "tokens" { cli.Log.Warnf("privacy_token notification didn't contain tag") return } parentAG := node.AttrGetter() sender := parentAG.JID("from") if !parentAG.OK() { cli.Log.Warnf("privacy_token notification didn't have a sender (%v)", parentAG.Error()) return } for _, child := range tokens.GetChildren() { ag := child.AttrGetter() if child.Tag != "token" { cli.Log.Warnf("privacy_token notification contained unexpected <%s> tag", child.Tag) } else if targetUser := ag.JID("jid"); targetUser != ownID { cli.Log.Warnf("privacy_token notification contained token for different user %s", targetUser) } else if tokenType := ag.String("type"); tokenType != "trusted_contact" { cli.Log.Warnf("privacy_token notification contained unexpected token type %s", tokenType) } else if token, ok := child.Content.([]byte); !ok { cli.Log.Warnf("privacy_token notification contained non-binary token") } else { timestamp := ag.UnixTime("t") if !ag.OK() { cli.Log.Warnf("privacy_token notification is missing some fields: %v", ag.Error()) } err := cli.Store.PrivacyTokens.PutPrivacyTokens(store.PrivacyToken{ User: sender, Token: token, Timestamp: timestamp, }) if err != nil { cli.Log.Errorf("Failed to save privacy token from %s: %v", sender, err) } else { cli.Log.Debugf("Stored privacy token from %s (ts: %v)", sender, timestamp) } } } } func (cli *Client) handleNotification(node *waBinary.Node) { ag := node.AttrGetter() notifType := ag.String("type") if !ag.OK() { return } go cli.sendAck(node) switch notifType { case "encrypt": go cli.handleEncryptNotification(node) case "server_sync": go cli.handleAppStateNotification(node) case "account_sync": go cli.handleAccountSyncNotification(node) case "devices": go cli.handleDeviceNotification(node) case "w:gp2": evt, err := cli.parseGroupNotification(node) if err != nil { cli.Log.Errorf("Failed to parse group notification: %v", err) } else { go cli.dispatchEvent(evt) } case "picture": go cli.handlePictureNotification(node) case "mediaretry": go cli.handleMediaRetryNotification(node) case "privacy_token": go cli.handlePrivacyTokenNotification(node) // Other types: business, disappearing_mode, server, status, pay, psa default: cli.Log.Debugf("Unhandled notification with type %s", notifType) } }