matterbridge/vendor/go.mau.fi/whatsmeow/retry.go
Wim 2f33fe86f5
Update dependencies and build to go1.22 (#2113)
* Update dependencies and build to go1.22

* Fix api changes wrt to dependencies

* Update golangci config
2024-05-23 23:44:31 +02:00

433 lines
14 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 (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"fmt"
"time"
"go.mau.fi/libsignal/ecc"
"go.mau.fi/libsignal/groups"
"go.mau.fi/libsignal/keys/prekey"
"go.mau.fi/libsignal/protocol"
"google.golang.org/protobuf/proto"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/binary/armadillo/waCommon"
"go.mau.fi/whatsmeow/binary/armadillo/waConsumerApplication"
"go.mau.fi/whatsmeow/binary/armadillo/waMsgApplication"
"go.mau.fi/whatsmeow/binary/armadillo/waMsgTransport"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
// Number of sent messages to cache in memory for handling retry receipts.
const recentMessagesSize = 256
type recentMessageKey struct {
To types.JID
ID types.MessageID
}
type RecentMessage struct {
wa *waProto.Message
fb *waMsgApplication.MessageApplication
}
func (rm RecentMessage) IsEmpty() bool {
return rm.wa == nil && rm.fb == nil
}
func (cli *Client) addRecentMessage(to types.JID, id types.MessageID, wa *waProto.Message, fb *waMsgApplication.MessageApplication) {
cli.recentMessagesLock.Lock()
key := recentMessageKey{to, id}
if cli.recentMessagesList[cli.recentMessagesPtr].ID != "" {
delete(cli.recentMessagesMap, cli.recentMessagesList[cli.recentMessagesPtr])
}
cli.recentMessagesMap[key] = RecentMessage{wa: wa, fb: fb}
cli.recentMessagesList[cli.recentMessagesPtr] = key
cli.recentMessagesPtr++
if cli.recentMessagesPtr >= len(cli.recentMessagesList) {
cli.recentMessagesPtr = 0
}
cli.recentMessagesLock.Unlock()
}
func (cli *Client) getRecentMessage(to types.JID, id types.MessageID) RecentMessage {
cli.recentMessagesLock.RLock()
msg, _ := cli.recentMessagesMap[recentMessageKey{to, id}]
cli.recentMessagesLock.RUnlock()
return msg
}
func (cli *Client) getMessageForRetry(receipt *events.Receipt, messageID types.MessageID) (RecentMessage, error) {
msg := cli.getRecentMessage(receipt.Chat, messageID)
if msg.IsEmpty() {
waMsg := cli.GetMessageForRetry(receipt.Sender, receipt.Chat, messageID)
if waMsg == nil {
return RecentMessage{}, fmt.Errorf("couldn't find message %s", messageID)
} else {
cli.Log.Debugf("Found message in GetMessageForRetry to accept retry receipt for %s/%s from %s", receipt.Chat, messageID, receipt.Sender)
}
msg = RecentMessage{wa: waMsg}
} else {
cli.Log.Debugf("Found message in local cache to accept retry receipt for %s/%s from %s", receipt.Chat, messageID, receipt.Sender)
}
return msg, nil
}
const recreateSessionTimeout = 1 * time.Hour
func (cli *Client) shouldRecreateSession(retryCount int, jid types.JID) (reason string, recreate bool) {
cli.sessionRecreateHistoryLock.Lock()
defer cli.sessionRecreateHistoryLock.Unlock()
if !cli.Store.ContainsSession(jid.SignalAddress()) {
cli.sessionRecreateHistory[jid] = time.Now()
return "we don't have a Signal session with them", true
} else if retryCount < 2 {
return "", false
}
prevTime, ok := cli.sessionRecreateHistory[jid]
if !ok || prevTime.Add(recreateSessionTimeout).Before(time.Now()) {
cli.sessionRecreateHistory[jid] = time.Now()
return "retry count > 1 and over an hour since last recreation", true
}
return "", false
}
type incomingRetryKey struct {
jid types.JID
messageID types.MessageID
}
// handleRetryReceipt handles an incoming retry receipt for an outgoing message.
func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.Node) error {
retryChild, ok := node.GetOptionalChildByTag("retry")
if !ok {
return &ElementMissingError{Tag: "retry", In: "retry receipt"}
}
ag := retryChild.AttrGetter()
messageID := ag.String("id")
timestamp := ag.UnixTime("t")
retryCount := ag.Int("count")
if !ag.OK() {
return ag.Error()
}
msg, err := cli.getMessageForRetry(receipt, messageID)
if err != nil {
return err
}
var fbConsumerMsg *waConsumerApplication.ConsumerApplication
if msg.fb != nil {
subProto, ok := msg.fb.GetPayload().GetSubProtocol().GetSubProtocol().(*waMsgApplication.MessageApplication_SubProtocolPayload_ConsumerMessage)
if ok {
fbConsumerMsg, err = subProto.Decode()
if err != nil {
return fmt.Errorf("failed to decode consumer message for retry: %w", err)
}
}
}
retryKey := incomingRetryKey{receipt.Sender, messageID}
cli.incomingRetryRequestCounterLock.Lock()
cli.incomingRetryRequestCounter[retryKey]++
internalCounter := cli.incomingRetryRequestCounter[retryKey]
cli.incomingRetryRequestCounterLock.Unlock()
if internalCounter >= 10 {
cli.Log.Warnf("Dropping retry request from %s for %s: internal retry counter is %d", messageID, receipt.Sender, internalCounter)
return nil
}
ownID := cli.getOwnID()
if ownID.IsEmpty() {
return ErrNotLoggedIn
}
var fbSKDM *waMsgTransport.MessageTransport_Protocol_Ancillary_SenderKeyDistributionMessage
var fbDSM *waMsgTransport.MessageTransport_Protocol_Integral_DeviceSentMessage
if receipt.IsGroup {
builder := groups.NewGroupSessionBuilder(cli.Store, pbSerializer)
senderKeyName := protocol.NewSenderKeyName(receipt.Chat.String(), ownID.SignalAddress())
signalSKDMessage, err := builder.Create(senderKeyName)
if err != nil {
cli.Log.Warnf("Failed to create sender key distribution message to include in retry of %s in %s to %s: %v", messageID, receipt.Chat, receipt.Sender, err)
}
if msg.wa != nil {
msg.wa.SenderKeyDistributionMessage = &waProto.SenderKeyDistributionMessage{
GroupId: proto.String(receipt.Chat.String()),
AxolotlSenderKeyDistributionMessage: signalSKDMessage.Serialize(),
}
} else {
fbSKDM = &waMsgTransport.MessageTransport_Protocol_Ancillary_SenderKeyDistributionMessage{
GroupID: receipt.Chat.String(),
AxolotlSenderKeyDistributionMessage: signalSKDMessage.Serialize(),
}
}
} else if receipt.IsFromMe {
if msg.wa != nil {
msg.wa = &waProto.Message{
DeviceSentMessage: &waProto.DeviceSentMessage{
DestinationJid: proto.String(receipt.Chat.String()),
Message: msg.wa,
},
}
} else {
fbDSM = &waMsgTransport.MessageTransport_Protocol_Integral_DeviceSentMessage{
DestinationJID: receipt.Chat.String(),
}
}
}
// TODO pre-retry callback for fb
if cli.PreRetryCallback != nil && !cli.PreRetryCallback(receipt, messageID, retryCount, msg.wa) {
cli.Log.Debugf("Cancelled retry receipt in PreRetryCallback")
return nil
}
var plaintext, frankingTag []byte
if msg.wa != nil {
plaintext, err = proto.Marshal(msg.wa)
if err != nil {
return fmt.Errorf("failed to marshal message: %w", err)
}
} else {
plaintext, err = proto.Marshal(msg.fb)
if err != nil {
return fmt.Errorf("failed to marshal consumer message: %w", err)
}
frankingHash := hmac.New(sha256.New, msg.fb.GetMetadata().GetFrankingKey())
frankingHash.Write(plaintext)
frankingTag = frankingHash.Sum(nil)
}
_, hasKeys := node.GetOptionalChildByTag("keys")
var bundle *prekey.Bundle
if hasKeys {
bundle, err = nodeToPreKeyBundle(uint32(receipt.Sender.Device), *node)
if err != nil {
return fmt.Errorf("failed to read prekey bundle in retry receipt: %w", err)
}
} else if reason, recreate := cli.shouldRecreateSession(retryCount, receipt.Sender); recreate {
cli.Log.Debugf("Fetching prekeys for %s for handling retry receipt with no prekey bundle because %s", receipt.Sender, reason)
var keys map[types.JID]preKeyResp
keys, err = cli.fetchPreKeys(context.TODO(), []types.JID{receipt.Sender})
if err != nil {
return err
}
bundle, err = keys[receipt.Sender].bundle, keys[receipt.Sender].err
if err != nil {
return fmt.Errorf("failed to fetch prekeys: %w", err)
} else if bundle == nil {
return fmt.Errorf("didn't get prekey bundle for %s (response size: %d)", receipt.Sender, len(keys))
}
}
encAttrs := waBinary.Attrs{}
var msgAttrs messageAttrs
if msg.wa != nil {
msgAttrs.MediaType = getMediaTypeFromMessage(msg.wa)
msgAttrs.Type = getTypeFromMessage(msg.wa)
} else if fbConsumerMsg != nil {
msgAttrs = getAttrsFromFBMessage(fbConsumerMsg)
} else {
msgAttrs.Type = "text"
}
if msgAttrs.MediaType != "" {
encAttrs["mediatype"] = msgAttrs.MediaType
}
var encrypted *waBinary.Node
var includeDeviceIdentity bool
if msg.wa != nil {
encrypted, includeDeviceIdentity, err = cli.encryptMessageForDevice(plaintext, receipt.Sender, bundle, encAttrs)
} else {
encrypted, err = cli.encryptMessageForDeviceV3(&waMsgTransport.MessageTransport_Payload{
ApplicationPayload: &waCommon.SubProtocol{
Payload: plaintext,
Version: FBMessageApplicationVersion,
},
FutureProof: waCommon.FutureProofBehavior_PLACEHOLDER,
}, fbSKDM, fbDSM, receipt.Sender, bundle, encAttrs)
}
if err != nil {
return fmt.Errorf("failed to encrypt message for retry: %w", err)
}
encrypted.Attrs["count"] = retryCount
attrs := waBinary.Attrs{
"to": node.Attrs["from"],
"type": msgAttrs.Type,
"id": messageID,
"t": timestamp.Unix(),
}
if !receipt.IsGroup {
attrs["device_fanout"] = false
}
if participant, ok := node.Attrs["participant"]; ok {
attrs["participant"] = participant
}
if recipient, ok := node.Attrs["recipient"]; ok {
attrs["recipient"] = recipient
}
if edit, ok := node.Attrs["edit"]; ok {
attrs["edit"] = edit
}
var content []waBinary.Node
if msg.wa != nil {
content = cli.getMessageContent(*encrypted, msg.wa, attrs, includeDeviceIdentity)
} else {
content = []waBinary.Node{
*encrypted,
{Tag: "franking", Content: []waBinary.Node{{Tag: "franking_tag", Content: frankingTag}}},
}
}
err = cli.sendNode(waBinary.Node{
Tag: "message",
Attrs: attrs,
Content: content,
})
if err != nil {
return fmt.Errorf("failed to send retry message: %w", err)
}
cli.Log.Debugf("Sent retry #%d for %s/%s to %s", retryCount, receipt.Chat, messageID, receipt.Sender)
return nil
}
func (cli *Client) cancelDelayedRequestFromPhone(msgID types.MessageID) {
if !cli.AutomaticMessageRerequestFromPhone || cli.MessengerConfig != nil {
return
}
cli.pendingPhoneRerequestsLock.RLock()
cancelPendingRequest, ok := cli.pendingPhoneRerequests[msgID]
if ok {
cancelPendingRequest()
}
cli.pendingPhoneRerequestsLock.RUnlock()
}
// RequestFromPhoneDelay specifies how long to wait for the sender to resend the message before requesting from your phone.
// This is only used if Client.AutomaticMessageRerequestFromPhone is true.
var RequestFromPhoneDelay = 5 * time.Second
func (cli *Client) delayedRequestMessageFromPhone(info *types.MessageInfo) {
if !cli.AutomaticMessageRerequestFromPhone || cli.MessengerConfig != nil {
return
}
cli.pendingPhoneRerequestsLock.Lock()
_, alreadyRequesting := cli.pendingPhoneRerequests[info.ID]
if alreadyRequesting {
cli.pendingPhoneRerequestsLock.Unlock()
return
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cli.pendingPhoneRerequests[info.ID] = cancel
cli.pendingPhoneRerequestsLock.Unlock()
defer func() {
cli.pendingPhoneRerequestsLock.Lock()
delete(cli.pendingPhoneRerequests, info.ID)
cli.pendingPhoneRerequestsLock.Unlock()
}()
select {
case <-time.After(RequestFromPhoneDelay):
case <-ctx.Done():
cli.Log.Debugf("Cancelled delayed request for message %s from phone", info.ID)
return
}
_, err := cli.SendMessage(
ctx,
cli.getOwnID().ToNonAD(),
cli.BuildUnavailableMessageRequest(info.Chat, info.Sender, info.ID),
SendRequestExtra{Peer: true},
)
if err != nil {
cli.Log.Warnf("Failed to send request for unavailable message %s to phone: %v", info.ID, err)
} else {
cli.Log.Debugf("Requested message %s from phone", info.ID)
}
}
// sendRetryReceipt sends a retry receipt for an incoming message.
func (cli *Client) sendRetryReceipt(node *waBinary.Node, info *types.MessageInfo, forceIncludeIdentity bool) {
id, _ := node.Attrs["id"].(string)
children := node.GetChildren()
var retryCountInMsg int
if len(children) == 1 && children[0].Tag == "enc" {
retryCountInMsg = children[0].AttrGetter().OptionalInt("count")
}
cli.messageRetriesLock.Lock()
cli.messageRetries[id]++
retryCount := cli.messageRetries[id]
// In case the message is a retry response, and we restarted in between, find the count from the message
if retryCount == 1 && retryCountInMsg > 0 {
retryCount = retryCountInMsg + 1
cli.messageRetries[id] = retryCount
}
cli.messageRetriesLock.Unlock()
if retryCount >= 5 {
cli.Log.Warnf("Not sending any more retry receipts for %s", id)
return
}
if retryCount == 1 {
go cli.delayedRequestMessageFromPhone(info)
}
var registrationIDBytes [4]byte
binary.BigEndian.PutUint32(registrationIDBytes[:], cli.Store.RegistrationID)
attrs := waBinary.Attrs{
"id": id,
"type": "retry",
"to": node.Attrs["from"],
}
if recipient, ok := node.Attrs["recipient"]; ok {
attrs["recipient"] = recipient
}
if participant, ok := node.Attrs["participant"]; ok {
attrs["participant"] = participant
}
payload := waBinary.Node{
Tag: "receipt",
Attrs: attrs,
Content: []waBinary.Node{
{Tag: "retry", Attrs: waBinary.Attrs{
"count": retryCount,
"id": id,
"t": node.Attrs["t"],
"v": 1,
}},
{Tag: "registration", Content: registrationIDBytes[:]},
},
}
if retryCount > 1 || forceIncludeIdentity {
if key, err := cli.Store.PreKeys.GenOnePreKey(); err != nil {
cli.Log.Errorf("Failed to get prekey for retry receipt: %v", err)
} else if deviceIdentity, err := proto.Marshal(cli.Store.Account); err != nil {
cli.Log.Errorf("Failed to marshal account info: %v", err)
return
} else {
payload.Content = append(payload.GetChildren(), waBinary.Node{
Tag: "keys",
Content: []waBinary.Node{
{Tag: "type", Content: []byte{ecc.DjbType}},
{Tag: "identity", Content: cli.Store.IdentityKey.Pub[:]},
preKeyToNode(key),
preKeyToNode(cli.Store.SignedPreKey),
{Tag: "device-identity", Content: deviceIdentity},
},
})
}
}
err := cli.sendNode(payload)
if err != nil {
cli.Log.Errorf("Failed to send retry receipt for %s: %v", id, err)
}
}