feat: add PIX payment and fix interactive messages (buttons, list, carousel)#40
feat: add PIX payment and fix interactive messages (buttons, list, carousel)#40impa365 wants to merge 1 commit intoEvolutionAPI:mainfrom
Conversation
…rousel) - Add SendPix function with full WhatsApp payment protocol support (AdditionalNodes biz/bot nodes, DeviceListMetadataVersion, NativeFlowMessage) - Add /send/pix route and SendPix handler with input validation - Rewrite SendButton: DocumentWithCaptionMessage wrapper + MessageSecret + AdditionalNodes biz nodes; supports reply, URL, call, copy and review_and_pay - Rewrite SendList: DocumentWithCaptionMessage wrapper + MessageSecret + AdditionalNodes (product_list v=2 biz nodes) - Fix SendCarousel: add MessageSecret (32 bytes), set HSCROLL_CARDS card type, add MessageParamsJSON+MessageVersion on card buttons, use json.Marshal instead of fmt.Sprintf, remove forced empty ContextInfo that broke iOS rendering, pass Quoted message, simplify image upload (remove thumbnail generation) - Fix SendMessage central: add AdditionalNodes support in sendExtra, add ButtonsMessage and ListMessage wrapped cases in DocumentWithCaptionMessage, fix InteractiveMessage handling (skip empty ContextInfo on direct carousel) - Remove unused image/jpeg import
There was a problem hiding this comment.
Pull request overview
Adds PIX payment message support and updates WhatsApp interactive message construction (buttons/lists/carousels) to match newer protocol expectations across Android/iOS, including biz stanza injection and MessageSecret handling.
Changes:
- Introduces
SendPixmessage builder andPOST /send/pixendpoint. - Reworks button/list sending to wrap messages in
DocumentWithCaptionMessage, generatesMessageSecret, and injects bizAdditionalNodes. - Updates carousel card native-flow params and refactors button param JSON building to use
json.Marshal.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/sendMessage/service/send_service.go | Adds PIX support and reworks interactive message building/routing (buttons/lists/carousels), plus AdditionalNodes passthrough. |
| pkg/sendMessage/handler/send_handler.go | Adds /send/pix handler with request validation. |
| pkg/routes/routes.go | Registers POST /send/pix route. |
| .gitignore | Adds an ignore entry for a local/reference folder. |
| .dockerignore | Ignores reference/analysis artifacts in Docker builds. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| resp, err := http.Get(data.ImageUrl) | ||
| if err == nil { | ||
| fileData, err := io.ReadAll(resp.Body) | ||
| resp.Body.Close() |
There was a problem hiding this comment.
http.Get is performed on data.ImageUrl / data.VideoUrl (user-provided) without any allowlist/denylist, scheme checks, size limits, or timeouts. This opens SSRF and resource exhaustion risk (e.g., fetching internal URLs or very large responses). Use an http.Client with timeouts, validate scheme/host, and enforce a maximum download size (e.g., io.LimitReader).
| for _, row := range section.Rows { | ||
| rows = append(rows, &waE2E.ListMessage_Row{ | ||
| Title: proto.String(rowTitle), | ||
| Description: proto.String(r.Description), | ||
| RowID: proto.String(rowId), | ||
| Title: proto.String(row.Title), | ||
| Description: proto.String(row.Description), | ||
| RowID: proto.String(row.RowId), | ||
| }) |
There was a problem hiding this comment.
List rows are now sent with RowID: proto.String(row.RowId) and Title: proto.String(row.Title) without generating fallbacks when these are empty. Previously this code ensured non-empty titles/IDs; sending empty RowIDs can cause WhatsApp to reject the list or render incorrectly. Add validation (reject empty titles/rowIds) or generate deterministic defaults when missing.
| Sections: sections, | ||
| // Generate MessageSecret (32 random bytes) | ||
| listMsgSecret := make([]byte, 32) | ||
| crypto_rand.Read(listMsgSecret) |
There was a problem hiding this comment.
crypto/rand.Read(listMsgSecret) ignores the returned count/error. If it fails or returns fewer than 32 bytes, MessageSecret may be invalid and interactive list rendering can break. Check n, err and handle failures explicitly.
| crypto_rand.Read(listMsgSecret) | |
| if _, err := io.ReadFull(crypto_rand.Reader, listMsgSecret); err != nil { | |
| return nil, fmt.Errorf("failed to generate list message secret: %w", err) | |
| } |
| interactiveMsg.ContextInfo = contextInfo | ||
| // Generate MessageSecret (32 random bytes) required for iOS to render | ||
| msgSecret := make([]byte, 32) | ||
| crypto_rand.Read(msgSecret) |
There was a problem hiding this comment.
crypto/rand.Read(msgSecret) ignores the returned count/error. If the RNG read fails or is partial, MessageSecret may be invalid and carousel rendering can break. Check n, err and handle failures explicitly.
| crypto_rand.Read(msgSecret) | |
| n, err := crypto_rand.Read(msgSecret) | |
| if err != nil { | |
| s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error generating carousel message secret: %v", instance.Id, err) | |
| return nil, err | |
| } | |
| if n != len(msgSecret) { | |
| err = fmt.Errorf("incomplete random read for carousel message secret: got %d bytes, want %d", n, len(msgSecret)) | |
| s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error generating carousel message secret: %v", instance.Id, err) | |
| return nil, err | |
| } |
| client, _ := s.ensureClientConnected(instance.Id) | ||
| uploaded, err := client.Upload(context.Background(), fileData, whatsmeow.MediaImage) | ||
| if err == nil { | ||
| interactive.Header = &waE2E.InteractiveMessage_Header{ | ||
| Title: proto.String(""), | ||
| HasMediaAttachment: proto.Bool(true), | ||
| Media: &waE2E.InteractiveMessage_Header_ImageMessage{ | ||
| ImageMessage: &waE2E.ImageMessage{ | ||
| URL: proto.String(uploaded.URL), | ||
| DirectPath: proto.String(uploaded.DirectPath), | ||
| MediaKey: uploaded.MediaKey, | ||
| Mimetype: proto.String("image/jpeg"), | ||
| FileEncSHA256: uploaded.FileEncSHA256, | ||
| FileSHA256: uploaded.FileSHA256, | ||
| FileLength: proto.Uint64(uint64(len(fileData))), | ||
| }, | ||
| }, |
There was a problem hiding this comment.
Here too client, _ := s.ensureClientConnected(...) ignores the error before calling client.Upload(...). If the client is disconnected, this can panic. Reuse the validated client from the beginning of SendButton or return an error if reconnection fails.
| client, _ := s.ensureClientConnected(instance.Id) | |
| uploaded, err := client.Upload(context.Background(), fileData, whatsmeow.MediaImage) | |
| if err == nil { | |
| interactive.Header = &waE2E.InteractiveMessage_Header{ | |
| Title: proto.String(""), | |
| HasMediaAttachment: proto.Bool(true), | |
| Media: &waE2E.InteractiveMessage_Header_ImageMessage{ | |
| ImageMessage: &waE2E.ImageMessage{ | |
| URL: proto.String(uploaded.URL), | |
| DirectPath: proto.String(uploaded.DirectPath), | |
| MediaKey: uploaded.MediaKey, | |
| Mimetype: proto.String("image/jpeg"), | |
| FileEncSHA256: uploaded.FileEncSHA256, | |
| FileSHA256: uploaded.FileSHA256, | |
| FileLength: proto.Uint64(uint64(len(fileData))), | |
| }, | |
| }, | |
| client, err := s.ensureClientConnected(instance.Id) | |
| if err == nil { | |
| uploaded, err := client.Upload(context.Background(), fileData, whatsmeow.MediaImage) | |
| if err == nil { | |
| interactive.Header = &waE2E.InteractiveMessage_Header{ | |
| Title: proto.String(""), | |
| HasMediaAttachment: proto.Bool(true), | |
| Media: &waE2E.InteractiveMessage_Header_ImageMessage{ | |
| ImageMessage: &waE2E.ImageMessage{ | |
| URL: proto.String(uploaded.URL), | |
| DirectPath: proto.String(uploaded.DirectPath), | |
| MediaKey: uploaded.MediaKey, | |
| Mimetype: proto.String("image/jpeg"), | |
| FileEncSHA256: uploaded.FileEncSHA256, | |
| FileSHA256: uploaded.FileSHA256, | |
| FileLength: proto.Uint64(uint64(len(fileData))), | |
| }, | |
| }, | |
| } |
| if data.KeyType == "" { | ||
| return nil, errors.New("keyType is required (CPF, CNPJ, EMAIL, PHONE, EVP)") | ||
| } | ||
| data.KeyType = strings.ToUpper(data.KeyType) | ||
|
|
There was a problem hiding this comment.
SendPix normalizes KeyType with strings.ToUpper, but the codebase already has mapKeyType() that maps values like random -> EVP. As-is, random becomes RANDOM and will be sent to WhatsApp unchanged. Consider validating against the supported set and mapping via mapKeyType (or equivalent) before building the payload.
| // Build internal payment payload | ||
| defaultAmount := pixAmount{Value: 100, Offset: 100} | ||
| payload := pixPaymentPayload{ | ||
| Currency: "BRL", | ||
| ReferenceID: utils.GenerateRandomString(11), | ||
| Type: "physical-goods", | ||
| TotalAmount: defaultAmount, | ||
| Order: pixOrder{ | ||
| Status: "pending", | ||
| OrderType: "ORDER", | ||
| Subtotal: defaultAmount, | ||
| Items: []pixItem{ |
There was a problem hiding this comment.
SendPix currently hardcodes defaultAmount := pixAmount{Value: 100, Offset: 100} and always sends that as the total/subtotal. This makes the PIX API effectively fixed-value and doesn’t match the PR description/API expectation of an optional amount/description; it will also be surprising to clients that omit an amount. Add fields (e.g., amount, description) to PixStruct and compute TotalAmount/Subtotal/Items from request data (or omit amounts when not provided if the protocol supports it).
| client, _ := s.ensureClientConnected(instance.Id) | ||
| uploaded, err := client.Upload(context.Background(), fileData, whatsmeow.MediaImage) | ||
| if err == nil { |
There was a problem hiding this comment.
In the media-header upload path you call client, _ := s.ensureClientConnected(...) and ignore the error. If the client isn’t connected, client can be nil and client.Upload(...) will panic. Reuse the already-validated client from the start of SendButton, or handle the error and return it before uploading.
| msg := &waE2E.Message{ | ||
| InteractiveMessage: interactive, | ||
| MessageContextInfo: &waE2E.MessageContextInfo{ | ||
| DeviceListMetadataVersion: proto.Int32(2), | ||
| DeviceListMetadata: &waE2E.DeviceListMetadata{}, | ||
| }, | ||
| } |
There was a problem hiding this comment.
SendPix doesn’t set MessageContextInfo.MessageSecret while other interactive-message senders (buttons/lists/carousel) now do. If iOS rendering requires MessageSecret for interactive messages, PIX messages may still fail to render. Consider generating a 32-byte secret and populating MessageContextInfo.MessageSecret here as well (and handle crypto/rand errors).
|
|
||
| // Generate MessageSecret (32 random bytes) required for iOS to render interactive messages | ||
| btnMsgSecret := make([]byte, 32) | ||
| crypto_rand.Read(btnMsgSecret) |
There was a problem hiding this comment.
crypto/rand.Read return values are ignored when generating MessageSecret. If the read fails or is partial, you may send an all-zero/short secret which can break rendering. Capture and check the returned n, err and fail the request (or retry) if err != nil || n != 32.
| crypto_rand.Read(btnMsgSecret) | |
| n, err := crypto_rand.Read(btnMsgSecret) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to generate message secret: %w", err) | |
| } | |
| if n != len(btnMsgSecret) { | |
| return nil, fmt.Errorf("failed to generate message secret: read %d bytes, expected %d", n, len(btnMsgSecret)) | |
| } |
|
Top..foi reativo isso.. bora subir |
paluan-batista
left a comment
There was a problem hiding this comment.
Bom dia amigo tudo bem?
Na intenção de ajudá-lo com um code review nesse projeto, deixo aqui algumas sugestões de melhorias que encontrei. De modo geral o projeto esta ficando muito bom.
| sendExtra.AdditionalNodes = data.AdditionalNodes | ||
| } | ||
|
|
||
| response, err := s.clientPointer[instance.Id].SendMessage(context.Background(), recipient, msg, sendExtra) |
There was a problem hiding this comment.
Bom dia tudo bem?
Usar context.Background() impede o cancelamento da operação se o cliente HTTP (o usuário da API) fechar a conexão ou se houver um timeout no nível do handler.
Sugestão: Alterar a assinatura do SendPix e SendMessage para aceitar ctx context.Context e propagar o contexto vindo do Gin.
|
|
||
| type PaymentItem struct { | ||
| Name string `json:"name"` | ||
| Amount int `json:"amount"` |
There was a problem hiding this comment.
Identifiquei uma inconsistência entre as structs de pagamento:
PaymentItem.Amount é int.
pixAmount.Value é int64.
Sugestão: Padronizar todos os valores monetários e quantidades para int64 para evitar overflows silenciosos em sistemas de 32 bits e manter a compatibilidade com os tipos do proto.
| return | ||
| } | ||
|
|
||
| if data.MerchantName == "" { |
There was a problem hiding this comment.
As validações de MerchantName, PixKey e KeyType estão presentes tanto no send_handler.go quanto no send_service.go.
Sugestão: Mover a lógica de validação para um método de domínio ou usar tags de validação do Gin (binding:"required") na struct PixStruct. Isso simplifica o handler e garante que o service seja testável isoladamente.
| data.KeyType = strings.ToUpper(data.KeyType) | ||
|
|
||
| // Build internal payment payload | ||
| defaultAmount := pixAmount{Value: 100, Offset: 100} |
There was a problem hiding this comment.
Atualmente, defaultAmount está fixado em Value: 100, Offset: 100 no SendPix.
Sugestão: Estes valores deveriam ser parametrizados no PixStruct para permitir cobranças de valores reais, ou documentados como um "placeholder" para pagamentos de valor aberto (se for o caso do protocolo).
| AdditionalNodes: &pixNodes, | ||
| }) | ||
| if err != nil { | ||
| s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error sending pix message: %v", instance.Id, err) |
There was a problem hiding this comment.
O log em SendPix utiliza LogError mas retorna o erro original:
Sugestão: Considere usar fmt.Errorf("failed to send pix: %w", err) para fornecer contexto sem perder o erro original (error wrapping).
paluan-batista
left a comment
There was a problem hiding this comment.
Afim de ajudar com um code review, deixei algumas sugestões.
PIX Payment Support + Button/List/Carousel Protocol Fixes
Description
This PR implements PIX payment message support and fixes the WhatsApp binary protocol for buttons, lists, and carousels to work correctly on both Android and iOS clients.
🆕 New: PIX Payment (
SendPix)PixStructwith all required fields:number,headerTitle,bodyText,merchantName,pixKey,keyType(CPF, CNPJ, EMAIL, PHONE, EVP), optionalamount,description,delay,quotedNativeFlowMessage(review_and_pay) withpixPaymentPayloadserialized as JSON insideButtonParamsJSONAdditionalNodesstanza nodes (biz+botXML nodes) for business payment flowDeviceListMetadataVersion: 2for correct end-to-end routingPOST /send/pixroute with full input validation in the handler🔧 Fix: Button messages (
SendButton)ButtonsMessageandInteractiveMessageinsideDocumentWithCaptionMessage— required by current WhatsApp protocol to display buttons in chatMessageSecretviacrypto/rand— required for iOS to render interactive messagesAdditionalNodesnodes (same pattern as official WhatsApp Business API)REPLY(quick reply),URL(cta_url),CALL(cta_call),COPY(cta_copy), andREVIEW_AND_PAYImageUrl/VideoUrlheader support🔧 Fix: List messages (
SendList)ListMessageinsideDocumentWithCaptionMessage— required by current WhatsApp protocolMessageSecret(32 bytes)product_listbizAdditionalNodeswithv=2attribute required for list rendering🔧 Fix: Carousel messages (
SendCarousel)MessageSecret(32 bytes) — was missing, causing silent failures on iOSCarouselCardType: HSCROLL_CARDS— required for horizontal scroll carousel layoutMessageParamsJSONandMessageVersionon each card'sNativeFlowMessagefmt.SprintfJSON construction withjson.Marshalfor all button paramsContextInfoon directInteractiveMessagethat was breaking iOS carousel renderingQuotedthroughSendDataStructcorrectly🔧 Fix:
SendMessagecentral dispatcherAdditionalNodespassthrough fromSendDataStructtowhatsmeow.SendRequestExtraButtonsMessagecase handling for messages wrapped inDocumentWithCaptionMessageListMessagecase handling for messages wrapped inDocumentWithCaptionMessageInteractiveMessagecase: appliesContextInfoonly on wrapped messages (buttons/lists); skips emptyContextInfoon directInteractiveMessage(carousel) to avoid iOS rendering breakage🧹 Chore
image/jpegimport (leftover from thumbnail generation code)AdditionalNodes *[]waBinary.Nodefield toSendRequestExtrain thewhatsmeow-libfork for biz stanza injectionType of Change
Testing
docker compose up -d --build)Português
Este PR implementa suporte a mensagens de pagamento PIX e corrige o protocolo binário do WhatsApp para botões, listas e carrosséis, garantindo funcionamento correto tanto no Android quanto no iOS.
🆕 Novo: Pagamento PIX (
SendPix)PixStructcom todos os campos necessários:number,headerTitle,bodyText,merchantName,pixKey,keyType(CPF, CNPJ, EMAIL, PHONE, EVP), e opcionaisamount,description,delay,quotedNativeFlowMessage(review_and_pay) compixPaymentPayloadserializado como JSON dentro deButtonParamsJSONAdditionalNodesbiz/bot obrigatórios (nós XMLbiz+bot) para o fluxo de pagamento empresarialDeviceListMetadataVersion: 2para roteamento correto ponta-a-pontaPOST /send/pixcom validação completa dos campos no handler🔧 Correção: Mensagens com botões (
SendButton)ButtonsMessageeInteractiveMessagedentro deDocumentWithCaptionMessage— obrigatório pelo protocolo atual do WhatsApp para exibir botões no chatMessageSecretde 32 bytes viacrypto/rand— necessário para o iOS renderizar mensagens interativasAdditionalNodesbiz (mesmo padrão da API oficial do WhatsApp Business)REPLY(resposta rápida),URL(cta_url),CALL(cta_call),COPY(cta_copy) eREVIEW_AND_PAYImageUrl/VideoUrl🔧 Correção: Mensagens de lista (
SendList)ListMessagedentro deDocumentWithCaptionMessage— obrigatório pelo protocolo atualMessageSecret(32 bytes)AdditionalNodesbizproduct_listcom atributov=2necessário para renderização da lista🔧 Correção: Mensagens de carrossel (
SendCarousel)MessageSecret(32 bytes) — estava faltando, causando falhas silenciosas no iOSCarouselCardType: HSCROLL_CARDS— obrigatório para o layout de carrossel com rolagem horizontalMessageParamsJSONeMessageVersionnoNativeFlowMessagede cada cardfmt.Sprintfporjson.Marshalpara todos os parâmetros de botõesContextInfovazio forçado noInteractiveMessagedireto que quebrava a renderização do carrossel no iOSQuotedcorretamente viaSendDataStruct🔧 Correção: Dispatcher central
SendMessageAdditionalNodesdeSendDataStructparawhatsmeow.SendRequestExtraButtonsMessagepara mensagens encapsuladas emDocumentWithCaptionMessageListMessagepara mensagens encapsuladas emDocumentWithCaptionMessageInteractiveMessage: aplicaContextInfoapenas em mensagens encapsuladas; omiteContextInfovazio emInteractiveMessagedireto (carrossel) para evitar quebra no iOS🧹 Limpeza
image/jpegnão utilizado (sobra do código de geração de thumbnail)AdditionalNodes *[]waBinary.NodeemSendRequestExtrano fork dowhatsmeow-libpara injeção de nós bizTipo de Mudança
Testes
docker compose up -d --build)