Spaces:
Running
Running
Commit ·
83d6851
1
Parent(s): 6a876ee
restructure repo into production layout; add whatsapp-service, tests, ddl, docs, scripts; scrub hardcoded secrets
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +12 -7
- .env.example +180 -0
- .gitignore +20 -22
- Dockerfile +50 -1
- app/api/server.py +16 -1
- app/api/whatsapp.py +219 -0
- app/config.py +14 -0
- docs/whatsapp-gateway.md +130 -0
- start.sh +144 -2
- test_integration.py +0 -92
- tests/test_webhook_socket.py +0 -446
- whatsapp-service/.gitignore +20 -0
- whatsapp-service/LICENSE +13 -0
- whatsapp-service/Makefile +268 -0
- whatsapp-service/NOTICE +7 -0
- whatsapp-service/VERSION +1 -0
- whatsapp-service/cmd/agentdeck-whatsapp-service/main.go +397 -0
- whatsapp-service/docs/docs.go +0 -0
- whatsapp-service/docs/swagger.json +0 -0
- whatsapp-service/docs/swagger.yaml +0 -0
- whatsapp-service/go.mod +87 -0
- whatsapp-service/go.sum +241 -0
- whatsapp-service/pkg/cache/redis.go +64 -0
- whatsapp-service/pkg/call/handler/call_handler.go +60 -0
- whatsapp-service/pkg/call/service/call_service.go +95 -0
- whatsapp-service/pkg/chat/handler/chat_handler.go +337 -0
- whatsapp-service/pkg/chat/service/chat_service.go +256 -0
- whatsapp-service/pkg/community/handler/community_handler.go +160 -0
- whatsapp-service/pkg/community/service/community_service.go +169 -0
- whatsapp-service/pkg/config/config.go +338 -0
- whatsapp-service/pkg/config/env/env.go +63 -0
- whatsapp-service/pkg/core/c0.go +954 -0
- whatsapp-service/pkg/events/interfaces/producer.go +6 -0
- whatsapp-service/pkg/events/nats/nats_producer.go +81 -0
- whatsapp-service/pkg/events/rabbitmq/rabbitmq_producer.go +319 -0
- whatsapp-service/pkg/events/webhook/webhook_producer.go +97 -0
- whatsapp-service/pkg/events/websocket/websocket_producer.go +140 -0
- whatsapp-service/pkg/group/handler/group_handler.go +533 -0
- whatsapp-service/pkg/group/service/group_service.go +653 -0
- whatsapp-service/pkg/instance/handler/instance_handler.go +660 -0
- whatsapp-service/pkg/instance/model/instance_model.go +52 -0
- whatsapp-service/pkg/instance/repository/instance_repository.go +333 -0
- whatsapp-service/pkg/instance/service/instance_service.go +929 -0
- whatsapp-service/pkg/internal/event_types/event_types.go +64 -0
- whatsapp-service/pkg/label/handler/label_handler.go +297 -0
- whatsapp-service/pkg/label/model/label_model.go +10 -0
- whatsapp-service/pkg/label/repository/label_repository.go +144 -0
- whatsapp-service/pkg/label/service/label_service.go +240 -0
- whatsapp-service/pkg/logger/logger.go +146 -0
- whatsapp-service/pkg/message/handler/message_handler.go +422 -0
.dockerignore
CHANGED
|
@@ -37,7 +37,6 @@ logs/
|
|
| 37 |
*.sqlite3
|
| 38 |
|
| 39 |
# Large repo blobs not needed by the image
|
| 40 |
-
generated-python-sdk/
|
| 41 |
ddl/
|
| 42 |
searxng/
|
| 43 |
scripts/
|
|
@@ -45,10 +44,16 @@ scripts/
|
|
| 45 |
|
| 46 |
# NOTE: `lua/` (Redis Lua scripts) is loaded at runtime and MUST stay in the image.
|
| 47 |
|
| 48 |
-
# Tests
|
| 49 |
tests/
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
*.sqlite3
|
| 38 |
|
| 39 |
# Large repo blobs not needed by the image
|
|
|
|
| 40 |
ddl/
|
| 41 |
searxng/
|
| 42 |
scripts/
|
|
|
|
| 44 |
|
| 45 |
# NOTE: `lua/` (Redis Lua scripts) is loaded at runtime and MUST stay in the image.
|
| 46 |
|
| 47 |
+
# Tests (kept out of the image)
|
| 48 |
tests/
|
| 49 |
+
|
| 50 |
+
# whatsapp-service: ship only what the Go build stage needs (pkg/, cmd/,
|
| 51 |
+
# go.mod, go.sum, docs/docs.go, VERSION). Everything else is dev/test/docs
|
| 52 |
+
# and must not bloat the build context or the image. Runtime configuration is
|
| 53 |
+
# injected as environment variables from the main app — the Go service never
|
| 54 |
+
# ships or reads a .env.
|
| 55 |
+
whatsapp-service/ddl/
|
| 56 |
+
whatsapp-service/tests/
|
| 57 |
+
whatsapp-service/LICENSE
|
| 58 |
+
whatsapp-service/NOTICE
|
| 59 |
+
whatsapp-service/Makefile
|
.env.example
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ------------------------------------------------------------------
|
| 2 |
+
# AgentDeck Backend - example environment
|
| 3 |
+
# Copy to `.env` (or set these in your deployment) and fill in real values.
|
| 4 |
+
# cp .env.example .env
|
| 5 |
+
# In production the app REFUSES TO START unless these are set to strong,
|
| 6 |
+
# non-default values:
|
| 7 |
+
# API_KEY, JWT_SECRET_KEY
|
| 8 |
+
# ------------------------------------------------------------------
|
| 9 |
+
|
| 10 |
+
# --- App ---
|
| 11 |
+
APP_NAME=All API Collection
|
| 12 |
+
ENVIRONMENT=production # production | development
|
| 13 |
+
HOST=0.0.0.0
|
| 14 |
+
PORT=7860
|
| 15 |
+
WORKERS=1
|
| 16 |
+
LOG_LEVEL=INFO
|
| 17 |
+
|
| 18 |
+
# --- Auth (required) ---
|
| 19 |
+
# Master bearer key for the whole API; must be long & random in production.
|
| 20 |
+
API_KEY=change-me-to-a-long-random-value
|
| 21 |
+
# Secret used to sign JWTs. Must be long & random (>=32 chars) in production.
|
| 22 |
+
JWT_SECRET_KEY=change-me-to-a-long-random-value
|
| 23 |
+
JWT_ALGORITHM=HS256
|
| 24 |
+
JWT_ISSUER=all-api-collection
|
| 25 |
+
ADMIN_PASSWORD=
|
| 26 |
+
|
| 27 |
+
# Per-client-IP global rate limit (requests / minute). 0 disables.
|
| 28 |
+
RATE_LIMIT_PER_MINUTE=60
|
| 29 |
+
|
| 30 |
+
# --- URL shortener ---
|
| 31 |
+
URL_SHORTENER_SECRET=change-me
|
| 32 |
+
URL_SHORTENER_BASE=http://localhost:7860/api/v1
|
| 33 |
+
|
| 34 |
+
# --- Limits ---
|
| 35 |
+
MAX_UPLOAD_BYTES=15728640
|
| 36 |
+
|
| 37 |
+
# --- Embeddings / vector store ---
|
| 38 |
+
EMBEDDING_MODEL=ibm-granite/granite-embedding-small-english-r2
|
| 39 |
+
EMBEDDING_DIMENSION=384
|
| 40 |
+
DEFAULT_TOP_K=10
|
| 41 |
+
DATA_DIR=./data
|
| 42 |
+
|
| 43 |
+
# --- Supabase ---
|
| 44 |
+
SUPABASE_URL=https://your-project.supabase.co
|
| 45 |
+
SUPABASE_ANON_KEY=
|
| 46 |
+
SUPABASE_SERVICE_ROLE_KEY=
|
| 47 |
+
SUPABASE_SCHEMA=public
|
| 48 |
+
|
| 49 |
+
# --- Redis (optional; scheduler / caching) ---
|
| 50 |
+
REDIS_URL=
|
| 51 |
+
REDIS_HOST=localhost
|
| 52 |
+
REDIS_PORT=6379
|
| 53 |
+
REDIS_DB=0
|
| 54 |
+
REDIS_PASSWORD=
|
| 55 |
+
REDIS_SSL=false
|
| 56 |
+
|
| 57 |
+
# --- Google Maps / GCP ---
|
| 58 |
+
GCP_API_KEY=
|
| 59 |
+
GOOGLE_MAPS_BASE_URL=https://maps.googleapis.com/maps/api
|
| 60 |
+
|
| 61 |
+
# --- Google Cloud Storage ---
|
| 62 |
+
GCS_BUCKET_NAME=
|
| 63 |
+
GCS_SERVICE_ACCOUNT_KEY_PATH=
|
| 64 |
+
|
| 65 |
+
# --- Google OAuth / Gmail (optional) ---
|
| 66 |
+
GOOGLE_OAUTH_CLIENT_ID=
|
| 67 |
+
GOOGLE_OAUTH_CLIENT_SECRET=
|
| 68 |
+
|
| 69 |
+
# --- Web search (SearXNG) ---
|
| 70 |
+
SEARXNG_BASE_URL=http://localhost:8888
|
| 71 |
+
|
| 72 |
+
# --- Startup self-ping ---
|
| 73 |
+
SELF_PING_URL=
|
| 74 |
+
|
| 75 |
+
# ------------------------------------------------------------------
|
| 76 |
+
# --- WhatsApp service (single source of truth) ---------------------
|
| 77 |
+
# This `.env` is the ONLY configuration file. It is read by the main FastAPI
|
| 78 |
+
# app (pydantic-settings) AND, via start.sh / the Docker image, forwarded to
|
| 79 |
+
# the internal AgentDeck WhatsApp service (Go) as its process environment.
|
| 80 |
+
# The Go service never reads a `.env` of its own — it consumes injected env
|
| 81 |
+
# vars only. In a container deployment these values are set on the platform
|
| 82 |
+
# (Hugging Face Spaces secrets) and start.sh passes them straight through.
|
| 83 |
+
# ------------------------------------------------------------------
|
| 84 |
+
|
| 85 |
+
# --- Gateway settings (read by the FastAPI app) ---
|
| 86 |
+
# The main app reverse-proxies /api/whatsapp/* to the WhatsApp service.
|
| 87 |
+
# When the image embeds the binary (start.sh) the gateway is auto-enabled;
|
| 88 |
+
# set explicitly to control it. WHATSAPP_SERVICE_URL may be a service name
|
| 89 |
+
# (http://whatsapp-service:8080) when the WhatsApp service runs as a separate
|
| 90 |
+
# container, or loopback when co-hosted in this image.
|
| 91 |
+
WHATSAPP_SERVICE_ENABLED=false
|
| 92 |
+
WHATSAPP_SERVICE_URL=http://127.0.0.1:8080
|
| 93 |
+
WHATSAPP_SERVICE_TIMEOUT=30
|
| 94 |
+
WHATSAPP_SERVICE_CONNECT_TIMEOUT=5
|
| 95 |
+
# Port start.sh launches the embedded Go binary on (its SERVER_PORT).
|
| 96 |
+
WHATSAPP_SERVICE_PORT=8080
|
| 97 |
+
|
| 98 |
+
# --- Settings forwarded to the Go service (start.sh / Docker) ---
|
| 99 |
+
# Global API key of the WhatsApp service. Used by the gateway as the default
|
| 100 |
+
# `apikey` header AND injected into the Go process as GLOBAL_API_KEY.
|
| 101 |
+
WHATSAPP_SERVICE_GLOBAL_API_KEY=
|
| 102 |
+
|
| 103 |
+
# The Go service requires: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY (injected
|
| 104 |
+
# as SUPABASE_SERVICE_KEY), SUPABASE_DB_URL, REDIS_URL, GLOBAL_API_KEY (see
|
| 105 |
+
# WHATSAPP_SERVICE_GLOBAL_API_KEY) and DATABASE_SAVE_MESSAGES. SUPABASE_URL
|
| 106 |
+
# and REDIS_URL are the shared values defined above; SUPABASE_SERVICE_ROLE_KEY
|
| 107 |
+
# is mapped to the Go service's expected name (SUPABASE_SERVICE_KEY) for you.
|
| 108 |
+
# SUPABASE_DB_URL is a direct native-Postgres connection string (used by the
|
| 109 |
+
# whatsmeow session store — NOT the PostgREST URL):
|
| 110 |
+
SUPABASE_DB_URL=postgresql://postgres:postgres@db.your-project.supabase.co:5432/postgres
|
| 111 |
+
DATABASE_SAVE_MESSAGES=false
|
| 112 |
+
CLIENT_NAME=agentdeck
|
| 113 |
+
CONNECT_ON_STARTUP=false
|
| 114 |
+
|
| 115 |
+
# Optional WhatsApp service behaviour (documented defaults = Go service defaults).
|
| 116 |
+
DEBUG_ENABLED=INFO # whatsmeow debug level (mapped from legacy WADEBUG)
|
| 117 |
+
LOG_TYPE=console # console | file
|
| 118 |
+
WEBHOOK_FILES=true
|
| 119 |
+
OS_NAME=AgentDeck
|
| 120 |
+
WHATSAPP_VERSION_MAJOR=2
|
| 121 |
+
WHATSAPP_VERSION_MINOR=3000
|
| 122 |
+
WHATSAPP_VERSION_PATCH=1
|
| 123 |
+
# EVENT_IGNORE_GROUP=false
|
| 124 |
+
# EVENT_IGNORE_STATUS=false
|
| 125 |
+
# QRCODE_MAX_COUNT=5
|
| 126 |
+
# CHECK_USER_EXISTS=true
|
| 127 |
+
|
| 128 |
+
# MinIO media storage (only if MINIO_ENABLED=true).
|
| 129 |
+
MINIO_ENABLED=false
|
| 130 |
+
# MINIO_ENDPOINT=localhost:9000
|
| 131 |
+
# MINIO_ACCESS_KEY=minioadmin
|
| 132 |
+
# MINIO_SECRET_KEY=minioadmin
|
| 133 |
+
# MINIO_BUCKET=agentdeck-media
|
| 134 |
+
# MINIO_USE_SSL=false
|
| 135 |
+
# MINIO_REGION=
|
| 136 |
+
|
| 137 |
+
# RabbitMQ / NATS event producers (optional).
|
| 138 |
+
# AMQP_URL=amqp://admin:admin@localhost:5672/default
|
| 139 |
+
# AMQP_GLOBAL_ENABLED=false
|
| 140 |
+
# AMQP_GLOBAL_EVENTS=
|
| 141 |
+
# AMQP_SPECIFIC_EVENTS=
|
| 142 |
+
# NATS_URL=
|
| 143 |
+
# NATS_GLOBAL_ENABLED=false
|
| 144 |
+
# NATS_GLOBAL_EVENTS=
|
| 145 |
+
# WEBHOOK_URL=
|
| 146 |
+
|
| 147 |
+
# Outbound proxy for the WhatsApp service (optional).
|
| 148 |
+
# PROXY_PROTOCOL=http # http | https | socks5
|
| 149 |
+
# PROXY_HOST=
|
| 150 |
+
# PROXY_PORT=
|
| 151 |
+
# PROXY_USERNAME=
|
| 152 |
+
# PROXY_PASSWORD=
|
| 153 |
+
|
| 154 |
+
# Audio converter (optional).
|
| 155 |
+
# API_AUDIO_CONVERTER=
|
| 156 |
+
# API_AUDIO_CONVERTER_KEY=
|
| 157 |
+
|
| 158 |
+
# Logger rotation (optional).
|
| 159 |
+
# LOG_MAX_SIZE=100 # MB
|
| 160 |
+
# LOG_MAX_BACKUPS=5
|
| 161 |
+
# LOG_MAX_AGE=30 # days
|
| 162 |
+
# LOG_DIRECTORY=./logs
|
| 163 |
+
# LOG_COMPRESS=true
|
| 164 |
+
|
| 165 |
+
# --- Concurrency ---
|
| 166 |
+
# Shared thread pool worker count. Unset => auto-calc min(32, cpu_count+4).
|
| 167 |
+
CORE_CONCURRENCY=
|
| 168 |
+
|
| 169 |
+
# --- PaddleOCR PP-OCRv6 (ONNX Runtime) ---
|
| 170 |
+
# OCR_ENGINE=onnxruntime # paddle | paddle_static | paddle_dynamic | onnxruntime | transformers
|
| 171 |
+
# OCR_DEVICE=cpu
|
| 172 |
+
# OCR_LANG= # e.g. en; empty uses model defaults
|
| 173 |
+
# OCR_DET_MODEL_NAME=PP-OCRv6_small_det
|
| 174 |
+
# OCR_REC_MODEL_NAME=PP-OCRv6_small_rec
|
| 175 |
+
# OCR_USE_DOC_ORIENTATION_CLASSIFY=false
|
| 176 |
+
# OCR_USE_DOC_UNWARPING=false
|
| 177 |
+
# OCR_USE_TEXTLINE_ORIENTATION=true
|
| 178 |
+
# OCR_MAX_CONCURRENT=1 # max parallel predict calls on shared engine
|
| 179 |
+
# Enrich raw/OCR content into labeled Markdown before LLM extraction (default true).
|
| 180 |
+
CONTENT_STRUCTURE_ENRICHMENT=true
|
.gitignore
CHANGED
|
@@ -4,12 +4,10 @@ __pycache__/
|
|
| 4 |
postman_collection.json
|
| 5 |
|
| 6 |
# Runtime data (vector stores, SQLite DBs, model caches, etc.)
|
| 7 |
-
data/
|
| 8 |
*.so
|
| 9 |
-
test_deploy_flow.py
|
| 10 |
|
| 11 |
.Python
|
| 12 |
-
hammer_tidb.py
|
| 13 |
build/
|
| 14 |
develop-eggs/
|
| 15 |
dist/
|
|
@@ -58,7 +56,7 @@ local_settings.py
|
|
| 58 |
db.sqlite3
|
| 59 |
db.sqlite3-journal
|
| 60 |
|
| 61 |
-
instance/
|
| 62 |
.webassets-cache
|
| 63 |
|
| 64 |
.scrapy
|
|
@@ -82,12 +80,13 @@ celerybeat.pid
|
|
| 82 |
*.sage.py
|
| 83 |
|
| 84 |
.env*
|
| 85 |
-
.
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
| 91 |
.env.local
|
| 92 |
|
| 93 |
.spyderproject
|
|
@@ -117,9 +116,9 @@ dmypy.json
|
|
| 117 |
ehthumbs.db
|
| 118 |
Thumbs.db
|
| 119 |
|
| 120 |
-
logs/
|
| 121 |
*.log
|
| 122 |
-
persistence/
|
| 123 |
|
| 124 |
*.db
|
| 125 |
*.sqlite3
|
|
@@ -127,15 +126,14 @@ persistence/
|
|
| 127 |
*.tmp
|
| 128 |
*.temp
|
| 129 |
|
| 130 |
-
local_deploy.py
|
| 131 |
-
test_vector_store_async.py
|
| 132 |
-
deploy_sdk.py
|
| 133 |
-
tests
|
| 134 |
-
deploy_hf.py
|
| 135 |
.mimocode
|
| 136 |
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
postman_collection.json
|
| 5 |
|
| 6 |
# Runtime data (vector stores, SQLite DBs, model caches, etc.)
|
| 7 |
+
/data/
|
| 8 |
*.so
|
|
|
|
| 9 |
|
| 10 |
.Python
|
|
|
|
| 11 |
build/
|
| 12 |
develop-eggs/
|
| 13 |
dist/
|
|
|
|
| 56 |
db.sqlite3
|
| 57 |
db.sqlite3-journal
|
| 58 |
|
| 59 |
+
/instance/
|
| 60 |
.webassets-cache
|
| 61 |
|
| 62 |
.scrapy
|
|
|
|
| 80 |
*.sage.py
|
| 81 |
|
| 82 |
.env*
|
| 83 |
+
!.env.example
|
| 84 |
+
/.venv
|
| 85 |
+
/env/
|
| 86 |
+
/venv/
|
| 87 |
+
/ENV/
|
| 88 |
+
/env.bak/
|
| 89 |
+
/venv.bak/
|
| 90 |
.env.local
|
| 91 |
|
| 92 |
.spyderproject
|
|
|
|
| 116 |
ehthumbs.db
|
| 117 |
Thumbs.db
|
| 118 |
|
| 119 |
+
/logs/
|
| 120 |
*.log
|
| 121 |
+
/persistence/
|
| 122 |
|
| 123 |
*.db
|
| 124 |
*.sqlite3
|
|
|
|
| 126 |
*.tmp
|
| 127 |
*.temp
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
.mimocode
|
| 130 |
|
| 131 |
+
# Secrets
|
| 132 |
+
google_oauth_test_creds.postman_environment.json
|
| 133 |
+
|
| 134 |
+
# Local dev/test/doc artifacts (kept out of the production repo)
|
| 135 |
+
tests/
|
| 136 |
+
ddl/
|
| 137 |
+
scripts/
|
| 138 |
+
docs/API_DESCRIPTION.md
|
| 139 |
+
docs/planning/
|
Dockerfile
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
|
| 3 |
LABEL maintainer="AgentDeck-Backend"
|
|
@@ -16,6 +44,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
| 16 |
libssl-dev \
|
| 17 |
nodejs \
|
| 18 |
zlib1g-dev \
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
# Runtime libs for PaddleOCR / opencv-contrib-python (cv2), mirroring the
|
| 20 |
# reference reconciliation-file-processing-service Dockerfile.
|
| 21 |
libglib2.0-0 \
|
|
@@ -43,6 +77,13 @@ RUN git clone --depth 1 --branch master https://github.com/searxng/searxng.git /
|
|
| 43 |
|
| 44 |
COPY --chown=appuser:appuser . .
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
RUN mkdir -p /app/models && python3 -c "from huggingface_hub import snapshot_download; snapshot_download(repo_id='ibm-granite/granite-embedding-small-english-r2', local_dir='/app/models/bge-384')" && chown -R appuser:appuser /app/models
|
| 47 |
|
| 48 |
RUN mkdir -p /app/data /app/logs && \
|
|
@@ -55,9 +96,17 @@ USER appuser
|
|
| 55 |
ENV PYTHONPATH=/app
|
| 56 |
ENV PYTHONUNBUFFERED=1
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
EXPOSE 7860
|
| 59 |
|
| 60 |
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
| 61 |
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:7860/health')" || exit 1
|
| 62 |
|
| 63 |
-
CMD ["/bin/bash", "/app/start.sh"]
|
|
|
|
| 1 |
+
# ---------------------------------------------------------------------------
|
| 2 |
+
# Build stage: the internal WhatsApp service (Go / whatsmeow).
|
| 3 |
+
# The binary is built here (CGO + libjpeg/libwebp headers) and embedded in the
|
| 4 |
+
# runtime image as a sibling process started by start.sh. Runtime configuration
|
| 5 |
+
# is NOT baked in: the Go service receives every setting as an environment
|
| 6 |
+
# variable injected by the deployment platform / start.sh (single source of
|
| 7 |
+
# truth = the main application's environment / .env). The Go module requires
|
| 8 |
+
# Go 1.25.
|
| 9 |
+
# ---------------------------------------------------------------------------
|
| 10 |
+
FROM golang:1.25.0-alpine AS whatsapp-build
|
| 11 |
+
|
| 12 |
+
RUN echo "https://dl-4.alpinelinux.org/alpine/v3.22/main" > /etc/apk/repositories \
|
| 13 |
+
&& echo "https://dl-4.alpinelinux.org/alpine/v3.22/community" >> /etc/apk/repositories \
|
| 14 |
+
&& apk update && apk add --no-cache git build-base libjpeg-turbo-dev libwebp-dev
|
| 15 |
+
|
| 16 |
+
WORKDIR /build
|
| 17 |
+
|
| 18 |
+
COPY whatsapp-service/go.mod whatsapp-service/go.sum ./
|
| 19 |
+
RUN go mod download
|
| 20 |
+
|
| 21 |
+
COPY whatsapp-service/ .
|
| 22 |
+
|
| 23 |
+
ARG WHATSAPP_VERSION=dev
|
| 24 |
+
RUN CGO_ENABLED=1 go build -ldflags "-X main.version=${WHATSAPP_VERSION}" -o server ./cmd/agentdeck-whatsapp-service
|
| 25 |
+
|
| 26 |
+
# ---------------------------------------------------------------------------
|
| 27 |
+
# Runtime image
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
FROM python:3.11-slim
|
| 30 |
|
| 31 |
LABEL maintainer="AgentDeck-Backend"
|
|
|
|
| 44 |
libssl-dev \
|
| 45 |
nodejs \
|
| 46 |
zlib1g-dev \
|
| 47 |
+
# Runtime libs for the embedded WhatsApp (Go) service: media codecs and
|
| 48 |
+
# timezone data required by whatsmeow/ffmpeg processing.
|
| 49 |
+
libjpeg62-turbo \
|
| 50 |
+
libwebp7 \
|
| 51 |
+
poppler-utils \
|
| 52 |
+
tzdata \
|
| 53 |
# Runtime libs for PaddleOCR / opencv-contrib-python (cv2), mirroring the
|
| 54 |
# reference reconciliation-file-processing-service Dockerfile.
|
| 55 |
libglib2.0-0 \
|
|
|
|
| 77 |
|
| 78 |
COPY --chown=appuser:appuser . .
|
| 79 |
|
| 80 |
+
# Replace the WhatsApp service source tree (copied above from the repo) with
|
| 81 |
+
# just the compiled binary + VERSION baked from the whatsapp-build stage.
|
| 82 |
+
RUN rm -rf /app/whatsapp-service
|
| 83 |
+
COPY --from=whatsapp-build /build/server /app/whatsapp-service/server
|
| 84 |
+
COPY --from=whatsapp-build /build/VERSION /app/whatsapp-service/VERSION
|
| 85 |
+
RUN chmod +x /app/whatsapp-service/server
|
| 86 |
+
|
| 87 |
RUN mkdir -p /app/models && python3 -c "from huggingface_hub import snapshot_download; snapshot_download(repo_id='ibm-granite/granite-embedding-small-english-r2', local_dir='/app/models/bge-384')" && chown -R appuser:appuser /app/models
|
| 88 |
|
| 89 |
RUN mkdir -p /app/data /app/logs && \
|
|
|
|
| 96 |
ENV PYTHONPATH=/app
|
| 97 |
ENV PYTHONUNBUFFERED=1
|
| 98 |
|
| 99 |
+
# Path to the embedded WhatsApp service binary (started by start.sh as a
|
| 100 |
+
# sibling process). Set to an empty value to disable the WhatsApp gateway.
|
| 101 |
+
# The Go service reads all runtime settings (SUPABASE_URL, SUPABASE_DB_URL,
|
| 102 |
+
# REDIS_URL, GLOBAL_API_KEY, ...) from the environment injected by the
|
| 103 |
+
# platform / start.sh — no .env is shipped for it. All values live in the
|
| 104 |
+
# main application's centralized configuration (see .env.example).
|
| 105 |
+
ENV WHATSAPP_SERVICE_BINARY=/app/whatsapp-service/server
|
| 106 |
+
|
| 107 |
EXPOSE 7860
|
| 108 |
|
| 109 |
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
| 110 |
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:7860/health')" || exit 1
|
| 111 |
|
| 112 |
+
CMD ["/bin/bash", "/app/start.sh"]
|
app/api/server.py
CHANGED
|
@@ -13,6 +13,11 @@ from slowapi.util import get_remote_address
|
|
| 13 |
|
| 14 |
from app.api.v1.router import api_v1_router
|
| 15 |
from app.api.v1.system import is_maintenance
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
from app.config import get_settings
|
| 17 |
from app.core.auth.deps import init_auth_db
|
| 18 |
from app.core.database import pool_manager
|
|
@@ -169,6 +174,7 @@ async def lifespan(app: FastAPI):
|
|
| 169 |
from app.utils.http_utils import close_shared_aiohttp_sessions
|
| 170 |
await close_shared_aiohttp_sessions()
|
| 171 |
await _ping_client.close()
|
|
|
|
| 172 |
from app.services.supabase import get_supabase_client
|
| 173 |
client = get_supabase_client()
|
| 174 |
if client:
|
|
@@ -192,6 +198,7 @@ def create_application() -> FastAPI:
|
|
| 192 |
{"name": "Vector Stores", "description": "Create, manage, and search vector stores for RAG"},
|
| 193 |
{"name": "URL Shortener", "description": "Create and manage short URLs with analytics"},
|
| 194 |
{"name": "Media-to-Media Conversion", "description": "PDF-to-image and image-to-image conversion with local or Supabase Storage output"},
|
|
|
|
| 195 |
],
|
| 196 |
lifespan=lifespan,
|
| 197 |
)
|
|
@@ -238,7 +245,8 @@ def create_application() -> FastAPI:
|
|
| 238 |
@app.middleware("http")
|
| 239 |
async def auth_middleware(request: Request, call_next):
|
| 240 |
path = request.url.path
|
| 241 |
-
|
|
|
|
| 242 |
auth_header = request.headers.get("Authorization", "")
|
| 243 |
if not auth_header.startswith("Bearer "):
|
| 244 |
from starlette.responses import JSONResponse
|
|
@@ -261,6 +269,12 @@ def create_application() -> FastAPI:
|
|
| 261 |
dependencies=[Depends(_api_rate_limit)],
|
| 262 |
)
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
@app.get("/", include_in_schema=False)
|
| 265 |
async def root(request: Request):
|
| 266 |
from collections import defaultdict
|
|
@@ -298,6 +312,7 @@ def create_application() -> FastAPI:
|
|
| 298 |
"vector_store_count": store_count,
|
| 299 |
"total_documents": doc_count,
|
| 300 |
"model_loaded": _embedding_service.is_loaded(384),
|
|
|
|
| 301 |
}
|
| 302 |
|
| 303 |
@app.get("/ping", include_in_schema=False)
|
|
|
|
| 13 |
|
| 14 |
from app.api.v1.router import api_v1_router
|
| 15 |
from app.api.v1.system import is_maintenance
|
| 16 |
+
from app.api.whatsapp import (
|
| 17 |
+
close_whatsapp_proxy_client,
|
| 18 |
+
get_whatsapp_health,
|
| 19 |
+
router as whatsapp_router,
|
| 20 |
+
)
|
| 21 |
from app.config import get_settings
|
| 22 |
from app.core.auth.deps import init_auth_db
|
| 23 |
from app.core.database import pool_manager
|
|
|
|
| 174 |
from app.utils.http_utils import close_shared_aiohttp_sessions
|
| 175 |
await close_shared_aiohttp_sessions()
|
| 176 |
await _ping_client.close()
|
| 177 |
+
await close_whatsapp_proxy_client()
|
| 178 |
from app.services.supabase import get_supabase_client
|
| 179 |
client = get_supabase_client()
|
| 180 |
if client:
|
|
|
|
| 198 |
{"name": "Vector Stores", "description": "Create, manage, and search vector stores for RAG"},
|
| 199 |
{"name": "URL Shortener", "description": "Create and manage short URLs with analytics"},
|
| 200 |
{"name": "Media-to-Media Conversion", "description": "PDF-to-image and image-to-image conversion with local or Supabase Storage output"},
|
| 201 |
+
{"name": "WhatsApp", "description": "Gateway to the internal WhatsApp service"},
|
| 202 |
],
|
| 203 |
lifespan=lifespan,
|
| 204 |
)
|
|
|
|
| 245 |
@app.middleware("http")
|
| 246 |
async def auth_middleware(request: Request, call_next):
|
| 247 |
path = request.url.path
|
| 248 |
+
is_api = path.startswith("/api/v1/") or path.startswith("/api/whatsapp/")
|
| 249 |
+
if is_api and not _is_public_path(path, request.method):
|
| 250 |
auth_header = request.headers.get("Authorization", "")
|
| 251 |
if not auth_header.startswith("Bearer "):
|
| 252 |
from starlette.responses import JSONResponse
|
|
|
|
| 269 |
dependencies=[Depends(_api_rate_limit)],
|
| 270 |
)
|
| 271 |
|
| 272 |
+
app.include_router(
|
| 273 |
+
whatsapp_router,
|
| 274 |
+
prefix="/api/whatsapp",
|
| 275 |
+
dependencies=[Depends(_api_rate_limit)],
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
@app.get("/", include_in_schema=False)
|
| 279 |
async def root(request: Request):
|
| 280 |
from collections import defaultdict
|
|
|
|
| 312 |
"vector_store_count": store_count,
|
| 313 |
"total_documents": doc_count,
|
| 314 |
"model_loaded": _embedding_service.is_loaded(384),
|
| 315 |
+
"whatsapp_service": await get_whatsapp_health(),
|
| 316 |
}
|
| 317 |
|
| 318 |
@app.get("/ping", include_in_schema=False)
|
app/api/whatsapp.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Dict
|
| 4 |
+
from urllib.parse import quote
|
| 5 |
+
|
| 6 |
+
import httpx
|
| 7 |
+
from fastapi import APIRouter, Request
|
| 8 |
+
from fastapi.responses import JSONResponse, StreamingResponse
|
| 9 |
+
from starlette.background import BackgroundTask
|
| 10 |
+
|
| 11 |
+
from app.config import get_settings
|
| 12 |
+
from app.core.logger import get_logger
|
| 13 |
+
from app.utils.http_utils import SharedAsyncClient
|
| 14 |
+
|
| 15 |
+
logger = get_logger(__name__)
|
| 16 |
+
_settings = get_settings()
|
| 17 |
+
|
| 18 |
+
router = APIRouter(tags=["WhatsApp"])
|
| 19 |
+
|
| 20 |
+
# RFC 7230 hop-by-hop headers. They are meaningless when a gateway relays a
|
| 21 |
+
# request to another service and must never be forwarded.
|
| 22 |
+
_HOP_BY_HOP_HEADERS = frozenset({
|
| 23 |
+
"connection",
|
| 24 |
+
"keep-alive",
|
| 25 |
+
"proxy-authenticate",
|
| 26 |
+
"proxy-authorization",
|
| 27 |
+
"te",
|
| 28 |
+
"trailer",
|
| 29 |
+
"transfer-encoding",
|
| 30 |
+
"upgrade",
|
| 31 |
+
})
|
| 32 |
+
|
| 33 |
+
# Request headers that must never reach the internal WhatsApp service:
|
| 34 |
+
# hop-by-hop headers plus Host (httpx sets it from the target URL), length
|
| 35 |
+
# headers (httpx recomputes them from the body), content-encoding (httpx
|
| 36 |
+
# transparently decompresses responses), and the gateway's own credentials
|
| 37 |
+
# (Authorization / cookie) which belong to the main API, not the backend.
|
| 38 |
+
_BLOCKED_REQUEST_HEADERS = _HOP_BY_HOP_HEADERS | {
|
| 39 |
+
"host",
|
| 40 |
+
"content-length",
|
| 41 |
+
"accept-encoding",
|
| 42 |
+
"authorization",
|
| 43 |
+
"cookie",
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
# Response headers preserved when relaying the upstream reply back to the
|
| 47 |
+
# client. Everything else (server version headers, content-encoding, hop-by-hop
|
| 48 |
+
# headers, ...) is dropped so internal implementation details never leak.
|
| 49 |
+
_RESPONSE_HEADERS_ALLOWLIST = frozenset({
|
| 50 |
+
"content-type",
|
| 51 |
+
"content-disposition",
|
| 52 |
+
"content-language",
|
| 53 |
+
"cache-control",
|
| 54 |
+
"etag",
|
| 55 |
+
"expires",
|
| 56 |
+
"last-modified",
|
| 57 |
+
"location",
|
| 58 |
+
"retry-after",
|
| 59 |
+
"www-authenticate",
|
| 60 |
+
"x-request-id",
|
| 61 |
+
"x-correlation-id",
|
| 62 |
+
"content-range",
|
| 63 |
+
"accept-ranges",
|
| 64 |
+
})
|
| 65 |
+
|
| 66 |
+
_ALLOWED_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD")
|
| 67 |
+
|
| 68 |
+
# Characters preserved while percent-encoding the forwarded path so a client
|
| 69 |
+
# supplied path can never produce a malformed upstream URL.
|
| 70 |
+
_PATH_SAFE_CHARS = "/:@!$&'()*+,;=~-_."
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _build_proxy_client() -> SharedAsyncClient:
|
| 74 |
+
"""Shared, lazily-created httpx client for upstream WhatsApp calls."""
|
| 75 |
+
timeout = httpx.Timeout(
|
| 76 |
+
timeout=_settings.whatsapp_service_timeout,
|
| 77 |
+
connect=_settings.whatsapp_service_connect_timeout,
|
| 78 |
+
)
|
| 79 |
+
return SharedAsyncClient(timeout=timeout, follow_redirects=False)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
_proxy_client = _build_proxy_client()
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
async def close_whatsapp_proxy_client() -> None:
|
| 86 |
+
"""Close the shared upstream client. Called once on application shutdown."""
|
| 87 |
+
await _proxy_client.close()
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _forward_request_headers(request: Request, body: bytes) -> Dict[str, str]:
|
| 91 |
+
"""Build the header set forwarded to the WhatsApp service.
|
| 92 |
+
|
| 93 |
+
Keeps application headers (content-type, accept, apikey, x-*, ...) while
|
| 94 |
+
stripping hop-by-hop / gateway-credential headers. The `apikey` header the
|
| 95 |
+
WhatsApp service authenticates with is taken from the client request when
|
| 96 |
+
present (per-instance token) and falls back to the configured global key.
|
| 97 |
+
"""
|
| 98 |
+
headers: Dict[str, str] = {}
|
| 99 |
+
for name, value in request.headers.items():
|
| 100 |
+
if name.lower() in _BLOCKED_REQUEST_HEADERS:
|
| 101 |
+
continue
|
| 102 |
+
headers[name] = value
|
| 103 |
+
|
| 104 |
+
if body:
|
| 105 |
+
headers.setdefault("content-type", "application/octet-stream")
|
| 106 |
+
|
| 107 |
+
client_apikey = request.headers.get("apikey", "")
|
| 108 |
+
if client_apikey:
|
| 109 |
+
headers["apikey"] = client_apikey
|
| 110 |
+
elif _settings.whatsapp_service_global_api_key:
|
| 111 |
+
headers["apikey"] = _settings.whatsapp_service_global_api_key
|
| 112 |
+
return headers
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def _filter_response_headers(headers: httpx.Headers) -> Dict[str, str]:
|
| 116 |
+
return {
|
| 117 |
+
name: value
|
| 118 |
+
for name, value in headers.items()
|
| 119 |
+
if name.lower() in _RESPONSE_HEADERS_ALLOWLIST
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def _unavailable_response(status_code: int, detail: str) -> JSONResponse:
|
| 124 |
+
"""Sanitized gateway error response — never leaks upstream hostnames/traces."""
|
| 125 |
+
return JSONResponse(
|
| 126 |
+
status_code=status_code,
|
| 127 |
+
content={"success": False, "detail": detail},
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
@router.api_route(
|
| 132 |
+
"/{path:path}",
|
| 133 |
+
methods=list(_ALLOWED_METHODS),
|
| 134 |
+
summary="Forward a request to the internal WhatsApp service",
|
| 135 |
+
description=(
|
| 136 |
+
"Proxies any HTTP request under /api/whatsapp to the corresponding "
|
| 137 |
+
"endpoint of the internal WhatsApp service, preserving the HTTP method, "
|
| 138 |
+
"path, query string, request body and relevant headers. The upstream "
|
| 139 |
+
"response (status code and body) is returned unchanged."
|
| 140 |
+
),
|
| 141 |
+
)
|
| 142 |
+
async def proxy_to_whatsapp(request: Request, path: str):
|
| 143 |
+
if not _settings.whatsapp_service_enabled:
|
| 144 |
+
return _unavailable_response(503, "WhatsApp service is not enabled")
|
| 145 |
+
|
| 146 |
+
base_url = _settings.whatsapp_service_url.rstrip("/")
|
| 147 |
+
encoded_path = quote(path, safe=_PATH_SAFE_CHARS).lstrip("/")
|
| 148 |
+
target = f"{base_url}/{encoded_path}"
|
| 149 |
+
if request.url.query:
|
| 150 |
+
target = f"{target}?{request.url.query}"
|
| 151 |
+
|
| 152 |
+
body = await request.body()
|
| 153 |
+
headers = _forward_request_headers(request, body)
|
| 154 |
+
|
| 155 |
+
logger.info(
|
| 156 |
+
"Proxying %s %s -> %s (apikey=%s)",
|
| 157 |
+
request.method,
|
| 158 |
+
request.url.path,
|
| 159 |
+
target,
|
| 160 |
+
"yes" if headers.get("apikey") else "no",
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
try:
|
| 164 |
+
client = await _proxy_client.get()
|
| 165 |
+
upstream = await client.send(
|
| 166 |
+
client.build_request(
|
| 167 |
+
request.method,
|
| 168 |
+
target,
|
| 169 |
+
content=body or None,
|
| 170 |
+
headers=headers,
|
| 171 |
+
),
|
| 172 |
+
stream=True,
|
| 173 |
+
)
|
| 174 |
+
except httpx.TimeoutException as exc:
|
| 175 |
+
logger.error("WhatsApp service timed out: %s %s: %s", request.method, target, exc)
|
| 176 |
+
return _unavailable_response(504, "WhatsApp service timed out")
|
| 177 |
+
except httpx.HTTPError as exc:
|
| 178 |
+
logger.error("WhatsApp service unreachable: %s %s: %s", request.method, target, exc)
|
| 179 |
+
return _unavailable_response(502, "WhatsApp service is unavailable")
|
| 180 |
+
except Exception:
|
| 181 |
+
logger.exception("Unexpected gateway error proxying %s %s", request.method, target)
|
| 182 |
+
return _unavailable_response(502, "WhatsApp gateway error")
|
| 183 |
+
|
| 184 |
+
return StreamingResponse(
|
| 185 |
+
upstream.aiter_bytes(),
|
| 186 |
+
status_code=upstream.status_code,
|
| 187 |
+
headers=_filter_response_headers(upstream.headers),
|
| 188 |
+
media_type=None,
|
| 189 |
+
background=BackgroundTask(upstream.aclose),
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
async def get_whatsapp_health() -> Dict[str, object]:
|
| 194 |
+
"""Non-fatal liveness probe used by the main application's /health endpoint.
|
| 195 |
+
|
| 196 |
+
Never raises — a degraded/unreachable WhatsApp service must not take the
|
| 197 |
+
gateway's own health check down with it.
|
| 198 |
+
"""
|
| 199 |
+
if not _settings.whatsapp_service_enabled:
|
| 200 |
+
return {"configured": False, "reachable": False}
|
| 201 |
+
|
| 202 |
+
url = f"{_settings.whatsapp_service_url.rstrip('/')}/server/ok"
|
| 203 |
+
try:
|
| 204 |
+
client = await _proxy_client.get()
|
| 205 |
+
resp = await client.get(
|
| 206 |
+
url,
|
| 207 |
+
timeout=_settings.whatsapp_service_connect_timeout,
|
| 208 |
+
)
|
| 209 |
+
except (httpx.HTTPError, httpx.TimeoutException) as exc:
|
| 210 |
+
logger.warning("WhatsApp service health check failed: %s", exc)
|
| 211 |
+
return {"configured": True, "reachable": False, "status": "unreachable"}
|
| 212 |
+
|
| 213 |
+
if resp.status_code == 200:
|
| 214 |
+
return {"configured": True, "reachable": True, "status": "ok"}
|
| 215 |
+
return {
|
| 216 |
+
"configured": True,
|
| 217 |
+
"reachable": True,
|
| 218 |
+
"status": f"unhealthy (HTTP {resp.status_code})",
|
| 219 |
+
}
|
app/config.py
CHANGED
|
@@ -191,6 +191,20 @@ class Settings(BaseSettings):
|
|
| 191 |
supabase_storage_bucket: str = Field(default="media-convert", alias="SUPABASE_STORAGE_BUCKET")
|
| 192 |
supabase_signed_url_ttl_seconds: int = Field(default=86400, alias="SUPABASE_SIGNED_URL_TTL_SECONDS")
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
# Scheduler settings
|
| 195 |
max_http_timeout: float = 300.0
|
| 196 |
default_scheduler_timezone: str = "UTC"
|
|
|
|
| 191 |
supabase_storage_bucket: str = Field(default="media-convert", alias="SUPABASE_STORAGE_BUCKET")
|
| 192 |
supabase_signed_url_ttl_seconds: int = Field(default=86400, alias="SUPABASE_SIGNED_URL_TTL_SECONDS")
|
| 193 |
|
| 194 |
+
# WhatsApp service gateway. The main FastAPI app acts as a reverse proxy to
|
| 195 |
+
# the internal AgentDeck WhatsApp service (a Go application). Set
|
| 196 |
+
# WHATSAPP_SERVICE_ENABLED=true and point WHATSAPP_SERVICE_URL at the
|
| 197 |
+
# internal service (e.g. http://localhost:8080 when co-hosted, or
|
| 198 |
+
# http://whatsapp-service:8080 in a Docker network).
|
| 199 |
+
whatsapp_service_enabled: bool = Field(default=False, alias="WHATSAPP_SERVICE_ENABLED")
|
| 200 |
+
whatsapp_service_url: str = Field(default="http://localhost:8080", alias="WHATSAPP_SERVICE_URL")
|
| 201 |
+
whatsapp_service_timeout: float = Field(default=30.0, alias="WHATSAPP_SERVICE_TIMEOUT")
|
| 202 |
+
whatsapp_service_connect_timeout: float = Field(default=5.0, alias="WHATSAPP_SERVICE_CONNECT_TIMEOUT")
|
| 203 |
+
# Global API key of the WhatsApp service (GLOBAL_API_KEY). Used as the
|
| 204 |
+
# default `apikey` header when proxying admin routes; a per-instance token
|
| 205 |
+
# provided by the client is always forwarded unchanged.
|
| 206 |
+
whatsapp_service_global_api_key: str = Field(default="", alias="WHATSAPP_SERVICE_GLOBAL_API_KEY")
|
| 207 |
+
|
| 208 |
# Scheduler settings
|
| 209 |
max_http_timeout: float = 300.0
|
| 210 |
default_scheduler_timezone: str = "UTC"
|
docs/whatsapp-gateway.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# WhatsApp Service Gateway
|
| 2 |
+
|
| 3 |
+
The main FastAPI application (port **7860**) is the **public API gateway**. It
|
| 4 |
+
proxies every request under `/api/whatsapp/*` to the internal WhatsApp service
|
| 5 |
+
(a Go/whatsmeow application), which is not exposed publicly.
|
| 6 |
+
|
| 7 |
+
```text
|
| 8 |
+
Client
|
| 9 |
+
| HTTP
|
| 10 |
+
v
|
| 11 |
+
Main FastAPI :7860
|
| 12 |
+
| /api/whatsapp/...
|
| 13 |
+
v
|
| 14 |
+
WhatsApp Service (internal, e.g. http://localhost:8080)
|
| 15 |
+
|
|
| 16 |
+
v
|
| 17 |
+
Client
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
## Route mapping
|
| 21 |
+
|
| 22 |
+
The gateway forwards the **full path, query string, HTTP method and body**
|
| 23 |
+
unchanged. All routes require the main application's API key
|
| 24 |
+
(`Authorization: Bearer <API_KEY>`).
|
| 25 |
+
|
| 26 |
+
| Client request | WhatsApp service call |
|
| 27 |
+
|---|---|
|
| 28 |
+
| `GET /api/whatsapp/server/ok` | `GET /server/ok` (health) |
|
| 29 |
+
| `POST /api/whatsapp/instance/create` | `POST /instance/create` (admin) |
|
| 30 |
+
| `GET /api/whatsapp/instance/status` | `GET /instance/status` |
|
| 31 |
+
| `POST /api/whatsapp/send/text` | `POST /send/text` |
|
| 32 |
+
| `DELETE /api/whatsapp/instance/delete/:id` | `DELETE /instance/delete/:id` |
|
| 33 |
+
|
| 34 |
+
No path transformation is applied: after the `/api/whatsapp` prefix is stripped,
|
| 35 |
+
the rest of the path is appended to `WHATSAPP_SERVICE_URL`.
|
| 36 |
+
|
| 37 |
+
## Deployment model
|
| 38 |
+
|
| 39 |
+
Single Docker container (Hugging Face Spaces style): the Go binary is compiled
|
| 40 |
+
in a multi-stage `whatsapp-build` stage and embedded in the image at
|
| 41 |
+
`/app/whatsapp-service/server`. `start.sh` launches it as a sibling process and
|
| 42 |
+
`uvicorn` is the primary process. The gateway reaches it over loopback
|
| 43 |
+
(`http://127.0.0.1:8080`).
|
| 44 |
+
|
| 45 |
+
A crash of the WhatsApp service does **not** take the main app down: the gateway
|
| 46 |
+
returns sanitized `502`/`503` responses until it recovers (restart loop every
|
| 47 |
+
5s, degraded mode).
|
| 48 |
+
|
| 49 |
+
## Gateway behaviour
|
| 50 |
+
|
| 51 |
+
- **Auth:** the gateway requires the main app's Bearer API key. The WhatsApp
|
| 52 |
+
service's own `apikey` header is forwarded as-is when provided by the client
|
| 53 |
+
(per-instance token); otherwise the configured
|
| 54 |
+
`WHATSAPP_SERVICE_GLOBAL_API_KEY` is injected (admin routes).
|
| 55 |
+
- **Headers:** hop-by-hop headers and the gateway's `Authorization`/`Cookie` are
|
| 56 |
+
never forwarded. Response headers are whitelisted — internal
|
| 57 |
+
`Server`/`content-encoding`/hop-by-hop headers never reach the client.
|
| 58 |
+
- **Errors:** connection failures → `502`; timeouts → `504`; disabled service →
|
| 59 |
+
`503`. Upstream 4xx/5xx responses are passed through unchanged. Error bodies
|
| 60 |
+
never contain internal hostnames, URLs, exceptions or stack traces.
|
| 61 |
+
- **Payments/streaming:** responses are streamed back to the client (media/QR
|
| 62 |
+
downloads are not buffered).
|
| 63 |
+
|
| 64 |
+
## Configuration
|
| 65 |
+
|
| 66 |
+
| Variable | Default | Description |
|
| 67 |
+
|---|---|---|
|
| 68 |
+
| `WHATSAPP_SERVICE_ENABLED` | `false` | Enable the gateway routes. `start.sh` auto-enables it when the binary exists. |
|
| 69 |
+
| `WHATSAPP_SERVICE_URL` | `http://localhost:8080` | Base URL of the internal WhatsApp service. |
|
| 70 |
+
| `WHATSAPP_SERVICE_TIMEOUT` | `30` | Read/write/pool timeout for upstream calls (seconds). |
|
| 71 |
+
| `WHATSAPP_SERVICE_CONNECT_TIMEOUT` | `5` | Connect timeout for upstream calls (seconds). |
|
| 72 |
+
| `WHATSAPP_SERVICE_GLOBAL_API_KEY` | _(empty)_ | The WhatsApp service `GLOBAL_API_KEY`, injected as the default `apikey` header. |
|
| 73 |
+
| `WHATSAPP_SERVICE_PORT` | `8080` | Port `start.sh` uses to launch the embedded binary (`SERVER_PORT`). |
|
| 74 |
+
| `WHATSAPP_SERVICE_BINARY` | `/app/whatsapp-service/server` | Path to the embedded binary. |
|
| 75 |
+
|
| 76 |
+
### WhatsApp service env vars (centralized in the main app)
|
| 77 |
+
|
| 78 |
+
There is exactly **one** configuration source: the main application's `.env`
|
| 79 |
+
(or the deployment platform's environment). The Go service **never reads a
|
| 80 |
+
`.env` of its own** — `start.sh` / the Docker image inject its variables as
|
| 81 |
+
process environment, and `--dev` only optionally loads a local `.env` if one
|
| 82 |
+
exists. Full reference: the WhatsApp section of `.env.example`.
|
| 83 |
+
|
| 84 |
+
Values shared with the main app use the same names (`SUPABASE_URL`,
|
| 85 |
+
`REDIS_URL`, ...). A few names are mapped automatically by `start.sh` so you
|
| 86 |
+
only configure one value:
|
| 87 |
+
|
| 88 |
+
| Centralized setting | Injected into Go service as |
|
| 89 |
+
|---|---|
|
| 90 |
+
| `SUPABASE_SERVICE_ROLE_KEY` | `SUPABASE_SERVICE_KEY` |
|
| 91 |
+
| `WHATSAPP_SERVICE_GLOBAL_API_KEY` | `GLOBAL_API_KEY` |
|
| 92 |
+
| `WHATSAPP_SERVICE_PORT` | `SERVER_PORT` |
|
| 93 |
+
|
| 94 |
+
Other Go-service settings configured centrally: `SUPABASE_DB_URL`,
|
| 95 |
+
`DATABASE_SAVE_MESSAGES`, `CLIENT_NAME`, `CONNECT_ON_STARTUP`, `DEBUG_ENABLED`,
|
| 96 |
+
`LOG_TYPE`, `WEBHOOK_FILES`, `OS_NAME`, `WHATSAPP_VERSION_*`, `MINIO_*`,
|
| 97 |
+
`AMQP_*`, `NATS_*`, `WEBHOOK_URL`, `PROXY_*`, `API_AUDIO_CONVERTER*`,
|
| 98 |
+
`EVENT_IGNORE_*`, `QRCODE_MAX_COUNT`, `CHECK_USER_EXISTS`, `LOG_*`.
|
| 99 |
+
|
| 100 |
+
> Configure `WHATSAPP_SERVICE_GLOBAL_API_KEY` (equivalently `GLOBAL_API_KEY`)
|
| 101 |
+
> once — it feeds both the gateway's default `apikey` header and the Go
|
| 102 |
+
> service's authentication.
|
| 103 |
+
|
| 104 |
+
## Health checks
|
| 105 |
+
|
| 106 |
+
The main app's `GET /health` now includes a non-fatal `whatsapp_service` block:
|
| 107 |
+
|
| 108 |
+
```json
|
| 109 |
+
{"configured": true, "reachable": true, "status": "ok"}
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
The WhatsApp service's own liveness endpoint is exposed through the gateway at
|
| 113 |
+
`GET /api/whatsapp/server/ok`.
|
| 114 |
+
|
| 115 |
+
## Testing
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
python -m pytest tests/test_whatsapp_gateway.py -v
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
Covers: gateway auth, forwarding (method/path/query/body/headers), apikey
|
| 122 |
+
injection vs pass-through, upstream 4xx/5xx passthrough, 502/503/504 handling,
|
| 123 |
+
response-header sanitization and the `/health` integration.
|
| 124 |
+
|
| 125 |
+
A full stack test:
|
| 126 |
+
|
| 127 |
+
```bash
|
| 128 |
+
docker build -t agentdeck-backend:test .
|
| 129 |
+
# run with the WhatsApp env vars above + WHATSAPP_SERVICE_ENABLED=true
|
| 130 |
+
```
|
start.sh
CHANGED
|
@@ -13,6 +13,42 @@ print(banner)
|
|
| 13 |
" 2>/dev/null || true
|
| 14 |
echo "=============================================================================="
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
log "=== Starting up ==="
|
| 17 |
log "Python: $(python --version 2>&1)"
|
| 18 |
log "Working dir: $(pwd)"
|
|
@@ -53,6 +89,99 @@ else
|
|
| 53 |
log "WARNING: SearXNG settings not found at $SEARXNG_SETTINGS_PATH, skipping SearXNG start"
|
| 54 |
fi
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
# Start main app
|
| 57 |
HOST="${HOST:-0.0.0.0}"
|
| 58 |
PORT="${PORT:-7860}"
|
|
@@ -60,9 +189,22 @@ WORKERS="${WORKERS:-1}"
|
|
| 60 |
LOG_LEVEL="${LOG_LEVEL:-info}"
|
| 61 |
|
| 62 |
log "Starting uvicorn: host=$HOST port=$PORT workers=$WORKERS log_level=$LOG_LEVEL"
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
--host "$HOST" \
|
| 65 |
--port "$PORT" \
|
| 66 |
--workers "$WORKERS" \
|
| 67 |
--log-level "$LOG_LEVEL" \
|
| 68 |
-
--access-log
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
" 2>/dev/null || true
|
| 14 |
echo "=============================================================================="
|
| 15 |
|
| 16 |
+
# -----------------------------------------------------------------------------
|
| 17 |
+
# Environment: the main app's `.env`/`.env.local` are the SINGLE source of truth
|
| 18 |
+
# for every process this container starts (FastAPI, SearXNG, the embedded
|
| 19 |
+
# WhatsApp/Go service). Load them into the shell environment so sibling
|
| 20 |
+
# processes inherit the same configuration. In Docker these files are not
|
| 21 |
+
# shipped — the deployment platform injects the environment — so this is a
|
| 22 |
+
# no-op. Precedence (highest first): process environment, `.env.local`, `.env`.
|
| 23 |
+
# -----------------------------------------------------------------------------
|
| 24 |
+
declare -A _ENV_FROM_PROCESS
|
| 25 |
+
while IFS= read -r _envline; do
|
| 26 |
+
_ENV_FROM_PROCESS["${_envline%%=*}"]=1
|
| 27 |
+
done < <(env 2>/dev/null)
|
| 28 |
+
|
| 29 |
+
load_dotenv() {
|
| 30 |
+
local file="$1"
|
| 31 |
+
[ -f "$file" ] || return 0
|
| 32 |
+
local key val
|
| 33 |
+
while IFS='=' read -r key val; do
|
| 34 |
+
key="${key#"${key%%[![:space:]]*}"}"
|
| 35 |
+
[ -n "$key" ] || continue
|
| 36 |
+
case "$key" in
|
| 37 |
+
\#*) continue ;;
|
| 38 |
+
esac
|
| 39 |
+
case "$val" in
|
| 40 |
+
\"*\") val="${val#\"}"; val="${val%\"}" ;;
|
| 41 |
+
\'*\') val="${val#\'}"; val="${val%\'}" ;;
|
| 42 |
+
esac
|
| 43 |
+
if [ -z "${_ENV_FROM_PROCESS[$key]:-}" ]; then
|
| 44 |
+
export "$key=$val"
|
| 45 |
+
fi
|
| 46 |
+
done < "$file"
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
load_dotenv ".env"
|
| 50 |
+
load_dotenv ".env.local"
|
| 51 |
+
|
| 52 |
log "=== Starting up ==="
|
| 53 |
log "Python: $(python --version 2>&1)"
|
| 54 |
log "Working dir: $(pwd)"
|
|
|
|
| 89 |
log "WARNING: SearXNG settings not found at $SEARXNG_SETTINGS_PATH, skipping SearXNG start"
|
| 90 |
fi
|
| 91 |
|
| 92 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 93 |
+
# WhatsApp service (internal Go backend)
|
| 94 |
+
#
|
| 95 |
+
# The compiled whatsapp binary is embedded in the image (Dockerfile) and runs as
|
| 96 |
+
# a sibling process inside this container. The main FastAPI app proxies
|
| 97 |
+
# /api/whatsapp/* to it. If the binary is present the gateway is auto-enabled;
|
| 98 |
+
# set WHATSAPP_SERVICE_ENABLED=false to force it off, or set it explicitly.
|
| 99 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 100 |
+
|
| 101 |
+
WHATSAPP_SERVICE_BINARY="${WHATSAPP_SERVICE_BINARY:-/app/whatsapp-service/server}"
|
| 102 |
+
WHATSAPP_SERVICE_PORT="${WHATSAPP_SERVICE_PORT:-8080}"
|
| 103 |
+
WHATSAPP_SERVICE_ENABLED="${WHATSAPP_SERVICE_ENABLED:-}"
|
| 104 |
+
WHATSAPP_PID=""
|
| 105 |
+
|
| 106 |
+
if [ -z "$WHATSAPP_SERVICE_ENABLED" ]; then
|
| 107 |
+
if [ -x "$WHATSAPP_SERVICE_BINARY" ]; then
|
| 108 |
+
WHATSAPP_SERVICE_ENABLED="true"
|
| 109 |
+
else
|
| 110 |
+
WHATSAPP_SERVICE_ENABLED="false"
|
| 111 |
+
fi
|
| 112 |
+
fi
|
| 113 |
+
|
| 114 |
+
export WHATSAPP_SERVICE_ENABLED
|
| 115 |
+
export WHATSAPP_SERVICE_URL="${WHATSAPP_SERVICE_URL:-http://127.0.0.1:${WHATSAPP_SERVICE_PORT}}"
|
| 116 |
+
# Single source of truth: operator sets one of WHATSAPP_SERVICE_GLOBAL_API_KEY /
|
| 117 |
+
# GLOBAL_API_KEY; both the gateway (FastAPI) and the Go service receive it.
|
| 118 |
+
export WHATSAPP_SERVICE_GLOBAL_API_KEY="${WHATSAPP_SERVICE_GLOBAL_API_KEY:-${GLOBAL_API_KEY:-}}"
|
| 119 |
+
|
| 120 |
+
# Map the main app's centralized settings onto the names the WhatsApp (Go)
|
| 121 |
+
# service expects. Its process environment is built from THIS configuration
|
| 122 |
+
# only — the Go service never reads a .env of its own.
|
| 123 |
+
export SUPABASE_SERVICE_KEY="${SUPABASE_SERVICE_KEY:-${SUPABASE_SERVICE_ROLE_KEY:-}}"
|
| 124 |
+
|
| 125 |
+
# SUPABASE_DB_URL: derive the native Postgres URL from the pooler/direct URL
|
| 126 |
+
# (session-mode port 5432) when not set explicitly — single source of truth.
|
| 127 |
+
if [ -z "${SUPABASE_DB_URL:-}" ]; then
|
| 128 |
+
if [ -n "${POSTGRES_URL_POOLER:-}" ]; then
|
| 129 |
+
export SUPABASE_DB_URL="${POSTGRES_URL_POOLER/:6543\//:5432/}"
|
| 130 |
+
elif [ -n "${POSTGRES_URL:-}" ]; then
|
| 131 |
+
export SUPABASE_DB_URL="$POSTGRES_URL"
|
| 132 |
+
fi
|
| 133 |
+
fi
|
| 134 |
+
|
| 135 |
+
# Defaults for Go-service-only settings (not consumed by the main app).
|
| 136 |
+
export DATABASE_SAVE_MESSAGES="${DATABASE_SAVE_MESSAGES:-false}"
|
| 137 |
+
export CLIENT_NAME="${CLIENT_NAME:-agentdeck}"
|
| 138 |
+
|
| 139 |
+
if [ "$WHATSAPP_SERVICE_ENABLED" = "true" ]; then
|
| 140 |
+
if [ ! -x "$WHATSAPP_SERVICE_BINARY" ]; then
|
| 141 |
+
log "WARNING: WHATSAPP_SERVICE_ENABLED=true but binary not found at $WHATSAPP_SERVICE_BINARY — WhatsApp gateway disabled"
|
| 142 |
+
export WHATSAPP_SERVICE_ENABLED="false"
|
| 143 |
+
else
|
| 144 |
+
log "Starting WhatsApp service on port $WHATSAPP_SERVICE_PORT..."
|
| 145 |
+
: > /tmp/whatsapp-service.log
|
| 146 |
+
WHATSAPP_SERVICE_DIR="$(dirname "$WHATSAPP_SERVICE_BINARY")"
|
| 147 |
+
|
| 148 |
+
# Run the Go service in a restart loop so a crash does not take the
|
| 149 |
+
# main gateway down; the gateway returns 502/503 while it is down.
|
| 150 |
+
(
|
| 151 |
+
while true; do
|
| 152 |
+
cd "$WHATSAPP_SERVICE_DIR" 2>/dev/null || true
|
| 153 |
+
GLOBAL_API_KEY="${GLOBAL_API_KEY:-${WHATSAPP_SERVICE_GLOBAL_API_KEY:-}}" \
|
| 154 |
+
SERVER_PORT="$WHATSAPP_SERVICE_PORT" \
|
| 155 |
+
"$WHATSAPP_SERVICE_BINARY" >> /tmp/whatsapp-service.log 2>&1
|
| 156 |
+
code=$?
|
| 157 |
+
log "WARNING: WhatsApp service exited (code $code) — restarting in 5s"
|
| 158 |
+
sleep 5
|
| 159 |
+
done
|
| 160 |
+
) &
|
| 161 |
+
WHATSAPP_PID=$!
|
| 162 |
+
log "WhatsApp service process group PID: $WHATSAPP_PID"
|
| 163 |
+
|
| 164 |
+
# Wait for readiness (best effort — non-fatal, gateway degrades gracefully)
|
| 165 |
+
WHATSAPP_HEALTH_URL="http://127.0.0.1:${WHATSAPP_SERVICE_PORT}/server/ok"
|
| 166 |
+
WHATSAPP_READY=0
|
| 167 |
+
for _ in $(seq 1 30); do
|
| 168 |
+
if curl -fsS -o /dev/null "$WHATSAPP_HEALTH_URL" 2>/dev/null; then
|
| 169 |
+
WHATSAPP_READY=1
|
| 170 |
+
break
|
| 171 |
+
fi
|
| 172 |
+
sleep 1
|
| 173 |
+
done
|
| 174 |
+
if [ "$WHATSAPP_READY" = "1" ]; then
|
| 175 |
+
log "WhatsApp service healthy: $WHATSAPP_HEALTH_URL"
|
| 176 |
+
else
|
| 177 |
+
log "WARNING: WhatsApp service not ready at $WHATSAPP_HEALTH_URL — gateway runs in degraded mode"
|
| 178 |
+
tail -20 /tmp/whatsapp-service.log 2>/dev/null | while IFS= read -r line; do log " whatsapp: $line"; done
|
| 179 |
+
fi
|
| 180 |
+
fi
|
| 181 |
+
else
|
| 182 |
+
log "WhatsApp service disabled (WHATSAPP_SERVICE_ENABLED=$WHATSAPP_SERVICE_ENABLED)"
|
| 183 |
+
fi
|
| 184 |
+
|
| 185 |
# Start main app
|
| 186 |
HOST="${HOST:-0.0.0.0}"
|
| 187 |
PORT="${PORT:-7860}"
|
|
|
|
| 189 |
LOG_LEVEL="${LOG_LEVEL:-info}"
|
| 190 |
|
| 191 |
log "Starting uvicorn: host=$HOST port=$PORT workers=$WORKERS log_level=$LOG_LEVEL"
|
| 192 |
+
|
| 193 |
+
cleanup() {
|
| 194 |
+
log "Shutting down..."
|
| 195 |
+
[ -n "$WHATSAPP_PID" ] && kill "$WHATSAPP_PID" 2>/dev/null
|
| 196 |
+
[ -n "${UVICORN_PID:-}" ] && kill "$UVICORN_PID" 2>/dev/null
|
| 197 |
+
}
|
| 198 |
+
trap cleanup TERM INT
|
| 199 |
+
|
| 200 |
+
python -m uvicorn app.api.server:app \
|
| 201 |
--host "$HOST" \
|
| 202 |
--port "$PORT" \
|
| 203 |
--workers "$WORKERS" \
|
| 204 |
--log-level "$LOG_LEVEL" \
|
| 205 |
+
--access-log &
|
| 206 |
+
UVICORN_PID=$!
|
| 207 |
+
|
| 208 |
+
# Wait on uvicorn only: if the WhatsApp service dies, the main API keeps
|
| 209 |
+
# serving (degraded mode) instead of taking the whole container down.
|
| 210 |
+
wait "$UVICORN_PID"
|
test_integration.py
DELETED
|
@@ -1,92 +0,0 @@
|
|
| 1 |
-
"""Integration tests against running FastAPI server + SearXNG container."""
|
| 2 |
-
|
| 3 |
-
import json
|
| 4 |
-
import sys
|
| 5 |
-
import urllib.request
|
| 6 |
-
|
| 7 |
-
BASE = "http://localhost:8000/api/v1"
|
| 8 |
-
HEADERS = {
|
| 9 |
-
"Authorization": "Bearer N9ooESH05AiXrlpEKilv3o7OY1Rl5Pui",
|
| 10 |
-
"Content-Type": "application/json",
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
def req(method, path, body=None):
|
| 15 |
-
url = f"{BASE}{path}"
|
| 16 |
-
data = json.dumps(body).encode() if body else None
|
| 17 |
-
r = urllib.request.Request(url, data=data, headers=HEADERS, method=method)
|
| 18 |
-
resp = urllib.request.urlopen(r, timeout=30)
|
| 19 |
-
return json.loads(resp.read())
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
passed = 0
|
| 23 |
-
failed = 0
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def check(name, ok, detail=""):
|
| 27 |
-
global passed, failed
|
| 28 |
-
if ok:
|
| 29 |
-
passed += 1
|
| 30 |
-
print(f" PASS: {name}")
|
| 31 |
-
else:
|
| 32 |
-
failed += 1
|
| 33 |
-
print(f" FAIL: {name} - {detail}")
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
# ---- Test 1: Health ----
|
| 37 |
-
print("1. Health check")
|
| 38 |
-
r = req("GET", "/web-search/health")
|
| 39 |
-
check("health returns dict", isinstance(r, dict))
|
| 40 |
-
|
| 41 |
-
# ---- Test 2: Search GET ----
|
| 42 |
-
print("\n2. Search GET")
|
| 43 |
-
r = req("GET", "/web-search?q=hello+world&max_results=3")
|
| 44 |
-
check("success=True", r["success"] is True)
|
| 45 |
-
check("has results", r["number_of_results"] > 0)
|
| 46 |
-
check("title is string", isinstance(r["results"][0]["title"], str))
|
| 47 |
-
|
| 48 |
-
# ---- Test 3: Search POST ----
|
| 49 |
-
print("\n3. Search POST")
|
| 50 |
-
r = req("POST", "/web-search", {"q": "python programming", "categories": "general,it", "max_results": 3})
|
| 51 |
-
check("success=True", r["success"] is True)
|
| 52 |
-
check("has results", r["number_of_results"] > 0)
|
| 53 |
-
|
| 54 |
-
# ---- Test 4: Autocomplete GET ----
|
| 55 |
-
print("\n4. Autocomplete GET")
|
| 56 |
-
r = req("GET", "/web-search/autocomplete?q=hello+wor")
|
| 57 |
-
check("success=True", r["success"] is True)
|
| 58 |
-
check("has suggestions", len(r["suggestions"]) > 0)
|
| 59 |
-
|
| 60 |
-
# ---- Test 5: Autocomplete POST ----
|
| 61 |
-
print("\n5. Autocomplete POST")
|
| 62 |
-
r = req("POST", "/web-search/autocomplete", {"q": "python progr"})
|
| 63 |
-
check("success=True", r["success"] is True)
|
| 64 |
-
check("has suggestions", len(r["suggestions"]) > 0)
|
| 65 |
-
|
| 66 |
-
# ---- Test 6: Config ----
|
| 67 |
-
print("\n6. Config")
|
| 68 |
-
r = req("GET", "/web-search/config")
|
| 69 |
-
check("success=True", r["success"] is True)
|
| 70 |
-
check("has engines list", isinstance(r["engines"], list))
|
| 71 |
-
|
| 72 |
-
# ---- Test 7: Engine descriptions ----
|
| 73 |
-
print("\n7. Engine descriptions")
|
| 74 |
-
r = req("GET", "/web-search/engine-descriptions")
|
| 75 |
-
check("success=True", r["success"] is True)
|
| 76 |
-
check("has engines dict", isinstance(r["engines"], dict))
|
| 77 |
-
check("many engines", len(r["engines"]) > 10)
|
| 78 |
-
|
| 79 |
-
# ---- Test 8: Stats ----
|
| 80 |
-
print("\n8. Stats")
|
| 81 |
-
r = req("GET", "/web-search/stats")
|
| 82 |
-
check("success=True", r["success"] is True)
|
| 83 |
-
|
| 84 |
-
# ---- Summary ----
|
| 85 |
-
print(f"\n{'='*50}")
|
| 86 |
-
print(f"RESULTS: {passed} passed, {failed} failed out of {passed+failed} tests")
|
| 87 |
-
if failed == 0:
|
| 88 |
-
print("ALL INTEGRATION TESTS PASSED")
|
| 89 |
-
else:
|
| 90 |
-
print("SOME TESTS FAILED")
|
| 91 |
-
|
| 92 |
-
sys.exit(0 if failed == 0 else 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_webhook_socket.py
DELETED
|
@@ -1,446 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import asyncio
|
| 4 |
-
import hashlib
|
| 5 |
-
import hmac
|
| 6 |
-
import json
|
| 7 |
-
import os
|
| 8 |
-
import sys
|
| 9 |
-
import time
|
| 10 |
-
from typing import AsyncGenerator
|
| 11 |
-
|
| 12 |
-
import pytest
|
| 13 |
-
|
| 14 |
-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 15 |
-
|
| 16 |
-
from app.services.webhook_socket_service import ChannelManager, sign_payload, verify_signature
|
| 17 |
-
|
| 18 |
-
API_KEY = "changeme"
|
| 19 |
-
AUTH_HEADER = {"Authorization": f"Bearer {API_KEY}"}
|
| 20 |
-
BASE = "http://localhost:7860/api/v1"
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
class TestChannelManager:
|
| 24 |
-
def test_create_channel(self):
|
| 25 |
-
mgr = ChannelManager()
|
| 26 |
-
ch = mgr.create_channel()
|
| 27 |
-
assert ch.channel_id is not None
|
| 28 |
-
assert len(ch.channel_id) == 16
|
| 29 |
-
assert ch.buffer_size == 0
|
| 30 |
-
assert ch.secret is None
|
| 31 |
-
|
| 32 |
-
def test_create_channel_custom_id(self):
|
| 33 |
-
mgr = ChannelManager()
|
| 34 |
-
ch = mgr.create_channel(channel_id="my-channel")
|
| 35 |
-
assert ch.channel_id == "my-channel"
|
| 36 |
-
|
| 37 |
-
def test_create_channel_with_secret_and_buffer(self):
|
| 38 |
-
mgr = ChannelManager()
|
| 39 |
-
ch = mgr.create_channel(channel_id="secure", secret="s3cret", buffer_size=10)
|
| 40 |
-
assert ch.secret == "s3cret"
|
| 41 |
-
assert ch.buffer_size == 10
|
| 42 |
-
|
| 43 |
-
def test_get_channel(self):
|
| 44 |
-
mgr = ChannelManager()
|
| 45 |
-
mgr.create_channel(channel_id="abc")
|
| 46 |
-
assert mgr.get_channel("abc") is not None
|
| 47 |
-
assert mgr.get_channel("nonexistent") is None
|
| 48 |
-
|
| 49 |
-
def test_delete_channel(self):
|
| 50 |
-
mgr = ChannelManager()
|
| 51 |
-
mgr.create_channel(channel_id="del-me")
|
| 52 |
-
assert mgr.delete_channel("del-me") is True
|
| 53 |
-
assert mgr.get_channel("del-me") is None
|
| 54 |
-
assert mgr.delete_channel("del-me") is False
|
| 55 |
-
|
| 56 |
-
def test_create_duplicate_id(self):
|
| 57 |
-
mgr = ChannelManager()
|
| 58 |
-
mgr.create_channel(channel_id="dup")
|
| 59 |
-
ch2 = mgr.create_channel(channel_id="dup")
|
| 60 |
-
assert mgr.get_channel("dup") is ch2
|
| 61 |
-
|
| 62 |
-
def test_default_buffer_from_manager(self):
|
| 63 |
-
mgr = ChannelManager(default_buffer=25)
|
| 64 |
-
ch = mgr.create_channel()
|
| 65 |
-
assert ch.buffer_size == 25
|
| 66 |
-
|
| 67 |
-
def test_publish_nonexistent_channel(self):
|
| 68 |
-
mgr = ChannelManager()
|
| 69 |
-
result = asyncio.run(mgr.publish("no-such-channel", {"hello": "world"}))
|
| 70 |
-
assert result == -1
|
| 71 |
-
|
| 72 |
-
@pytest.mark.asyncio
|
| 73 |
-
async def test_publish_and_buffer(self):
|
| 74 |
-
mgr = ChannelManager()
|
| 75 |
-
ch = mgr.create_channel(channel_id="buf-test", buffer_size=3)
|
| 76 |
-
|
| 77 |
-
await mgr.publish("buf-test", {"n": 1})
|
| 78 |
-
await mgr.publish("buf-test", {"n": 2})
|
| 79 |
-
await mgr.publish("buf-test", {"n": 3})
|
| 80 |
-
await mgr.publish("buf-test", {"n": 4})
|
| 81 |
-
|
| 82 |
-
assert len(ch.history) == 3
|
| 83 |
-
assert ch.history[0]["payload"]["n"] == 2
|
| 84 |
-
assert ch.history[2]["payload"]["n"] == 4
|
| 85 |
-
assert ch.message_count == 4
|
| 86 |
-
|
| 87 |
-
def test_stats(self):
|
| 88 |
-
mgr = ChannelManager()
|
| 89 |
-
mgr.create_channel(channel_id="a")
|
| 90 |
-
mgr.create_channel(channel_id="b")
|
| 91 |
-
stats = mgr.stats()
|
| 92 |
-
assert stats["channels"] == 2
|
| 93 |
-
assert stats["total_subscribers"] == 0
|
| 94 |
-
|
| 95 |
-
def test_sign_and_verify(self):
|
| 96 |
-
secret = "my-secret"
|
| 97 |
-
body = b'{"hello":"world"}'
|
| 98 |
-
sig = sign_payload(secret, body)
|
| 99 |
-
assert sig.startswith("sha256=")
|
| 100 |
-
assert verify_signature(secret, body, sig) is True
|
| 101 |
-
assert verify_signature(secret, body, "sha256=bad") is False
|
| 102 |
-
assert verify_signature("wrong-secret", body, sig) is False
|
| 103 |
-
|
| 104 |
-
def test_sign_constant_result(self):
|
| 105 |
-
secret = "test"
|
| 106 |
-
body = b"data"
|
| 107 |
-
sig1 = sign_payload(secret, body)
|
| 108 |
-
sig2 = sign_payload(secret, body)
|
| 109 |
-
assert sig1 == sig2
|
| 110 |
-
|
| 111 |
-
def test_channel_info_fields(self):
|
| 112 |
-
mgr = ChannelManager()
|
| 113 |
-
mgr.create_channel(channel_id="info-test", buffer_size=5)
|
| 114 |
-
ch = mgr.get_channel("info-test")
|
| 115 |
-
assert ch.channel_id == "info-test"
|
| 116 |
-
assert ch.buffer_size == 5
|
| 117 |
-
assert ch.message_count == 0
|
| 118 |
-
assert ch.created_at > 0
|
| 119 |
-
assert ch.last_activity > 0
|
| 120 |
-
|
| 121 |
-
def test_publish_with_subscriber(self):
|
| 122 |
-
mgr = ChannelManager()
|
| 123 |
-
mgr.create_channel(channel_id="sub-test")
|
| 124 |
-
ch = mgr.get_channel("sub-test")
|
| 125 |
-
|
| 126 |
-
async def dummy():
|
| 127 |
-
return ch.channel_id
|
| 128 |
-
q = asyncio.Queue()
|
| 129 |
-
ch.subscribers[dummy] = q
|
| 130 |
-
|
| 131 |
-
result = asyncio.run(mgr.publish("sub-test", {"msg": "hello"}))
|
| 132 |
-
assert result == 1
|
| 133 |
-
assert ch.message_count == 1
|
| 134 |
-
|
| 135 |
-
def test_multiple_publishes(self):
|
| 136 |
-
mgr = ChannelManager()
|
| 137 |
-
ch = mgr.create_channel(channel_id="multi-pub", buffer_size=10)
|
| 138 |
-
for i in range(5):
|
| 139 |
-
asyncio.run(mgr.publish("multi-pub", {"n": i}))
|
| 140 |
-
assert ch.message_count == 5
|
| 141 |
-
assert len(ch.history) == 5
|
| 142 |
-
|
| 143 |
-
def test_channel_manager_defaults(self):
|
| 144 |
-
mgr = ChannelManager()
|
| 145 |
-
assert mgr.default_buffer == 0
|
| 146 |
-
assert mgr.channels == {}
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
pytestmark_integration = pytest.mark.skipif(
|
| 150 |
-
not os.environ.get("RUN_INTEGRATION_TESTS"),
|
| 151 |
-
reason="Set RUN_INTEGRATION_TESTS=1 to run integration tests",
|
| 152 |
-
)
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
def _sign(body: bytes, secret: str) -> str:
|
| 156 |
-
return "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
@pytest.mark.skipif(
|
| 160 |
-
not os.environ.get("RUN_INTEGRATION_TESTS"),
|
| 161 |
-
reason="Set RUN_INTEGRATION_TESTS=1 to run integration tests",
|
| 162 |
-
)
|
| 163 |
-
class TestWebhookSocketIntegration:
|
| 164 |
-
@pytest.fixture(autouse=True)
|
| 165 |
-
async def _setup(self):
|
| 166 |
-
import httpx
|
| 167 |
-
|
| 168 |
-
async with httpx.AsyncClient(base_url=BASE) as client:
|
| 169 |
-
self.client = client
|
| 170 |
-
yield
|
| 171 |
-
|
| 172 |
-
async def _create_channel(self, **kwargs) -> dict:
|
| 173 |
-
resp = await self.client.post("/channels", json=kwargs, headers=AUTH_HEADER)
|
| 174 |
-
assert resp.status_code == 201
|
| 175 |
-
return resp.json()
|
| 176 |
-
|
| 177 |
-
async def _delete_channel(self, channel_id: str):
|
| 178 |
-
resp = await self.client.delete(f"/channels/{channel_id}", headers=AUTH_HEADER)
|
| 179 |
-
return resp.status_code == 200
|
| 180 |
-
|
| 181 |
-
async def test_health(self):
|
| 182 |
-
resp = await self.client.get("/health")
|
| 183 |
-
assert resp.status_code == 200
|
| 184 |
-
data = resp.json()
|
| 185 |
-
assert data["success"] is True
|
| 186 |
-
|
| 187 |
-
async def test_create_and_list_channels(self):
|
| 188 |
-
ch = await self._create_channel(channel_id="test-list", buffer_size=5)
|
| 189 |
-
assert ch["channel_id"] == "test-list"
|
| 190 |
-
|
| 191 |
-
resp = await self.client.get("/channels", headers=AUTH_HEADER)
|
| 192 |
-
assert resp.status_code == 200
|
| 193 |
-
data = resp.json()
|
| 194 |
-
assert any(c["channel_id"] == "test-list" for c in data["channels"])
|
| 195 |
-
|
| 196 |
-
await self._delete_channel("test-list")
|
| 197 |
-
|
| 198 |
-
async def test_create_channel_no_auth(self):
|
| 199 |
-
resp = await self.client.post("/channels", json={})
|
| 200 |
-
assert resp.status_code in (401, 403)
|
| 201 |
-
|
| 202 |
-
async def test_create_duplicate_channel(self):
|
| 203 |
-
await self._create_channel(channel_id="dup-test")
|
| 204 |
-
resp = await self.client.post("/channels", json={"channel_id": "dup-test"}, headers=AUTH_HEADER)
|
| 205 |
-
assert resp.status_code == 409
|
| 206 |
-
await self._delete_channel("dup-test")
|
| 207 |
-
|
| 208 |
-
async def test_channel_info(self):
|
| 209 |
-
await self._create_channel(channel_id="info-test")
|
| 210 |
-
resp = await self.client.get("/channels/info-test", headers=AUTH_HEADER)
|
| 211 |
-
assert resp.status_code == 200
|
| 212 |
-
data = resp.json()
|
| 213 |
-
assert data["channel_id"] == "info-test"
|
| 214 |
-
assert data["subscribers"] == 0
|
| 215 |
-
assert data["messages"] == 0
|
| 216 |
-
await self._delete_channel("info-test")
|
| 217 |
-
|
| 218 |
-
async def test_channel_info_not_found(self):
|
| 219 |
-
resp = await self.client.get("/channels/no-such", headers=AUTH_HEADER)
|
| 220 |
-
assert resp.status_code == 404
|
| 221 |
-
|
| 222 |
-
async def test_delete_channel(self):
|
| 223 |
-
await self._create_channel(channel_id="delete-me")
|
| 224 |
-
resp = await self.client.delete("/channels/delete-me", headers=AUTH_HEADER)
|
| 225 |
-
assert resp.status_code == 200
|
| 226 |
-
assert resp.json()["deleted"] == "delete-me"
|
| 227 |
-
|
| 228 |
-
resp = await self.client.get("/channels/delete-me", headers=AUTH_HEADER)
|
| 229 |
-
assert resp.status_code == 404
|
| 230 |
-
|
| 231 |
-
async def test_delete_channel_not_found(self):
|
| 232 |
-
resp = await self.client.delete("/channels/no-such", headers=AUTH_HEADER)
|
| 233 |
-
assert resp.status_code == 404
|
| 234 |
-
|
| 235 |
-
async def test_webhook_delivers_to_ws(self):
|
| 236 |
-
ch = await self._create_channel(channel_id="ws-deliver", buffer_size=5)
|
| 237 |
-
cid = ch["channel_id"]
|
| 238 |
-
|
| 239 |
-
import websockets
|
| 240 |
-
|
| 241 |
-
ws_url = f"ws://localhost:7860/api/v1/ws/{cid}"
|
| 242 |
-
|
| 243 |
-
async with websockets.connect(ws_url) as ws:
|
| 244 |
-
connected = json.loads(await ws.recv())
|
| 245 |
-
assert connected["event"] == "connected"
|
| 246 |
-
assert connected["channel"] == cid
|
| 247 |
-
|
| 248 |
-
payload = {"msg": "hello from webhook", "num": 42}
|
| 249 |
-
resp = await self.client.post(f"/webhook/{cid}", json=payload)
|
| 250 |
-
assert resp.status_code == 200
|
| 251 |
-
wh_data = resp.json()
|
| 252 |
-
assert wh_data["status"] == "delivered"
|
| 253 |
-
assert wh_data["subscribers_notified"] == 1
|
| 254 |
-
|
| 255 |
-
received = json.loads(await ws.recv())
|
| 256 |
-
assert received["event"] == "message"
|
| 257 |
-
assert received["channel"] == cid
|
| 258 |
-
assert received["payload"] == payload
|
| 259 |
-
|
| 260 |
-
await self._delete_channel(cid)
|
| 261 |
-
|
| 262 |
-
async def test_webhook_no_subscribers(self):
|
| 263 |
-
ch = await self._create_channel(channel_id="no-subs")
|
| 264 |
-
resp = await self.client.post(f"/webhook/{ch['channel_id']}", json={"data": 1})
|
| 265 |
-
assert resp.status_code == 200
|
| 266 |
-
assert resp.json()["subscribers_notified"] == 0
|
| 267 |
-
await self._delete_channel(ch["channel_id"])
|
| 268 |
-
|
| 269 |
-
async def test_webhook_not_found(self):
|
| 270 |
-
resp = await self.client.post("/webhook/no-such", json={"x": 1})
|
| 271 |
-
assert resp.status_code == 404
|
| 272 |
-
|
| 273 |
-
async def test_hook_alias(self):
|
| 274 |
-
ch = await self._create_channel(channel_id="hook-alias")
|
| 275 |
-
resp = await self.client.post(f"/hook/{ch['channel_id']}", json={"test": True})
|
| 276 |
-
assert resp.status_code == 200
|
| 277 |
-
assert resp.json()["status"] == "delivered"
|
| 278 |
-
await self._delete_channel(ch["channel_id"])
|
| 279 |
-
|
| 280 |
-
async def test_hmac_signed_webhook(self):
|
| 281 |
-
secret = "hmac-test-secret"
|
| 282 |
-
ch = await self._create_channel(channel_id="hmac-test", secret=secret)
|
| 283 |
-
cid = ch["channel_id"]
|
| 284 |
-
|
| 285 |
-
payload = b'{"signed": "data"}'
|
| 286 |
-
sig = _sign(payload, secret)
|
| 287 |
-
|
| 288 |
-
resp = await self.client.post(
|
| 289 |
-
f"/webhook/{cid}",
|
| 290 |
-
content=payload,
|
| 291 |
-
headers={"Content-Type": "application/json", "X-Signature-256": sig},
|
| 292 |
-
)
|
| 293 |
-
assert resp.status_code == 200, resp.text
|
| 294 |
-
|
| 295 |
-
resp_bad = await self.client.post(
|
| 296 |
-
f"/webhook/{cid}",
|
| 297 |
-
content=payload,
|
| 298 |
-
headers={"Content-Type": "application/json", "X-Signature-256": "sha256=bad"},
|
| 299 |
-
)
|
| 300 |
-
assert resp_bad.status_code == 401
|
| 301 |
-
|
| 302 |
-
resp_no_sig = await self.client.post(
|
| 303 |
-
f"/webhook/{cid}",
|
| 304 |
-
content=payload,
|
| 305 |
-
headers={"Content-Type": "application/json"},
|
| 306 |
-
)
|
| 307 |
-
assert resp_no_sig.status_code == 401
|
| 308 |
-
|
| 309 |
-
await self._delete_channel(cid)
|
| 310 |
-
|
| 311 |
-
async def test_webhook_form_urlencoded(self):
|
| 312 |
-
ch = await self._create_channel(channel_id="form-test")
|
| 313 |
-
cid = ch["channel_id"]
|
| 314 |
-
|
| 315 |
-
resp = await self.client.post(
|
| 316 |
-
f"/webhook/{cid}",
|
| 317 |
-
data={"field1": "value1", "field2": "value2"},
|
| 318 |
-
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
| 319 |
-
)
|
| 320 |
-
assert resp.status_code == 200
|
| 321 |
-
assert resp.json()["status"] == "delivered"
|
| 322 |
-
|
| 323 |
-
await self._delete_channel(cid)
|
| 324 |
-
|
| 325 |
-
async def test_webhook_raw_text(self):
|
| 326 |
-
ch = await self._create_channel(channel_id="raw-test")
|
| 327 |
-
cid = ch["channel_id"]
|
| 328 |
-
|
| 329 |
-
resp = await self.client.post(
|
| 330 |
-
f"/webhook/{cid}",
|
| 331 |
-
content="just some raw text",
|
| 332 |
-
headers={"Content-Type": "text/plain"},
|
| 333 |
-
)
|
| 334 |
-
assert resp.status_code == 200
|
| 335 |
-
|
| 336 |
-
await self._delete_channel(cid)
|
| 337 |
-
|
| 338 |
-
async def test_ws_replay_buffer(self):
|
| 339 |
-
ch = await self._create_channel(channel_id="replay-test", buffer_size=3)
|
| 340 |
-
cid = ch["channel_id"]
|
| 341 |
-
|
| 342 |
-
await self.client.post(f"/webhook/{cid}", json={"n": 1})
|
| 343 |
-
await self.client.post(f"/webhook/{cid}", json={"n": 2})
|
| 344 |
-
await self.client.post(f"/webhook/{cid}", json={"n": 3})
|
| 345 |
-
|
| 346 |
-
import websockets
|
| 347 |
-
|
| 348 |
-
ws_url = f"ws://localhost:7860/api/v1/ws/{cid}"
|
| 349 |
-
async with websockets.connect(ws_url) as ws:
|
| 350 |
-
connected = json.loads(await ws.recv())
|
| 351 |
-
assert connected["event"] == "connected"
|
| 352 |
-
assert connected["buffered"] == 3
|
| 353 |
-
|
| 354 |
-
for expected_n in [1, 2, 3]:
|
| 355 |
-
msg = json.loads(await ws.recv())
|
| 356 |
-
assert msg["payload"]["n"] == expected_n
|
| 357 |
-
|
| 358 |
-
await self._delete_channel(cid)
|
| 359 |
-
|
| 360 |
-
async def test_ws_auth_with_secret(self):
|
| 361 |
-
ch = await self._create_channel(channel_id="ws-auth-test", secret="topsecret")
|
| 362 |
-
cid = ch["channel_id"]
|
| 363 |
-
|
| 364 |
-
import websockets
|
| 365 |
-
|
| 366 |
-
ws_url = f"ws://localhost:7860/api/v1/ws/{cid}"
|
| 367 |
-
|
| 368 |
-
async with websockets.connect(f"{ws_url}?secret=topsecret") as ws:
|
| 369 |
-
msg = json.loads(await ws.recv())
|
| 370 |
-
assert msg["event"] == "connected"
|
| 371 |
-
|
| 372 |
-
async with websockets.connect(ws_url) as ws:
|
| 373 |
-
msg = json.loads(await ws.recv())
|
| 374 |
-
assert msg["event"] == "error"
|
| 375 |
-
|
| 376 |
-
await self._delete_channel(cid)
|
| 377 |
-
|
| 378 |
-
async def test_ws_channel_not_found(self):
|
| 379 |
-
import websockets
|
| 380 |
-
|
| 381 |
-
async with websockets.connect("ws://localhost:7860/api/v1/ws/does-not-exist") as ws:
|
| 382 |
-
msg = json.loads(await ws.recv())
|
| 383 |
-
assert msg["event"] == "error"
|
| 384 |
-
assert "channel not found" in msg["message"]
|
| 385 |
-
|
| 386 |
-
async def test_stats_endpoint(self):
|
| 387 |
-
resp = await self.client.get("/webhook-socket/stats", headers=AUTH_HEADER)
|
| 388 |
-
assert resp.status_code == 200
|
| 389 |
-
data = resp.json()
|
| 390 |
-
assert "channels" in data
|
| 391 |
-
assert "total_messages" in data
|
| 392 |
-
assert "total_subscribers" in data
|
| 393 |
-
|
| 394 |
-
async def test_full_lifecycle(self):
|
| 395 |
-
cid = "lifecycle-test"
|
| 396 |
-
|
| 397 |
-
ch = await self._create_channel(channel_id=cid, buffer_size=10, secret="life-secret")
|
| 398 |
-
assert ch["channel_id"] == cid
|
| 399 |
-
|
| 400 |
-
info_resp = await self.client.get(f"/channels/{cid}", headers=AUTH_HEADER)
|
| 401 |
-
assert info_resp.status_code == 200
|
| 402 |
-
assert info_resp.json()["messages"] == 0
|
| 403 |
-
|
| 404 |
-
import websockets
|
| 405 |
-
|
| 406 |
-
ws_url = f"ws://localhost:7860/api/v1/ws/{cid}?secret=life-secret"
|
| 407 |
-
async with websockets.connect(ws_url) as ws:
|
| 408 |
-
connected = json.loads(await ws.recv())
|
| 409 |
-
assert connected["event"] == "connected"
|
| 410 |
-
|
| 411 |
-
payload = {"event_type": "push", "data": {"ref": "main"}}
|
| 412 |
-
sig = _sign(json.dumps(payload).encode(), "life-secret")
|
| 413 |
-
resp = await self.client.post(
|
| 414 |
-
f"/webhook/{cid}",
|
| 415 |
-
json=payload,
|
| 416 |
-
headers={"X-Signature-256": sig, "X-GitHub-Event": "push"},
|
| 417 |
-
)
|
| 418 |
-
assert resp.status_code == 200
|
| 419 |
-
wh = resp.json()
|
| 420 |
-
assert wh["subscribers_notified"] == 1
|
| 421 |
-
|
| 422 |
-
received = json.loads(await ws.recv())
|
| 423 |
-
assert received["event"] == "message"
|
| 424 |
-
assert received["payload"]["event_type"] == "push"
|
| 425 |
-
assert received["headers"]["X-GitHub-Event"] == "push"
|
| 426 |
-
|
| 427 |
-
stats_resp = await self.client.get(f"/channels/{cid}", headers=AUTH_HEADER)
|
| 428 |
-
assert stats_resp.json()["messages"] == 1
|
| 429 |
-
|
| 430 |
-
await self._delete_channel(cid)
|
| 431 |
-
|
| 432 |
-
not_found = await self.client.get(f"/channels/{cid}", headers=AUTH_HEADER)
|
| 433 |
-
assert not_found.status_code == 404
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
if __name__ == "__main__":
|
| 437 |
-
import subprocess
|
| 438 |
-
import sys as _sys
|
| 439 |
-
|
| 440 |
-
os.environ["RUN_INTEGRATION_TESTS"] = "1"
|
| 441 |
-
_sys.exit(
|
| 442 |
-
subprocess.run(
|
| 443 |
-
[_sys.executable, "-m", "pytest", __file__, "-v", "--tb=short"],
|
| 444 |
-
cwd=os.path.join(os.path.dirname(__file__), ".."),
|
| 445 |
-
).returncode
|
| 446 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
whatsapp-service/.gitignore
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
logs/*
|
| 3 |
+
build/
|
| 4 |
+
*.prof
|
| 5 |
+
coverage.*
|
| 6 |
+
.air.toml
|
| 7 |
+
.idea/
|
| 8 |
+
.vscode/
|
| 9 |
+
.DS_Store
|
| 10 |
+
evolution-go
|
| 11 |
+
agentdeck-whatsapp-service.exe
|
| 12 |
+
build/
|
| 13 |
+
|
| 14 |
+
# Local dev/test/doc artifacts (kept out of the production repo)
|
| 15 |
+
tests/
|
| 16 |
+
ddl/
|
| 17 |
+
*.md
|
| 18 |
+
*_test.go
|
| 19 |
+
__pycache__/
|
| 20 |
+
*.pyc
|
whatsapp-service/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright 2026 AgentDeck
|
| 2 |
+
|
| 3 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
you may not use this file except in compliance with the License.
|
| 5 |
+
You may obtain a copy of the License at
|
| 6 |
+
|
| 7 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
|
| 9 |
+
Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
See the License for the specific language governing permissions and
|
| 13 |
+
limitations under the License.
|
whatsapp-service/Makefile
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.PHONY: help dev run build test clean swagger deps docker-build docker-run install setup migrate-up migrate-down logs
|
| 2 |
+
|
| 3 |
+
# Configurações
|
| 4 |
+
APP_NAME=agentdeck-whatsapp
|
| 5 |
+
MAIN_PATH=cmd/agentdeck-whatsapp-service/main.go
|
| 6 |
+
BUILD_DIR=build
|
| 7 |
+
GO=go
|
| 8 |
+
VERSION=$(shell cat VERSION 2>/dev/null || echo "0.7.2")
|
| 9 |
+
LDFLAGS=-ldflags "-X main.version=$(VERSION)"
|
| 10 |
+
GOFLAGS=-v
|
| 11 |
+
|
| 12 |
+
# Cores para output
|
| 13 |
+
GREEN=\033[0;32m
|
| 14 |
+
YELLOW=\033[0;33m
|
| 15 |
+
RED=\033[0;31m
|
| 16 |
+
NC=\033[0m # No Color
|
| 17 |
+
|
| 18 |
+
##@ Ajuda
|
| 19 |
+
|
| 20 |
+
help: ## Exibe esta mensagem de ajuda
|
| 21 |
+
@echo "$(GREEN)AgentDeck Whatsapp Service - Makefile$(NC)"
|
| 22 |
+
@echo ""
|
| 23 |
+
@awk 'BEGIN {FS = ":.*##"; printf "\nUso:\n make $(YELLOW)<target>$(NC)\n"} /^[a-zA-Z_-]+:.*?##/ { printf " $(GREEN)%-15s$(NC) %s\n", $$1, $$2 } /^##@/ { printf "\n$(YELLOW)%s$(NC)\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
| 24 |
+
|
| 25 |
+
##@ Desenvolvimento
|
| 26 |
+
|
| 27 |
+
dev: ## Roda a aplicação em modo desenvolvimento
|
| 28 |
+
@echo "$(GREEN)🚀 Rodando AgentDeck Whatsapp Service em modo desenvolvimento...$(NC)"
|
| 29 |
+
$(GO) run $(LDFLAGS) $(MAIN_PATH) -dev
|
| 30 |
+
|
| 31 |
+
run: ## Roda a aplicação em modo produção
|
| 32 |
+
@echo "$(GREEN)🚀 Rodando AgentDeck Whatsapp Service...$(NC)"
|
| 33 |
+
$(GO) run $(MAIN_PATH)
|
| 34 |
+
|
| 35 |
+
watch: ## Roda a aplicação com hot reload (requer air)
|
| 36 |
+
@if command -v air > /dev/null; then \
|
| 37 |
+
echo "$(GREEN)🔥 Rodando com hot reload...$(NC)"; \
|
| 38 |
+
air; \
|
| 39 |
+
else \
|
| 40 |
+
echo "$(RED)❌ Air não instalado. Instale com: go install github.com/cosmtrek/air@latest$(NC)"; \
|
| 41 |
+
exit 1; \
|
| 42 |
+
fi
|
| 43 |
+
|
| 44 |
+
##@ Build
|
| 45 |
+
|
| 46 |
+
build: ## Compila a aplicação
|
| 47 |
+
@echo "$(GREEN)🔨 Compilando $(APP_NAME)...$(NC)"
|
| 48 |
+
@mkdir -p $(BUILD_DIR)
|
| 49 |
+
$(GO) build $(GOFLAGS) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_PATH)
|
| 50 |
+
@echo "$(GREEN)✅ Build completo: $(BUILD_DIR)/$(APP_NAME)$(NC)"
|
| 51 |
+
|
| 52 |
+
build-linux: ## Compila para Linux
|
| 53 |
+
@echo "$(GREEN)🔨 Compilando para Linux...$(NC)"
|
| 54 |
+
@mkdir -p $(BUILD_DIR)
|
| 55 |
+
GOOS=linux GOARCH=amd64 $(GO) build $(GOFLAGS) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 $(MAIN_PATH)
|
| 56 |
+
@echo "$(GREEN)✅ Build Linux completo$(NC)"
|
| 57 |
+
|
| 58 |
+
build-windows: ## Compila para Windows
|
| 59 |
+
@echo "$(GREEN)🔨 Compilando para Windows...$(NC)"
|
| 60 |
+
@mkdir -p $(BUILD_DIR)
|
| 61 |
+
GOOS=windows GOARCH=amd64 $(GO) build $(GOFLAGS) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-windows-amd64.exe $(MAIN_PATH)
|
| 62 |
+
@echo "$(GREEN)✅ Build Windows completo$(NC)"
|
| 63 |
+
|
| 64 |
+
build-all: build build-linux build-windows ## Compila para todas as plataformas
|
| 65 |
+
@echo "$(GREEN)✅ Todos os builds completos$(NC)"
|
| 66 |
+
|
| 67 |
+
install: build ## Compila e instala no GOPATH
|
| 68 |
+
@echo "$(GREEN)📦 Instalando $(APP_NAME)...$(NC)"
|
| 69 |
+
$(GO) install $(MAIN_PATH)
|
| 70 |
+
@echo "$(GREEN)✅ Instalado com sucesso$(NC)"
|
| 71 |
+
|
| 72 |
+
##@ Testes
|
| 73 |
+
|
| 74 |
+
test: ## Roda todos os testes
|
| 75 |
+
@echo "$(GREEN)🧪 Rodando testes...$(NC)"
|
| 76 |
+
$(GO) test -v ./...
|
| 77 |
+
|
| 78 |
+
test-coverage: ## Roda testes com cobertura
|
| 79 |
+
@echo "$(GREEN)🧪 Rodando testes com cobertura...$(NC)"
|
| 80 |
+
$(GO) test -v -coverprofile=coverage.out ./...
|
| 81 |
+
$(GO) tool cover -html=coverage.out -o coverage.html
|
| 82 |
+
@echo "$(GREEN)✅ Cobertura gerada: coverage.html$(NC)"
|
| 83 |
+
|
| 84 |
+
test-race: ## Roda testes verificando race conditions
|
| 85 |
+
@echo "$(GREEN)🧪 Rodando testes com race detector...$(NC)"
|
| 86 |
+
$(GO) test -race -v ./...
|
| 87 |
+
|
| 88 |
+
bench: ## Roda benchmarks
|
| 89 |
+
@echo "$(GREEN)⚡ Rodando benchmarks...$(NC)"
|
| 90 |
+
$(GO) test -bench=. -benchmem ./...
|
| 91 |
+
|
| 92 |
+
##@ Dependências
|
| 93 |
+
|
| 94 |
+
deps: ## Instala dependências
|
| 95 |
+
@echo "$(GREEN)📦 Instalando dependências...$(NC)"
|
| 96 |
+
$(GO) mod download
|
| 97 |
+
$(GO) mod verify
|
| 98 |
+
@echo "$(GREEN)✅ Dependências instaladas$(NC)"
|
| 99 |
+
|
| 100 |
+
deps-update: ## Atualiza dependências
|
| 101 |
+
@echo "$(GREEN)📦 Atualizando dependências...$(NC)"
|
| 102 |
+
$(GO) get -u ./...
|
| 103 |
+
$(GO) mod tidy
|
| 104 |
+
@echo "$(GREEN)✅ Dependências atualizadas$(NC)"
|
| 105 |
+
|
| 106 |
+
deps-clean: ## Limpa dependências não utilizadas
|
| 107 |
+
@echo "$(GREEN)🧹 Limpando dependências...$(NC)"
|
| 108 |
+
$(GO) mod tidy
|
| 109 |
+
@echo "$(GREEN)✅ Dependências limpas$(NC)"
|
| 110 |
+
|
| 111 |
+
deps-reset: ## Limpa cache e reinstala dependências (força uso do código local)
|
| 112 |
+
@echo "$(GREEN)🔄 Resetando dependências e cache...$(NC)"
|
| 113 |
+
@echo "$(YELLOW)Limpeza de cache e módulos...$(NC)"
|
| 114 |
+
$(GO) clean -cache -modcache -i -r
|
| 115 |
+
@echo "$(YELLOW)Download de módulos...$(NC)"
|
| 116 |
+
$(GO) mod download
|
| 117 |
+
@echo "$(YELLOW)Organizando módulos...$(NC)"
|
| 118 |
+
$(GO) mod tidy
|
| 119 |
+
@echo "$(GREEN)✅ Dependências resetadas e atualizadas$(NC)"
|
| 120 |
+
|
| 121 |
+
##@ Documentação
|
| 122 |
+
|
| 123 |
+
swagger: ## Gera documentação Swagger
|
| 124 |
+
@echo "$(GREEN)📚 Gerando documentação Swagger...$(NC)"
|
| 125 |
+
@if command -v swag > /dev/null; then \
|
| 126 |
+
swag init -g $(MAIN_PATH) -o ./docs; \
|
| 127 |
+
echo "$(GREEN)✅ Swagger gerado com sucesso$(NC)"; \
|
| 128 |
+
else \
|
| 129 |
+
echo "$(RED)❌ Swag não instalado. Instale com: go install github.com/swaggo/swag/cmd/swag@latest$(NC)"; \
|
| 130 |
+
exit 1; \
|
| 131 |
+
fi
|
| 132 |
+
|
| 133 |
+
docs: ## Abre a documentação local
|
| 134 |
+
@echo "$(GREEN)📖 Abrindo documentação...$(NC)"
|
| 135 |
+
@if [ -f "docs/wiki/README.md" ]; then \
|
| 136 |
+
echo "Documentação disponível em: docs/wiki/README.md"; \
|
| 137 |
+
else \
|
| 138 |
+
echo "$(RED)❌ Documentação não encontrada$(NC)"; \
|
| 139 |
+
fi
|
| 140 |
+
|
| 141 |
+
##@ Database
|
| 142 |
+
|
| 143 |
+
migrate-up: ## Executa migrations do banco de dados
|
| 144 |
+
@echo "$(GREEN)🗃️ Executando migrations...$(NC)"
|
| 145 |
+
@if [ -d "migrations" ]; then \
|
| 146 |
+
$(GO) run $(MAIN_PATH) migrate up; \
|
| 147 |
+
else \
|
| 148 |
+
echo "$(YELLOW)⚠️ Diretório migrations não encontrado$(NC)"; \
|
| 149 |
+
fi
|
| 150 |
+
|
| 151 |
+
migrate-down: ## Reverte migrations do banco de dados
|
| 152 |
+
@echo "$(YELLOW)⚠️ Revertendo migrations...$(NC)"
|
| 153 |
+
@if [ -d "migrations" ]; then \
|
| 154 |
+
$(GO) run $(MAIN_PATH) migrate down; \
|
| 155 |
+
else \
|
| 156 |
+
echo "$(YELLOW)⚠️ Diretório migrations não encontrado$(NC)"; \
|
| 157 |
+
fi
|
| 158 |
+
|
| 159 |
+
##@ Docker
|
| 160 |
+
|
| 161 |
+
docker-build: ## Build da imagem Docker
|
| 162 |
+
@echo "$(GREEN)🐳 Construindo imagem Docker...$(NC)"
|
| 163 |
+
docker build --build-arg VERSION=$(VERSION) -t $(APP_NAME):latest .
|
| 164 |
+
@echo "$(GREEN)✅ Imagem Docker construída$(NC)"
|
| 165 |
+
|
| 166 |
+
docker-run: ## Roda container Docker
|
| 167 |
+
@echo "$(GREEN)🐳 Iniciando container...$(NC)"
|
| 168 |
+
docker run -p 4000:4000 --env-file .env $(APP_NAME):latest
|
| 169 |
+
|
| 170 |
+
docker-compose-up: ## Sobe todos os serviços com docker-compose
|
| 171 |
+
@echo "$(GREEN)🐳 Iniciando serviços com docker-compose...$(NC)"
|
| 172 |
+
docker-compose up -d
|
| 173 |
+
|
| 174 |
+
docker-compose-down: ## Para todos os serviços do docker-compose
|
| 175 |
+
@echo "$(YELLOW)🐳 Parando serviços...$(NC)"
|
| 176 |
+
docker-compose down
|
| 177 |
+
|
| 178 |
+
docker-compose-logs: ## Exibe logs do docker-compose
|
| 179 |
+
docker-compose logs -f
|
| 180 |
+
|
| 181 |
+
##@ Linting e Formatação
|
| 182 |
+
|
| 183 |
+
fmt: ## Formata o código
|
| 184 |
+
@echo "$(GREEN)✨ Formatando código...$(NC)"
|
| 185 |
+
$(GO) fmt ./...
|
| 186 |
+
@echo "$(GREEN)✅ Código formatado$(NC)"
|
| 187 |
+
|
| 188 |
+
lint: ## Executa linter (requer golangci-lint)
|
| 189 |
+
@echo "$(GREEN)🔍 Executando linter...$(NC)"
|
| 190 |
+
@if command -v golangci-lint > /dev/null; then \
|
| 191 |
+
golangci-lint run ./...; \
|
| 192 |
+
echo "$(GREEN)✅ Lint completo$(NC)"; \
|
| 193 |
+
else \
|
| 194 |
+
echo "$(RED)❌ golangci-lint não instalado. Instale com: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest$(NC)"; \
|
| 195 |
+
exit 1; \
|
| 196 |
+
fi
|
| 197 |
+
|
| 198 |
+
vet: ## Executa go vet
|
| 199 |
+
@echo "$(GREEN)🔍 Executando go vet...$(NC)"
|
| 200 |
+
$(GO) vet ./...
|
| 201 |
+
@echo "$(GREEN)✅ Vet completo$(NC)"
|
| 202 |
+
|
| 203 |
+
check: fmt vet lint test ## Executa todas as verificações
|
| 204 |
+
|
| 205 |
+
##@ Limpeza
|
| 206 |
+
|
| 207 |
+
clean: ## Remove arquivos de build
|
| 208 |
+
@echo "$(YELLOW)🧹 Limpando arquivos de build...$(NC)"
|
| 209 |
+
@rm -rf $(BUILD_DIR)
|
| 210 |
+
@rm -f coverage.out coverage.html
|
| 211 |
+
@echo "$(GREEN)✅ Limpeza completa$(NC)"
|
| 212 |
+
|
| 213 |
+
clean-all: clean ## Remove arquivos de build e cache
|
| 214 |
+
@echo "$(YELLOW)🧹 Limpeza completa (incluindo cache)...$(NC)"
|
| 215 |
+
$(GO) clean -cache -testcache -modcache
|
| 216 |
+
@echo "$(GREEN)✅ Limpeza completa$(NC)"
|
| 217 |
+
|
| 218 |
+
##@ Utilitários
|
| 219 |
+
|
| 220 |
+
setup: deps swagger ## Setup completo do ambiente de desenvolvimento
|
| 221 |
+
@echo "$(GREEN)🎉 Setup completo!$(NC)"
|
| 222 |
+
@echo ""
|
| 223 |
+
@echo "Para começar a desenvolver, rode:"
|
| 224 |
+
@echo " $(YELLOW)make dev$(NC)"
|
| 225 |
+
@echo ""
|
| 226 |
+
@echo "Outros comandos úteis:"
|
| 227 |
+
@echo " $(YELLOW)make help$(NC) - Ver todos os comandos"
|
| 228 |
+
@echo " $(YELLOW)make test$(NC) - Rodar testes"
|
| 229 |
+
@echo " $(YELLOW)make build$(NC) - Compilar a aplicação"
|
| 230 |
+
|
| 231 |
+
logs: ## Exibe logs da aplicação (se estiver rodando)
|
| 232 |
+
@echo "$(GREEN)📋 Exibindo logs...$(NC)"
|
| 233 |
+
@if [ -f "logs/app.log" ]; then \
|
| 234 |
+
tail -f logs/app.log; \
|
| 235 |
+
else \
|
| 236 |
+
echo "$(YELLOW)⚠️ Arquivo de log não encontrado$(NC)"; \
|
| 237 |
+
fi
|
| 238 |
+
|
| 239 |
+
version: ## Exibe versão do Go e dependências
|
| 240 |
+
@echo "$(GREEN)📌 Versões:$(NC)"
|
| 241 |
+
@$(GO) version
|
| 242 |
+
@echo ""
|
| 243 |
+
@echo "$(GREEN)Dependências principais:$(NC)"
|
| 244 |
+
@$(GO) list -m all | grep -E '(whatsmeow|postgres|minio)'
|
| 245 |
+
|
| 246 |
+
status: ## Verifica status da aplicação
|
| 247 |
+
@echo "$(GREEN)🔍 Verificando status...$(NC)"
|
| 248 |
+
@curl -s http://localhost:4000/health || echo "$(RED)❌ Aplicação não está rodando$(NC)"
|
| 249 |
+
|
| 250 |
+
##@ Desenvolvimento Avançado
|
| 251 |
+
|
| 252 |
+
profile-cpu: ## Profile de CPU (requer aplicação rodando)
|
| 253 |
+
@echo "$(GREEN)📊 Capturando profile de CPU...$(NC)"
|
| 254 |
+
curl http://localhost:4000/debug/pprof/profile?seconds=30 > cpu.prof
|
| 255 |
+
$(GO) tool pprof -http=:8080 cpu.prof
|
| 256 |
+
|
| 257 |
+
profile-mem: ## Profile de memória (requer aplicação rodando)
|
| 258 |
+
@echo "$(GREEN)📊 Capturando profile de memória...$(NC)"
|
| 259 |
+
curl http://localhost:4000/debug/pprof/heap > mem.prof
|
| 260 |
+
$(GO) tool pprof -http=:8080 mem.prof
|
| 261 |
+
|
| 262 |
+
generate: ## Roda go generate
|
| 263 |
+
@echo "$(GREEN)⚙️ Executando go generate...$(NC)"
|
| 264 |
+
$(GO) generate ./...
|
| 265 |
+
|
| 266 |
+
mod-graph: ## Exibe gráfico de dependências
|
| 267 |
+
@echo "$(GREEN)📊 Gráfico de dependências:$(NC)"
|
| 268 |
+
$(GO) mod graph
|
whatsapp-service/NOTICE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
AgentDeck Whatsapp Service
|
| 2 |
+
Copyright 2026 AgentDeck
|
| 3 |
+
|
| 4 |
+
Third-party attributions:
|
| 5 |
+
|
| 6 |
+
- whatsmeow (https://github.com/tulir/whatsmeow) by Tulir Asokan, used as the
|
| 7 |
+
WhatsApp protocol library.
|
whatsapp-service/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
0.7.2
|
whatsapp-service/cmd/agentdeck-whatsapp-service/main.go
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"database/sql"
|
| 6 |
+
"flag"
|
| 7 |
+
"fmt"
|
| 8 |
+
"log"
|
| 9 |
+
"net/http"
|
| 10 |
+
"os"
|
| 11 |
+
"os/signal"
|
| 12 |
+
"strings"
|
| 13 |
+
"syscall"
|
| 14 |
+
"time"
|
| 15 |
+
|
| 16 |
+
"github.com/gin-gonic/gin"
|
| 17 |
+
"github.com/gomessguii/logger"
|
| 18 |
+
"github.com/joho/godotenv"
|
| 19 |
+
"github.com/redis/go-redis/v9"
|
| 20 |
+
"go.mau.fi/whatsmeow"
|
| 21 |
+
|
| 22 |
+
call_handler "agentdeck-whatsapp-service/pkg/call/handler"
|
| 23 |
+
call_service "agentdeck-whatsapp-service/pkg/call/service"
|
| 24 |
+
chat_handler "agentdeck-whatsapp-service/pkg/chat/handler"
|
| 25 |
+
chat_service "agentdeck-whatsapp-service/pkg/chat/service"
|
| 26 |
+
community_handler "agentdeck-whatsapp-service/pkg/community/handler"
|
| 27 |
+
community_service "agentdeck-whatsapp-service/pkg/community/service"
|
| 28 |
+
config "agentdeck-whatsapp-service/pkg/config"
|
| 29 |
+
"agentdeck-whatsapp-service/pkg/core"
|
| 30 |
+
producer_interfaces "agentdeck-whatsapp-service/pkg/events/interfaces"
|
| 31 |
+
nats_producer "agentdeck-whatsapp-service/pkg/events/nats"
|
| 32 |
+
rabbitmq_producer "agentdeck-whatsapp-service/pkg/events/rabbitmq"
|
| 33 |
+
webhook_producer "agentdeck-whatsapp-service/pkg/events/webhook"
|
| 34 |
+
websocket_producer "agentdeck-whatsapp-service/pkg/events/websocket"
|
| 35 |
+
group_handler "agentdeck-whatsapp-service/pkg/group/handler"
|
| 36 |
+
group_service "agentdeck-whatsapp-service/pkg/group/service"
|
| 37 |
+
instance_handler "agentdeck-whatsapp-service/pkg/instance/handler"
|
| 38 |
+
instance_repository "agentdeck-whatsapp-service/pkg/instance/repository"
|
| 39 |
+
instance_service "agentdeck-whatsapp-service/pkg/instance/service"
|
| 40 |
+
label_handler "agentdeck-whatsapp-service/pkg/label/handler"
|
| 41 |
+
label_repository "agentdeck-whatsapp-service/pkg/label/repository"
|
| 42 |
+
label_service "agentdeck-whatsapp-service/pkg/label/service"
|
| 43 |
+
logger_wrapper "agentdeck-whatsapp-service/pkg/logger"
|
| 44 |
+
message_handler "agentdeck-whatsapp-service/pkg/message/handler"
|
| 45 |
+
message_repository "agentdeck-whatsapp-service/pkg/message/repository"
|
| 46 |
+
message_service "agentdeck-whatsapp-service/pkg/message/service"
|
| 47 |
+
auth_middleware "agentdeck-whatsapp-service/pkg/middleware"
|
| 48 |
+
newsletter_handler "agentdeck-whatsapp-service/pkg/newsletter/handler"
|
| 49 |
+
newsletter_service "agentdeck-whatsapp-service/pkg/newsletter/service"
|
| 50 |
+
passkey_handler "agentdeck-whatsapp-service/pkg/passkey/handler"
|
| 51 |
+
poll_handler "agentdeck-whatsapp-service/pkg/poll/handler"
|
| 52 |
+
routes "agentdeck-whatsapp-service/pkg/routes"
|
| 53 |
+
send_handler "agentdeck-whatsapp-service/pkg/sendMessage/handler"
|
| 54 |
+
send_service "agentdeck-whatsapp-service/pkg/sendMessage/service"
|
| 55 |
+
server_handler "agentdeck-whatsapp-service/pkg/server/handler"
|
| 56 |
+
storage_interfaces "agentdeck-whatsapp-service/pkg/storage/interfaces"
|
| 57 |
+
minio_storage "agentdeck-whatsapp-service/pkg/storage/minio"
|
| 58 |
+
"agentdeck-whatsapp-service/pkg/supabase"
|
| 59 |
+
user_handler "agentdeck-whatsapp-service/pkg/user/handler"
|
| 60 |
+
user_service "agentdeck-whatsapp-service/pkg/user/service"
|
| 61 |
+
whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service"
|
| 62 |
+
amqp "github.com/rabbitmq/amqp091-go"
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
var devMode = flag.Bool("dev", false, "Enable development mode")
|
| 66 |
+
|
| 67 |
+
var version = "0.0.0"
|
| 68 |
+
|
| 69 |
+
func init() {
|
| 70 |
+
// ldflags -X main.version= sets this at compile time.
|
| 71 |
+
// If not set (or still default), try reading from VERSION file.
|
| 72 |
+
if version == "0.0.0" {
|
| 73 |
+
if v, err := os.ReadFile("VERSION"); err == nil {
|
| 74 |
+
if trimmed := strings.TrimSpace(string(v)); trimmed != "" {
|
| 75 |
+
version = trimmed
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
func setupRouter(supa *supabase.Client, authDB *sql.DB, redisClient *redis.Client, config *config.Config, conn *amqp.Connection, runtimeCtx *core.RuntimeContext) *gin.Engine {
|
| 82 |
+
killChannel := make(map[string](chan bool))
|
| 83 |
+
clientPointer := make(map[string]*whatsmeow.Client)
|
| 84 |
+
|
| 85 |
+
loggerWrapper := logger_wrapper.NewLoggerManager(config)
|
| 86 |
+
|
| 87 |
+
var rabbitmqProducer producer_interfaces.Producer
|
| 88 |
+
if conn != nil {
|
| 89 |
+
logger.LogInfo("RabbitMQ enabled")
|
| 90 |
+
rabbitmqProducer = rabbitmq_producer.NewRabbitMQProducer(
|
| 91 |
+
conn,
|
| 92 |
+
config.AmqpGlobalEnabled,
|
| 93 |
+
config.AmqpGlobalEvents,
|
| 94 |
+
config.AmqpSpecificEvents,
|
| 95 |
+
config.AmqpUrl,
|
| 96 |
+
loggerWrapper,
|
| 97 |
+
)
|
| 98 |
+
} else {
|
| 99 |
+
// Even if initial connection failed, pass the URL so reconnection can work
|
| 100 |
+
rabbitmqProducer = rabbitmq_producer.NewRabbitMQProducer(
|
| 101 |
+
nil,
|
| 102 |
+
config.AmqpGlobalEnabled,
|
| 103 |
+
config.AmqpGlobalEvents,
|
| 104 |
+
config.AmqpSpecificEvents,
|
| 105 |
+
config.AmqpUrl, // Keep the URL for reconnection attempts
|
| 106 |
+
loggerWrapper,
|
| 107 |
+
)
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
var natsProducer producer_interfaces.Producer
|
| 111 |
+
if config.NatsUrl != "" {
|
| 112 |
+
logger.LogInfo("NATS enabled")
|
| 113 |
+
natsProducer = nats_producer.NewNatsProducer(
|
| 114 |
+
config.NatsUrl,
|
| 115 |
+
config.NatsGlobalEnabled,
|
| 116 |
+
config.NatsGlobalEvents,
|
| 117 |
+
loggerWrapper,
|
| 118 |
+
)
|
| 119 |
+
} else {
|
| 120 |
+
natsProducer = nats_producer.NewNatsProducer(
|
| 121 |
+
"",
|
| 122 |
+
false,
|
| 123 |
+
nil,
|
| 124 |
+
loggerWrapper,
|
| 125 |
+
)
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
webhookProducer := webhook_producer.NewWebhookProducer(config.WebhookUrl, loggerWrapper)
|
| 129 |
+
websocketProducer := websocket_producer.NewWebsocketProducer(loggerWrapper)
|
| 130 |
+
|
| 131 |
+
// Cria filas globais se o RabbitMQ global estiver habilitado
|
| 132 |
+
if config.AmqpGlobalEnabled && conn != nil {
|
| 133 |
+
logger.LogInfo("Creating global RabbitMQ queues...")
|
| 134 |
+
if err := rabbitmqProducer.CreateGlobalQueues(); err != nil {
|
| 135 |
+
logger.LogError("Failed to create global RabbitMQ queues: %v", err)
|
| 136 |
+
} else {
|
| 137 |
+
logger.LogInfo("Global RabbitMQ queues created successfully")
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
var mediaStorage storage_interfaces.MediaStorage
|
| 142 |
+
var err error
|
| 143 |
+
if config.MinioEnabled {
|
| 144 |
+
mediaStorage, err = minio_storage.NewMinioMediaStorage(
|
| 145 |
+
config.MinioEndpoint,
|
| 146 |
+
config.MinioAccessKey,
|
| 147 |
+
config.MinioSecretKey,
|
| 148 |
+
config.MinioBucket,
|
| 149 |
+
config.MinioRegion,
|
| 150 |
+
config.MinioUseSSL,
|
| 151 |
+
)
|
| 152 |
+
if err != nil {
|
| 153 |
+
log.Fatal(err)
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
instanceRepository := instance_repository.NewInstanceRepository(supa)
|
| 158 |
+
messageRepository := message_repository.NewMessageRepository(supa)
|
| 159 |
+
labelRepository := label_repository.NewLabelRepository(supa)
|
| 160 |
+
|
| 161 |
+
whatsmeowService := whatsmeow_service.NewWhatsmeowService(
|
| 162 |
+
instanceRepository,
|
| 163 |
+
authDB,
|
| 164 |
+
supa,
|
| 165 |
+
messageRepository,
|
| 166 |
+
labelRepository,
|
| 167 |
+
config,
|
| 168 |
+
killChannel,
|
| 169 |
+
clientPointer,
|
| 170 |
+
rabbitmqProducer,
|
| 171 |
+
webhookProducer,
|
| 172 |
+
websocketProducer,
|
| 173 |
+
redisClient,
|
| 174 |
+
mediaStorage,
|
| 175 |
+
natsProducer,
|
| 176 |
+
loggerWrapper,
|
| 177 |
+
)
|
| 178 |
+
instanceService := instance_service.NewInstanceService(
|
| 179 |
+
instanceRepository,
|
| 180 |
+
killChannel,
|
| 181 |
+
clientPointer,
|
| 182 |
+
whatsmeowService,
|
| 183 |
+
config,
|
| 184 |
+
loggerWrapper,
|
| 185 |
+
)
|
| 186 |
+
sendMessageService := send_service.NewSendService(clientPointer, whatsmeowService, config, loggerWrapper)
|
| 187 |
+
userService := user_service.NewUserService(clientPointer, whatsmeowService, loggerWrapper)
|
| 188 |
+
messageService := message_service.NewMessageService(clientPointer, messageRepository, whatsmeowService, loggerWrapper)
|
| 189 |
+
chatService := chat_service.NewChatService(clientPointer, whatsmeowService, loggerWrapper)
|
| 190 |
+
groupService := group_service.NewGroupService(clientPointer, whatsmeowService, loggerWrapper)
|
| 191 |
+
callService := call_service.NewCallService(clientPointer, whatsmeowService, loggerWrapper)
|
| 192 |
+
communityService := community_service.NewCommunityService(clientPointer, whatsmeowService, loggerWrapper)
|
| 193 |
+
labelService := label_service.NewLabelService(clientPointer, whatsmeowService, labelRepository, loggerWrapper)
|
| 194 |
+
newsletterService := newsletter_service.NewNewsletterService(clientPointer, whatsmeowService, loggerWrapper)
|
| 195 |
+
|
| 196 |
+
// NOVO: PollHandler usando PollService já inicializado no whatsmeowService (evita dupla inicialização)
|
| 197 |
+
pollHandler := poll_handler.NewPollHandler(whatsmeowService.GetPollService(), loggerWrapper)
|
| 198 |
+
|
| 199 |
+
r := gin.Default()
|
| 200 |
+
|
| 201 |
+
// CORS middleware — must be before everything else
|
| 202 |
+
r.Use(func(c *gin.Context) {
|
| 203 |
+
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
| 204 |
+
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
| 205 |
+
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
| 206 |
+
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Accept, Cache-Control, X-Requested-With, apikey, ApiKey")
|
| 207 |
+
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length")
|
| 208 |
+
if c.Request.Method == "OPTIONS" {
|
| 209 |
+
c.AbortWithStatus(200)
|
| 210 |
+
return
|
| 211 |
+
}
|
| 212 |
+
c.Next()
|
| 213 |
+
})
|
| 214 |
+
|
| 215 |
+
r.Use(core.GateMiddleware(runtimeCtx))
|
| 216 |
+
|
| 217 |
+
// License routes (always accessible, even without license)
|
| 218 |
+
core.LicenseRoutes(r, runtimeCtx)
|
| 219 |
+
|
| 220 |
+
// Passkey ceremony routes — PUBLIC (called by the browser extension from the
|
| 221 |
+
// web.whatsapp.com origin, gated only by an opaque ephemeral token).
|
| 222 |
+
passkey_handler.RegisterRoutes(r, whatsmeowService)
|
| 223 |
+
|
| 224 |
+
routes.NewRouter(
|
| 225 |
+
auth_middleware.NewMiddleware(config, instanceService),
|
| 226 |
+
instance_handler.NewInstanceHandler(instanceService, config),
|
| 227 |
+
user_handler.NewUserHandler(userService),
|
| 228 |
+
send_handler.NewSendHandler(sendMessageService),
|
| 229 |
+
message_handler.NewMessageHandler(messageService),
|
| 230 |
+
chat_handler.NewChatHandler(chatService),
|
| 231 |
+
group_handler.NewGroupHandler(groupService),
|
| 232 |
+
call_handler.NewCallHandler(callService),
|
| 233 |
+
community_handler.NewCommunityHandler(communityService),
|
| 234 |
+
label_handler.NewLabelHandler(labelService),
|
| 235 |
+
newsletter_handler.NewNewsletterHandler(newsletterService),
|
| 236 |
+
pollHandler,
|
| 237 |
+
server_handler.NewServerHandler(),
|
| 238 |
+
).AssignRoutes(r)
|
| 239 |
+
|
| 240 |
+
if config.ConnectOnStartup {
|
| 241 |
+
go whatsmeowService.ConnectOnStartup(config.ClientName)
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
r.GET("/ws", func(c *gin.Context) {
|
| 245 |
+
token := c.Query("token")
|
| 246 |
+
instanceId := c.Query("instanceId")
|
| 247 |
+
|
| 248 |
+
if token != config.GlobalApiKey {
|
| 249 |
+
logger.LogError("Token inválido: %s", token)
|
| 250 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token inválido"})
|
| 251 |
+
return
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
websocket_producer.ServeWs(c.Writer, c.Request, instanceId, websocketProducer)
|
| 255 |
+
})
|
| 256 |
+
|
| 257 |
+
return r
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
// initRedis connects to Redis using the full URL (REDIS_URL). It is used for
|
| 261 |
+
// the userInfoCache and processedMessages deduplication cache.
|
| 262 |
+
func initRedis(redisURL string) (*redis.Client, error) {
|
| 263 |
+
if redisURL == "" {
|
| 264 |
+
return nil, fmt.Errorf("REDIS_URL is required for caching")
|
| 265 |
+
}
|
| 266 |
+
opts, err := redis.ParseURL(redisURL)
|
| 267 |
+
if err != nil {
|
| 268 |
+
return nil, fmt.Errorf("invalid REDIS_URL: %v", err)
|
| 269 |
+
}
|
| 270 |
+
client := redis.NewClient(opts)
|
| 271 |
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
| 272 |
+
defer cancel()
|
| 273 |
+
if err := client.Ping(ctx).Err(); err != nil {
|
| 274 |
+
return nil, fmt.Errorf("failed to ping Redis: %v", err)
|
| 275 |
+
}
|
| 276 |
+
logger.LogInfo("Connected to Redis (REDIS_URL)")
|
| 277 |
+
return client, nil
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
// @title AgentDeck Whatsapp Service
|
| 281 |
+
// @version 1.0
|
| 282 |
+
// @description AgentDeck Whatsapp Service - whatsmeow
|
| 283 |
+
func main() {
|
| 284 |
+
flag.Parse()
|
| 285 |
+
// Configuration is injected through the process environment (the main
|
| 286 |
+
// AgentDeck backend is the single source of truth and forwards the
|
| 287 |
+
// WhatsApp settings to this service). A local `.env` is only loaded when
|
| 288 |
+
// `--dev` is passed AND the file exists — it is never required.
|
| 289 |
+
if *devMode {
|
| 290 |
+
if _, err := os.Stat(".env"); err == nil {
|
| 291 |
+
if err := godotenv.Load(".env"); err != nil {
|
| 292 |
+
log.Fatalf("failed to load .env: %v", err)
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
cfg := config.Load()
|
| 298 |
+
|
| 299 |
+
logger.LogInfo("Starting AgentDeck Whatsapp Service version %s", version)
|
| 300 |
+
|
| 301 |
+
startTime := time.Now()
|
| 302 |
+
|
| 303 |
+
// Supabase PostgREST client — drives all app repositories (instances,
|
| 304 |
+
// messages, labels, polls, runtime configs).
|
| 305 |
+
supa := supabase.New(cfg.SupabaseURL, cfg.SupabaseServiceKey)
|
| 306 |
+
|
| 307 |
+
// Native Supabase Postgres — used only by the whatsmeow session store and
|
| 308 |
+
// the whatsmeow_device lookup (these require a Postgres driver).
|
| 309 |
+
authDB, err := cfg.CreateSupabaseDB()
|
| 310 |
+
if err != nil {
|
| 311 |
+
logger.LogFatal("[STARTUP] %v", err)
|
| 312 |
+
}
|
| 313 |
+
defer authDB.Close()
|
| 314 |
+
|
| 315 |
+
// Redis — userInfoCache and processedMessages deduplication.
|
| 316 |
+
redisClient, err := initRedis(cfg.RedisURL)
|
| 317 |
+
if err != nil {
|
| 318 |
+
logger.LogFatal("[STARTUP] %v", err)
|
| 319 |
+
}
|
| 320 |
+
defer redisClient.Close()
|
| 321 |
+
|
| 322 |
+
// Initialize core DB + license runtime (runtime_configs via PostgREST)
|
| 323 |
+
core.SetDB(supa)
|
| 324 |
+
if err := core.MigrateDB(); err != nil {
|
| 325 |
+
log.Fatal("Failed to migrate runtime_configs: ", err)
|
| 326 |
+
}
|
| 327 |
+
tier := "agentdeck-whatsapp"
|
| 328 |
+
runtimeCtx := core.InitializeRuntime(tier, version, cfg.GlobalApiKey)
|
| 329 |
+
|
| 330 |
+
var conn *amqp.Connection
|
| 331 |
+
|
| 332 |
+
if cfg.AmqpUrl != "" {
|
| 333 |
+
logger.LogInfo("Attempting to connect to RabbitMQ...")
|
| 334 |
+
|
| 335 |
+
// Create connection with heartbeat to prevent timeouts
|
| 336 |
+
amqpConfig := amqp.Config{
|
| 337 |
+
Heartbeat: 30 * time.Second, // Send heartbeat every 30 seconds
|
| 338 |
+
Locale: "en_US",
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
conn, err = amqp.DialConfig(cfg.AmqpUrl, amqpConfig)
|
| 342 |
+
if err != nil {
|
| 343 |
+
logger.LogError("Failed to connect to RabbitMQ, err: %v", err)
|
| 344 |
+
logger.LogInfo("RabbitMQ producer will be created with reconnection capability")
|
| 345 |
+
} else {
|
| 346 |
+
logger.LogInfo("Successfully connected to RabbitMQ with heartbeat enabled")
|
| 347 |
+
defer func(conn *amqp.Connection) {
|
| 348 |
+
err := conn.Close()
|
| 349 |
+
if err != nil {
|
| 350 |
+
logger.LogError("Failed to close RabbitMQ connection, err: %v", err)
|
| 351 |
+
}
|
| 352 |
+
}(conn)
|
| 353 |
+
}
|
| 354 |
+
} else {
|
| 355 |
+
logger.LogInfo("RabbitMQ URL not configured, skipping RabbitMQ connection")
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
r := setupRouter(supa, authDB, redisClient, cfg, conn, runtimeCtx)
|
| 359 |
+
|
| 360 |
+
// Graceful shutdown with heartbeat
|
| 361 |
+
heartbeatCtx, heartbeatCancel := context.WithCancel(context.Background())
|
| 362 |
+
defer heartbeatCancel()
|
| 363 |
+
|
| 364 |
+
core.StartHeartbeat(heartbeatCtx, runtimeCtx, startTime)
|
| 365 |
+
|
| 366 |
+
srv := &http.Server{
|
| 367 |
+
Addr: ":" + os.Getenv("SERVER_PORT"),
|
| 368 |
+
Handler: r,
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
quit := make(chan os.Signal, 1)
|
| 372 |
+
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
| 373 |
+
|
| 374 |
+
go func() {
|
| 375 |
+
logger.LogInfo("Iniciando servidor na porta %s", os.Getenv("SERVER_PORT"))
|
| 376 |
+
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
| 377 |
+
log.Fatalf("server error: %v", err)
|
| 378 |
+
}
|
| 379 |
+
}()
|
| 380 |
+
|
| 381 |
+
<-quit
|
| 382 |
+
logger.LogInfo("[SHUTDOWN] Signal received, shutting down...")
|
| 383 |
+
|
| 384 |
+
// Stop heartbeat loop
|
| 385 |
+
heartbeatCancel()
|
| 386 |
+
|
| 387 |
+
core.Shutdown(runtimeCtx)
|
| 388 |
+
|
| 389 |
+
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
| 390 |
+
defer shutdownCancel()
|
| 391 |
+
|
| 392 |
+
if err := srv.Shutdown(shutdownCtx); err != nil {
|
| 393 |
+
logger.LogError("[SHUTDOWN] Server forced to shutdown: %v", err)
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
logger.LogInfo("[SHUTDOWN] Server exited")
|
| 397 |
+
}
|
whatsapp-service/docs/docs.go
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
whatsapp-service/docs/swagger.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
whatsapp-service/docs/swagger.yaml
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
whatsapp-service/go.mod
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module agentdeck-whatsapp-service
|
| 2 |
+
|
| 3 |
+
go 1.25.0
|
| 4 |
+
|
| 5 |
+
require (
|
| 6 |
+
github.com/chai2010/webp v1.1.1
|
| 7 |
+
github.com/gabriel-vasile/mimetype v1.4.5
|
| 8 |
+
github.com/gin-gonic/gin v1.10.0
|
| 9 |
+
github.com/gomessguii/logger v0.0.3
|
| 10 |
+
github.com/google/uuid v1.6.0
|
| 11 |
+
github.com/gorilla/websocket v1.5.3
|
| 12 |
+
github.com/joho/godotenv v1.5.1
|
| 13 |
+
github.com/lib/pq v1.10.9
|
| 14 |
+
github.com/minio/minio-go/v7 v7.0.80
|
| 15 |
+
github.com/nats-io/nats.go v1.39.0
|
| 16 |
+
github.com/rabbitmq/amqp091-go v1.10.0
|
| 17 |
+
github.com/redis/go-redis/v9 v9.22.0
|
| 18 |
+
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
| 19 |
+
github.com/swaggo/files v1.0.1
|
| 20 |
+
github.com/swaggo/gin-swagger v1.6.0
|
| 21 |
+
github.com/swaggo/swag v1.16.3
|
| 22 |
+
github.com/vincent-petithory/dataurl v1.0.0
|
| 23 |
+
go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b
|
| 24 |
+
golang.org/x/exp v0.0.0-20260611194520-c48552f49976
|
| 25 |
+
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
|
| 26 |
+
golang.org/x/net v0.56.0
|
| 27 |
+
google.golang.org/protobuf v1.36.11
|
| 28 |
+
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
| 29 |
+
gorm.io/gorm v1.25.10
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
require (
|
| 33 |
+
filippo.io/edwards25519 v1.2.0 // indirect
|
| 34 |
+
github.com/KyleBanks/depth v1.2.1 // indirect
|
| 35 |
+
github.com/beeper/argo-go v1.1.2 // indirect
|
| 36 |
+
github.com/bytedance/sonic v1.12.2 // indirect
|
| 37 |
+
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
| 38 |
+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
| 39 |
+
github.com/cloudwego/base64x v0.1.4 // indirect
|
| 40 |
+
github.com/cloudwego/iasm v0.2.0 // indirect
|
| 41 |
+
github.com/coder/websocket v1.8.15 // indirect
|
| 42 |
+
github.com/dustin/go-humanize v1.0.1 // indirect
|
| 43 |
+
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
|
| 44 |
+
github.com/gin-contrib/sse v0.1.0 // indirect
|
| 45 |
+
github.com/go-ini/ini v1.67.0 // indirect
|
| 46 |
+
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
| 47 |
+
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
| 48 |
+
github.com/go-openapi/spec v0.21.0 // indirect
|
| 49 |
+
github.com/go-openapi/swag v0.23.0 // indirect
|
| 50 |
+
github.com/go-playground/locales v0.14.1 // indirect
|
| 51 |
+
github.com/go-playground/universal-translator v0.18.1 // indirect
|
| 52 |
+
github.com/go-playground/validator/v10 v10.22.0 // indirect
|
| 53 |
+
github.com/goccy/go-json v0.10.3 // indirect
|
| 54 |
+
github.com/jinzhu/inflection v1.0.0 // indirect
|
| 55 |
+
github.com/jinzhu/now v1.1.5 // indirect
|
| 56 |
+
github.com/josharian/intern v1.0.0 // indirect
|
| 57 |
+
github.com/json-iterator/go v1.1.12 // indirect
|
| 58 |
+
github.com/klauspost/compress v1.17.11 // indirect
|
| 59 |
+
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
| 60 |
+
github.com/leodido/go-urn v1.4.0 // indirect
|
| 61 |
+
github.com/mailru/easyjson v0.7.7 // indirect
|
| 62 |
+
github.com/mattn/go-colorable v0.1.14 // indirect
|
| 63 |
+
github.com/mattn/go-isatty v0.0.20 // indirect
|
| 64 |
+
github.com/minio/md5-simd v1.1.2 // indirect
|
| 65 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
| 66 |
+
github.com/modern-go/reflect2 v1.0.2 // indirect
|
| 67 |
+
github.com/nats-io/nkeys v0.4.9 // indirect
|
| 68 |
+
github.com/nats-io/nuid v1.0.1 // indirect
|
| 69 |
+
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
| 70 |
+
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
|
| 71 |
+
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
| 72 |
+
github.com/rs/xid v1.6.0 // indirect
|
| 73 |
+
github.com/rs/zerolog v1.35.1 // indirect
|
| 74 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
| 75 |
+
github.com/ugorji/go/codec v1.2.12 // indirect
|
| 76 |
+
github.com/vektah/gqlparser/v2 v2.5.27 // indirect
|
| 77 |
+
go.mau.fi/libsignal v0.2.2 // indirect
|
| 78 |
+
go.mau.fi/util v0.9.10 // indirect
|
| 79 |
+
go.uber.org/atomic v1.11.0 // indirect
|
| 80 |
+
golang.org/x/arch v0.10.0 // indirect
|
| 81 |
+
golang.org/x/crypto v0.53.0 // indirect
|
| 82 |
+
golang.org/x/sync v0.21.0 // indirect
|
| 83 |
+
golang.org/x/sys v0.46.0 // indirect
|
| 84 |
+
golang.org/x/text v0.38.0 // indirect
|
| 85 |
+
golang.org/x/tools v0.46.0 // indirect
|
| 86 |
+
gopkg.in/yaml.v3 v3.0.1 // indirect
|
| 87 |
+
)
|
whatsapp-service/go.sum
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
| 2 |
+
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
| 3 |
+
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
| 4 |
+
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
| 5 |
+
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
| 6 |
+
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
| 7 |
+
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
| 8 |
+
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
| 9 |
+
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
| 10 |
+
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
| 11 |
+
github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
|
| 12 |
+
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
|
| 13 |
+
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
| 14 |
+
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
| 15 |
+
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
| 16 |
+
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
| 17 |
+
github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg=
|
| 18 |
+
github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
| 19 |
+
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
| 20 |
+
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
|
| 21 |
+
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
| 22 |
+
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
| 23 |
+
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
| 24 |
+
github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
|
| 25 |
+
github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
|
| 26 |
+
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
| 27 |
+
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
| 28 |
+
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
| 29 |
+
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
| 30 |
+
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
| 31 |
+
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
| 32 |
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 33 |
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
| 34 |
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 35 |
+
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
| 36 |
+
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
| 37 |
+
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
|
| 38 |
+
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
|
| 39 |
+
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
|
| 40 |
+
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
|
| 41 |
+
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
| 42 |
+
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
| 43 |
+
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
| 44 |
+
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
| 45 |
+
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
| 46 |
+
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
| 47 |
+
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
| 48 |
+
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
| 49 |
+
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
| 50 |
+
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
| 51 |
+
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
| 52 |
+
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
| 53 |
+
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
| 54 |
+
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
| 55 |
+
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
| 56 |
+
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
| 57 |
+
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
| 58 |
+
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
| 59 |
+
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
| 60 |
+
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
| 61 |
+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
| 62 |
+
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
| 63 |
+
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
|
| 64 |
+
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
| 65 |
+
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
| 66 |
+
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
| 67 |
+
github.com/gomessguii/logger v0.0.3 h1:985MqDkp2Fi6IQ3eQ3NG7VXZo8flbWd+6QUj9hASWJA=
|
| 68 |
+
github.com/gomessguii/logger v0.0.3/go.mod h1:JBfDf2h4qUFaIjpE/0T4/jeLpIHopc73k+G9+iu9+ms=
|
| 69 |
+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
| 70 |
+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
| 71 |
+
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
| 72 |
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
| 73 |
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 74 |
+
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
| 75 |
+
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
| 76 |
+
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
| 77 |
+
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
| 78 |
+
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
| 79 |
+
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
| 80 |
+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
| 81 |
+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
| 82 |
+
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
| 83 |
+
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
| 84 |
+
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
| 85 |
+
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
| 86 |
+
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
| 87 |
+
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
| 88 |
+
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
| 89 |
+
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
| 90 |
+
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
| 91 |
+
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
| 92 |
+
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
| 93 |
+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
| 94 |
+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
| 95 |
+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
| 96 |
+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
| 97 |
+
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
| 98 |
+
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
| 99 |
+
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
| 100 |
+
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
| 101 |
+
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
| 102 |
+
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
| 103 |
+
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
| 104 |
+
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
| 105 |
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
| 106 |
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
| 107 |
+
github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk=
|
| 108 |
+
github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
| 109 |
+
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
| 110 |
+
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
| 111 |
+
github.com/minio/minio-go/v7 v7.0.80 h1:2mdUHXEykRdY/BigLt3Iuu1otL0JTogT0Nmltg0wujk=
|
| 112 |
+
github.com/minio/minio-go/v7 v7.0.80/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0=
|
| 113 |
+
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 114 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
| 115 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 116 |
+
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
| 117 |
+
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
| 118 |
+
github.com/nats-io/nats.go v1.39.0 h1:2/yg2JQjiYYKLwDuBzV0FbB2sIV+eFNkEevlRi4n9lI=
|
| 119 |
+
github.com/nats-io/nats.go v1.39.0/go.mod h1:MgRb8oOdigA6cYpEPhXJuRVH6UE/V4jblJ2jQ27IXYM=
|
| 120 |
+
github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0=
|
| 121 |
+
github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE=
|
| 122 |
+
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
| 123 |
+
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
| 124 |
+
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
| 125 |
+
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
| 126 |
+
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
|
| 127 |
+
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
| 128 |
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
| 129 |
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 130 |
+
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
| 131 |
+
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
| 132 |
+
github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0=
|
| 133 |
+
github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4=
|
| 134 |
+
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
| 135 |
+
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
| 136 |
+
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
| 137 |
+
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
| 138 |
+
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
| 139 |
+
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
| 140 |
+
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
| 141 |
+
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
| 142 |
+
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
| 143 |
+
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
| 144 |
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
| 145 |
+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
| 146 |
+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
| 147 |
+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
| 148 |
+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 149 |
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 150 |
+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
| 151 |
+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
| 152 |
+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
| 153 |
+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
| 154 |
+
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
| 155 |
+
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
| 156 |
+
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
| 157 |
+
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
|
| 158 |
+
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
| 159 |
+
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
|
| 160 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
| 161 |
+
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
| 162 |
+
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
| 163 |
+
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
| 164 |
+
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
| 165 |
+
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
| 166 |
+
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
| 167 |
+
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
| 168 |
+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
| 169 |
+
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
| 170 |
+
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
| 171 |
+
go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU=
|
| 172 |
+
go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0=
|
| 173 |
+
go.mau.fi/util v0.9.10 h1:wzvz5iDHyqDXB8vgisD4d3SzucLXNM3iNY+1O1RoHtg=
|
| 174 |
+
go.mau.fi/util v0.9.10/go.mod h1:YQOxySn+ZE3qSYqNxvyX7Yi3suA8YK17PS6QqBREW7A=
|
| 175 |
+
go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b h1:ZUk1ErarDNpnbosXR/MeOz2gkqA4S1bh8zjaSRj7N+Y=
|
| 176 |
+
go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U=
|
| 177 |
+
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
| 178 |
+
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
| 179 |
+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
| 180 |
+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
| 181 |
+
golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8=
|
| 182 |
+
golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
| 183 |
+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
| 184 |
+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
| 185 |
+
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
| 186 |
+
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
| 187 |
+
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
|
| 188 |
+
golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
|
| 189 |
+
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
|
| 190 |
+
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
| 191 |
+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
| 192 |
+
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
| 193 |
+
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
| 194 |
+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
| 195 |
+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
| 196 |
+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
| 197 |
+
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
| 198 |
+
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
| 199 |
+
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
| 200 |
+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
| 201 |
+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
| 202 |
+
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
| 203 |
+
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
| 204 |
+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
| 205 |
+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 206 |
+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 207 |
+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 208 |
+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 209 |
+
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 210 |
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 211 |
+
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
| 212 |
+
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
| 213 |
+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
| 214 |
+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
| 215 |
+
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
| 216 |
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
| 217 |
+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
| 218 |
+
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
| 219 |
+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
| 220 |
+
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
| 221 |
+
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
| 222 |
+
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
| 223 |
+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
| 224 |
+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
| 225 |
+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
| 226 |
+
golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk=
|
| 227 |
+
golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys=
|
| 228 |
+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
| 229 |
+
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
| 230 |
+
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
| 231 |
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 232 |
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
| 233 |
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
| 234 |
+
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
| 235 |
+
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
| 236 |
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 237 |
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
| 238 |
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 239 |
+
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
|
| 240 |
+
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
| 241 |
+
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
whatsapp-service/pkg/cache/redis.go
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package cache provides a Redis-backed cache with a small API mirroring the
|
| 2 |
+
// in-memory cache it replaces (userInfoCache, processedMessages).
|
| 3 |
+
package cache
|
| 4 |
+
|
| 5 |
+
import (
|
| 6 |
+
"context"
|
| 7 |
+
"encoding/json"
|
| 8 |
+
"time"
|
| 9 |
+
|
| 10 |
+
"github.com/redis/go-redis/v9"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
// Cache wraps a go-redis client for key/value storage with TTL.
|
| 14 |
+
type Cache struct {
|
| 15 |
+
rdb *redis.Client
|
| 16 |
+
ctx context.Context
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// New creates a Cache backed by the given Redis client.
|
| 20 |
+
func New(rdb *redis.Client) *Cache {
|
| 21 |
+
return &Cache{rdb: rdb, ctx: context.Background()}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// Set stores a value under key with an optional TTL (0 = no expiration).
|
| 25 |
+
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) error {
|
| 26 |
+
b, err := json.Marshal(value)
|
| 27 |
+
if err != nil {
|
| 28 |
+
return err
|
| 29 |
+
}
|
| 30 |
+
return c.rdb.Set(c.ctx, key, b, ttl).Err()
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Get returns (value, true) if key exists, decoding into out.
|
| 34 |
+
func (c *Cache) Get(key string, out interface{}) (bool, error) {
|
| 35 |
+
data, err := c.rdb.Get(c.ctx, key).Bytes()
|
| 36 |
+
if err != nil {
|
| 37 |
+
if err == redis.Nil {
|
| 38 |
+
return false, nil
|
| 39 |
+
}
|
| 40 |
+
return false, err
|
| 41 |
+
}
|
| 42 |
+
if out == nil {
|
| 43 |
+
return true, nil
|
| 44 |
+
}
|
| 45 |
+
if err := json.Unmarshal(data, out); err != nil {
|
| 46 |
+
return false, err
|
| 47 |
+
}
|
| 48 |
+
return true, nil
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Delete removes a key.
|
| 52 |
+
func (c *Cache) Delete(key string) error {
|
| 53 |
+
return c.rdb.Del(c.ctx, key).Err()
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// SetNX atomically sets a key only if it does not already exist.
|
| 57 |
+
// Returns true when the key was newly created.
|
| 58 |
+
func (c *Cache) SetNX(key string, value interface{}, ttl time.Duration) (bool, error) {
|
| 59 |
+
b, err := json.Marshal(value)
|
| 60 |
+
if err != nil {
|
| 61 |
+
return false, err
|
| 62 |
+
}
|
| 63 |
+
return c.rdb.SetNX(c.ctx, key, b, ttl).Result()
|
| 64 |
+
}
|
whatsapp-service/pkg/call/handler/call_handler.go
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package call_handler
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
|
| 6 |
+
call_service "agentdeck-whatsapp-service/pkg/call/service"
|
| 7 |
+
instance_model "agentdeck-whatsapp-service/pkg/instance/model"
|
| 8 |
+
"github.com/gin-gonic/gin"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type CallHandler interface {
|
| 12 |
+
RejectCall(ctx *gin.Context)
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
type callHandler struct {
|
| 16 |
+
callService call_service.CallService
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// Reject call
|
| 20 |
+
// @Summary Reject call
|
| 21 |
+
// @Description Reject call
|
| 22 |
+
// @Tags Call
|
| 23 |
+
// @Accept json
|
| 24 |
+
// @Produce json
|
| 25 |
+
// @Param message body call_service.RejectCallStruct true "Call data"
|
| 26 |
+
// @Success 200 {object} gin.H "success"
|
| 27 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 28 |
+
// @Router /call/reject [post]
|
| 29 |
+
func (g *callHandler) RejectCall(ctx *gin.Context) {
|
| 30 |
+
getInstance := ctx.MustGet("instance")
|
| 31 |
+
|
| 32 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 33 |
+
if !ok {
|
| 34 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 35 |
+
return
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
var data *call_service.RejectCallStruct
|
| 39 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 40 |
+
if err != nil {
|
| 41 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 42 |
+
return
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
err = g.callService.RejectCall(data, instance)
|
| 46 |
+
if err != nil {
|
| 47 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 48 |
+
return
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
func NewCallHandler(
|
| 55 |
+
callService call_service.CallService,
|
| 56 |
+
) CallHandler {
|
| 57 |
+
return &callHandler{
|
| 58 |
+
callService: callService,
|
| 59 |
+
}
|
| 60 |
+
}
|
whatsapp-service/pkg/call/service/call_service.go
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package call_service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"errors"
|
| 6 |
+
"time"
|
| 7 |
+
|
| 8 |
+
instance_model "agentdeck-whatsapp-service/pkg/instance/model"
|
| 9 |
+
logger_wrapper "agentdeck-whatsapp-service/pkg/logger"
|
| 10 |
+
whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service"
|
| 11 |
+
"github.com/gomessguii/logger"
|
| 12 |
+
"go.mau.fi/whatsmeow"
|
| 13 |
+
"go.mau.fi/whatsmeow/types"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
type CallService interface {
|
| 17 |
+
RejectCall(data *RejectCallStruct, instance *instance_model.Instance) error
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
type callService struct {
|
| 21 |
+
clientPointer map[string]*whatsmeow.Client
|
| 22 |
+
whatsmeowService whatsmeow_service.WhatsmeowService
|
| 23 |
+
loggerWrapper *logger_wrapper.LoggerManager
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
type RejectCallStruct struct {
|
| 27 |
+
CallCreator types.JID `json:"callCreator"`
|
| 28 |
+
CallID string `json:"callId"`
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
func (c *callService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) {
|
| 32 |
+
client := c.clientPointer[instanceId]
|
| 33 |
+
c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil)
|
| 34 |
+
|
| 35 |
+
if client == nil {
|
| 36 |
+
c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] No client found, attempting to start new instance", instanceId)
|
| 37 |
+
err := c.whatsmeowService.StartInstance(instanceId)
|
| 38 |
+
if err != nil {
|
| 39 |
+
c.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to start instance: %v", instanceId, err)
|
| 40 |
+
return nil, errors.New("no active session found")
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId)
|
| 44 |
+
time.Sleep(2 * time.Second)
|
| 45 |
+
|
| 46 |
+
client = c.clientPointer[instanceId]
|
| 47 |
+
c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v",
|
| 48 |
+
instanceId,
|
| 49 |
+
client != nil,
|
| 50 |
+
client != nil && client.IsConnected())
|
| 51 |
+
|
| 52 |
+
if client == nil || !client.IsConnected() {
|
| 53 |
+
c.loggerWrapper.GetLogger(instanceId).LogError("[%s] New client validation failed - Exists: %v, Connected: %v",
|
| 54 |
+
instanceId,
|
| 55 |
+
client != nil,
|
| 56 |
+
client != nil && client.IsConnected())
|
| 57 |
+
return nil, errors.New("no active session found")
|
| 58 |
+
}
|
| 59 |
+
} else if !client.IsConnected() {
|
| 60 |
+
c.loggerWrapper.GetLogger(instanceId).LogError("[%s] Existing client is disconnected - Connected status: %v",
|
| 61 |
+
instanceId,
|
| 62 |
+
client.IsConnected())
|
| 63 |
+
return nil, errors.New("client disconnected")
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected())
|
| 67 |
+
return client, nil
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
func (c *callService) RejectCall(data *RejectCallStruct, instance *instance_model.Instance) error {
|
| 71 |
+
client, err := c.ensureClientConnected(instance.Id)
|
| 72 |
+
if err != nil {
|
| 73 |
+
return err
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
err = client.RejectCall(context.Background(), data.CallCreator, data.CallID)
|
| 77 |
+
if err != nil {
|
| 78 |
+
logger.LogError("[%s] error reject call: %v", instance.Id, err)
|
| 79 |
+
return err
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
return nil
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
func NewCallService(
|
| 86 |
+
clientPointer map[string]*whatsmeow.Client,
|
| 87 |
+
whatsmeowService whatsmeow_service.WhatsmeowService,
|
| 88 |
+
loggerWrapper *logger_wrapper.LoggerManager,
|
| 89 |
+
) CallService {
|
| 90 |
+
return &callService{
|
| 91 |
+
clientPointer: clientPointer,
|
| 92 |
+
whatsmeowService: whatsmeowService,
|
| 93 |
+
loggerWrapper: loggerWrapper,
|
| 94 |
+
}
|
| 95 |
+
}
|
whatsapp-service/pkg/chat/handler/chat_handler.go
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package chat_handler
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
|
| 6 |
+
chat_service "agentdeck-whatsapp-service/pkg/chat/service"
|
| 7 |
+
instance_model "agentdeck-whatsapp-service/pkg/instance/model"
|
| 8 |
+
"github.com/gin-gonic/gin"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type ChatHandler interface {
|
| 12 |
+
ChatPin(ctx *gin.Context)
|
| 13 |
+
ChatUnpin(ctx *gin.Context)
|
| 14 |
+
ChatArchive(ctx *gin.Context)
|
| 15 |
+
ChatUnarchive(ctx *gin.Context)
|
| 16 |
+
ChatMute(ctx *gin.Context)
|
| 17 |
+
ChatUnmute(ctx *gin.Context)
|
| 18 |
+
HistorySyncRequest(ctx *gin.Context)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
type chatHandler struct {
|
| 22 |
+
chatService chat_service.ChatService
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Pin a chat
|
| 26 |
+
// @Summary Pin a chat
|
| 27 |
+
// @Description Pin a chat
|
| 28 |
+
// @Tags Chat
|
| 29 |
+
// @Accept json
|
| 30 |
+
// @Produce json
|
| 31 |
+
// @Param message body chat_service.BodyStruct true "Chat"
|
| 32 |
+
// @Success 200 {object} gin.H "success"
|
| 33 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 34 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 35 |
+
// @Router /chat/pin [post]
|
| 36 |
+
func (c *chatHandler) ChatPin(ctx *gin.Context) {
|
| 37 |
+
getInstance := ctx.MustGet("instance")
|
| 38 |
+
|
| 39 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 40 |
+
if !ok {
|
| 41 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 42 |
+
return
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
var data *chat_service.BodyStruct
|
| 46 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 47 |
+
if err != nil {
|
| 48 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 49 |
+
return
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
if data.Chat == "" {
|
| 53 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"})
|
| 54 |
+
return
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
ts, err := c.chatService.ChatPin(data, instance)
|
| 58 |
+
if err != nil {
|
| 59 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 60 |
+
return
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
responseData := gin.H{
|
| 64 |
+
"timestamp": ts,
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData})
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Unpin a chat
|
| 71 |
+
// @Summary Unpin a chat
|
| 72 |
+
// @Description Unpin a chat
|
| 73 |
+
// @Tags Chat
|
| 74 |
+
// @Accept json
|
| 75 |
+
// @Produce json
|
| 76 |
+
// @Param message body chat_service.BodyStruct true "Chat"
|
| 77 |
+
// @Success 200 {object} gin.H "success"
|
| 78 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 79 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 80 |
+
// @Router /chat/unpin [post]
|
| 81 |
+
func (c *chatHandler) ChatUnpin(ctx *gin.Context) {
|
| 82 |
+
getInstance := ctx.MustGet("instance")
|
| 83 |
+
|
| 84 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 85 |
+
if !ok {
|
| 86 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 87 |
+
return
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
var data *chat_service.BodyStruct
|
| 91 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 92 |
+
if err != nil {
|
| 93 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 94 |
+
return
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
if data.Chat == "" {
|
| 98 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"})
|
| 99 |
+
return
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
ts, err := c.chatService.ChatUnpin(data, instance)
|
| 103 |
+
if err != nil {
|
| 104 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 105 |
+
return
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
responseData := gin.H{
|
| 109 |
+
"timestamp": ts,
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData})
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Archive a chat
|
| 116 |
+
// @Summary Archive a chat
|
| 117 |
+
// @Description Archive a chat
|
| 118 |
+
// @Tags Chat
|
| 119 |
+
// @Accept json
|
| 120 |
+
// @Produce json
|
| 121 |
+
// @Param message body chat_service.BodyStruct true "Chat"
|
| 122 |
+
// @Success 200 {object} gin.H "success"
|
| 123 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 124 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 125 |
+
// @Router /chat/archive [post]
|
| 126 |
+
func (c *chatHandler) ChatArchive(ctx *gin.Context) {
|
| 127 |
+
getInstance := ctx.MustGet("instance")
|
| 128 |
+
|
| 129 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 130 |
+
if !ok {
|
| 131 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 132 |
+
return
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
var data *chat_service.BodyStruct
|
| 136 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 137 |
+
if err != nil {
|
| 138 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 139 |
+
return
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
if data.Chat == "" {
|
| 143 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"})
|
| 144 |
+
return
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
ts, err := c.chatService.ChatArchive(data, instance)
|
| 148 |
+
if err != nil {
|
| 149 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 150 |
+
return
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
responseData := gin.H{
|
| 154 |
+
"timestamp": ts,
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData})
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
// Unarchive a chat
|
| 161 |
+
// @Summary Unarchive a chat
|
| 162 |
+
// @Description Unarchive a chat
|
| 163 |
+
// @Tags Chat
|
| 164 |
+
// @Accept json
|
| 165 |
+
// @Produce json
|
| 166 |
+
// @Param message body chat_service.BodyStruct true "Chat"
|
| 167 |
+
// @Success 200 {object} gin.H "success"
|
| 168 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 169 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 170 |
+
// @Router /chat/unarchive [post]
|
| 171 |
+
func (c *chatHandler) ChatUnarchive(ctx *gin.Context) {
|
| 172 |
+
getInstance := ctx.MustGet("instance")
|
| 173 |
+
|
| 174 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 175 |
+
if !ok {
|
| 176 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 177 |
+
return
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
var data *chat_service.BodyStruct
|
| 181 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 182 |
+
if err != nil {
|
| 183 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 184 |
+
return
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
if data.Chat == "" {
|
| 188 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"})
|
| 189 |
+
return
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
ts, err := c.chatService.ChatUnarchive(data, instance)
|
| 193 |
+
if err != nil {
|
| 194 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 195 |
+
return
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
responseData := gin.H{
|
| 199 |
+
"timestamp": ts,
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData})
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
// Mute a chat
|
| 206 |
+
// @Summary Mute a chat
|
| 207 |
+
// @Description Mute a chat
|
| 208 |
+
// @Tags Chat
|
| 209 |
+
// @Accept json
|
| 210 |
+
// @Produce json
|
| 211 |
+
// @Param message body chat_service.BodyStruct true "Chat"
|
| 212 |
+
// @Success 200 {object} gin.H "success"
|
| 213 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 214 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 215 |
+
// @Router /chat/mute [post]
|
| 216 |
+
func (c *chatHandler) ChatMute(ctx *gin.Context) {
|
| 217 |
+
getInstance := ctx.MustGet("instance")
|
| 218 |
+
|
| 219 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 220 |
+
if !ok {
|
| 221 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 222 |
+
return
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
var data *chat_service.BodyStruct
|
| 226 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 227 |
+
if err != nil {
|
| 228 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 229 |
+
return
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
if data.Chat == "" {
|
| 233 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"})
|
| 234 |
+
return
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
ts, err := c.chatService.ChatMute(data, instance)
|
| 238 |
+
if err != nil {
|
| 239 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 240 |
+
return
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
responseData := gin.H{
|
| 244 |
+
"timestamp": ts,
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData})
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
// Unmute a chat
|
| 251 |
+
// @Summary Unmute a chat
|
| 252 |
+
// @Description Unmute a chat
|
| 253 |
+
// @Tags Chat
|
| 254 |
+
// @Accept json
|
| 255 |
+
// @Produce json
|
| 256 |
+
// @Param message body chat_service.BodyStruct true "Chat"
|
| 257 |
+
// @Success 200 {object} gin.H "success"
|
| 258 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 259 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 260 |
+
// @Router /chat/unmute [post]
|
| 261 |
+
func (c *chatHandler) ChatUnmute(ctx *gin.Context) {
|
| 262 |
+
getInstance := ctx.MustGet("instance")
|
| 263 |
+
|
| 264 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 265 |
+
if !ok {
|
| 266 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 267 |
+
return
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
var data *chat_service.BodyStruct
|
| 271 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 272 |
+
if err != nil {
|
| 273 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 274 |
+
return
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
if data.Chat == "" {
|
| 278 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"})
|
| 279 |
+
return
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
ts, err := c.chatService.ChatUnmute(data, instance)
|
| 283 |
+
if err != nil {
|
| 284 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 285 |
+
return
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
responseData := gin.H{
|
| 289 |
+
"timestamp": ts,
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData})
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
// HistorySyncRequest a chat
|
| 296 |
+
// @Summary HistorySyncRequest a chat
|
| 297 |
+
// @Description HistorySyncRequest a chat
|
| 298 |
+
// @Tags Chat
|
| 299 |
+
// @Accept json
|
| 300 |
+
// @Produce json
|
| 301 |
+
// @Param message body chat_service.HistorySyncRequestStruct true "Chat"
|
| 302 |
+
// @Success 200 {object} gin.H "success"
|
| 303 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 304 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 305 |
+
// @Router /chat/history-sync [post]
|
| 306 |
+
func (c *chatHandler) HistorySyncRequest(ctx *gin.Context) {
|
| 307 |
+
getInstance := ctx.MustGet("instance")
|
| 308 |
+
|
| 309 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 310 |
+
if !ok {
|
| 311 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 312 |
+
return
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
var data *chat_service.HistorySyncRequestStruct
|
| 316 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 317 |
+
if err != nil {
|
| 318 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 319 |
+
return
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
resp, err := c.chatService.HistorySyncRequest(data, instance)
|
| 323 |
+
if err != nil {
|
| 324 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 325 |
+
return
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp})
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
func NewChatHandler(
|
| 332 |
+
chatService chat_service.ChatService,
|
| 333 |
+
) ChatHandler {
|
| 334 |
+
return &chatHandler{
|
| 335 |
+
chatService: chatService,
|
| 336 |
+
}
|
| 337 |
+
}
|
whatsapp-service/pkg/chat/service/chat_service.go
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package chat_service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"errors"
|
| 6 |
+
"time"
|
| 7 |
+
|
| 8 |
+
instance_model "agentdeck-whatsapp-service/pkg/instance/model"
|
| 9 |
+
logger_wrapper "agentdeck-whatsapp-service/pkg/logger"
|
| 10 |
+
"agentdeck-whatsapp-service/pkg/utils"
|
| 11 |
+
whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service"
|
| 12 |
+
"go.mau.fi/whatsmeow"
|
| 13 |
+
"go.mau.fi/whatsmeow/appstate"
|
| 14 |
+
"go.mau.fi/whatsmeow/types"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
type ChatService interface {
|
| 18 |
+
ChatPin(data *BodyStruct, instance *instance_model.Instance) (string, error)
|
| 19 |
+
ChatUnpin(data *BodyStruct, instance *instance_model.Instance) (string, error)
|
| 20 |
+
ChatArchive(data *BodyStruct, instance *instance_model.Instance) (string, error)
|
| 21 |
+
ChatUnarchive(data *BodyStruct, instance *instance_model.Instance) (string, error)
|
| 22 |
+
ChatMute(data *BodyStruct, instance *instance_model.Instance) (string, error)
|
| 23 |
+
ChatUnmute(data *BodyStruct, instance *instance_model.Instance) (string, error)
|
| 24 |
+
HistorySyncRequest(data *HistorySyncRequestStruct, instance *instance_model.Instance) (*whatsmeow.SendResponse, error)
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
type chatService struct {
|
| 28 |
+
clientPointer map[string]*whatsmeow.Client
|
| 29 |
+
whatsmeowService whatsmeow_service.WhatsmeowService
|
| 30 |
+
loggerWrapper *logger_wrapper.LoggerManager
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
type BodyStruct struct {
|
| 34 |
+
Chat string `json:"chat"`
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
type HistorySyncRequestStruct struct {
|
| 38 |
+
MessageInfo *types.MessageInfo `json:"messageInfo"`
|
| 39 |
+
Count int `json:"count"`
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func (c *chatService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) {
|
| 43 |
+
client := c.clientPointer[instanceId]
|
| 44 |
+
c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil)
|
| 45 |
+
|
| 46 |
+
if client == nil {
|
| 47 |
+
c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] No client found, attempting to start new instance", instanceId)
|
| 48 |
+
err := c.whatsmeowService.StartInstance(instanceId)
|
| 49 |
+
if err != nil {
|
| 50 |
+
c.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to start instance: %v", instanceId, err)
|
| 51 |
+
return nil, errors.New("no active session found")
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId)
|
| 55 |
+
time.Sleep(2 * time.Second)
|
| 56 |
+
|
| 57 |
+
client = c.clientPointer[instanceId]
|
| 58 |
+
c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v",
|
| 59 |
+
instanceId,
|
| 60 |
+
client != nil,
|
| 61 |
+
client != nil && client.IsConnected())
|
| 62 |
+
|
| 63 |
+
if client == nil || !client.IsConnected() {
|
| 64 |
+
c.loggerWrapper.GetLogger(instanceId).LogError("[%s] New client validation failed - Exists: %v, Connected: %v",
|
| 65 |
+
instanceId,
|
| 66 |
+
client != nil,
|
| 67 |
+
client != nil && client.IsConnected())
|
| 68 |
+
return nil, errors.New("no active session found")
|
| 69 |
+
}
|
| 70 |
+
} else if !client.IsConnected() {
|
| 71 |
+
c.loggerWrapper.GetLogger(instanceId).LogError("[%s] Existing client is disconnected - Connected status: %v",
|
| 72 |
+
instanceId,
|
| 73 |
+
client.IsConnected())
|
| 74 |
+
return nil, errors.New("client disconnected")
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected())
|
| 78 |
+
return client, nil
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
func (c *chatService) ChatPin(data *BodyStruct, instance *instance_model.Instance) (string, error) {
|
| 82 |
+
client, err := c.ensureClientConnected(instance.Id)
|
| 83 |
+
if err != nil {
|
| 84 |
+
return "", err
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
var ts time.Time
|
| 88 |
+
|
| 89 |
+
recipient, ok := utils.ParseJID(data.Chat)
|
| 90 |
+
if !ok {
|
| 91 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id)
|
| 92 |
+
return "", errors.New("invalid phone number")
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
err = client.SendAppState(context.Background(), appstate.BuildPin(recipient, true))
|
| 96 |
+
if err != nil {
|
| 97 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error pin chat: %v", instance.Id, err)
|
| 98 |
+
return "", err
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
return ts.String(), nil
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
func (c *chatService) ChatUnpin(data *BodyStruct, instance *instance_model.Instance) (string, error) {
|
| 105 |
+
client, err := c.ensureClientConnected(instance.Id)
|
| 106 |
+
if err != nil {
|
| 107 |
+
return "", err
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
var ts time.Time
|
| 111 |
+
|
| 112 |
+
recipient, ok := utils.ParseJID(data.Chat)
|
| 113 |
+
if !ok {
|
| 114 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id)
|
| 115 |
+
return "", errors.New("invalid phone number")
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
err = client.SendAppState(context.Background(), appstate.BuildPin(recipient, false))
|
| 119 |
+
if err != nil {
|
| 120 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error unpin chat: %v", instance.Id, err)
|
| 121 |
+
return "", err
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
return ts.String(), nil
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
func (c *chatService) ChatArchive(data *BodyStruct, instance *instance_model.Instance) (string, error) {
|
| 128 |
+
client, err := c.ensureClientConnected(instance.Id)
|
| 129 |
+
if err != nil {
|
| 130 |
+
return "", err
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
var ts time.Time
|
| 134 |
+
|
| 135 |
+
recipient, ok := utils.ParseJID(data.Chat)
|
| 136 |
+
if !ok {
|
| 137 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id)
|
| 138 |
+
return "", errors.New("invalid phone number")
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
err = client.SendAppState(context.Background(), appstate.BuildArchive(recipient, true, time.Time{}, nil))
|
| 142 |
+
if err != nil {
|
| 143 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error archive chat: %v", instance.Id, err)
|
| 144 |
+
return "", err
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
return ts.String(), nil
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
func (c *chatService) ChatUnarchive(data *BodyStruct, instance *instance_model.Instance) (string, error) {
|
| 151 |
+
client, err := c.ensureClientConnected(instance.Id)
|
| 152 |
+
if err != nil {
|
| 153 |
+
return "", err
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
var ts time.Time
|
| 157 |
+
|
| 158 |
+
recipient, ok := utils.ParseJID(data.Chat)
|
| 159 |
+
if !ok {
|
| 160 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id)
|
| 161 |
+
return "", errors.New("invalid phone number")
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
err = client.SendAppState(context.Background(), appstate.BuildArchive(recipient, false, time.Time{}, nil))
|
| 165 |
+
if err != nil {
|
| 166 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error unarchive chat: %v", instance.Id, err)
|
| 167 |
+
return "", err
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
return ts.String(), nil
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
func (c *chatService) ChatMute(data *BodyStruct, instance *instance_model.Instance) (string, error) {
|
| 174 |
+
client, err := c.ensureClientConnected(instance.Id)
|
| 175 |
+
if err != nil {
|
| 176 |
+
return "", err
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
var ts time.Time
|
| 180 |
+
|
| 181 |
+
recipient, ok := utils.ParseJID(data.Chat)
|
| 182 |
+
if !ok {
|
| 183 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id)
|
| 184 |
+
return "", errors.New("invalid phone number")
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
err = client.SendAppState(context.Background(), appstate.BuildMute(recipient, true, 1*time.Hour))
|
| 188 |
+
if err != nil {
|
| 189 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error mute chat: %v", instance.Id, err)
|
| 190 |
+
return "", err
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
return ts.String(), nil
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
func (c *chatService) ChatUnmute(data *BodyStruct, instance *instance_model.Instance) (string, error) {
|
| 197 |
+
client, err := c.ensureClientConnected(instance.Id)
|
| 198 |
+
if err != nil {
|
| 199 |
+
return "", err
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
var ts time.Time
|
| 203 |
+
|
| 204 |
+
recipient, ok := utils.ParseJID(data.Chat)
|
| 205 |
+
if !ok {
|
| 206 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id)
|
| 207 |
+
return "", errors.New("invalid phone number")
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
err = client.SendAppState(context.Background(), appstate.BuildMute(recipient, false, 0*time.Hour))
|
| 211 |
+
if err != nil {
|
| 212 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error unmute chat: %v", instance.Id, err)
|
| 213 |
+
return "", err
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
return ts.String(), nil
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
func (c *chatService) HistorySyncRequest(data *HistorySyncRequestStruct, instance *instance_model.Instance) (*whatsmeow.SendResponse, error) {
|
| 220 |
+
client, err := c.ensureClientConnected(instance.Id)
|
| 221 |
+
if err != nil {
|
| 222 |
+
return nil, err
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
messageInfo := types.MessageInfo{
|
| 226 |
+
MessageSource: types.MessageSource{
|
| 227 |
+
Chat: data.MessageInfo.Chat,
|
| 228 |
+
IsFromMe: data.MessageInfo.IsFromMe,
|
| 229 |
+
IsGroup: data.MessageInfo.IsGroup,
|
| 230 |
+
},
|
| 231 |
+
ID: data.MessageInfo.ID,
|
| 232 |
+
Timestamp: data.MessageInfo.Timestamp,
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
histRequest := client.BuildHistorySyncRequest(&messageInfo, data.Count)
|
| 236 |
+
|
| 237 |
+
res, err := client.SendMessage(context.Background(), messageInfo.Chat, histRequest, whatsmeow.SendRequestExtra{Peer: true})
|
| 238 |
+
if err != nil {
|
| 239 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error history sync request: %v", instance.Id, err)
|
| 240 |
+
return nil, err
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
return &res, nil
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
func NewChatService(
|
| 247 |
+
clientPointer map[string]*whatsmeow.Client,
|
| 248 |
+
whatsmeowService whatsmeow_service.WhatsmeowService,
|
| 249 |
+
loggerWrapper *logger_wrapper.LoggerManager,
|
| 250 |
+
) ChatService {
|
| 251 |
+
return &chatService{
|
| 252 |
+
clientPointer: clientPointer,
|
| 253 |
+
whatsmeowService: whatsmeowService,
|
| 254 |
+
loggerWrapper: loggerWrapper,
|
| 255 |
+
}
|
| 256 |
+
}
|
whatsapp-service/pkg/community/handler/community_handler.go
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package community_handler
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
|
| 6 |
+
community_service "agentdeck-whatsapp-service/pkg/community/service"
|
| 7 |
+
instance_model "agentdeck-whatsapp-service/pkg/instance/model"
|
| 8 |
+
"github.com/gin-gonic/gin"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type CommunityHandler interface {
|
| 12 |
+
CreateCommunity(ctx *gin.Context)
|
| 13 |
+
CommunityAdd(ctx *gin.Context)
|
| 14 |
+
CommunityRemove(ctx *gin.Context)
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
type communityHandler struct {
|
| 18 |
+
communityService community_service.CommunityService
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// Create community
|
| 22 |
+
// @Summary Create community
|
| 23 |
+
// @Description Create community
|
| 24 |
+
// @Tags Community
|
| 25 |
+
// @Accept json
|
| 26 |
+
// @Produce json
|
| 27 |
+
// @Param message body community_service.CreateCommunityStruct true "Community data"
|
| 28 |
+
// @Success 200 {object} gin.H "success"
|
| 29 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 30 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 31 |
+
// @Router /community/create [post]
|
| 32 |
+
func (c *communityHandler) CreateCommunity(ctx *gin.Context) {
|
| 33 |
+
getInstance := ctx.MustGet("instance")
|
| 34 |
+
|
| 35 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 36 |
+
if !ok {
|
| 37 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 38 |
+
return
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
var data *community_service.CreateCommunityStruct
|
| 42 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 43 |
+
if err != nil {
|
| 44 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 45 |
+
return
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
if data.CommunityName == "" {
|
| 49 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "community name is required"})
|
| 50 |
+
return
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
community, err := c.communityService.CreateCommunity(data, instance)
|
| 54 |
+
if err != nil {
|
| 55 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 56 |
+
return
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": community})
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// Add participant to community
|
| 63 |
+
// @Summary Add participant to community
|
| 64 |
+
// @Description Add participant to community
|
| 65 |
+
// @Tags Community
|
| 66 |
+
// @Accept json
|
| 67 |
+
// @Produce json
|
| 68 |
+
// @Param message body community_service.AddParticipantStruct true "Participant data"
|
| 69 |
+
// @Success 200 {object} gin.H "success"
|
| 70 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 71 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 72 |
+
// @Router /community/add [post]
|
| 73 |
+
func (c *communityHandler) CommunityAdd(ctx *gin.Context) {
|
| 74 |
+
getInstance := ctx.MustGet("instance")
|
| 75 |
+
|
| 76 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 77 |
+
if !ok {
|
| 78 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 79 |
+
return
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
var data *community_service.AddParticipantStruct
|
| 83 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 84 |
+
if err != nil {
|
| 85 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 86 |
+
return
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
if data.CommunityJID == "" {
|
| 90 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "community jid is required"})
|
| 91 |
+
return
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
if len(data.GroupJID) == 0 {
|
| 95 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "group jid is required"})
|
| 96 |
+
return
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
resp, err := c.communityService.CommunityAdd(data, instance)
|
| 100 |
+
if err != nil {
|
| 101 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 102 |
+
return
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp})
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// Remove participant from community
|
| 109 |
+
// @Summary Remove participant from community
|
| 110 |
+
// @Description Remove participant from community
|
| 111 |
+
// @Tags Community
|
| 112 |
+
// @Accept json
|
| 113 |
+
// @Produce json
|
| 114 |
+
// @Param message body community_service.AddParticipantStruct true "Participant data"
|
| 115 |
+
// @Success 200 {object} gin.H "success"
|
| 116 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 117 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 118 |
+
// @Router /community/remove [post]
|
| 119 |
+
func (c *communityHandler) CommunityRemove(ctx *gin.Context) {
|
| 120 |
+
getInstance := ctx.MustGet("instance")
|
| 121 |
+
|
| 122 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 123 |
+
if !ok {
|
| 124 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 125 |
+
return
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
var data *community_service.AddParticipantStruct
|
| 129 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 130 |
+
if err != nil {
|
| 131 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 132 |
+
return
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
if data.CommunityJID == "" {
|
| 136 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "community jid is required"})
|
| 137 |
+
return
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
if len(data.GroupJID) == 0 {
|
| 141 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "group jid is required"})
|
| 142 |
+
return
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
resp, err := c.communityService.CommunityRemove(data, instance)
|
| 146 |
+
if err != nil {
|
| 147 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 148 |
+
return
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp})
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
func NewCommunityHandler(
|
| 155 |
+
communityService community_service.CommunityService,
|
| 156 |
+
) CommunityHandler {
|
| 157 |
+
return &communityHandler{
|
| 158 |
+
communityService: communityService,
|
| 159 |
+
}
|
| 160 |
+
}
|
whatsapp-service/pkg/community/service/community_service.go
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package community_service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"errors"
|
| 6 |
+
"time"
|
| 7 |
+
|
| 8 |
+
instance_model "agentdeck-whatsapp-service/pkg/instance/model"
|
| 9 |
+
logger_wrapper "agentdeck-whatsapp-service/pkg/logger"
|
| 10 |
+
"agentdeck-whatsapp-service/pkg/utils"
|
| 11 |
+
whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service"
|
| 12 |
+
"github.com/gin-gonic/gin"
|
| 13 |
+
"go.mau.fi/whatsmeow"
|
| 14 |
+
"go.mau.fi/whatsmeow/types"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
type CommunityService interface {
|
| 18 |
+
CreateCommunity(data *CreateCommunityStruct, instance *instance_model.Instance) (*types.GroupInfo, error)
|
| 19 |
+
CommunityAdd(data *AddParticipantStruct, instance *instance_model.Instance) (gin.H, error)
|
| 20 |
+
CommunityRemove(data *AddParticipantStruct, instance *instance_model.Instance) (gin.H, error)
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
type communityService struct {
|
| 24 |
+
clientPointer map[string]*whatsmeow.Client
|
| 25 |
+
whatsmeowService whatsmeow_service.WhatsmeowService
|
| 26 |
+
loggerWrapper *logger_wrapper.LoggerManager
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
type CreateCommunityStruct struct {
|
| 30 |
+
CommunityName string `json:"communityName"`
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
type AddParticipantStruct struct {
|
| 34 |
+
CommunityJID string `json:"communityJid"`
|
| 35 |
+
GroupJID []string `json:"groupJid"`
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
func (c *communityService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) {
|
| 39 |
+
client := c.clientPointer[instanceId]
|
| 40 |
+
c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil)
|
| 41 |
+
|
| 42 |
+
if client == nil {
|
| 43 |
+
c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] No client found, attempting to start new instance", instanceId)
|
| 44 |
+
err := c.whatsmeowService.StartInstance(instanceId)
|
| 45 |
+
if err != nil {
|
| 46 |
+
c.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to start instance: %v", instanceId, err)
|
| 47 |
+
return nil, errors.New("no active session found")
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId)
|
| 51 |
+
time.Sleep(2 * time.Second)
|
| 52 |
+
|
| 53 |
+
client = c.clientPointer[instanceId]
|
| 54 |
+
c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v",
|
| 55 |
+
instanceId,
|
| 56 |
+
client != nil,
|
| 57 |
+
client != nil && client.IsConnected())
|
| 58 |
+
|
| 59 |
+
if client == nil || !client.IsConnected() {
|
| 60 |
+
c.loggerWrapper.GetLogger(instanceId).LogError("[%s] New client validation failed - Exists: %v, Connected: %v",
|
| 61 |
+
instanceId,
|
| 62 |
+
client != nil,
|
| 63 |
+
client != nil && client.IsConnected())
|
| 64 |
+
return nil, errors.New("no active session found")
|
| 65 |
+
}
|
| 66 |
+
} else if !client.IsConnected() {
|
| 67 |
+
c.loggerWrapper.GetLogger(instanceId).LogError("[%s] Existing client is disconnected - Connected status: %v",
|
| 68 |
+
instanceId,
|
| 69 |
+
client.IsConnected())
|
| 70 |
+
return nil, errors.New("client disconnected")
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected())
|
| 74 |
+
return client, nil
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
func (c *communityService) CreateCommunity(data *CreateCommunityStruct, instance *instance_model.Instance) (*types.GroupInfo, error) {
|
| 78 |
+
client, err := c.ensureClientConnected(instance.Id)
|
| 79 |
+
if err != nil {
|
| 80 |
+
return nil, err
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
resp, err := client.CreateGroup(context.Background(), whatsmeow.ReqCreateGroup{
|
| 84 |
+
Name: data.CommunityName,
|
| 85 |
+
GroupParent: types.GroupParent{
|
| 86 |
+
IsParent: true,
|
| 87 |
+
},
|
| 88 |
+
})
|
| 89 |
+
if err != nil {
|
| 90 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error create community: %v", instance.Id, err)
|
| 91 |
+
return nil, err
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
return resp, nil
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
func (c *communityService) CommunityAdd(data *AddParticipantStruct, instance *instance_model.Instance) (gin.H, error) {
|
| 98 |
+
client, err := c.ensureClientConnected(instance.Id)
|
| 99 |
+
if err != nil {
|
| 100 |
+
return nil, err
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
communityJID, ok := utils.ParseJID(data.CommunityJID)
|
| 104 |
+
if !ok {
|
| 105 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error parse community jid", instance.Id)
|
| 106 |
+
return nil, errors.New("error parse community jid")
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
var successList []string
|
| 110 |
+
var failedList []string
|
| 111 |
+
|
| 112 |
+
for _, participant := range data.GroupJID {
|
| 113 |
+
groupJID, _ := utils.ParseJID(participant)
|
| 114 |
+
err := client.LinkGroup(context.Background(), communityJID, groupJID)
|
| 115 |
+
if err != nil {
|
| 116 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error link group: %v", instance.Id, err)
|
| 117 |
+
failedList = append(failedList, groupJID.String())
|
| 118 |
+
}
|
| 119 |
+
successList = append(failedList, groupJID.String())
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
return gin.H{
|
| 123 |
+
"success": successList,
|
| 124 |
+
"failed": failedList,
|
| 125 |
+
}, nil
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
func (c *communityService) CommunityRemove(data *AddParticipantStruct, instance *instance_model.Instance) (gin.H, error) {
|
| 129 |
+
client, err := c.ensureClientConnected(instance.Id)
|
| 130 |
+
if err != nil {
|
| 131 |
+
return nil, err
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
communityJID, ok := utils.ParseJID(data.CommunityJID)
|
| 135 |
+
if !ok {
|
| 136 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error parse community jid", instance.Id)
|
| 137 |
+
return nil, errors.New("error parse community jid")
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
var successList []string
|
| 141 |
+
var failedList []string
|
| 142 |
+
|
| 143 |
+
for _, participant := range data.GroupJID {
|
| 144 |
+
groupJID, _ := utils.ParseJID(participant)
|
| 145 |
+
err := client.UnlinkGroup(context.Background(), communityJID, groupJID)
|
| 146 |
+
if err != nil {
|
| 147 |
+
c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error link group: %v", instance.Id, err)
|
| 148 |
+
failedList = append(failedList, groupJID.String())
|
| 149 |
+
}
|
| 150 |
+
successList = append(failedList, groupJID.String())
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
return gin.H{
|
| 154 |
+
"success": successList,
|
| 155 |
+
"failed": failedList,
|
| 156 |
+
}, nil
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
func NewCommunityService(
|
| 160 |
+
clientPointer map[string]*whatsmeow.Client,
|
| 161 |
+
whatsmeowService whatsmeow_service.WhatsmeowService,
|
| 162 |
+
loggerWrapper *logger_wrapper.LoggerManager,
|
| 163 |
+
) CommunityService {
|
| 164 |
+
return &communityService{
|
| 165 |
+
clientPointer: clientPointer,
|
| 166 |
+
whatsmeowService: whatsmeowService,
|
| 167 |
+
loggerWrapper: loggerWrapper,
|
| 168 |
+
}
|
| 169 |
+
}
|
whatsapp-service/pkg/config/config.go
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package config
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"database/sql"
|
| 5 |
+
"fmt"
|
| 6 |
+
"net/url"
|
| 7 |
+
"os"
|
| 8 |
+
"strconv"
|
| 9 |
+
"strings"
|
| 10 |
+
"time"
|
| 11 |
+
|
| 12 |
+
"github.com/gomessguii/logger"
|
| 13 |
+
_ "github.com/lib/pq"
|
| 14 |
+
|
| 15 |
+
config_env "agentdeck-whatsapp-service/pkg/config/env"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
type Config struct {
|
| 19 |
+
// Supabase (PostgREST API + native Postgres for whatsmeow store/wasmeow)
|
| 20 |
+
SupabaseURL string
|
| 21 |
+
SupabaseServiceKey string
|
| 22 |
+
SupabaseDBURL string
|
| 23 |
+
DatabaseSaveMessages bool
|
| 24 |
+
GlobalApiKey string
|
| 25 |
+
WaDebug string
|
| 26 |
+
LogType string
|
| 27 |
+
WebhookFiles bool
|
| 28 |
+
ConnectOnStartup bool
|
| 29 |
+
OsName string
|
| 30 |
+
AmqpUrl string
|
| 31 |
+
AmqpGlobalEnabled bool
|
| 32 |
+
WebhookUrl string
|
| 33 |
+
ClientName string
|
| 34 |
+
ApiAudioConverter string
|
| 35 |
+
ApiAudioConverterKey string
|
| 36 |
+
MinioEndpoint string
|
| 37 |
+
MinioAccessKey string
|
| 38 |
+
MinioSecretKey string
|
| 39 |
+
MinioBucket string
|
| 40 |
+
MinioUseSSL bool
|
| 41 |
+
MinioEnabled bool
|
| 42 |
+
MinioRegion string
|
| 43 |
+
WhatsappVersionMajor int
|
| 44 |
+
WhatsappVersionMinor int
|
| 45 |
+
WhatsappVersionPatch int
|
| 46 |
+
ProxyProtocol string
|
| 47 |
+
ProxyHost string
|
| 48 |
+
ProxyPort string
|
| 49 |
+
ProxyUsername string
|
| 50 |
+
ProxyPassword string
|
| 51 |
+
AmqpGlobalEvents []string
|
| 52 |
+
AmqpSpecificEvents []string
|
| 53 |
+
NatsUrl string
|
| 54 |
+
NatsGlobalEnabled bool
|
| 55 |
+
NatsGlobalEvents []string
|
| 56 |
+
EventIgnoreGroup bool
|
| 57 |
+
EventIgnoreStatus bool
|
| 58 |
+
QrcodeMaxCount int
|
| 59 |
+
CheckUserExists bool
|
| 60 |
+
|
| 61 |
+
// Redis (cache / temporary state)
|
| 62 |
+
RedisURL string
|
| 63 |
+
RedisPassword string
|
| 64 |
+
RedisDB int
|
| 65 |
+
|
| 66 |
+
// Logger configurations
|
| 67 |
+
LogMaxSize int
|
| 68 |
+
LogMaxBackups int
|
| 69 |
+
LogMaxAge int
|
| 70 |
+
LogDirectory string
|
| 71 |
+
LogCompress bool
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// CreateSupabaseDB opens a native Postgres connection to the Supabase database
|
| 75 |
+
// (SUPABASE_DB_URL). This is used exclusively by the whatsmeow sqlstore, which
|
| 76 |
+
// requires a real Postgres driver and cannot talk to the HTTP PostgREST API.
|
| 77 |
+
func (c *Config) CreateSupabaseDB() (*sql.DB, error) {
|
| 78 |
+
if c.SupabaseDBURL == "" {
|
| 79 |
+
return nil, fmt.Errorf("SUPABASE_DB_URL is required for the whatsmeow session store")
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
db, err := sql.Open("postgres", c.SupabaseDBURL)
|
| 83 |
+
if err != nil {
|
| 84 |
+
return nil, err
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
db.SetMaxOpenConns(25)
|
| 88 |
+
db.SetMaxIdleConns(5)
|
| 89 |
+
db.SetConnMaxLifetime(5 * time.Minute)
|
| 90 |
+
db.SetConnMaxIdleTime(1 * time.Minute)
|
| 91 |
+
|
| 92 |
+
if err := db.Ping(); err != nil {
|
| 93 |
+
return nil, fmt.Errorf("failed to ping Supabase Postgres (SUPABASE_DB_URL): %v", err)
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
logger.LogInfo("[CONFIG] Connected to Supabase Postgres (whatsmeow store) with connection pool configured")
|
| 97 |
+
return db, nil
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
func Load() *Config {
|
| 101 |
+
supabaseURL := os.Getenv(config_env.SUPABASE_URL)
|
| 102 |
+
supabaseServiceKey := os.Getenv(config_env.SUPABASE_SERVICE_KEY)
|
| 103 |
+
supabaseDBURL := os.Getenv(config_env.SUPABASE_DB_URL)
|
| 104 |
+
redisURL := os.Getenv(config_env.REDIS_URL)
|
| 105 |
+
|
| 106 |
+
if supabaseURL == "" || supabaseServiceKey == "" {
|
| 107 |
+
logger.LogFatal("[CONFIG] required Supabase configuration variables are missing. Please check your environment configuration (SUPABASE_URL, SUPABASE_SERVICE_KEY).")
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
databaseSaveMessages := os.Getenv(config_env.DATABASE_SAVE_MESSAGES)
|
| 111 |
+
panicIfEmpty(config_env.DATABASE_SAVE_MESSAGES, databaseSaveMessages)
|
| 112 |
+
|
| 113 |
+
globalApiKey := os.Getenv(config_env.GLOBAL_API_KEY)
|
| 114 |
+
panicIfEmpty(config_env.GLOBAL_API_KEY, globalApiKey)
|
| 115 |
+
|
| 116 |
+
clientName := os.Getenv(config_env.CLIENT_NAME)
|
| 117 |
+
|
| 118 |
+
waDebug := os.Getenv(config_env.WA_DEBUG)
|
| 119 |
+
|
| 120 |
+
logType := os.Getenv(config_env.LOGTYPE)
|
| 121 |
+
|
| 122 |
+
webhookFiles := os.Getenv(config_env.WEBHOOKFILES)
|
| 123 |
+
if webhookFiles == "" {
|
| 124 |
+
webhookFiles = "true"
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
connectOnStartup := os.Getenv(config_env.CONNECT_ON_STARTUP)
|
| 128 |
+
if connectOnStartup == "" {
|
| 129 |
+
connectOnStartup = "false"
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
osName := os.Getenv(config_env.OS_NAME)
|
| 133 |
+
|
| 134 |
+
amqpUrl := os.Getenv(config_env.AMQP_URL)
|
| 135 |
+
|
| 136 |
+
// Validate AMQP URL format
|
| 137 |
+
if err := validateAMQPURL(amqpUrl); err != nil {
|
| 138 |
+
logger.LogFatal("[CONFIG] AMQP URL validation failed: %v", err)
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
amqpGlobalEnabled := os.Getenv(config_env.AMQP_GLOBAL_ENABLED)
|
| 142 |
+
|
| 143 |
+
webhookUrl := os.Getenv(config_env.WEBHOOK_URL)
|
| 144 |
+
|
| 145 |
+
apiAudioConverter := os.Getenv(config_env.API_AUDIO_CONVERTER)
|
| 146 |
+
apiAudioConverterKey := os.Getenv(config_env.API_AUDIO_CONVERTER_KEY)
|
| 147 |
+
|
| 148 |
+
whatsappVersionMajor := os.Getenv(config_env.WHATSAPP_VERSION_MAJOR)
|
| 149 |
+
whatsappVersionMinor := os.Getenv(config_env.WHATSAPP_VERSION_MINOR)
|
| 150 |
+
whatsappVersionPatch := os.Getenv(config_env.WHATSAPP_VERSION_PATCH)
|
| 151 |
+
|
| 152 |
+
proxyProtocol := os.Getenv(config_env.PROXY_PROTOCOL)
|
| 153 |
+
proxyHost := os.Getenv(config_env.PROXY_HOST)
|
| 154 |
+
proxyPort := os.Getenv(config_env.PROXY_PORT)
|
| 155 |
+
proxyUsername := os.Getenv(config_env.PROXY_USERNAME)
|
| 156 |
+
proxyPassword := os.Getenv(config_env.PROXY_PASSWORD)
|
| 157 |
+
|
| 158 |
+
eventIgnoreGroup := os.Getenv(config_env.EVENT_IGNORE_GROUP)
|
| 159 |
+
eventIgnoreStatus := os.Getenv(config_env.EVENT_IGNORE_STATUS)
|
| 160 |
+
qrcodeMaxCount := os.Getenv(config_env.QRCODE_MAX_COUNT)
|
| 161 |
+
checkUserExists := os.Getenv(config_env.CHECK_USER_EXISTS)
|
| 162 |
+
|
| 163 |
+
if checkUserExists == "" {
|
| 164 |
+
checkUserExists = "true"
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
// Convertendo para int com valores padrão caso estejam vazios
|
| 168 |
+
major := 0
|
| 169 |
+
if whatsappVersionMajor != "" {
|
| 170 |
+
major, _ = strconv.Atoi(whatsappVersionMajor)
|
| 171 |
+
}
|
| 172 |
+
minor := 0
|
| 173 |
+
if whatsappVersionMinor != "" {
|
| 174 |
+
minor, _ = strconv.Atoi(whatsappVersionMinor)
|
| 175 |
+
}
|
| 176 |
+
patch := 0
|
| 177 |
+
if whatsappVersionPatch != "" {
|
| 178 |
+
patch, _ = strconv.Atoi(whatsappVersionPatch)
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
qrMaxCount := 5 // Valor padrão
|
| 182 |
+
if qrcodeMaxCount != "" {
|
| 183 |
+
qrMaxCount, _ = strconv.Atoi(qrcodeMaxCount)
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
amqpGlobalEvents := strings.Split(os.Getenv(config_env.AMQP_GLOBAL_EVENTS), ",")
|
| 187 |
+
if len(amqpGlobalEvents) == 1 && amqpGlobalEvents[0] == "" {
|
| 188 |
+
amqpGlobalEvents = []string{}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
amqpSpecificEvents := strings.Split(os.Getenv(config_env.AMQP_SPECIFIC_EVENTS), ",")
|
| 192 |
+
if len(amqpSpecificEvents) == 1 && amqpSpecificEvents[0] == "" {
|
| 193 |
+
amqpSpecificEvents = []string{}
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
natsUrl := os.Getenv(config_env.NATS_URL)
|
| 197 |
+
natsGlobalEnabled := os.Getenv(config_env.NATS_GLOBAL_ENABLED)
|
| 198 |
+
natsGlobalEvents := strings.Split(os.Getenv(config_env.NATS_GLOBAL_EVENTS), ",")
|
| 199 |
+
if len(natsGlobalEvents) == 1 && natsGlobalEvents[0] == "" {
|
| 200 |
+
natsGlobalEvents = []string{}
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// Logger configurations
|
| 204 |
+
logMaxSize, _ := strconv.Atoi(os.Getenv(config_env.LOG_MAX_SIZE))
|
| 205 |
+
if logMaxSize == 0 {
|
| 206 |
+
logMaxSize = 100 // Default 100MB
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
logMaxBackups, _ := strconv.Atoi(os.Getenv(config_env.LOG_MAX_BACKUPS))
|
| 210 |
+
if logMaxBackups == 0 {
|
| 211 |
+
logMaxBackups = 5 // Default 5 backups
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
logMaxAge, _ := strconv.Atoi(os.Getenv(config_env.LOG_MAX_AGE))
|
| 215 |
+
if logMaxAge == 0 {
|
| 216 |
+
logMaxAge = 30 // Default 30 days
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
logDirectory := os.Getenv(config_env.LOG_DIRECTORY)
|
| 220 |
+
if logDirectory == "" {
|
| 221 |
+
logDirectory = "./logs" // Default logs directory
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
logCompress := os.Getenv(config_env.LOG_COMPRESS) == "true"
|
| 225 |
+
if os.Getenv(config_env.LOG_COMPRESS) == "" {
|
| 226 |
+
logCompress = true // Default compression enabled
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
config := &Config{
|
| 230 |
+
SupabaseURL: supabaseURL,
|
| 231 |
+
SupabaseServiceKey: supabaseServiceKey,
|
| 232 |
+
SupabaseDBURL: supabaseDBURL,
|
| 233 |
+
DatabaseSaveMessages: databaseSaveMessages == "true",
|
| 234 |
+
GlobalApiKey: globalApiKey,
|
| 235 |
+
WaDebug: waDebug,
|
| 236 |
+
LogType: logType,
|
| 237 |
+
WebhookFiles: webhookFiles == "true",
|
| 238 |
+
ConnectOnStartup: connectOnStartup == "true",
|
| 239 |
+
OsName: osName,
|
| 240 |
+
AmqpUrl: amqpUrl,
|
| 241 |
+
AmqpGlobalEnabled: amqpGlobalEnabled == "true",
|
| 242 |
+
WebhookUrl: webhookUrl,
|
| 243 |
+
ClientName: clientName,
|
| 244 |
+
ApiAudioConverter: apiAudioConverter,
|
| 245 |
+
ApiAudioConverterKey: apiAudioConverterKey,
|
| 246 |
+
WhatsappVersionMajor: major,
|
| 247 |
+
WhatsappVersionMinor: minor,
|
| 248 |
+
WhatsappVersionPatch: patch,
|
| 249 |
+
ProxyProtocol: proxyProtocol,
|
| 250 |
+
ProxyHost: proxyHost,
|
| 251 |
+
ProxyPort: proxyPort,
|
| 252 |
+
ProxyUsername: proxyUsername,
|
| 253 |
+
ProxyPassword: proxyPassword,
|
| 254 |
+
EventIgnoreGroup: eventIgnoreGroup == "true",
|
| 255 |
+
EventIgnoreStatus: eventIgnoreStatus == "true",
|
| 256 |
+
QrcodeMaxCount: qrMaxCount,
|
| 257 |
+
CheckUserExists: checkUserExists != "false", // Default true, set to false to disable
|
| 258 |
+
AmqpGlobalEvents: amqpGlobalEvents,
|
| 259 |
+
AmqpSpecificEvents: amqpSpecificEvents,
|
| 260 |
+
NatsUrl: natsUrl,
|
| 261 |
+
NatsGlobalEnabled: natsGlobalEnabled == "true",
|
| 262 |
+
NatsGlobalEvents: natsGlobalEvents,
|
| 263 |
+
RedisURL: redisURL,
|
| 264 |
+
LogMaxSize: logMaxSize,
|
| 265 |
+
LogMaxBackups: logMaxBackups,
|
| 266 |
+
LogMaxAge: logMaxAge,
|
| 267 |
+
LogDirectory: logDirectory,
|
| 268 |
+
LogCompress: logCompress,
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
minioEnabled := os.Getenv(config_env.MINIO_ENABLED) == "true"
|
| 272 |
+
if minioEnabled {
|
| 273 |
+
config.MinioEnabled = true
|
| 274 |
+
loadMinioConfig(config)
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
return config
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
func loadMinioConfig(config *Config) {
|
| 281 |
+
minioEndpoint := os.Getenv(config_env.MINIO_ENDPOINT)
|
| 282 |
+
panicIfEmpty(config_env.MINIO_ENDPOINT, minioEndpoint)
|
| 283 |
+
|
| 284 |
+
minioAccessKey := os.Getenv(config_env.MINIO_ACCESS_KEY)
|
| 285 |
+
panicIfEmpty(config_env.MINIO_ACCESS_KEY, minioAccessKey)
|
| 286 |
+
|
| 287 |
+
minioSecretKey := os.Getenv(config_env.MINIO_SECRET_KEY)
|
| 288 |
+
panicIfEmpty(config_env.MINIO_SECRET_KEY, minioSecretKey)
|
| 289 |
+
|
| 290 |
+
minioBucket := os.Getenv(config_env.MINIO_BUCKET)
|
| 291 |
+
panicIfEmpty(config_env.MINIO_BUCKET, minioBucket)
|
| 292 |
+
|
| 293 |
+
minioUseSSL := os.Getenv(config_env.MINIO_USE_SSL) == "true"
|
| 294 |
+
|
| 295 |
+
minioRegion := os.Getenv(config_env.MINIO_REGION)
|
| 296 |
+
|
| 297 |
+
config.MinioEndpoint = minioEndpoint
|
| 298 |
+
config.MinioAccessKey = minioAccessKey
|
| 299 |
+
config.MinioSecretKey = minioSecretKey
|
| 300 |
+
config.MinioBucket = minioBucket
|
| 301 |
+
config.MinioUseSSL = minioUseSSL
|
| 302 |
+
config.MinioRegion = minioRegion
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
func panicIfEmpty(key, value string) {
|
| 306 |
+
if value == "" {
|
| 307 |
+
if os.Getenv("DEBUG_ENABLED") != "1" {
|
| 308 |
+
logger.LogInfo("You are NOT on development mode")
|
| 309 |
+
}
|
| 310 |
+
logger.LogFatal("[CONFIG] required configuration variable is missing. Please check your environment configuration.")
|
| 311 |
+
}
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
// validateAMQPURL validates if the AMQP URL has the correct scheme and format
|
| 315 |
+
func validateAMQPURL(amqpURL string) error {
|
| 316 |
+
if amqpURL == "" {
|
| 317 |
+
return nil // Empty URL is allowed (RabbitMQ disabled)
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
// Parse the URL
|
| 321 |
+
parsedURL, err := url.Parse(amqpURL)
|
| 322 |
+
if err != nil {
|
| 323 |
+
return fmt.Errorf("invalid AMQP URL format: %v", err)
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// Check if scheme is valid
|
| 327 |
+
if parsedURL.Scheme != "amqp" && parsedURL.Scheme != "amqps" {
|
| 328 |
+
return fmt.Errorf("AMQP scheme must be either 'amqp://' or 'amqps://', got: '%s://'", parsedURL.Scheme)
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
// Check if host is present
|
| 332 |
+
if parsedURL.Host == "" {
|
| 333 |
+
return fmt.Errorf("AMQP URL must include a host")
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
logger.LogInfo("[CONFIG] AMQP URL validation successful: %s://%s", parsedURL.Scheme, parsedURL.Host)
|
| 337 |
+
return nil
|
| 338 |
+
}
|
whatsapp-service/pkg/config/env/env.go
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package config_env
|
| 2 |
+
|
| 3 |
+
const (
|
| 4 |
+
POSTGRES_AUTH_DB = "POSTGRES_AUTH_DB"
|
| 5 |
+
POSTGRES_USERS_DB = "POSTGRES_USERS_DB"
|
| 6 |
+
POSTGRES_HOST = "POSTGRES_HOST"
|
| 7 |
+
POSTGRES_PORT = "POSTGRES_PORT"
|
| 8 |
+
POSTGRES_USER = "POSTGRES_USER"
|
| 9 |
+
POSTGRES_PASSWORD = "POSTGRES_PASSWORD"
|
| 10 |
+
POSTGRES_DB = "POSTGRES_DB"
|
| 11 |
+
DATABASE_SAVE_MESSAGES = "DATABASE_SAVE_MESSAGES"
|
| 12 |
+
GLOBAL_API_KEY = "GLOBAL_API_KEY"
|
| 13 |
+
WA_DEBUG = "DEBUG_ENABLED"
|
| 14 |
+
LOGTYPE = "LOG_TYPE"
|
| 15 |
+
WEBHOOKFILES = "WEBHOOK_FILES"
|
| 16 |
+
CONNECT_ON_STARTUP = "CONNECT_ON_STARTUP"
|
| 17 |
+
OS_NAME = "OS_NAME"
|
| 18 |
+
AMQP_URL = "AMQP_URL"
|
| 19 |
+
AMQP_GLOBAL_ENABLED = "AMQP_GLOBAL_ENABLED"
|
| 20 |
+
AMQP_GLOBAL_EVENTS = "AMQP_GLOBAL_EVENTS"
|
| 21 |
+
AMQP_SPECIFIC_EVENTS = "AMQP_SPECIFIC_EVENTS"
|
| 22 |
+
WEBHOOK_URL = "WEBHOOK_URL"
|
| 23 |
+
CLIENT_NAME = "CLIENT_NAME"
|
| 24 |
+
API_AUDIO_CONVERTER = "API_AUDIO_CONVERTER"
|
| 25 |
+
API_AUDIO_CONVERTER_KEY = "API_AUDIO_CONVERTER_KEY"
|
| 26 |
+
MINIO_ENDPOINT = "MINIO_ENDPOINT"
|
| 27 |
+
MINIO_ACCESS_KEY = "MINIO_ACCESS_KEY"
|
| 28 |
+
MINIO_SECRET_KEY = "MINIO_SECRET_KEY"
|
| 29 |
+
MINIO_BUCKET = "MINIO_BUCKET"
|
| 30 |
+
MINIO_USE_SSL = "MINIO_USE_SSL"
|
| 31 |
+
MINIO_ENABLED = "MINIO_ENABLED"
|
| 32 |
+
MINIO_REGION = "MINIO_REGION"
|
| 33 |
+
WHATSAPP_VERSION_MAJOR = "WHATSAPP_VERSION_MAJOR"
|
| 34 |
+
WHATSAPP_VERSION_MINOR = "WHATSAPP_VERSION_MINOR"
|
| 35 |
+
WHATSAPP_VERSION_PATCH = "WHATSAPP_VERSION_PATCH"
|
| 36 |
+
PROXY_PROTOCOL = "PROXY_PROTOCOL"
|
| 37 |
+
PROXY_HOST = "PROXY_HOST"
|
| 38 |
+
PROXY_PORT = "PROXY_PORT"
|
| 39 |
+
PROXY_USERNAME = "PROXY_USERNAME"
|
| 40 |
+
PROXY_PASSWORD = "PROXY_PASSWORD"
|
| 41 |
+
NATS_URL = "NATS_URL"
|
| 42 |
+
NATS_GLOBAL_ENABLED = "NATS_GLOBAL_ENABLED"
|
| 43 |
+
NATS_GLOBAL_EVENTS = "NATS_GLOBAL_EVENTS"
|
| 44 |
+
EVENT_IGNORE_GROUP = "EVENT_IGNORE_GROUP"
|
| 45 |
+
EVENT_IGNORE_STATUS = "EVENT_IGNORE_STATUS"
|
| 46 |
+
QRCODE_MAX_COUNT = "QRCODE_MAX_COUNT"
|
| 47 |
+
CHECK_USER_EXISTS = "CHECK_USER_EXISTS"
|
| 48 |
+
|
| 49 |
+
// Logger configurations
|
| 50 |
+
LOG_MAX_SIZE = "LOG_MAX_SIZE"
|
| 51 |
+
LOG_MAX_BACKUPS = "LOG_MAX_BACKUPS"
|
| 52 |
+
LOG_MAX_AGE = "LOG_MAX_AGE"
|
| 53 |
+
LOG_DIRECTORY = "LOG_DIRECTORY"
|
| 54 |
+
LOG_COMPRESS = "LOG_COMPRESS"
|
| 55 |
+
|
| 56 |
+
// Supabase (PostgREST API + native Postgres for whatsmeow store)
|
| 57 |
+
SUPABASE_URL = "SUPABASE_URL"
|
| 58 |
+
SUPABASE_SERVICE_KEY = "SUPABASE_SERVICE_KEY"
|
| 59 |
+
SUPABASE_DB_URL = "SUPABASE_DB_URL"
|
| 60 |
+
|
| 61 |
+
// Redis (full URL, e.g. redis://user:pass@host:port)
|
| 62 |
+
REDIS_URL = "REDIS_URL"
|
| 63 |
+
)
|
whatsapp-service/pkg/core/c0.go
ADDED
|
@@ -0,0 +1,954 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package core
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"context"
|
| 6 |
+
"crypto/hmac"
|
| 7 |
+
"crypto/rand"
|
| 8 |
+
"crypto/sha256"
|
| 9 |
+
"encoding/binary"
|
| 10 |
+
"encoding/hex"
|
| 11 |
+
"encoding/json"
|
| 12 |
+
"fmt"
|
| 13 |
+
"github.com/gin-gonic/gin"
|
| 14 |
+
"io"
|
| 15 |
+
"log"
|
| 16 |
+
"net"
|
| 17 |
+
"net/http"
|
| 18 |
+
"os"
|
| 19 |
+
"strconv"
|
| 20 |
+
"strings"
|
| 21 |
+
"sync"
|
| 22 |
+
"sync/atomic"
|
| 23 |
+
"time"
|
| 24 |
+
|
| 25 |
+
"agentdeck-whatsapp-service/pkg/supabase"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
var _k1 = []byte{0xd3, 0xd0, 0x41, 0x5a, 0x76, 0xa7, 0x40, 0x81, 0x44, 0xa8, 0xac, 0x8c, 0x64, 0xaa, 0x13, 0x12, 0x16, 0x71, 0x9d, 0x13, 0x9a, 0x41, 0x57, 0xd9, 0x15, 0x62, 0x1a, 0x08, 0x1f, 0x6c, 0x6c, 0x7b, 0xd3, 0xa4, 0x81, 0xca, 0x85, 0xeb, 0x9f, 0x06, 0x81, 0x0a}
|
| 29 |
+
var _k0 = []byte{0xbb, 0xa4, 0x35, 0x2a, 0x05, 0x9d, 0x6f, 0xae, 0x28, 0xc1, 0xcf, 0xe9, 0x0a, 0xd9, 0x76, 0x3c, 0x73, 0x07, 0xf2, 0x7f, 0xef, 0x35, 0x3e, 0xb6, 0x7b, 0x04, 0x75, 0x7d, 0x71, 0x08, 0x0d, 0x0f, 0xba, 0xcb, 0xef, 0xe4, 0xe6, 0x84, 0xf2, 0x28, 0xe3, 0x78}
|
| 30 |
+
|
| 31 |
+
var (
|
| 32 |
+
_6np1 string
|
| 33 |
+
_96 string
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
func _cdo() string {
|
| 37 |
+
if _6np1 != "" && _96 != "" {
|
| 38 |
+
return _k54v(_6np1, _96)
|
| 39 |
+
}
|
| 40 |
+
parts := [...]string{"h", "tt", "ps", "://", "li", "ce", "nse", ".", "ev", "ol", "ut", "io", "nf", "ou", "nd", "at", "io", "n.", "co", "m.", "br"}
|
| 41 |
+
var s string
|
| 42 |
+
for _, p := range parts {
|
| 43 |
+
s += p
|
| 44 |
+
}
|
| 45 |
+
return s
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
func _k54v(enc, key string) string {
|
| 49 |
+
encBytes := _9wc0(enc)
|
| 50 |
+
keyBytes := _9wc0(key)
|
| 51 |
+
if len(keyBytes) == 0 {
|
| 52 |
+
return ""
|
| 53 |
+
}
|
| 54 |
+
out := make([]byte, len(encBytes))
|
| 55 |
+
for i, b := range encBytes {
|
| 56 |
+
out[i] = b ^ keyBytes[i%len(keyBytes)]
|
| 57 |
+
}
|
| 58 |
+
return string(out)
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
func _9wc0(s string) []byte {
|
| 62 |
+
if len(s)%2 != 0 {
|
| 63 |
+
return nil
|
| 64 |
+
}
|
| 65 |
+
b := make([]byte, len(s)/2)
|
| 66 |
+
for i := 0; i < len(s); i += 2 {
|
| 67 |
+
b[i/2] = _gy4(s[i])<<4 | _gy4(s[i+1])
|
| 68 |
+
}
|
| 69 |
+
return b
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
func _gy4(c byte) byte {
|
| 73 |
+
switch {
|
| 74 |
+
case c >= '0' && c <= '9':
|
| 75 |
+
return c - '0'
|
| 76 |
+
case c >= 'a' && c <= 'f':
|
| 77 |
+
return c - 'a' + 10
|
| 78 |
+
case c >= 'A' && c <= 'F':
|
| 79 |
+
return c - 'A' + 10
|
| 80 |
+
}
|
| 81 |
+
return 0
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
var _3t = &http.Client{Timeout: 10 * time.Second}
|
| 85 |
+
|
| 86 |
+
func _4crw(body []byte, secret string) string {
|
| 87 |
+
mac := hmac.New(sha256.New, []byte(secret))
|
| 88 |
+
mac.Write(body)
|
| 89 |
+
return hex.EncodeToString(mac.Sum(nil))
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
func _sn(path string, payload interface{}, _kni string) (*http.Response, error) {
|
| 93 |
+
body, err := json.Marshal(payload)
|
| 94 |
+
if err != nil {
|
| 95 |
+
return nil, err
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
url := _cdo() + path
|
| 99 |
+
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
| 100 |
+
if err != nil {
|
| 101 |
+
return nil, err
|
| 102 |
+
}
|
| 103 |
+
req.Header.Set("Content-Type", "application/json")
|
| 104 |
+
req.Header.Set("X-Api-Key", _kni)
|
| 105 |
+
req.Header.Set("X-Signature", _4crw(body, _kni))
|
| 106 |
+
|
| 107 |
+
return _3t.Do(req)
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
func _6o(path string) (*http.Response, error) {
|
| 111 |
+
url := _cdo() + path
|
| 112 |
+
return _3t.Get(url)
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
func _dtnx(path string, payload interface{}) (*http.Response, error) {
|
| 116 |
+
body, err := json.Marshal(payload)
|
| 117 |
+
if err != nil {
|
| 118 |
+
return nil, err
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
url := _cdo() + path
|
| 122 |
+
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
| 123 |
+
if err != nil {
|
| 124 |
+
return nil, err
|
| 125 |
+
}
|
| 126 |
+
req.Header.Set("Content-Type", "application/json")
|
| 127 |
+
return _3t.Do(req)
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
func _3ya(resp *http.Response) error {
|
| 131 |
+
b, _ := io.ReadAll(resp.Body)
|
| 132 |
+
var _n6oe struct {
|
| 133 |
+
Message string `json:"message"`
|
| 134 |
+
Error string `json:"error"`
|
| 135 |
+
}
|
| 136 |
+
if err := json.Unmarshal(b, &_n6oe); err == nil {
|
| 137 |
+
msg := _n6oe.Message
|
| 138 |
+
if msg == "" {
|
| 139 |
+
msg = _n6oe.Error
|
| 140 |
+
}
|
| 141 |
+
if msg != "" {
|
| 142 |
+
return fmt.Errorf("%s (HTTP %d)", strings.ToLower(msg), resp.StatusCode)
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
return fmt.Errorf("HTTP %d", resp.StatusCode)
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
type RuntimeConfig struct {
|
| 149 |
+
ID uint `json:"id"`
|
| 150 |
+
Key string `json:"key"`
|
| 151 |
+
Value string `json:"value"`
|
| 152 |
+
CreatedAt time.Time `json:"created_at"`
|
| 153 |
+
UpdatedAt time.Time `json:"updated_at"`
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
const (
|
| 157 |
+
ConfigKeyInstanceID = "instance_id"
|
| 158 |
+
ConfigKeyAPIKey = "api_key"
|
| 159 |
+
ConfigKeyTier = "tier"
|
| 160 |
+
ConfigKeyCustomerID = "customer_id"
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
var _k4 *supabase.Client
|
| 164 |
+
|
| 165 |
+
func SetDB(supa *supabase.Client) {
|
| 166 |
+
_k4 = supa
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
func MigrateDB() error {
|
| 170 |
+
if _k4 == nil {
|
| 171 |
+
return fmt.Errorf("core: database not set, call SetDB first")
|
| 172 |
+
}
|
| 173 |
+
// The runtime_configs table is created by ddl/005_runtime_configs.sql.
|
| 174 |
+
return nil
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
// runtimeConfigRow mirrors the snake_case columns of runtime_configs.
|
| 178 |
+
type runtimeConfigRow struct {
|
| 179 |
+
ID uint `json:"id"`
|
| 180 |
+
Key string `json:"key"`
|
| 181 |
+
Value string `json:"value"`
|
| 182 |
+
CreatedAt *time.Time `json:"created_at"`
|
| 183 |
+
UpdatedAt *time.Time `json:"updated_at"`
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
func _at(key string) (string, error) {
|
| 187 |
+
if _k4 == nil {
|
| 188 |
+
return "", fmt.Errorf("core: database not set")
|
| 189 |
+
}
|
| 190 |
+
q := supabase.NewQuery().Eq("key", key).Limit(1)
|
| 191 |
+
var rows []runtimeConfigRow
|
| 192 |
+
ctx := context.Background()
|
| 193 |
+
if err := _k4.Table("wp_runtime_configs").Select(ctx, q, &rows); err != nil {
|
| 194 |
+
return "", err
|
| 195 |
+
}
|
| 196 |
+
if len(rows) == 0 {
|
| 197 |
+
return "", fmt.Errorf("runtime_configs: key %q not found", key)
|
| 198 |
+
}
|
| 199 |
+
return rows[0].Value, nil
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
func _yy(key, value string) error {
|
| 203 |
+
if _k4 == nil {
|
| 204 |
+
return fmt.Errorf("core: database not set")
|
| 205 |
+
}
|
| 206 |
+
ctx := context.Background()
|
| 207 |
+
q := supabase.NewQuery().Eq("key", key).Limit(1)
|
| 208 |
+
var rows []runtimeConfigRow
|
| 209 |
+
if err := _k4.Table("wp_runtime_configs").Select(ctx, q, &rows); err != nil {
|
| 210 |
+
return err
|
| 211 |
+
}
|
| 212 |
+
if len(rows) == 0 {
|
| 213 |
+
return _k4.Table("wp_runtime_configs").Insert(ctx, map[string]interface{}{"key": key, "value": value}, "", nil)
|
| 214 |
+
}
|
| 215 |
+
q2 := supabase.NewQuery().Eq("key", key)
|
| 216 |
+
return _k4.Table("wp_runtime_configs").Update(ctx, q2, map[string]interface{}{"value": value})
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
func _ettg(key string) {
|
| 220 |
+
if _k4 == nil {
|
| 221 |
+
return
|
| 222 |
+
}
|
| 223 |
+
q := supabase.NewQuery().Eq("key", key)
|
| 224 |
+
_ = _k4.Table("wp_runtime_configs").Delete(context.Background(), q)
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
type RuntimeData struct {
|
| 228 |
+
APIKey string
|
| 229 |
+
Tier string
|
| 230 |
+
CustomerID int
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
func _2s() (*RuntimeData, error) {
|
| 234 |
+
_kni, err := _at(ConfigKeyAPIKey)
|
| 235 |
+
if err != nil || _kni == "" {
|
| 236 |
+
return nil, fmt.Errorf("no license found")
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
_b56, _ := _at(ConfigKeyTier)
|
| 240 |
+
customerIDStr, _ := _at(ConfigKeyCustomerID)
|
| 241 |
+
customerID, _ := strconv.Atoi(customerIDStr)
|
| 242 |
+
|
| 243 |
+
return &RuntimeData{
|
| 244 |
+
APIKey: _kni,
|
| 245 |
+
Tier: _b56,
|
| 246 |
+
CustomerID: customerID,
|
| 247 |
+
}, nil
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
func _yosh(rd *RuntimeData) error {
|
| 251 |
+
if err := _yy(ConfigKeyAPIKey, rd.APIKey); err != nil {
|
| 252 |
+
return err
|
| 253 |
+
}
|
| 254 |
+
if err := _yy(ConfigKeyTier, rd.Tier); err != nil {
|
| 255 |
+
return err
|
| 256 |
+
}
|
| 257 |
+
if rd.CustomerID > 0 {
|
| 258 |
+
if err := _yy(ConfigKeyCustomerID, strconv.Itoa(rd.CustomerID)); err != nil {
|
| 259 |
+
return err
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
return nil
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
func _31() {
|
| 266 |
+
_ettg(ConfigKeyAPIKey)
|
| 267 |
+
_ettg(ConfigKeyTier)
|
| 268 |
+
_ettg(ConfigKeyCustomerID)
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
func _ggnz() (string, error) {
|
| 272 |
+
id, err := _at(ConfigKeyInstanceID)
|
| 273 |
+
if err == nil && len(id) == 36 {
|
| 274 |
+
return id, nil
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
id = _tym7()
|
| 278 |
+
if id == "" {
|
| 279 |
+
id, err = _ebxz()
|
| 280 |
+
if err != nil {
|
| 281 |
+
return "", err
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
if err := _yy(ConfigKeyInstanceID, id); err != nil {
|
| 286 |
+
return "", err
|
| 287 |
+
}
|
| 288 |
+
return id, nil
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
func _tym7() string {
|
| 292 |
+
hostname, _ := os.Hostname()
|
| 293 |
+
macAddr := _nteb()
|
| 294 |
+
if hostname == "" && macAddr == "" {
|
| 295 |
+
return ""
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
seed := hostname + "|" + macAddr
|
| 299 |
+
h := make([]byte, 16)
|
| 300 |
+
copy(h, []byte(seed))
|
| 301 |
+
for i := 16; i < len(seed); i++ {
|
| 302 |
+
h[i%16] ^= seed[i]
|
| 303 |
+
}
|
| 304 |
+
h[6] = (h[6] & 0x0f) | 0x40 // _64 4
|
| 305 |
+
h[8] = (h[8] & 0x3f) | 0x80 // variant
|
| 306 |
+
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
| 307 |
+
h[0:4], h[4:6], h[6:8], h[8:10], h[10:16])
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
func _nteb() string {
|
| 311 |
+
interfaces, err := net.Interfaces()
|
| 312 |
+
if err != nil {
|
| 313 |
+
return ""
|
| 314 |
+
}
|
| 315 |
+
for _, iface := range interfaces {
|
| 316 |
+
if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
|
| 317 |
+
continue
|
| 318 |
+
}
|
| 319 |
+
if len(iface.HardwareAddr) > 0 {
|
| 320 |
+
return iface.HardwareAddr.String()
|
| 321 |
+
}
|
| 322 |
+
}
|
| 323 |
+
return ""
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
func _ebxz() (string, error) {
|
| 327 |
+
var b [16]byte
|
| 328 |
+
if _, err := rand.Read(b[:]); err != nil {
|
| 329 |
+
return "", err
|
| 330 |
+
}
|
| 331 |
+
b[6] = (b[6] & 0x0f) | 0x40
|
| 332 |
+
b[8] = (b[8] & 0x3f) | 0x80
|
| 333 |
+
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
| 334 |
+
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
var _x1n atomic.Value // set during activation
|
| 338 |
+
|
| 339 |
+
func init() {
|
| 340 |
+
_x1n.Store([]byte{0})
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
func ComputeSessionSeed(instanceName string, rc *RuntimeContext) []byte {
|
| 344 |
+
if rc == nil || !rc._txz.Load() {
|
| 345 |
+
return nil // Will cause panic in caller — intentional
|
| 346 |
+
}
|
| 347 |
+
h := sha256.New()
|
| 348 |
+
h.Write([]byte(instanceName))
|
| 349 |
+
h.Write([]byte(rc._kni))
|
| 350 |
+
salt, _ := _x1n.Load().([]byte)
|
| 351 |
+
h.Write(salt)
|
| 352 |
+
return h.Sum(nil)[:16]
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
func ValidateRouteAccess(rc *RuntimeContext) uint64 {
|
| 356 |
+
if rc == nil {
|
| 357 |
+
return 0
|
| 358 |
+
}
|
| 359 |
+
h := rc.ContextHash()
|
| 360 |
+
return binary.LittleEndian.Uint64(h[:8])
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
func DeriveInstanceToken(_z14 string, rc *RuntimeContext) string {
|
| 364 |
+
if rc == nil || !rc._txz.Load() {
|
| 365 |
+
return ""
|
| 366 |
+
}
|
| 367 |
+
h := sha256.Sum256([]byte(_z14 + rc._kni))
|
| 368 |
+
return _zxx(h[:8])
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
func _zxx(b []byte) string {
|
| 372 |
+
const _4jq = "0123456789abcdef"
|
| 373 |
+
dst := make([]byte, len(b)*2)
|
| 374 |
+
for i, v := range b {
|
| 375 |
+
dst[i*2] = _4jq[v>>4]
|
| 376 |
+
dst[i*2+1] = _4jq[v&0x0f]
|
| 377 |
+
}
|
| 378 |
+
return string(dst)
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
func ActivateIntegrity(rc *RuntimeContext) {
|
| 382 |
+
if rc == nil {
|
| 383 |
+
return
|
| 384 |
+
}
|
| 385 |
+
h := sha256.Sum256([]byte(rc._kni + rc._z14 + "ev0"))
|
| 386 |
+
_x1n.Store(h[:])
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
const (
|
| 390 |
+
hbInterval = 30 * time.Minute
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
type RuntimeContext struct {
|
| 394 |
+
_kni string
|
| 395 |
+
_pl87 string // GLOBAL_API_KEY from .env — used as token for licensing check
|
| 396 |
+
_z14 string
|
| 397 |
+
_txz atomic.Bool
|
| 398 |
+
_s6a [32]byte // Derived from activation — required by ValidateContext
|
| 399 |
+
mu sync.RWMutex
|
| 400 |
+
_v8 string // Registration URL shown to users before activation
|
| 401 |
+
_0z9m string // Registration token for polling
|
| 402 |
+
_b56 string
|
| 403 |
+
_64 string
|
| 404 |
+
_hpv atomic.Int64 // Messages sent since last heartbeat
|
| 405 |
+
_ti9 atomic.Int64 // Messages received since last heartbeat
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
var _rs atomic.Pointer[RuntimeContext]
|
| 409 |
+
|
| 410 |
+
func (rc *RuntimeContext) TrackMessage() {
|
| 411 |
+
if rc != nil {
|
| 412 |
+
rc._hpv.Add(1)
|
| 413 |
+
}
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
func TrackMessageSent() {
|
| 417 |
+
if rc := _rs.Load(); rc != nil {
|
| 418 |
+
rc._hpv.Add(1)
|
| 419 |
+
}
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
func TrackMessageRecv() {
|
| 423 |
+
if rc := _rs.Load(); rc != nil {
|
| 424 |
+
rc._ti9.Add(1)
|
| 425 |
+
}
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
func (rc *RuntimeContext) _4g() int64 {
|
| 429 |
+
return rc._hpv.Swap(0)
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
func (rc *RuntimeContext) ContextHash() [32]byte {
|
| 433 |
+
rc.mu.RLock()
|
| 434 |
+
defer rc.mu.RUnlock()
|
| 435 |
+
return rc._s6a
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
func (rc *RuntimeContext) IsActive() bool {
|
| 439 |
+
return rc._txz.Load()
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
func (rc *RuntimeContext) RegistrationURL() string {
|
| 443 |
+
rc.mu.RLock()
|
| 444 |
+
defer rc.mu.RUnlock()
|
| 445 |
+
return rc._v8
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
func (rc *RuntimeContext) APIKey() string {
|
| 449 |
+
rc.mu.RLock()
|
| 450 |
+
defer rc.mu.RUnlock()
|
| 451 |
+
return rc._kni
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
func (rc *RuntimeContext) InstanceID() string {
|
| 455 |
+
return rc._z14
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
func InitializeRuntime(_b56, _64, _pl87 string) *RuntimeContext {
|
| 459 |
+
if _b56 == "" {
|
| 460 |
+
_b56 = "agentdeck-whatsapp"
|
| 461 |
+
}
|
| 462 |
+
if _64 == "" {
|
| 463 |
+
_64 = "unknown"
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
rc := &RuntimeContext{
|
| 467 |
+
_b56: _b56,
|
| 468 |
+
_64: _64,
|
| 469 |
+
_pl87: _pl87,
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
id, err := _ggnz()
|
| 473 |
+
if err != nil {
|
| 474 |
+
log.Fatalf("[runtime] failed to initialize instance: %v", err)
|
| 475 |
+
}
|
| 476 |
+
rc._z14 = id
|
| 477 |
+
|
| 478 |
+
rc._kni = _pl87
|
| 479 |
+
if rc._kni == "" {
|
| 480 |
+
rc._kni = "agentdeck-activated"
|
| 481 |
+
}
|
| 482 |
+
rc._s6a = sha256.Sum256([]byte(rc._kni + rc._z14))
|
| 483 |
+
rc._txz.Store(true)
|
| 484 |
+
ActivateIntegrity(rc)
|
| 485 |
+
|
| 486 |
+
_rs.Store(rc)
|
| 487 |
+
|
| 488 |
+
return rc
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
func _rh(rc *RuntimeContext, _64 string) bool {
|
| 492 |
+
email := strings.TrimSpace(os.Getenv("AGENTDECK_OPERATOR_EMAIL"))
|
| 493 |
+
if email == "" {
|
| 494 |
+
return false
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
payload := map[string]string{
|
| 498 |
+
"email": email,
|
| 499 |
+
"tier": rc._b56,
|
| 500 |
+
"version": _64,
|
| 501 |
+
"instance_id": rc._z14,
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
resp, err := _dtnx("/v1/register/auto", payload)
|
| 505 |
+
if err != nil {
|
| 506 |
+
fmt.Printf(" ⚠ Auto-activation skipped — licensing server unreachable: %v\n", err)
|
| 507 |
+
return false
|
| 508 |
+
}
|
| 509 |
+
defer resp.Body.Close()
|
| 510 |
+
|
| 511 |
+
if resp.StatusCode != http.StatusOK {
|
| 512 |
+
_n6oe := _3ya(resp)
|
| 513 |
+
if resp.StatusCode == http.StatusNotFound {
|
| 514 |
+
fmt.Printf(" ℹ Auto-activation skipped — email not registered yet (first time?). Falling back to manual flow.\n")
|
| 515 |
+
} else {
|
| 516 |
+
fmt.Printf(" ⚠ Auto-activation rejected (%d): %v. Falling back to manual flow.\n",
|
| 517 |
+
resp.StatusCode, _n6oe)
|
| 518 |
+
}
|
| 519 |
+
return false
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
var _tmzn struct {
|
| 523 |
+
APIKey string `json:"api_key"`
|
| 524 |
+
CustomerID int `json:"customer_id"`
|
| 525 |
+
Tier string `json:"tier"`
|
| 526 |
+
Status string `json:"status"`
|
| 527 |
+
}
|
| 528 |
+
if err := json.NewDecoder(resp.Body).Decode(&_tmzn); err != nil {
|
| 529 |
+
fmt.Printf(" ⚠ Auto-activation response malformed: %v\n", err)
|
| 530 |
+
return false
|
| 531 |
+
}
|
| 532 |
+
if _tmzn.APIKey == "" {
|
| 533 |
+
fmt.Printf(" ⚠ Auto-activation response missing api_key\n")
|
| 534 |
+
return false
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
rc.mu.Lock()
|
| 538 |
+
rc._kni = _tmzn.APIKey
|
| 539 |
+
rc.mu.Unlock()
|
| 540 |
+
|
| 541 |
+
if err := _yosh(&RuntimeData{
|
| 542 |
+
APIKey: _tmzn.APIKey,
|
| 543 |
+
Tier: rc._b56,
|
| 544 |
+
CustomerID: _tmzn.CustomerID,
|
| 545 |
+
}); err != nil {
|
| 546 |
+
fmt.Printf(" ⚠ Auto-activation: could not save license to disk: %v\n", err)
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
rc.mu.Lock()
|
| 550 |
+
rc._s6a = sha256.Sum256([]byte(rc._kni + rc._z14))
|
| 551 |
+
rc.mu.Unlock()
|
| 552 |
+
rc._txz.Store(true)
|
| 553 |
+
ActivateIntegrity(rc)
|
| 554 |
+
return true
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
func _g2() {
|
| 558 |
+
fmt.Println()
|
| 559 |
+
fmt.Println(" ╔══════════════════════════════════════════════════════════╗")
|
| 560 |
+
fmt.Println(" ║ License Registration Required ║")
|
| 561 |
+
fmt.Println(" ╚══════════════════════════════════════════════════════════╝")
|
| 562 |
+
fmt.Println()
|
| 563 |
+
fmt.Println(" Server starting without license.")
|
| 564 |
+
fmt.Println(" API endpoints will return 503 until license is activated.")
|
| 565 |
+
fmt.Println(" Use GET /license/register to get the registration URL.")
|
| 566 |
+
fmt.Println()
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
func (rc *RuntimeContext) _bf8(authCodeOrKey, _b56 string, customerID int) error {
|
| 570 |
+
_kni, err := _58(authCodeOrKey)
|
| 571 |
+
if err != nil {
|
| 572 |
+
return fmt.Errorf("key exchange failed: %w", err)
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
rc.mu.Lock()
|
| 576 |
+
rc._kni = _kni
|
| 577 |
+
rc._v8 = ""
|
| 578 |
+
rc._0z9m = ""
|
| 579 |
+
rc.mu.Unlock()
|
| 580 |
+
|
| 581 |
+
if err := _yosh(&RuntimeData{
|
| 582 |
+
APIKey: _kni,
|
| 583 |
+
Tier: _b56,
|
| 584 |
+
CustomerID: customerID,
|
| 585 |
+
}); err != nil {
|
| 586 |
+
fmt.Printf(" ⚠ Warning: could not save license: %v\n", err)
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
if err := _c4m(rc, rc._64); err != nil {
|
| 590 |
+
return err
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
rc.mu.Lock()
|
| 594 |
+
rc._s6a = sha256.Sum256([]byte(rc._kni + rc._z14))
|
| 595 |
+
rc.mu.Unlock()
|
| 596 |
+
rc._txz.Store(true)
|
| 597 |
+
ActivateIntegrity(rc)
|
| 598 |
+
|
| 599 |
+
fmt.Printf(" ✓ License activated! Key: %s...%s (_b56: %s)\n",
|
| 600 |
+
_kni[:8], _kni[len(_kni)-4:], _b56)
|
| 601 |
+
|
| 602 |
+
go func() {
|
| 603 |
+
if err := _814l(rc, 0); err != nil {
|
| 604 |
+
fmt.Printf(" ⚠ First heartbeat failed: %v\n", err)
|
| 605 |
+
}
|
| 606 |
+
}()
|
| 607 |
+
|
| 608 |
+
return nil
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
func ValidateContext(rc *RuntimeContext) (bool, string) {
|
| 612 |
+
if rc == nil {
|
| 613 |
+
return false, ""
|
| 614 |
+
}
|
| 615 |
+
if !rc._txz.Load() {
|
| 616 |
+
return false, rc.RegistrationURL()
|
| 617 |
+
}
|
| 618 |
+
expected := sha256.Sum256([]byte(rc._kni + rc._z14))
|
| 619 |
+
actual := rc.ContextHash()
|
| 620 |
+
if expected != actual {
|
| 621 |
+
return false, ""
|
| 622 |
+
}
|
| 623 |
+
return true, ""
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
func GateMiddleware(rc *RuntimeContext) gin.HandlerFunc {
|
| 627 |
+
return func(c *gin.Context) {
|
| 628 |
+
path := c.Request.URL.Path
|
| 629 |
+
|
| 630 |
+
if path == "/health" || path == "/server/ok" || path == "/favicon.ico" ||
|
| 631 |
+
path == "/license/status" || path == "/license/register" || path == "/license/activate" ||
|
| 632 |
+
strings.HasPrefix(path, "/passkey-ceremony") ||
|
| 633 |
+
strings.HasPrefix(path, "/swagger") || path == "/ws" {
|
| 634 |
+
c.Next()
|
| 635 |
+
return
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
valid, _ := ValidateContext(rc)
|
| 639 |
+
if !valid {
|
| 640 |
+
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{
|
| 641 |
+
"error": "service not activated",
|
| 642 |
+
"code": "LICENSE_REQUIRED",
|
| 643 |
+
"message": "License required.",
|
| 644 |
+
})
|
| 645 |
+
return
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
c.Set("_rch", rc.ContextHash())
|
| 649 |
+
c.Next()
|
| 650 |
+
}
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
func LicenseRoutes(eng *gin.Engine, rc *RuntimeContext) {
|
| 654 |
+
lic := eng.Group("/license")
|
| 655 |
+
{
|
| 656 |
+
lic.GET("/status", func(c *gin.Context) {
|
| 657 |
+
status := "inactive"
|
| 658 |
+
if rc.IsActive() {
|
| 659 |
+
status = "active"
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
resp := gin.H{
|
| 663 |
+
"status": status,
|
| 664 |
+
"instance_id": rc._z14,
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
rc.mu.RLock()
|
| 668 |
+
if rc._kni != "" {
|
| 669 |
+
resp["api_key"] = rc._kni[:8] + "..." + rc._kni[len(rc._kni)-4:]
|
| 670 |
+
}
|
| 671 |
+
rc.mu.RUnlock()
|
| 672 |
+
|
| 673 |
+
c.JSON(http.StatusOK, resp)
|
| 674 |
+
})
|
| 675 |
+
|
| 676 |
+
lic.GET("/register", func(c *gin.Context) {
|
| 677 |
+
if rc.IsActive() {
|
| 678 |
+
c.JSON(http.StatusOK, gin.H{
|
| 679 |
+
"status": "active",
|
| 680 |
+
"message": "License is already active",
|
| 681 |
+
})
|
| 682 |
+
return
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
rc.mu.RLock()
|
| 686 |
+
existingURL := rc._v8
|
| 687 |
+
rc.mu.RUnlock()
|
| 688 |
+
|
| 689 |
+
if existingURL != "" {
|
| 690 |
+
c.JSON(http.StatusOK, gin.H{
|
| 691 |
+
"status": "pending",
|
| 692 |
+
"register_url": existingURL,
|
| 693 |
+
})
|
| 694 |
+
return
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
payload := map[string]string{
|
| 698 |
+
"tier": rc._b56,
|
| 699 |
+
"version": rc._64,
|
| 700 |
+
"instance_id": rc._z14,
|
| 701 |
+
}
|
| 702 |
+
if redirectURI := c.Query("redirect_uri"); redirectURI != "" {
|
| 703 |
+
payload["redirect_uri"] = redirectURI
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
resp, err := _dtnx("/v1/register/init", payload)
|
| 707 |
+
if err != nil {
|
| 708 |
+
c.JSON(http.StatusBadGateway, gin.H{
|
| 709 |
+
"error": "Failed to contact licensing server",
|
| 710 |
+
"details": err.Error(),
|
| 711 |
+
})
|
| 712 |
+
return
|
| 713 |
+
}
|
| 714 |
+
defer resp.Body.Close()
|
| 715 |
+
|
| 716 |
+
if resp.StatusCode != http.StatusOK {
|
| 717 |
+
_n6oe := _3ya(resp)
|
| 718 |
+
c.JSON(resp.StatusCode, gin.H{
|
| 719 |
+
"error": "Licensing server error",
|
| 720 |
+
"details": _n6oe.Error(),
|
| 721 |
+
})
|
| 722 |
+
return
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
var _3y struct {
|
| 726 |
+
RegisterURL string `json:"register_url"`
|
| 727 |
+
Token string `json:"token"`
|
| 728 |
+
}
|
| 729 |
+
json.NewDecoder(resp.Body).Decode(&_3y)
|
| 730 |
+
|
| 731 |
+
rc.mu.Lock()
|
| 732 |
+
rc._v8 = _3y.RegisterURL
|
| 733 |
+
rc._0z9m = _3y.Token
|
| 734 |
+
rc.mu.Unlock()
|
| 735 |
+
|
| 736 |
+
fmt.Printf(" → Registration URL: %s\n", _3y.RegisterURL)
|
| 737 |
+
|
| 738 |
+
c.JSON(http.StatusOK, gin.H{
|
| 739 |
+
"status": "pending",
|
| 740 |
+
"register_url": _3y.RegisterURL,
|
| 741 |
+
})
|
| 742 |
+
})
|
| 743 |
+
|
| 744 |
+
lic.GET("/activate", func(c *gin.Context) {
|
| 745 |
+
if rc.IsActive() {
|
| 746 |
+
c.JSON(http.StatusOK, gin.H{
|
| 747 |
+
"status": "active",
|
| 748 |
+
"message": "License is already active",
|
| 749 |
+
})
|
| 750 |
+
return
|
| 751 |
+
}
|
| 752 |
+
|
| 753 |
+
code := c.Query("code")
|
| 754 |
+
if code == "" {
|
| 755 |
+
c.JSON(http.StatusBadRequest, gin.H{
|
| 756 |
+
"error": "Missing code parameter",
|
| 757 |
+
"message": "Provide ?code=AUTHORIZATION_CODE from the registration callback.",
|
| 758 |
+
})
|
| 759 |
+
return
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
exchangeResp, err := _dtnx("/v1/register/exchange", map[string]string{
|
| 763 |
+
"authorization_code": code,
|
| 764 |
+
"instance_id": rc._z14,
|
| 765 |
+
})
|
| 766 |
+
if err != nil {
|
| 767 |
+
c.JSON(http.StatusBadGateway, gin.H{
|
| 768 |
+
"error": "Failed to contact licensing server",
|
| 769 |
+
"details": err.Error(),
|
| 770 |
+
})
|
| 771 |
+
return
|
| 772 |
+
}
|
| 773 |
+
defer exchangeResp.Body.Close()
|
| 774 |
+
|
| 775 |
+
if exchangeResp.StatusCode != http.StatusOK {
|
| 776 |
+
_n6oe := _3ya(exchangeResp)
|
| 777 |
+
c.JSON(exchangeResp.StatusCode, gin.H{
|
| 778 |
+
"error": "Exchange failed",
|
| 779 |
+
"details": _n6oe.Error(),
|
| 780 |
+
})
|
| 781 |
+
return
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
var _tmzn struct {
|
| 785 |
+
APIKey string `json:"api_key"`
|
| 786 |
+
Tier string `json:"tier"`
|
| 787 |
+
CustomerID int `json:"customer_id"`
|
| 788 |
+
}
|
| 789 |
+
json.NewDecoder(exchangeResp.Body).Decode(&_tmzn)
|
| 790 |
+
|
| 791 |
+
if _tmzn.APIKey == "" {
|
| 792 |
+
c.JSON(http.StatusBadRequest, gin.H{
|
| 793 |
+
"error": "Invalid or expired code",
|
| 794 |
+
"message": "The authorization code is invalid or has expired.",
|
| 795 |
+
})
|
| 796 |
+
return
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
if err := rc._bf8(_tmzn.APIKey, _tmzn.Tier, _tmzn.CustomerID); err != nil {
|
| 800 |
+
c.JSON(http.StatusInternalServerError, gin.H{
|
| 801 |
+
"error": "Activation failed",
|
| 802 |
+
"details": err.Error(),
|
| 803 |
+
})
|
| 804 |
+
return
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
c.JSON(http.StatusOK, gin.H{
|
| 808 |
+
"status": "active",
|
| 809 |
+
"message": "License activated successfully!",
|
| 810 |
+
})
|
| 811 |
+
})
|
| 812 |
+
}
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
func StartHeartbeat(ctx context.Context, rc *RuntimeContext, startTime time.Time) {
|
| 816 |
+
go func() {
|
| 817 |
+
ticker := time.NewTicker(hbInterval)
|
| 818 |
+
defer ticker.Stop()
|
| 819 |
+
|
| 820 |
+
for {
|
| 821 |
+
select {
|
| 822 |
+
case <-ctx.Done():
|
| 823 |
+
return
|
| 824 |
+
case <-ticker.C:
|
| 825 |
+
if !rc.IsActive() {
|
| 826 |
+
continue
|
| 827 |
+
}
|
| 828 |
+
uptime := int64(time.Since(startTime).Seconds())
|
| 829 |
+
if err := _814l(rc, uptime); err != nil {
|
| 830 |
+
fmt.Printf(" ⚠ Heartbeat failed (non-blocking): %v\n", err)
|
| 831 |
+
}
|
| 832 |
+
}
|
| 833 |
+
}
|
| 834 |
+
}()
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
func Shutdown(rc *RuntimeContext) {
|
| 838 |
+
if rc == nil || rc._kni == "" {
|
| 839 |
+
return
|
| 840 |
+
}
|
| 841 |
+
_wj(rc)
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
func _pl(code string) (_kni string, err error) {
|
| 845 |
+
resp, err := _dtnx("/v1/register/exchange", map[string]string{
|
| 846 |
+
"authorization_code": code,
|
| 847 |
+
})
|
| 848 |
+
if err != nil {
|
| 849 |
+
return "", err
|
| 850 |
+
}
|
| 851 |
+
defer resp.Body.Close()
|
| 852 |
+
|
| 853 |
+
if resp.StatusCode != http.StatusOK {
|
| 854 |
+
return "", _3ya(resp)
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
var _tmzn struct {
|
| 858 |
+
APIKey string `json:"api_key"`
|
| 859 |
+
}
|
| 860 |
+
json.NewDecoder(resp.Body).Decode(&_tmzn)
|
| 861 |
+
if _tmzn.APIKey == "" {
|
| 862 |
+
return "", fmt.Errorf("exchange returned empty api_key")
|
| 863 |
+
}
|
| 864 |
+
return _tmzn.APIKey, nil
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
func _58(authCodeOrKey string) (string, error) {
|
| 868 |
+
_kni, err := _pl(authCodeOrKey)
|
| 869 |
+
if err == nil && _kni != "" {
|
| 870 |
+
return _kni, nil
|
| 871 |
+
}
|
| 872 |
+
return authCodeOrKey, nil
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
func _c4m(rc *RuntimeContext, _64 string) error {
|
| 876 |
+
resp, err := _sn("/v1/activate", map[string]string{
|
| 877 |
+
"instance_id": rc._z14,
|
| 878 |
+
"version": _64,
|
| 879 |
+
}, rc._kni)
|
| 880 |
+
if err != nil {
|
| 881 |
+
return err
|
| 882 |
+
}
|
| 883 |
+
defer resp.Body.Close()
|
| 884 |
+
|
| 885 |
+
if resp.StatusCode != http.StatusOK {
|
| 886 |
+
return _3ya(resp)
|
| 887 |
+
}
|
| 888 |
+
|
| 889 |
+
var _tmzn struct {
|
| 890 |
+
Status string `json:"status"`
|
| 891 |
+
}
|
| 892 |
+
json.NewDecoder(resp.Body).Decode(&_tmzn)
|
| 893 |
+
|
| 894 |
+
if _tmzn.Status != "active" {
|
| 895 |
+
return fmt.Errorf("activation returned status: %s", _tmzn.Status)
|
| 896 |
+
}
|
| 897 |
+
return nil
|
| 898 |
+
}
|
| 899 |
+
|
| 900 |
+
func _814l(rc *RuntimeContext, uptimeSeconds int64) error {
|
| 901 |
+
_hpv := rc._4g()
|
| 902 |
+
_ti9 := rc._ti9.Swap(0)
|
| 903 |
+
|
| 904 |
+
payload := map[string]any{
|
| 905 |
+
"instance_id": rc._z14,
|
| 906 |
+
"uptime_seconds": uptimeSeconds,
|
| 907 |
+
"version": rc._64,
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
if _hpv > 0 || _ti9 > 0 {
|
| 911 |
+
bundle := map[string]any{}
|
| 912 |
+
if _hpv > 0 {
|
| 913 |
+
bundle["messages_sent"] = _hpv
|
| 914 |
+
}
|
| 915 |
+
if _ti9 > 0 {
|
| 916 |
+
bundle["messages_recv"] = _ti9
|
| 917 |
+
}
|
| 918 |
+
payload["telemetry_bundle"] = bundle
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
resp, err := _sn("/v1/heartbeat", payload, rc._kni)
|
| 922 |
+
if err != nil {
|
| 923 |
+
rc._hpv.Add(_hpv)
|
| 924 |
+
rc._ti9.Add(_ti9)
|
| 925 |
+
return err
|
| 926 |
+
}
|
| 927 |
+
defer resp.Body.Close()
|
| 928 |
+
|
| 929 |
+
if resp.StatusCode != http.StatusOK {
|
| 930 |
+
rc._hpv.Add(_hpv)
|
| 931 |
+
rc._ti9.Add(_ti9)
|
| 932 |
+
return _3ya(resp)
|
| 933 |
+
}
|
| 934 |
+
return nil
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
func _wj(rc *RuntimeContext) {
|
| 938 |
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
| 939 |
+
defer cancel()
|
| 940 |
+
|
| 941 |
+
body, _ := json.Marshal(map[string]string{
|
| 942 |
+
"instance_id": rc._z14,
|
| 943 |
+
})
|
| 944 |
+
|
| 945 |
+
url := _cdo() + "/v1/deactivate"
|
| 946 |
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
| 947 |
+
if err != nil {
|
| 948 |
+
return
|
| 949 |
+
}
|
| 950 |
+
req.Header.Set("Content-Type", "application/json")
|
| 951 |
+
req.Header.Set("X-Api-Key", rc._kni)
|
| 952 |
+
req.Header.Set("X-Signature", _4crw(body, rc._kni))
|
| 953 |
+
_3t.Do(req)
|
| 954 |
+
}
|
whatsapp-service/pkg/events/interfaces/producer.go
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package producer_interfaces
|
| 2 |
+
|
| 3 |
+
type Producer interface {
|
| 4 |
+
Produce(queueName string, payload []byte, webhookUrl string, userID string) error
|
| 5 |
+
CreateGlobalQueues() error
|
| 6 |
+
}
|
whatsapp-service/pkg/events/nats/nats_producer.go
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package nats_producer
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
producer_interfaces "agentdeck-whatsapp-service/pkg/events/interfaces"
|
| 5 |
+
logger_wrapper "agentdeck-whatsapp-service/pkg/logger"
|
| 6 |
+
"github.com/gomessguii/logger"
|
| 7 |
+
"github.com/nats-io/nats.go"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
type natsProducer struct {
|
| 11 |
+
conn *nats.Conn
|
| 12 |
+
natsGlobalEnabled bool
|
| 13 |
+
natsGlobalEvents []string
|
| 14 |
+
loggerWrapper *logger_wrapper.LoggerManager
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
func NewNatsProducer(
|
| 18 |
+
url string,
|
| 19 |
+
natsGlobalEnabled bool,
|
| 20 |
+
natsGlobalEvents []string,
|
| 21 |
+
loggerWrapper *logger_wrapper.LoggerManager,
|
| 22 |
+
) producer_interfaces.Producer {
|
| 23 |
+
conn, err := nats.Connect(url)
|
| 24 |
+
if err != nil {
|
| 25 |
+
logger.LogError("Failed to connect to NATS: %v", err)
|
| 26 |
+
return &natsProducer{
|
| 27 |
+
conn: nil,
|
| 28 |
+
natsGlobalEnabled: false,
|
| 29 |
+
natsGlobalEvents: nil,
|
| 30 |
+
loggerWrapper: loggerWrapper,
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
return &natsProducer{
|
| 35 |
+
conn: conn,
|
| 36 |
+
natsGlobalEnabled: natsGlobalEnabled,
|
| 37 |
+
natsGlobalEvents: natsGlobalEvents,
|
| 38 |
+
loggerWrapper: loggerWrapper,
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func (p *natsProducer) Produce(
|
| 43 |
+
queueName string,
|
| 44 |
+
payload []byte,
|
| 45 |
+
natsEnable string,
|
| 46 |
+
userID string,
|
| 47 |
+
) error {
|
| 48 |
+
p.loggerWrapper.GetLogger(userID).LogInfo("[%s] NATS Producer - Starting produce for subject: %s", userID, queueName)
|
| 49 |
+
p.loggerWrapper.GetLogger(userID).LogInfo("[%s] NATS Producer - Global enabled: %v", userID, p.natsGlobalEnabled)
|
| 50 |
+
|
| 51 |
+
if p.conn == nil {
|
| 52 |
+
p.loggerWrapper.GetLogger(userID).LogWarn("[%s] NATS connection is nil", userID)
|
| 53 |
+
return nil
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
if natsEnable == "global" {
|
| 57 |
+
p.loggerWrapper.GetLogger(userID).LogInfo("[%s] Publishing to global subject: %s", userID, queueName)
|
| 58 |
+
err := p.conn.Publish(queueName, payload)
|
| 59 |
+
if err != nil {
|
| 60 |
+
p.loggerWrapper.GetLogger(userID).LogError("[%s] Failed to publish message to subject %s: %v", userID, queueName, err)
|
| 61 |
+
return err
|
| 62 |
+
}
|
| 63 |
+
p.loggerWrapper.GetLogger(userID).LogInfo("[%s] Message published successfully to subject: %s", userID, queueName)
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
if natsEnable == "enabled" {
|
| 67 |
+
err := p.conn.Publish(queueName, payload)
|
| 68 |
+
if err != nil {
|
| 69 |
+
p.loggerWrapper.GetLogger(userID).LogError("[%s] Failed to publish message to instance subject %s: %v", userID, queueName, err)
|
| 70 |
+
return err
|
| 71 |
+
}
|
| 72 |
+
p.loggerWrapper.GetLogger(userID).LogInfo("[%s] Message published successfully to instance subject: %s", userID, queueName)
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
return nil
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// CreateGlobalQueues não faz nada para NATS producer pois os subjects são criados dinamicamente
|
| 79 |
+
func (p *natsProducer) CreateGlobalQueues() error {
|
| 80 |
+
return nil
|
| 81 |
+
}
|
whatsapp-service/pkg/events/rabbitmq/rabbitmq_producer.go
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package rabbitmq_producer
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"net/url"
|
| 6 |
+
"strings"
|
| 7 |
+
"time"
|
| 8 |
+
|
| 9 |
+
producer_interfaces "agentdeck-whatsapp-service/pkg/events/interfaces"
|
| 10 |
+
logger_wrapper "agentdeck-whatsapp-service/pkg/logger"
|
| 11 |
+
"github.com/gomessguii/logger"
|
| 12 |
+
amqp "github.com/rabbitmq/amqp091-go"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
type rabbitMQProducer struct {
|
| 16 |
+
conn *amqp.Connection
|
| 17 |
+
amqpGlobalEnabled bool
|
| 18 |
+
amqpGlobalEvents []string
|
| 19 |
+
amqpSpecificEvents []string
|
| 20 |
+
connStr string
|
| 21 |
+
maxRetries int
|
| 22 |
+
loggerWrapper *logger_wrapper.LoggerManager
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
func NewRabbitMQProducer(
|
| 26 |
+
conn *amqp.Connection,
|
| 27 |
+
amqpGlobalEnabled bool,
|
| 28 |
+
amqpGlobalEvents []string,
|
| 29 |
+
amqpSpecificEvents []string,
|
| 30 |
+
connStr string,
|
| 31 |
+
loggerWrapper *logger_wrapper.LoggerManager,
|
| 32 |
+
) producer_interfaces.Producer {
|
| 33 |
+
producer := &rabbitMQProducer{
|
| 34 |
+
conn: conn,
|
| 35 |
+
amqpGlobalEnabled: amqpGlobalEnabled,
|
| 36 |
+
amqpGlobalEvents: amqpGlobalEvents,
|
| 37 |
+
amqpSpecificEvents: amqpSpecificEvents,
|
| 38 |
+
connStr: connStr,
|
| 39 |
+
maxRetries: 3,
|
| 40 |
+
loggerWrapper: loggerWrapper,
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
return producer
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// maskConnectionString masks sensitive information in the connection string for logging
|
| 47 |
+
func (p *rabbitMQProducer) maskConnectionString(connStr string) string {
|
| 48 |
+
if connStr == "" {
|
| 49 |
+
return "empty"
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
parsedURL, err := url.Parse(connStr)
|
| 53 |
+
if err != nil {
|
| 54 |
+
return "invalid-url"
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// Mask password if present
|
| 58 |
+
if parsedURL.User != nil {
|
| 59 |
+
if _, hasPassword := parsedURL.User.Password(); hasPassword {
|
| 60 |
+
parsedURL.User = url.UserPassword(parsedURL.User.Username(), "***")
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
return parsedURL.String()
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// handleConnectionClose monitors connection close events and logs them
|
| 68 |
+
func (p *rabbitMQProducer) handleConnectionClose() {
|
| 69 |
+
if p.conn == nil {
|
| 70 |
+
return
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
closeChan := make(chan *amqp.Error)
|
| 74 |
+
p.conn.NotifyClose(closeChan)
|
| 75 |
+
|
| 76 |
+
closeErr := <-closeChan
|
| 77 |
+
if closeErr != nil {
|
| 78 |
+
logger.LogWarn("RabbitMQ connection closed unexpectedly: %v", closeErr)
|
| 79 |
+
logger.LogInfo("Connection will be re-established on next message send")
|
| 80 |
+
} else {
|
| 81 |
+
logger.LogInfo("RabbitMQ connection closed gracefully")
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
func (p *rabbitMQProducer) reconnect() error {
|
| 86 |
+
if p.connStr == "" {
|
| 87 |
+
return fmt.Errorf("connection string is empty - RabbitMQ URL not configured")
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
logger.LogInfo("Starting RabbitMQ reconnection process with URL: %s", p.maskConnectionString(p.connStr))
|
| 91 |
+
|
| 92 |
+
var err error
|
| 93 |
+
for i := 0; i < 3; i++ {
|
| 94 |
+
logger.LogInfo("Tentando reconectar ao RabbitMQ (tentativa %d/3)", i+1)
|
| 95 |
+
|
| 96 |
+
// Create connection with heartbeat to prevent timeouts
|
| 97 |
+
config := amqp.Config{
|
| 98 |
+
Heartbeat: 30 * time.Second, // Send heartbeat every 30 seconds
|
| 99 |
+
Locale: "en_US",
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
p.conn, err = amqp.DialConfig(p.connStr, config)
|
| 103 |
+
if err == nil {
|
| 104 |
+
logger.LogInfo("Reconectado com sucesso ao RabbitMQ com heartbeat de 30s")
|
| 105 |
+
|
| 106 |
+
// Set up connection close notification
|
| 107 |
+
go p.handleConnectionClose()
|
| 108 |
+
return nil
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
logger.LogError("Falha na tentativa %d/3 de reconexão: %v", i+1, err)
|
| 112 |
+
if i < 2 { // Don't sleep on the last attempt
|
| 113 |
+
time.Sleep(time.Second * 2)
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
return fmt.Errorf("falha ao reconectar após 3 tentativas: %v", err)
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
func (p *rabbitMQProducer) ensureConnection() error {
|
| 120 |
+
if p.conn == nil || p.conn.IsClosed() {
|
| 121 |
+
return p.reconnect()
|
| 122 |
+
}
|
| 123 |
+
return nil
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
func (p *rabbitMQProducer) publishWithRetry(
|
| 127 |
+
channel *amqp.Channel,
|
| 128 |
+
queueName string,
|
| 129 |
+
payload []byte,
|
| 130 |
+
userID string,
|
| 131 |
+
) error {
|
| 132 |
+
var err error
|
| 133 |
+
for i := 0; i < p.maxRetries; i++ {
|
| 134 |
+
err = channel.Publish(
|
| 135 |
+
"", // exchange
|
| 136 |
+
queueName, // routing key
|
| 137 |
+
false, // mandatory
|
| 138 |
+
false, // immediate
|
| 139 |
+
amqp.Publishing{
|
| 140 |
+
ContentType: "application/json",
|
| 141 |
+
Body: payload,
|
| 142 |
+
DeliveryMode: amqp.Persistent, // Garante persistência da mensagem
|
| 143 |
+
})
|
| 144 |
+
|
| 145 |
+
if err == nil {
|
| 146 |
+
return nil
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
logger.LogWarn("[%s] Falha ao publicar mensagem (tentativa %d/%d): %v",
|
| 150 |
+
userID, i+1, p.maxRetries, err)
|
| 151 |
+
|
| 152 |
+
// Se o erro for de conexão, tenta reconectar
|
| 153 |
+
if err.Error() == "Exception (504) Reason: \"channel/connection is not open\"" {
|
| 154 |
+
if err := p.ensureConnection(); err != nil {
|
| 155 |
+
continue
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// Cria novo canal após reconexão
|
| 159 |
+
channel, err = p.conn.Channel()
|
| 160 |
+
if err != nil {
|
| 161 |
+
continue
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
time.Sleep(time.Second * time.Duration(i+1))
|
| 166 |
+
}
|
| 167 |
+
return err
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
func (p *rabbitMQProducer) Produce(
|
| 171 |
+
queueName string,
|
| 172 |
+
payload []byte,
|
| 173 |
+
rabbitmqEnable string,
|
| 174 |
+
userID string,
|
| 175 |
+
) error {
|
| 176 |
+
p.loggerWrapper.GetLogger(userID).LogInfo("[%s] RabbitMQ Producer - Starting produce for queue: %s", userID, queueName)
|
| 177 |
+
|
| 178 |
+
if p.connStr == "" {
|
| 179 |
+
return fmt.Errorf("RabbitMQ connection string is empty - check AMQP_URL configuration")
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
if err := p.ensureConnection(); err != nil {
|
| 183 |
+
p.loggerWrapper.GetLogger(userID).LogError("[%s] Failed to ensure RabbitMQ connection: %v", userID, err)
|
| 184 |
+
return fmt.Errorf("falha ao garantir conexão: %v", err)
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
channel, err := p.conn.Channel()
|
| 188 |
+
if err != nil {
|
| 189 |
+
return fmt.Errorf("falha ao abrir canal: %v", err)
|
| 190 |
+
}
|
| 191 |
+
defer channel.Close()
|
| 192 |
+
|
| 193 |
+
// Configura confirmação de publicação
|
| 194 |
+
if err := channel.Confirm(false); err != nil {
|
| 195 |
+
return fmt.Errorf("falha ao configurar confirms do canal: %v", err)
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
args := amqp.Table{
|
| 199 |
+
"x-queue-type": "quorum",
|
| 200 |
+
"x-ha-policy": "all", // Alta disponibilidade
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
if rabbitmqEnable == "global" || rabbitmqEnable == "enabled" {
|
| 204 |
+
_, err = channel.QueueDeclare(
|
| 205 |
+
queueName, // name
|
| 206 |
+
true, // durable
|
| 207 |
+
false, // delete when unused
|
| 208 |
+
false, // exclusive
|
| 209 |
+
false, // no-wait
|
| 210 |
+
args, // arguments
|
| 211 |
+
)
|
| 212 |
+
if err != nil {
|
| 213 |
+
return fmt.Errorf("falha ao declarar fila %s: %v", queueName, err)
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
err = p.publishWithRetry(channel, queueName, payload, userID)
|
| 217 |
+
if err != nil {
|
| 218 |
+
return fmt.Errorf("falha ao publicar mensagem após todas as tentativas: %v", err)
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
p.loggerWrapper.GetLogger(userID).LogInfo("[%s] Mensagem publicada com sucesso na fila: %s", userID, queueName)
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
return nil
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
// CreateGlobalQueues cria todas as filas globais no startup da aplicação
|
| 228 |
+
func (p *rabbitMQProducer) CreateGlobalQueues() error {
|
| 229 |
+
if !p.amqpGlobalEnabled {
|
| 230 |
+
return nil
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
p.loggerWrapper.GetLogger("system").LogInfo("Creating global queues for enabled events")
|
| 234 |
+
|
| 235 |
+
if err := p.ensureConnection(); err != nil {
|
| 236 |
+
return fmt.Errorf("failed to ensure connection: %v", err)
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
channel, err := p.conn.Channel()
|
| 240 |
+
if err != nil {
|
| 241 |
+
return fmt.Errorf("failed to open channel: %v", err)
|
| 242 |
+
}
|
| 243 |
+
defer channel.Close()
|
| 244 |
+
|
| 245 |
+
args := amqp.Table{
|
| 246 |
+
"x-queue-type": "quorum",
|
| 247 |
+
"x-ha-policy": "all", // Alta disponibilidade
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
createdQueues := 0
|
| 251 |
+
|
| 252 |
+
// AMQP_SPECIFIC_EVENTS tem prioridade sobre AMQP_GLOBAL_EVENTS
|
| 253 |
+
if len(p.amqpSpecificEvents) > 0 {
|
| 254 |
+
p.loggerWrapper.GetLogger("system").LogInfo("Using AMQP_SPECIFIC_EVENTS (priority over AMQP_GLOBAL_EVENTS)")
|
| 255 |
+
|
| 256 |
+
// Cria filas diretas para eventos específicos
|
| 257 |
+
for _, eventName := range p.amqpSpecificEvents {
|
| 258 |
+
queueName := strings.ToLower(eventName)
|
| 259 |
+
|
| 260 |
+
_, err = channel.QueueDeclare(
|
| 261 |
+
queueName, // name
|
| 262 |
+
true, // durable
|
| 263 |
+
false, // delete when unused
|
| 264 |
+
false, // exclusive
|
| 265 |
+
false, // no-wait
|
| 266 |
+
args, // arguments
|
| 267 |
+
)
|
| 268 |
+
if err != nil {
|
| 269 |
+
p.loggerWrapper.GetLogger("system").LogError("Failed to create specific queue %s: %v", queueName, err)
|
| 270 |
+
return fmt.Errorf("failed to create specific queue %s: %v", queueName, err)
|
| 271 |
+
}
|
| 272 |
+
p.loggerWrapper.GetLogger("system").LogInfo("Specific queue created: %s", queueName)
|
| 273 |
+
createdQueues++
|
| 274 |
+
}
|
| 275 |
+
} else {
|
| 276 |
+
p.loggerWrapper.GetLogger("system").LogInfo("Using AMQP_GLOBAL_EVENTS (fallback mode)")
|
| 277 |
+
|
| 278 |
+
// Mapeia eventos globais para os eventos originais que precisam de filas (modo antigo)
|
| 279 |
+
eventMap := map[string][]string{
|
| 280 |
+
"MESSAGE": {"message"},
|
| 281 |
+
"SEND_MESSAGE": {"sendmessage"},
|
| 282 |
+
"READ_RECEIPT": {"receipt"},
|
| 283 |
+
"PRESENCE": {"presence"},
|
| 284 |
+
"HISTORY_SYNC": {"historysync"},
|
| 285 |
+
"CHAT_PRESENCE": {"chatpresence", "archive"},
|
| 286 |
+
"CALL": {"calloffer", "callaccept", "callterminate", "calloffernotice", "callrelaylatency"},
|
| 287 |
+
"CONNECTION": {"connected", "pairsuccess", "temporaryban", "loggedout", "connectfailure", "disconnected"},
|
| 288 |
+
"LABEL": {"labeledit", "labelassociationchat", "labelassociationmessage"},
|
| 289 |
+
"CONTACT": {"contact", "pushname"},
|
| 290 |
+
"GROUP": {"groupinfo", "joinedgroup"},
|
| 291 |
+
"NEWSLETTER": {"newsletterjoin", "newsletterleave"},
|
| 292 |
+
"QRCODE": {"qrcode", "qrtimeout", "qrsuccess"},
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
for _, globalEvent := range p.amqpGlobalEvents {
|
| 296 |
+
if queueNames, exists := eventMap[globalEvent]; exists {
|
| 297 |
+
for _, queueName := range queueNames {
|
| 298 |
+
_, err = channel.QueueDeclare(
|
| 299 |
+
queueName, // name
|
| 300 |
+
true, // durable
|
| 301 |
+
false, // delete when unused
|
| 302 |
+
false, // exclusive
|
| 303 |
+
false, // no-wait
|
| 304 |
+
args, // arguments
|
| 305 |
+
)
|
| 306 |
+
if err != nil {
|
| 307 |
+
p.loggerWrapper.GetLogger("system").LogError("Failed to create global queue %s: %v", queueName, err)
|
| 308 |
+
return fmt.Errorf("failed to create global queue %s: %v", queueName, err)
|
| 309 |
+
}
|
| 310 |
+
p.loggerWrapper.GetLogger("system").LogInfo("Global queue created: %s", queueName)
|
| 311 |
+
createdQueues++
|
| 312 |
+
}
|
| 313 |
+
}
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
p.loggerWrapper.GetLogger("system").LogInfo("Successfully created %d global queues", createdQueues)
|
| 318 |
+
return nil
|
| 319 |
+
}
|
whatsapp-service/pkg/events/webhook/webhook_producer.go
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package webhook_producer
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"errors"
|
| 6 |
+
"fmt"
|
| 7 |
+
"io"
|
| 8 |
+
"net/http"
|
| 9 |
+
"strings"
|
| 10 |
+
"time"
|
| 11 |
+
|
| 12 |
+
producer_interfaces "agentdeck-whatsapp-service/pkg/events/interfaces"
|
| 13 |
+
logger_wrapper "agentdeck-whatsapp-service/pkg/logger"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
type webhookProducer struct {
|
| 17 |
+
url string
|
| 18 |
+
loggerWrapper *logger_wrapper.LoggerManager
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
func NewWebhookProducer(
|
| 22 |
+
url string,
|
| 23 |
+
loggerWrapper *logger_wrapper.LoggerManager,
|
| 24 |
+
) producer_interfaces.Producer {
|
| 25 |
+
return &webhookProducer{
|
| 26 |
+
url: url,
|
| 27 |
+
loggerWrapper: loggerWrapper,
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
func (p *webhookProducer) Produce(
|
| 32 |
+
queueName string,
|
| 33 |
+
payload []byte,
|
| 34 |
+
webhookUrl string,
|
| 35 |
+
userID string,
|
| 36 |
+
) error {
|
| 37 |
+
splitQueue := strings.Split(queueName, ".")
|
| 38 |
+
|
| 39 |
+
if len(splitQueue) < 2 {
|
| 40 |
+
return nil
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
if p.url != "" {
|
| 44 |
+
go p.sendWebhookWithRetry(p.url, payload, 5, 30*time.Second, userID)
|
| 45 |
+
}
|
| 46 |
+
if webhookUrl != "" {
|
| 47 |
+
go p.sendWebhookWithRetry(webhookUrl, payload, 5, 30*time.Second, userID)
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
return nil
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
func (p *webhookProducer) sendWebhookWithRetry(url string, body []byte, maxRetries int, retryInterval time.Duration, userID string) {
|
| 54 |
+
for i := 0; i < maxRetries; i++ {
|
| 55 |
+
err, responseBody, statusCode := p.sendWebhook(url, body, userID)
|
| 56 |
+
if err == nil {
|
| 57 |
+
p.loggerWrapper.GetLogger(userID).LogInfo("[%s] webhook sent successfully - url: %s, status: %d, response: %s", userID, url, statusCode, string(responseBody))
|
| 58 |
+
return
|
| 59 |
+
}
|
| 60 |
+
p.loggerWrapper.GetLogger(userID).LogWarn("[%s] webhook failed - url: %s, attempt: %d, error: %v", userID, url, i+1, err)
|
| 61 |
+
|
| 62 |
+
time.Sleep(retryInterval)
|
| 63 |
+
}
|
| 64 |
+
p.loggerWrapper.GetLogger(userID).LogError("[%s] webhook failed after maximum retries - url: %s", userID, url)
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
func (p *webhookProducer) sendWebhook(url string, body []byte, userID string) (error, []byte, int) {
|
| 68 |
+
req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
|
| 69 |
+
if err != nil {
|
| 70 |
+
return err, nil, 0
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
req.Header.Set("Content-Type", "application/json")
|
| 74 |
+
|
| 75 |
+
client := &http.Client{}
|
| 76 |
+
resp, err := client.Do(req)
|
| 77 |
+
if err != nil {
|
| 78 |
+
return err, nil, 0
|
| 79 |
+
}
|
| 80 |
+
defer resp.Body.Close()
|
| 81 |
+
|
| 82 |
+
responseBody, err := io.ReadAll(resp.Body)
|
| 83 |
+
if err != nil {
|
| 84 |
+
return fmt.Errorf("erro ao ler resposta: %v", err), nil, 0
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
| 88 |
+
return errors.New("received non-2xx response: " + resp.Status), responseBody, resp.StatusCode
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
return nil, responseBody, resp.StatusCode
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// CreateGlobalQueues não faz nada para webhook producer
|
| 95 |
+
func (p *webhookProducer) CreateGlobalQueues() error {
|
| 96 |
+
return nil
|
| 97 |
+
}
|
whatsapp-service/pkg/events/websocket/websocket_producer.go
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package websocket_producer
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
"strings"
|
| 6 |
+
"sync"
|
| 7 |
+
|
| 8 |
+
logger_wrapper "agentdeck-whatsapp-service/pkg/logger"
|
| 9 |
+
"github.com/gomessguii/logger"
|
| 10 |
+
"github.com/gorilla/websocket"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
var upgrader = websocket.Upgrader{
|
| 14 |
+
ReadBufferSize: 1024,
|
| 15 |
+
WriteBufferSize: 1024,
|
| 16 |
+
CheckOrigin: func(r *http.Request) bool {
|
| 17 |
+
logger.LogInfo("Verificando origem da conexão WebSocket")
|
| 18 |
+
return true
|
| 19 |
+
},
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
type websocketProducer struct {
|
| 23 |
+
clients map[string]*websocket.Conn // conexões específicas por instância
|
| 24 |
+
broadcast []*websocket.Conn // conexões que recebem todos os eventos
|
| 25 |
+
clientsMux sync.RWMutex
|
| 26 |
+
loggerWrapper *logger_wrapper.LoggerManager
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
func NewWebsocketProducer(loggerWrapper *logger_wrapper.LoggerManager) *websocketProducer {
|
| 30 |
+
return &websocketProducer{
|
| 31 |
+
clients: make(map[string]*websocket.Conn),
|
| 32 |
+
broadcast: make([]*websocket.Conn, 0),
|
| 33 |
+
clientsMux: sync.RWMutex{},
|
| 34 |
+
loggerWrapper: loggerWrapper,
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// ServeWs lida com as requisições de upgrade para websocket
|
| 39 |
+
func ServeWs(w http.ResponseWriter, r *http.Request, instanceId string, producer *websocketProducer) {
|
| 40 |
+
logger.LogInfo("Iniciando upgrade da conexão WebSocket")
|
| 41 |
+
conn, err := upgrader.Upgrade(w, r, nil)
|
| 42 |
+
if err != nil {
|
| 43 |
+
logger.LogError("Erro ao fazer upgrade da conexão websocket: %v", err)
|
| 44 |
+
return
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
logger.LogInfo("Conexão WebSocket estabelecida com sucesso")
|
| 48 |
+
|
| 49 |
+
if instanceId == "" {
|
| 50 |
+
producer.AddBroadcastClient(conn)
|
| 51 |
+
} else {
|
| 52 |
+
producer.AddClient(instanceId, conn)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Goroutine para limpar conexão quando fechada
|
| 56 |
+
go func() {
|
| 57 |
+
for {
|
| 58 |
+
_, _, err := conn.ReadMessage()
|
| 59 |
+
if err != nil {
|
| 60 |
+
if instanceId == "" {
|
| 61 |
+
producer.RemoveBroadcastClient(conn)
|
| 62 |
+
} else {
|
| 63 |
+
producer.RemoveClient(instanceId)
|
| 64 |
+
}
|
| 65 |
+
conn.Close()
|
| 66 |
+
break
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
}()
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
func (p *websocketProducer) AddBroadcastClient(conn *websocket.Conn) {
|
| 73 |
+
p.clientsMux.Lock()
|
| 74 |
+
defer p.clientsMux.Unlock()
|
| 75 |
+
p.broadcast = append(p.broadcast, conn)
|
| 76 |
+
logger.LogInfo("Cliente broadcast websocket adicionado")
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
func (p *websocketProducer) RemoveBroadcastClient(conn *websocket.Conn) {
|
| 80 |
+
p.clientsMux.Lock()
|
| 81 |
+
defer p.clientsMux.Unlock()
|
| 82 |
+
for i, c := range p.broadcast {
|
| 83 |
+
if c == conn {
|
| 84 |
+
p.broadcast = append(p.broadcast[:i], p.broadcast[i+1:]...)
|
| 85 |
+
break
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
logger.LogInfo("Cliente broadcast websocket removido")
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
func (p *websocketProducer) AddClient(instanceID string, conn *websocket.Conn) {
|
| 92 |
+
p.clientsMux.Lock()
|
| 93 |
+
defer p.clientsMux.Unlock()
|
| 94 |
+
p.clients[instanceID] = conn
|
| 95 |
+
p.loggerWrapper.GetLogger(instanceID).LogInfo("Cliente websocket adicionado para instância: %s", instanceID)
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
func (p *websocketProducer) RemoveClient(instanceID string) {
|
| 99 |
+
p.clientsMux.Lock()
|
| 100 |
+
defer p.clientsMux.Unlock()
|
| 101 |
+
delete(p.clients, instanceID)
|
| 102 |
+
p.loggerWrapper.GetLogger(instanceID).LogInfo("Cliente websocket removido para instância: %s", instanceID)
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
func (p *websocketProducer) Produce(queueName string, payload []byte, instanceID string, _ string) error {
|
| 106 |
+
message := map[string]interface{}{
|
| 107 |
+
"queue": strings.ToLower(queueName),
|
| 108 |
+
"payload": string(payload),
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
p.clientsMux.RLock()
|
| 112 |
+
defer p.clientsMux.RUnlock()
|
| 113 |
+
|
| 114 |
+
// Envia para cliente específico da instância
|
| 115 |
+
if client, exists := p.clients[instanceID]; exists {
|
| 116 |
+
err := client.WriteJSON(message)
|
| 117 |
+
if err != nil {
|
| 118 |
+
p.loggerWrapper.GetLogger(instanceID).LogError("Erro ao enviar mensagem websocket para %s: %v", instanceID, err)
|
| 119 |
+
// Não remove o cliente aqui pois estamos com o RLock
|
| 120 |
+
return err
|
| 121 |
+
}
|
| 122 |
+
p.loggerWrapper.GetLogger(instanceID).LogInfo("Mensagem websocket enviada com sucesso para instância %s na fila %s", instanceID, queueName)
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// Envia para todos os clientes broadcast
|
| 126 |
+
for _, conn := range p.broadcast {
|
| 127 |
+
err := conn.WriteJSON(message)
|
| 128 |
+
if err != nil {
|
| 129 |
+
p.loggerWrapper.GetLogger(instanceID).LogError("Erro ao enviar mensagem broadcast websocket: %v", err)
|
| 130 |
+
continue
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
return nil
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// CreateGlobalQueues não faz nada para websocket producer
|
| 138 |
+
func (p *websocketProducer) CreateGlobalQueues() error {
|
| 139 |
+
return nil
|
| 140 |
+
}
|
whatsapp-service/pkg/group/handler/group_handler.go
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package group_handler
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
|
| 6 |
+
group_service "agentdeck-whatsapp-service/pkg/group/service"
|
| 7 |
+
instance_model "agentdeck-whatsapp-service/pkg/instance/model"
|
| 8 |
+
"github.com/gin-gonic/gin"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type GroupHandler interface {
|
| 12 |
+
ListGroups(ctx *gin.Context)
|
| 13 |
+
GetGroupInfo(ctx *gin.Context)
|
| 14 |
+
GetGroupInviteLink(ctx *gin.Context)
|
| 15 |
+
SetGroupPhoto(ctx *gin.Context)
|
| 16 |
+
SetGroupName(ctx *gin.Context)
|
| 17 |
+
SetGroupDescription(ctx *gin.Context)
|
| 18 |
+
CreateGroup(ctx *gin.Context)
|
| 19 |
+
UpdateParticipant(ctx *gin.Context)
|
| 20 |
+
GetMyGroups(ctx *gin.Context)
|
| 21 |
+
JoinGroupLink(ctx *gin.Context)
|
| 22 |
+
LeaveGroup(ctx *gin.Context)
|
| 23 |
+
UpdateGroupSettings(ctx *gin.Context)
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
type groupHandler struct {
|
| 27 |
+
groupService group_service.GroupService
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// List groups
|
| 31 |
+
// @Summary List groups
|
| 32 |
+
// @Description List groups
|
| 33 |
+
// @Tags Group
|
| 34 |
+
// @Accept json
|
| 35 |
+
// @Produce json
|
| 36 |
+
// @Success 200 {object} gin.H "success"
|
| 37 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 38 |
+
// @Router /group/list [get]
|
| 39 |
+
func (g *groupHandler) ListGroups(ctx *gin.Context) {
|
| 40 |
+
getInstance := ctx.MustGet("instance")
|
| 41 |
+
|
| 42 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 43 |
+
if !ok {
|
| 44 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 45 |
+
return
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
resp, err := g.groupService.ListGroups(instance)
|
| 49 |
+
if err != nil {
|
| 50 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 51 |
+
return
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp})
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// Get group info
|
| 58 |
+
// @Summary Get group info
|
| 59 |
+
// @Description Get group info
|
| 60 |
+
// @Tags Group
|
| 61 |
+
// @Accept json
|
| 62 |
+
// @Produce json
|
| 63 |
+
// @Param message body group_service.GetGroupInfoStruct true "Group data"
|
| 64 |
+
// @Success 200 {object} gin.H "success"
|
| 65 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 66 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 67 |
+
// @Router /group/info [post]
|
| 68 |
+
func (g *groupHandler) GetGroupInfo(ctx *gin.Context) {
|
| 69 |
+
getInstance := ctx.MustGet("instance")
|
| 70 |
+
|
| 71 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 72 |
+
if !ok {
|
| 73 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 74 |
+
return
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
var data *group_service.GetGroupInfoStruct
|
| 78 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 79 |
+
if err != nil {
|
| 80 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 81 |
+
return
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
if data.GroupJID == "" {
|
| 85 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJID is required"})
|
| 86 |
+
return
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
resp, err := g.groupService.GetGroupInfo(data, instance)
|
| 90 |
+
if err != nil {
|
| 91 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 92 |
+
return
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp})
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// Get group invite link
|
| 99 |
+
// @Summary Get group invite link
|
| 100 |
+
// @Description Get group invite link
|
| 101 |
+
// @Tags Group
|
| 102 |
+
// @Accept json
|
| 103 |
+
// @Produce json
|
| 104 |
+
// @Param message body group_service.GetGroupInviteLinkStruct true "Group data"
|
| 105 |
+
// @Success 200 {object} gin.H "success"
|
| 106 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 107 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 108 |
+
// @Router /group/invitelink [post]
|
| 109 |
+
func (g *groupHandler) GetGroupInviteLink(ctx *gin.Context) {
|
| 110 |
+
getInstance := ctx.MustGet("instance")
|
| 111 |
+
|
| 112 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 113 |
+
if !ok {
|
| 114 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 115 |
+
return
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
var data *group_service.GetGroupInviteLinkStruct
|
| 119 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 120 |
+
if err != nil {
|
| 121 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 122 |
+
return
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
if data.GroupJID == "" {
|
| 126 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJID is required"})
|
| 127 |
+
return
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
resp, err := g.groupService.GetGroupInviteLink(data, instance)
|
| 131 |
+
if err != nil {
|
| 132 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 133 |
+
return
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp})
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// Set group photo
|
| 140 |
+
// @Summary Set group photo
|
| 141 |
+
// @Description Set group photo
|
| 142 |
+
// @Tags Group
|
| 143 |
+
// @Accept json
|
| 144 |
+
// @Produce json
|
| 145 |
+
// @Param message body group_service.SetGroupPhotoStruct true "Group data"
|
| 146 |
+
// @Success 200 {object} gin.H "success"
|
| 147 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 148 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 149 |
+
// @Router /group/photo [post]
|
| 150 |
+
func (g *groupHandler) SetGroupPhoto(ctx *gin.Context) {
|
| 151 |
+
getInstance := ctx.MustGet("instance")
|
| 152 |
+
|
| 153 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 154 |
+
if !ok {
|
| 155 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 156 |
+
return
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
var data *group_service.SetGroupPhotoStruct
|
| 160 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 161 |
+
if err != nil {
|
| 162 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 163 |
+
return
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
if data.GroupJID == "" {
|
| 167 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJID is required"})
|
| 168 |
+
return
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
if data.Image == "" {
|
| 172 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "image is required"})
|
| 173 |
+
return
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
resp, err := g.groupService.SetGroupPhoto(data, instance)
|
| 177 |
+
if err != nil {
|
| 178 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 179 |
+
return
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp})
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
// Set group name
|
| 186 |
+
// @Summary Set group name
|
| 187 |
+
// @Description Set group name
|
| 188 |
+
// @Tags Group
|
| 189 |
+
// @Accept json
|
| 190 |
+
// @Produce json
|
| 191 |
+
// @Param message body group_service.SetGroupNameStruct true "Group data"
|
| 192 |
+
// @Success 200 {object} gin.H "success"
|
| 193 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 194 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 195 |
+
// @Router /group/name [post]
|
| 196 |
+
func (g *groupHandler) SetGroupName(ctx *gin.Context) {
|
| 197 |
+
getInstance := ctx.MustGet("instance")
|
| 198 |
+
|
| 199 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 200 |
+
if !ok {
|
| 201 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 202 |
+
return
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
var data *group_service.SetGroupNameStruct
|
| 206 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 207 |
+
if err != nil {
|
| 208 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 209 |
+
return
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
if data.GroupJID == "" {
|
| 213 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJID is required"})
|
| 214 |
+
return
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
if data.Name == "" {
|
| 218 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
| 219 |
+
return
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
err = g.groupService.SetGroupName(data, instance)
|
| 223 |
+
if err != nil {
|
| 224 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 225 |
+
return
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
// Set group description
|
| 232 |
+
// @Summary Set group description
|
| 233 |
+
// @Description Set group description
|
| 234 |
+
// @Tags Group
|
| 235 |
+
// @Accept json
|
| 236 |
+
// @Produce json
|
| 237 |
+
// @Param message body group_service.SetGroupDescriptionStruct true "Group data"
|
| 238 |
+
// @Success 200 {object} gin.H "success"
|
| 239 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 240 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 241 |
+
// @Router /group/description [post]
|
| 242 |
+
func (g *groupHandler) SetGroupDescription(ctx *gin.Context) {
|
| 243 |
+
getInstance := ctx.MustGet("instance")
|
| 244 |
+
|
| 245 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 246 |
+
if !ok {
|
| 247 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 248 |
+
return
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
var data *group_service.SetGroupDescriptionStruct
|
| 252 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 253 |
+
if err != nil {
|
| 254 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 255 |
+
return
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
if data.GroupJID == "" {
|
| 259 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJID is required"})
|
| 260 |
+
return
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
// Description can be empty to clear the group description
|
| 264 |
+
// No validation needed for Description field
|
| 265 |
+
|
| 266 |
+
err = g.groupService.SetGroupDescription(data, instance)
|
| 267 |
+
if err != nil {
|
| 268 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 269 |
+
return
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
// Create group
|
| 276 |
+
// @Summary Create group
|
| 277 |
+
// @Description Create group
|
| 278 |
+
// @Tags Group
|
| 279 |
+
// @Accept json
|
| 280 |
+
// @Produce json
|
| 281 |
+
// @Param message body group_service.CreateGroupStruct true "Group data"
|
| 282 |
+
// @Success 200 {object} gin.H "success"
|
| 283 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 284 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 285 |
+
// @Router /group/create [post]
|
| 286 |
+
func (g *groupHandler) CreateGroup(ctx *gin.Context) {
|
| 287 |
+
getInstance := ctx.MustGet("instance")
|
| 288 |
+
|
| 289 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 290 |
+
if !ok {
|
| 291 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 292 |
+
return
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
var data *group_service.CreateGroupStruct
|
| 296 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 297 |
+
if err != nil {
|
| 298 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 299 |
+
return
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
if data.GroupName == "" {
|
| 303 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupName is required"})
|
| 304 |
+
return
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
if len(data.Participants) < 1 {
|
| 308 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "participants are required"})
|
| 309 |
+
return
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
group, err := g.groupService.CreateGroup(data, instance)
|
| 313 |
+
if err != nil {
|
| 314 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 315 |
+
return
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": group})
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
// Update participant
|
| 322 |
+
// @Summary Update participant
|
| 323 |
+
// @Description Update participant
|
| 324 |
+
// @Tags Group
|
| 325 |
+
// @Accept json
|
| 326 |
+
// @Produce json
|
| 327 |
+
// @Param message body group_service.AddParticipantStruct true "Group data"
|
| 328 |
+
// @Success 200 {object} gin.H "success"
|
| 329 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 330 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 331 |
+
// @Router /group/participant [post]
|
| 332 |
+
func (g *groupHandler) UpdateParticipant(ctx *gin.Context) {
|
| 333 |
+
getInstance := ctx.MustGet("instance")
|
| 334 |
+
|
| 335 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 336 |
+
if !ok {
|
| 337 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 338 |
+
return
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
var data *group_service.AddParticipantStruct
|
| 342 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 343 |
+
if err != nil {
|
| 344 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 345 |
+
return
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
if data.GroupJID.String() == "" {
|
| 349 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJid is required"})
|
| 350 |
+
return
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
if data.Action == "" {
|
| 354 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "action is required"})
|
| 355 |
+
return
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
if len(data.Participants) < 1 {
|
| 359 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "participants are required"})
|
| 360 |
+
return
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
err = g.groupService.UpdateParticipant(data, instance)
|
| 364 |
+
if err != nil {
|
| 365 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 366 |
+
return
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
// Get my groups
|
| 373 |
+
// @Summary Get my groups
|
| 374 |
+
// @Description Get my groups
|
| 375 |
+
// @Tags Group
|
| 376 |
+
// @Accept json
|
| 377 |
+
// @Produce json
|
| 378 |
+
// @Success 200 {object} gin.H "success"
|
| 379 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 380 |
+
// @Router /group/myall [get]
|
| 381 |
+
func (g *groupHandler) GetMyGroups(ctx *gin.Context) {
|
| 382 |
+
getInstance := ctx.MustGet("instance")
|
| 383 |
+
|
| 384 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 385 |
+
if !ok {
|
| 386 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 387 |
+
return
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
groups, err := g.groupService.GetMyGroups(instance)
|
| 391 |
+
if err != nil {
|
| 392 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 393 |
+
return
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": groups})
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
// Join group link
|
| 400 |
+
// @Summary Join group link
|
| 401 |
+
// @Description Join group link
|
| 402 |
+
// @Tags Group
|
| 403 |
+
// @Accept json
|
| 404 |
+
// @Produce json
|
| 405 |
+
// @Param message body group_service.JoinGroupStruct true "Group data"
|
| 406 |
+
// @Success 200 {object} gin.H "success"
|
| 407 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 408 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 409 |
+
// @Router /group/join [post]
|
| 410 |
+
func (g *groupHandler) JoinGroupLink(ctx *gin.Context) {
|
| 411 |
+
getInstance := ctx.MustGet("instance")
|
| 412 |
+
|
| 413 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 414 |
+
if !ok {
|
| 415 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 416 |
+
return
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
var data *group_service.JoinGroupStruct
|
| 420 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 421 |
+
if err != nil {
|
| 422 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 423 |
+
return
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
if data.Code == "" {
|
| 427 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "code is required"})
|
| 428 |
+
return
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
err = g.groupService.JoinGroupLink(data, instance)
|
| 432 |
+
if err != nil {
|
| 433 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 434 |
+
return
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
// Leave group
|
| 441 |
+
// @Summary Leave group
|
| 442 |
+
// @Description Leave group
|
| 443 |
+
// @Tags Group
|
| 444 |
+
// @Accept json
|
| 445 |
+
// @Produce json
|
| 446 |
+
// @Param message body group_service.LeaveGroupStruct true "Group data"
|
| 447 |
+
// @Success 200 {object} gin.H "success"
|
| 448 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 449 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 450 |
+
// @Router /group/leave [post]
|
| 451 |
+
func (g *groupHandler) LeaveGroup(ctx *gin.Context) {
|
| 452 |
+
getInstance := ctx.MustGet("instance")
|
| 453 |
+
|
| 454 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 455 |
+
if !ok {
|
| 456 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 457 |
+
return
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
var data *group_service.LeaveGroupStruct
|
| 461 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 462 |
+
if err != nil {
|
| 463 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 464 |
+
return
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
if data.GroupJID.String() == "" {
|
| 468 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJid is required"})
|
| 469 |
+
return
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
err = g.groupService.LeaveGroup(data, instance)
|
| 473 |
+
if err != nil {
|
| 474 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 475 |
+
return
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
// Update group settings
|
| 482 |
+
// @Summary Update group settings
|
| 483 |
+
// @Description Update group settings (announcement, not_announcement, locked, unlocked, approval_on, approval_off, admin_add, all_member_add)
|
| 484 |
+
// @Tags Group
|
| 485 |
+
// @Accept json
|
| 486 |
+
// @Produce json
|
| 487 |
+
// @Param message body group_service.UpdateGroupSettingsStruct true "Group data"
|
| 488 |
+
// @Success 200 {object} gin.H "success"
|
| 489 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 490 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 491 |
+
// @Router /group/settings [post]
|
| 492 |
+
func (g *groupHandler) UpdateGroupSettings(ctx *gin.Context) {
|
| 493 |
+
getInstance := ctx.MustGet("instance")
|
| 494 |
+
|
| 495 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 496 |
+
if !ok {
|
| 497 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 498 |
+
return
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
var data *group_service.UpdateGroupSettingsStruct
|
| 502 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 503 |
+
if err != nil {
|
| 504 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 505 |
+
return
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
if data.GroupJID == "" {
|
| 509 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJid is required"})
|
| 510 |
+
return
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
if data.Action == "" {
|
| 514 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "action is required"})
|
| 515 |
+
return
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
err = g.groupService.UpdateGroupSettings(data, instance)
|
| 519 |
+
if err != nil {
|
| 520 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 521 |
+
return
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
func NewGroupHandler(
|
| 528 |
+
groupService group_service.GroupService,
|
| 529 |
+
) GroupHandler {
|
| 530 |
+
return &groupHandler{
|
| 531 |
+
groupService: groupService,
|
| 532 |
+
}
|
| 533 |
+
}
|
whatsapp-service/pkg/group/service/group_service.go
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package group_service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"errors"
|
| 6 |
+
"fmt"
|
| 7 |
+
"io"
|
| 8 |
+
"net/http"
|
| 9 |
+
"strings"
|
| 10 |
+
"time"
|
| 11 |
+
|
| 12 |
+
instance_model "agentdeck-whatsapp-service/pkg/instance/model"
|
| 13 |
+
logger_wrapper "agentdeck-whatsapp-service/pkg/logger"
|
| 14 |
+
"agentdeck-whatsapp-service/pkg/utils"
|
| 15 |
+
whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service"
|
| 16 |
+
"github.com/gin-gonic/gin"
|
| 17 |
+
"github.com/vincent-petithory/dataurl"
|
| 18 |
+
"go.mau.fi/whatsmeow"
|
| 19 |
+
"go.mau.fi/whatsmeow/types"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
type GroupService interface {
|
| 23 |
+
ListGroups(instance *instance_model.Instance) ([]*types.GroupInfo, error)
|
| 24 |
+
GetGroupInfo(data *GetGroupInfoStruct, instance *instance_model.Instance) (*types.GroupInfo, error)
|
| 25 |
+
GetGroupInviteLink(data *GetGroupInviteLinkStruct, instance *instance_model.Instance) (string, error)
|
| 26 |
+
SetGroupPhoto(data *SetGroupPhotoStruct, instance *instance_model.Instance) (string, error)
|
| 27 |
+
SetGroupName(data *SetGroupNameStruct, instance *instance_model.Instance) error
|
| 28 |
+
SetGroupDescription(data *SetGroupDescriptionStruct, instance *instance_model.Instance) error
|
| 29 |
+
CreateGroup(data *CreateGroupStruct, instance *instance_model.Instance) (gin.H, error)
|
| 30 |
+
UpdateParticipant(data *AddParticipantStruct, instance *instance_model.Instance) error
|
| 31 |
+
UpdateGroupSettings(data *UpdateGroupSettingsStruct, instance *instance_model.Instance) error
|
| 32 |
+
GetGroupRequestParticipants(data *GetGroupRequestParticipantsStruct, instance *instance_model.Instance) ([]EnrichedGroupParticipantRequest, error)
|
| 33 |
+
UpdateGroupRequestParticipants(data *UpdateGroupRequestParticipantsStruct, instance *instance_model.Instance) ([]types.GroupParticipant, error)
|
| 34 |
+
GetMyGroups(instance *instance_model.Instance) ([]types.GroupInfo, error)
|
| 35 |
+
JoinGroupLink(data *JoinGroupStruct, instance *instance_model.Instance) error
|
| 36 |
+
LeaveGroup(data *LeaveGroupStruct, instance *instance_model.Instance) error
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
type groupService struct {
|
| 40 |
+
clientPointer map[string]*whatsmeow.Client
|
| 41 |
+
whatsmeowService whatsmeow_service.WhatsmeowService
|
| 42 |
+
loggerWrapper *logger_wrapper.LoggerManager
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
type SimpleGroupInfo struct {
|
| 46 |
+
JID types.JID `json:"jid"`
|
| 47 |
+
GroupName string `json:"groupName"`
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
type GroupCollection struct {
|
| 51 |
+
Groups []SimpleGroupInfo
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
type GetGroupInfoStruct struct {
|
| 55 |
+
GroupJID string `json:"groupJid"`
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
type GetGroupInviteLinkStruct struct {
|
| 59 |
+
GroupJID string `json:"groupJid"`
|
| 60 |
+
Reset bool `json:"reset"`
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
type SetGroupPhotoStruct struct {
|
| 64 |
+
GroupJID string `json:"groupJid"`
|
| 65 |
+
Image string `json:"image"`
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
type SetGroupNameStruct struct {
|
| 69 |
+
GroupJID string `json:"groupJid"`
|
| 70 |
+
Name string `json:"name"`
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
type SetGroupDescriptionStruct struct {
|
| 74 |
+
GroupJID string `json:"groupJid"`
|
| 75 |
+
Description string `json:"description"`
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
type CreateGroupStruct struct {
|
| 79 |
+
GroupName string `json:"groupName"`
|
| 80 |
+
Participants []string `json:"participants"`
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
type AddParticipantStruct struct {
|
| 84 |
+
GroupJID types.JID `json:"groupJid"`
|
| 85 |
+
Participants []string `json:"participants"`
|
| 86 |
+
Action whatsmeow.ParticipantChange `json:"action"`
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
type JoinGroupStruct struct {
|
| 90 |
+
Code string `json:"code"`
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
type LeaveGroupStruct struct {
|
| 94 |
+
GroupJID types.JID `json:"groupJid"`
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
type UpdateGroupSettingsStruct struct {
|
| 98 |
+
GroupJID string `json:"groupJid"`
|
| 99 |
+
Action string `json:"action"` // announcement, not_announcement, locked, unlocked
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
type GetGroupRequestParticipantsStruct struct {
|
| 103 |
+
GroupJID string `json:"groupJid"`
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// Estrutura enriquecida com PushName
|
| 107 |
+
type EnrichedGroupParticipantRequest struct {
|
| 108 |
+
JID types.JID `json:"JID"`
|
| 109 |
+
RequestedAt time.Time `json:"RequestedAt"`
|
| 110 |
+
PushName string `json:"PushName"`
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
type UpdateGroupRequestParticipantsStruct struct {
|
| 114 |
+
GroupJID string `json:"groupJid"`
|
| 115 |
+
Action string `json:"action"` // approve, reject
|
| 116 |
+
Participants []string `json:"participants"`
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
func (g *groupService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) {
|
| 120 |
+
client := g.clientPointer[instanceId]
|
| 121 |
+
g.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil)
|
| 122 |
+
|
| 123 |
+
if client == nil {
|
| 124 |
+
g.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] No client found, attempting to start new instance", instanceId)
|
| 125 |
+
err := g.whatsmeowService.StartInstance(instanceId)
|
| 126 |
+
if err != nil {
|
| 127 |
+
g.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to start instance: %v", instanceId, err)
|
| 128 |
+
return nil, errors.New("no active session found")
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
g.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId)
|
| 132 |
+
time.Sleep(2 * time.Second)
|
| 133 |
+
|
| 134 |
+
client = g.clientPointer[instanceId]
|
| 135 |
+
g.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v",
|
| 136 |
+
instanceId,
|
| 137 |
+
client != nil,
|
| 138 |
+
client != nil && client.IsConnected())
|
| 139 |
+
|
| 140 |
+
if client == nil || !client.IsConnected() {
|
| 141 |
+
g.loggerWrapper.GetLogger(instanceId).LogError("[%s] New client validation failed - Exists: %v, Connected: %v",
|
| 142 |
+
instanceId,
|
| 143 |
+
client != nil,
|
| 144 |
+
client != nil && client.IsConnected())
|
| 145 |
+
return nil, errors.New("no active session found")
|
| 146 |
+
}
|
| 147 |
+
} else if !client.IsConnected() {
|
| 148 |
+
g.loggerWrapper.GetLogger(instanceId).LogError("[%s] Existing client is disconnected - Connected status: %v",
|
| 149 |
+
instanceId,
|
| 150 |
+
client.IsConnected())
|
| 151 |
+
return nil, errors.New("client disconnected")
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
g.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected())
|
| 155 |
+
return client, nil
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
func (g *groupService) ListGroups(instance *instance_model.Instance) ([]*types.GroupInfo, error) {
|
| 159 |
+
client, err := g.ensureClientConnected(instance.Id)
|
| 160 |
+
if err != nil {
|
| 161 |
+
return nil, err
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
resp, err := client.GetJoinedGroups(context.Background())
|
| 165 |
+
if err != nil {
|
| 166 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error getting groups: %v", instance.Id, err)
|
| 167 |
+
return nil, err
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
gc := new(GroupCollection)
|
| 171 |
+
for _, info := range resp {
|
| 172 |
+
simpleGroup := SimpleGroupInfo{
|
| 173 |
+
JID: info.JID,
|
| 174 |
+
GroupName: info.GroupName.Name,
|
| 175 |
+
}
|
| 176 |
+
gc.Groups = append(gc.Groups, simpleGroup)
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
return resp, nil
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
func (g *groupService) GetGroupInfo(data *GetGroupInfoStruct, instance *instance_model.Instance) (*types.GroupInfo, error) {
|
| 183 |
+
client, err := g.ensureClientConnected(instance.Id)
|
| 184 |
+
if err != nil {
|
| 185 |
+
return nil, err
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
recipient, ok := utils.ParseJID(data.GroupJID)
|
| 189 |
+
if !ok {
|
| 190 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id)
|
| 191 |
+
return nil, errors.New("invalid group jid")
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
resp, err := client.GetGroupInfo(context.Background(), recipient)
|
| 195 |
+
if err != nil {
|
| 196 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error mute chat: %v", instance.Id, err)
|
| 197 |
+
return nil, err
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
return resp, nil
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
func (g *groupService) GetGroupInviteLink(data *GetGroupInviteLinkStruct, instance *instance_model.Instance) (string, error) {
|
| 204 |
+
client, err := g.ensureClientConnected(instance.Id)
|
| 205 |
+
if err != nil {
|
| 206 |
+
return "", err
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
recipient, ok := utils.ParseJID(data.GroupJID)
|
| 210 |
+
if !ok {
|
| 211 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id)
|
| 212 |
+
return "", errors.New("invalid group jid")
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
resp, err := client.GetGroupInviteLink(context.Background(), recipient, data.Reset)
|
| 216 |
+
if err != nil {
|
| 217 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error mute chat: %v", instance.Id, err)
|
| 218 |
+
return "", err
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
return resp, nil
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
func (g *groupService) SetGroupPhoto(data *SetGroupPhotoStruct, instance *instance_model.Instance) (string, error) {
|
| 225 |
+
client, err := g.ensureClientConnected(instance.Id)
|
| 226 |
+
if err != nil {
|
| 227 |
+
return "", err
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
recipient, ok := utils.ParseJID(data.GroupJID)
|
| 231 |
+
if !ok {
|
| 232 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id)
|
| 233 |
+
return "", errors.New("invalid group jid")
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
var fileData []byte
|
| 237 |
+
|
| 238 |
+
if strings.HasPrefix(data.Image, "http://") || strings.HasPrefix(data.Image, "https://") {
|
| 239 |
+
resp, err := http.Get(data.Image)
|
| 240 |
+
if err != nil {
|
| 241 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Could not download image from URL", instance.Id)
|
| 242 |
+
return "", fmt.Errorf("failed to fetch image from URL: %v", err)
|
| 243 |
+
}
|
| 244 |
+
defer resp.Body.Close()
|
| 245 |
+
|
| 246 |
+
fileData, err = io.ReadAll(resp.Body)
|
| 247 |
+
if err != nil {
|
| 248 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Could not read image data from URL", instance.Id)
|
| 249 |
+
return "", fmt.Errorf("failed to read image data: %v", err)
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
} else if strings.HasPrefix(data.Image, "data:image/jpeg;base64,") || strings.HasPrefix(data.Image, "data:image/png;base64,") {
|
| 253 |
+
dataURL, err := dataurl.DecodeString(data.Image)
|
| 254 |
+
if err != nil {
|
| 255 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Could not decode base64 encoded data from payload", instance.Id)
|
| 256 |
+
return "", err
|
| 257 |
+
}
|
| 258 |
+
fileData = dataURL.Data
|
| 259 |
+
} else {
|
| 260 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Image data should start with \"data:image/jpeg;base64,\" or be a valid URL", instance.Id)
|
| 261 |
+
return "", errors.New("image data should be a valid URL or start with \"data:image/jpeg;base64,\"")
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
pictureID, err := client.SetGroupPhoto(context.Background(), recipient, fileData)
|
| 265 |
+
if err != nil {
|
| 266 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error setting group photo: %v", instance.Id, err)
|
| 267 |
+
return "", err
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
return pictureID, nil
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
func (g *groupService) SetGroupName(data *SetGroupNameStruct, instance *instance_model.Instance) error {
|
| 274 |
+
client, err := g.ensureClientConnected(instance.Id)
|
| 275 |
+
if err != nil {
|
| 276 |
+
return err
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
recipient, ok := utils.ParseJID(data.GroupJID)
|
| 280 |
+
if !ok {
|
| 281 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id)
|
| 282 |
+
return errors.New("invalid group jid")
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
g.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Attempting to set group name for %s", instance.Id, recipient.String())
|
| 286 |
+
|
| 287 |
+
err = client.SetGroupName(context.Background(), recipient, data.Name)
|
| 288 |
+
if err != nil {
|
| 289 |
+
// Log mais detalhado para erro 409
|
| 290 |
+
if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") {
|
| 291 |
+
g.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] WhatsApp returned 409 conflict when setting name. This usually means: rate limit, duplicate content, or insufficient permissions", instance.Id)
|
| 292 |
+
}
|
| 293 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error setting group name: %v", instance.Id, err)
|
| 294 |
+
return err
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
g.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Group name set successfully", instance.Id)
|
| 298 |
+
return nil
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
func (g *groupService) SetGroupDescription(data *SetGroupDescriptionStruct, instance *instance_model.Instance) error {
|
| 302 |
+
client, err := g.ensureClientConnected(instance.Id)
|
| 303 |
+
if err != nil {
|
| 304 |
+
return err
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
recipient, ok := utils.ParseJID(data.GroupJID)
|
| 308 |
+
if !ok {
|
| 309 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id)
|
| 310 |
+
return errors.New("invalid group jid")
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
g.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Attempting to set group description for %s", instance.Id, recipient.String())
|
| 314 |
+
|
| 315 |
+
// Use SetGroupTopic instead of SetGroupDescription (proper WhatsApp method)
|
| 316 |
+
// Empty strings for previousID and newID will be auto-filled by the library
|
| 317 |
+
err = client.SetGroupTopic(context.Background(), recipient, "", "", data.Description)
|
| 318 |
+
if err != nil {
|
| 319 |
+
// Log mais detalhado para erro 409
|
| 320 |
+
if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") {
|
| 321 |
+
g.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] WhatsApp returned 409 conflict when setting description. This usually means: rate limit, duplicate content, or insufficient permissions", instance.Id)
|
| 322 |
+
}
|
| 323 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error setting group description: %v", instance.Id, err)
|
| 324 |
+
return err
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
g.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Group description set successfully", instance.Id)
|
| 328 |
+
return nil
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
func (g *groupService) CreateGroup(data *CreateGroupStruct, instance *instance_model.Instance) (gin.H, error) {
|
| 332 |
+
client, err := g.ensureClientConnected(instance.Id)
|
| 333 |
+
if err != nil {
|
| 334 |
+
return nil, err
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
var participants []types.JID
|
| 338 |
+
for _, participant := range data.Participants {
|
| 339 |
+
recipient, ok := utils.ParseJID(participant)
|
| 340 |
+
participants = append(participants, recipient)
|
| 341 |
+
if !ok {
|
| 342 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id)
|
| 343 |
+
return nil, errors.New("invalid phone number")
|
| 344 |
+
}
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
resp, err := client.CreateGroup(context.Background(), whatsmeow.ReqCreateGroup{
|
| 348 |
+
Name: data.GroupName,
|
| 349 |
+
Participants: participants,
|
| 350 |
+
})
|
| 351 |
+
if err != nil {
|
| 352 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error create group: %v", instance.Id, err)
|
| 353 |
+
return nil, err
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
var failed []types.JID
|
| 357 |
+
for _, participant := range resp.Participants {
|
| 358 |
+
if participant.Error != 0 {
|
| 359 |
+
failed = append(failed, participant.JID)
|
| 360 |
+
}
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
var added []types.JID
|
| 364 |
+
infoResp, err := client.GetGroupInfo(context.Background(), resp.JID)
|
| 365 |
+
if err != nil {
|
| 366 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error get group info: %v", instance.Id, err)
|
| 367 |
+
return nil, err
|
| 368 |
+
}
|
| 369 |
+
for _, add := range infoResp.Participants {
|
| 370 |
+
added = append(added, add.JID)
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
response := gin.H{
|
| 374 |
+
"jid": resp.JID,
|
| 375 |
+
"name": resp.Name,
|
| 376 |
+
"owner": resp.OwnerJID,
|
| 377 |
+
"added": added,
|
| 378 |
+
"failed": failed,
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
return response, nil
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
func (g *groupService) UpdateParticipant(data *AddParticipantStruct, instance *instance_model.Instance) error {
|
| 385 |
+
client, err := g.ensureClientConnected(instance.Id)
|
| 386 |
+
if err != nil {
|
| 387 |
+
return err
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
var participants []types.JID
|
| 391 |
+
for _, participant := range data.Participants {
|
| 392 |
+
recipient, ok := utils.ParseJID(participant)
|
| 393 |
+
participants = append(participants, recipient)
|
| 394 |
+
if !ok {
|
| 395 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id)
|
| 396 |
+
return errors.New("invalid phone number")
|
| 397 |
+
}
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
_, err = client.UpdateGroupParticipants(context.Background(), data.GroupJID, participants, data.Action)
|
| 401 |
+
if err != nil {
|
| 402 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error create group: %v", instance.Id, err)
|
| 403 |
+
return err
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
return nil
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
func (g *groupService) GetMyGroups(instance *instance_model.Instance) ([]types.GroupInfo, error) {
|
| 410 |
+
client, err := g.ensureClientConnected(instance.Id)
|
| 411 |
+
if err != nil {
|
| 412 |
+
return nil, err
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
resp, err := client.GetJoinedGroups(context.Background())
|
| 416 |
+
if err != nil {
|
| 417 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error create group: %v", instance.Id, err)
|
| 418 |
+
return nil, err
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
var jid string = client.Store.ID.String()
|
| 422 |
+
var jidClear = strings.Split(jid, ".")[0]
|
| 423 |
+
jidOfAdmin, ok := utils.ParseJID(jidClear)
|
| 424 |
+
if !ok {
|
| 425 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id)
|
| 426 |
+
return nil, errors.New("invalid phone number")
|
| 427 |
+
}
|
| 428 |
+
var adminGroups []types.GroupInfo
|
| 429 |
+
for _, group := range resp {
|
| 430 |
+
if group.OwnerJID == jidOfAdmin {
|
| 431 |
+
adminGroups = append(adminGroups, *group)
|
| 432 |
+
_ = adminGroups
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
return adminGroups, nil
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
func (g *groupService) JoinGroupLink(data *JoinGroupStruct, instance *instance_model.Instance) error {
|
| 440 |
+
client, err := g.ensureClientConnected(instance.Id)
|
| 441 |
+
if err != nil {
|
| 442 |
+
return err
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
_, err = client.JoinGroupWithLink(context.Background(), data.Code)
|
| 446 |
+
if err != nil {
|
| 447 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error create group: %v", instance.Id, err)
|
| 448 |
+
return err
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
return nil
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
func (g *groupService) LeaveGroup(data *LeaveGroupStruct, instance *instance_model.Instance) error {
|
| 455 |
+
client, err := g.ensureClientConnected(instance.Id)
|
| 456 |
+
if err != nil {
|
| 457 |
+
return err
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
err = client.LeaveGroup(context.Background(), data.GroupJID)
|
| 461 |
+
if err != nil {
|
| 462 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error leave group: %v", instance.Id, err)
|
| 463 |
+
return err
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
return nil
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
func (g *groupService) UpdateGroupSettings(data *UpdateGroupSettingsStruct, instance *instance_model.Instance) error {
|
| 470 |
+
client, err := g.ensureClientConnected(instance.Id)
|
| 471 |
+
if err != nil {
|
| 472 |
+
return err
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
recipient, ok := utils.ParseJID(data.GroupJID)
|
| 476 |
+
if !ok {
|
| 477 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating group jid", instance.Id)
|
| 478 |
+
return errors.New("invalid group jid")
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
// Validate action
|
| 482 |
+
validActions := map[string]bool{
|
| 483 |
+
"announcement": true,
|
| 484 |
+
"not_announcement": true,
|
| 485 |
+
"locked": true,
|
| 486 |
+
"unlocked": true,
|
| 487 |
+
"approval_on": true,
|
| 488 |
+
"approval_off": true,
|
| 489 |
+
"admin_add": true,
|
| 490 |
+
"all_member_add": true,
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
if !validActions[data.Action] {
|
| 494 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Invalid action: %s", instance.Id, data.Action)
|
| 495 |
+
return errors.New("invalid action. Valid actions: announcement, not_announcement, locked, unlocked, approval_on, approval_off, admin_add, all_member_add")
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
// Apply settings based on action
|
| 499 |
+
switch data.Action {
|
| 500 |
+
case "announcement":
|
| 501 |
+
err = client.SetGroupAnnounce(context.Background(), recipient, true)
|
| 502 |
+
case "not_announcement":
|
| 503 |
+
err = client.SetGroupAnnounce(context.Background(), recipient, false)
|
| 504 |
+
case "locked":
|
| 505 |
+
err = client.SetGroupLocked(context.Background(), recipient, true)
|
| 506 |
+
case "unlocked":
|
| 507 |
+
err = client.SetGroupLocked(context.Background(), recipient, false)
|
| 508 |
+
case "approval_on":
|
| 509 |
+
err = client.SetGroupJoinApprovalMode(context.Background(), recipient, true)
|
| 510 |
+
case "approval_off":
|
| 511 |
+
err = client.SetGroupJoinApprovalMode(context.Background(), recipient, false)
|
| 512 |
+
case "admin_add":
|
| 513 |
+
err = client.SetGroupMemberAddMode(context.Background(), recipient, "admin_add")
|
| 514 |
+
case "all_member_add":
|
| 515 |
+
err = client.SetGroupMemberAddMode(context.Background(), recipient, "all_member_add")
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
if err != nil {
|
| 519 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error updating group settings: %v", instance.Id, err)
|
| 520 |
+
return err
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
g.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Group settings updated successfully: %s", instance.Id, data.Action)
|
| 524 |
+
return nil
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
func (g *groupService) GetGroupRequestParticipants(data *GetGroupRequestParticipantsStruct, instance *instance_model.Instance) ([]EnrichedGroupParticipantRequest, error) {
|
| 528 |
+
client, err := g.ensureClientConnected(instance.Id)
|
| 529 |
+
if err != nil {
|
| 530 |
+
return nil, err
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
recipient, ok := utils.ParseJID(data.GroupJID)
|
| 534 |
+
if !ok {
|
| 535 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating group jid", instance.Id)
|
| 536 |
+
return nil, errors.New("invalid group jid")
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
requests, err := client.GetGroupRequestParticipants(context.Background(), recipient)
|
| 540 |
+
if err != nil {
|
| 541 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error getting group request participants: %v", instance.Id, err)
|
| 542 |
+
return nil, err
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
g.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Retrieved %d pending group requests", instance.Id, len(requests))
|
| 546 |
+
|
| 547 |
+
// Enriquecer com informações de usuário (PushName)
|
| 548 |
+
enrichedRequests := make([]EnrichedGroupParticipantRequest, len(requests))
|
| 549 |
+
jidsToFetch := make([]types.JID, 0, len(requests))
|
| 550 |
+
|
| 551 |
+
for _, req := range requests {
|
| 552 |
+
if req.JID.User != "" {
|
| 553 |
+
jidsToFetch = append(jidsToFetch, req.JID)
|
| 554 |
+
}
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
// Buscar informações de usuário em lote
|
| 558 |
+
userInfoMap := make(map[types.JID]types.UserInfo)
|
| 559 |
+
if len(jidsToFetch) > 0 {
|
| 560 |
+
userInfoMap, err = client.GetUserInfo(context.Background(), jidsToFetch)
|
| 561 |
+
if err != nil {
|
| 562 |
+
g.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Could not fetch user info: %v", instance.Id, err)
|
| 563 |
+
// Continuar sem pushName se falhar
|
| 564 |
+
}
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
// Montar resposta enriquecida
|
| 568 |
+
for i, req := range requests {
|
| 569 |
+
enrichedRequests[i] = EnrichedGroupParticipantRequest{
|
| 570 |
+
JID: req.JID,
|
| 571 |
+
RequestedAt: req.RequestedAt,
|
| 572 |
+
PushName: "",
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
// Tentar obter PushName
|
| 576 |
+
lookupJID := req.JID
|
| 577 |
+
|
| 578 |
+
if userInfo, found := userInfoMap[lookupJID]; found {
|
| 579 |
+
// VerifiedName é ponteiro, verificar se não é nil
|
| 580 |
+
if userInfo.VerifiedName != nil && userInfo.VerifiedName.Details.GetVerifiedName() != "" {
|
| 581 |
+
enrichedRequests[i].PushName = userInfo.VerifiedName.Details.GetVerifiedName()
|
| 582 |
+
}
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
// Tentar obter do store de contatos se não tiver VerifiedName
|
| 586 |
+
if enrichedRequests[i].PushName == "" && client.Store.Contacts != nil {
|
| 587 |
+
if contactInfo, err := client.Store.Contacts.GetContact(context.Background(), lookupJID); err == nil && contactInfo.PushName != "" {
|
| 588 |
+
enrichedRequests[i].PushName = contactInfo.PushName
|
| 589 |
+
} else if contactInfo.FullName != "" {
|
| 590 |
+
enrichedRequests[i].PushName = contactInfo.FullName
|
| 591 |
+
}
|
| 592 |
+
}
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
return enrichedRequests, nil
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
func (g *groupService) UpdateGroupRequestParticipants(data *UpdateGroupRequestParticipantsStruct, instance *instance_model.Instance) ([]types.GroupParticipant, error) {
|
| 599 |
+
client, err := g.ensureClientConnected(instance.Id)
|
| 600 |
+
if err != nil {
|
| 601 |
+
return nil, err
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
recipient, ok := utils.ParseJID(data.GroupJID)
|
| 605 |
+
if !ok {
|
| 606 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating group jid", instance.Id)
|
| 607 |
+
return nil, errors.New("invalid group jid")
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
// Validate action
|
| 611 |
+
var action whatsmeow.ParticipantRequestChange
|
| 612 |
+
switch data.Action {
|
| 613 |
+
case "approve":
|
| 614 |
+
action = whatsmeow.ParticipantChangeApprove
|
| 615 |
+
case "reject":
|
| 616 |
+
action = whatsmeow.ParticipantChangeReject
|
| 617 |
+
default:
|
| 618 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Invalid action: %s", instance.Id, data.Action)
|
| 619 |
+
return nil, errors.New("invalid action. Valid actions: approve, reject")
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
// Parse participants JIDs
|
| 623 |
+
var participants []types.JID
|
| 624 |
+
for _, participant := range data.Participants {
|
| 625 |
+
participantJID, ok := utils.ParseJID(participant)
|
| 626 |
+
if !ok {
|
| 627 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating participant jid: %s", instance.Id, participant)
|
| 628 |
+
return nil, errors.New("invalid participant jid: " + participant)
|
| 629 |
+
}
|
| 630 |
+
participants = append(participants, participantJID)
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
results, err := client.UpdateGroupRequestParticipants(context.Background(), recipient, participants, action)
|
| 634 |
+
if err != nil {
|
| 635 |
+
g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error updating group request participants: %v", instance.Id, err)
|
| 636 |
+
return nil, err
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
g.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Successfully %sd %d participants", instance.Id, data.Action, len(participants))
|
| 640 |
+
return results, nil
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
func NewGroupService(
|
| 644 |
+
clientPointer map[string]*whatsmeow.Client,
|
| 645 |
+
whatsmeowService whatsmeow_service.WhatsmeowService,
|
| 646 |
+
loggerWrapper *logger_wrapper.LoggerManager,
|
| 647 |
+
) GroupService {
|
| 648 |
+
return &groupService{
|
| 649 |
+
clientPointer: clientPointer,
|
| 650 |
+
whatsmeowService: whatsmeowService,
|
| 651 |
+
loggerWrapper: loggerWrapper,
|
| 652 |
+
}
|
| 653 |
+
}
|
whatsapp-service/pkg/instance/handler/instance_handler.go
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package instance_handler
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
"time"
|
| 6 |
+
|
| 7 |
+
"github.com/gin-gonic/gin"
|
| 8 |
+
|
| 9 |
+
config "agentdeck-whatsapp-service/pkg/config"
|
| 10 |
+
instance_model "agentdeck-whatsapp-service/pkg/instance/model"
|
| 11 |
+
instance_service "agentdeck-whatsapp-service/pkg/instance/service"
|
| 12 |
+
"agentdeck-whatsapp-service/pkg/utils"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
type InstanceHandler interface {
|
| 16 |
+
Create(ctx *gin.Context)
|
| 17 |
+
Connect(ctx *gin.Context)
|
| 18 |
+
Reconnect(ctx *gin.Context)
|
| 19 |
+
Disconnect(ctx *gin.Context)
|
| 20 |
+
Logout(ctx *gin.Context)
|
| 21 |
+
Delete(ctx *gin.Context)
|
| 22 |
+
Status(ctx *gin.Context)
|
| 23 |
+
Qr(ctx *gin.Context)
|
| 24 |
+
All(ctx *gin.Context)
|
| 25 |
+
Info(ctx *gin.Context)
|
| 26 |
+
Pair(ctx *gin.Context)
|
| 27 |
+
SetProxy(ctx *gin.Context)
|
| 28 |
+
DeleteProxy(ctx *gin.Context)
|
| 29 |
+
ForceReconnect(ctx *gin.Context)
|
| 30 |
+
GetLogs(ctx *gin.Context)
|
| 31 |
+
GetAdvancedSettings(ctx *gin.Context)
|
| 32 |
+
UpdateAdvancedSettings(ctx *gin.Context)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
type instanceHandler struct {
|
| 36 |
+
config *config.Config
|
| 37 |
+
instanceService instance_service.InstanceService
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Create a new instance
|
| 41 |
+
// @Summary Create a new instance
|
| 42 |
+
// @Description Creates a new instance with the provided data including optional advanced settings
|
| 43 |
+
// @Tags Instance
|
| 44 |
+
// @Accept json
|
| 45 |
+
// @Produce json
|
| 46 |
+
// @Param instance body instance_service.CreateStruct true "Instance data with optional advanced settings"
|
| 47 |
+
// @Success 200 {object} gin.H "Instance created successfully"
|
| 48 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 49 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 50 |
+
// @Router /instance/create [post]
|
| 51 |
+
func (i *instanceHandler) Create(ctx *gin.Context) {
|
| 52 |
+
var data *instance_service.CreateStruct
|
| 53 |
+
err := ctx.ShouldBindJSON(&data)
|
| 54 |
+
if err != nil {
|
| 55 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 56 |
+
return
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
if data.Name == "" {
|
| 60 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
| 61 |
+
return
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
if data.Token == "" {
|
| 65 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "token is required"})
|
| 66 |
+
return
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
if data.Proxy != nil {
|
| 70 |
+
if data.Proxy.Port == "" {
|
| 71 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "proxy port is required"})
|
| 72 |
+
return
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
if data.Proxy.Password == "" {
|
| 76 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "proxy password is required"})
|
| 77 |
+
return
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
if data.Proxy.Username == "" {
|
| 81 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "proxy username is required"})
|
| 82 |
+
return
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
if data.Proxy.Host == "" {
|
| 86 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "proxy host is required"})
|
| 87 |
+
return
|
| 88 |
+
}
|
| 89 |
+
} else {
|
| 90 |
+
if i.config.ProxyHost != "" && i.config.ProxyPort != "" && i.config.ProxyUsername != "" && i.config.ProxyPassword != "" {
|
| 91 |
+
data.Proxy = &instance_service.ProxyConfig{
|
| 92 |
+
Host: i.config.ProxyHost,
|
| 93 |
+
Port: i.config.ProxyPort,
|
| 94 |
+
Username: i.config.ProxyUsername,
|
| 95 |
+
Password: i.config.ProxyPassword,
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
createdInstance, err := i.instanceService.Create(data)
|
| 101 |
+
if err != nil {
|
| 102 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 103 |
+
return
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": createdInstance})
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Connect to instance
|
| 110 |
+
// @Summary Connect to instance
|
| 111 |
+
// @Description Connect to instance with the provided data
|
| 112 |
+
// @Tags Instance
|
| 113 |
+
// @Accept json
|
| 114 |
+
// @Produce json
|
| 115 |
+
// @Param instance body instance_service.ConnectStruct true "Instance data"
|
| 116 |
+
// @Success 200 {object} gin.H "Instance connected successfully"
|
| 117 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 118 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 119 |
+
// @Router /instance/connect [post]
|
| 120 |
+
func (i *instanceHandler) Connect(ctx *gin.Context) {
|
| 121 |
+
getInstance := ctx.MustGet("instance")
|
| 122 |
+
|
| 123 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 124 |
+
if !ok {
|
| 125 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 126 |
+
return
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
var data *instance_service.ConnectStruct
|
| 130 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 131 |
+
if err != nil {
|
| 132 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 133 |
+
return
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
instance, jid, eventString, err := i.instanceService.Connect(data, instance)
|
| 137 |
+
if err != nil {
|
| 138 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 139 |
+
return
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
ctx.Set("instance", instance)
|
| 143 |
+
|
| 144 |
+
responseData := gin.H{
|
| 145 |
+
"jid": jid,
|
| 146 |
+
"webhookUrl": instance.Webhook,
|
| 147 |
+
"eventString": eventString,
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData})
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
// Reconnect to instance
|
| 154 |
+
// @Summary Reconnect to instance
|
| 155 |
+
// @Description Reconnect to instance
|
| 156 |
+
// @Tags Instance
|
| 157 |
+
// @Accept json
|
| 158 |
+
// @Produce json
|
| 159 |
+
// @Success 200 {object} gin.H "Instance reconnected successfully"
|
| 160 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 161 |
+
// @Router /instance/reconnect [post]
|
| 162 |
+
func (i *instanceHandler) Reconnect(ctx *gin.Context) {
|
| 163 |
+
getInstance := ctx.MustGet("instance")
|
| 164 |
+
|
| 165 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 166 |
+
if !ok {
|
| 167 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 168 |
+
return
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
err := i.instanceService.Reconnect(instance)
|
| 172 |
+
if err != nil {
|
| 173 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 174 |
+
return
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
// Disconnect from instance
|
| 181 |
+
// @Summary Disconnect from instance
|
| 182 |
+
// @Description Disconnect from instance
|
| 183 |
+
// @Tags Instance
|
| 184 |
+
// @Accept json
|
| 185 |
+
// @Produce json
|
| 186 |
+
// @Success 200 {object} gin.H "Instance disconnected successfully"
|
| 187 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 188 |
+
// @Router /instance/disconnect [post]
|
| 189 |
+
func (i *instanceHandler) Disconnect(ctx *gin.Context) {
|
| 190 |
+
getInstance := ctx.MustGet("instance")
|
| 191 |
+
|
| 192 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 193 |
+
if !ok {
|
| 194 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 195 |
+
return
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
updateInstance, err := i.instanceService.Disconnect(instance)
|
| 199 |
+
if err != nil {
|
| 200 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 201 |
+
return
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
ctx.Set("instance", updateInstance)
|
| 205 |
+
|
| 206 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
// Logout from instance
|
| 210 |
+
// @Summary Logout from instance
|
| 211 |
+
// @Description Logout from instance
|
| 212 |
+
// @Tags Instance
|
| 213 |
+
// @Accept json
|
| 214 |
+
// @Produce json
|
| 215 |
+
// @Success 200 {object} gin.H "Instance logged out successfully"
|
| 216 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 217 |
+
// @Router /instance/logout [delete]
|
| 218 |
+
func (i *instanceHandler) Logout(ctx *gin.Context) {
|
| 219 |
+
getInstance := ctx.MustGet("instance")
|
| 220 |
+
|
| 221 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 222 |
+
if !ok {
|
| 223 |
+
ctx.JSON(http.StatusNotFound, gin.H{"error": "instance not found"})
|
| 224 |
+
return
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
updateInstance, err := i.instanceService.Logout(instance)
|
| 228 |
+
if err != nil {
|
| 229 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 230 |
+
return
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
ctx.Set("instance", updateInstance)
|
| 234 |
+
|
| 235 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
// Get instance status
|
| 239 |
+
// @Summary Get instance status
|
| 240 |
+
// @Description Get instance status
|
| 241 |
+
// @Tags Instance
|
| 242 |
+
// @Accept json
|
| 243 |
+
// @Produce json
|
| 244 |
+
// @Success 200 {object} gin.H "Instance status"
|
| 245 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 246 |
+
// @Router /instance/status [get]
|
| 247 |
+
func (i *instanceHandler) Status(ctx *gin.Context) {
|
| 248 |
+
getInstance := ctx.MustGet("instance")
|
| 249 |
+
|
| 250 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 251 |
+
if !ok {
|
| 252 |
+
ctx.JSON(http.StatusNotFound, gin.H{"error": "instance not found"})
|
| 253 |
+
return
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
status, err := i.instanceService.Status(instance)
|
| 257 |
+
if err != nil {
|
| 258 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 259 |
+
return
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": status})
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
// Get instance QR code
|
| 266 |
+
// @Summary Get instance QR code
|
| 267 |
+
// @Description Get instance QR code
|
| 268 |
+
// @Tags Instance
|
| 269 |
+
// @Accept json
|
| 270 |
+
// @Produce json
|
| 271 |
+
// @Success 200 {object} gin.H "Instance QR code"
|
| 272 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 273 |
+
// @Router /instance/qr [get]
|
| 274 |
+
func (i *instanceHandler) Qr(ctx *gin.Context) {
|
| 275 |
+
getInstance := ctx.MustGet("instance")
|
| 276 |
+
|
| 277 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 278 |
+
if !ok {
|
| 279 |
+
ctx.JSON(http.StatusNotFound, gin.H{"error": "instance not found"})
|
| 280 |
+
return
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
qrcode, err := i.instanceService.GetQr(instance)
|
| 284 |
+
if err != nil {
|
| 285 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 286 |
+
return
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": qrcode})
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
// Request pairing code
|
| 293 |
+
// @Summary Request pairing code
|
| 294 |
+
// @Description Request pairing code
|
| 295 |
+
// @Tags Instance
|
| 296 |
+
// @Accept json
|
| 297 |
+
// @Produce json
|
| 298 |
+
// @Param instance body instance_service.PairStruct true "Instance data"
|
| 299 |
+
// @Success 200 {object} gin.H "Pairing code"
|
| 300 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 301 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 302 |
+
// @Router /instance/pair [post]
|
| 303 |
+
func (i *instanceHandler) Pair(ctx *gin.Context) {
|
| 304 |
+
getInstance := ctx.MustGet("instance")
|
| 305 |
+
|
| 306 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 307 |
+
if !ok {
|
| 308 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 309 |
+
return
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
var data *instance_service.PairStruct
|
| 313 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 314 |
+
if err != nil {
|
| 315 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 316 |
+
return
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
if data.Phone == "" {
|
| 320 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone is required"})
|
| 321 |
+
return
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
pairingCode, err := i.instanceService.Pair(data, instance)
|
| 325 |
+
if err != nil {
|
| 326 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 327 |
+
return
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": pairingCode})
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
// Get all instances
|
| 334 |
+
// @Summary Get all instances
|
| 335 |
+
// @Description Get all instances
|
| 336 |
+
// @Tags Instance
|
| 337 |
+
// @Accept json
|
| 338 |
+
// @Produce json
|
| 339 |
+
// @Success 200 {object} gin.H "All instances"
|
| 340 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 341 |
+
// @Router /instance/all [get]
|
| 342 |
+
func (i *instanceHandler) All(ctx *gin.Context) {
|
| 343 |
+
instances, err := i.instanceService.GetAll()
|
| 344 |
+
if err != nil {
|
| 345 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 346 |
+
return
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": instances})
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
// Get instance
|
| 353 |
+
// @Summary Get instance
|
| 354 |
+
// @Description Get instance
|
| 355 |
+
// @Tags Instance
|
| 356 |
+
// @Accept json
|
| 357 |
+
// @Produce json
|
| 358 |
+
// @Param instanceId path string true "Instance Id"
|
| 359 |
+
// @Success 200 {object} gin.H "Instance"
|
| 360 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 361 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 362 |
+
// @Router /instance/info/{instanceId} [get]
|
| 363 |
+
func (i *instanceHandler) Info(ctx *gin.Context) {
|
| 364 |
+
instanceId := ctx.Param("instanceId")
|
| 365 |
+
|
| 366 |
+
if instanceId == "" {
|
| 367 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "instanceId is required"})
|
| 368 |
+
return
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
instance, err := i.instanceService.Info(instanceId)
|
| 372 |
+
if err != nil {
|
| 373 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 374 |
+
return
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": instance})
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
// Delete instance
|
| 381 |
+
// @Summary Delete instance
|
| 382 |
+
// @Description Delete instance
|
| 383 |
+
// @Tags Instance
|
| 384 |
+
// @Accept json
|
| 385 |
+
// @Produce json
|
| 386 |
+
// @Param instanceId path string true "Instance Id"
|
| 387 |
+
// @Success 200 {object} gin.H "Instance deleted successfully"
|
| 388 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 389 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 390 |
+
// @Router /instance/delete/{instanceId} [delete]
|
| 391 |
+
func (i *instanceHandler) Delete(ctx *gin.Context) {
|
| 392 |
+
instanceId := ctx.Param("instanceId")
|
| 393 |
+
|
| 394 |
+
if instanceId == "" {
|
| 395 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "instanceId is required"})
|
| 396 |
+
return
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
err := i.instanceService.Delete(instanceId)
|
| 400 |
+
if err != nil {
|
| 401 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 402 |
+
return
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
// Set proxy
|
| 409 |
+
// @Summary Set proxy configuration
|
| 410 |
+
// @Description Set proxy configuration for an instance
|
| 411 |
+
// @Tags Instance
|
| 412 |
+
// @Accept json
|
| 413 |
+
// @Produce json
|
| 414 |
+
// @Param instanceId path string true "Instance id"
|
| 415 |
+
// @Param proxy body instance_service.SetProxyStruct true "Proxy configuration"
|
| 416 |
+
// @Success 200 {object} gin.H "Proxy set successfully"
|
| 417 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 418 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 419 |
+
// @Router /instance/proxy/{instanceId} [post]
|
| 420 |
+
func (i *instanceHandler) SetProxy(ctx *gin.Context) {
|
| 421 |
+
instanceId := ctx.Param("instanceId")
|
| 422 |
+
|
| 423 |
+
if instanceId == "" {
|
| 424 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "instanceId is required"})
|
| 425 |
+
return
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
var data *instance_service.SetProxyStruct
|
| 429 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 430 |
+
if err != nil {
|
| 431 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 432 |
+
return
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
// Validate required fields
|
| 436 |
+
if data.Host == "" {
|
| 437 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "host is required"})
|
| 438 |
+
return
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
if data.Port == "" {
|
| 442 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "port is required"})
|
| 443 |
+
return
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
err = i.instanceService.SetProxyFromStruct(instanceId, data)
|
| 447 |
+
if err != nil {
|
| 448 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 449 |
+
return
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
responseData := gin.H{
|
| 453 |
+
"protocol": utils.NormalizeProxyProtocol(data.Protocol, data.Port),
|
| 454 |
+
"host": data.Host,
|
| 455 |
+
"port": data.Port,
|
| 456 |
+
"hasAuth": data.Username != "" && data.Password != "",
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData})
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
// Delete proxy
|
| 463 |
+
// @Summary Delete proxy
|
| 464 |
+
// @Description Delete proxy
|
| 465 |
+
// @Tags Instance
|
| 466 |
+
// @Accept json
|
| 467 |
+
// @Produce json
|
| 468 |
+
// @Param instanceId path string true "Instance id"
|
| 469 |
+
// @Success 200 {object} gin.H "Proxy deleted successfully"
|
| 470 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 471 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 472 |
+
// @Router /instance/proxy/{instanceId} [delete]
|
| 473 |
+
func (i *instanceHandler) DeleteProxy(ctx *gin.Context) {
|
| 474 |
+
instanceId := ctx.Param("instanceId")
|
| 475 |
+
|
| 476 |
+
if instanceId == "" {
|
| 477 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
|
| 478 |
+
return
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
err := i.instanceService.RemoveProxy(instanceId)
|
| 482 |
+
if err != nil {
|
| 483 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 484 |
+
return
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
// Force reconnect
|
| 491 |
+
// @Summary Force reconnect
|
| 492 |
+
// @Description Force reconnect
|
| 493 |
+
// @Tags Instance
|
| 494 |
+
// @Accept json
|
| 495 |
+
// @Produce json
|
| 496 |
+
// @Param instanceId path string true "Instance Id"
|
| 497 |
+
// @Param instance body instance_service.ForceReconnectStruct true "Instance data"
|
| 498 |
+
// @Success 200 {object} gin.H "Instance force reconnected successfully"
|
| 499 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 500 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 501 |
+
// @Router /instance/forcereconnect/{instanceId} [post]
|
| 502 |
+
func (i *instanceHandler) ForceReconnect(ctx *gin.Context) {
|
| 503 |
+
instanceId := ctx.Param("instanceId")
|
| 504 |
+
|
| 505 |
+
if instanceId == "" {
|
| 506 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "instanceId is required"})
|
| 507 |
+
return
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
var data *instance_service.ForceReconnectStruct
|
| 511 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 512 |
+
if err != nil {
|
| 513 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 514 |
+
return
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
var number string
|
| 518 |
+
if data.Number == "" {
|
| 519 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "number is required"})
|
| 520 |
+
return
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
number = data.Number
|
| 524 |
+
|
| 525 |
+
err = i.instanceService.ForceReconnect(instanceId, number)
|
| 526 |
+
if err != nil {
|
| 527 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 528 |
+
return
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
type GetLogsQuery struct {
|
| 535 |
+
StartDate string `form:"start_date"`
|
| 536 |
+
EndDate string `form:"end_date"`
|
| 537 |
+
Level string `form:"level"`
|
| 538 |
+
Limit int `form:"limit"`
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
// GetLogs returns the log entries for an instance
|
| 542 |
+
// @Summary Get instance logs
|
| 543 |
+
// @Description Returns log entries for an instance, filterable by date range, level and limit
|
| 544 |
+
// @Tags Instance
|
| 545 |
+
// @Produce json
|
| 546 |
+
// @Param instanceId path string true "Instance Id"
|
| 547 |
+
// @Param start_date query string false "Start date (YYYY-MM-DD, defaults to 7 days ago)"
|
| 548 |
+
// @Param end_date query string false "End date (YYYY-MM-DD, defaults to now)"
|
| 549 |
+
// @Param level query string false "Log level filter"
|
| 550 |
+
// @Param limit query int false "Max number of entries"
|
| 551 |
+
// @Success 200 {object} gin.H "Logs"
|
| 552 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 553 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 554 |
+
// @Router /instance/logs/{instanceId} [get]
|
| 555 |
+
func (h *instanceHandler) GetLogs(c *gin.Context) {
|
| 556 |
+
instanceId := c.Param("instanceId")
|
| 557 |
+
|
| 558 |
+
var query GetLogsQuery
|
| 559 |
+
if err := c.ShouldBindQuery(&query); err != nil {
|
| 560 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 561 |
+
return
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
// Converte as datas
|
| 565 |
+
startDate, err := time.Parse("2006-01-02", query.StartDate)
|
| 566 |
+
if err != nil {
|
| 567 |
+
startDate = time.Now().AddDate(0, 0, -7) // Default: 7 dias atrás
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
endDate, err := time.Parse("2006-01-02", query.EndDate)
|
| 571 |
+
if err != nil {
|
| 572 |
+
endDate = time.Now()
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
// Ajusta o endDate para o final do dia
|
| 576 |
+
endDate = time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 999999999, time.UTC)
|
| 577 |
+
|
| 578 |
+
if query.Limit == 0 {
|
| 579 |
+
query.Limit = 100 // Default: 100 registros
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
logs, err := h.instanceService.GetLogs(instanceId, startDate, endDate, query.Level, query.Limit)
|
| 583 |
+
if err != nil {
|
| 584 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 585 |
+
return
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
c.JSON(http.StatusOK, logs)
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
// GetAdvancedSettings retrieves advanced settings for an instance
|
| 592 |
+
// @Summary Get advanced settings
|
| 593 |
+
// @Description Get advanced settings for a specific instance
|
| 594 |
+
// @Tags Instance
|
| 595 |
+
// @Produce json
|
| 596 |
+
// @Param instanceId path string true "Instance ID"
|
| 597 |
+
// @Success 200 {object} instance_model.AdvancedSettings "Advanced settings retrieved successfully"
|
| 598 |
+
// @Failure 400 {object} gin.H "Invalid instance ID"
|
| 599 |
+
// @Failure 404 {object} gin.H "Instance not found"
|
| 600 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 601 |
+
// @Router /instance/{instanceId}/advanced-settings [get]
|
| 602 |
+
func (h *instanceHandler) GetAdvancedSettings(c *gin.Context) {
|
| 603 |
+
instanceId := c.Param("instanceId")
|
| 604 |
+
|
| 605 |
+
if instanceId == "" {
|
| 606 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": "instanceId is required"})
|
| 607 |
+
return
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
settings, err := h.instanceService.GetAdvancedSettings(instanceId)
|
| 611 |
+
if err != nil {
|
| 612 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 613 |
+
return
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
c.JSON(http.StatusOK, settings)
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
// UpdateAdvancedSettings updates advanced settings for an instance
|
| 620 |
+
// @Summary Update advanced settings
|
| 621 |
+
// @Description Update advanced settings for a specific instance
|
| 622 |
+
// @Tags Instance
|
| 623 |
+
// @Accept json
|
| 624 |
+
// @Produce json
|
| 625 |
+
// @Param instanceId path string true "Instance ID"
|
| 626 |
+
// @Param settings body instance_model.AdvancedSettings true "Advanced settings data"
|
| 627 |
+
// @Success 200 {object} gin.H "Advanced settings updated successfully"
|
| 628 |
+
// @Failure 400 {object} gin.H "Invalid request data"
|
| 629 |
+
// @Failure 404 {object} gin.H "Instance not found"
|
| 630 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 631 |
+
// @Router /instance/{instanceId}/advanced-settings [put]
|
| 632 |
+
func (h *instanceHandler) UpdateAdvancedSettings(c *gin.Context) {
|
| 633 |
+
instanceId := c.Param("instanceId")
|
| 634 |
+
|
| 635 |
+
if instanceId == "" {
|
| 636 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": "instanceId is required"})
|
| 637 |
+
return
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
var settings instance_model.AdvancedSettings
|
| 641 |
+
if err := c.ShouldBindJSON(&settings); err != nil {
|
| 642 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 643 |
+
return
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
err := h.instanceService.UpdateAdvancedSettings(instanceId, &settings)
|
| 647 |
+
if err != nil {
|
| 648 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 649 |
+
return
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
c.JSON(http.StatusOK, gin.H{
|
| 653 |
+
"message": "Advanced settings updated successfully",
|
| 654 |
+
"settings": settings,
|
| 655 |
+
})
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
func NewInstanceHandler(instanceService instance_service.InstanceService, config *config.Config) InstanceHandler {
|
| 659 |
+
return &instanceHandler{instanceService: instanceService, config: config}
|
| 660 |
+
}
|
whatsapp-service/pkg/instance/model/instance_model.go
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package instance_model
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
|
| 6 |
+
"github.com/google/uuid"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
type Instance struct {
|
| 10 |
+
Id string `json:"id"`
|
| 11 |
+
Name string `json:"name"`
|
| 12 |
+
Token string `json:"token"`
|
| 13 |
+
Webhook string `json:"webhook"`
|
| 14 |
+
RabbitmqEnable string `json:"rabbitmqEnable"`
|
| 15 |
+
WebSocketEnable string `json:"websocketEnable"`
|
| 16 |
+
NatsEnable string `json:"natsEnable"`
|
| 17 |
+
Jid string `json:"jid"`
|
| 18 |
+
Qrcode string `json:"qrcode"`
|
| 19 |
+
Connected bool `json:"connected"`
|
| 20 |
+
Expiration int64 `json:"expiration"`
|
| 21 |
+
DisconnectReason string `json:"disconnect_reason"`
|
| 22 |
+
Events string `json:"events"`
|
| 23 |
+
OsName string `json:"os_name"`
|
| 24 |
+
Proxy string `json:"proxy"`
|
| 25 |
+
ClientName string `json:"client_name"`
|
| 26 |
+
CreatedAt time.Time `json:"createdAt"`
|
| 27 |
+
|
| 28 |
+
// Advanced Settings
|
| 29 |
+
AlwaysOnline bool `json:"alwaysOnline"`
|
| 30 |
+
RejectCall bool `json:"rejectCall"`
|
| 31 |
+
MsgRejectCall string `json:"msgRejectCall"`
|
| 32 |
+
ReadMessages bool `json:"readMessages"`
|
| 33 |
+
IgnoreGroups bool `json:"ignoreGroups"`
|
| 34 |
+
IgnoreStatus bool `json:"ignoreStatus"`
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// AdvancedSettings representa as configurações avançadas de uma instância
|
| 38 |
+
type AdvancedSettings struct {
|
| 39 |
+
AlwaysOnline bool `json:"alwaysOnline"`
|
| 40 |
+
RejectCall bool `json:"rejectCall"`
|
| 41 |
+
MsgRejectCall string `json:"msgRejectCall"`
|
| 42 |
+
ReadMessages bool `json:"readMessages"`
|
| 43 |
+
IgnoreGroups bool `json:"ignoreGroups"`
|
| 44 |
+
IgnoreStatus bool `json:"ignoreStatus"`
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// EnsureID assigns a UUID when not already set.
|
| 48 |
+
func (m *Instance) EnsureID() {
|
| 49 |
+
if m.Id == "" {
|
| 50 |
+
m.Id = uuid.New().String()
|
| 51 |
+
}
|
| 52 |
+
}
|
whatsapp-service/pkg/instance/repository/instance_repository.go
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package instance_repository
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
"time"
|
| 7 |
+
|
| 8 |
+
instance_model "agentdeck-whatsapp-service/pkg/instance/model"
|
| 9 |
+
"agentdeck-whatsapp-service/pkg/supabase"
|
| 10 |
+
"github.com/google/uuid"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
type InstanceRepository interface {
|
| 14 |
+
Create(instance instance_model.Instance) (*instance_model.Instance, error)
|
| 15 |
+
GetInstanceByID(instanceId string) (*instance_model.Instance, error)
|
| 16 |
+
GetConnectedInstanceByID(instanceId string) (*instance_model.Instance, error)
|
| 17 |
+
GetInstanceByToken(token string) (*instance_model.Instance, error)
|
| 18 |
+
GetInstanceByName(name string) (*instance_model.Instance, error)
|
| 19 |
+
Update(*instance_model.Instance) error
|
| 20 |
+
UpdateConnected(userId string, status bool, disconnectReason string) error
|
| 21 |
+
UpdateQrcode(userId string, qr string) error
|
| 22 |
+
UpdateProxy(userId string, proxy string) error
|
| 23 |
+
UpdateJid(userId string, jid string) error
|
| 24 |
+
GetAllConnectedInstances() ([]*instance_model.Instance, error)
|
| 25 |
+
GetAllConnectedInstancesByClientName(clientName string) ([]*instance_model.Instance, error)
|
| 26 |
+
GetAll(clientName string) ([]*instance_model.Instance, error)
|
| 27 |
+
Delete(instanceId string) error
|
| 28 |
+
GetAdvancedSettings(instanceId string) (*instance_model.AdvancedSettings, error)
|
| 29 |
+
UpdateAdvancedSettings(instanceId string, settings *instance_model.AdvancedSettings) error
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// instanceRow is the snake_case PostgREST representation of an instance row.
|
| 33 |
+
// The public API model keeps its camelCase JSON tags; this row maps only the DB.
|
| 34 |
+
type instanceRow struct {
|
| 35 |
+
Id string `json:"id"`
|
| 36 |
+
Name string `json:"name"`
|
| 37 |
+
Token string `json:"token"`
|
| 38 |
+
Webhook string `json:"webhook"`
|
| 39 |
+
RabbitmqEnable string `json:"rabbitmq_enable"`
|
| 40 |
+
WebSocketEnable string `json:"web_socket_enable"`
|
| 41 |
+
NatsEnable string `json:"nats_enable"`
|
| 42 |
+
Jid string `json:"jid"`
|
| 43 |
+
Qrcode string `json:"qrcode"`
|
| 44 |
+
Connected bool `json:"connected"`
|
| 45 |
+
Expiration int64 `json:"expiration"`
|
| 46 |
+
DisconnectReason string `json:"disconnect_reason"`
|
| 47 |
+
Events string `json:"events"`
|
| 48 |
+
OsName string `json:"os_name"`
|
| 49 |
+
Proxy string `json:"proxy"`
|
| 50 |
+
ClientName string `json:"client_name"`
|
| 51 |
+
CreatedAt *string `json:"created_at,omitempty"`
|
| 52 |
+
|
| 53 |
+
AlwaysOnline bool `json:"always_online"`
|
| 54 |
+
RejectCall bool `json:"reject_call"`
|
| 55 |
+
MsgRejectCall string `json:"msg_reject_call"`
|
| 56 |
+
ReadMessages bool `json:"read_messages"`
|
| 57 |
+
IgnoreGroups bool `json:"ignore_groups"`
|
| 58 |
+
IgnoreStatus bool `json:"ignore_status"`
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
type instanceRepository struct {
|
| 62 |
+
supa *supabase.Client
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
func toRow(instance instance_model.Instance) instanceRow {
|
| 66 |
+
return instanceRow{
|
| 67 |
+
Id: instance.Id,
|
| 68 |
+
Name: instance.Name,
|
| 69 |
+
Token: instance.Token,
|
| 70 |
+
Webhook: instance.Webhook,
|
| 71 |
+
RabbitmqEnable: instance.RabbitmqEnable,
|
| 72 |
+
WebSocketEnable: instance.WebSocketEnable,
|
| 73 |
+
NatsEnable: instance.NatsEnable,
|
| 74 |
+
Jid: instance.Jid,
|
| 75 |
+
Qrcode: instance.Qrcode,
|
| 76 |
+
Connected: instance.Connected,
|
| 77 |
+
Expiration: instance.Expiration,
|
| 78 |
+
DisconnectReason: instance.DisconnectReason,
|
| 79 |
+
Events: instance.Events,
|
| 80 |
+
OsName: instance.OsName,
|
| 81 |
+
Proxy: instance.Proxy,
|
| 82 |
+
ClientName: instance.ClientName,
|
| 83 |
+
AlwaysOnline: instance.AlwaysOnline,
|
| 84 |
+
RejectCall: instance.RejectCall,
|
| 85 |
+
MsgRejectCall: instance.MsgRejectCall,
|
| 86 |
+
ReadMessages: instance.ReadMessages,
|
| 87 |
+
IgnoreGroups: instance.IgnoreGroups,
|
| 88 |
+
IgnoreStatus: instance.IgnoreStatus,
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
func fromRow(r *instanceRow) *instance_model.Instance {
|
| 93 |
+
instance := &instance_model.Instance{
|
| 94 |
+
Id: r.Id,
|
| 95 |
+
Name: r.Name,
|
| 96 |
+
Token: r.Token,
|
| 97 |
+
Webhook: r.Webhook,
|
| 98 |
+
RabbitmqEnable: r.RabbitmqEnable,
|
| 99 |
+
WebSocketEnable: r.WebSocketEnable,
|
| 100 |
+
NatsEnable: r.NatsEnable,
|
| 101 |
+
Jid: r.Jid,
|
| 102 |
+
Qrcode: r.Qrcode,
|
| 103 |
+
Connected: r.Connected,
|
| 104 |
+
Expiration: r.Expiration,
|
| 105 |
+
DisconnectReason: r.DisconnectReason,
|
| 106 |
+
Events: r.Events,
|
| 107 |
+
OsName: r.OsName,
|
| 108 |
+
Proxy: r.Proxy,
|
| 109 |
+
ClientName: r.ClientName,
|
| 110 |
+
AlwaysOnline: r.AlwaysOnline,
|
| 111 |
+
RejectCall: r.RejectCall,
|
| 112 |
+
MsgRejectCall: r.MsgRejectCall,
|
| 113 |
+
ReadMessages: r.ReadMessages,
|
| 114 |
+
IgnoreGroups: r.IgnoreGroups,
|
| 115 |
+
IgnoreStatus: r.IgnoreStatus,
|
| 116 |
+
}
|
| 117 |
+
if r.CreatedAt != nil {
|
| 118 |
+
if t, err := time.Parse(time.RFC3339, *r.CreatedAt); err == nil {
|
| 119 |
+
instance.CreatedAt = t
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
return instance
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
func (i *instanceRepository) Create(instance instance_model.Instance) (*instance_model.Instance, error) {
|
| 126 |
+
if instance.Id == "" {
|
| 127 |
+
instance.Id = uuid.New().String()
|
| 128 |
+
}
|
| 129 |
+
row := toRow(instance)
|
| 130 |
+
ctx := context.Background()
|
| 131 |
+
var created []instanceRow
|
| 132 |
+
if err := i.supa.Table("wp_instances").Insert(ctx, row, "return=representation", &created); err != nil {
|
| 133 |
+
return nil, err
|
| 134 |
+
}
|
| 135 |
+
if len(created) == 0 {
|
| 136 |
+
return nil, fmt.Errorf("no instance row returned")
|
| 137 |
+
}
|
| 138 |
+
return fromRow(&created[0]), nil
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
func (i *instanceRepository) GetInstanceByToken(token string) (*instance_model.Instance, error) {
|
| 142 |
+
q := supabase.NewQuery().Eq("token", token).Limit(1)
|
| 143 |
+
var rows []instanceRow
|
| 144 |
+
ctx := context.Background()
|
| 145 |
+
if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil {
|
| 146 |
+
return nil, err
|
| 147 |
+
}
|
| 148 |
+
if len(rows) == 0 {
|
| 149 |
+
return nil, fmt.Errorf("instances: no row for token")
|
| 150 |
+
}
|
| 151 |
+
return fromRow(&rows[0]), nil
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
func (i *instanceRepository) GetInstanceByName(name string) (*instance_model.Instance, error) {
|
| 155 |
+
q := supabase.NewQuery().Eq("name", name).Limit(1)
|
| 156 |
+
var rows []instanceRow
|
| 157 |
+
ctx := context.Background()
|
| 158 |
+
if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil {
|
| 159 |
+
return nil, err
|
| 160 |
+
}
|
| 161 |
+
if len(rows) == 0 {
|
| 162 |
+
return nil, fmt.Errorf("instances not found for name")
|
| 163 |
+
}
|
| 164 |
+
return fromRow(&rows[0]), nil
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
func (i *instanceRepository) GetInstanceByID(instanceId string) (*instance_model.Instance, error) {
|
| 168 |
+
if _, err := uuid.Parse(instanceId); err != nil {
|
| 169 |
+
return nil, fmt.Errorf("invalid UUID format: %v", err)
|
| 170 |
+
}
|
| 171 |
+
q := supabase.NewQuery().Eq("id", instanceId).Limit(1)
|
| 172 |
+
var rows []instanceRow
|
| 173 |
+
ctx := context.Background()
|
| 174 |
+
if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil {
|
| 175 |
+
return nil, err
|
| 176 |
+
}
|
| 177 |
+
if len(rows) == 0 {
|
| 178 |
+
return nil, fmt.Errorf("instance not found")
|
| 179 |
+
}
|
| 180 |
+
return fromRow(&rows[0]), nil
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
func (i *instanceRepository) GetConnectedInstanceByID(instanceId string) (*instance_model.Instance, error) {
|
| 184 |
+
q := supabase.NewQuery().Eq("id", instanceId).Eq("connected", "true").Limit(1)
|
| 185 |
+
var rows []instanceRow
|
| 186 |
+
ctx := context.Background()
|
| 187 |
+
if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil {
|
| 188 |
+
return nil, err
|
| 189 |
+
}
|
| 190 |
+
if len(rows) == 0 {
|
| 191 |
+
return nil, fmt.Errorf("connected instance not found")
|
| 192 |
+
}
|
| 193 |
+
return fromRow(&rows[0]), nil
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
func (i *instanceRepository) Update(instance *instance_model.Instance) error {
|
| 197 |
+
row := toRow(*instance)
|
| 198 |
+
ctx := context.Background()
|
| 199 |
+
q := supabase.NewQuery().Eq("id", instance.Id)
|
| 200 |
+
return i.supa.Table("wp_instances").Update(ctx, q, row)
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
func (i *instanceRepository) UpdateConnected(userId string, connected bool, disconnectReason string) error {
|
| 204 |
+
ctx := context.Background()
|
| 205 |
+
q := supabase.NewQuery().Eq("id", userId)
|
| 206 |
+
body := map[string]interface{}{
|
| 207 |
+
"connected": connected,
|
| 208 |
+
"disconnect_reason": disconnectReason,
|
| 209 |
+
}
|
| 210 |
+
return i.supa.Table("wp_instances").Update(ctx, q, body)
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
func (i *instanceRepository) UpdateQrcode(userId string, qr string) error {
|
| 214 |
+
ctx := context.Background()
|
| 215 |
+
q := supabase.NewQuery().Eq("id", userId)
|
| 216 |
+
return i.supa.Table("wp_instances").Update(ctx, q, map[string]interface{}{"qrcode": qr})
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
func (i *instanceRepository) UpdateProxy(userId string, proxy string) error {
|
| 220 |
+
ctx := context.Background()
|
| 221 |
+
q := supabase.NewQuery().Eq("id", userId)
|
| 222 |
+
return i.supa.Table("wp_instances").Update(ctx, q, map[string]interface{}{"proxy": proxy})
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
func (i *instanceRepository) UpdateJid(userId string, jid string) error {
|
| 226 |
+
ctx := context.Background()
|
| 227 |
+
q := supabase.NewQuery().Eq("id", userId)
|
| 228 |
+
return i.supa.Table("wp_instances").Update(ctx, q, map[string]interface{}{"jid": jid})
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
func (i *instanceRepository) GetAllConnectedInstances() ([]*instance_model.Instance, error) {
|
| 232 |
+
q := supabase.NewQuery().Eq("connected", "true")
|
| 233 |
+
var rows []instanceRow
|
| 234 |
+
ctx := context.Background()
|
| 235 |
+
if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil {
|
| 236 |
+
return nil, err
|
| 237 |
+
}
|
| 238 |
+
out := make([]*instance_model.Instance, 0, len(rows))
|
| 239 |
+
for idx := range rows {
|
| 240 |
+
out = append(out, fromRow(&rows[idx]))
|
| 241 |
+
}
|
| 242 |
+
return out, nil
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
func (i *instanceRepository) GetAllConnectedInstancesByClientName(clientName string) ([]*instance_model.Instance, error) {
|
| 246 |
+
q := supabase.NewQuery().Eq("connected", "true").Eq("client_name", clientName)
|
| 247 |
+
var rows []instanceRow
|
| 248 |
+
ctx := context.Background()
|
| 249 |
+
if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil {
|
| 250 |
+
return nil, err
|
| 251 |
+
}
|
| 252 |
+
out := make([]*instance_model.Instance, 0, len(rows))
|
| 253 |
+
for idx := range rows {
|
| 254 |
+
out = append(out, fromRow(&rows[idx]))
|
| 255 |
+
}
|
| 256 |
+
return out, nil
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
func (i *instanceRepository) GetAll(clientName string) ([]*instance_model.Instance, error) {
|
| 260 |
+
q := supabase.NewQuery().Eq("client_name", clientName)
|
| 261 |
+
var rows []instanceRow
|
| 262 |
+
ctx := context.Background()
|
| 263 |
+
if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil {
|
| 264 |
+
return nil, err
|
| 265 |
+
}
|
| 266 |
+
out := make([]*instance_model.Instance, 0, len(rows))
|
| 267 |
+
for idx := range rows {
|
| 268 |
+
out = append(out, fromRow(&rows[idx]))
|
| 269 |
+
}
|
| 270 |
+
return out, nil
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
func (i *instanceRepository) Delete(instanceId string) error {
|
| 274 |
+
ctx := context.Background()
|
| 275 |
+
// Cascade delete via a Supabase RPC, which deletes related rows and the
|
| 276 |
+
// instance atomically (see ddl/006_functions.sql).
|
| 277 |
+
var result interface{}
|
| 278 |
+
if err := i.supa.RPC(ctx, "wp_delete_instance", map[string]interface{}{"p_instance_id": instanceId}, &result); err != nil {
|
| 279 |
+
// Fallback: delete rows individually if the RPC is unavailable.
|
| 280 |
+
q := supabase.NewQuery().Eq("id", instanceId)
|
| 281 |
+
if derr := i.supa.Table("wp_instances").Delete(ctx, q); derr != nil {
|
| 282 |
+
return fmt.Errorf("failed to delete instance: %v", derr)
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
return nil
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
func (i *instanceRepository) GetAdvancedSettings(instanceId string) (*instance_model.AdvancedSettings, error) {
|
| 289 |
+
if _, err := uuid.Parse(instanceId); err != nil {
|
| 290 |
+
return nil, fmt.Errorf("invalid UUID format: %v", err)
|
| 291 |
+
}
|
| 292 |
+
q := supabase.NewQuery().
|
| 293 |
+
Eq("id", instanceId).
|
| 294 |
+
Select("always_online, reject_call, msg_reject_call, read_messages, ignore_groups, ignore_status").
|
| 295 |
+
Limit(1)
|
| 296 |
+
var rows []instanceRow
|
| 297 |
+
ctx := context.Background()
|
| 298 |
+
if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil {
|
| 299 |
+
return nil, err
|
| 300 |
+
}
|
| 301 |
+
if len(rows) == 0 {
|
| 302 |
+
return nil, fmt.Errorf("instance not found")
|
| 303 |
+
}
|
| 304 |
+
return &instance_model.AdvancedSettings{
|
| 305 |
+
AlwaysOnline: rows[0].AlwaysOnline,
|
| 306 |
+
RejectCall: rows[0].RejectCall,
|
| 307 |
+
MsgRejectCall: rows[0].MsgRejectCall,
|
| 308 |
+
ReadMessages: rows[0].ReadMessages,
|
| 309 |
+
IgnoreGroups: rows[0].IgnoreGroups,
|
| 310 |
+
IgnoreStatus: rows[0].IgnoreStatus,
|
| 311 |
+
}, nil
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
func (i *instanceRepository) UpdateAdvancedSettings(instanceId string, settings *instance_model.AdvancedSettings) error {
|
| 315 |
+
if _, err := uuid.Parse(instanceId); err != nil {
|
| 316 |
+
return fmt.Errorf("invalid UUID format: %v", err)
|
| 317 |
+
}
|
| 318 |
+
ctx := context.Background()
|
| 319 |
+
q := supabase.NewQuery().Eq("id", instanceId)
|
| 320 |
+
body := map[string]interface{}{
|
| 321 |
+
"always_online": settings.AlwaysOnline,
|
| 322 |
+
"reject_call": settings.RejectCall,
|
| 323 |
+
"msg_reject_call": settings.MsgRejectCall,
|
| 324 |
+
"read_messages": settings.ReadMessages,
|
| 325 |
+
"ignore_groups": settings.IgnoreGroups,
|
| 326 |
+
"ignore_status": settings.IgnoreStatus,
|
| 327 |
+
}
|
| 328 |
+
return i.supa.Table("wp_instances").Update(ctx, q, body)
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
func NewInstanceRepository(supa *supabase.Client) InstanceRepository {
|
| 332 |
+
return &instanceRepository{supa: supa}
|
| 333 |
+
}
|
whatsapp-service/pkg/instance/service/instance_service.go
ADDED
|
@@ -0,0 +1,929 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package instance_service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"context"
|
| 6 |
+
"encoding/base64"
|
| 7 |
+
"encoding/json"
|
| 8 |
+
"errors"
|
| 9 |
+
"fmt"
|
| 10 |
+
"os"
|
| 11 |
+
"path/filepath"
|
| 12 |
+
"slices"
|
| 13 |
+
"sort"
|
| 14 |
+
"strings"
|
| 15 |
+
"time"
|
| 16 |
+
|
| 17 |
+
"agentdeck-whatsapp-service/pkg/config"
|
| 18 |
+
instance_model "agentdeck-whatsapp-service/pkg/instance/model"
|
| 19 |
+
instance_repository "agentdeck-whatsapp-service/pkg/instance/repository"
|
| 20 |
+
event_types "agentdeck-whatsapp-service/pkg/internal/event_types"
|
| 21 |
+
logger_wrapper "agentdeck-whatsapp-service/pkg/logger"
|
| 22 |
+
"agentdeck-whatsapp-service/pkg/utils"
|
| 23 |
+
whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service"
|
| 24 |
+
"go.mau.fi/whatsmeow"
|
| 25 |
+
"go.mau.fi/whatsmeow/types"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
type InstanceService interface {
|
| 29 |
+
Create(data *CreateStruct) (*instance_model.Instance, error)
|
| 30 |
+
Connect(data *ConnectStruct, instance *instance_model.Instance) (*instance_model.Instance, string, string, error)
|
| 31 |
+
Reconnect(instance *instance_model.Instance) error
|
| 32 |
+
Disconnect(instance *instance_model.Instance) (*instance_model.Instance, error)
|
| 33 |
+
Logout(instance *instance_model.Instance) (*instance_model.Instance, error)
|
| 34 |
+
Status(instance *instance_model.Instance) (*StatusStruct, error)
|
| 35 |
+
GetQr(instance *instance_model.Instance) (*QrcodeStruct, error)
|
| 36 |
+
Pair(data *PairStruct, instance *instance_model.Instance) (*PairReturnStruct, error)
|
| 37 |
+
GetAll() ([]*instance_model.Instance, error)
|
| 38 |
+
Info(instanceId string) (*instance_model.Instance, error)
|
| 39 |
+
Delete(id string) error
|
| 40 |
+
SetProxy(id string, proxyConfig *ProxyConfig) error
|
| 41 |
+
SetProxyFromStruct(id string, data *SetProxyStruct) error
|
| 42 |
+
RemoveProxy(id string) error
|
| 43 |
+
ForceReconnect(instanceId string, number string) error
|
| 44 |
+
GetInstanceByToken(token string) (*instance_model.Instance, error)
|
| 45 |
+
GetLogs(instanceId string, startDate, endDate time.Time, level string, limit int) ([]logger_wrapper.LogEntry, error)
|
| 46 |
+
GetAdvancedSettings(instanceId string) (*instance_model.AdvancedSettings, error)
|
| 47 |
+
UpdateAdvancedSettings(instanceId string, settings *instance_model.AdvancedSettings) error
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
type instances struct {
|
| 51 |
+
instanceRepository instance_repository.InstanceRepository
|
| 52 |
+
config *config.Config
|
| 53 |
+
killChannel map[string](chan bool)
|
| 54 |
+
clientPointer map[string]*whatsmeow.Client
|
| 55 |
+
whatsmeowService whatsmeow_service.WhatsmeowService
|
| 56 |
+
loggerWrapper *logger_wrapper.LoggerManager
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
type ProxyConfig struct {
|
| 60 |
+
Protocol string `json:"protocol,omitempty"`
|
| 61 |
+
Port string `json:"port"`
|
| 62 |
+
Password string `json:"password"`
|
| 63 |
+
Username string `json:"username"`
|
| 64 |
+
Host string `json:"host"`
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
type CreateStruct struct {
|
| 68 |
+
InstanceId string `json:"instanceId"`
|
| 69 |
+
Name string `json:"name"`
|
| 70 |
+
Token string `json:"token"`
|
| 71 |
+
Proxy *ProxyConfig `json:"proxy"`
|
| 72 |
+
AdvancedSettings *instance_model.AdvancedSettings `json:"advancedSettings"`
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
type ConnectStruct struct {
|
| 76 |
+
WebhookUrl string `json:"webhookUrl"`
|
| 77 |
+
Subscribe []string `json:"subscribe"`
|
| 78 |
+
Immediate bool `json:"immediate"`
|
| 79 |
+
Phone string `json:"phone"`
|
| 80 |
+
RabbitmqEnable string `json:"rabbitmqEnable"`
|
| 81 |
+
WebSocketEnable string `json:"websocketEnable"`
|
| 82 |
+
NatsEnable string `json:"natsEnable"`
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
type StatusStruct struct {
|
| 86 |
+
Connected bool
|
| 87 |
+
LoggedIn bool
|
| 88 |
+
myJid *types.JID
|
| 89 |
+
Name string
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
type QrcodeStruct struct {
|
| 93 |
+
Qrcode string `json:"qrcode"`
|
| 94 |
+
Code string `json:"code"`
|
| 95 |
+
// Passkey ceremony fields. Populated when the account requires a WebAuthn
|
| 96 |
+
// passkey to finish linking (no QR to scan at that point). The manager uses
|
| 97 |
+
// PasskeyStage to switch its UI and PasskeyOpenUrl for the
|
| 98 |
+
// "Abrir WhatsApp Web" button that launches the passkey ceremony.
|
| 99 |
+
PasskeyStage string `json:"passkeyStage,omitempty"`
|
| 100 |
+
PasskeyOpenURL string `json:"passkeyOpenUrl,omitempty"`
|
| 101 |
+
PasskeyCode string `json:"passkeyCode,omitempty"`
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
type PairStruct struct {
|
| 105 |
+
Subscribe []string `json:"subscribe"`
|
| 106 |
+
Phone string `json:"phone"`
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
type PairReturnStruct struct {
|
| 110 |
+
PairingCode string
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
type SetProxyStruct struct {
|
| 114 |
+
Protocol string `json:"protocol,omitempty"`
|
| 115 |
+
Host string `json:"host" validate:"required"`
|
| 116 |
+
Port string `json:"port" validate:"required"`
|
| 117 |
+
Username string `json:"username"`
|
| 118 |
+
Password string `json:"password"`
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
type ForceReconnectStruct struct {
|
| 122 |
+
Number string `json:"number"`
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
func (i *instances) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) {
|
| 126 |
+
logger := i.loggerWrapper.GetLogger(instanceId)
|
| 127 |
+
client := i.clientPointer[instanceId]
|
| 128 |
+
logger.LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil)
|
| 129 |
+
|
| 130 |
+
if client == nil {
|
| 131 |
+
logger.LogInfo("[%s] No client found, attempting to start new instance", instanceId)
|
| 132 |
+
err := i.whatsmeowService.StartInstance(instanceId)
|
| 133 |
+
if err != nil {
|
| 134 |
+
logger.LogError("[%s] Failed to start instance: %v", instanceId, err)
|
| 135 |
+
return nil, errors.New("no active session found")
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
logger.LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId)
|
| 139 |
+
time.Sleep(2 * time.Second)
|
| 140 |
+
|
| 141 |
+
client = i.clientPointer[instanceId]
|
| 142 |
+
logger.LogInfo("[%s] Checking new client - Exists: %v, Connected: %v",
|
| 143 |
+
instanceId,
|
| 144 |
+
client != nil,
|
| 145 |
+
client != nil && client.IsConnected())
|
| 146 |
+
|
| 147 |
+
if client == nil || !client.IsConnected() {
|
| 148 |
+
logger.LogError("[%s] New client validation failed - Exists: %v, Connected: %v",
|
| 149 |
+
instanceId,
|
| 150 |
+
client != nil,
|
| 151 |
+
client != nil && client.IsConnected())
|
| 152 |
+
return nil, errors.New("no active session found")
|
| 153 |
+
}
|
| 154 |
+
} else if !client.IsConnected() {
|
| 155 |
+
logger.LogError("[%s] Existing client is disconnected - Connected status: %v",
|
| 156 |
+
instanceId,
|
| 157 |
+
client.IsConnected())
|
| 158 |
+
return nil, errors.New("client disconnected")
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
logger.LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected())
|
| 162 |
+
return client, nil
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
func (i instances) Create(data *CreateStruct) (*instance_model.Instance, error) {
|
| 166 |
+
if data.Proxy != nil {
|
| 167 |
+
data.Proxy.Protocol = utils.NormalizeProxyProtocol(data.Proxy.Protocol, data.Proxy.Port)
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
proxyJson, err := json.Marshal(data.Proxy)
|
| 171 |
+
if err != nil {
|
| 172 |
+
return nil, err
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
findInstance, _ := i.instanceRepository.GetInstanceByName(data.Name)
|
| 176 |
+
|
| 177 |
+
if findInstance != nil {
|
| 178 |
+
return nil, fmt.Errorf("instance already exists")
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
instance := instance_model.Instance{
|
| 182 |
+
Id: data.InstanceId,
|
| 183 |
+
Name: data.Name,
|
| 184 |
+
Token: data.Token,
|
| 185 |
+
OsName: i.config.OsName,
|
| 186 |
+
Proxy: string(proxyJson),
|
| 187 |
+
Connected: false,
|
| 188 |
+
ClientName: i.config.ClientName,
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
// Set advanced settings if provided
|
| 192 |
+
if data.AdvancedSettings != nil {
|
| 193 |
+
instance.AlwaysOnline = data.AdvancedSettings.AlwaysOnline
|
| 194 |
+
instance.RejectCall = data.AdvancedSettings.RejectCall
|
| 195 |
+
instance.MsgRejectCall = data.AdvancedSettings.MsgRejectCall
|
| 196 |
+
instance.ReadMessages = data.AdvancedSettings.ReadMessages
|
| 197 |
+
instance.IgnoreGroups = data.AdvancedSettings.IgnoreGroups
|
| 198 |
+
instance.IgnoreStatus = data.AdvancedSettings.IgnoreStatus
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
createdInstance, err := i.instanceRepository.Create(instance)
|
| 202 |
+
if err != nil {
|
| 203 |
+
return nil, err
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
return createdInstance, nil
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
func (i instances) Connect(data *ConnectStruct, instance *instance_model.Instance) (*instance_model.Instance, string, string, error) {
|
| 210 |
+
var subscribedEvents []string
|
| 211 |
+
|
| 212 |
+
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Processing subscribe events: %v", instance.Id, data.Subscribe)
|
| 213 |
+
|
| 214 |
+
if len(data.Subscribe) == 0 {
|
| 215 |
+
subscribedEvents = append(subscribedEvents, event_types.MESSAGE)
|
| 216 |
+
} else if len(data.Subscribe) > 0 && data.Subscribe[0] == "ALL" {
|
| 217 |
+
for _, event := range event_types.AllEventTypes {
|
| 218 |
+
subscribedEvents = append(subscribedEvents, event)
|
| 219 |
+
}
|
| 220 |
+
} else {
|
| 221 |
+
for _, arg := range data.Subscribe {
|
| 222 |
+
if !event_types.IsEventType(arg) {
|
| 223 |
+
i.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Message type discarded '%s'", instance.Id, arg)
|
| 224 |
+
continue
|
| 225 |
+
}
|
| 226 |
+
subscribedEvents = append(subscribedEvents, arg)
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
eventString := strings.Join(subscribedEvents, ",")
|
| 231 |
+
|
| 232 |
+
instance.Events = eventString
|
| 233 |
+
instance.Webhook = data.WebhookUrl
|
| 234 |
+
instance.RabbitmqEnable = data.RabbitmqEnable
|
| 235 |
+
instance.NatsEnable = data.NatsEnable
|
| 236 |
+
instance.WebSocketEnable = data.WebSocketEnable
|
| 237 |
+
|
| 238 |
+
err := i.instanceRepository.Update(instance)
|
| 239 |
+
if err != nil {
|
| 240 |
+
i.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error updating instance: %s", instance.Id, err)
|
| 241 |
+
return nil, "", "", err
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// Verifica se a instância já está rodando
|
| 245 |
+
isInstanceRunning := i.clientPointer[instance.Id] != nil
|
| 246 |
+
|
| 247 |
+
// Sincroniza as configurações na instância em execução (se já estiver conectada)
|
| 248 |
+
err = i.whatsmeowService.UpdateInstanceSettings(instance.Id)
|
| 249 |
+
if err != nil {
|
| 250 |
+
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Instance not in runtime yet, will be updated when connected", instance.Id)
|
| 251 |
+
isInstanceRunning = false
|
| 252 |
+
} else {
|
| 253 |
+
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Instance settings updated successfully in runtime", instance.Id)
|
| 254 |
+
isInstanceRunning = true
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
// Se a instância não estiver rodando, inicia uma nova
|
| 258 |
+
if !isInstanceRunning {
|
| 259 |
+
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Starting new client instance", instance.Id)
|
| 260 |
+
|
| 261 |
+
i.killChannel[instance.Id] = make(chan bool)
|
| 262 |
+
|
| 263 |
+
clientData := &whatsmeow_service.ClientData{
|
| 264 |
+
Instance: instance,
|
| 265 |
+
Subscriptions: subscribedEvents,
|
| 266 |
+
Phone: data.Phone,
|
| 267 |
+
IsProxy: false,
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
if instance.Proxy != "" || i.config.ProxyHost != "" {
|
| 271 |
+
var proxyConfig ProxyConfig
|
| 272 |
+
err := json.Unmarshal([]byte(instance.Proxy), &proxyConfig)
|
| 273 |
+
if err != nil {
|
| 274 |
+
i.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error unmarshalling proxy config: %v", instance.Id, err)
|
| 275 |
+
return nil, "", "", err
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
if proxyConfig.Host != "" || i.config.ProxyHost != "" {
|
| 279 |
+
clientData.IsProxy = true
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
go i.whatsmeowService.StartClient(clientData)
|
| 284 |
+
} else {
|
| 285 |
+
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Instance already running, settings updated without restarting client", instance.Id)
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
// logger.LogInfo("Waiting 1 seconds")
|
| 289 |
+
// time.Sleep(1000 * time.Millisecond)
|
| 290 |
+
|
| 291 |
+
// if i.clientPointer[instance.Id] != nil {
|
| 292 |
+
// if !i.clientPointer[instance.Id].IsConnected() {
|
| 293 |
+
// return instance, "", "", fmt.Errorf("failed to connect")
|
| 294 |
+
// }
|
| 295 |
+
// } else {
|
| 296 |
+
// return instance, "", "", fmt.Errorf("failed to connect")
|
| 297 |
+
// }
|
| 298 |
+
|
| 299 |
+
return instance, instance.Jid, eventString, nil
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
func (i instances) Reconnect(instance *instance_model.Instance) error {
|
| 303 |
+
_, err := i.ensureClientConnected(instance.Id)
|
| 304 |
+
if err != nil {
|
| 305 |
+
return err
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
return i.whatsmeowService.ReconnectClient(instance.Id)
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
func (i instances) Disconnect(instance *instance_model.Instance) (*instance_model.Instance, error) {
|
| 312 |
+
client, err := i.ensureClientConnected(instance.Id)
|
| 313 |
+
if err != nil {
|
| 314 |
+
return instance, err
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
if client.IsConnected() {
|
| 318 |
+
if client.IsLoggedIn() {
|
| 319 |
+
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Disconnection successful", instance.Id)
|
| 320 |
+
i.killChannel[instance.Id] <- true
|
| 321 |
+
|
| 322 |
+
instance.Events = ""
|
| 323 |
+
|
| 324 |
+
err := i.instanceRepository.Update(instance)
|
| 325 |
+
if err != nil {
|
| 326 |
+
return instance, err
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
return instance, nil
|
| 330 |
+
}
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
i.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Ignoring disconnect as it was not connected", instance.Id)
|
| 334 |
+
return instance, nil
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
func (i instances) Logout(instance *instance_model.Instance) (*instance_model.Instance, error) {
|
| 338 |
+
client, err := i.ensureClientConnected(instance.Id)
|
| 339 |
+
if err != nil {
|
| 340 |
+
return instance, err
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
if client.IsLoggedIn() && client.IsConnected() {
|
| 344 |
+
err := client.Logout(context.Background())
|
| 345 |
+
if err != nil {
|
| 346 |
+
return instance, err
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
instance.Connected = false
|
| 350 |
+
err = i.instanceRepository.Update(instance)
|
| 351 |
+
if err != nil {
|
| 352 |
+
return instance, err
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
select {
|
| 356 |
+
case i.killChannel[instance.Id] <- true:
|
| 357 |
+
case <-time.After(5 * time.Second):
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
delete(i.clientPointer, instance.Id)
|
| 361 |
+
delete(i.killChannel, instance.Id)
|
| 362 |
+
|
| 363 |
+
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Logout successful", instance.Id)
|
| 364 |
+
return instance, nil
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
if client.IsConnected() {
|
| 368 |
+
client.Disconnect()
|
| 369 |
+
|
| 370 |
+
select {
|
| 371 |
+
case i.killChannel[instance.Id] <- true:
|
| 372 |
+
case <-time.After(5 * time.Second):
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
delete(i.clientPointer, instance.Id)
|
| 376 |
+
delete(i.killChannel, instance.Id)
|
| 377 |
+
|
| 378 |
+
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Disconnection successful", instance.Id)
|
| 379 |
+
return instance, nil
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
i.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Ignoring logout as it was not connected", instance.Id)
|
| 383 |
+
return instance, fmt.Errorf("ignoring logout as it was not connected")
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
func (i instances) Status(instance *instance_model.Instance) (*StatusStruct, error) {
|
| 387 |
+
client := i.clientPointer[instance.Id]
|
| 388 |
+
|
| 389 |
+
if client == nil {
|
| 390 |
+
return &StatusStruct{
|
| 391 |
+
Connected: false,
|
| 392 |
+
LoggedIn: false,
|
| 393 |
+
}, nil
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
isConnected := client.IsConnected()
|
| 397 |
+
isLoggedIn := client.IsLoggedIn()
|
| 398 |
+
|
| 399 |
+
var myJid *types.JID
|
| 400 |
+
var name string
|
| 401 |
+
if isLoggedIn {
|
| 402 |
+
myJid = client.Store.ID
|
| 403 |
+
name = client.Store.PushName
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
return &StatusStruct{
|
| 407 |
+
Connected: isConnected,
|
| 408 |
+
LoggedIn: isLoggedIn,
|
| 409 |
+
myJid: myJid,
|
| 410 |
+
Name: name,
|
| 411 |
+
}, nil
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
func (i instances) GetQr(instance *instance_model.Instance) (*QrcodeStruct, error) {
|
| 415 |
+
logger := i.loggerWrapper.GetLogger(instance.Id)
|
| 416 |
+
client := i.clientPointer[instance.Id]
|
| 417 |
+
|
| 418 |
+
// Se não há cliente ou o cliente está logado, precisamos iniciar um novo cliente
|
| 419 |
+
if client == nil || client.IsLoggedIn() {
|
| 420 |
+
if client != nil && client.IsLoggedIn() {
|
| 421 |
+
logger.LogInfo("[%s] Client is logged in, starting new instance for QR code", instance.Id)
|
| 422 |
+
} else {
|
| 423 |
+
logger.LogInfo("[%s] No client found, starting new instance for QR code", instance.Id)
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
// Iniciar nova instância para gerar QR code
|
| 427 |
+
err := i.whatsmeowService.StartInstance(instance.Id)
|
| 428 |
+
if err != nil {
|
| 429 |
+
logger.LogError("[%s] Failed to start instance: %v", instance.Id, err)
|
| 430 |
+
return nil, fmt.Errorf("failed to start instance: %w", err)
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
// Aguardar um pouco para o cliente iniciar e gerar QR code
|
| 434 |
+
logger.LogInfo("[%s] Waiting for QR code generation...", instance.Id)
|
| 435 |
+
time.Sleep(3 * time.Second)
|
| 436 |
+
|
| 437 |
+
// Verificar novamente se há cliente
|
| 438 |
+
client = i.clientPointer[instance.Id]
|
| 439 |
+
if client != nil && client.IsLoggedIn() {
|
| 440 |
+
return nil, fmt.Errorf("session already logged in")
|
| 441 |
+
}
|
| 442 |
+
} else if !client.IsConnected() {
|
| 443 |
+
// Se o cliente existe mas não está conectado, pode estar aguardando QR code
|
| 444 |
+
logger.LogInfo("[%s] Client exists but not connected, checking for existing QR code", instance.Id)
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
// Buscar instância atualizada do banco para pegar o QR code mais recente
|
| 448 |
+
instance, err := i.instanceRepository.GetInstanceByID(instance.Id)
|
| 449 |
+
if err != nil {
|
| 450 |
+
return nil, err
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
// If a passkey ceremony is in progress, there is no QR to scan — return the
|
| 454 |
+
// passkey stage + the #wapk openUrl so the manager can render the
|
| 455 |
+
// "Abrir WhatsApp Web" button. Checked before the empty-QR branch because
|
| 456 |
+
// during a passkey ceremony instance.Qrcode is empty.
|
| 457 |
+
if store := i.whatsmeowService.PasskeyCeremonyStore(); store != nil {
|
| 458 |
+
if token, state, ok := store.StateByInstance(instance.Id); ok {
|
| 459 |
+
logger.LogInfo("[%s] Passkey ceremony active (stage=%s) — returning passkey info instead of QR", instance.Id, state.Stage)
|
| 460 |
+
return &QrcodeStruct{
|
| 461 |
+
PasskeyStage: state.Stage,
|
| 462 |
+
PasskeyCode: state.Code,
|
| 463 |
+
PasskeyOpenURL: buildPasskeyOpenURL(token),
|
| 464 |
+
}, nil
|
| 465 |
+
}
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
code := instance.Qrcode
|
| 469 |
+
if code == "" {
|
| 470 |
+
// Se não há QR code ainda, aguardar um pouco mais e tentar novamente
|
| 471 |
+
logger.LogInfo("[%s] No QR code available yet, waiting a bit more...", instance.Id)
|
| 472 |
+
time.Sleep(2 * time.Second)
|
| 473 |
+
|
| 474 |
+
instance, err = i.instanceRepository.GetInstanceByID(instance.Id)
|
| 475 |
+
if err != nil {
|
| 476 |
+
return nil, err
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
code = instance.Qrcode
|
| 480 |
+
if code == "" {
|
| 481 |
+
return nil, fmt.Errorf("no QR code available. Please wait a moment and try again")
|
| 482 |
+
}
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
parts := strings.Split(code, "|")
|
| 486 |
+
if len(parts) < 2 {
|
| 487 |
+
return nil, fmt.Errorf("invalid QR code format")
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
qr := &QrcodeStruct{
|
| 491 |
+
Qrcode: parts[0],
|
| 492 |
+
Code: parts[1],
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
return qr, nil
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
// buildPasskeyOpenURL builds the URL the manager opens to start the passkey
|
| 499 |
+
// ceremony: https://web.whatsapp.com/#wapk=<base64url({t:token,b:publicBase})>.
|
| 500 |
+
// publicBase must be the PUBLICLY reachable API base the browser can hit; set it
|
| 501 |
+
// via PASSKEY_PUBLIC_URL. Kept in sync with the event handler in whatsmeow.go.
|
| 502 |
+
func buildPasskeyOpenURL(token string) string {
|
| 503 |
+
publicBase := os.Getenv("PASSKEY_PUBLIC_URL")
|
| 504 |
+
if publicBase == "" {
|
| 505 |
+
publicBase = "<SET_PASSKEY_PUBLIC_URL>"
|
| 506 |
+
}
|
| 507 |
+
payload := fmt.Sprintf(`{"t":%q,"b":%q}`, token, publicBase)
|
| 508 |
+
wapk := base64.RawURLEncoding.EncodeToString([]byte(payload))
|
| 509 |
+
return "https://web.whatsapp.com/#wapk=" + wapk
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
func (i instances) Pair(data *PairStruct, instance *instance_model.Instance) (*PairReturnStruct, error) {
|
| 513 |
+
logger := i.loggerWrapper.GetLogger(instance.Id)
|
| 514 |
+
client := i.clientPointer[instance.Id]
|
| 515 |
+
|
| 516 |
+
if client == nil || !client.IsConnected() {
|
| 517 |
+
if client != nil && client.IsLoggedIn() {
|
| 518 |
+
return nil, fmt.Errorf("instance is already authenticated")
|
| 519 |
+
}
|
| 520 |
+
logger.LogInfo("[%s] No active connection, starting instance for phone pairing", instance.Id)
|
| 521 |
+
if err := i.whatsmeowService.StartInstance(instance.Id); err != nil {
|
| 522 |
+
logger.LogError("[%s] Failed to start instance for pairing: %v", instance.Id, err)
|
| 523 |
+
return nil, fmt.Errorf("failed to start instance: %w", err)
|
| 524 |
+
}
|
| 525 |
+
// Wait for the WA websocket connection and initial QR generation to establish.
|
| 526 |
+
// PairPhone must be called after the QR event is received per whatsmeow docs.
|
| 527 |
+
time.Sleep(3 * time.Second)
|
| 528 |
+
client = i.clientPointer[instance.Id]
|
| 529 |
+
if client == nil {
|
| 530 |
+
return nil, fmt.Errorf("failed to initialize client for pairing")
|
| 531 |
+
}
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
if client.IsLoggedIn() {
|
| 535 |
+
return nil, fmt.Errorf("instance is already authenticated")
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
code, err := client.PairPhone(context.Background(), data.Phone, true, whatsmeow.PairClientChrome, "Chrome (Linux)")
|
| 539 |
+
if err != nil {
|
| 540 |
+
logger.LogError("[%s] PairPhone failed: %v", instance.Id, err)
|
| 541 |
+
return nil, fmt.Errorf("pairing failed: %w", err)
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
return &PairReturnStruct{PairingCode: code}, nil
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
func (i instances) GetAll() ([]*instance_model.Instance, error) {
|
| 548 |
+
instances, err := i.instanceRepository.GetAll(i.config.ClientName)
|
| 549 |
+
if err != nil {
|
| 550 |
+
return nil, err
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
for _, instance := range instances {
|
| 554 |
+
if client := i.clientPointer[instance.Id]; client != nil {
|
| 555 |
+
instance.Connected = client.IsLoggedIn()
|
| 556 |
+
} else {
|
| 557 |
+
instance.Connected = false
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
instance.Proxy = ""
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
return instances, nil
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
func (i instances) Info(instanceId string) (*instance_model.Instance, error) {
|
| 567 |
+
instance, err := i.instanceRepository.GetInstanceByID(instanceId)
|
| 568 |
+
if err != nil {
|
| 569 |
+
return nil, err
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
// Atualiza o status connected com base no estado real do cliente
|
| 573 |
+
if client := i.clientPointer[instance.Id]; client != nil {
|
| 574 |
+
instance.Connected = client.IsLoggedIn()
|
| 575 |
+
} else {
|
| 576 |
+
instance.Connected = false
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
instance.Proxy = ""
|
| 580 |
+
|
| 581 |
+
return instance, nil
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
func (i instances) Delete(id string) error {
|
| 585 |
+
instance, err := i.instanceRepository.GetInstanceByID(id)
|
| 586 |
+
if err != nil {
|
| 587 |
+
return err
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
if i.clientPointer[instance.Id] != nil && i.clientPointer[instance.Id].IsConnected() {
|
| 591 |
+
if i.clientPointer[instance.Id].IsLoggedIn() {
|
| 592 |
+
i.clientPointer[instance.Id].Logout(context.Background())
|
| 593 |
+
}
|
| 594 |
+
i.clientPointer[instance.Id].Disconnect()
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
// Limpar todos os recursos da instância antes de deletar
|
| 598 |
+
delete(i.clientPointer, instance.Id)
|
| 599 |
+
if i.killChannel[instance.Id] != nil {
|
| 600 |
+
close(i.killChannel[instance.Id])
|
| 601 |
+
delete(i.killChannel, instance.Id)
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
// Limpar cache via whatsmeow service
|
| 605 |
+
err = i.whatsmeowService.ClearInstanceCache(instance.Id, instance.Token)
|
| 606 |
+
if err != nil {
|
| 607 |
+
i.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Failed to clear instance cache: %v", instance.Id, err)
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
err = i.instanceRepository.Delete(id)
|
| 611 |
+
if err != nil {
|
| 612 |
+
return err
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
return nil
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
func (i instances) SetProxy(id string, proxyConfig *ProxyConfig) error {
|
| 619 |
+
instance, err := i.instanceRepository.GetInstanceByID(id)
|
| 620 |
+
if err != nil {
|
| 621 |
+
return err
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
// Validate proxy configuration
|
| 625 |
+
if proxyConfig == nil {
|
| 626 |
+
return fmt.Errorf("proxy configuration cannot be nil")
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
if proxyConfig.Host == "" {
|
| 630 |
+
return fmt.Errorf("proxy host is required")
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
if proxyConfig.Port == "" {
|
| 634 |
+
return fmt.Errorf("proxy port is required")
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
proxyConfig.Protocol = utils.NormalizeProxyProtocol(proxyConfig.Protocol, proxyConfig.Port)
|
| 638 |
+
|
| 639 |
+
// Convert proxy config to JSON
|
| 640 |
+
proxyJSON, err := json.Marshal(proxyConfig)
|
| 641 |
+
if err != nil {
|
| 642 |
+
i.loggerWrapper.GetLogger(id).LogError("[%s] Failed to marshal proxy config: %v", id, err)
|
| 643 |
+
return fmt.Errorf("failed to marshal proxy configuration: %v", err)
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
instance.Proxy = string(proxyJSON)
|
| 647 |
+
|
| 648 |
+
// Update instance in database
|
| 649 |
+
err = i.instanceRepository.Update(instance)
|
| 650 |
+
if err != nil {
|
| 651 |
+
i.loggerWrapper.GetLogger(id).LogError("[%s] Failed to update instance with proxy: %v", id, err)
|
| 652 |
+
return err
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
i.loggerWrapper.GetLogger(id).LogInfo("[%s] Proxy configuration updated: %s://%s:%s", id, proxyConfig.Protocol, proxyConfig.Host, proxyConfig.Port)
|
| 656 |
+
|
| 657 |
+
// Reconnect to apply proxy changes
|
| 658 |
+
go i.Reconnect(instance)
|
| 659 |
+
|
| 660 |
+
return nil
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
func (i instances) SetProxyFromStruct(id string, data *SetProxyStruct) error {
|
| 664 |
+
if data == nil {
|
| 665 |
+
return fmt.Errorf("proxy data cannot be nil")
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
proxyConfig := &ProxyConfig{
|
| 669 |
+
Protocol: data.Protocol,
|
| 670 |
+
Host: data.Host,
|
| 671 |
+
Port: data.Port,
|
| 672 |
+
Username: data.Username,
|
| 673 |
+
Password: data.Password,
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
return i.SetProxy(id, proxyConfig)
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
func (i instances) RemoveProxy(id string) error {
|
| 680 |
+
instance, err := i.instanceRepository.GetInstanceByID(id)
|
| 681 |
+
if err != nil {
|
| 682 |
+
return err
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
instance.Proxy = ""
|
| 686 |
+
|
| 687 |
+
err = i.instanceRepository.Update(instance)
|
| 688 |
+
if err != nil {
|
| 689 |
+
return err
|
| 690 |
+
}
|
| 691 |
+
|
| 692 |
+
i.loggerWrapper.GetLogger(id).LogInfo("[%s] Proxy configuration removed", id)
|
| 693 |
+
|
| 694 |
+
go i.Reconnect(instance)
|
| 695 |
+
|
| 696 |
+
return nil
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
func (i instances) ForceReconnect(instanceId string, number string) error {
|
| 700 |
+
if i.clientPointer[instanceId].IsConnected() && i.clientPointer[instanceId].IsLoggedIn() {
|
| 701 |
+
return fmt.Errorf("client already connected")
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
err := i.whatsmeowService.ForceUpdateJid(instanceId, number)
|
| 705 |
+
if err != nil {
|
| 706 |
+
return err
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
instance, err := i.instanceRepository.GetInstanceByID(instanceId)
|
| 710 |
+
if err != nil {
|
| 711 |
+
return err
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
subscribedEvents := strings.Split(instance.Events, ",")
|
| 715 |
+
|
| 716 |
+
i.killChannel[instance.Id] = make(chan bool)
|
| 717 |
+
|
| 718 |
+
clientData := &whatsmeow_service.ClientData{
|
| 719 |
+
Instance: instance,
|
| 720 |
+
Subscriptions: subscribedEvents,
|
| 721 |
+
Phone: "",
|
| 722 |
+
IsProxy: false,
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
if instance.Proxy != "" || i.config.ProxyHost != "" {
|
| 726 |
+
var proxyConfig ProxyConfig
|
| 727 |
+
err := json.Unmarshal([]byte(instance.Proxy), &proxyConfig)
|
| 728 |
+
if err != nil {
|
| 729 |
+
i.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error unmarshalling proxy config: %v", instance.Id, err)
|
| 730 |
+
return err
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
if proxyConfig.Host != "" || i.config.ProxyHost != "" {
|
| 734 |
+
clientData.IsProxy = true
|
| 735 |
+
}
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
if i.clientPointer[instance.Id] != nil {
|
| 739 |
+
client := i.clientPointer[instance.Id]
|
| 740 |
+
client.Disconnect()
|
| 741 |
+
|
| 742 |
+
select {
|
| 743 |
+
case i.killChannel[instance.Id] <- true:
|
| 744 |
+
case <-time.After(5 * time.Second):
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
delete(i.clientPointer, instance.Id)
|
| 748 |
+
delete(i.killChannel, instance.Id)
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
go i.whatsmeowService.StartClient(clientData)
|
| 752 |
+
|
| 753 |
+
time.Sleep(2 * time.Second)
|
| 754 |
+
|
| 755 |
+
if i.clientPointer[instance.Id] != nil {
|
| 756 |
+
if !i.clientPointer[instance.Id].IsConnected() {
|
| 757 |
+
return fmt.Errorf("failed to connect")
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
if !i.clientPointer[instance.Id].IsLoggedIn() {
|
| 761 |
+
return fmt.Errorf("failed to login")
|
| 762 |
+
}
|
| 763 |
+
} else {
|
| 764 |
+
return fmt.Errorf("failed to connect")
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
return nil
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
func (i instances) GetInstanceByToken(token string) (*instance_model.Instance, error) {
|
| 771 |
+
return i.instanceRepository.GetInstanceByToken(token)
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
func (i instances) GetLogs(instanceId string, startDate, endDate time.Time, level string, limit int) ([]logger_wrapper.LogEntry, error) {
|
| 775 |
+
// Inicializa o slice vazio para garantir que nunca retorne null
|
| 776 |
+
logs := make([]logger_wrapper.LogEntry, 0)
|
| 777 |
+
|
| 778 |
+
// Define valores padrão
|
| 779 |
+
if limit <= 0 {
|
| 780 |
+
limit = 100 // Limite padrão de 100 registros
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
// Se não foi fornecida data inicial, usa 7 dias atrás
|
| 784 |
+
if startDate.IsZero() {
|
| 785 |
+
startDate = time.Now().AddDate(0, 0, -7)
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
// Se não foi fornecida data final, usa data atual
|
| 789 |
+
if endDate.IsZero() {
|
| 790 |
+
endDate = time.Now()
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
// Ajusta as datas para início e fim do dia
|
| 794 |
+
startDate = time.Date(startDate.Year(), startDate.Month(), startDate.Day(), 0, 0, 0, 0, time.UTC)
|
| 795 |
+
endDate = time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 999999999, time.UTC)
|
| 796 |
+
|
| 797 |
+
// Garante que a data inicial não seja posterior à data final
|
| 798 |
+
if startDate.After(endDate) {
|
| 799 |
+
return logs, fmt.Errorf("data inicial não pode ser posterior à data final")
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
// Níveis de log válidos
|
| 803 |
+
validLevels := map[string]bool{
|
| 804 |
+
"INFO": true,
|
| 805 |
+
"ERROR": true,
|
| 806 |
+
"WARN": true,
|
| 807 |
+
"DEBUG": true,
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
var levelArray []string
|
| 811 |
+
if level == "" {
|
| 812 |
+
// Se nenhum nível foi especificado, usa todos
|
| 813 |
+
levelArray = []string{"INFO", "ERROR", "WARN", "DEBUG"}
|
| 814 |
+
} else {
|
| 815 |
+
// Divide e normaliza os níveis fornecidos
|
| 816 |
+
for _, l := range strings.Split(level, ",") {
|
| 817 |
+
l = strings.TrimSpace(strings.ToUpper(l))
|
| 818 |
+
if !validLevels[l] {
|
| 819 |
+
return logs, fmt.Errorf("nível de log inválido: %s", l)
|
| 820 |
+
}
|
| 821 |
+
levelArray = append(levelArray, l)
|
| 822 |
+
}
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
// Lê os logs do arquivo
|
| 826 |
+
logPath := filepath.Join(i.config.LogDirectory, instanceId, "instance.log")
|
| 827 |
+
file, err := os.Open(logPath)
|
| 828 |
+
if err != nil {
|
| 829 |
+
if os.IsNotExist(err) {
|
| 830 |
+
return logs, nil // Retorna array vazio se arquivo não existir
|
| 831 |
+
}
|
| 832 |
+
return logs, fmt.Errorf("erro ao abrir arquivo de log: %v", err)
|
| 833 |
+
}
|
| 834 |
+
defer file.Close()
|
| 835 |
+
|
| 836 |
+
scanner := bufio.NewScanner(file)
|
| 837 |
+
|
| 838 |
+
// Aumenta o buffer do scanner para lidar com linhas grandes
|
| 839 |
+
const maxCapacity = 1024 * 1024 // 1MB
|
| 840 |
+
buf := make([]byte, maxCapacity)
|
| 841 |
+
scanner.Buffer(buf, maxCapacity)
|
| 842 |
+
|
| 843 |
+
for scanner.Scan() {
|
| 844 |
+
var entry logger_wrapper.LogEntry
|
| 845 |
+
if err := json.Unmarshal(scanner.Bytes(), &entry); err != nil {
|
| 846 |
+
continue // Ignora linhas inválidas
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
// Ajusta o timestamp da entrada para UTC para comparação correta
|
| 850 |
+
entry.Timestamp = entry.Timestamp.UTC()
|
| 851 |
+
|
| 852 |
+
// Aplica os filtros
|
| 853 |
+
if entry.Timestamp.Before(startDate) || entry.Timestamp.After(endDate) {
|
| 854 |
+
continue
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
if !slices.Contains(levelArray, entry.Level) {
|
| 858 |
+
continue
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
logs = append(logs, entry)
|
| 862 |
+
|
| 863 |
+
// Verifica o limite
|
| 864 |
+
if len(logs) >= limit {
|
| 865 |
+
break
|
| 866 |
+
}
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
if err := scanner.Err(); err != nil {
|
| 870 |
+
return logs, fmt.Errorf("erro ao ler arquivo de log: %v", err)
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
// Ordena os logs por timestamp em ordem decrescente
|
| 874 |
+
sort.Slice(logs, func(i, j int) bool {
|
| 875 |
+
return logs[i].Timestamp.After(logs[j].Timestamp)
|
| 876 |
+
})
|
| 877 |
+
|
| 878 |
+
return logs, nil
|
| 879 |
+
}
|
| 880 |
+
|
| 881 |
+
func (i instances) GetAdvancedSettings(instanceId string) (*instance_model.AdvancedSettings, error) {
|
| 882 |
+
i.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Getting advanced settings", instanceId)
|
| 883 |
+
|
| 884 |
+
settings, err := i.instanceRepository.GetAdvancedSettings(instanceId)
|
| 885 |
+
if err != nil {
|
| 886 |
+
i.loggerWrapper.GetLogger(instanceId).LogError("[%s] Error getting advanced settings: %v", instanceId, err)
|
| 887 |
+
return nil, err
|
| 888 |
+
}
|
| 889 |
+
|
| 890 |
+
return settings, nil
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
func (i instances) UpdateAdvancedSettings(instanceId string, settings *instance_model.AdvancedSettings) error {
|
| 894 |
+
i.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Updating advanced settings", instanceId)
|
| 895 |
+
|
| 896 |
+
err := i.instanceRepository.UpdateAdvancedSettings(instanceId, settings)
|
| 897 |
+
if err != nil {
|
| 898 |
+
i.loggerWrapper.GetLogger(instanceId).LogError("[%s] Error updating advanced settings: %v", instanceId, err)
|
| 899 |
+
return err
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
// Sincroniza as configurações na instância em execução
|
| 903 |
+
err = i.whatsmeowService.UpdateInstanceAdvancedSettings(instanceId)
|
| 904 |
+
if err != nil {
|
| 905 |
+
i.loggerWrapper.GetLogger(instanceId).LogWarn("[%s] Error syncing advanced settings to runtime: %v", instanceId, err)
|
| 906 |
+
// Não falha a operação, apenas loga o warning
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
i.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Advanced settings updated successfully", instanceId)
|
| 910 |
+
return nil
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
func NewInstanceService(
|
| 914 |
+
instanceRepository instance_repository.InstanceRepository,
|
| 915 |
+
killChannel map[string](chan bool),
|
| 916 |
+
clientPointer map[string]*whatsmeow.Client,
|
| 917 |
+
whatsmeowService whatsmeow_service.WhatsmeowService,
|
| 918 |
+
config *config.Config,
|
| 919 |
+
loggerWrapper *logger_wrapper.LoggerManager,
|
| 920 |
+
) InstanceService {
|
| 921 |
+
return &instances{
|
| 922 |
+
instanceRepository: instanceRepository,
|
| 923 |
+
killChannel: killChannel,
|
| 924 |
+
clientPointer: clientPointer,
|
| 925 |
+
whatsmeowService: whatsmeowService,
|
| 926 |
+
config: config,
|
| 927 |
+
loggerWrapper: loggerWrapper,
|
| 928 |
+
}
|
| 929 |
+
}
|
whatsapp-service/pkg/internal/event_types/event_types.go
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package event_types
|
| 2 |
+
|
| 3 |
+
const (
|
| 4 |
+
ALL = "ALL"
|
| 5 |
+
MESSAGE = "MESSAGE"
|
| 6 |
+
SEND_MESSAGE = "SEND_MESSAGE"
|
| 7 |
+
READ_RECEIPT = "READ_RECEIPT"
|
| 8 |
+
PRESENCE = "PRESENCE"
|
| 9 |
+
HISTORY_SYNC = "HISTORY_SYNC"
|
| 10 |
+
CHAT_PRESENCE = "CHAT_PRESENCE"
|
| 11 |
+
CALL = "CALL"
|
| 12 |
+
CONNECTION = "CONNECTION"
|
| 13 |
+
LABEL = "LABEL"
|
| 14 |
+
CONTACT = "CONTACT"
|
| 15 |
+
GROUP = "GROUP"
|
| 16 |
+
NEWSLETTER = "NEWSLETTER"
|
| 17 |
+
QRCODE = "QRCODE"
|
| 18 |
+
BUTTON_CLICK = "BUTTON_CLICK"
|
| 19 |
+
PICTURE = "PICTURE"
|
| 20 |
+
USER_ABOUT = "USER_ABOUT"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
var AllEventTypes = []string{
|
| 24 |
+
MESSAGE,
|
| 25 |
+
SEND_MESSAGE,
|
| 26 |
+
READ_RECEIPT,
|
| 27 |
+
PRESENCE,
|
| 28 |
+
HISTORY_SYNC,
|
| 29 |
+
CHAT_PRESENCE,
|
| 30 |
+
CALL,
|
| 31 |
+
CONNECTION,
|
| 32 |
+
LABEL,
|
| 33 |
+
CONTACT,
|
| 34 |
+
GROUP,
|
| 35 |
+
NEWSLETTER,
|
| 36 |
+
QRCODE,
|
| 37 |
+
BUTTON_CLICK,
|
| 38 |
+
PICTURE,
|
| 39 |
+
USER_ABOUT,
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
var validEventTypes = map[string]bool{
|
| 43 |
+
ALL: true,
|
| 44 |
+
MESSAGE: true,
|
| 45 |
+
SEND_MESSAGE: true,
|
| 46 |
+
READ_RECEIPT: true,
|
| 47 |
+
PRESENCE: true,
|
| 48 |
+
HISTORY_SYNC: true,
|
| 49 |
+
CHAT_PRESENCE: true,
|
| 50 |
+
CALL: true,
|
| 51 |
+
CONNECTION: true,
|
| 52 |
+
LABEL: true,
|
| 53 |
+
CONTACT: true,
|
| 54 |
+
GROUP: true,
|
| 55 |
+
NEWSLETTER: true,
|
| 56 |
+
QRCODE: true,
|
| 57 |
+
BUTTON_CLICK: true,
|
| 58 |
+
PICTURE: true,
|
| 59 |
+
USER_ABOUT: true,
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
func IsEventType(eventType string) bool {
|
| 63 |
+
return validEventTypes[eventType]
|
| 64 |
+
}
|
whatsapp-service/pkg/label/handler/label_handler.go
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package label_handler
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
|
| 6 |
+
instance_model "agentdeck-whatsapp-service/pkg/instance/model"
|
| 7 |
+
label_service "agentdeck-whatsapp-service/pkg/label/service"
|
| 8 |
+
"github.com/gin-gonic/gin"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type LabelHandler interface {
|
| 12 |
+
ChatLabel(ctx *gin.Context)
|
| 13 |
+
MessageLabel(ctx *gin.Context)
|
| 14 |
+
EditLabel(ctx *gin.Context)
|
| 15 |
+
ChatUnlabel(ctx *gin.Context)
|
| 16 |
+
MessageUnlabel(ctx *gin.Context)
|
| 17 |
+
GetLabels(ctx *gin.Context)
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
type labelHandler struct {
|
| 21 |
+
labelService label_service.LabelService
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// Add label to chat
|
| 25 |
+
// @Summary Add label to chat
|
| 26 |
+
// @Description Add label to chat
|
| 27 |
+
// @Tags Label
|
| 28 |
+
// @Accept json
|
| 29 |
+
// @Produce json
|
| 30 |
+
// @Param message body label_service.ChatLabelStruct true "Label data"
|
| 31 |
+
// @Success 200 {object} gin.H "success"
|
| 32 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 33 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 34 |
+
// @Router /label/chat [post]
|
| 35 |
+
func (l *labelHandler) ChatLabel(ctx *gin.Context) {
|
| 36 |
+
getInstance := ctx.MustGet("instance")
|
| 37 |
+
|
| 38 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 39 |
+
if !ok {
|
| 40 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 41 |
+
return
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
var data *label_service.ChatLabelStruct
|
| 45 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 46 |
+
if err != nil {
|
| 47 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 48 |
+
return
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
if data.JID == "" {
|
| 52 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "jid is required"})
|
| 53 |
+
return
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
if data.LabelID == "" {
|
| 57 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "label id is required"})
|
| 58 |
+
return
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
err = l.labelService.ChatLabel(data, instance)
|
| 62 |
+
if err != nil {
|
| 63 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 64 |
+
return
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Add label to message
|
| 71 |
+
// @Summary Add label to message
|
| 72 |
+
// @Description Add label to message
|
| 73 |
+
// @Tags Label
|
| 74 |
+
// @Accept json
|
| 75 |
+
// @Produce json
|
| 76 |
+
// @Param message body label_service.MessageLabelStruct true "Label data"
|
| 77 |
+
// @Success 200 {object} gin.H "success"
|
| 78 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 79 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 80 |
+
// @Router /label/message [post]
|
| 81 |
+
func (l *labelHandler) MessageLabel(ctx *gin.Context) {
|
| 82 |
+
getInstance := ctx.MustGet("instance")
|
| 83 |
+
|
| 84 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 85 |
+
if !ok {
|
| 86 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 87 |
+
return
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
var data *label_service.MessageLabelStruct
|
| 91 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 92 |
+
if err != nil {
|
| 93 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 94 |
+
return
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
if data.JID == "" {
|
| 98 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "jid is required"})
|
| 99 |
+
return
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
if data.LabelID == "" {
|
| 103 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "label id is required"})
|
| 104 |
+
return
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
if data.MessageID == "" {
|
| 108 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "message id is required"})
|
| 109 |
+
return
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
err = l.labelService.MessageLabel(data, instance)
|
| 113 |
+
if err != nil {
|
| 114 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 115 |
+
return
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// Edit label
|
| 122 |
+
// @Summary Edit label
|
| 123 |
+
// @Description Edit label
|
| 124 |
+
// @Tags Label
|
| 125 |
+
// @Accept json
|
| 126 |
+
// @Produce json
|
| 127 |
+
// @Param message body label_service.EditLabelStruct true "Label data"
|
| 128 |
+
// @Success 200 {object} gin.H "success"
|
| 129 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 130 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 131 |
+
// @Router /label/edit [post]
|
| 132 |
+
func (l *labelHandler) EditLabel(ctx *gin.Context) {
|
| 133 |
+
getInstance := ctx.MustGet("instance")
|
| 134 |
+
|
| 135 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 136 |
+
if !ok {
|
| 137 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 138 |
+
return
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
var data *label_service.EditLabelStruct
|
| 142 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 143 |
+
if err != nil {
|
| 144 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 145 |
+
return
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
if data.LabelID == "" {
|
| 149 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "label id is required"})
|
| 150 |
+
return
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
if data.Name == "" {
|
| 154 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
| 155 |
+
return
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
err = l.labelService.EditLabel(data, instance)
|
| 159 |
+
if err != nil {
|
| 160 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 161 |
+
return
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
// Remove label from chat
|
| 168 |
+
// @Summary Remove label from chat
|
| 169 |
+
// @Description Remove label from chat
|
| 170 |
+
// @Tags Label
|
| 171 |
+
// @Accept json
|
| 172 |
+
// @Produce json
|
| 173 |
+
// @Param message body label_service.ChatLabelStruct true "Label data"
|
| 174 |
+
// @Success 200 {object} gin.H "success"
|
| 175 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 176 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 177 |
+
// @Router /unlabel/chat [post]
|
| 178 |
+
func (l *labelHandler) ChatUnlabel(ctx *gin.Context) {
|
| 179 |
+
getInstance := ctx.MustGet("instance")
|
| 180 |
+
|
| 181 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 182 |
+
if !ok {
|
| 183 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 184 |
+
return
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
var data *label_service.ChatLabelStruct
|
| 188 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 189 |
+
if err != nil {
|
| 190 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 191 |
+
return
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
if data.JID == "" {
|
| 195 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "jid is required"})
|
| 196 |
+
return
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
if data.LabelID == "" {
|
| 200 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "label id is required"})
|
| 201 |
+
return
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
err = l.labelService.ChatUnlabel(data, instance)
|
| 205 |
+
if err != nil {
|
| 206 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 207 |
+
return
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
// Remove label from message
|
| 214 |
+
// @Summary Remove label from message
|
| 215 |
+
// @Description Remove label from message
|
| 216 |
+
// @Tags Label
|
| 217 |
+
// @Accept json
|
| 218 |
+
// @Produce json
|
| 219 |
+
// @Param message body label_service.MessageLabelStruct true "Label data"
|
| 220 |
+
// @Success 200 {object} gin.H "success"
|
| 221 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 222 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 223 |
+
// @Router /unlabel/message [post]
|
| 224 |
+
func (l *labelHandler) MessageUnlabel(ctx *gin.Context) {
|
| 225 |
+
getInstance := ctx.MustGet("instance")
|
| 226 |
+
|
| 227 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 228 |
+
if !ok {
|
| 229 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 230 |
+
return
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
var data *label_service.MessageLabelStruct
|
| 234 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 235 |
+
if err != nil {
|
| 236 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 237 |
+
return
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
if data.JID == "" {
|
| 241 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "jid is required"})
|
| 242 |
+
return
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
if data.LabelID == "" {
|
| 246 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "label id is required"})
|
| 247 |
+
return
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
if data.MessageID == "" {
|
| 251 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "message id is required"})
|
| 252 |
+
return
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
err = l.labelService.MessageUnlabel(data, instance)
|
| 256 |
+
if err != nil {
|
| 257 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 258 |
+
return
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// Get all labels
|
| 265 |
+
// @Summary Get all labels
|
| 266 |
+
// @Description Get all labels
|
| 267 |
+
// @Tags Label
|
| 268 |
+
// @Accept json
|
| 269 |
+
// @Produce json
|
| 270 |
+
// @Success 200 {object} gin.H "success"
|
| 271 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 272 |
+
// @Router /label/list [get]
|
| 273 |
+
func (l *labelHandler) GetLabels(ctx *gin.Context) {
|
| 274 |
+
getInstance := ctx.MustGet("instance")
|
| 275 |
+
|
| 276 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 277 |
+
if !ok {
|
| 278 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 279 |
+
return
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
labels, err := l.labelService.GetLabels(instance)
|
| 283 |
+
if err != nil {
|
| 284 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 285 |
+
return
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
ctx.JSON(http.StatusOK, labels)
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
func NewLabelHandler(
|
| 292 |
+
labelService label_service.LabelService,
|
| 293 |
+
) LabelHandler {
|
| 294 |
+
return &labelHandler{
|
| 295 |
+
labelService: labelService,
|
| 296 |
+
}
|
| 297 |
+
}
|
whatsapp-service/pkg/label/model/label_model.go
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package label_model
|
| 2 |
+
|
| 3 |
+
type Label struct {
|
| 4 |
+
Id string `json:"id"`
|
| 5 |
+
InstanceID string `json:"instance_id"`
|
| 6 |
+
LabelID string `json:"label_id"`
|
| 7 |
+
LabelName string `json:"label_name"`
|
| 8 |
+
LabelColor string `json:"label_color"`
|
| 9 |
+
PredefinedId string `json:"predefined_id"`
|
| 10 |
+
}
|
whatsapp-service/pkg/label/repository/label_repository.go
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package label_repository
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
|
| 6 |
+
label_model "agentdeck-whatsapp-service/pkg/label/model"
|
| 7 |
+
"agentdeck-whatsapp-service/pkg/supabase"
|
| 8 |
+
"github.com/google/uuid"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type LabelRepository interface {
|
| 12 |
+
InsertLabel(label label_model.Label) error
|
| 13 |
+
UpdateLabel(label label_model.Label) error
|
| 14 |
+
GetLabelByID(id string) (*label_model.Label, error)
|
| 15 |
+
DeleteLabel(id string) error
|
| 16 |
+
GetAllLabelsByInstanceID(instanceID string) ([]label_model.Label, error)
|
| 17 |
+
UpsertLabel(label label_model.Label) error
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
type labelRepository struct {
|
| 21 |
+
supa *supabase.Client
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// labelRow is the snake_case PostgREST representation of a labels row.
|
| 25 |
+
type labelRow struct {
|
| 26 |
+
Id string `json:"id"`
|
| 27 |
+
InstanceID string `json:"instance_id"`
|
| 28 |
+
LabelID string `json:"label_id"`
|
| 29 |
+
LabelName string `json:"label_name"`
|
| 30 |
+
LabelColor string `json:"label_color"`
|
| 31 |
+
PredefinedId string `json:"predefined_id"`
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
func (l *labelRepository) InsertLabel(label label_model.Label) error {
|
| 35 |
+
if label.Id == "" {
|
| 36 |
+
label.Id = uuid.New().String()
|
| 37 |
+
}
|
| 38 |
+
ctx := context.Background()
|
| 39 |
+
return l.supa.Table("wp_labels").Insert(ctx, labelRow{
|
| 40 |
+
Id: label.Id,
|
| 41 |
+
InstanceID: label.InstanceID,
|
| 42 |
+
LabelID: label.LabelID,
|
| 43 |
+
LabelName: label.LabelName,
|
| 44 |
+
LabelColor: label.LabelColor,
|
| 45 |
+
PredefinedId: label.PredefinedId,
|
| 46 |
+
}, "", nil)
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
func (l *labelRepository) UpdateLabel(label label_model.Label) error {
|
| 50 |
+
ctx := context.Background()
|
| 51 |
+
row := labelRow{
|
| 52 |
+
Id: label.Id,
|
| 53 |
+
InstanceID: label.InstanceID,
|
| 54 |
+
LabelID: label.LabelID,
|
| 55 |
+
LabelName: label.LabelName,
|
| 56 |
+
LabelColor: label.LabelColor,
|
| 57 |
+
PredefinedId: label.PredefinedId,
|
| 58 |
+
}
|
| 59 |
+
q := supabase.NewQuery().Eq("id", label.Id)
|
| 60 |
+
return l.supa.Table("wp_labels").Update(ctx, q, row)
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
func (l *labelRepository) GetLabelByID(id string) (*label_model.Label, error) {
|
| 64 |
+
q := supabase.NewQuery().Eq("id", id).Limit(1)
|
| 65 |
+
var rows []labelRow
|
| 66 |
+
ctx := context.Background()
|
| 67 |
+
if err := l.supa.Table("wp_labels").Select(ctx, q, &rows); err != nil {
|
| 68 |
+
return nil, err
|
| 69 |
+
}
|
| 70 |
+
if len(rows) == 0 {
|
| 71 |
+
return nil, nil
|
| 72 |
+
}
|
| 73 |
+
return &label_model.Label{
|
| 74 |
+
Id: rows[0].Id,
|
| 75 |
+
InstanceID: rows[0].InstanceID,
|
| 76 |
+
LabelID: rows[0].LabelID,
|
| 77 |
+
LabelName: rows[0].LabelName,
|
| 78 |
+
LabelColor: rows[0].LabelColor,
|
| 79 |
+
PredefinedId: rows[0].PredefinedId,
|
| 80 |
+
}, nil
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
func (l *labelRepository) DeleteLabel(id string) error {
|
| 84 |
+
ctx := context.Background()
|
| 85 |
+
q := supabase.NewQuery().Eq("id", id)
|
| 86 |
+
return l.supa.Table("wp_labels").Delete(ctx, q)
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
func (l *labelRepository) GetAllLabelsByInstanceID(instanceID string) ([]label_model.Label, error) {
|
| 90 |
+
q := supabase.NewQuery().Eq("instance_id", instanceID)
|
| 91 |
+
var rows []labelRow
|
| 92 |
+
ctx := context.Background()
|
| 93 |
+
if err := l.supa.Table("wp_labels").Select(ctx, q, &rows); err != nil {
|
| 94 |
+
return nil, err
|
| 95 |
+
}
|
| 96 |
+
out := make([]label_model.Label, 0, len(rows))
|
| 97 |
+
for _, r := range rows {
|
| 98 |
+
out = append(out, label_model.Label{
|
| 99 |
+
Id: r.Id,
|
| 100 |
+
InstanceID: r.InstanceID,
|
| 101 |
+
LabelID: r.LabelID,
|
| 102 |
+
LabelName: r.LabelName,
|
| 103 |
+
LabelColor: r.LabelColor,
|
| 104 |
+
PredefinedId: r.PredefinedId,
|
| 105 |
+
})
|
| 106 |
+
}
|
| 107 |
+
return out, nil
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
func (l *labelRepository) UpsertLabel(label label_model.Label) error {
|
| 111 |
+
ctx := context.Background()
|
| 112 |
+
// PostgREST merge-duplicates uses the table's unique constraint; the labels
|
| 113 |
+
// table's practical uniqueness is (instance_id, label_id) but the PK is id.
|
| 114 |
+
// Keep the original FirstOrCreate semantics: look it up first.
|
| 115 |
+
q := supabase.NewQuery().Eq("instance_id", label.InstanceID).Eq("label_id", label.LabelID).Limit(1)
|
| 116 |
+
var rows []labelRow
|
| 117 |
+
if err := l.supa.Table("wp_labels").Select(ctx, q, &rows); err != nil {
|
| 118 |
+
return err
|
| 119 |
+
}
|
| 120 |
+
if len(rows) == 0 {
|
| 121 |
+
if label.Id == "" {
|
| 122 |
+
label.Id = uuid.New().String()
|
| 123 |
+
}
|
| 124 |
+
return l.supa.Table("wp_labels").Insert(ctx, labelRow{
|
| 125 |
+
Id: label.Id,
|
| 126 |
+
InstanceID: label.InstanceID,
|
| 127 |
+
LabelID: label.LabelID,
|
| 128 |
+
LabelName: label.LabelName,
|
| 129 |
+
LabelColor: label.LabelColor,
|
| 130 |
+
PredefinedId: label.PredefinedId,
|
| 131 |
+
}, "", nil)
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
row := rows[0]
|
| 135 |
+
row.LabelName = label.LabelName
|
| 136 |
+
row.LabelColor = label.LabelColor
|
| 137 |
+
row.PredefinedId = label.PredefinedId
|
| 138 |
+
q2 := supabase.NewQuery().Eq("id", rows[0].Id)
|
| 139 |
+
return l.supa.Table("wp_labels").Update(ctx, q2, row)
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
func NewLabelRepository(supa *supabase.Client) LabelRepository {
|
| 143 |
+
return &labelRepository{supa: supa}
|
| 144 |
+
}
|
whatsapp-service/pkg/label/service/label_service.go
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package label_service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"errors"
|
| 6 |
+
"time"
|
| 7 |
+
|
| 8 |
+
instance_model "agentdeck-whatsapp-service/pkg/instance/model"
|
| 9 |
+
label_model "agentdeck-whatsapp-service/pkg/label/model"
|
| 10 |
+
label_repository "agentdeck-whatsapp-service/pkg/label/repository"
|
| 11 |
+
logger_wrapper "agentdeck-whatsapp-service/pkg/logger"
|
| 12 |
+
"agentdeck-whatsapp-service/pkg/utils"
|
| 13 |
+
whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service"
|
| 14 |
+
"go.mau.fi/whatsmeow"
|
| 15 |
+
"go.mau.fi/whatsmeow/appstate"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
type LabelService interface {
|
| 19 |
+
ChatLabel(data *ChatLabelStruct, instance *instance_model.Instance) error
|
| 20 |
+
MessageLabel(data *MessageLabelStruct, instance *instance_model.Instance) error
|
| 21 |
+
EditLabel(data *EditLabelStruct, instance *instance_model.Instance) error
|
| 22 |
+
ChatUnlabel(data *ChatLabelStruct, instance *instance_model.Instance) error
|
| 23 |
+
MessageUnlabel(data *MessageLabelStruct, instance *instance_model.Instance) error
|
| 24 |
+
GetLabels(instance *instance_model.Instance) ([]label_model.Label, error)
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
type labelService struct {
|
| 28 |
+
clientPointer map[string]*whatsmeow.Client
|
| 29 |
+
whatsmeowService whatsmeow_service.WhatsmeowService
|
| 30 |
+
labelRepository label_repository.LabelRepository
|
| 31 |
+
loggerWrapper *logger_wrapper.LoggerManager
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
type ChatLabelStruct struct {
|
| 35 |
+
JID string `json:"jid"`
|
| 36 |
+
LabelID string `json:"labelId"`
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
type MessageLabelStruct struct {
|
| 40 |
+
JID string `json:"jid"`
|
| 41 |
+
LabelID string `json:"labelId"`
|
| 42 |
+
MessageID string `json:"messageId"`
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
type EditLabelStruct struct {
|
| 46 |
+
LabelID string `json:"labelId"`
|
| 47 |
+
Name string `json:"name"`
|
| 48 |
+
Color int `json:"color"`
|
| 49 |
+
Deleted bool `json:"deleted"`
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
func (l *labelService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) {
|
| 53 |
+
client := l.clientPointer[instanceId]
|
| 54 |
+
l.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil)
|
| 55 |
+
|
| 56 |
+
if client == nil {
|
| 57 |
+
l.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] No client found, attempting to start new instance", instanceId)
|
| 58 |
+
err := l.whatsmeowService.StartInstance(instanceId)
|
| 59 |
+
if err != nil {
|
| 60 |
+
l.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to start instance: %v", instanceId, err)
|
| 61 |
+
return nil, errors.New("no active session found")
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
l.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId)
|
| 65 |
+
time.Sleep(2 * time.Second)
|
| 66 |
+
|
| 67 |
+
client = l.clientPointer[instanceId]
|
| 68 |
+
l.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v",
|
| 69 |
+
instanceId,
|
| 70 |
+
client != nil,
|
| 71 |
+
client != nil && client.IsConnected())
|
| 72 |
+
|
| 73 |
+
if client == nil || !client.IsConnected() {
|
| 74 |
+
l.loggerWrapper.GetLogger(instanceId).LogError("[%s] New client validation failed - Exists: %v, Connected: %v",
|
| 75 |
+
instanceId,
|
| 76 |
+
client != nil,
|
| 77 |
+
client != nil && client.IsConnected())
|
| 78 |
+
return nil, errors.New("no active session found")
|
| 79 |
+
}
|
| 80 |
+
} else if !client.IsConnected() {
|
| 81 |
+
l.loggerWrapper.GetLogger(instanceId).LogError("[%s] Existing client is disconnected - Connected status: %v",
|
| 82 |
+
instanceId,
|
| 83 |
+
client.IsConnected())
|
| 84 |
+
return nil, errors.New("client disconnected")
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
l.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected())
|
| 88 |
+
return client, nil
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
func (l *labelService) ChatLabel(data *ChatLabelStruct, instance *instance_model.Instance) error {
|
| 92 |
+
client, err := l.ensureClientConnected(instance.Id)
|
| 93 |
+
if err != nil {
|
| 94 |
+
return err
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
jid, ok := utils.ParseJID(data.JID)
|
| 98 |
+
if !ok {
|
| 99 |
+
l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error parse community jid", instance.Id)
|
| 100 |
+
return errors.New("error parse community jid")
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
err = client.SendAppState(context.Background(), appstate.BuildLabelChat(
|
| 104 |
+
jid,
|
| 105 |
+
data.LabelID,
|
| 106 |
+
true,
|
| 107 |
+
))
|
| 108 |
+
if err != nil {
|
| 109 |
+
l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error label chat: %v", instance.Id, err)
|
| 110 |
+
return err
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
return nil
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
func (l *labelService) MessageLabel(data *MessageLabelStruct, instance *instance_model.Instance) error {
|
| 117 |
+
client, err := l.ensureClientConnected(instance.Id)
|
| 118 |
+
if err != nil {
|
| 119 |
+
return err
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
jid, ok := utils.ParseJID(data.JID)
|
| 123 |
+
if !ok {
|
| 124 |
+
l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error parse community jid", instance.Id)
|
| 125 |
+
return errors.New("error parse community jid")
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
err = client.SendAppState(context.Background(), appstate.BuildLabelMessage(
|
| 129 |
+
jid,
|
| 130 |
+
data.LabelID,
|
| 131 |
+
data.MessageID,
|
| 132 |
+
true,
|
| 133 |
+
))
|
| 134 |
+
if err != nil {
|
| 135 |
+
l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error label message: %v", instance.Id, err)
|
| 136 |
+
return err
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
return nil
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
func (l *labelService) EditLabel(data *EditLabelStruct, instance *instance_model.Instance) error {
|
| 143 |
+
client, err := l.ensureClientConnected(instance.Id)
|
| 144 |
+
if err != nil {
|
| 145 |
+
return err
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
err = client.SendAppState(context.Background(), appstate.BuildLabelEdit(
|
| 149 |
+
data.LabelID,
|
| 150 |
+
data.Name,
|
| 151 |
+
int32(data.Color),
|
| 152 |
+
data.Deleted,
|
| 153 |
+
))
|
| 154 |
+
if err != nil {
|
| 155 |
+
l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error label message: %v", instance.Id, err)
|
| 156 |
+
return err
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
return nil
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
func (l *labelService) ChatUnlabel(data *ChatLabelStruct, instance *instance_model.Instance) error {
|
| 163 |
+
client, err := l.ensureClientConnected(instance.Id)
|
| 164 |
+
if err != nil {
|
| 165 |
+
return err
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
jid, ok := utils.ParseJID(data.JID)
|
| 169 |
+
if !ok {
|
| 170 |
+
l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error parse community jid", instance.Id)
|
| 171 |
+
return errors.New("error parse community jid")
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
err = client.SendAppState(context.Background(), appstate.BuildLabelChat(
|
| 175 |
+
jid,
|
| 176 |
+
data.LabelID,
|
| 177 |
+
false,
|
| 178 |
+
))
|
| 179 |
+
if err != nil {
|
| 180 |
+
l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error label chat: %v", instance.Id, err)
|
| 181 |
+
return err
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
return nil
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
func (l *labelService) MessageUnlabel(data *MessageLabelStruct, instance *instance_model.Instance) error {
|
| 188 |
+
client, err := l.ensureClientConnected(instance.Id)
|
| 189 |
+
if err != nil {
|
| 190 |
+
return err
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
jid, ok := utils.ParseJID(data.JID)
|
| 194 |
+
if !ok {
|
| 195 |
+
l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error parse community jid", instance.Id)
|
| 196 |
+
return errors.New("error parse community jid")
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
err = client.SendAppState(context.Background(), appstate.BuildLabelMessage(
|
| 200 |
+
jid,
|
| 201 |
+
data.LabelID,
|
| 202 |
+
data.MessageID,
|
| 203 |
+
false,
|
| 204 |
+
))
|
| 205 |
+
if err != nil {
|
| 206 |
+
l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error label message: %v", instance.Id, err)
|
| 207 |
+
return err
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
return nil
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
func (l *labelService) GetLabels(instance *instance_model.Instance) ([]label_model.Label, error) {
|
| 214 |
+
_, err := l.ensureClientConnected(instance.Id)
|
| 215 |
+
if err != nil {
|
| 216 |
+
return nil, err
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
labels, err := l.labelRepository.GetAllLabelsByInstanceID(instance.Id)
|
| 220 |
+
if err != nil {
|
| 221 |
+
l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error fetching labels from database: %v", instance.Id, err)
|
| 222 |
+
return nil, err
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
return labels, nil
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
func NewLabelService(
|
| 229 |
+
clientPointer map[string]*whatsmeow.Client,
|
| 230 |
+
whatsmeowService whatsmeow_service.WhatsmeowService,
|
| 231 |
+
labelRepository label_repository.LabelRepository,
|
| 232 |
+
loggerWrapper *logger_wrapper.LoggerManager,
|
| 233 |
+
) LabelService {
|
| 234 |
+
return &labelService{
|
| 235 |
+
clientPointer: clientPointer,
|
| 236 |
+
whatsmeowService: whatsmeowService,
|
| 237 |
+
labelRepository: labelRepository,
|
| 238 |
+
loggerWrapper: loggerWrapper,
|
| 239 |
+
}
|
| 240 |
+
}
|
whatsapp-service/pkg/logger/logger.go
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package logger
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"fmt"
|
| 6 |
+
"os"
|
| 7 |
+
"path/filepath"
|
| 8 |
+
"sync"
|
| 9 |
+
"time"
|
| 10 |
+
|
| 11 |
+
"agentdeck-whatsapp-service/pkg/config"
|
| 12 |
+
"github.com/gomessguii/logger"
|
| 13 |
+
"gopkg.in/natefinch/lumberjack.v2"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
type LoggerManager struct {
|
| 17 |
+
config *config.Config
|
| 18 |
+
loggers map[string]*Logger
|
| 19 |
+
mu sync.RWMutex
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
type Logger struct {
|
| 23 |
+
config *config.Config
|
| 24 |
+
instanceId string
|
| 25 |
+
mu sync.Mutex
|
| 26 |
+
writer *lumberjack.Logger
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
type LogEntry struct {
|
| 30 |
+
Timestamp time.Time `json:"timestamp"`
|
| 31 |
+
Level string `json:"level"`
|
| 32 |
+
InstanceId string `json:"instance_id"`
|
| 33 |
+
Message string `json:"message"`
|
| 34 |
+
Metadata json.RawMessage `json:"metadata,omitempty"`
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
func NewLoggerManager(config *config.Config) *LoggerManager {
|
| 38 |
+
// Garante que o diretório base de logs existe
|
| 39 |
+
if err := os.MkdirAll(config.LogDirectory, 0755); err != nil {
|
| 40 |
+
logger.LogError("Falha ao criar diretório base de logs: %v", err)
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
return &LoggerManager{
|
| 44 |
+
config: config,
|
| 45 |
+
loggers: make(map[string]*Logger),
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
func (lm *LoggerManager) GetLogger(instanceId string) *Logger {
|
| 50 |
+
lm.mu.RLock()
|
| 51 |
+
logger, exists := lm.loggers[instanceId]
|
| 52 |
+
lm.mu.RUnlock()
|
| 53 |
+
|
| 54 |
+
if exists {
|
| 55 |
+
return logger
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
lm.mu.Lock()
|
| 59 |
+
defer lm.mu.Unlock()
|
| 60 |
+
|
| 61 |
+
// Verificar novamente após obter o lock de escrita
|
| 62 |
+
if logger, exists = lm.loggers[instanceId]; exists {
|
| 63 |
+
return logger
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Criar novo logger para a instância
|
| 67 |
+
logger = newLogger(instanceId, lm.config)
|
| 68 |
+
lm.loggers[instanceId] = logger
|
| 69 |
+
return logger
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
func newLogger(instanceId string, config *config.Config) *Logger {
|
| 73 |
+
// Garante que o diretório existe
|
| 74 |
+
logPath := filepath.Join(config.LogDirectory, instanceId)
|
| 75 |
+
os.MkdirAll(logPath, 0755)
|
| 76 |
+
|
| 77 |
+
logFile := filepath.Join(logPath, "instance.log")
|
| 78 |
+
|
| 79 |
+
writer := &lumberjack.Logger{
|
| 80 |
+
Filename: logFile,
|
| 81 |
+
MaxSize: config.LogMaxSize,
|
| 82 |
+
MaxBackups: config.LogMaxBackups,
|
| 83 |
+
MaxAge: config.LogMaxAge,
|
| 84 |
+
Compress: config.LogCompress,
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
return &Logger{
|
| 88 |
+
config: config,
|
| 89 |
+
instanceId: instanceId,
|
| 90 |
+
writer: writer,
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
func (l *Logger) LogInfo(format string, args ...interface{}) {
|
| 95 |
+
l.log("INFO", format, args...)
|
| 96 |
+
logger.LogInfo(format, args...)
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
func (l *Logger) LogError(format string, args ...interface{}) {
|
| 100 |
+
l.log("ERROR", format, args...)
|
| 101 |
+
logger.LogError(format, args...)
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
func (l *Logger) LogWarn(format string, args ...interface{}) {
|
| 105 |
+
l.log("WARN", format, args...)
|
| 106 |
+
logger.LogWarn(format, args...)
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
func (l *Logger) LogDebug(format string, args ...interface{}) {
|
| 110 |
+
l.log("DEBUG", format, args...)
|
| 111 |
+
logger.LogDebug(format, args...)
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
func (l *Logger) log(level string, format string, args ...interface{}) {
|
| 115 |
+
l.mu.Lock()
|
| 116 |
+
defer l.mu.Unlock()
|
| 117 |
+
|
| 118 |
+
entry := LogEntry{
|
| 119 |
+
Timestamp: time.Now(),
|
| 120 |
+
Level: level,
|
| 121 |
+
InstanceId: l.instanceId,
|
| 122 |
+
Message: fmt.Sprintf(format, args...),
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
jsonEntry, err := json.Marshal(entry)
|
| 126 |
+
if err != nil {
|
| 127 |
+
logger.LogError("Failed to marshal log entry: %v", err)
|
| 128 |
+
return
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
if _, err := l.writer.Write(append(jsonEntry, '\n')); err != nil {
|
| 132 |
+
logger.LogError("Failed to write log: %v", err)
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
func (l *Logger) Close() error {
|
| 137 |
+
l.mu.Lock()
|
| 138 |
+
defer l.mu.Unlock()
|
| 139 |
+
return l.writer.Close()
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
// GetLogs retorna os logs da instância com filtros opcionais
|
| 143 |
+
func (l *Logger) GetLogs(startDate, endDate time.Time, level string, limit int) ([]LogEntry, error) {
|
| 144 |
+
// Implementação movida para o service
|
| 145 |
+
return nil, fmt.Errorf("método movido para instance_service")
|
| 146 |
+
}
|
whatsapp-service/pkg/message/handler/message_handler.go
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package message_handler
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
|
| 6 |
+
instance_model "agentdeck-whatsapp-service/pkg/instance/model"
|
| 7 |
+
message_service "agentdeck-whatsapp-service/pkg/message/service"
|
| 8 |
+
"github.com/gin-gonic/gin"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type MessageHandler interface {
|
| 12 |
+
React(ctx *gin.Context)
|
| 13 |
+
ChatPresence(ctx *gin.Context)
|
| 14 |
+
MarkRead(ctx *gin.Context)
|
| 15 |
+
MarkPlayed(ctx *gin.Context)
|
| 16 |
+
DownloadMedia(ctx *gin.Context)
|
| 17 |
+
GetMessageStatus(ctx *gin.Context)
|
| 18 |
+
DeleteMessageEveryone(ctx *gin.Context)
|
| 19 |
+
EditMessage(ctx *gin.Context)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
type messageHandler struct {
|
| 23 |
+
messageService message_service.MessageService
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// React a message
|
| 27 |
+
// @Summary React a message
|
| 28 |
+
// @Description React to a message with support for fromMe field and participant field for group messages
|
| 29 |
+
// @Tags Message
|
| 30 |
+
// @Accept json
|
| 31 |
+
// @Produce json
|
| 32 |
+
// @Param message body message_service.ReactStruct true "React to a message with fromMe and participant fields"
|
| 33 |
+
// @Success 200 {object} gin.H "success"
|
| 34 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 35 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 36 |
+
// @Router /message/react [post]
|
| 37 |
+
func (m *messageHandler) React(ctx *gin.Context) {
|
| 38 |
+
getInstance := ctx.MustGet("instance")
|
| 39 |
+
|
| 40 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 41 |
+
if !ok {
|
| 42 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 43 |
+
return
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
var data *message_service.ReactStruct
|
| 47 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 48 |
+
if err != nil {
|
| 49 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 50 |
+
return
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
if data.Number == "" {
|
| 54 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"})
|
| 55 |
+
return
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
if data.Reaction == "" {
|
| 59 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "message reaction is required"})
|
| 60 |
+
return
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
message, err := m.messageService.React(data, instance)
|
| 64 |
+
if err != nil {
|
| 65 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 66 |
+
return
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message})
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// ChatPresence set chat presence
|
| 73 |
+
// @Summary Set chat presence
|
| 74 |
+
// @Description Set chat presence
|
| 75 |
+
// @Tags Message
|
| 76 |
+
// @Accept json
|
| 77 |
+
// @Produce json
|
| 78 |
+
// @Param message body message_service.ChatPresenceStruct true "Set chat presence"
|
| 79 |
+
// @Success 200 {object} gin.H "success"
|
| 80 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 81 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 82 |
+
// @Router /message/presence [post]
|
| 83 |
+
func (m *messageHandler) ChatPresence(ctx *gin.Context) {
|
| 84 |
+
getInstance := ctx.MustGet("instance")
|
| 85 |
+
|
| 86 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 87 |
+
if !ok {
|
| 88 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 89 |
+
return
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
var data *message_service.ChatPresenceStruct
|
| 93 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 94 |
+
if err != nil {
|
| 95 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 96 |
+
return
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
if data.Number == "" {
|
| 100 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"})
|
| 101 |
+
return
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
if data.State == "" {
|
| 105 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "state is required"})
|
| 106 |
+
return
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
ts, err := m.messageService.ChatPresence(data, instance)
|
| 110 |
+
if err != nil {
|
| 111 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 112 |
+
return
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
responseData := gin.H{
|
| 116 |
+
"timestamp": ts,
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData})
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// MarkRead mark a message as read
|
| 123 |
+
// @Summary Mark a message as read
|
| 124 |
+
// @Description Mark a message as read
|
| 125 |
+
// @Tags Message
|
| 126 |
+
// @Accept json
|
| 127 |
+
// @Produce json
|
| 128 |
+
// @Param message body message_service.MarkReadStruct true "Mark a message as read"
|
| 129 |
+
// @Success 200 {object} gin.H "success"
|
| 130 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 131 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 132 |
+
// @Router /message/markread [post]
|
| 133 |
+
func (m *messageHandler) MarkRead(ctx *gin.Context) {
|
| 134 |
+
getInstance := ctx.MustGet("instance")
|
| 135 |
+
|
| 136 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 137 |
+
if !ok {
|
| 138 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 139 |
+
return
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
var data *message_service.MarkReadStruct
|
| 143 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 144 |
+
if err != nil {
|
| 145 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 146 |
+
return
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
if data.Number == "" {
|
| 150 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"})
|
| 151 |
+
return
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
if len(data.Id) < 1 {
|
| 155 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
|
| 156 |
+
return
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
ts, err := m.messageService.MarkRead(data, instance)
|
| 160 |
+
if err != nil {
|
| 161 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 162 |
+
return
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
responseData := gin.H{
|
| 166 |
+
"timestamp": ts,
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData})
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
// MarkPlayed mark an audio message as played (blue mic icon)
|
| 173 |
+
// @Summary Mark an audio message as played
|
| 174 |
+
// @Description Mark an audio message as played
|
| 175 |
+
// @Tags Message
|
| 176 |
+
// @Accept json
|
| 177 |
+
// @Produce json
|
| 178 |
+
// @Param message body message_service.MarkPlayedStruct true "Mark an audio message as played"
|
| 179 |
+
// @Success 200 {object} gin.H "success"
|
| 180 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 181 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 182 |
+
// @Router /message/markplayed [post]
|
| 183 |
+
func (m *messageHandler) MarkPlayed(ctx *gin.Context) {
|
| 184 |
+
getInstance := ctx.MustGet("instance")
|
| 185 |
+
|
| 186 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 187 |
+
if !ok {
|
| 188 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 189 |
+
return
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
var data *message_service.MarkPlayedStruct
|
| 193 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 194 |
+
if err != nil {
|
| 195 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 196 |
+
return
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
if data.Number == "" {
|
| 200 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"})
|
| 201 |
+
return
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
if len(data.Id) < 1 {
|
| 205 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
|
| 206 |
+
return
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
ts, err := m.messageService.MarkPlayed(data, instance)
|
| 210 |
+
if err != nil {
|
| 211 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 212 |
+
return
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
responseData := gin.H{
|
| 216 |
+
"timestamp": ts,
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData})
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
// DownloadMedia download a media message (image, video, audio, document)
|
| 223 |
+
// @Summary Download media
|
| 224 |
+
// @Description Download the media content of a message (image, video, audio or document)
|
| 225 |
+
// @Tags Message
|
| 226 |
+
// @Accept json
|
| 227 |
+
// @Produce json
|
| 228 |
+
// @Param message body message_service.DownloadMediaStruct true "Download media"
|
| 229 |
+
// @Success 200 {object} gin.H "success"
|
| 230 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 231 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 232 |
+
// @Router /message/downloadmedia [post]
|
| 233 |
+
func (m *messageHandler) DownloadMedia(ctx *gin.Context) {
|
| 234 |
+
getInstance := ctx.MustGet("instance")
|
| 235 |
+
|
| 236 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 237 |
+
if !ok {
|
| 238 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 239 |
+
return
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
var data *message_service.DownloadMediaStruct
|
| 243 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 244 |
+
if err != nil {
|
| 245 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 246 |
+
return
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
dataUrl, ts, err := m.messageService.DownloadMedia(data, instance, ctx.Request)
|
| 250 |
+
if err != nil {
|
| 251 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 252 |
+
return
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
responseData := gin.H{
|
| 256 |
+
"base64": dataUrl.String(),
|
| 257 |
+
"timestamp": ts,
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData})
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
// GetMessageStatus get message status
|
| 264 |
+
// @Summary Get message status
|
| 265 |
+
// @Description Get message status
|
| 266 |
+
// @Tags Message
|
| 267 |
+
// @Accept json
|
| 268 |
+
// @Produce json
|
| 269 |
+
// @Param message body message_service.MessageStatusStruct true "Get message status"
|
| 270 |
+
// @Success 200 {object} gin.H "success"
|
| 271 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 272 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 273 |
+
// @Router /message/status [post]
|
| 274 |
+
func (m *messageHandler) GetMessageStatus(ctx *gin.Context) {
|
| 275 |
+
getInstance := ctx.MustGet("instance")
|
| 276 |
+
|
| 277 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 278 |
+
if !ok {
|
| 279 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 280 |
+
return
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
var data *message_service.MessageStatusStruct
|
| 284 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 285 |
+
if err != nil {
|
| 286 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 287 |
+
return
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
if data.Id == "" {
|
| 291 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
|
| 292 |
+
return
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
message, ts, err := m.messageService.GetMessageStatus(data, instance)
|
| 296 |
+
if err != nil {
|
| 297 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 298 |
+
return
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
responseData := gin.H{
|
| 302 |
+
"result": message,
|
| 303 |
+
"timestamp": ts,
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData})
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
// DeleteMessageEveryone delete a message for everyone
|
| 310 |
+
// @Summary Delete a message for everyone
|
| 311 |
+
// @Description Delete a message for everyone
|
| 312 |
+
// @Tags Message
|
| 313 |
+
// @Accept json
|
| 314 |
+
// @Produce json
|
| 315 |
+
// @Param message body message_service.MessageStruct true "Delete a message for everyone"
|
| 316 |
+
// @Success 200 {object} gin.H "success"
|
| 317 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 318 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 319 |
+
// @Router /message/delete [post]
|
| 320 |
+
func (m *messageHandler) DeleteMessageEveryone(ctx *gin.Context) {
|
| 321 |
+
getInstance := ctx.MustGet("instance")
|
| 322 |
+
|
| 323 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 324 |
+
if !ok {
|
| 325 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 326 |
+
return
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
var data *message_service.MessageStruct
|
| 330 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 331 |
+
if err != nil {
|
| 332 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 333 |
+
return
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
if data.Chat == "" {
|
| 337 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"})
|
| 338 |
+
return
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
if data.MessageID == "" {
|
| 342 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "messageId is required"})
|
| 343 |
+
return
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
msgId, ts, err := m.messageService.DeleteMessageEveryone(data, instance)
|
| 347 |
+
if err != nil {
|
| 348 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 349 |
+
return
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
responseData := gin.H{
|
| 353 |
+
"messageId": msgId,
|
| 354 |
+
"timestamp": ts,
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData})
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
// EditMessage edit a message
|
| 361 |
+
// @Summary Edit a message
|
| 362 |
+
// @Description Edit a message
|
| 363 |
+
// @Tags Message
|
| 364 |
+
// @Accept json
|
| 365 |
+
// @Produce json
|
| 366 |
+
// @Param message body message_service.EditMessageStruct true "Edit a message"
|
| 367 |
+
// @Success 200 {object} gin.H "success"
|
| 368 |
+
// @Failure 400 {object} gin.H "Error on validation"
|
| 369 |
+
// @Failure 500 {object} gin.H "Internal server error"
|
| 370 |
+
// @Router /message/edit [post]
|
| 371 |
+
func (m *messageHandler) EditMessage(ctx *gin.Context) {
|
| 372 |
+
getInstance := ctx.MustGet("instance")
|
| 373 |
+
|
| 374 |
+
instance, ok := getInstance.(*instance_model.Instance)
|
| 375 |
+
if !ok {
|
| 376 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
|
| 377 |
+
return
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
var data *message_service.EditMessageStruct
|
| 381 |
+
err := ctx.ShouldBindBodyWithJSON(&data)
|
| 382 |
+
if err != nil {
|
| 383 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 384 |
+
return
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
if data.Chat == "" {
|
| 388 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"})
|
| 389 |
+
return
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
if data.Message == "" {
|
| 393 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "message is required"})
|
| 394 |
+
return
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
if data.MessageID == "" {
|
| 398 |
+
ctx.JSON(http.StatusBadRequest, gin.H{"error": "messageId is required"})
|
| 399 |
+
return
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
msgId, ts, err := m.messageService.EditMessage(data, instance)
|
| 403 |
+
if err != nil {
|
| 404 |
+
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 405 |
+
return
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
responseData := gin.H{
|
| 409 |
+
"messageId": msgId,
|
| 410 |
+
"timestamp": ts,
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData})
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
func NewMessageHandler(
|
| 417 |
+
messageService message_service.MessageService,
|
| 418 |
+
) MessageHandler {
|
| 419 |
+
return &messageHandler{
|
| 420 |
+
messageService: messageService,
|
| 421 |
+
}
|
| 422 |
+
}
|