Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3721,6 +3721,34 @@ await sock.updateMemberLabel(
)
```

## 馃З Raw Message (`raw`)
Use raw mode when you want to send a message built directly from WhatsApp proto message keys.

> [!IMPORTANT]
> - Raw mode is **explicit only**: set `raw: true`.
> - Do **not** mix helper fields (`text`, `image`, `buttons`, `interactiveButtons`, etc.) with raw mode.
> - Top-level keys must be valid `proto.Message` keys (for example `extendedTextMessage`, `imageMessage`, `videoMessage`).

```ts
await sock.sendMessage(jid, {
extendedTextMessage: {
text: '馃搩 Built manually from scratch using the raw WhatsApp proto structure',
contextInfo: {
externalAdReply: {
title: '@itsliaaa/baileys',
thumbnail: fs.readFileSync('./path/to/image.jpg'),
sourceApp: 'whatsapp',
showAdAttribution: true,
mediaType: 1
}
}
},
raw: true
}, {
quoted: message
})
```

## 馃懃 Group Status (`groupStatus`)
Post a message as a **group status update**. When `groupStatus: true` is set, the library:

Expand Down
28 changes: 28 additions & 0 deletions assets/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3863,6 +3863,34 @@ sock.ws.on('CB:edge_routing,id:abcd,routing_info', (node: BinaryNode) => { })

## Additional Message Utilities

### Raw Message (`raw`)
Use raw mode when you want to send a message built directly from WhatsApp proto message keys.

> [!IMPORTANT]
> - Raw mode is **explicit only**: set `raw: true`.
> - Do **not** mix helper fields (`text`, `image`, `buttons`, `interactiveButtons`, etc.) with raw mode.
> - Top-level keys must be valid `proto.Message` keys (for example `extendedTextMessage`, `imageMessage`, `videoMessage`).

```ts
await sock.sendMessage(jid, {
extendedTextMessage: {
text: '馃搩 Built manually from scratch using the raw WhatsApp proto structure',
contextInfo: {
externalAdReply: {
title: '@itsliaaa/baileys',
Comment thread
innovatorssoft marked this conversation as resolved.
thumbnail: fs.readFileSync('./path/to/image.jpg'),
sourceApp: 'whatsapp',
showAdAttribution: true,
mediaType: 1
}
}
},
raw: true
}, {
quoted: message
})
```

### Group Status (`groupStatus`)

Post a message as a **group status update**. When `groupStatus: true` is set, the library:
Expand Down
6 changes: 5 additions & 1 deletion lib/Types/Message.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,11 @@ export type RichMessageHelpers = {
table?: string[][]
}

export type AnyMessageContent = AnyRegularMessageContent | {
export type RawMessageContent = WAProto.IMessage & {
raw: true
}
Comment thread
innovatorssoft marked this conversation as resolved.

export type AnyMessageContent = AnyRegularMessageContent | RawMessageContent | {
forward: WAMessage
force?: boolean
} | {
Expand Down
46 changes: 46 additions & 0 deletions lib/Utils/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,49 @@ const MessageTypeProto = {
'document': Types_1.WAProto.Message.DocumentMessage,
}

const MESSAGE_PROTO_KEYS = new Set(
Object.keys(WAProto_1.proto.Message.prototype).filter(
key => !key.startsWith('_') && typeof WAProto_1.proto.Message.prototype[key] !== 'function'
)
)

const RAW_MODE_HELPER_KEYS = new Set([
'raw', 'text', 'linkPreview', 'mentions', 'mentionAll', 'contextInfo', 'buttons', 'templateButtons',
'interactiveButtons', 'nativeFlow', 'optionText', 'optionTitle', 'offerText', 'offerCode', 'offerUrl',
'offerExpiration', 'title', 'subtitle', 'footer', 'hasMediaAttachment', 'sections', 'buttonText', 'listType',
'cards', 'contacts', 'location', 'react', 'delete', 'forward', 'force', 'disappearingMessagesInChat',
'groupInvite', 'adminInvite', 'pin', 'keep', 'call', 'event', 'payment', 'paymentInvite', 'poll', 'pollResult',
'order', 'product', 'businessOwnerJid', 'stickerPack', 'sharePhoneNumber', 'requestPhoneNumber', 'album',
'interactiveAsTemplate', 'groupStatus', 'ephemeral', 'edit', 'viewOnce', 'viewOnceV2', 'viewOnceV2Extension',
'viewOnceExt', 'spoiler', 'isLottie', 'secureMetaServiceLabel', 'code', 'links', 'table', 'richResponse',
'productList', 'thumbnail', 'audioFooter', 'caption', 'shop', 'shopSurface', 'collection', 'bizJid', 'id',
'ptv', 'image', 'video', 'audio', 'sticker', 'document', 'mimetype', 'jpegThumbnail', 'gifPlayback', 'fileName',
'seconds'
])

const buildRawMessageContent = (message) => {
const rawPayload = { ...message }
delete rawPayload.raw

const helperKeys = Object.keys(rawPayload).filter(
key => RAW_MODE_HELPER_KEYS.has(key) && !MESSAGE_PROTO_KEYS.has(key)
)
if (helperKeys.length) {
throw new boom_1.Boom(`Raw mode does not support helper fields: ${helperKeys.join(', ')}`, { statusCode: 400 })
}

const invalidKeys = Object.keys(rawPayload).filter(key => !MESSAGE_PROTO_KEYS.has(key))
if (invalidKeys.length) {
throw new boom_1.Boom(`Raw mode payload contains unsupported top-level keys: ${invalidKeys.join(', ')}`, { statusCode: 400 })
}

if (!Object.keys(rawPayload).length) {
throw new boom_1.Boom('Raw mode payload must include at least one proto message key', { statusCode: 400 })
}

return Types_1.WAProto.Message.fromObject(rawPayload)
Comment thread
innovatorssoft marked this conversation as resolved.
}

/**
* Uses a regex to test whether the string contains a URL, and returns the URL if it does.
* @param text eg. hello https://google.com
Expand Down Expand Up @@ -518,6 +561,9 @@ const buildMentionContextInfo = (message) => {
}

const generateWAMessageContent = async (message, options) => {
if ('raw' in message && message.raw === true) {
return buildRawMessageContent(message)
}

let m = {}
if ('text' in message) {
Expand Down
131 changes: 121 additions & 10 deletions test/messages.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
const { generateWAMessageContent } = require('../lib/Utils/messages')
const { generateWAMessageContent, generateWAMessage } = require('../lib/Utils/messages')

const testLogger = {
trace: jest.fn(),
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
child: jest.fn(() => testLogger)
}

describe('generateWAMessageContent Buttons Parsing', () => {
test('should parse mixed native flow and response buttons correctly', async () => {
Expand Down Expand Up @@ -28,15 +37,7 @@ describe('generateWAMessageContent Buttons Parsing', () => {
]
}

const options = {
logger: {
trace: jest.fn(),
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn()
}
}
const options = { logger: testLogger }

const result = await generateWAMessageContent(message, options)

Expand Down Expand Up @@ -65,6 +66,116 @@ describe('generateWAMessageContent Buttons Parsing', () => {
})
})

describe('generateWAMessageContent Raw Mode', () => {
test('should serialize raw extendedTextMessage payload', async () => {
const message = {
raw: true,
extendedTextMessage: {
text: 'Raw payload',
contextInfo: {
externalAdReply: {
title: '@itsliaaa/baileys',
sourceApp: 'whatsapp',
showAdAttribution: true,
mediaType: 1
}
}
}
}

const result = await generateWAMessageContent(message, { logger: testLogger })

expect(result.extendedTextMessage).toBeDefined()
expect(result.extendedTextMessage.text).toBe('Raw payload')
expect(result.extendedTextMessage.contextInfo.externalAdReply.title).toBe('@itsliaaa/baileys')
})

test('should preserve raw contextInfo and still attach quoted metadata', async () => {
const jid = '1234567890@s.whatsapp.net'
const quoted = {
key: {
remoteJid: jid,
fromMe: false,
id: 'quoted-message-id',
participant: '1111111111@s.whatsapp.net'
},
message: {
conversation: 'quoted text'
}
}

const result = await generateWAMessage(jid, {
raw: true,
extendedTextMessage: {
text: 'Raw with quote',
contextInfo: {
externalAdReply: {
title: '@itsliaaa/baileys',
sourceApp: 'whatsapp',
showAdAttribution: true,
mediaType: 1
}
}
}
}, {
userJid: '9999999999@s.whatsapp.net',
quoted,
logger: testLogger
})

const contextInfo = result.message.extendedTextMessage.contextInfo
expect(contextInfo.externalAdReply.title).toBe('@itsliaaa/baileys')
expect(contextInfo.stanzaId).toBe('quoted-message-id')
expect(contextInfo.participant).toBe('1111111111@s.whatsapp.net')
expect(contextInfo.quotedMessage).toBeDefined()
})

test('should reject mixed helper and raw payload fields', async () => {
await expect(generateWAMessageContent({
raw: true,
text: 'hello',
extendedTextMessage: {
text: 'Raw payload'
}
}, { logger: testLogger }))
.rejects
.toThrow('Raw mode does not support helper fields: text')
})

test('should reject invalid raw top-level keys', async () => {
await expect(generateWAMessageContent({
raw: true,
notAProtoField: {
value: 'x'
}
}, { logger: testLogger }))
.rejects
.toThrow('Raw mode payload contains unsupported top-level keys: notAProtoField')
})

test('should reject raw payload keys inherited from proto functions', async () => {
await expect(generateWAMessageContent({
raw: true,
toJSON: {
value: 'x'
}
}, { logger: testLogger }))
.rejects
.toThrow('Raw mode payload contains unsupported top-level keys: toJSON')
})

test('should allow raw proto keys that overlap helper names', async () => {
const result = await generateWAMessageContent({
raw: true,
call: {
callKey: 'abc'
}
}, { logger: testLogger })

expect(result.call).toBeDefined()
})
})

describe('Interactive Carousel Biz Node', () => {
const { shouldIncludeBizBinaryNode } = require('../lib/Utils/messages')
const { getBizBinaryNode } = require('../lib/WABinary/generic-utils')
Expand Down
Loading