forked from jshiffer/matterbridge
2f33fe86f5
* Update dependencies and build to go1.22 * Fix api changes wrt to dependencies * Update golangci config
397 lines
13 KiB
Go
397 lines
13 KiB
Go
// 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 (
|
|
"encoding/json"
|
|
"errors"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"go.mau.fi/whatsmeow/appstate"
|
|
waBinary "go.mau.fi/whatsmeow/binary"
|
|
waProto "go.mau.fi/whatsmeow/binary/proto"
|
|
"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.devices)
|
|
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.devices = append(cached.devices, changedDeviceJID)
|
|
case "remove":
|
|
for i, jid := range cached.devices {
|
|
if jid == changedDeviceJID {
|
|
cached.devices = append(cached.devices[:i], cached.devices[i+1:]...)
|
|
}
|
|
}
|
|
case "update":
|
|
// ???
|
|
}
|
|
newParticipantHash := participantListHashV2(cached.devices)
|
|
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) handleFBDeviceNotification(node *waBinary.Node) {
|
|
cli.userDevicesCacheLock.Lock()
|
|
defer cli.userDevicesCacheLock.Unlock()
|
|
jid := node.AttrGetter().JID("from")
|
|
userDevices := parseFBDeviceList(jid, node.GetChildByTag("devices"))
|
|
cli.userDevicesCache[jid] = userDevices
|
|
}
|
|
|
|
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.devices)
|
|
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() {
|
|
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] = deviceCache{devices: newDeviceList, dhash: expectedNewHash}
|
|
}
|
|
}
|
|
|
|
func (cli *Client) handleBlocklist(node *waBinary.Node) {
|
|
ag := node.AttrGetter()
|
|
evt := events.Blocklist{
|
|
Action: events.BlocklistAction(ag.OptionalString("action")),
|
|
DHash: ag.String("dhash"),
|
|
PrevDHash: ag.OptionalString("prev_dhash"),
|
|
}
|
|
for _, child := range node.GetChildren() {
|
|
ag := child.AttrGetter()
|
|
change := events.BlocklistChange{
|
|
JID: ag.JID("jid"),
|
|
Action: events.BlocklistChangeAction(ag.String("action")),
|
|
}
|
|
if !ag.OK() {
|
|
cli.Log.Warnf("Unexpected data in blocklist event child %v: %v", child.XMLString(), ag.Error())
|
|
continue
|
|
}
|
|
evt.Changes = append(evt.Changes, change)
|
|
}
|
|
cli.dispatchEvent(&evt)
|
|
}
|
|
|
|
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)
|
|
case "picture":
|
|
cli.dispatchEvent(&events.Picture{
|
|
Timestamp: node.AttrGetter().UnixTime("t"),
|
|
JID: cli.getOwnID().ToNonAD(),
|
|
})
|
|
case "blocklist":
|
|
cli.handleBlocklist(&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 <tokens> 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) parseNewsletterMessages(node *waBinary.Node) []*types.NewsletterMessage {
|
|
children := node.GetChildren()
|
|
output := make([]*types.NewsletterMessage, 0, len(children))
|
|
for _, child := range children {
|
|
if child.Tag != "message" {
|
|
continue
|
|
}
|
|
msg := types.NewsletterMessage{
|
|
MessageServerID: child.AttrGetter().Int("server_id"),
|
|
ViewsCount: 0,
|
|
ReactionCounts: nil,
|
|
}
|
|
for _, subchild := range child.GetChildren() {
|
|
switch subchild.Tag {
|
|
case "plaintext":
|
|
byteContent, ok := subchild.Content.([]byte)
|
|
if ok {
|
|
msg.Message = new(waProto.Message)
|
|
err := proto.Unmarshal(byteContent, msg.Message)
|
|
if err != nil {
|
|
cli.Log.Warnf("Failed to unmarshal newsletter message: %v", err)
|
|
msg.Message = nil
|
|
}
|
|
}
|
|
case "views_count":
|
|
msg.ViewsCount = subchild.AttrGetter().Int("count")
|
|
case "reactions":
|
|
msg.ReactionCounts = make(map[string]int)
|
|
for _, reaction := range subchild.GetChildren() {
|
|
rag := reaction.AttrGetter()
|
|
msg.ReactionCounts[rag.String("code")] = rag.Int("count")
|
|
}
|
|
}
|
|
}
|
|
output = append(output, &msg)
|
|
}
|
|
return output
|
|
}
|
|
|
|
func (cli *Client) handleNewsletterNotification(node *waBinary.Node) {
|
|
ag := node.AttrGetter()
|
|
liveUpdates := node.GetChildByTag("live_updates")
|
|
cli.dispatchEvent(&events.NewsletterLiveUpdate{
|
|
JID: ag.JID("from"),
|
|
Time: ag.UnixTime("t"),
|
|
Messages: cli.parseNewsletterMessages(&liveUpdates),
|
|
})
|
|
}
|
|
|
|
type newsLetterEventWrapper struct {
|
|
Data newsletterEvent `json:"data"`
|
|
}
|
|
|
|
type newsletterEvent struct {
|
|
Join *events.NewsletterJoin `json:"xwa2_notify_newsletter_on_join"`
|
|
Leave *events.NewsletterLeave `json:"xwa2_notify_newsletter_on_leave"`
|
|
MuteChange *events.NewsletterMuteChange `json:"xwa2_notify_newsletter_on_mute_change"`
|
|
// _on_admin_metadata_update -> id, thread_metadata, messages
|
|
// _on_metadata_update
|
|
// _on_state_change -> id, is_requestor, state
|
|
}
|
|
|
|
func (cli *Client) handleMexNotification(node *waBinary.Node) {
|
|
for _, child := range node.GetChildren() {
|
|
if child.Tag != "update" {
|
|
continue
|
|
}
|
|
childData, ok := child.Content.([]byte)
|
|
if !ok {
|
|
continue
|
|
}
|
|
var wrapper newsLetterEventWrapper
|
|
err := json.Unmarshal(childData, &wrapper)
|
|
if err != nil {
|
|
cli.Log.Errorf("Failed to unmarshal JSON in mex event: %v", err)
|
|
continue
|
|
}
|
|
if wrapper.Data.Join != nil {
|
|
cli.dispatchEvent(wrapper.Data.Join)
|
|
} else if wrapper.Data.Leave != nil {
|
|
cli.dispatchEvent(wrapper.Data.Leave)
|
|
} else if wrapper.Data.MuteChange != nil {
|
|
cli.dispatchEvent(wrapper.Data.MuteChange)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 "fbid:devices":
|
|
go cli.handleFBDeviceNotification(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)
|
|
case "link_code_companion_reg":
|
|
go cli.tryHandleCodePairNotification(node)
|
|
case "newsletter":
|
|
go cli.handleNewsletterNotification(node)
|
|
case "mex":
|
|
go cli.handleMexNotification(node)
|
|
// Other types: business, disappearing_mode, server, status, pay, psa
|
|
default:
|
|
cli.Log.Debugf("Unhandled notification with type %s", notifType)
|
|
}
|
|
}
|