From b5b3024b86cde6bb9b846b1abb0221c182a71bac Mon Sep 17 00:00:00 2001 From: cesar carlos Date: Mon, 27 Jul 2026 21:59:14 +0000 Subject: [PATCH] fix(instance): stop Connect/advanced-settings from wiping config Connect was a destructive upsert: empty subscribe forced events=MESSAGE and empty rabbitmqEnable/webhook/nats/websocket cleared persisted values via full Save. Advanced settings PUT wrote omitted bools as false. Apply connect fields only when explicitly provided, persist with column-selective Updates, and treat AdvancedSettings bools as pointers so partial PUTs leave untouched flags alone. Closes #111 Co-authored-by: Cursor --- pkg/instance/handler/instance_handler.go | 12 +- pkg/instance/model/instance_model.go | 18 ++- .../repository/advanced_settings_test.go | 52 ++++++++ .../repository/instance_repository.go | 60 +++++++-- pkg/instance/service/connect_settings.go | 76 +++++++++++ pkg/instance/service/connect_settings_test.go | 119 ++++++++++++++++++ pkg/instance/service/instance_service.go | 81 ++++++------ 7 files changed, 353 insertions(+), 65 deletions(-) create mode 100644 pkg/instance/repository/advanced_settings_test.go create mode 100644 pkg/instance/service/connect_settings.go create mode 100644 pkg/instance/service/connect_settings_test.go diff --git a/pkg/instance/handler/instance_handler.go b/pkg/instance/handler/instance_handler.go index c269bb20..d78ffc7e 100644 --- a/pkg/instance/handler/instance_handler.go +++ b/pkg/instance/handler/instance_handler.go @@ -649,9 +649,19 @@ func (h *instanceHandler) UpdateAdvancedSettings(c *gin.Context) { return } + // Return persisted settings (full row) so partial PUT responses stay consistent. + persisted, err := h.instanceService.GetAdvancedSettings(instanceId) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "message": "Advanced settings updated successfully", + "settings": settings, + }) + return + } + c.JSON(http.StatusOK, gin.H{ "message": "Advanced settings updated successfully", - "settings": settings, + "settings": persisted, }) } diff --git a/pkg/instance/model/instance_model.go b/pkg/instance/model/instance_model.go index 3fe81861..2e9a4b33 100644 --- a/pkg/instance/model/instance_model.go +++ b/pkg/instance/model/instance_model.go @@ -35,14 +35,15 @@ type Instance struct { IgnoreStatus bool `json:"ignoreStatus" gorm:"default:false"` } -// AdvancedSettings representa as configurações avançadas de uma instância +// AdvancedSettings representa as configurações avançadas de uma instância. +// Bool fields are pointers so omitted JSON keys are not written as false on PUT. type AdvancedSettings struct { - AlwaysOnline bool `json:"alwaysOnline"` - RejectCall bool `json:"rejectCall"` + AlwaysOnline *bool `json:"alwaysOnline"` + RejectCall *bool `json:"rejectCall"` MsgRejectCall string `json:"msgRejectCall"` - ReadMessages bool `json:"readMessages"` - IgnoreGroups bool `json:"ignoreGroups"` - IgnoreStatus bool `json:"ignoreStatus"` + ReadMessages *bool `json:"readMessages"` + IgnoreGroups *bool `json:"ignoreGroups"` + IgnoreStatus *bool `json:"ignoreStatus"` } func (m *Instance) BeforeCreate(tx *gorm.DB) (err error) { @@ -51,3 +52,8 @@ func (m *Instance) BeforeCreate(tx *gorm.DB) (err error) { } return } + +// BoolPtr returns a pointer to v (helper for AdvancedSettings responses/tests). +func BoolPtr(v bool) *bool { + return &v +} diff --git a/pkg/instance/repository/advanced_settings_test.go b/pkg/instance/repository/advanced_settings_test.go new file mode 100644 index 00000000..88b04146 --- /dev/null +++ b/pkg/instance/repository/advanced_settings_test.go @@ -0,0 +1,52 @@ +package instance_repository + +import ( + "testing" + + instance_model "github.com/evolution-foundation/evolution-go/pkg/instance/model" +) + +func TestBuildAdvancedSettingsUpdates(t *testing.T) { + trueVal := true + + t.Run("only AlwaysOnline", func(t *testing.T) { + updates := buildAdvancedSettingsUpdates(&instance_model.AdvancedSettings{ + AlwaysOnline: &trueVal, + }) + if len(updates) != 1 { + t.Fatalf("expected only always_online, got %#v", updates) + } + if updates["always_online"] != true { + t.Fatalf("always_online = %#v, want true", updates["always_online"]) + } + }) + + t.Run("explicit false is written", func(t *testing.T) { + falseVal := false + updates := buildAdvancedSettingsUpdates(&instance_model.AdvancedSettings{ + IgnoreGroups: &falseVal, + }) + if len(updates) != 1 { + t.Fatalf("expected only ignore_groups, got %#v", updates) + } + if updates["ignore_groups"] != false { + t.Fatalf("ignore_groups = %#v, want false", updates["ignore_groups"]) + } + }) + + t.Run("msgRejectCall non-empty", func(t *testing.T) { + updates := buildAdvancedSettingsUpdates(&instance_model.AdvancedSettings{ + MsgRejectCall: "busy", + }) + if updates["msg_reject_call"] != "busy" { + t.Fatalf("msg_reject_call = %#v, want busy", updates["msg_reject_call"]) + } + }) + + t.Run("nil settings", func(t *testing.T) { + updates := buildAdvancedSettingsUpdates(nil) + if len(updates) != 0 { + t.Fatalf("expected empty map, got %#v", updates) + } + }) +} diff --git a/pkg/instance/repository/instance_repository.go b/pkg/instance/repository/instance_repository.go index ecc667ed..84fc69d1 100644 --- a/pkg/instance/repository/instance_repository.go +++ b/pkg/instance/repository/instance_repository.go @@ -26,6 +26,7 @@ type InstanceRepository interface { UpdateQrcode(userId string, qr string) error UpdateProxy(userId string, proxy string) error UpdateJid(userId string, jid string) error + UpdateConnectSettings(instanceId string, updates map[string]interface{}) error GetAllConnectedInstances() ([]*instance_model.Instance, error) GetAllConnectedInstancesByClientName(clientName string) ([]*instance_model.Instance, error) GetAll(clientName string) ([]*instance_model.Instance, error) @@ -116,6 +117,17 @@ func (i *instanceRepository) UpdateJid(userId string, jid string) error { return i.db.Model(&instance_model.Instance{}).Where("id = ?", userId).Update("jid", jid).Error } +func (i *instanceRepository) UpdateConnectSettings(instanceId string, updates map[string]interface{}) error { + if len(updates) == 0 { + return nil + } + err := i.db.Model(&instance_model.Instance{}).Where("id = ?", instanceId).Updates(updates).Error + if err != nil { + logger.LogError("Error updating connect settings in DB: %v", err) + } + return err +} + func (i *instanceRepository) GetAllConnectedInstances() ([]*instance_model.Instance, error) { var instances []*instance_model.Instance err := i.db.Where("connected = ?", true).Find(&instances).Error @@ -181,12 +193,12 @@ func (i *instanceRepository) GetAdvancedSettings(instanceId string) (*instance_m } settings := &instance_model.AdvancedSettings{ - AlwaysOnline: instance.AlwaysOnline, - RejectCall: instance.RejectCall, + AlwaysOnline: instance_model.BoolPtr(instance.AlwaysOnline), + RejectCall: instance_model.BoolPtr(instance.RejectCall), MsgRejectCall: instance.MsgRejectCall, - ReadMessages: instance.ReadMessages, - IgnoreGroups: instance.IgnoreGroups, - IgnoreStatus: instance.IgnoreStatus, + ReadMessages: instance_model.BoolPtr(instance.ReadMessages), + IgnoreGroups: instance_model.BoolPtr(instance.IgnoreGroups), + IgnoreStatus: instance_model.BoolPtr(instance.IgnoreStatus), } return settings, nil @@ -198,13 +210,9 @@ func (i *instanceRepository) UpdateAdvancedSettings(instanceId string, settings return fmt.Errorf("invalid UUID format: %v", err) } - updates := map[string]interface{}{ - "always_online": settings.AlwaysOnline, - "reject_call": settings.RejectCall, - "msg_reject_call": settings.MsgRejectCall, - "read_messages": settings.ReadMessages, - "ignore_groups": settings.IgnoreGroups, - "ignore_status": settings.IgnoreStatus, + updates := buildAdvancedSettingsUpdates(settings) + if len(updates) == 0 { + return nil } err := i.db.Model(&instance_model.Instance{}).Where("id = ?", instanceId).Updates(updates).Error @@ -216,6 +224,34 @@ func (i *instanceRepository) UpdateAdvancedSettings(instanceId string, settings return nil } +// buildAdvancedSettingsUpdates only includes fields explicitly provided (*bool != nil). +// MsgRejectCall is always written on PUT so an empty string can clear the reject message. +func buildAdvancedSettingsUpdates(settings *instance_model.AdvancedSettings) map[string]interface{} { + updates := map[string]interface{}{} + if settings == nil { + return updates + } + if settings.AlwaysOnline != nil { + updates["always_online"] = *settings.AlwaysOnline + } + if settings.RejectCall != nil { + updates["reject_call"] = *settings.RejectCall + } + if settings.ReadMessages != nil { + updates["read_messages"] = *settings.ReadMessages + } + if settings.IgnoreGroups != nil { + updates["ignore_groups"] = *settings.IgnoreGroups + } + if settings.IgnoreStatus != nil { + updates["ignore_status"] = *settings.IgnoreStatus + } + if settings.MsgRejectCall != "" { + updates["msg_reject_call"] = settings.MsgRejectCall + } + return updates +} + func NewInstanceRepository(db *gorm.DB) InstanceRepository { return &instanceRepository{ db: db, diff --git a/pkg/instance/service/connect_settings.go b/pkg/instance/service/connect_settings.go new file mode 100644 index 00000000..4d919c6c --- /dev/null +++ b/pkg/instance/service/connect_settings.go @@ -0,0 +1,76 @@ +package instance_service + +import ( + "strings" + + instance_model "github.com/evolution-foundation/evolution-go/pkg/instance/model" + event_types "github.com/evolution-foundation/evolution-go/pkg/internal/event_types" +) + +// applyConnectSettings mutates instance only for fields explicitly provided. +// Empty subscribe keeps existing Events; defaults to MESSAGE only when Events is empty. +// Empty producer strings keep existing values; send "disabled" or "false" to turn off. +func applyConnectSettings(instance *instance_model.Instance, data *ConnectStruct) map[string]interface{} { + updates := map[string]interface{}{} + if instance == nil || data == nil { + return updates + } + + if len(data.Subscribe) > 0 { + var subscribedEvents []string + if data.Subscribe[0] == "ALL" { + subscribedEvents = append(subscribedEvents, event_types.AllEventTypes...) + } else { + for _, arg := range data.Subscribe { + if !event_types.IsEventType(arg) { + continue + } + subscribedEvents = append(subscribedEvents, arg) + } + } + eventString := strings.Join(subscribedEvents, ",") + instance.Events = eventString + updates["events"] = eventString + } else if instance.Events == "" { + instance.Events = event_types.MESSAGE + updates["events"] = event_types.MESSAGE + } + + if data.WebhookUrl != "" { + instance.Webhook = data.WebhookUrl + updates["webhook"] = data.WebhookUrl + } + if data.RabbitmqEnable != "" { + instance.RabbitmqEnable = data.RabbitmqEnable + updates["rabbitmq_enable"] = data.RabbitmqEnable + } + if data.NatsEnable != "" { + instance.NatsEnable = data.NatsEnable + updates["nats_enable"] = data.NatsEnable + } + if data.WebSocketEnable != "" { + instance.WebSocketEnable = data.WebSocketEnable + updates["web_socket_enable"] = data.WebSocketEnable + } + + return updates +} + +func splitSubscribedEvents(events string) []string { + if events == "" { + return []string{event_types.MESSAGE} + } + parts := strings.Split(events, ",") + out := make([]string, 0, len(parts)) + for _, p := range parts { + p = strings.TrimSpace(p) + if p == "" { + continue + } + out = append(out, p) + } + if len(out) == 0 { + return []string{event_types.MESSAGE} + } + return out +} diff --git a/pkg/instance/service/connect_settings_test.go b/pkg/instance/service/connect_settings_test.go new file mode 100644 index 00000000..547d8856 --- /dev/null +++ b/pkg/instance/service/connect_settings_test.go @@ -0,0 +1,119 @@ +package instance_service + +import ( + "strings" + "testing" + + instance_model "github.com/evolution-foundation/evolution-go/pkg/instance/model" + event_types "github.com/evolution-foundation/evolution-go/pkg/internal/event_types" +) + +func TestApplyConnectSettings(t *testing.T) { + tests := []struct { + name string + instance instance_model.Instance + data ConnectStruct + wantEvents string + wantRabbitmq string + wantWebhook string + wantUpdatesKeys []string + wantNoUpdateKey string + }{ + { + name: "empty subscribe keeps existing events", + instance: instance_model.Instance{ + Events: "MESSAGE,SEND_MESSAGE,CONNECTION", + RabbitmqEnable: "enabled", + }, + data: ConnectStruct{}, + wantEvents: "MESSAGE,SEND_MESSAGE,CONNECTION", + wantRabbitmq: "enabled", + wantUpdatesKeys: nil, + wantNoUpdateKey: "events", + }, + { + name: "empty subscribe and empty events defaults to MESSAGE", + instance: instance_model.Instance{ + Events: "", + }, + data: ConnectStruct{}, + wantEvents: event_types.MESSAGE, + wantUpdatesKeys: []string{"events"}, + }, + { + name: "subscribe ALL expands all event types", + instance: instance_model.Instance{}, + data: ConnectStruct{ + Subscribe: []string{"ALL"}, + }, + wantEvents: strings.Join(event_types.AllEventTypes, ","), + wantUpdatesKeys: []string{"events"}, + }, + { + name: "empty rabbitmqEnable preserves enabled", + instance: instance_model.Instance{ + Events: "MESSAGE", + RabbitmqEnable: "enabled", + }, + data: ConnectStruct{ + Subscribe: []string{"MESSAGE"}, + }, + wantEvents: "MESSAGE", + wantRabbitmq: "enabled", + wantNoUpdateKey: "rabbitmq_enable", + }, + { + name: "rabbitmqEnable disabled is written", + instance: instance_model.Instance{ + Events: "MESSAGE", + RabbitmqEnable: "enabled", + }, + data: ConnectStruct{ + RabbitmqEnable: "disabled", + }, + wantEvents: "MESSAGE", + wantRabbitmq: "disabled", + wantUpdatesKeys: []string{"rabbitmq_enable"}, + }, + { + name: "webhook disabled is written", + instance: instance_model.Instance{ + Events: "MESSAGE", + Webhook: "https://example.com/hook", + }, + data: ConnectStruct{ + WebhookUrl: "disabled", + }, + wantEvents: "MESSAGE", + wantWebhook: "disabled", + wantUpdatesKeys: []string{"webhook"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + inst := tt.instance + updates := applyConnectSettings(&inst, &tt.data) + + if inst.Events != tt.wantEvents { + t.Fatalf("Events = %q, want %q", inst.Events, tt.wantEvents) + } + if tt.wantRabbitmq != "" && inst.RabbitmqEnable != tt.wantRabbitmq { + t.Fatalf("RabbitmqEnable = %q, want %q", inst.RabbitmqEnable, tt.wantRabbitmq) + } + if tt.wantWebhook != "" && inst.Webhook != tt.wantWebhook { + t.Fatalf("Webhook = %q, want %q", inst.Webhook, tt.wantWebhook) + } + for _, key := range tt.wantUpdatesKeys { + if _, ok := updates[key]; !ok { + t.Fatalf("expected updates to contain %q, got %#v", key, updates) + } + } + if tt.wantNoUpdateKey != "" { + if _, ok := updates[tt.wantNoUpdateKey]; ok { + t.Fatalf("expected updates to omit %q, got %#v", tt.wantNoUpdateKey, updates) + } + } + }) + } +} diff --git a/pkg/instance/service/instance_service.go b/pkg/instance/service/instance_service.go index fa43c946..1b14c10e 100644 --- a/pkg/instance/service/instance_service.go +++ b/pkg/instance/service/instance_service.go @@ -17,7 +17,6 @@ import ( "github.com/evolution-foundation/evolution-go/pkg/config" instance_model "github.com/evolution-foundation/evolution-go/pkg/instance/model" instance_repository "github.com/evolution-foundation/evolution-go/pkg/instance/repository" - event_types "github.com/evolution-foundation/evolution-go/pkg/internal/event_types" logger_wrapper "github.com/evolution-foundation/evolution-go/pkg/logger" "github.com/evolution-foundation/evolution-go/pkg/utils" whatsmeow_service "github.com/evolution-foundation/evolution-go/pkg/whatsmeow/service" @@ -188,14 +187,24 @@ func (i instances) Create(data *CreateStruct) (*instance_model.Instance, error) ClientName: i.config.ClientName, } - // Set advanced settings if provided + // Set advanced settings if provided (nil pointers are left as defaults). if data.AdvancedSettings != nil { - instance.AlwaysOnline = data.AdvancedSettings.AlwaysOnline - instance.RejectCall = data.AdvancedSettings.RejectCall + if data.AdvancedSettings.AlwaysOnline != nil { + instance.AlwaysOnline = *data.AdvancedSettings.AlwaysOnline + } + if data.AdvancedSettings.RejectCall != nil { + instance.RejectCall = *data.AdvancedSettings.RejectCall + } instance.MsgRejectCall = data.AdvancedSettings.MsgRejectCall - instance.ReadMessages = data.AdvancedSettings.ReadMessages - instance.IgnoreGroups = data.AdvancedSettings.IgnoreGroups - instance.IgnoreStatus = data.AdvancedSettings.IgnoreStatus + if data.AdvancedSettings.ReadMessages != nil { + instance.ReadMessages = *data.AdvancedSettings.ReadMessages + } + if data.AdvancedSettings.IgnoreGroups != nil { + instance.IgnoreGroups = *data.AdvancedSettings.IgnoreGroups + } + if data.AdvancedSettings.IgnoreStatus != nil { + instance.IgnoreStatus = *data.AdvancedSettings.IgnoreStatus + } } createdInstance, err := i.instanceRepository.Create(instance) @@ -207,45 +216,36 @@ func (i instances) Create(data *CreateStruct) (*instance_model.Instance, error) } func (i instances) Connect(data *ConnectStruct, instance *instance_model.Instance) (*instance_model.Instance, string, string, error) { - var subscribedEvents []string - i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Processing subscribe events: %v", instance.Id, data.Subscribe) - if len(data.Subscribe) == 0 { - subscribedEvents = append(subscribedEvents, event_types.MESSAGE) - } else if len(data.Subscribe) > 0 && data.Subscribe[0] == "ALL" { - for _, event := range event_types.AllEventTypes { - subscribedEvents = append(subscribedEvents, event) - } - } else { - for _, arg := range data.Subscribe { - if !event_types.IsEventType(arg) { - i.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Message type discarded '%s'", instance.Id, arg) - continue - } - subscribedEvents = append(subscribedEvents, arg) - } - } + oldEvents := instance.Events + oldRabbitmq := instance.RabbitmqEnable - eventString := strings.Join(subscribedEvents, ",") + updates := applyConnectSettings(instance, data) - instance.Events = eventString - instance.Webhook = data.WebhookUrl - instance.RabbitmqEnable = data.RabbitmqEnable - instance.NatsEnable = data.NatsEnable - instance.WebSocketEnable = data.WebSocketEnable + if instance.Events != oldEvents { + i.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] events changed: %q -> %q", instance.Id, oldEvents, instance.Events) + } + if instance.RabbitmqEnable != oldRabbitmq { + i.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] rabbitmqEnable changed: %q -> %q", instance.Id, oldRabbitmq, instance.RabbitmqEnable) + } - err := i.instanceRepository.Update(instance) - if err != nil { - i.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error updating instance: %s", instance.Id, err) - return nil, "", "", err + if len(updates) > 0 { + err := i.instanceRepository.UpdateConnectSettings(instance.Id, updates) + if err != nil { + i.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error updating instance: %s", instance.Id, err) + return nil, "", "", err + } } + subscribedEvents := splitSubscribedEvents(instance.Events) + eventString := instance.Events + // Verifica se a instância já está rodando isInstanceRunning := i.clientPointer[instance.Id] != nil // Sincroniza as configurações na instância em execução (se já estiver conectada) - err = i.whatsmeowService.UpdateInstanceSettings(instance.Id) + err := i.whatsmeowService.UpdateInstanceSettings(instance.Id) if err != nil { i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Instance not in runtime yet, will be updated when connected", instance.Id) isInstanceRunning = false @@ -285,17 +285,6 @@ func (i instances) Connect(data *ConnectStruct, instance *instance_model.Instanc i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Instance already running, settings updated without restarting client", instance.Id) } - // logger.LogInfo("Waiting 1 seconds") - // time.Sleep(1000 * time.Millisecond) - - // if i.clientPointer[instance.Id] != nil { - // if !i.clientPointer[instance.Id].IsConnected() { - // return instance, "", "", fmt.Errorf("failed to connect") - // } - // } else { - // return instance, "", "", fmt.Errorf("failed to connect") - // } - return instance, instance.Jid, eventString, nil }