send: add support for reporting tokens

This commit is contained in:
Tulir Asokan
2025-06-05 16:44:36 +03:00
parent 9cd01960b5
commit 66acc15b3d
5 changed files with 201 additions and 4 deletions
+3
View File
@@ -161,6 +161,8 @@ type Client struct {
// Should SubscribePresence return an error if no privacy token is stored for the user?
ErrorOnSubscribePresenceWithoutToken bool
SendReportingTokens bool
BackgroundEventCtx context.Context
phoneLinkingCache *phoneLinkingCache
@@ -253,6 +255,7 @@ func NewClient(deviceStore *store.Device, log waLog.Logger) *Client {
EnableAutoReconnect: true,
AutoTrustIdentity: true,
SendReportingTokens: true,
BackgroundEventCtx: context.Background(),
}
Generated
+6 -2
View File
@@ -607,8 +607,8 @@ func (int *DangerousInternalClient) SendNewsletter(to types.JID, id types.Messag
return int.c.sendNewsletter(to, id, message, mediaID, timings)
}
func (int *DangerousInternalClient) SendGroup(ctx context.Context, to types.JID, participants []types.JID, id types.MessageID, message *waE2E.Message, timings *MessageDebugTimings, extraParams nodeExtraParams) (string, []byte, error) {
return int.c.sendGroup(ctx, to, participants, id, message, timings, extraParams)
func (int *DangerousInternalClient) SendGroup(ctx context.Context, ownID, to types.JID, participants []types.JID, id types.MessageID, message *waE2E.Message, timings *MessageDebugTimings, extraParams nodeExtraParams) (string, []byte, error) {
return int.c.sendGroup(ctx, ownID, to, participants, id, message, timings, extraParams)
}
func (int *DangerousInternalClient) SendPeerMessage(ctx context.Context, to types.JID, id types.MessageID, message *waE2E.Message, timings *MessageDebugTimings) ([]byte, error) {
@@ -623,6 +623,10 @@ func (int *DangerousInternalClient) PreparePeerMessageNode(ctx context.Context,
return int.c.preparePeerMessageNode(ctx, to, id, message, timings)
}
func (int *DangerousInternalClient) GetMessageReportingToken(msgProtobuf []byte, msg *waE2E.Message, senderJID, remoteJID types.JID, messageID types.MessageID) waBinary.Node {
return int.c.getMessageReportingToken(msgProtobuf, msg, senderJID, remoteJID, messageID)
}
func (int *DangerousInternalClient) GetMessageContent(baseNode waBinary.Node, message *waE2E.Message, msgAttrs waBinary.Attrs, includeIdentity bool, extraParams nodeExtraParams) []waBinary.Node {
return int.c.getMessageContent(baseNode, message, msgAttrs, includeIdentity, extraParams)
}
+1
View File
@@ -0,0 +1 @@
[{"f":1},{"f":3,"s":[{"f":2},{"f":3},{"f":8},{"f":11},{"f":17,"s":[{"f":21},{"f":22}]},{"f":25}]},{"f":4,"s":[{"f":1},{"f":16},{"f":17,"s":[{"f":21},{"f":22}]}]},{"f":5,"s":[{"f":3},{"f":4},{"f":5},{"f":16},{"f":17,"s":[{"f":21},{"f":22}]}]},{"f":6,"s":[{"f":1},{"f":17,"s":[{"f":21},{"f":22}]},{"f":30}]},{"f":7,"s":[{"f":2},{"f":7},{"f":10},{"f":17,"s":[{"f":21},{"f":22}]},{"f":20}]},{"f":8,"s":[{"f":2},{"f":7},{"f":9},{"f":17,"s":[{"f":21},{"f":22}]},{"f":21}]},{"f":9,"s":[{"f":2},{"f":6},{"f":7},{"f":13},{"f":17,"s":[{"f":21},{"f":22}]},{"f":20}]},{"f":12,"s":[{"f":1},{"f":2},{"f":14,"m":true},{"f":15}]},{"f":18,"s":[{"f":6},{"f":16},{"f":17,"s":[{"f":21},{"f":22}]}]},{"f":26,"s":[{"f":4},{"f":5},{"f":8},{"f":13},{"f":17,"s":[{"f":21},{"f":22}]}]},{"f":28,"s":[{"f":1},{"f":2},{"f":4},{"f":5},{"f":6},{"f":7,"s":[{"f":21},{"f":22}]}]},{"f":37,"s":[{"f":1,"m":true}]},{"f":49,"s":[{"f":2},{"f":3,"s":[{"f":1},{"f":2}]},{"f":5,"s":[{"f":21},{"f":22}]},{"f":8,"s":[{"f":1},{"f":2}]}]},{"f":53,"s":[{"f":1,"m":true}]},{"f":55,"s":[{"f":1,"m":true}]},{"f":58,"s":[{"f":1,"m":true}]},{"f":59,"s":[{"f":1,"m":true}]},{"f":60,"s":[{"f":2},{"f":3,"s":[{"f":1},{"f":2}]},{"f":5,"s":[{"f":21},{"f":22}]},{"f":8,"s":[{"f":1},{"f":2}]}]},{"f":64,"s":[{"f":2},{"f":3,"s":[{"f":1},{"f":2}]},{"f":5,"s":[{"f":21},{"f":22}]},{"f":8,"s":[{"f":1},{"f":2}]}]},{"f":66,"s":[{"f":2},{"f":6},{"f":7},{"f":13},{"f":17,"s":[{"f":21},{"f":22}]},{"f":20}]},{"f":74,"s":[{"f":1,"m":true}]},{"f":87,"s":[{"f":1,"m":true}]},{"f":88,"s":[{"f":1},{"f":2,"s":[{"f":1}]},{"f":3,"s":[{"f":21},{"f":22}]}]},{"f":92,"s":[{"f":1,"m":true}]},{"f":93,"s":[{"f":1,"m":true}]},{"f":94,"s":[{"f":1,"m":true}]}]
+181
View File
@@ -0,0 +1,181 @@
// Copyright (c) 2025 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 (
"crypto/hmac"
"crypto/sha256"
_ "embed"
"encoding/binary"
"encoding/json"
"fmt"
"sort"
"sync"
"go.mau.fi/util/exerrors"
"go.mau.fi/util/exstrings"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/types"
)
//go:embed reportingfields.json
var reportingFieldsJSON string
var getReportingFields = sync.OnceValue(func() (output []reportingField) {
exerrors.PanicIfNotNil(json.Unmarshal(exstrings.UnsafeBytes(reportingFieldsJSON), &output))
return
})
type reportingField struct {
FieldNumber int `json:"f"`
IsMessage bool `json:"m,omitempty"`
Subfields []reportingField `json:"s,omitempty"`
}
func (cli *Client) shouldIncludeReportingToken(message *waE2E.Message) bool {
if !cli.SendReportingTokens || message.GetMessageContextInfo().GetMessageSecret() == nil {
return false
}
return message.ReactionMessage == nil &&
message.EncReactionMessage == nil &&
message.EncEventResponseMessage == nil &&
message.PollUpdateMessage == nil
}
func (cli *Client) getMessageReportingToken(
msgProtobuf []byte,
msg *waE2E.Message,
senderJID, remoteJID types.JID,
messageID types.MessageID,
) waBinary.Node {
reportingSecret, _ := generateMsgSecretKey(
EncSecretReportToken, senderJID, messageID, remoteJID,
msg.GetMessageContextInfo().GetMessageSecret(),
)
hasher := hmac.New(sha256.New, reportingSecret)
hasher.Write(getReportingToken(msgProtobuf))
return waBinary.Node{
Tag: "reporting",
Content: []waBinary.Node{{
Tag: "reporting_token",
Attrs: waBinary.Attrs{"v": "2"},
Content: hasher.Sum(nil)[:16],
}},
}
}
func getReportingToken(messageProtobuf []byte) []byte {
tok := extractReportingTokenContent(messageProtobuf, getReportingFields())
fmt.Println("TOKEN:")
fmt.Println(string(tok))
fmt.Printf("%x\n", tok)
return tok
}
// Helper to find config for a field number
func getConfigForField(fields []reportingField, fieldNum int) *reportingField {
for i := range fields {
if fields[i].FieldNumber == fieldNum {
return &fields[i]
}
}
return nil
}
// Protobuf wire types
const (
wireVarint = 0
wire64bit = 1
wireBytes = 2
wire32bit = 5
)
// Extracts the reporting token content recursively
func extractReportingTokenContent(data []byte, config []reportingField) []byte {
type field struct {
Num int
Bytes []byte
}
var fields []field
i := 0
for i < len(data) {
// Read tag (varint)
tag, tagLen := binary.Uvarint(data[i:])
if tagLen <= 0 {
break // malformed
}
fieldNum := int(tag >> 3)
wireType := int(tag & 0x7)
fieldCfg := getConfigForField(config, fieldNum)
fieldStart := i
i += tagLen
if fieldCfg == nil {
// Skip field
switch wireType {
case wireVarint:
_, n := binary.Uvarint(data[i:])
i += n
case wire64bit:
i += 8
case wireBytes:
l, n := binary.Uvarint(data[i:])
i += n + int(l)
case wire32bit:
i += 4
default:
return nil
}
continue
}
switch wireType {
case wireVarint:
_, n := binary.Uvarint(data[i:])
i += n
fields = append(fields, field{Num: fieldNum, Bytes: data[fieldStart:i]})
case wire64bit:
i += 8
fields = append(fields, field{Num: fieldNum, Bytes: data[fieldStart:i]})
case wireBytes:
l, n := binary.Uvarint(data[i:])
valStart := i + n
valEnd := valStart + int(l)
if fieldCfg.IsMessage || len(fieldCfg.Subfields) > 0 {
// Recursively extract subfields
sub := extractReportingTokenContent(data[valStart:valEnd], fieldCfg.Subfields)
if len(sub) > 0 {
// Re-encode tag and length
buf := make([]byte, 0, tagLen+n+len(sub))
tagBuf := make([]byte, binary.MaxVarintLen64)
tagN := binary.PutUvarint(tagBuf, tag)
lenBuf := make([]byte, binary.MaxVarintLen64)
lenN := binary.PutUvarint(lenBuf, uint64(len(sub)))
buf = append(buf, tagBuf[:tagN]...)
buf = append(buf, lenBuf[:lenN]...)
buf = append(buf, sub...)
fields = append(fields, field{Num: fieldNum, Bytes: buf})
}
} else {
fields = append(fields, field{Num: fieldNum, Bytes: data[fieldStart:valEnd]})
}
i = valEnd
case wire32bit:
i += 4
fields = append(fields, field{Num: fieldNum, Bytes: data[fieldStart:i]})
default:
return nil
}
}
// Sort by field number
sort.Slice(fields, func(i, j int) bool { return fields[i].Num < fields[j].Num })
// Concatenate
var out []byte
for _, f := range fields {
out = append(out, f.Bytes...)
}
return out
}
+10 -2
View File
@@ -224,7 +224,7 @@ func (cli *Client) SendMessage(ctx context.Context, to types.JID, message *waE2E
}
isBotMode := isInlineBotMode || to.IsBot()
needsMessageSecret := isBotMode
needsMessageSecret := isBotMode || cli.shouldIncludeReportingToken(message)
var extraParams nodeExtraParams
if needsMessageSecret {
@@ -361,7 +361,7 @@ func (cli *Client) SendMessage(ctx context.Context, to types.JID, message *waE2E
var data []byte
switch to.Server {
case types.GroupServer, types.BroadcastServer:
phash, data, err = cli.sendGroup(ctx, to, groupParticipants, req.ID, message, &resp.DebugTimings, extraParams)
phash, data, err = cli.sendGroup(ctx, ownID, to, groupParticipants, req.ID, message, &resp.DebugTimings, extraParams)
case types.DefaultUserServer, types.BotServer, types.HiddenUserServer:
if req.Peer {
data, err = cli.sendPeerMessage(ctx, to, req.ID, message, &resp.DebugTimings)
@@ -688,6 +688,7 @@ type nodeExtraParams struct {
func (cli *Client) sendGroup(
ctx context.Context,
ownID,
to types.JID,
participants []types.JID,
id types.MessageID,
@@ -746,6 +747,9 @@ func (cli *Client) sendGroup(
skMsg.Attrs["mediatype"] = mediaType
}
node.Content = append(node.GetChildren(), skMsg)
if cli.shouldIncludeReportingToken(message) {
node.Content = append(node.GetChildren(), cli.getMessageReportingToken(plaintext, message, ownID, to, id))
}
start = time.Now()
data, err := cli.sendNodeAndGetData(*node)
@@ -800,6 +804,10 @@ func (cli *Client) sendDM(
return nil, err
}
if cli.shouldIncludeReportingToken(message) {
node.Content = append(node.GetChildren(), cli.getMessageReportingToken(messagePlaintext, message, ownID, to, id))
}
if tcToken, err := cli.Store.PrivacyTokens.GetPrivacyToken(ctx, to); err != nil {
cli.Log.Warnf("Failed to get privacy token for %s: %v", to, err)
} else if tcToken != nil {