From 8cf30e93c538ed361061407fad5f666ae3a7ec3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20M=C3=BChlbauer?= Date: Sat, 9 May 2026 11:37:05 -0300 Subject: [PATCH 1/5] Add CTWA referral passthrough and persistence --- docs/wiki/recursos-avancados/events-system.md | 10 +++ go.mod | 4 +- go.sum | 4 ++ pkg/message/model/message_model.go | 13 ++-- pkg/message/repository/message_repository.go | 7 +- .../repository/message_repository_test.go | 64 ++++++++++++++++++ pkg/whatsmeow/service/referral.go | 50 ++++++++++++++ pkg/whatsmeow/service/referral_test.go | 66 +++++++++++++++++++ pkg/whatsmeow/service/whatsmeow.go | 40 +++++++---- 9 files changed, 240 insertions(+), 18 deletions(-) create mode 100644 pkg/message/repository/message_repository_test.go create mode 100644 pkg/whatsmeow/service/referral.go create mode 100644 pkg/whatsmeow/service/referral_test.go diff --git a/docs/wiki/recursos-avancados/events-system.md b/docs/wiki/recursos-avancados/events-system.md index ef653007..81ac61dd 100644 --- a/docs/wiki/recursos-avancados/events-system.md +++ b/docs/wiki/recursos-avancados/events-system.md @@ -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)**: diff --git a/go.mod b/go.mod index ef2a864e..fa8b05b1 100644 --- a/go.mod +++ b/go.mod @@ -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 ) @@ -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 diff --git a/go.sum b/go.sum index 4ec26f67..4a35e866 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/message/model/message_model.go b/pkg/message/model/message_model.go index 3644ca5f..dd4544c2 100644 --- a/pkg/message/model/message_model.go +++ b/pkg/message/model/message_model.go @@ -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) { diff --git a/pkg/message/repository/message_repository.go b/pkg/message/repository/message_repository.go index 5a61ddb2..66f28409 100644 --- a/pkg/message/repository/message_repository.go +++ b/pkg/message/repository/message_repository.go @@ -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 } diff --git a/pkg/message/repository/message_repository_test.go b/pkg/message/repository/message_repository_test.go new file mode 100644 index 00000000..525b17e5 --- /dev/null +++ b/pkg/message/repository/message_repository_test.go @@ -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) + } +} diff --git a/pkg/whatsmeow/service/referral.go b/pkg/whatsmeow/service/referral.go new file mode 100644 index 00000000..987d5d34 --- /dev/null +++ b/pkg/whatsmeow/service/referral.go @@ -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 +} diff --git a/pkg/whatsmeow/service/referral_test.go b/pkg/whatsmeow/service/referral_test.go new file mode 100644 index 00000000..53696140 --- /dev/null +++ b/pkg/whatsmeow/service/referral_test.go @@ -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 +} diff --git a/pkg/whatsmeow/service/whatsmeow.go b/pkg/whatsmeow/service/whatsmeow.go index 78ec6c17..f007a832 100644 --- a/pkg/whatsmeow/service/whatsmeow.go +++ b/pkg/whatsmeow/service/whatsmeow.go @@ -1161,6 +1161,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) @@ -1256,6 +1258,10 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { dataMap["isQuoted"] = true } + if len(referral) > 0 { + dataMap["referral"] = referral + } + if mycli.config.WebhookFiles { isMedia := false @@ -1514,6 +1520,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, + } + + go mycli.messageRepository.InsertMessage(message) + } + // ===== BUTTON CLICK EVENT DETECTION ===== // Detecta cliques em botões e emite evento separado "ButtonClick" // Suporta 3 formatos: ButtonsResponseMessage, InteractiveResponseMessage (NativeFlow), TemplateButtonReplyMessage @@ -1577,17 +1595,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, From a3fe577115918b1e58186cdf342a65ba5d2d6780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20M=C3=BChlbauer?= Date: Sat, 9 May 2026 11:54:05 -0300 Subject: [PATCH 2/5] Log message persistence failures --- pkg/whatsmeow/service/whatsmeow.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pkg/whatsmeow/service/whatsmeow.go b/pkg/whatsmeow/service/whatsmeow.go index f007a832..3c133858 100644 --- a/pkg/whatsmeow/service/whatsmeow.go +++ b/pkg/whatsmeow/service/whatsmeow.go @@ -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 @@ -1529,7 +1541,7 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { Referral: referral, } - go mycli.messageRepository.InsertMessage(message) + mycli.persistMessageAsync(message) } // ===== BUTTON CLICK EVENT DETECTION ===== @@ -1661,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 { @@ -1686,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()) From dfc0f59200304829ee5f5856285f91cbea68d081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20M=C3=BChlbauer?= Date: Fri, 26 Jun 2026 12:28:20 -0300 Subject: [PATCH 3/5] Trim referral feature diff --- go.mod | 4 +- go.sum | 6 -- pkg/message/model/message_model.go | 14 ++- pkg/message/repository/message_repository.go | 8 +- .../repository/message_repository_test.go | 85 +++++++------------ 5 files changed, 43 insertions(+), 74 deletions(-) diff --git a/go.mod b/go.mod index fa8b05b1..ef2a864e 100644 --- a/go.mod +++ b/go.mod @@ -27,8 +27,7 @@ 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/driver/sqlite v1.6.0 - gorm.io/gorm v1.30.0 + gorm.io/gorm v1.25.10 modernc.org/sqlite v1.33.1 ) @@ -68,7 +67,6 @@ 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 diff --git a/go.sum b/go.sum index 4a35e866..fa1072f3 100644 --- a/go.sum +++ b/go.sum @@ -115,8 +115,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= -github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.80 h1:2mdUHXEykRdY/BigLt3Iuu1otL0JTogT0Nmltg0wujk= @@ -250,12 +248,8 @@ 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= diff --git a/pkg/message/model/message_model.go b/pkg/message/model/message_model.go index dd4544c2..321119fe 100644 --- a/pkg/message/model/message_model.go +++ b/pkg/message/model/message_model.go @@ -1,19 +1,17 @@ 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"` - Referral json.RawMessage `json:"referral,omitempty" gorm:"type:jsonb"` + 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 []byte `json:"referral,omitempty"` } func (m *Message) BeforeCreate(tx *gorm.DB) (err error) { diff --git a/pkg/message/repository/message_repository.go b/pkg/message/repository/message_repository.go index 66f28409..c8b74e40 100644 --- a/pkg/message/repository/message_repository.go +++ b/pkg/message/repository/message_repository.go @@ -17,15 +17,19 @@ type messageRepository struct { db *gorm.DB } -func (m *messageRepository) InsertMessage(message message_model.Message) error { +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(updates), + DoUpdates: clause.AssignmentColumns(messageUpdateColumns(message)), }).Create(&message).Error } diff --git a/pkg/message/repository/message_repository_test.go b/pkg/message/repository/message_repository_test.go index 525b17e5..8b774c62 100644 --- a/pkg/message/repository/message_repository_test.go +++ b/pkg/message/repository/message_repository_test.go @@ -1,64 +1,39 @@ 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) - } +func TestMessageUpdateColumns(t *testing.T) { + t.Run("without referral", func(t *testing.T) { + updates := messageUpdateColumns(message_model.Message{}) + + expected := []string{"timestamp", "status", "source"} + if len(updates) != len(expected) { + t.Fatalf("expected %d update columns, got %d", len(expected), len(updates)) + } + + for i, column := range expected { + if updates[i] != column { + t.Fatalf("expected column %q at position %d, got %q", column, i, updates[i]) + } + } + }) + + t.Run("with referral", func(t *testing.T) { + updates := messageUpdateColumns(message_model.Message{Referral: []byte(`{"ctwaClid":"abc123"}`)}) + + expected := []string{"timestamp", "status", "source", "referral"} + if len(updates) != len(expected) { + t.Fatalf("expected %d update columns, got %d", len(expected), len(updates)) + } + + for i, column := range expected { + if updates[i] != column { + t.Fatalf("expected column %q at position %d, got %q", column, i, updates[i]) + } + } + }) } From 80c95851289e6583c51284027f738ba4f40ebb92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20M=C3=BChlbauer?= Date: Fri, 26 Jun 2026 16:29:01 -0300 Subject: [PATCH 4/5] Restore referral jsonb schema --- go.mod | 1 + pkg/message/model/message_model.go | 14 +-- .../repository/message_repository_test.go | 91 ++++++++++++++----- 3 files changed, 75 insertions(+), 31 deletions(-) diff --git a/go.mod b/go.mod index ef2a864e..7e912ace 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( ) require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/beeper/argo-go v1.1.2 // indirect diff --git a/pkg/message/model/message_model.go b/pkg/message/model/message_model.go index 321119fe..dd4544c2 100644 --- a/pkg/message/model/message_model.go +++ b/pkg/message/model/message_model.go @@ -1,17 +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"` - Referral []byte `json:"referral,omitempty"` + 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) { diff --git a/pkg/message/repository/message_repository_test.go b/pkg/message/repository/message_repository_test.go index 8b774c62..2ff2d1bd 100644 --- a/pkg/message/repository/message_repository_test.go +++ b/pkg/message/repository/message_repository_test.go @@ -1,39 +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 TestMessageUpdateColumns(t *testing.T) { - t.Run("without referral", func(t *testing.T) { - updates := messageUpdateColumns(message_model.Message{}) +func TestInsertMessagePreservesReferralOnStatusUpdate(t *testing.T) { + sqlDB, _, err := sqlmock.New() + if err != nil { + t.Fatalf("open sqlmock db: %v", err) + } + defer sqlDB.Close() - expected := []string{"timestamp", "status", "source"} - if len(updates) != len(expected) { - t.Fatalf("expected %d update columns, got %d", len(expected), len(updates)) - } - - for i, column := range expected { - if updates[i] != column { - t.Fatalf("expected column %q at position %d, got %q", column, i, updates[i]) - } - } + 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) + } - t.Run("with referral", func(t *testing.T) { - updates := messageUpdateColumns(message_model.Message{Referral: []byte(`{"ctwaClid":"abc123"}`)}) + repo := NewMessageRepository(gormDB) + referral := json.RawMessage(`{"ctwaClid":"abc123","showAdAttribution":true}`) - expected := []string{"timestamp", "status", "source", "referral"} - if len(updates) != len(expected) { - t.Fatalf("expected %d update columns, got %d", len(expected), len(updates)) - } + initial := message_model.Message{ + MessageID: "msg-1", + Timestamp: "2026-05-09 10:00:00", + Status: "Received", + Source: "1551999999999", + Referral: referral, + } - for i, column := range expected { - if updates[i] != column { - t.Fatalf("expected column %q at position %d, got %q", column, i, updates[i]) - } - } - }) + 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) + } } From 4e018ebca7414bccd40478e88344155aa0684f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20M=C3=BChlbauer?= Date: Fri, 26 Jun 2026 18:17:35 -0300 Subject: [PATCH 5/5] Tidy sqlmock dependency --- go.mod | 2 +- go.sum | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 7e912ace..25a94471 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -32,7 +33,6 @@ require ( ) require ( - github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/beeper/argo-go v1.1.2 // indirect diff --git a/go.sum b/go.sum index fa1072f3..6e766130 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -115,6 +116,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= +github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.80 h1:2mdUHXEykRdY/BigLt3Iuu1otL0JTogT0Nmltg0wujk=