implement message reporting tokens (#1906)
* feat: implement message reporting tokens and privacy token handling * feat: add support for privacy tokens in profile picture requests and history sync * chore: pr feedback purpshell * fix: improve privacy token handling and error messaging in socket configuration * feat: enhance privacy token handling with improved sender mapping * chore: removing tc token in favor of #2080 * chore: revert some unecessary changes * feat(reporting): enhance reporting token extraction and compilation logic * feat(reporting): add unit tests for reporting token utilities * fix(reporting): streamline reporting token attachment logic in message sending * fix: adjust reporting token inclusion logic to prevent retries * chore: add return type to shouldIncludeReportingToken and improve getToken function type safety
This commit is contained in:
committed by
GitHub
parent
4e681d3219
commit
c9c3481817
@@ -35,6 +35,7 @@ import {
|
||||
} from '../Utils'
|
||||
import { getUrlInfo } from '../Utils/link-preview'
|
||||
import { makeKeyedMutex } from '../Utils/make-mutex'
|
||||
import { getMessageReportingToken, shouldIncludeReportingToken } from '../Utils/reporting-utils'
|
||||
import {
|
||||
areJidsSameUser,
|
||||
type BinaryNode,
|
||||
@@ -624,6 +625,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
const destinationJid = !isStatus ? finalJid : statusJid
|
||||
const binaryNodeContent: BinaryNode[] = []
|
||||
const devices: DeviceWithJid[] = []
|
||||
let reportingMessage: proto.IMessage | undefined
|
||||
|
||||
const meMsg: proto.IMessage = {
|
||||
deviceSentMessage: {
|
||||
@@ -734,6 +736,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
}
|
||||
|
||||
const bytes = encodeWAMessage(patched)
|
||||
reportingMessage = patched
|
||||
const groupAddressingMode = additionalAttributes?.['addressing_mode'] || groupData?.addressingMode || 'lid'
|
||||
const groupSenderIdentity = groupAddressingMode === 'lid' && meLid ? meLid : meId
|
||||
|
||||
@@ -798,6 +801,12 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
}
|
||||
|
||||
const { user: ownUser } = jidDecode(ownId)!
|
||||
if (!participant) {
|
||||
const patchedForReporting = await patchMessageBeforeSending(message, [jid])
|
||||
reportingMessage = Array.isArray(patchedForReporting)
|
||||
? patchedForReporting.find(item => item.recipientJid === jid) || patchedForReporting[0]
|
||||
: patchedForReporting
|
||||
}
|
||||
|
||||
if (!isRetryResend) {
|
||||
const targetUserServer = isLid ? 'lid' : 's.whatsapp.net'
|
||||
@@ -970,6 +979,30 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
logger.debug({ jid }, 'adding device identity')
|
||||
}
|
||||
|
||||
if (
|
||||
!isNewsletter &&
|
||||
!isRetryResend &&
|
||||
reportingMessage?.messageContextInfo?.messageSecret &&
|
||||
shouldIncludeReportingToken(reportingMessage)
|
||||
) {
|
||||
try {
|
||||
const encoded = encodeWAMessage(reportingMessage)
|
||||
const reportingKey: WAMessageKey = {
|
||||
id: msgId,
|
||||
fromMe: true,
|
||||
remoteJid: destinationJid,
|
||||
participant: participant?.jid
|
||||
}
|
||||
const reportingNode = await getMessageReportingToken(encoded, reportingMessage, reportingKey)
|
||||
if (reportingNode) {
|
||||
;(stanza.content as BinaryNode[]).push(reportingNode)
|
||||
logger.trace({ jid }, 'added reporting token to message')
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.warn({ jid, trace: error?.stack }, 'failed to attach reporting token')
|
||||
}
|
||||
}
|
||||
|
||||
const contactTcTokenData =
|
||||
!isGroup && !isRetryResend && !isStatus ? await authState.keys.get('tctoken', [destinationJid]) : {}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
getRawMediaUploadData,
|
||||
type MediaDownloadOptions
|
||||
} from './messages-media'
|
||||
import { shouldIncludeReportingToken } from './reporting-utils'
|
||||
|
||||
type MediaUploadData = {
|
||||
media: WAMediaUpload
|
||||
@@ -617,6 +618,13 @@ export const generateWAMessageContent = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldIncludeReportingToken(m)) {
|
||||
m.messageContextInfo = m.messageContextInfo || {}
|
||||
if (!m.messageContextInfo.messageSecret) {
|
||||
m.messageContextInfo.messageSecret = randomBytes(32)
|
||||
}
|
||||
}
|
||||
|
||||
return WAProto.Message.create(m)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
import { createHmac } from 'crypto'
|
||||
import { proto } from '../../WAProto/index.js'
|
||||
import type { WAMessageContent, WAMessageKey } from '../Types'
|
||||
import type { BinaryNode } from '../WABinary'
|
||||
import { hkdf } from './crypto'
|
||||
|
||||
export type ReportingField = {
|
||||
f: number
|
||||
m?: boolean
|
||||
s?: ReportingField[]
|
||||
}
|
||||
|
||||
type CompiledReportingField = {
|
||||
m?: boolean
|
||||
children?: Map<number, CompiledReportingField>
|
||||
}
|
||||
|
||||
const reportingFields: ReportingField[] = [
|
||||
{ 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 }] }
|
||||
]
|
||||
|
||||
const compileReportingFields = (fields: readonly ReportingField[]): Map<number, CompiledReportingField> => {
|
||||
const map = new Map<number, CompiledReportingField>()
|
||||
for (const f of fields) {
|
||||
map.set(f.f, {
|
||||
m: f.m,
|
||||
children: f.s ? compileReportingFields(f.s) : undefined
|
||||
})
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
const compiledReportingFields = compileReportingFields(reportingFields)
|
||||
const EMPTY_MAP: Map<number, CompiledReportingField> = new Map()
|
||||
|
||||
const ENC_SECRET_REPORT_TOKEN = 'Report Token'
|
||||
|
||||
const WIRE = {
|
||||
VARINT: 0,
|
||||
FIXED64: 1,
|
||||
BYTES: 2,
|
||||
FIXED32: 5
|
||||
} as const
|
||||
|
||||
export const shouldIncludeReportingToken = (message: proto.IMessage): boolean =>
|
||||
!message.reactionMessage &&
|
||||
!message.encReactionMessage &&
|
||||
!message.encEventResponseMessage &&
|
||||
!message.pollUpdateMessage
|
||||
|
||||
const generateMsgSecretKey = async (
|
||||
modificationType: string,
|
||||
origMsgId: string,
|
||||
origMsgSender: string,
|
||||
modificationSender: string,
|
||||
origMsgSecret: Uint8Array
|
||||
) => {
|
||||
const useCaseSecret = Buffer.concat([
|
||||
Buffer.from(origMsgId, 'utf8'),
|
||||
Buffer.from(origMsgSender, 'utf8'),
|
||||
Buffer.from(modificationSender, 'utf8'),
|
||||
Buffer.from(modificationType, 'utf8')
|
||||
])
|
||||
|
||||
return hkdf(origMsgSecret, 32, { info: useCaseSecret.toString('latin1') })
|
||||
}
|
||||
|
||||
type FieldBytes = { num: number; bytes: Buffer }
|
||||
|
||||
const extractReportingTokenContent = (data: Buffer, cfg: Map<number, CompiledReportingField>): Buffer | null => {
|
||||
const out: FieldBytes[] = []
|
||||
let i = 0
|
||||
|
||||
while (i < data.length) {
|
||||
const tag = decodeVarint(data, i)
|
||||
if (!tag.ok) {
|
||||
return null
|
||||
}
|
||||
|
||||
const fieldNum = tag.value >> 3
|
||||
const wireType = tag.value & 0x7
|
||||
|
||||
const fieldStart = i
|
||||
i += tag.bytes
|
||||
|
||||
const fieldCfg = cfg.get(fieldNum)
|
||||
|
||||
const pushSlice = (end: number): boolean => {
|
||||
if (end > data.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
out.push({ num: fieldNum, bytes: data.subarray(fieldStart, end) })
|
||||
i = end
|
||||
return true
|
||||
}
|
||||
|
||||
const skip = (end: number): boolean => {
|
||||
if (end > data.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
i = end
|
||||
return true
|
||||
}
|
||||
|
||||
if (wireType === WIRE.VARINT) {
|
||||
const v = decodeVarint(data, i)
|
||||
if (!v.ok) {
|
||||
return null
|
||||
}
|
||||
|
||||
const end = i + v.bytes
|
||||
|
||||
if (!fieldCfg) {
|
||||
if (!skip(end)) {
|
||||
return null
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (!pushSlice(end)) {
|
||||
return null
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (wireType === WIRE.FIXED64) {
|
||||
const end = i + 8
|
||||
|
||||
if (!fieldCfg) {
|
||||
if (!skip(end)) {
|
||||
return null
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (!pushSlice(end)) {
|
||||
return null
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (wireType === WIRE.FIXED32) {
|
||||
const end = i + 4
|
||||
|
||||
if (!fieldCfg) {
|
||||
if (!skip(end)) {
|
||||
return null
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (!pushSlice(end)) {
|
||||
return null
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (wireType === WIRE.BYTES) {
|
||||
const len = decodeVarint(data, i)
|
||||
if (!len.ok) {
|
||||
return null
|
||||
}
|
||||
|
||||
const valStart = i + len.bytes
|
||||
const valEnd = valStart + len.value
|
||||
if (valEnd > data.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!fieldCfg) {
|
||||
i = valEnd
|
||||
continue
|
||||
}
|
||||
|
||||
if (fieldCfg.m || fieldCfg.children) {
|
||||
const sub = extractReportingTokenContent(data.subarray(valStart, valEnd), fieldCfg.children ?? EMPTY_MAP)
|
||||
if (sub === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (sub.length > 0) {
|
||||
const newTag = encodeVarint(tag.value)
|
||||
const newLen = encodeVarint(sub.length)
|
||||
out.push({
|
||||
num: fieldNum,
|
||||
bytes: Buffer.concat([newTag, newLen, sub])
|
||||
})
|
||||
}
|
||||
|
||||
i = valEnd
|
||||
continue
|
||||
}
|
||||
|
||||
out.push({ num: fieldNum, bytes: data.subarray(fieldStart, valEnd) })
|
||||
i = valEnd
|
||||
continue
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
if (out.length === 0) {
|
||||
return Buffer.alloc(0)
|
||||
}
|
||||
|
||||
out.sort((a, b) => a.num - b.num)
|
||||
return Buffer.concat(out.map(f => f.bytes))
|
||||
}
|
||||
|
||||
type Varint = { value: number; bytes: number; ok: boolean }
|
||||
|
||||
const decodeVarint = (buffer: Buffer, offset: number): Varint => {
|
||||
let value = 0
|
||||
let bytes = 0
|
||||
let shift = 0
|
||||
|
||||
while (offset + bytes < buffer.length) {
|
||||
const current = buffer[offset + bytes]!
|
||||
value |= (current & 0x7f) << shift
|
||||
bytes++
|
||||
|
||||
if ((current & 0x80) === 0) {
|
||||
return { value, bytes, ok: true }
|
||||
}
|
||||
|
||||
shift += 7
|
||||
|
||||
if (shift > 35) {
|
||||
return { value: 0, bytes: 0, ok: false }
|
||||
}
|
||||
}
|
||||
|
||||
return { value: 0, bytes: 0, ok: false }
|
||||
}
|
||||
|
||||
const encodeVarint = (value: number): Buffer => {
|
||||
const parts: number[] = []
|
||||
let remaining = value >>> 0
|
||||
|
||||
while (remaining > 0x7f) {
|
||||
parts.push((remaining & 0x7f) | 0x80)
|
||||
remaining >>>= 7
|
||||
}
|
||||
|
||||
parts.push(remaining)
|
||||
return Buffer.from(parts)
|
||||
}
|
||||
|
||||
export const getMessageReportingToken = async (
|
||||
msgProtobuf: Buffer,
|
||||
message: WAMessageContent,
|
||||
key: WAMessageKey
|
||||
): Promise<BinaryNode | null> => {
|
||||
const msgSecret = message.messageContextInfo?.messageSecret
|
||||
if (!msgSecret || !key.id) {
|
||||
return null
|
||||
}
|
||||
|
||||
const from = key.fromMe ? key.remoteJid! : key.participant || key.remoteJid!
|
||||
const to = key.fromMe ? key.participant || key.remoteJid! : key.remoteJid!
|
||||
|
||||
const reportingSecret = await generateMsgSecretKey(ENC_SECRET_REPORT_TOKEN, key.id, from, to, msgSecret)
|
||||
|
||||
const content = extractReportingTokenContent(msgProtobuf, compiledReportingFields)
|
||||
if (!content || content.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const reportingToken = createHmac('sha256', reportingSecret).update(content).digest().subarray(0, 16)
|
||||
|
||||
return {
|
||||
tag: 'reporting',
|
||||
attrs: {},
|
||||
content: [
|
||||
{
|
||||
tag: 'reporting_token',
|
||||
attrs: { v: '2' },
|
||||
content: reportingToken
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
import { proto } from '../../../WAProto'
|
||||
import type { WAMessageKey } from '../../Types'
|
||||
import { getMessageReportingToken, shouldIncludeReportingToken } from '../../Utils/reporting-utils'
|
||||
import type { BinaryNode } from '../../WABinary'
|
||||
|
||||
describe('Reporting Utils', () => {
|
||||
describe('shouldIncludeReportingToken', () => {
|
||||
const includedMessages: [string, proto.IMessage][] = [
|
||||
['conversation', { conversation: 'Hello' }],
|
||||
['extendedTextMessage', { extendedTextMessage: { text: 'Link' } }],
|
||||
['imageMessage', { imageMessage: { url: 'url', mimetype: 'image/jpeg' } }],
|
||||
['videoMessage', { videoMessage: { url: 'url', mimetype: 'video/mp4' } }],
|
||||
['documentMessage', { documentMessage: { url: 'url', mimetype: 'application/pdf' } }],
|
||||
['audioMessage', { audioMessage: { url: 'url', mimetype: 'audio/ogg' } }],
|
||||
['stickerMessage', { stickerMessage: { url: 'url', mimetype: 'image/webp' } }]
|
||||
]
|
||||
|
||||
const excludedMessages: [string, proto.IMessage][] = [
|
||||
['reactionMessage', { reactionMessage: { key: { id: 'id' }, text: '👍' } }],
|
||||
[
|
||||
'encReactionMessage',
|
||||
{
|
||||
encReactionMessage: { targetMessageKey: { id: 'id' }, encPayload: Buffer.from('x'), encIv: Buffer.from('x') }
|
||||
}
|
||||
],
|
||||
[
|
||||
'pollUpdateMessage',
|
||||
{
|
||||
pollUpdateMessage: {
|
||||
pollCreationMessageKey: { id: 'id' },
|
||||
vote: { encPayload: Buffer.from('x'), encIv: Buffer.from('x') }
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
'encEventResponseMessage',
|
||||
{
|
||||
encEventResponseMessage: {
|
||||
eventCreationMessageKey: { id: 'id' },
|
||||
encPayload: Buffer.from('x'),
|
||||
encIv: Buffer.from('x')
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
it.each(includedMessages)('should return true for %s', (_, message) => {
|
||||
expect(shouldIncludeReportingToken(message)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(excludedMessages)('should return false for %s', (_, message) => {
|
||||
expect(shouldIncludeReportingToken(message)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMessageReportingToken', () => {
|
||||
const createKey = (overrides?: Partial<WAMessageKey>): WAMessageKey => ({
|
||||
id: 'test-id',
|
||||
fromMe: true,
|
||||
remoteJid: '123@s.whatsapp.net',
|
||||
...overrides
|
||||
})
|
||||
|
||||
const withSecret = (content: proto.IMessage, secret = randomBytes(32)): proto.IMessage => ({
|
||||
...content,
|
||||
messageContextInfo: { messageSecret: secret }
|
||||
})
|
||||
|
||||
const encode = (msg: proto.IMessage) => Buffer.from(proto.Message.encode(msg).finish())
|
||||
const getToken = (result: BinaryNode | null): Buffer | undefined => {
|
||||
const content = result?.content
|
||||
if (Array.isArray(content) && content[0]) {
|
||||
return content[0].content as Buffer | undefined
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
it('should return null when message secret is missing', async () => {
|
||||
const msg: proto.IMessage = { conversation: 'Hello' }
|
||||
expect(await getMessageReportingToken(encode(msg), msg, createKey())).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null when message key id is missing', async () => {
|
||||
const msg = withSecret({ conversation: 'Hello' })
|
||||
expect(await getMessageReportingToken(encode(msg), msg, createKey({ id: '' }))).toBeNull()
|
||||
})
|
||||
|
||||
it('should return valid reporting node structure', async () => {
|
||||
const msg = withSecret({ conversation: 'Hello' })
|
||||
const result = await getMessageReportingToken(encode(msg), msg, createKey())
|
||||
|
||||
expect(result).toMatchObject({ tag: 'reporting', attrs: {} })
|
||||
const content = result?.content as any[]
|
||||
expect(content).toHaveLength(1)
|
||||
expect(content[0]).toMatchObject({ tag: 'reporting_token', attrs: { v: '2' } })
|
||||
expect(content[0].content).toHaveLength(16)
|
||||
})
|
||||
|
||||
it('should produce consistent tokens for the same input', async () => {
|
||||
const secret = randomBytes(32)
|
||||
const msg = withSecret({ conversation: 'Test' }, secret)
|
||||
const encoded = encode(msg)
|
||||
const key = createKey()
|
||||
|
||||
const token1 = getToken(await getMessageReportingToken(encoded, msg, key))
|
||||
const token2 = getToken(await getMessageReportingToken(encoded, msg, key))
|
||||
expect(token1).toEqual(token2)
|
||||
})
|
||||
|
||||
const uniquenessTests: [string, () => [proto.IMessage, WAMessageKey, proto.IMessage, WAMessageKey]][] = [
|
||||
[
|
||||
'different secrets',
|
||||
() => {
|
||||
const msg1 = withSecret({ conversation: 'Same' }, randomBytes(32))
|
||||
const msg2 = withSecret({ conversation: 'Same' }, randomBytes(32))
|
||||
return [msg1, createKey(), msg2, createKey()]
|
||||
}
|
||||
],
|
||||
[
|
||||
'different message IDs',
|
||||
() => {
|
||||
const secret = randomBytes(32)
|
||||
const msg = withSecret({ conversation: 'Same' }, secret)
|
||||
return [msg, createKey({ id: 'id-1' }), msg, createKey({ id: 'id-2' })]
|
||||
}
|
||||
],
|
||||
[
|
||||
'different remoteJids',
|
||||
() => {
|
||||
const secret = randomBytes(32)
|
||||
const msg = withSecret({ conversation: 'Same' }, secret)
|
||||
return [
|
||||
msg,
|
||||
createKey({ remoteJid: '111@s.whatsapp.net' }),
|
||||
msg,
|
||||
createKey({ remoteJid: '222@s.whatsapp.net' })
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
it.each(uniquenessTests)('should produce different tokens for %s', async (_, setup) => {
|
||||
const [msg1, key1, msg2, key2] = setup()
|
||||
const token1 = getToken(await getMessageReportingToken(encode(msg1), msg1, key1))
|
||||
const token2 = getToken(await getMessageReportingToken(encode(msg2), msg2, key2))
|
||||
expect(token1).not.toEqual(token2)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['group messages', { remoteJid: '123@g.us', participant: '456@s.whatsapp.net' }],
|
||||
['incoming messages', { fromMe: false }]
|
||||
])('should handle %s', async (_, keyOverrides) => {
|
||||
const msg = withSecret({ conversation: 'Test' })
|
||||
const result = await getMessageReportingToken(encode(msg), msg, createKey(keyOverrides))
|
||||
expect(result?.tag).toBe('reporting')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user