538 lines
18 KiB
Go
538 lines
18 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"
|
|
"runtime/debug"
|
|
"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/proto/waCommon"
|
|
"go.mau.fi/whatsmeow/proto/waConsumerApplication"
|
|
"go.mau.fi/whatsmeow/proto/waE2E"
|
|
"go.mau.fi/whatsmeow/proto/waMsgApplication"
|
|
"go.mau.fi/whatsmeow/proto/waMsgTransport"
|
|
"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 *waE2E.Message
|
|
fb *waMsgApplication.MessageApplication
|
|
}
|
|
|
|
func (rm RecentMessage) IsEmpty() bool {
|
|
return rm.wa == nil && rm.fb == nil
|
|
}
|
|
|
|
func (cli *Client) addRecentMessage(ctx context.Context, to types.JID, id types.MessageID, wa *waE2E.Message, fb *waMsgApplication.MessageApplication) error {
|
|
if cli.UseRetryMessageStore {
|
|
var buf []byte
|
|
var format string
|
|
var err error
|
|
if wa != nil {
|
|
buf, err = proto.Marshal(wa)
|
|
format = "wa"
|
|
} else if fb != nil {
|
|
buf, err = proto.Marshal(fb)
|
|
format = "fb"
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal message for retry store: %w", err)
|
|
}
|
|
if buf != nil {
|
|
err = cli.Store.EventBuffer.AddOutgoingEvent(ctx, to, id, format, buf)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add message to retry store: %w", err)
|
|
}
|
|
if time.Since(cli.lastRetryStoreClear) > 12*time.Hour {
|
|
err = cli.Store.EventBuffer.DeleteOldOutgoingEvents(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to clear old messages from retry store: %w", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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()
|
|
return nil
|
|
}
|
|
|
|
func (cli *Client) getRecentMessage(to types.JID, id types.MessageID) RecentMessage {
|
|
cli.recentMessagesLock.RLock()
|
|
defer cli.recentMessagesLock.RUnlock()
|
|
return cli.recentMessagesMap[recentMessageKey{to, id}]
|
|
}
|
|
|
|
func (cli *Client) getMessageForRetry(ctx context.Context, receipt *events.Receipt, messageID types.MessageID) (*RecentMessage, error) {
|
|
msg := cli.getRecentMessage(receipt.Chat, messageID)
|
|
if !msg.IsEmpty() {
|
|
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
|
|
}
|
|
var altChat types.JID
|
|
var err error
|
|
switch receipt.Chat.Server {
|
|
case types.DefaultUserServer:
|
|
altChat, err = cli.Store.LIDs.GetLIDForPN(ctx, receipt.Chat)
|
|
case types.HiddenUserServer:
|
|
altChat, err = cli.Store.LIDs.GetPNForLID(ctx, receipt.Chat)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get alternate JID for %s: %w", receipt.Chat, err)
|
|
} else if !altChat.IsEmpty() {
|
|
msg = cli.getRecentMessage(altChat, messageID)
|
|
if !msg.IsEmpty() {
|
|
cli.Log.Debugf("Found message in local cache with alternate chat JID %s to accept retry receipt for %s/%s from %s", altChat, receipt.Chat, messageID, receipt.Sender)
|
|
return &msg, nil
|
|
}
|
|
}
|
|
if cli.UseRetryMessageStore {
|
|
format, buf, err := cli.Store.EventBuffer.GetOutgoingEvent(ctx, receipt.Chat, altChat, messageID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get message from retry store: %w", err)
|
|
}
|
|
return parseRecentMessage(format, buf)
|
|
}
|
|
waMsg := cli.GetMessageForRetry(receipt.Sender, receipt.Chat, messageID)
|
|
if waMsg != nil {
|
|
cli.Log.Debugf("Found message in GetMessageForRetry to accept retry receipt for %s/%s from %s", receipt.Chat, messageID, receipt.Sender)
|
|
return &RecentMessage{wa: waMsg}, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func parseRecentMessage(format string, buf []byte) (*RecentMessage, error) {
|
|
var rm RecentMessage
|
|
var err error
|
|
switch format {
|
|
case "wa":
|
|
rm.wa = &waE2E.Message{}
|
|
err = proto.Unmarshal(buf, rm.wa)
|
|
case "fb":
|
|
rm.fb = &waMsgApplication.MessageApplication{}
|
|
err = proto.Unmarshal(buf, rm.fb)
|
|
default:
|
|
err = fmt.Errorf("unknown format in retry store: %s", format)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal payload in retry store: %w", err)
|
|
}
|
|
return &rm, nil
|
|
}
|
|
|
|
const recreateSessionTimeout = 1 * time.Hour
|
|
|
|
func (cli *Client) shouldRecreateSession(ctx context.Context, retryCount int, jid types.JID) (reason string, recreate bool) {
|
|
cli.sessionRecreateHistoryLock.Lock()
|
|
defer cli.sessionRecreateHistoryLock.Unlock()
|
|
if contains, err := cli.Store.ContainsSession(ctx, jid.SignalAddress()); err != nil {
|
|
return "", false
|
|
} else if !contains {
|
|
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
|
|
}
|
|
|
|
func (cli *Client) tryHandleRetryReceipt(ctx context.Context, receipt *events.Receipt, node *waBinary.Node) {
|
|
defer func() {
|
|
err := recover()
|
|
if err != nil {
|
|
cli.Log.Errorf("Retry receipt handler panicked: %v\n%s", err, debug.Stack())
|
|
}
|
|
}()
|
|
if cli.retrySema != nil {
|
|
err := cli.retrySema.Acquire(ctx, 1)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer cli.retrySema.Release(1)
|
|
}
|
|
err := cli.handleRetryReceipt(ctx, receipt, node)
|
|
if err != nil {
|
|
cli.Log.Errorf("Failed to handle retry receipt for %s/%s from %s: %v", receipt.Chat, receipt.MessageIDs[0], receipt.Sender, err)
|
|
}
|
|
}
|
|
|
|
// handleRetryReceipt handles an incoming retry receipt for an outgoing message.
|
|
func (cli *Client) handleRetryReceipt(ctx context.Context, 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(ctx, receipt, messageID)
|
|
if err != nil {
|
|
return err
|
|
} else if msg == nil {
|
|
return fmt.Errorf("couldn't find message %s", messageID)
|
|
}
|
|
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
|
|
}
|
|
|
|
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(), cli.getOwnLID().SignalAddress())
|
|
signalSKDMessage, err := builder.Create(ctx, 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)
|
|
} else if msg.wa != nil {
|
|
msg.wa.SenderKeyDistributionMessage = &waE2E.SenderKeyDistributionMessage{
|
|
GroupID: proto.String(receipt.Chat.String()),
|
|
AxolotlSenderKeyDistributionMessage: signalSKDMessage.Serialize(),
|
|
}
|
|
} else {
|
|
fbSKDM = &waMsgTransport.MessageTransport_Protocol_Ancillary_SenderKeyDistributionMessage{
|
|
GroupID: proto.String(receipt.Chat.String()),
|
|
AxolotlSenderKeyDistributionMessage: signalSKDMessage.Serialize(),
|
|
}
|
|
}
|
|
} else if receipt.IsFromMe {
|
|
if msg.wa != nil {
|
|
msg.wa = &waE2E.Message{
|
|
DeviceSentMessage: &waE2E.DeviceSentMessage{
|
|
DestinationJID: proto.String(receipt.Chat.String()),
|
|
Message: msg.wa,
|
|
},
|
|
}
|
|
} else {
|
|
fbDSM = &waMsgTransport.MessageTransport_Protocol_Integral_DeviceSentMessage{
|
|
DestinationJID: proto.String(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(ctx, 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(ctx, []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 {
|
|
encryptionIdentity := receipt.Sender
|
|
if receipt.Sender.Server == types.DefaultUserServer {
|
|
lidForPN, err := cli.Store.LIDs.GetLIDForPN(ctx, receipt.Sender)
|
|
if err != nil {
|
|
cli.Log.Warnf("Failed to get LID for %s: %v", receipt.Sender, err)
|
|
} else if !lidForPN.IsEmpty() {
|
|
cli.migrateSessionStore(ctx, receipt.Sender, lidForPN)
|
|
encryptionIdentity = lidForPN
|
|
}
|
|
}
|
|
encrypted, includeDeviceIdentity, err = cli.encryptMessageForDevice(ctx, plaintext, encryptionIdentity, bundle, encAttrs, nil)
|
|
} else {
|
|
encrypted, err = cli.encryptMessageForDeviceV3(ctx, &waMsgTransport.MessageTransport_Payload{
|
|
ApplicationPayload: &waCommon.SubProtocol{
|
|
Payload: plaintext,
|
|
Version: proto.Int32(FBMessageApplicationVersion),
|
|
},
|
|
FutureProof: waCommon.FutureProofBehavior_PLACEHOLDER.Enum(),
|
|
}, 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, nodeExtraParams{},
|
|
)
|
|
} else {
|
|
content = []waBinary.Node{
|
|
*encrypted,
|
|
{Tag: "franking", Content: []waBinary.Node{{Tag: "franking_tag", Content: frankingTag}}},
|
|
}
|
|
}
|
|
err = cli.sendNode(ctx, 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(cli.BackgroundEventCtx)
|
|
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
|
|
}
|
|
cli.immediateRequestMessageFromPhone(ctx, info)
|
|
}
|
|
|
|
func (cli *Client) immediateRequestMessageFromPhone(ctx context.Context, info *types.MessageInfo) {
|
|
_, err := cli.SendPeerMessage(ctx, cli.BuildUnavailableMessageRequest(info.Chat, info.Sender, info.ID))
|
|
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)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (cli *Client) clearDelayedMessageRequests() {
|
|
cli.pendingPhoneRerequestsLock.Lock()
|
|
defer cli.pendingPhoneRerequestsLock.Unlock()
|
|
for _, cancel := range cli.pendingPhoneRerequests {
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
// sendRetryReceipt sends a retry receipt for an incoming message.
|
|
func (cli *Client) sendRetryReceipt(ctx context.Context, 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 {
|
|
if cli.SynchronousAck {
|
|
cli.immediateRequestMessageFromPhone(ctx, info)
|
|
} else {
|
|
go cli.delayedRequestMessageFromPhone(info)
|
|
}
|
|
}
|
|
|
|
var registrationIDBytes [4]byte
|
|
binary.BigEndian.PutUint32(registrationIDBytes[:], cli.Store.RegistrationID)
|
|
attrs := buildBaseReceipt(info.ID, node)
|
|
attrs["type"] = "retry"
|
|
if info.Type == "peer_msg" && info.IsFromMe {
|
|
attrs["category"] = "peer"
|
|
}
|
|
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(ctx); 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(ctx, payload)
|
|
if err != nil {
|
|
cli.Log.Errorf("Failed to send retry receipt for %s: %v", id, err)
|
|
}
|
|
}
|