From b56692e4d464e737bd026f14fee2b54985853f61 Mon Sep 17 00:00:00 2001 From: Ismael Ash Date: Mon, 30 Mar 2026 22:06:08 -0300 Subject: [PATCH 1/4] docs: improve setup instructions and add docker compose Update README to recommend using the new start.sh script for easier Docker setup. Add docker-compose.yml to define all required services (PostgreSQL, RabbitMQ, MinIO). Add start.sh script to automate submodule initialization, environment configuration, and container startup. --- README.md | 9 +++-- docker-compose.yml | 86 ++++++++++++++++++++++++++++++++++++++++++++++ start.sh | 68 ++++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 docker-compose.yml create mode 100755 start.sh diff --git a/README.md b/README.md index bc89966..3bbc3fe 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,18 @@ Evolution Go is a high-performance WhatsApp API built in Go, part of the [Evolut ### Docker (Recommended) +The easiest and fastest way to start the project with all its dependencies (PostgreSQL, RabbitMQ, MinIO) is by using our automated script: + ```bash git clone https://github.com/EvolutionAPI/evolution-go.git cd evolution-go -make docker-build -make docker-run + +# Runs the script that will prepare dependencies, variables, and start the containers +bash ./start.sh ``` +> **Note:** The `start.sh` script will initialize submodules, create your `.env` based on `.env.example` (adjusting hosts for Docker), and run `docker compose up -d --build`. + ### Local Development ```bash diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..92c617c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,86 @@ +version: '3.8' + +services: + evolution-go: + build: + context: . + dockerfile: Dockerfile + image: evolution-go:latest + container_name: evolution-go + restart: unless-stopped + ports: + - "8080:8080" + env_file: + - .env + volumes: + - evolution_data:/app/dbdata + - evolution_logs:/app/logs + networks: + - evolution_network + depends_on: + - postgres + - rabbitmq + - minio + + postgres: + image: postgres:15-alpine + container_name: evolution-postgres + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: root + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./docker/examples/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql + networks: + - evolution_network + + rabbitmq: + image: rabbitmq:3-management-alpine + container_name: evolution-rabbitmq + restart: unless-stopped + environment: + RABBITMQ_DEFAULT_USER: admin + RABBITMQ_DEFAULT_PASS: admin + RABBITMQ_DEFAULT_VHOST: default + ports: + - "5672:5672" + - "15672:15672" + volumes: + - rabbitmq_data:/var/lib/rabbitmq + networks: + - evolution_network + + minio: + image: minio/minio:latest + container_name: evolution-minio + restart: unless-stopped + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio_data:/data + networks: + - evolution_network + +volumes: + evolution_data: + driver: local + evolution_logs: + driver: local + postgres_data: + driver: local + rabbitmq_data: + driver: local + minio_data: + driver: local + +networks: + evolution_network: + driver: bridge diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..343dbed --- /dev/null +++ b/start.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# Output colors +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${BLUE}=== Initializing Evolution Go via Docker ===${NC}\n" + +# 1. Update git submodules (fixes empty folder error in whatsmeow-lib) +echo -e "${YELLOW}[1/4] Preparing dependencies (git submodules)...${NC}" +git submodule update --init --recursive +if [ $? -ne 0 ]; then + echo -e "${RED}Error initializing submodules. Please check if git is installed.${NC}" + exit 1 +fi +echo -e "${GREEN}Dependencies prepared successfully!${NC}\n" + +# 2. Create .env file if it doesn't exist +echo -e "${YELLOW}[2/4] Configuring environment variables...${NC}" +if [ ! -f .env ]; then + if [ -f .env.example ]; then + cp .env.example .env + + # Automatically adjust for Docker Compose + sed -i 's/localhost:5432/postgres:5432/g' .env + sed -i 's/localhost:5672/rabbitmq:5672/g' .env + sed -i 's/localhost:9000/minio:9000/g' .env + + echo -e "${GREEN}.env file created from .env.example and adjusted for Docker!${NC}\n" + else + echo -e "${RED}.env.example file not found!${NC}" + exit 1 + fi +else + echo -e "${GREEN}.env file already exists, keeping current configuration.${NC}\n" +fi + +# 3. Build and start containers via Docker Compose +echo -e "${YELLOW}[3/4] Building image and starting containers (This may take a few minutes)...${NC}" +if command -v docker-compose &> /dev/null; then + docker-compose up -d --build +elif docker compose version &> /dev/null; then + docker compose up -d --build +else + echo -e "${RED}Docker Compose not found. Please install Docker Compose first.${NC}" + exit 1 +fi + +if [ $? -ne 0 ]; then + echo -e "${RED}Error starting Docker containers.${NC}" + exit 1 +fi +echo -e "${GREEN}Containers started successfully!${NC}\n" + +# 4. Finalization +echo -e "${BLUE}=== All set! ===${NC}" +echo -e "Evolution Go and its dependencies are running in the background." +echo -e "\nServices available:" +echo -e "- Evolution Go API: ${GREEN}http://localhost:8080${NC}" +echo -e "- Swagger Docs: ${GREEN}http://localhost:8080/swagger/index.html${NC}" +echo -e "- Manager UI: ${GREEN}http://localhost:8080/manager/login${NC}" +echo -e "- RabbitMQ Admin: ${GREEN}http://localhost:15672${NC} (admin/admin)" +echo -e "- MinIO Console: ${GREEN}http://localhost:9001${NC} (minioadmin/minioadmin)" +echo -e "\nTo view API logs, run:" +echo -e "${YELLOW}docker compose logs -f evolution-go${NC}" From 57d10a567c54b4456b6971e5dd127540995f764a Mon Sep 17 00:00:00 2001 From: Ismael Ash Date: Wed, 8 Apr 2026 14:23:34 -0300 Subject: [PATCH 2/4] chore(docker): simplify service container names and remove explicit image tag Remove explicit image tag for evolution-go service to rely on build context Standardize container names by removing project prefix (evolution-) Add POSTGRES_DB environment variable for postgres service --- docker-compose.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 92c617c..53018b5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,6 @@ services: build: context: . dockerfile: Dockerfile - image: evolution-go:latest container_name: evolution-go restart: unless-stopped ports: @@ -24,11 +23,12 @@ services: postgres: image: postgres:15-alpine - container_name: evolution-postgres + container_name: postgres restart: unless-stopped environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: root + POSTGRES_DB: postgres ports: - "5432:5432" volumes: @@ -39,7 +39,7 @@ services: rabbitmq: image: rabbitmq:3-management-alpine - container_name: evolution-rabbitmq + container_name: rabbitmq restart: unless-stopped environment: RABBITMQ_DEFAULT_USER: admin @@ -55,7 +55,7 @@ services: minio: image: minio/minio:latest - container_name: evolution-minio + container_name: minio restart: unless-stopped command: server /data --console-address ":9001" environment: From c9de08920e807ae3b2ccf75c466562ad02a4a76b Mon Sep 17 00:00:00 2001 From: Ismael Ash Date: Wed, 8 Apr 2026 14:41:19 -0300 Subject: [PATCH 3/4] feat: add configurable telemetry with silent error handling Introduce TELEMETRY_ENABLED environment variable to control telemetry collection. When disabled, middleware skips telemetry entirely. When enabled, common network errors (DNS lookup failures, connection refused) are silently ignored to avoid log spam in restricted environments. --- .env.example | 1 + cmd/evolution-go/main.go | 2 +- pkg/config/config.go | 6 ++++-- pkg/config/env/env.go | 1 + pkg/telemetry/telemetry.go | 20 ++++++++++++++++---- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 831e431..05ded31 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,7 @@ LOGTYPE=console WEBHOOKFILES=true CONNECT_ON_STARTUP=true +TELEMETRY_ENABLED=true OS_NAME=Evolution GO diff --git a/cmd/evolution-go/main.go b/cmd/evolution-go/main.go index ddc760e..6ada342 100644 --- a/cmd/evolution-go/main.go +++ b/cmd/evolution-go/main.go @@ -200,7 +200,7 @@ func setupRouter(db *gorm.DB, authDB *sql.DB, sqliteDB *sql.DB, config *config.C // NOVO: PollHandler usando PollService já inicializado no whatsmeowService (evita dupla inicialização) pollHandler := poll_handler.NewPollHandler(whatsmeowService.GetPollService(), loggerWrapper) - telemetry := telemetry.NewTelemetryService() + telemetry := telemetry.NewTelemetryService(config.TelemetryEnabled) r := gin.Default() diff --git a/pkg/config/config.go b/pkg/config/config.go index a7a540a..283a0c2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -60,6 +60,7 @@ type Config struct { EventIgnoreStatus bool QrcodeMaxCount int CheckUserExists bool + TelemetryEnabled bool // Logger configurations LogMaxSize int @@ -67,7 +68,6 @@ type Config struct { LogMaxAge int LogDirectory string LogCompress bool - } // EnsureDBExists connects to postgres (without the target database) and creates it if it doesn't exist. @@ -274,6 +274,7 @@ func Load() *Config { eventIgnoreStatus := os.Getenv(config_env.EVENT_IGNORE_STATUS) qrcodeMaxCount := os.Getenv(config_env.QRCODE_MAX_COUNT) checkUserExists := os.Getenv(config_env.CHECK_USER_EXISTS) + telemetryEnabled := os.Getenv(config_env.TELEMETRY_ENABLED) if checkUserExists == "" { checkUserExists = "true" @@ -372,7 +373,8 @@ func Load() *Config { EventIgnoreGroup: eventIgnoreGroup == "true", EventIgnoreStatus: eventIgnoreStatus == "true", QrcodeMaxCount: qrMaxCount, - CheckUserExists: checkUserExists != "false", // Default true, set to false to disable + CheckUserExists: checkUserExists != "false", // Default true, set to false to disable + TelemetryEnabled: telemetryEnabled != "false", // Default true, set to false to disable AmqpGlobalEvents: amqpGlobalEvents, AmqpSpecificEvents: amqpSpecificEvents, NatsUrl: natsUrl, diff --git a/pkg/config/env/env.go b/pkg/config/env/env.go index 0fbd936..cb855f9 100644 --- a/pkg/config/env/env.go +++ b/pkg/config/env/env.go @@ -44,6 +44,7 @@ const ( EVENT_IGNORE_STATUS = "EVENT_IGNORE_STATUS" QRCODE_MAX_COUNT = "QRCODE_MAX_COUNT" CHECK_USER_EXISTS = "CHECK_USER_EXISTS" + TELEMETRY_ENABLED = "TELEMETRY_ENABLED" // Logger configurations LOG_MAX_SIZE = "LOG_MAX_SIZE" diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go index 067a543..5df7f43 100644 --- a/pkg/telemetry/telemetry.go +++ b/pkg/telemetry/telemetry.go @@ -5,6 +5,7 @@ import ( "encoding/json" "log" "net/http" + "strings" "time" "github.com/gin-gonic/gin" @@ -16,10 +17,16 @@ type TelemetryData struct { Timestamp time.Time `json:"timestamp"` } -type telemetryService struct{} +type telemetryService struct { + enabled bool +} func (t *telemetryService) TelemetryMiddleware() gin.HandlerFunc { return func(c *gin.Context) { + if !t.enabled { + c.Next() + return + } route := c.FullPath() go SendTelemetry(route) c.Next() @@ -51,12 +58,17 @@ func SendTelemetry(route string) { resp, err := http.Post(url, "application/json", bytes.NewBuffer(data)) if err != nil { - log.Println("Erro ao enviar telemetria:", err) + // Silence DNS lookup errors or connection refused as they are common in restricted environments + if !strings.Contains(err.Error(), "no such host") && !strings.Contains(err.Error(), "connection refused") { + log.Println("Erro ao enviar telemetria:", err) + } return } defer resp.Body.Close() } -func NewTelemetryService() TelemetryService { - return &telemetryService{} +func NewTelemetryService(enabled bool) TelemetryService { + return &telemetryService{ + enabled: enabled, + } } From 09e17e1643836201219904538791335588b9288c Mon Sep 17 00:00:00 2001 From: Ismael Ash Date: Wed, 8 Apr 2026 14:53:33 -0300 Subject: [PATCH 4/4] feat(webhook): add group and newsletter subscription checks for events Extend webhook event filtering to support GROUP and NEWSLETTER subscriptions for Message, SendMessage, and Receipt events. When the main subscription (e.g., MESSAGE) is not present, check if the event originates from a group or newsletter chat and forward it if the corresponding subscription exists. --- pkg/whatsmeow/service/whatsmeow.go | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/pkg/whatsmeow/service/whatsmeow.go b/pkg/whatsmeow/service/whatsmeow.go index 6af7174..c932a9e 100644 --- a/pkg/whatsmeow/service/whatsmeow.go +++ b/pkg/whatsmeow/service/whatsmeow.go @@ -1914,16 +1914,59 @@ func (w *whatsmeowService) CallWebhook(instance *instance_model.Instance, queueN if contains(subscriptions, "MESSAGE") { w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) w.sendToQueueOrWebhook(instance, queueName, jsonData) + } else { + // Check if message is from group or newsletter and if user is subscribed to it + if dataMap, ok := data["data"].(map[string]interface{}); ok { + if infoMap, ok := dataMap["Info"].(map[string]interface{}); ok { + if chat, ok := infoMap["Chat"].(string); ok { + if strings.HasSuffix(chat, "@g.us") && contains(subscriptions, "GROUP") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s (Group)", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } else if strings.HasSuffix(chat, "@newsletter") && contains(subscriptions, "NEWSLETTER") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s (Newsletter)", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + } + } + } } case "SendMessage": if contains(subscriptions, "SEND_MESSAGE") { w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) w.sendToQueueOrWebhook(instance, queueName, jsonData) + } else { + // Check if message is to group or newsletter and if user is subscribed to it + if dataMap, ok := data["data"].(map[string]interface{}); ok { + if infoMap, ok := dataMap["Info"].(map[string]interface{}); ok { + if chat, ok := infoMap["Chat"].(string); ok { + if strings.HasSuffix(chat, "@g.us") && contains(subscriptions, "GROUP") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s (Group)", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } else if strings.HasSuffix(chat, "@newsletter") && contains(subscriptions, "NEWSLETTER") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s (Newsletter)", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + } + } + } } case "Receipt": if contains(subscriptions, "READ_RECEIPT") { w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) w.sendToQueueOrWebhook(instance, queueName, jsonData) + } else { + // Check if receipt is from group or newsletter and if user is subscribed to it + if dataMap, ok := data["data"].(map[string]interface{}); ok { + if chat, ok := dataMap["Chat"].(string); ok { + if strings.HasSuffix(chat, "@g.us") && contains(subscriptions, "GROUP") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s (Group)", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } else if strings.HasSuffix(chat, "@newsletter") && contains(subscriptions, "NEWSLETTER") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s (Newsletter)", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + } + } } case "Presence": if contains(subscriptions, "PRESENCE") {