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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ LOGTYPE=console
WEBHOOKFILES=true

CONNECT_ON_STARTUP=true
TELEMETRY_ENABLED=true

OS_NAME=Evolution GO

Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion cmd/evolution-go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
86 changes: 86 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
version: '3.8'

services:
evolution-go:
build:
context: .
dockerfile: Dockerfile
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: postgres
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: root
POSTGRES_DB: postgres
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: 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: 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
6 changes: 4 additions & 2 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ type Config struct {
EventIgnoreStatus bool
QrcodeMaxCount int
CheckUserExists bool
TelemetryEnabled bool

// Logger configurations
LogMaxSize int
LogMaxBackups int
LogMaxAge int
LogDirectory string
LogCompress bool

}

// EnsureDBExists connects to postgres (without the target database) and creates it if it doesn't exist.
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions pkg/config/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
20 changes: 16 additions & 4 deletions pkg/telemetry/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"log"
"net/http"
"strings"
"time"

"github.com/gin-gonic/gin"
Expand All @@ -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()
Expand Down Expand Up @@ -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") {
Comment on lines +61 to +62
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Error filtering based on substrings in err.Error() is brittle and may miss or misclassify network errors.

Using strings.Contains(err.Error(), "no such host") / "connection refused" hard-codes behavior to specific message text, which can differ by OS, Go version, or localization and may change over time. This can cause relevant errors to be hidden or misclassified. Prefer checking concrete error types instead, e.g. using errors.Is with syscall.ECONNREFUSED or asserting to *net.OpError / net.Error and inspecting the underlying cause so DNS/connection issues are handled reliably without relying on message strings.

Suggested implementation:

	resp, err := http.Post(url, "application/json", bytes.NewBuffer(data))
	if err != nil {
		// Silence expected DNS/connection errors (common in restricted environments),
		// but log unexpected failures so issues are still visible.
		var opErr *net.OpError
		if errors.As(err, &opErr) {
			// DNS resolution failures (e.g. unknown host / no such host)
			if _, ok := opErr.Err.(*net.DNSError); ok {
				return
			}

			// Connection refused and similar low-level network issues
			if sysErr, ok := opErr.Err.(*os.SyscallError); ok && errors.Is(sysErr, syscall.ECONNREFUSED) {
				return
			}
		}

		log.Println("Erro ao enviar telemetria:", err)

To make this compile and fully integrate:

  1. Ensure the following imports are present at the top of pkg/telemetry/telemetry.go:
    • errors
    • net
    • os
    • syscall
  2. Remove the strings import if it is only used in the removed strings.Contains checks.
  3. Confirm that this code is inside a function where return correctly exits the telemetry send operation (e.g. SendTelemetry); if the function should not return here, adjust the control flow accordingly (e.g. replace return with just a comment and let the function continue if appropriate).

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,
}
}
43 changes: 43 additions & 0 deletions pkg/whatsmeow/service/whatsmeow.go
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
68 changes: 68 additions & 0 deletions start.sh
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +28 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Using sed -i without a backup suffix is not portable between GNU and BSD/macOS sed.

On GNU sed -i works without an argument, but BSD/macOS sed requires a suffix (e.g. -i ''). This will cause the script to fail on macOS. Consider either branching on the OS to choose the correct sed -i invocation, or avoiding -i by writing to a temporary file and moving it back to .env for portability.


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