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
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ require (
google.golang.org/protobuf v1.36.11
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.10
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.30.0
modernc.org/sqlite v1.33.1
)

Expand Down Expand Up @@ -67,6 +68,7 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.34 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,12 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
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
7 changes: 6 additions & 1 deletion pkg/message/repository/message_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ type messageRepository struct {
}

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

return m.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "message_id"}},
DoUpdates: clause.AssignmentColumns([]string{"timestamp", "status", "source"}),
DoUpdates: clause.AssignmentColumns(updates),
}).Create(&message).Error
}

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

import (
"encoding/json"
"testing"

message_model "github.com/EvolutionAPI/evolution-go/pkg/message/model"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)

func TestInsertMessagePreservesReferralOnStatusUpdate(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite db: %v", err)
}

if err := db.AutoMigrate(&message_model.Message{}); err != nil {
t.Fatalf("auto migrate: %v", err)
}

repo := NewMessageRepository(db)
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)
}

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)
}

got, err := repo.GetMessageByID("msg-1")
if err != nil {
t.Fatalf("get message: %v", err)
}

if got == nil {
t.Fatal("expected message, got nil")
}

if got.Status != "Read" {
t.Fatalf("expected status Read, got %q", got.Status)
}

if string(got.Referral) != string(referral) {
t.Fatalf("expected referral %s, got %s", referral, got.Referral)
}
}
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