validops-east-1 commited on
Commit
83d6851
·
1 Parent(s): 6a876ee

restructure repo into production layout; add whatsapp-service, tests, ddl, docs, scripts; scrub hardcoded secrets

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +12 -7
  2. .env.example +180 -0
  3. .gitignore +20 -22
  4. Dockerfile +50 -1
  5. app/api/server.py +16 -1
  6. app/api/whatsapp.py +219 -0
  7. app/config.py +14 -0
  8. docs/whatsapp-gateway.md +130 -0
  9. start.sh +144 -2
  10. test_integration.py +0 -92
  11. tests/test_webhook_socket.py +0 -446
  12. whatsapp-service/.gitignore +20 -0
  13. whatsapp-service/LICENSE +13 -0
  14. whatsapp-service/Makefile +268 -0
  15. whatsapp-service/NOTICE +7 -0
  16. whatsapp-service/VERSION +1 -0
  17. whatsapp-service/cmd/agentdeck-whatsapp-service/main.go +397 -0
  18. whatsapp-service/docs/docs.go +0 -0
  19. whatsapp-service/docs/swagger.json +0 -0
  20. whatsapp-service/docs/swagger.yaml +0 -0
  21. whatsapp-service/go.mod +87 -0
  22. whatsapp-service/go.sum +241 -0
  23. whatsapp-service/pkg/cache/redis.go +64 -0
  24. whatsapp-service/pkg/call/handler/call_handler.go +60 -0
  25. whatsapp-service/pkg/call/service/call_service.go +95 -0
  26. whatsapp-service/pkg/chat/handler/chat_handler.go +337 -0
  27. whatsapp-service/pkg/chat/service/chat_service.go +256 -0
  28. whatsapp-service/pkg/community/handler/community_handler.go +160 -0
  29. whatsapp-service/pkg/community/service/community_service.go +169 -0
  30. whatsapp-service/pkg/config/config.go +338 -0
  31. whatsapp-service/pkg/config/env/env.go +63 -0
  32. whatsapp-service/pkg/core/c0.go +954 -0
  33. whatsapp-service/pkg/events/interfaces/producer.go +6 -0
  34. whatsapp-service/pkg/events/nats/nats_producer.go +81 -0
  35. whatsapp-service/pkg/events/rabbitmq/rabbitmq_producer.go +319 -0
  36. whatsapp-service/pkg/events/webhook/webhook_producer.go +97 -0
  37. whatsapp-service/pkg/events/websocket/websocket_producer.go +140 -0
  38. whatsapp-service/pkg/group/handler/group_handler.go +533 -0
  39. whatsapp-service/pkg/group/service/group_service.go +653 -0
  40. whatsapp-service/pkg/instance/handler/instance_handler.go +660 -0
  41. whatsapp-service/pkg/instance/model/instance_model.go +52 -0
  42. whatsapp-service/pkg/instance/repository/instance_repository.go +333 -0
  43. whatsapp-service/pkg/instance/service/instance_service.go +929 -0
  44. whatsapp-service/pkg/internal/event_types/event_types.go +64 -0
  45. whatsapp-service/pkg/label/handler/label_handler.go +297 -0
  46. whatsapp-service/pkg/label/model/label_model.go +10 -0
  47. whatsapp-service/pkg/label/repository/label_repository.go +144 -0
  48. whatsapp-service/pkg/label/service/label_service.go +240 -0
  49. whatsapp-service/pkg/logger/logger.go +146 -0
  50. 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 / deploy scripts (kept out of the image)
49
  tests/
50
- test_integration.py
51
- local_deploy.py
52
- deploy_sdk.py
53
- deploy_hf.py
54
- server.log
 
 
 
 
 
 
 
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
- .venv
86
- env/
87
- venv/
88
- ENV/
89
- env.bak/
90
- venv.bak/
 
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
- ddl
138
- API_DESCRIPTION.md
139
- ENTERPRISE-API-ROADMAP*.md
140
- API-Implementation-Plan
141
- google_oauth_test_creds.postman_environment.json
 
 
 
 
 
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
- if path.startswith("/api/v1/") and not _is_public_path(path, request.method):
 
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
- exec python -m uvicorn app.api.server:app \
 
 
 
 
 
 
 
 
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
+ }