177 lines
4.5 KiB
Go
177 lines
4.5 KiB
Go
// 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"
|
|
"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 {
|
|
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, remoteJID, messageID, senderJID,
|
|
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 {
|
|
return extractReportingTokenContent(messageProtobuf, getReportingFields())
|
|
}
|
|
|
|
// 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
|
|
}
|