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
10 changes: 10 additions & 0 deletions docs/wiki/recursos-avancados/events-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,21 @@ Content-Type: application/json
"message": {
"conversation": "Olá!"
},
"referral": {
"ctwaClid": "FAKE_CLID_abc123xyz",
"sourceURL": "https://fb.me/fake-ad-link",
"sourceID": "123456789012345",
"sourceType": "ad",
"showAdAttribution": true,
"automatedGreetingMessageShown": true
},
"messageTimestamp": "1699999999"
}
}
```

> **Nota**: quando a conversa vier de um anúncio Click-to-WhatsApp, o payload também inclui `data.referral` com os metadados brutos de `contextInfo.externalAdReply`, e o mesmo JSON é persistido no registro da mensagem.

### Implementação no Servidor Receptor

**Node.js (Express)**:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/EvolutionAPI/evolution-go
go 1.25.0

require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/chai2010/webp v1.1.1
github.com/gabriel-vasile/mimetype v1.4.5
github.com/gin-gonic/gin v1.10.0
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
Expand Down
13 changes: 8 additions & 5 deletions pkg/message/model/message_model.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package message_model

import (
"encoding/json"

"github.com/google/uuid"
"gorm.io/gorm"
)

type Message struct {
Id string `json:"id" gorm:"type:uuid;primaryKey"`
MessageID string `json:"message_id" gorm:"unique"`
Timestamp string `json:"timestamp"`
Status string `json:"status"`
Source string `json:"source"`
Id string `json:"id" gorm:"type:uuid;primaryKey"`
MessageID string `json:"message_id" gorm:"unique"`
Timestamp string `json:"timestamp"`
Status string `json:"status"`
Source string `json:"source"`
Referral json.RawMessage `json:"referral,omitempty" gorm:"type:jsonb"`
}

func (m *Message) BeforeCreate(tx *gorm.DB) (err error) {
Expand Down
11 changes: 10 additions & 1 deletion pkg/message/repository/message_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,19 @@ type messageRepository struct {
db *gorm.DB
}

func messageUpdateColumns(message message_model.Message) []string {
updates := []string{"timestamp", "status", "source"}
if len(message.Referral) > 0 {
updates = append(updates, "referral")
}

return updates
}

func (m *messageRepository) InsertMessage(message message_model.Message) error {
return m.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "message_id"}},
DoUpdates: clause.AssignmentColumns([]string{"timestamp", "status", "source"}),
DoUpdates: clause.AssignmentColumns(messageUpdateColumns(message)),
}).Create(&message).Error
}

Expand Down
80 changes: 80 additions & 0 deletions pkg/message/repository/message_repository_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package message_repository

import (
"bytes"
"encoding/json"
"log"
"strings"
"testing"

"github.com/DATA-DOG/go-sqlmock"
message_model "github.com/EvolutionAPI/evolution-go/pkg/message/model"
"gorm.io/driver/postgres"
"gorm.io/gorm"
gormlogger "gorm.io/gorm/logger"
)

func TestInsertMessagePreservesReferralOnStatusUpdate(t *testing.T) {
sqlDB, _, err := sqlmock.New()
if err != nil {
t.Fatalf("open sqlmock db: %v", err)
}
defer sqlDB.Close()

var logBuffer bytes.Buffer
gormDB, err := gorm.Open(postgres.New(postgres.Config{
Conn: sqlDB,
WithoutReturning: true,
}), &gorm.Config{
DryRun: true,
SkipDefaultTransaction: true,
Logger: gormlogger.New(
log.New(&logBuffer, "", 0),
gormlogger.Config{LogLevel: gormlogger.Info, Colorful: false},
),
})
if err != nil {
t.Fatalf("open gorm db: %v", err)
}

repo := NewMessageRepository(gormDB)
referral := json.RawMessage(`{"ctwaClid":"abc123","showAdAttribution":true}`)

initial := message_model.Message{
MessageID: "msg-1",
Timestamp: "2026-05-09 10:00:00",
Status: "Received",
Source: "1551999999999",
Referral: referral,
}

if err := repo.InsertMessage(initial); err != nil {
t.Fatalf("insert initial message: %v", err)
}

initialSQL := logBuffer.String()
if !strings.Contains(initialSQL, `"referral"="excluded"."referral"`) {
t.Fatalf("expected initial upsert SQL to update referral, got %q", initialSQL)
}

logBuffer.Reset()

updated := message_model.Message{
MessageID: "msg-1",
Timestamp: "2026-05-09 10:05:00",
Status: "Read",
Source: "1551999999999",
}

if err := repo.InsertMessage(updated); err != nil {
t.Fatalf("insert updated message: %v", err)
}

updatedSQL := logBuffer.String()
if strings.Contains(updatedSQL, `"referral"="excluded"."referral"`) {
t.Fatalf("expected updated upsert SQL to omit referral update, got %q", updatedSQL)
}
if !strings.Contains(updatedSQL, `"timestamp"="excluded"."timestamp","status"="excluded"."status","source"="excluded"."source"`) {
t.Fatalf("expected updated upsert SQL to keep core columns, got %q", updatedSQL)
}
}
50 changes: 50 additions & 0 deletions pkg/whatsmeow/service/referral.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package whatsmeow_service

import (
"encoding/json"

"go.mau.fi/whatsmeow/proto/waE2E"
"google.golang.org/protobuf/encoding/protojson"
)

func extractReferralFromMessage(message *waE2E.Message) json.RawMessage {
contextInfo := getContextInfoFromMessage(message)
if contextInfo == nil || contextInfo.GetExternalAdReply() == nil {
return nil
}

referral, err := protojson.Marshal(contextInfo.GetExternalAdReply())
if err != nil || len(referral) == 0 {
return nil
}

return json.RawMessage(referral)
}

func getContextInfoFromMessage(message *waE2E.Message) *waE2E.ContextInfo {
if message == nil {
return nil
}

if extendedText := message.GetExtendedTextMessage(); extendedText != nil {
return extendedText.GetContextInfo()
}

if image := message.GetImageMessage(); image != nil {
return image.GetContextInfo()
}

if audio := message.GetAudioMessage(); audio != nil {
return audio.GetContextInfo()
}

if document := message.GetDocumentMessage(); document != nil {
return document.GetContextInfo()
}

if video := message.GetVideoMessage(); video != nil {
return video.GetContextInfo()
}

return nil
}
66 changes: 66 additions & 0 deletions pkg/whatsmeow/service/referral_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package whatsmeow_service

import (
"encoding/json"
"testing"

"go.mau.fi/whatsmeow/proto/waE2E"
)

func TestExtractReferralFromMessage(t *testing.T) {
message := &waE2E.Message{
ExtendedTextMessage: &waE2E.ExtendedTextMessage{
Text: stringPtr("Hello! I want to know more about this ad."),
ContextInfo: &waE2E.ContextInfo{
ExternalAdReply: &waE2E.ContextInfo_ExternalAdReplyInfo{
Title: stringPtr("Your Dream Farm"),
Body: stringPtr("Discover exclusive rural properties in the countryside."),
CtwaClid: stringPtr("FAKE_CLID_abc123xyz"),
Ref: stringPtr("landing_page_01"),
SourceApp: stringPtr("facebook"),
SourceType: stringPtr("ad"),
SourceID: stringPtr("123456789012345"),
SourceURL: stringPtr("https://fb.me/fake-ad-link"),
ShowAdAttribution: boolPtr(true),
ClickToWhatsappCall: boolPtr(true),
AutomatedGreetingMessageShown: boolPtr(true),
GreetingMessageBody: stringPtr("Hello! I want to know more about this ad."),
},
},
},
}

referral := extractReferralFromMessage(message)
if len(referral) == 0 {
t.Fatal("expected referral payload, got empty")
}

var got map[string]any
if err := json.Unmarshal(referral, &got); err != nil {
t.Fatalf("unmarshal referral: %v", err)
}

if got["ctwaClid"] != "FAKE_CLID_abc123xyz" {
t.Fatalf("expected ctwaClid to be preserved, got %#v", got["ctwaClid"])
}

if got["sourceURL"] != "https://fb.me/fake-ad-link" {
t.Fatalf("expected sourceURL to be preserved, got %#v", got["sourceURL"])
}

if got["showAdAttribution"] != true {
t.Fatalf("expected showAdAttribution to be preserved, got %#v", got["showAdAttribution"])
}

if got["automatedGreetingMessageShown"] != true {
t.Fatalf("expected automatedGreetingMessageShown to be preserved, got %#v", got["automatedGreetingMessageShown"])
}
}

func stringPtr(v string) *string {
return &v
}

func boolPtr(v bool) *bool {
return &v
}
56 changes: 43 additions & 13 deletions pkg/whatsmeow/service/whatsmeow.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,18 @@ type MyClient struct {
qrcodeCount int
}

func (mycli *MyClient) persistMessageAsync(message message_model.Message) {
if mycli == nil || mycli.messageRepository == nil {
return
}

go func() {
if err := mycli.messageRepository.InsertMessage(message); err != nil {
mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to persist message %s: %v", mycli.userID, message.MessageID, err)
}
}()
}

type ClientData struct {
Instance *instance_model.Instance
Subscriptions []string
Expand Down Expand Up @@ -1161,6 +1173,8 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) {
dataMap = make(map[string]interface{})
}

referral := extractReferralFromMessage(evt.Message)

if evt.Message.GetPollUpdateMessage() != nil {
fmt.Printf("[POLL DEBUG] 🎯 PollUpdateMessage detected!\n")
fmt.Printf("[POLL DEBUG] � BEFORE accessing evt.Info - Sender: %s, Server: %s\n", evt.Info.Sender.String(), evt.Info.Sender.Server)
Expand Down Expand Up @@ -1256,6 +1270,10 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) {
dataMap["isQuoted"] = true
}

if len(referral) > 0 {
dataMap["referral"] = referral
}

if mycli.config.WebhookFiles {
isMedia := false

Expand Down Expand Up @@ -1514,6 +1532,18 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) {

postMap["data"] = dataMap

if mycli.config.DatabaseSaveMessages {
message := message_model.Message{
MessageID: evt.Info.ID,
Timestamp: evt.Info.Timestamp.Format("2006-01-02 15:04:05"),
Status: "Received",
Source: evt.Info.Chat.ToNonAD().User,
Referral: referral,
}

mycli.persistMessageAsync(message)
}

// ===== BUTTON CLICK EVENT DETECTION =====
// Detecta cliques em botões e emite evento separado "ButtonClick"
// Suporta 3 formatos: ButtonsResponseMessage, InteractiveResponseMessage (NativeFlow), TemplateButtonReplyMessage
Expand Down Expand Up @@ -1577,17 +1607,17 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) {
buttonClickMap := map[string]interface{}{
"event": "ButtonClick",
"data": map[string]interface{}{
"buttonId": buttonClickData["buttonId"],
"buttonText": buttonClickData["buttonText"],
"type": buttonClickData["type"],
"phone": dataMap["Sender"],
"jid": dataMap["Sender"],
"pushName": dataMap["PushName"],
"messageId": dataMap["ID"],
"chat": dataMap["Chat"],
"fromMe": dataMap["FromMe"],
"timestamp": evt.Info.Timestamp.Unix(),
"extraData": buttonClickData,
"buttonId": buttonClickData["buttonId"],
"buttonText": buttonClickData["buttonText"],
"type": buttonClickData["type"],
"phone": dataMap["Sender"],
"jid": dataMap["Sender"],
"pushName": dataMap["PushName"],
"messageId": dataMap["ID"],
"chat": dataMap["Chat"],
"fromMe": dataMap["FromMe"],
"timestamp": evt.Info.Timestamp.Unix(),
"extraData": buttonClickData,
},
"instanceToken": mycli.token,
"instanceId": mycli.userID,
Expand Down Expand Up @@ -1643,7 +1673,7 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) {
message.Source = evt.Chat.ToNonAD().User

if mycli.config.DatabaseSaveMessages {
go mycli.messageRepository.InsertMessage(message)
mycli.persistMessageAsync(message)
}
}
} else {
Expand All @@ -1668,7 +1698,7 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) {
mycli.processedMessages.Set(messageKey, true, 30*time.Minute)

if mycli.config.DatabaseSaveMessages {
go mycli.messageRepository.InsertMessage(message)
mycli.persistMessageAsync(message)
}

mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Message delivered to %s", mycli.userID, evt.SourceString())
Expand Down