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:
João Lucas de Oliveira Lopes
2026-01-08 16:50:49 -03:00
committed by GitHub
parent 4e681d3219
commit c9c3481817
4 changed files with 545 additions and 0 deletions
+160
View File
@@ -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')
})
})
})