Skip to content

Commit 732ebbc

Browse files
committed
feat(cloud): parse inbound reply context so msg.replied() resolves on cloud
1 parent a624ed2 commit 732ebbc

2 files changed

Lines changed: 59 additions & 2 deletions

File tree

src/cloud/translate/inbound.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,38 @@ const contactsNode = (msg: CloudWebhookMessage): Record<string, unknown> | null
151151
return { contactsArrayMessage: { displayName: `${cards.length} contacts`, contacts: cards } }
152152
}
153153

154+
/**
155+
* Attach a baileys-shaped contextInfo (from Meta's `context: { id, from }`) so the decoder can
156+
* resolve the quoted message via the store — enables `msg.replied()` on cloud replies. A stub
157+
* `quotedMessage` is required to enter the decoder's resolve branch (it looks up by stanzaId).
158+
*/
159+
const applyQuotedContext = (
160+
message: Record<string, unknown>,
161+
msg: CloudWebhookMessage,
162+
): Record<string, unknown> => {
163+
const context = msg['context'] as { id?: string; from?: string } | undefined
164+
if (!context?.id) return message
165+
const contextInfo = {
166+
stanzaId: context.id,
167+
...(context.from ? { participant: toJid(context.from) } : {}),
168+
quotedMessage: {},
169+
}
170+
if (typeof message['conversation'] === 'string') {
171+
return { extendedTextMessage: { text: message['conversation'], contextInfo } }
172+
}
173+
const key = Object.keys(message)[0]
174+
if (key) (message[key] as Record<string, unknown>)['contextInfo'] = contextInfo
175+
return message
176+
}
177+
154178
const translateMessage = (msg: CloudWebhookMessage, value: CloudWebhookValue): WAMessage | null => {
155179
if (!msg.id || !msg.from) return null
156-
const message: Record<string, unknown> | null =
180+
const node: Record<string, unknown> | null =
157181
msg.type === 'text' && typeof msg.text?.body === 'string'
158182
? { conversation: msg.text.body }
159183
: (mediaNode(msg) ?? interactiveNode(msg) ?? locationNode(msg) ?? contactsNode(msg))
160-
if (message === null) return null
184+
if (node === null) return null
185+
const message = applyQuotedContext(node, msg)
161186
const pushName = contactName(value, msg.from)
162187
return {
163188
key: { id: msg.id, remoteJid: toJid(msg.from), fromMe: false },

tests/integration/cloud-receive-text.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,35 @@ describe('integration: cloud webhook without connect()', () => {
107107
expect(texts[0]?.text).toBe('halo bot')
108108
})
109109
})
110+
111+
describe('integration: cloud inbound quoted/context parsing', () => {
112+
function reply(quotedId: string) {
113+
return post(JSON.stringify({
114+
object: 'whatsapp_business_account',
115+
entry: [{ id: 'W', changes: [{ field: 'messages', value: {
116+
messaging_product: 'whatsapp', metadata: { phone_number_id: '555' },
117+
contacts: [{ profile: { name: 'Budi' }, wa_id: '628111000222' }],
118+
messages: [{ from: '628111000222', id: 'wamid.REPLY', timestamp: '1752350000',
119+
type: 'text', text: { body: 'setuju' }, context: { from: '555', id: quotedId } }],
120+
} }] }],
121+
}))
122+
}
123+
124+
it('a reply carries context.id → msg.replied() resolves the stored original', async () => {
125+
const c = await connectedClient()
126+
// bot sends a message first → stored under its wamid
127+
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ messages: [{ id: 'wamid.ORIG' }] }), { status: 200 }))
128+
await c.send('628111000222').text('pertanyaan?')
129+
await new Promise((r) => setTimeout(r, 5))
130+
131+
let quoted: MessageContext | null = null
132+
const done = new Promise<void>((resolve) => {
133+
c.on('text', async (m) => { if (m.text === 'setuju') { quoted = await m.replied(); resolve() } })
134+
})
135+
await c.webhook()(reply('wamid.ORIG'))
136+
await done
137+
expect(quoted).not.toBeNull()
138+
expect(quoted?.chatId).toBe('wamid.ORIG')
139+
expect(quoted?.text).toBe('pertanyaan?')
140+
})
141+
})

0 commit comments

Comments
 (0)