send: add support for reporting tokens
This commit is contained in:
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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}]}]
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user