diff --git a/.dockerignore b/.dockerignore index 1a0a938d3dc0e330763a290889010b4492da2331..1ac755fb4809055c4916f550cd6b8bff9d91943c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -37,7 +37,6 @@ logs/ *.sqlite3 # Large repo blobs not needed by the image -generated-python-sdk/ ddl/ searxng/ scripts/ @@ -45,10 +44,16 @@ scripts/ # NOTE: `lua/` (Redis Lua scripts) is loaded at runtime and MUST stay in the image. -# Tests / deploy scripts (kept out of the image) +# Tests (kept out of the image) tests/ -test_integration.py -local_deploy.py -deploy_sdk.py -deploy_hf.py -server.log + +# whatsapp-service: ship only what the Go build stage needs (pkg/, cmd/, +# go.mod, go.sum, docs/docs.go, VERSION). Everything else is dev/test/docs +# and must not bloat the build context or the image. Runtime configuration is +# injected as environment variables from the main app — the Go service never +# ships or reads a .env. +whatsapp-service/ddl/ +whatsapp-service/tests/ +whatsapp-service/LICENSE +whatsapp-service/NOTICE +whatsapp-service/Makefile diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..29c522dd72c053537d8f3c8609fd49feb535a255 --- /dev/null +++ b/.env.example @@ -0,0 +1,180 @@ +# ------------------------------------------------------------------ +# AgentDeck Backend - example environment +# Copy to `.env` (or set these in your deployment) and fill in real values. +# cp .env.example .env +# In production the app REFUSES TO START unless these are set to strong, +# non-default values: +# API_KEY, JWT_SECRET_KEY +# ------------------------------------------------------------------ + +# --- App --- +APP_NAME=All API Collection +ENVIRONMENT=production # production | development +HOST=0.0.0.0 +PORT=7860 +WORKERS=1 +LOG_LEVEL=INFO + +# --- Auth (required) --- +# Master bearer key for the whole API; must be long & random in production. +API_KEY=change-me-to-a-long-random-value +# Secret used to sign JWTs. Must be long & random (>=32 chars) in production. +JWT_SECRET_KEY=change-me-to-a-long-random-value +JWT_ALGORITHM=HS256 +JWT_ISSUER=all-api-collection +ADMIN_PASSWORD= + +# Per-client-IP global rate limit (requests / minute). 0 disables. +RATE_LIMIT_PER_MINUTE=60 + +# --- URL shortener --- +URL_SHORTENER_SECRET=change-me +URL_SHORTENER_BASE=http://localhost:7860/api/v1 + +# --- Limits --- +MAX_UPLOAD_BYTES=15728640 + +# --- Embeddings / vector store --- +EMBEDDING_MODEL=ibm-granite/granite-embedding-small-english-r2 +EMBEDDING_DIMENSION=384 +DEFAULT_TOP_K=10 +DATA_DIR=./data + +# --- Supabase --- +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_ANON_KEY= +SUPABASE_SERVICE_ROLE_KEY= +SUPABASE_SCHEMA=public + +# --- Redis (optional; scheduler / caching) --- +REDIS_URL= +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_DB=0 +REDIS_PASSWORD= +REDIS_SSL=false + +# --- Google Maps / GCP --- +GCP_API_KEY= +GOOGLE_MAPS_BASE_URL=https://maps.googleapis.com/maps/api + +# --- Google Cloud Storage --- +GCS_BUCKET_NAME= +GCS_SERVICE_ACCOUNT_KEY_PATH= + +# --- Google OAuth / Gmail (optional) --- +GOOGLE_OAUTH_CLIENT_ID= +GOOGLE_OAUTH_CLIENT_SECRET= + +# --- Web search (SearXNG) --- +SEARXNG_BASE_URL=http://localhost:8888 + +# --- Startup self-ping --- +SELF_PING_URL= + +# ------------------------------------------------------------------ +# --- WhatsApp service (single source of truth) --------------------- +# This `.env` is the ONLY configuration file. It is read by the main FastAPI +# app (pydantic-settings) AND, via start.sh / the Docker image, forwarded to +# the internal AgentDeck WhatsApp service (Go) as its process environment. +# The Go service never reads a `.env` of its own — it consumes injected env +# vars only. In a container deployment these values are set on the platform +# (Hugging Face Spaces secrets) and start.sh passes them straight through. +# ------------------------------------------------------------------ + +# --- Gateway settings (read by the FastAPI app) --- +# The main app reverse-proxies /api/whatsapp/* to the WhatsApp service. +# When the image embeds the binary (start.sh) the gateway is auto-enabled; +# set explicitly to control it. WHATSAPP_SERVICE_URL may be a service name +# (http://whatsapp-service:8080) when the WhatsApp service runs as a separate +# container, or loopback when co-hosted in this image. +WHATSAPP_SERVICE_ENABLED=false +WHATSAPP_SERVICE_URL=http://127.0.0.1:8080 +WHATSAPP_SERVICE_TIMEOUT=30 +WHATSAPP_SERVICE_CONNECT_TIMEOUT=5 +# Port start.sh launches the embedded Go binary on (its SERVER_PORT). +WHATSAPP_SERVICE_PORT=8080 + +# --- Settings forwarded to the Go service (start.sh / Docker) --- +# Global API key of the WhatsApp service. Used by the gateway as the default +# `apikey` header AND injected into the Go process as GLOBAL_API_KEY. +WHATSAPP_SERVICE_GLOBAL_API_KEY= + +# The Go service requires: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY (injected +# as SUPABASE_SERVICE_KEY), SUPABASE_DB_URL, REDIS_URL, GLOBAL_API_KEY (see +# WHATSAPP_SERVICE_GLOBAL_API_KEY) and DATABASE_SAVE_MESSAGES. SUPABASE_URL +# and REDIS_URL are the shared values defined above; SUPABASE_SERVICE_ROLE_KEY +# is mapped to the Go service's expected name (SUPABASE_SERVICE_KEY) for you. +# SUPABASE_DB_URL is a direct native-Postgres connection string (used by the +# whatsmeow session store — NOT the PostgREST URL): +SUPABASE_DB_URL=postgresql://postgres:postgres@db.your-project.supabase.co:5432/postgres +DATABASE_SAVE_MESSAGES=false +CLIENT_NAME=agentdeck +CONNECT_ON_STARTUP=false + +# Optional WhatsApp service behaviour (documented defaults = Go service defaults). +DEBUG_ENABLED=INFO # whatsmeow debug level (mapped from legacy WADEBUG) +LOG_TYPE=console # console | file +WEBHOOK_FILES=true +OS_NAME=AgentDeck +WHATSAPP_VERSION_MAJOR=2 +WHATSAPP_VERSION_MINOR=3000 +WHATSAPP_VERSION_PATCH=1 +# EVENT_IGNORE_GROUP=false +# EVENT_IGNORE_STATUS=false +# QRCODE_MAX_COUNT=5 +# CHECK_USER_EXISTS=true + +# MinIO media storage (only if MINIO_ENABLED=true). +MINIO_ENABLED=false +# MINIO_ENDPOINT=localhost:9000 +# MINIO_ACCESS_KEY=minioadmin +# MINIO_SECRET_KEY=minioadmin +# MINIO_BUCKET=agentdeck-media +# MINIO_USE_SSL=false +# MINIO_REGION= + +# RabbitMQ / NATS event producers (optional). +# AMQP_URL=amqp://admin:admin@localhost:5672/default +# AMQP_GLOBAL_ENABLED=false +# AMQP_GLOBAL_EVENTS= +# AMQP_SPECIFIC_EVENTS= +# NATS_URL= +# NATS_GLOBAL_ENABLED=false +# NATS_GLOBAL_EVENTS= +# WEBHOOK_URL= + +# Outbound proxy for the WhatsApp service (optional). +# PROXY_PROTOCOL=http # http | https | socks5 +# PROXY_HOST= +# PROXY_PORT= +# PROXY_USERNAME= +# PROXY_PASSWORD= + +# Audio converter (optional). +# API_AUDIO_CONVERTER= +# API_AUDIO_CONVERTER_KEY= + +# Logger rotation (optional). +# LOG_MAX_SIZE=100 # MB +# LOG_MAX_BACKUPS=5 +# LOG_MAX_AGE=30 # days +# LOG_DIRECTORY=./logs +# LOG_COMPRESS=true + +# --- Concurrency --- +# Shared thread pool worker count. Unset => auto-calc min(32, cpu_count+4). +CORE_CONCURRENCY= + +# --- PaddleOCR PP-OCRv6 (ONNX Runtime) --- +# OCR_ENGINE=onnxruntime # paddle | paddle_static | paddle_dynamic | onnxruntime | transformers +# OCR_DEVICE=cpu +# OCR_LANG= # e.g. en; empty uses model defaults +# OCR_DET_MODEL_NAME=PP-OCRv6_small_det +# OCR_REC_MODEL_NAME=PP-OCRv6_small_rec +# OCR_USE_DOC_ORIENTATION_CLASSIFY=false +# OCR_USE_DOC_UNWARPING=false +# OCR_USE_TEXTLINE_ORIENTATION=true +# OCR_MAX_CONCURRENT=1 # max parallel predict calls on shared engine +# Enrich raw/OCR content into labeled Markdown before LLM extraction (default true). +CONTENT_STRUCTURE_ENRICHMENT=true diff --git a/.gitignore b/.gitignore index 1cf8822828f3b145992929bfe155ed852c08f6eb..fee1483116ccd7e790c7865c4f81974c5ae79153 100644 --- a/.gitignore +++ b/.gitignore @@ -4,12 +4,10 @@ __pycache__/ postman_collection.json # Runtime data (vector stores, SQLite DBs, model caches, etc.) -data/ +/data/ *.so -test_deploy_flow.py .Python -hammer_tidb.py build/ develop-eggs/ dist/ @@ -58,7 +56,7 @@ local_settings.py db.sqlite3 db.sqlite3-journal -instance/ +/instance/ .webassets-cache .scrapy @@ -82,12 +80,13 @@ celerybeat.pid *.sage.py .env* -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ +!.env.example +/.venv +/env/ +/venv/ +/ENV/ +/env.bak/ +/venv.bak/ .env.local .spyderproject @@ -117,9 +116,9 @@ dmypy.json ehthumbs.db Thumbs.db -logs/ +/logs/ *.log -persistence/ +/persistence/ *.db *.sqlite3 @@ -127,15 +126,14 @@ persistence/ *.tmp *.temp -local_deploy.py -test_vector_store_async.py -deploy_sdk.py -tests -deploy_hf.py .mimocode -ddl -API_DESCRIPTION.md -ENTERPRISE-API-ROADMAP*.md -API-Implementation-Plan -google_oauth_test_creds.postman_environment.json \ No newline at end of file +# Secrets +google_oauth_test_creds.postman_environment.json + +# Local dev/test/doc artifacts (kept out of the production repo) +tests/ +ddl/ +scripts/ +docs/API_DESCRIPTION.md +docs/planning/ diff --git a/Dockerfile b/Dockerfile index 1126c8abf9f7ee0034f6aef4c37984869ae33cc0..59928a815e7f8dbfb7ca7710955efb7213a4a0ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,31 @@ +# --------------------------------------------------------------------------- +# Build stage: the internal WhatsApp service (Go / whatsmeow). +# The binary is built here (CGO + libjpeg/libwebp headers) and embedded in the +# runtime image as a sibling process started by start.sh. Runtime configuration +# is NOT baked in: the Go service receives every setting as an environment +# variable injected by the deployment platform / start.sh (single source of +# truth = the main application's environment / .env). The Go module requires +# Go 1.25. +# --------------------------------------------------------------------------- +FROM golang:1.25.0-alpine AS whatsapp-build + +RUN echo "https://dl-4.alpinelinux.org/alpine/v3.22/main" > /etc/apk/repositories \ + && echo "https://dl-4.alpinelinux.org/alpine/v3.22/community" >> /etc/apk/repositories \ + && apk update && apk add --no-cache git build-base libjpeg-turbo-dev libwebp-dev + +WORKDIR /build + +COPY whatsapp-service/go.mod whatsapp-service/go.sum ./ +RUN go mod download + +COPY whatsapp-service/ . + +ARG WHATSAPP_VERSION=dev +RUN CGO_ENABLED=1 go build -ldflags "-X main.version=${WHATSAPP_VERSION}" -o server ./cmd/agentdeck-whatsapp-service + +# --------------------------------------------------------------------------- +# Runtime image +# --------------------------------------------------------------------------- FROM python:3.11-slim LABEL maintainer="AgentDeck-Backend" @@ -16,6 +44,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libssl-dev \ nodejs \ zlib1g-dev \ + # Runtime libs for the embedded WhatsApp (Go) service: media codecs and + # timezone data required by whatsmeow/ffmpeg processing. + libjpeg62-turbo \ + libwebp7 \ + poppler-utils \ + tzdata \ # Runtime libs for PaddleOCR / opencv-contrib-python (cv2), mirroring the # reference reconciliation-file-processing-service Dockerfile. libglib2.0-0 \ @@ -43,6 +77,13 @@ RUN git clone --depth 1 --branch master https://github.com/searxng/searxng.git / COPY --chown=appuser:appuser . . +# Replace the WhatsApp service source tree (copied above from the repo) with +# just the compiled binary + VERSION baked from the whatsapp-build stage. +RUN rm -rf /app/whatsapp-service +COPY --from=whatsapp-build /build/server /app/whatsapp-service/server +COPY --from=whatsapp-build /build/VERSION /app/whatsapp-service/VERSION +RUN chmod +x /app/whatsapp-service/server + 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 RUN mkdir -p /app/data /app/logs && \ @@ -55,9 +96,17 @@ USER appuser ENV PYTHONPATH=/app ENV PYTHONUNBUFFERED=1 +# Path to the embedded WhatsApp service binary (started by start.sh as a +# sibling process). Set to an empty value to disable the WhatsApp gateway. +# The Go service reads all runtime settings (SUPABASE_URL, SUPABASE_DB_URL, +# REDIS_URL, GLOBAL_API_KEY, ...) from the environment injected by the +# platform / start.sh — no .env is shipped for it. All values live in the +# main application's centralized configuration (see .env.example). +ENV WHATSAPP_SERVICE_BINARY=/app/whatsapp-service/server + EXPOSE 7860 HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:7860/health')" || exit 1 -CMD ["/bin/bash", "/app/start.sh"] \ No newline at end of file +CMD ["/bin/bash", "/app/start.sh"] diff --git a/app/api/server.py b/app/api/server.py index b560b1f8aa10c8abccd34e2c0f85aff734f12ee9..69b2306bc569bf4693ecc6e8208ed04b4e870610 100644 --- a/app/api/server.py +++ b/app/api/server.py @@ -13,6 +13,11 @@ from slowapi.util import get_remote_address from app.api.v1.router import api_v1_router from app.api.v1.system import is_maintenance +from app.api.whatsapp import ( + close_whatsapp_proxy_client, + get_whatsapp_health, + router as whatsapp_router, +) from app.config import get_settings from app.core.auth.deps import init_auth_db from app.core.database import pool_manager @@ -169,6 +174,7 @@ async def lifespan(app: FastAPI): from app.utils.http_utils import close_shared_aiohttp_sessions await close_shared_aiohttp_sessions() await _ping_client.close() + await close_whatsapp_proxy_client() from app.services.supabase import get_supabase_client client = get_supabase_client() if client: @@ -192,6 +198,7 @@ def create_application() -> FastAPI: {"name": "Vector Stores", "description": "Create, manage, and search vector stores for RAG"}, {"name": "URL Shortener", "description": "Create and manage short URLs with analytics"}, {"name": "Media-to-Media Conversion", "description": "PDF-to-image and image-to-image conversion with local or Supabase Storage output"}, + {"name": "WhatsApp", "description": "Gateway to the internal WhatsApp service"}, ], lifespan=lifespan, ) @@ -238,7 +245,8 @@ def create_application() -> FastAPI: @app.middleware("http") async def auth_middleware(request: Request, call_next): path = request.url.path - if path.startswith("/api/v1/") and not _is_public_path(path, request.method): + is_api = path.startswith("/api/v1/") or path.startswith("/api/whatsapp/") + if is_api and not _is_public_path(path, request.method): auth_header = request.headers.get("Authorization", "") if not auth_header.startswith("Bearer "): from starlette.responses import JSONResponse @@ -261,6 +269,12 @@ def create_application() -> FastAPI: dependencies=[Depends(_api_rate_limit)], ) + app.include_router( + whatsapp_router, + prefix="/api/whatsapp", + dependencies=[Depends(_api_rate_limit)], + ) + @app.get("/", include_in_schema=False) async def root(request: Request): from collections import defaultdict @@ -298,6 +312,7 @@ def create_application() -> FastAPI: "vector_store_count": store_count, "total_documents": doc_count, "model_loaded": _embedding_service.is_loaded(384), + "whatsapp_service": await get_whatsapp_health(), } @app.get("/ping", include_in_schema=False) diff --git a/app/api/whatsapp.py b/app/api/whatsapp.py new file mode 100644 index 0000000000000000000000000000000000000000..2618a47ff0f93f16a0e69f6d46363db49f2a1852 --- /dev/null +++ b/app/api/whatsapp.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +from typing import Dict +from urllib.parse import quote + +import httpx +from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse, StreamingResponse +from starlette.background import BackgroundTask + +from app.config import get_settings +from app.core.logger import get_logger +from app.utils.http_utils import SharedAsyncClient + +logger = get_logger(__name__) +_settings = get_settings() + +router = APIRouter(tags=["WhatsApp"]) + +# RFC 7230 hop-by-hop headers. They are meaningless when a gateway relays a +# request to another service and must never be forwarded. +_HOP_BY_HOP_HEADERS = frozenset({ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", +}) + +# Request headers that must never reach the internal WhatsApp service: +# hop-by-hop headers plus Host (httpx sets it from the target URL), length +# headers (httpx recomputes them from the body), content-encoding (httpx +# transparently decompresses responses), and the gateway's own credentials +# (Authorization / cookie) which belong to the main API, not the backend. +_BLOCKED_REQUEST_HEADERS = _HOP_BY_HOP_HEADERS | { + "host", + "content-length", + "accept-encoding", + "authorization", + "cookie", +} + +# Response headers preserved when relaying the upstream reply back to the +# client. Everything else (server version headers, content-encoding, hop-by-hop +# headers, ...) is dropped so internal implementation details never leak. +_RESPONSE_HEADERS_ALLOWLIST = frozenset({ + "content-type", + "content-disposition", + "content-language", + "cache-control", + "etag", + "expires", + "last-modified", + "location", + "retry-after", + "www-authenticate", + "x-request-id", + "x-correlation-id", + "content-range", + "accept-ranges", +}) + +_ALLOWED_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD") + +# Characters preserved while percent-encoding the forwarded path so a client +# supplied path can never produce a malformed upstream URL. +_PATH_SAFE_CHARS = "/:@!$&'()*+,;=~-_." + + +def _build_proxy_client() -> SharedAsyncClient: + """Shared, lazily-created httpx client for upstream WhatsApp calls.""" + timeout = httpx.Timeout( + timeout=_settings.whatsapp_service_timeout, + connect=_settings.whatsapp_service_connect_timeout, + ) + return SharedAsyncClient(timeout=timeout, follow_redirects=False) + + +_proxy_client = _build_proxy_client() + + +async def close_whatsapp_proxy_client() -> None: + """Close the shared upstream client. Called once on application shutdown.""" + await _proxy_client.close() + + +def _forward_request_headers(request: Request, body: bytes) -> Dict[str, str]: + """Build the header set forwarded to the WhatsApp service. + + Keeps application headers (content-type, accept, apikey, x-*, ...) while + stripping hop-by-hop / gateway-credential headers. The `apikey` header the + WhatsApp service authenticates with is taken from the client request when + present (per-instance token) and falls back to the configured global key. + """ + headers: Dict[str, str] = {} + for name, value in request.headers.items(): + if name.lower() in _BLOCKED_REQUEST_HEADERS: + continue + headers[name] = value + + if body: + headers.setdefault("content-type", "application/octet-stream") + + client_apikey = request.headers.get("apikey", "") + if client_apikey: + headers["apikey"] = client_apikey + elif _settings.whatsapp_service_global_api_key: + headers["apikey"] = _settings.whatsapp_service_global_api_key + return headers + + +def _filter_response_headers(headers: httpx.Headers) -> Dict[str, str]: + return { + name: value + for name, value in headers.items() + if name.lower() in _RESPONSE_HEADERS_ALLOWLIST + } + + +def _unavailable_response(status_code: int, detail: str) -> JSONResponse: + """Sanitized gateway error response — never leaks upstream hostnames/traces.""" + return JSONResponse( + status_code=status_code, + content={"success": False, "detail": detail}, + ) + + +@router.api_route( + "/{path:path}", + methods=list(_ALLOWED_METHODS), + summary="Forward a request to the internal WhatsApp service", + description=( + "Proxies any HTTP request under /api/whatsapp to the corresponding " + "endpoint of the internal WhatsApp service, preserving the HTTP method, " + "path, query string, request body and relevant headers. The upstream " + "response (status code and body) is returned unchanged." + ), +) +async def proxy_to_whatsapp(request: Request, path: str): + if not _settings.whatsapp_service_enabled: + return _unavailable_response(503, "WhatsApp service is not enabled") + + base_url = _settings.whatsapp_service_url.rstrip("/") + encoded_path = quote(path, safe=_PATH_SAFE_CHARS).lstrip("/") + target = f"{base_url}/{encoded_path}" + if request.url.query: + target = f"{target}?{request.url.query}" + + body = await request.body() + headers = _forward_request_headers(request, body) + + logger.info( + "Proxying %s %s -> %s (apikey=%s)", + request.method, + request.url.path, + target, + "yes" if headers.get("apikey") else "no", + ) + + try: + client = await _proxy_client.get() + upstream = await client.send( + client.build_request( + request.method, + target, + content=body or None, + headers=headers, + ), + stream=True, + ) + except httpx.TimeoutException as exc: + logger.error("WhatsApp service timed out: %s %s: %s", request.method, target, exc) + return _unavailable_response(504, "WhatsApp service timed out") + except httpx.HTTPError as exc: + logger.error("WhatsApp service unreachable: %s %s: %s", request.method, target, exc) + return _unavailable_response(502, "WhatsApp service is unavailable") + except Exception: + logger.exception("Unexpected gateway error proxying %s %s", request.method, target) + return _unavailable_response(502, "WhatsApp gateway error") + + return StreamingResponse( + upstream.aiter_bytes(), + status_code=upstream.status_code, + headers=_filter_response_headers(upstream.headers), + media_type=None, + background=BackgroundTask(upstream.aclose), + ) + + +async def get_whatsapp_health() -> Dict[str, object]: + """Non-fatal liveness probe used by the main application's /health endpoint. + + Never raises — a degraded/unreachable WhatsApp service must not take the + gateway's own health check down with it. + """ + if not _settings.whatsapp_service_enabled: + return {"configured": False, "reachable": False} + + url = f"{_settings.whatsapp_service_url.rstrip('/')}/server/ok" + try: + client = await _proxy_client.get() + resp = await client.get( + url, + timeout=_settings.whatsapp_service_connect_timeout, + ) + except (httpx.HTTPError, httpx.TimeoutException) as exc: + logger.warning("WhatsApp service health check failed: %s", exc) + return {"configured": True, "reachable": False, "status": "unreachable"} + + if resp.status_code == 200: + return {"configured": True, "reachable": True, "status": "ok"} + return { + "configured": True, + "reachable": True, + "status": f"unhealthy (HTTP {resp.status_code})", + } diff --git a/app/config.py b/app/config.py index e90be7e9abea55f809fe22085c791cff3a074d37..7aaf5d713c39c11c36c5d18ad71b414d2606dd8e 100644 --- a/app/config.py +++ b/app/config.py @@ -191,6 +191,20 @@ class Settings(BaseSettings): supabase_storage_bucket: str = Field(default="media-convert", alias="SUPABASE_STORAGE_BUCKET") supabase_signed_url_ttl_seconds: int = Field(default=86400, alias="SUPABASE_SIGNED_URL_TTL_SECONDS") + # WhatsApp service gateway. The main FastAPI app acts as a reverse proxy to + # the internal AgentDeck WhatsApp service (a Go application). Set + # WHATSAPP_SERVICE_ENABLED=true and point WHATSAPP_SERVICE_URL at the + # internal service (e.g. http://localhost:8080 when co-hosted, or + # http://whatsapp-service:8080 in a Docker network). + whatsapp_service_enabled: bool = Field(default=False, alias="WHATSAPP_SERVICE_ENABLED") + whatsapp_service_url: str = Field(default="http://localhost:8080", alias="WHATSAPP_SERVICE_URL") + whatsapp_service_timeout: float = Field(default=30.0, alias="WHATSAPP_SERVICE_TIMEOUT") + whatsapp_service_connect_timeout: float = Field(default=5.0, alias="WHATSAPP_SERVICE_CONNECT_TIMEOUT") + # Global API key of the WhatsApp service (GLOBAL_API_KEY). Used as the + # default `apikey` header when proxying admin routes; a per-instance token + # provided by the client is always forwarded unchanged. + whatsapp_service_global_api_key: str = Field(default="", alias="WHATSAPP_SERVICE_GLOBAL_API_KEY") + # Scheduler settings max_http_timeout: float = 300.0 default_scheduler_timezone: str = "UTC" diff --git a/docs/whatsapp-gateway.md b/docs/whatsapp-gateway.md new file mode 100644 index 0000000000000000000000000000000000000000..600eba1ac3dc5e1882aaa939e3f07f171f292dcc --- /dev/null +++ b/docs/whatsapp-gateway.md @@ -0,0 +1,130 @@ +# WhatsApp Service Gateway + +The main FastAPI application (port **7860**) is the **public API gateway**. It +proxies every request under `/api/whatsapp/*` to the internal WhatsApp service +(a Go/whatsmeow application), which is not exposed publicly. + +```text +Client + | HTTP + v +Main FastAPI :7860 + | /api/whatsapp/... + v +WhatsApp Service (internal, e.g. http://localhost:8080) + | + v +Client +``` + +## Route mapping + +The gateway forwards the **full path, query string, HTTP method and body** +unchanged. All routes require the main application's API key +(`Authorization: Bearer `). + +| Client request | WhatsApp service call | +|---|---| +| `GET /api/whatsapp/server/ok` | `GET /server/ok` (health) | +| `POST /api/whatsapp/instance/create` | `POST /instance/create` (admin) | +| `GET /api/whatsapp/instance/status` | `GET /instance/status` | +| `POST /api/whatsapp/send/text` | `POST /send/text` | +| `DELETE /api/whatsapp/instance/delete/:id` | `DELETE /instance/delete/:id` | + +No path transformation is applied: after the `/api/whatsapp` prefix is stripped, +the rest of the path is appended to `WHATSAPP_SERVICE_URL`. + +## Deployment model + +Single Docker container (Hugging Face Spaces style): the Go binary is compiled +in a multi-stage `whatsapp-build` stage and embedded in the image at +`/app/whatsapp-service/server`. `start.sh` launches it as a sibling process and +`uvicorn` is the primary process. The gateway reaches it over loopback +(`http://127.0.0.1:8080`). + +A crash of the WhatsApp service does **not** take the main app down: the gateway +returns sanitized `502`/`503` responses until it recovers (restart loop every +5s, degraded mode). + +## Gateway behaviour + +- **Auth:** the gateway requires the main app's Bearer API key. The WhatsApp + service's own `apikey` header is forwarded as-is when provided by the client + (per-instance token); otherwise the configured + `WHATSAPP_SERVICE_GLOBAL_API_KEY` is injected (admin routes). +- **Headers:** hop-by-hop headers and the gateway's `Authorization`/`Cookie` are + never forwarded. Response headers are whitelisted — internal + `Server`/`content-encoding`/hop-by-hop headers never reach the client. +- **Errors:** connection failures → `502`; timeouts → `504`; disabled service → + `503`. Upstream 4xx/5xx responses are passed through unchanged. Error bodies + never contain internal hostnames, URLs, exceptions or stack traces. +- **Payments/streaming:** responses are streamed back to the client (media/QR + downloads are not buffered). + +## Configuration + +| Variable | Default | Description | +|---|---|---| +| `WHATSAPP_SERVICE_ENABLED` | `false` | Enable the gateway routes. `start.sh` auto-enables it when the binary exists. | +| `WHATSAPP_SERVICE_URL` | `http://localhost:8080` | Base URL of the internal WhatsApp service. | +| `WHATSAPP_SERVICE_TIMEOUT` | `30` | Read/write/pool timeout for upstream calls (seconds). | +| `WHATSAPP_SERVICE_CONNECT_TIMEOUT` | `5` | Connect timeout for upstream calls (seconds). | +| `WHATSAPP_SERVICE_GLOBAL_API_KEY` | _(empty)_ | The WhatsApp service `GLOBAL_API_KEY`, injected as the default `apikey` header. | +| `WHATSAPP_SERVICE_PORT` | `8080` | Port `start.sh` uses to launch the embedded binary (`SERVER_PORT`). | +| `WHATSAPP_SERVICE_BINARY` | `/app/whatsapp-service/server` | Path to the embedded binary. | + +### WhatsApp service env vars (centralized in the main app) + +There is exactly **one** configuration source: the main application's `.env` +(or the deployment platform's environment). The Go service **never reads a +`.env` of its own** — `start.sh` / the Docker image inject its variables as +process environment, and `--dev` only optionally loads a local `.env` if one +exists. Full reference: the WhatsApp section of `.env.example`. + +Values shared with the main app use the same names (`SUPABASE_URL`, +`REDIS_URL`, ...). A few names are mapped automatically by `start.sh` so you +only configure one value: + +| Centralized setting | Injected into Go service as | +|---|---| +| `SUPABASE_SERVICE_ROLE_KEY` | `SUPABASE_SERVICE_KEY` | +| `WHATSAPP_SERVICE_GLOBAL_API_KEY` | `GLOBAL_API_KEY` | +| `WHATSAPP_SERVICE_PORT` | `SERVER_PORT` | + +Other Go-service settings configured centrally: `SUPABASE_DB_URL`, +`DATABASE_SAVE_MESSAGES`, `CLIENT_NAME`, `CONNECT_ON_STARTUP`, `DEBUG_ENABLED`, +`LOG_TYPE`, `WEBHOOK_FILES`, `OS_NAME`, `WHATSAPP_VERSION_*`, `MINIO_*`, +`AMQP_*`, `NATS_*`, `WEBHOOK_URL`, `PROXY_*`, `API_AUDIO_CONVERTER*`, +`EVENT_IGNORE_*`, `QRCODE_MAX_COUNT`, `CHECK_USER_EXISTS`, `LOG_*`. + +> Configure `WHATSAPP_SERVICE_GLOBAL_API_KEY` (equivalently `GLOBAL_API_KEY`) +> once — it feeds both the gateway's default `apikey` header and the Go +> service's authentication. + +## Health checks + +The main app's `GET /health` now includes a non-fatal `whatsapp_service` block: + +```json +{"configured": true, "reachable": true, "status": "ok"} +``` + +The WhatsApp service's own liveness endpoint is exposed through the gateway at +`GET /api/whatsapp/server/ok`. + +## Testing + +```bash +python -m pytest tests/test_whatsapp_gateway.py -v +``` + +Covers: gateway auth, forwarding (method/path/query/body/headers), apikey +injection vs pass-through, upstream 4xx/5xx passthrough, 502/503/504 handling, +response-header sanitization and the `/health` integration. + +A full stack test: + +```bash +docker build -t agentdeck-backend:test . +# run with the WhatsApp env vars above + WHATSAPP_SERVICE_ENABLED=true +``` diff --git a/start.sh b/start.sh index 023fcf72e188f86a44525b978b95301c1ff3f65e..3473a344246fe611e6cb5235853ab6ca0703ec48 100644 --- a/start.sh +++ b/start.sh @@ -13,6 +13,42 @@ print(banner) " 2>/dev/null || true echo "==============================================================================" +# ----------------------------------------------------------------------------- +# Environment: the main app's `.env`/`.env.local` are the SINGLE source of truth +# for every process this container starts (FastAPI, SearXNG, the embedded +# WhatsApp/Go service). Load them into the shell environment so sibling +# processes inherit the same configuration. In Docker these files are not +# shipped — the deployment platform injects the environment — so this is a +# no-op. Precedence (highest first): process environment, `.env.local`, `.env`. +# ----------------------------------------------------------------------------- +declare -A _ENV_FROM_PROCESS +while IFS= read -r _envline; do + _ENV_FROM_PROCESS["${_envline%%=*}"]=1 +done < <(env 2>/dev/null) + +load_dotenv() { + local file="$1" + [ -f "$file" ] || return 0 + local key val + while IFS='=' read -r key val; do + key="${key#"${key%%[![:space:]]*}"}" + [ -n "$key" ] || continue + case "$key" in + \#*) continue ;; + esac + case "$val" in + \"*\") val="${val#\"}"; val="${val%\"}" ;; + \'*\') val="${val#\'}"; val="${val%\'}" ;; + esac + if [ -z "${_ENV_FROM_PROCESS[$key]:-}" ]; then + export "$key=$val" + fi + done < "$file" +} + +load_dotenv ".env" +load_dotenv ".env.local" + log "=== Starting up ===" log "Python: $(python --version 2>&1)" log "Working dir: $(pwd)" @@ -53,6 +89,99 @@ else log "WARNING: SearXNG settings not found at $SEARXNG_SETTINGS_PATH, skipping SearXNG start" fi +# ───────────────────────────────────────────────────────────────────────────── +# WhatsApp service (internal Go backend) +# +# The compiled whatsapp binary is embedded in the image (Dockerfile) and runs as +# a sibling process inside this container. The main FastAPI app proxies +# /api/whatsapp/* to it. If the binary is present the gateway is auto-enabled; +# set WHATSAPP_SERVICE_ENABLED=false to force it off, or set it explicitly. +# ───────────────────────────────────────────────────────────────────────────── + +WHATSAPP_SERVICE_BINARY="${WHATSAPP_SERVICE_BINARY:-/app/whatsapp-service/server}" +WHATSAPP_SERVICE_PORT="${WHATSAPP_SERVICE_PORT:-8080}" +WHATSAPP_SERVICE_ENABLED="${WHATSAPP_SERVICE_ENABLED:-}" +WHATSAPP_PID="" + +if [ -z "$WHATSAPP_SERVICE_ENABLED" ]; then + if [ -x "$WHATSAPP_SERVICE_BINARY" ]; then + WHATSAPP_SERVICE_ENABLED="true" + else + WHATSAPP_SERVICE_ENABLED="false" + fi +fi + +export WHATSAPP_SERVICE_ENABLED +export WHATSAPP_SERVICE_URL="${WHATSAPP_SERVICE_URL:-http://127.0.0.1:${WHATSAPP_SERVICE_PORT}}" +# Single source of truth: operator sets one of WHATSAPP_SERVICE_GLOBAL_API_KEY / +# GLOBAL_API_KEY; both the gateway (FastAPI) and the Go service receive it. +export WHATSAPP_SERVICE_GLOBAL_API_KEY="${WHATSAPP_SERVICE_GLOBAL_API_KEY:-${GLOBAL_API_KEY:-}}" + +# Map the main app's centralized settings onto the names the WhatsApp (Go) +# service expects. Its process environment is built from THIS configuration +# only — the Go service never reads a .env of its own. +export SUPABASE_SERVICE_KEY="${SUPABASE_SERVICE_KEY:-${SUPABASE_SERVICE_ROLE_KEY:-}}" + +# SUPABASE_DB_URL: derive the native Postgres URL from the pooler/direct URL +# (session-mode port 5432) when not set explicitly — single source of truth. +if [ -z "${SUPABASE_DB_URL:-}" ]; then + if [ -n "${POSTGRES_URL_POOLER:-}" ]; then + export SUPABASE_DB_URL="${POSTGRES_URL_POOLER/:6543\//:5432/}" + elif [ -n "${POSTGRES_URL:-}" ]; then + export SUPABASE_DB_URL="$POSTGRES_URL" + fi +fi + +# Defaults for Go-service-only settings (not consumed by the main app). +export DATABASE_SAVE_MESSAGES="${DATABASE_SAVE_MESSAGES:-false}" +export CLIENT_NAME="${CLIENT_NAME:-agentdeck}" + +if [ "$WHATSAPP_SERVICE_ENABLED" = "true" ]; then + if [ ! -x "$WHATSAPP_SERVICE_BINARY" ]; then + log "WARNING: WHATSAPP_SERVICE_ENABLED=true but binary not found at $WHATSAPP_SERVICE_BINARY — WhatsApp gateway disabled" + export WHATSAPP_SERVICE_ENABLED="false" + else + log "Starting WhatsApp service on port $WHATSAPP_SERVICE_PORT..." + : > /tmp/whatsapp-service.log + WHATSAPP_SERVICE_DIR="$(dirname "$WHATSAPP_SERVICE_BINARY")" + + # Run the Go service in a restart loop so a crash does not take the + # main gateway down; the gateway returns 502/503 while it is down. + ( + while true; do + cd "$WHATSAPP_SERVICE_DIR" 2>/dev/null || true + GLOBAL_API_KEY="${GLOBAL_API_KEY:-${WHATSAPP_SERVICE_GLOBAL_API_KEY:-}}" \ + SERVER_PORT="$WHATSAPP_SERVICE_PORT" \ + "$WHATSAPP_SERVICE_BINARY" >> /tmp/whatsapp-service.log 2>&1 + code=$? + log "WARNING: WhatsApp service exited (code $code) — restarting in 5s" + sleep 5 + done + ) & + WHATSAPP_PID=$! + log "WhatsApp service process group PID: $WHATSAPP_PID" + + # Wait for readiness (best effort — non-fatal, gateway degrades gracefully) + WHATSAPP_HEALTH_URL="http://127.0.0.1:${WHATSAPP_SERVICE_PORT}/server/ok" + WHATSAPP_READY=0 + for _ in $(seq 1 30); do + if curl -fsS -o /dev/null "$WHATSAPP_HEALTH_URL" 2>/dev/null; then + WHATSAPP_READY=1 + break + fi + sleep 1 + done + if [ "$WHATSAPP_READY" = "1" ]; then + log "WhatsApp service healthy: $WHATSAPP_HEALTH_URL" + else + log "WARNING: WhatsApp service not ready at $WHATSAPP_HEALTH_URL — gateway runs in degraded mode" + tail -20 /tmp/whatsapp-service.log 2>/dev/null | while IFS= read -r line; do log " whatsapp: $line"; done + fi + fi +else + log "WhatsApp service disabled (WHATSAPP_SERVICE_ENABLED=$WHATSAPP_SERVICE_ENABLED)" +fi + # Start main app HOST="${HOST:-0.0.0.0}" PORT="${PORT:-7860}" @@ -60,9 +189,22 @@ WORKERS="${WORKERS:-1}" LOG_LEVEL="${LOG_LEVEL:-info}" log "Starting uvicorn: host=$HOST port=$PORT workers=$WORKERS log_level=$LOG_LEVEL" -exec python -m uvicorn app.api.server:app \ + +cleanup() { + log "Shutting down..." + [ -n "$WHATSAPP_PID" ] && kill "$WHATSAPP_PID" 2>/dev/null + [ -n "${UVICORN_PID:-}" ] && kill "$UVICORN_PID" 2>/dev/null +} +trap cleanup TERM INT + +python -m uvicorn app.api.server:app \ --host "$HOST" \ --port "$PORT" \ --workers "$WORKERS" \ --log-level "$LOG_LEVEL" \ - --access-log + --access-log & +UVICORN_PID=$! + +# Wait on uvicorn only: if the WhatsApp service dies, the main API keeps +# serving (degraded mode) instead of taking the whole container down. +wait "$UVICORN_PID" diff --git a/test_integration.py b/test_integration.py deleted file mode 100644 index b0fd5d77f04ea3b0e2fca5c92769b223e2048acd..0000000000000000000000000000000000000000 --- a/test_integration.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Integration tests against running FastAPI server + SearXNG container.""" - -import json -import sys -import urllib.request - -BASE = "http://localhost:8000/api/v1" -HEADERS = { - "Authorization": "Bearer N9ooESH05AiXrlpEKilv3o7OY1Rl5Pui", - "Content-Type": "application/json", -} - - -def req(method, path, body=None): - url = f"{BASE}{path}" - data = json.dumps(body).encode() if body else None - r = urllib.request.Request(url, data=data, headers=HEADERS, method=method) - resp = urllib.request.urlopen(r, timeout=30) - return json.loads(resp.read()) - - -passed = 0 -failed = 0 - - -def check(name, ok, detail=""): - global passed, failed - if ok: - passed += 1 - print(f" PASS: {name}") - else: - failed += 1 - print(f" FAIL: {name} - {detail}") - - -# ---- Test 1: Health ---- -print("1. Health check") -r = req("GET", "/web-search/health") -check("health returns dict", isinstance(r, dict)) - -# ---- Test 2: Search GET ---- -print("\n2. Search GET") -r = req("GET", "/web-search?q=hello+world&max_results=3") -check("success=True", r["success"] is True) -check("has results", r["number_of_results"] > 0) -check("title is string", isinstance(r["results"][0]["title"], str)) - -# ---- Test 3: Search POST ---- -print("\n3. Search POST") -r = req("POST", "/web-search", {"q": "python programming", "categories": "general,it", "max_results": 3}) -check("success=True", r["success"] is True) -check("has results", r["number_of_results"] > 0) - -# ---- Test 4: Autocomplete GET ---- -print("\n4. Autocomplete GET") -r = req("GET", "/web-search/autocomplete?q=hello+wor") -check("success=True", r["success"] is True) -check("has suggestions", len(r["suggestions"]) > 0) - -# ---- Test 5: Autocomplete POST ---- -print("\n5. Autocomplete POST") -r = req("POST", "/web-search/autocomplete", {"q": "python progr"}) -check("success=True", r["success"] is True) -check("has suggestions", len(r["suggestions"]) > 0) - -# ---- Test 6: Config ---- -print("\n6. Config") -r = req("GET", "/web-search/config") -check("success=True", r["success"] is True) -check("has engines list", isinstance(r["engines"], list)) - -# ---- Test 7: Engine descriptions ---- -print("\n7. Engine descriptions") -r = req("GET", "/web-search/engine-descriptions") -check("success=True", r["success"] is True) -check("has engines dict", isinstance(r["engines"], dict)) -check("many engines", len(r["engines"]) > 10) - -# ---- Test 8: Stats ---- -print("\n8. Stats") -r = req("GET", "/web-search/stats") -check("success=True", r["success"] is True) - -# ---- Summary ---- -print(f"\n{'='*50}") -print(f"RESULTS: {passed} passed, {failed} failed out of {passed+failed} tests") -if failed == 0: - print("ALL INTEGRATION TESTS PASSED") -else: - print("SOME TESTS FAILED") - -sys.exit(0 if failed == 0 else 1) diff --git a/tests/test_webhook_socket.py b/tests/test_webhook_socket.py deleted file mode 100644 index 5b512a1aea4ed2f6c9a1db0dc4a1b126d6e97284..0000000000000000000000000000000000000000 --- a/tests/test_webhook_socket.py +++ /dev/null @@ -1,446 +0,0 @@ -from __future__ import annotations - -import asyncio -import hashlib -import hmac -import json -import os -import sys -import time -from typing import AsyncGenerator - -import pytest - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) - -from app.services.webhook_socket_service import ChannelManager, sign_payload, verify_signature - -API_KEY = "changeme" -AUTH_HEADER = {"Authorization": f"Bearer {API_KEY}"} -BASE = "http://localhost:7860/api/v1" - - -class TestChannelManager: - def test_create_channel(self): - mgr = ChannelManager() - ch = mgr.create_channel() - assert ch.channel_id is not None - assert len(ch.channel_id) == 16 - assert ch.buffer_size == 0 - assert ch.secret is None - - def test_create_channel_custom_id(self): - mgr = ChannelManager() - ch = mgr.create_channel(channel_id="my-channel") - assert ch.channel_id == "my-channel" - - def test_create_channel_with_secret_and_buffer(self): - mgr = ChannelManager() - ch = mgr.create_channel(channel_id="secure", secret="s3cret", buffer_size=10) - assert ch.secret == "s3cret" - assert ch.buffer_size == 10 - - def test_get_channel(self): - mgr = ChannelManager() - mgr.create_channel(channel_id="abc") - assert mgr.get_channel("abc") is not None - assert mgr.get_channel("nonexistent") is None - - def test_delete_channel(self): - mgr = ChannelManager() - mgr.create_channel(channel_id="del-me") - assert mgr.delete_channel("del-me") is True - assert mgr.get_channel("del-me") is None - assert mgr.delete_channel("del-me") is False - - def test_create_duplicate_id(self): - mgr = ChannelManager() - mgr.create_channel(channel_id="dup") - ch2 = mgr.create_channel(channel_id="dup") - assert mgr.get_channel("dup") is ch2 - - def test_default_buffer_from_manager(self): - mgr = ChannelManager(default_buffer=25) - ch = mgr.create_channel() - assert ch.buffer_size == 25 - - def test_publish_nonexistent_channel(self): - mgr = ChannelManager() - result = asyncio.run(mgr.publish("no-such-channel", {"hello": "world"})) - assert result == -1 - - @pytest.mark.asyncio - async def test_publish_and_buffer(self): - mgr = ChannelManager() - ch = mgr.create_channel(channel_id="buf-test", buffer_size=3) - - await mgr.publish("buf-test", {"n": 1}) - await mgr.publish("buf-test", {"n": 2}) - await mgr.publish("buf-test", {"n": 3}) - await mgr.publish("buf-test", {"n": 4}) - - assert len(ch.history) == 3 - assert ch.history[0]["payload"]["n"] == 2 - assert ch.history[2]["payload"]["n"] == 4 - assert ch.message_count == 4 - - def test_stats(self): - mgr = ChannelManager() - mgr.create_channel(channel_id="a") - mgr.create_channel(channel_id="b") - stats = mgr.stats() - assert stats["channels"] == 2 - assert stats["total_subscribers"] == 0 - - def test_sign_and_verify(self): - secret = "my-secret" - body = b'{"hello":"world"}' - sig = sign_payload(secret, body) - assert sig.startswith("sha256=") - assert verify_signature(secret, body, sig) is True - assert verify_signature(secret, body, "sha256=bad") is False - assert verify_signature("wrong-secret", body, sig) is False - - def test_sign_constant_result(self): - secret = "test" - body = b"data" - sig1 = sign_payload(secret, body) - sig2 = sign_payload(secret, body) - assert sig1 == sig2 - - def test_channel_info_fields(self): - mgr = ChannelManager() - mgr.create_channel(channel_id="info-test", buffer_size=5) - ch = mgr.get_channel("info-test") - assert ch.channel_id == "info-test" - assert ch.buffer_size == 5 - assert ch.message_count == 0 - assert ch.created_at > 0 - assert ch.last_activity > 0 - - def test_publish_with_subscriber(self): - mgr = ChannelManager() - mgr.create_channel(channel_id="sub-test") - ch = mgr.get_channel("sub-test") - - async def dummy(): - return ch.channel_id - q = asyncio.Queue() - ch.subscribers[dummy] = q - - result = asyncio.run(mgr.publish("sub-test", {"msg": "hello"})) - assert result == 1 - assert ch.message_count == 1 - - def test_multiple_publishes(self): - mgr = ChannelManager() - ch = mgr.create_channel(channel_id="multi-pub", buffer_size=10) - for i in range(5): - asyncio.run(mgr.publish("multi-pub", {"n": i})) - assert ch.message_count == 5 - assert len(ch.history) == 5 - - def test_channel_manager_defaults(self): - mgr = ChannelManager() - assert mgr.default_buffer == 0 - assert mgr.channels == {} - - -pytestmark_integration = pytest.mark.skipif( - not os.environ.get("RUN_INTEGRATION_TESTS"), - reason="Set RUN_INTEGRATION_TESTS=1 to run integration tests", -) - - -def _sign(body: bytes, secret: str) -> str: - return "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() - - -@pytest.mark.skipif( - not os.environ.get("RUN_INTEGRATION_TESTS"), - reason="Set RUN_INTEGRATION_TESTS=1 to run integration tests", -) -class TestWebhookSocketIntegration: - @pytest.fixture(autouse=True) - async def _setup(self): - import httpx - - async with httpx.AsyncClient(base_url=BASE) as client: - self.client = client - yield - - async def _create_channel(self, **kwargs) -> dict: - resp = await self.client.post("/channels", json=kwargs, headers=AUTH_HEADER) - assert resp.status_code == 201 - return resp.json() - - async def _delete_channel(self, channel_id: str): - resp = await self.client.delete(f"/channels/{channel_id}", headers=AUTH_HEADER) - return resp.status_code == 200 - - async def test_health(self): - resp = await self.client.get("/health") - assert resp.status_code == 200 - data = resp.json() - assert data["success"] is True - - async def test_create_and_list_channels(self): - ch = await self._create_channel(channel_id="test-list", buffer_size=5) - assert ch["channel_id"] == "test-list" - - resp = await self.client.get("/channels", headers=AUTH_HEADER) - assert resp.status_code == 200 - data = resp.json() - assert any(c["channel_id"] == "test-list" for c in data["channels"]) - - await self._delete_channel("test-list") - - async def test_create_channel_no_auth(self): - resp = await self.client.post("/channels", json={}) - assert resp.status_code in (401, 403) - - async def test_create_duplicate_channel(self): - await self._create_channel(channel_id="dup-test") - resp = await self.client.post("/channels", json={"channel_id": "dup-test"}, headers=AUTH_HEADER) - assert resp.status_code == 409 - await self._delete_channel("dup-test") - - async def test_channel_info(self): - await self._create_channel(channel_id="info-test") - resp = await self.client.get("/channels/info-test", headers=AUTH_HEADER) - assert resp.status_code == 200 - data = resp.json() - assert data["channel_id"] == "info-test" - assert data["subscribers"] == 0 - assert data["messages"] == 0 - await self._delete_channel("info-test") - - async def test_channel_info_not_found(self): - resp = await self.client.get("/channels/no-such", headers=AUTH_HEADER) - assert resp.status_code == 404 - - async def test_delete_channel(self): - await self._create_channel(channel_id="delete-me") - resp = await self.client.delete("/channels/delete-me", headers=AUTH_HEADER) - assert resp.status_code == 200 - assert resp.json()["deleted"] == "delete-me" - - resp = await self.client.get("/channels/delete-me", headers=AUTH_HEADER) - assert resp.status_code == 404 - - async def test_delete_channel_not_found(self): - resp = await self.client.delete("/channels/no-such", headers=AUTH_HEADER) - assert resp.status_code == 404 - - async def test_webhook_delivers_to_ws(self): - ch = await self._create_channel(channel_id="ws-deliver", buffer_size=5) - cid = ch["channel_id"] - - import websockets - - ws_url = f"ws://localhost:7860/api/v1/ws/{cid}" - - async with websockets.connect(ws_url) as ws: - connected = json.loads(await ws.recv()) - assert connected["event"] == "connected" - assert connected["channel"] == cid - - payload = {"msg": "hello from webhook", "num": 42} - resp = await self.client.post(f"/webhook/{cid}", json=payload) - assert resp.status_code == 200 - wh_data = resp.json() - assert wh_data["status"] == "delivered" - assert wh_data["subscribers_notified"] == 1 - - received = json.loads(await ws.recv()) - assert received["event"] == "message" - assert received["channel"] == cid - assert received["payload"] == payload - - await self._delete_channel(cid) - - async def test_webhook_no_subscribers(self): - ch = await self._create_channel(channel_id="no-subs") - resp = await self.client.post(f"/webhook/{ch['channel_id']}", json={"data": 1}) - assert resp.status_code == 200 - assert resp.json()["subscribers_notified"] == 0 - await self._delete_channel(ch["channel_id"]) - - async def test_webhook_not_found(self): - resp = await self.client.post("/webhook/no-such", json={"x": 1}) - assert resp.status_code == 404 - - async def test_hook_alias(self): - ch = await self._create_channel(channel_id="hook-alias") - resp = await self.client.post(f"/hook/{ch['channel_id']}", json={"test": True}) - assert resp.status_code == 200 - assert resp.json()["status"] == "delivered" - await self._delete_channel(ch["channel_id"]) - - async def test_hmac_signed_webhook(self): - secret = "hmac-test-secret" - ch = await self._create_channel(channel_id="hmac-test", secret=secret) - cid = ch["channel_id"] - - payload = b'{"signed": "data"}' - sig = _sign(payload, secret) - - resp = await self.client.post( - f"/webhook/{cid}", - content=payload, - headers={"Content-Type": "application/json", "X-Signature-256": sig}, - ) - assert resp.status_code == 200, resp.text - - resp_bad = await self.client.post( - f"/webhook/{cid}", - content=payload, - headers={"Content-Type": "application/json", "X-Signature-256": "sha256=bad"}, - ) - assert resp_bad.status_code == 401 - - resp_no_sig = await self.client.post( - f"/webhook/{cid}", - content=payload, - headers={"Content-Type": "application/json"}, - ) - assert resp_no_sig.status_code == 401 - - await self._delete_channel(cid) - - async def test_webhook_form_urlencoded(self): - ch = await self._create_channel(channel_id="form-test") - cid = ch["channel_id"] - - resp = await self.client.post( - f"/webhook/{cid}", - data={"field1": "value1", "field2": "value2"}, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) - assert resp.status_code == 200 - assert resp.json()["status"] == "delivered" - - await self._delete_channel(cid) - - async def test_webhook_raw_text(self): - ch = await self._create_channel(channel_id="raw-test") - cid = ch["channel_id"] - - resp = await self.client.post( - f"/webhook/{cid}", - content="just some raw text", - headers={"Content-Type": "text/plain"}, - ) - assert resp.status_code == 200 - - await self._delete_channel(cid) - - async def test_ws_replay_buffer(self): - ch = await self._create_channel(channel_id="replay-test", buffer_size=3) - cid = ch["channel_id"] - - await self.client.post(f"/webhook/{cid}", json={"n": 1}) - await self.client.post(f"/webhook/{cid}", json={"n": 2}) - await self.client.post(f"/webhook/{cid}", json={"n": 3}) - - import websockets - - ws_url = f"ws://localhost:7860/api/v1/ws/{cid}" - async with websockets.connect(ws_url) as ws: - connected = json.loads(await ws.recv()) - assert connected["event"] == "connected" - assert connected["buffered"] == 3 - - for expected_n in [1, 2, 3]: - msg = json.loads(await ws.recv()) - assert msg["payload"]["n"] == expected_n - - await self._delete_channel(cid) - - async def test_ws_auth_with_secret(self): - ch = await self._create_channel(channel_id="ws-auth-test", secret="topsecret") - cid = ch["channel_id"] - - import websockets - - ws_url = f"ws://localhost:7860/api/v1/ws/{cid}" - - async with websockets.connect(f"{ws_url}?secret=topsecret") as ws: - msg = json.loads(await ws.recv()) - assert msg["event"] == "connected" - - async with websockets.connect(ws_url) as ws: - msg = json.loads(await ws.recv()) - assert msg["event"] == "error" - - await self._delete_channel(cid) - - async def test_ws_channel_not_found(self): - import websockets - - async with websockets.connect("ws://localhost:7860/api/v1/ws/does-not-exist") as ws: - msg = json.loads(await ws.recv()) - assert msg["event"] == "error" - assert "channel not found" in msg["message"] - - async def test_stats_endpoint(self): - resp = await self.client.get("/webhook-socket/stats", headers=AUTH_HEADER) - assert resp.status_code == 200 - data = resp.json() - assert "channels" in data - assert "total_messages" in data - assert "total_subscribers" in data - - async def test_full_lifecycle(self): - cid = "lifecycle-test" - - ch = await self._create_channel(channel_id=cid, buffer_size=10, secret="life-secret") - assert ch["channel_id"] == cid - - info_resp = await self.client.get(f"/channels/{cid}", headers=AUTH_HEADER) - assert info_resp.status_code == 200 - assert info_resp.json()["messages"] == 0 - - import websockets - - ws_url = f"ws://localhost:7860/api/v1/ws/{cid}?secret=life-secret" - async with websockets.connect(ws_url) as ws: - connected = json.loads(await ws.recv()) - assert connected["event"] == "connected" - - payload = {"event_type": "push", "data": {"ref": "main"}} - sig = _sign(json.dumps(payload).encode(), "life-secret") - resp = await self.client.post( - f"/webhook/{cid}", - json=payload, - headers={"X-Signature-256": sig, "X-GitHub-Event": "push"}, - ) - assert resp.status_code == 200 - wh = resp.json() - assert wh["subscribers_notified"] == 1 - - received = json.loads(await ws.recv()) - assert received["event"] == "message" - assert received["payload"]["event_type"] == "push" - assert received["headers"]["X-GitHub-Event"] == "push" - - stats_resp = await self.client.get(f"/channels/{cid}", headers=AUTH_HEADER) - assert stats_resp.json()["messages"] == 1 - - await self._delete_channel(cid) - - not_found = await self.client.get(f"/channels/{cid}", headers=AUTH_HEADER) - assert not_found.status_code == 404 - - -if __name__ == "__main__": - import subprocess - import sys as _sys - - os.environ["RUN_INTEGRATION_TESTS"] = "1" - _sys.exit( - subprocess.run( - [_sys.executable, "-m", "pytest", __file__, "-v", "--tb=short"], - cwd=os.path.join(os.path.dirname(__file__), ".."), - ).returncode - ) diff --git a/whatsapp-service/.gitignore b/whatsapp-service/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..1e74ac47c684b94c120b3c30e763e5c608beec3e --- /dev/null +++ b/whatsapp-service/.gitignore @@ -0,0 +1,20 @@ +.env +logs/* +build/ +*.prof +coverage.* +.air.toml +.idea/ +.vscode/ +.DS_Store +evolution-go +agentdeck-whatsapp-service.exe +build/ + +# Local dev/test/doc artifacts (kept out of the production repo) +tests/ +ddl/ +*.md +*_test.go +__pycache__/ +*.pyc diff --git a/whatsapp-service/LICENSE b/whatsapp-service/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..a407bec70694d58d6bd457265004984789a73cae --- /dev/null +++ b/whatsapp-service/LICENSE @@ -0,0 +1,13 @@ +Copyright 2026 AgentDeck + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/whatsapp-service/Makefile b/whatsapp-service/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..6ad378b2b7c082d456b522495d433fb4b91d3485 --- /dev/null +++ b/whatsapp-service/Makefile @@ -0,0 +1,268 @@ +.PHONY: help dev run build test clean swagger deps docker-build docker-run install setup migrate-up migrate-down logs + +# Configurações +APP_NAME=agentdeck-whatsapp +MAIN_PATH=cmd/agentdeck-whatsapp-service/main.go +BUILD_DIR=build +GO=go +VERSION=$(shell cat VERSION 2>/dev/null || echo "0.7.2") +LDFLAGS=-ldflags "-X main.version=$(VERSION)" +GOFLAGS=-v + +# Cores para output +GREEN=\033[0;32m +YELLOW=\033[0;33m +RED=\033[0;31m +NC=\033[0m # No Color + +##@ Ajuda + +help: ## Exibe esta mensagem de ajuda + @echo "$(GREEN)AgentDeck Whatsapp Service - Makefile$(NC)" + @echo "" + @awk 'BEGIN {FS = ":.*##"; printf "\nUso:\n make $(YELLOW)$(NC)\n"} /^[a-zA-Z_-]+:.*?##/ { printf " $(GREEN)%-15s$(NC) %s\n", $$1, $$2 } /^##@/ { printf "\n$(YELLOW)%s$(NC)\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Desenvolvimento + +dev: ## Roda a aplicação em modo desenvolvimento + @echo "$(GREEN)🚀 Rodando AgentDeck Whatsapp Service em modo desenvolvimento...$(NC)" + $(GO) run $(LDFLAGS) $(MAIN_PATH) -dev + +run: ## Roda a aplicação em modo produção + @echo "$(GREEN)🚀 Rodando AgentDeck Whatsapp Service...$(NC)" + $(GO) run $(MAIN_PATH) + +watch: ## Roda a aplicação com hot reload (requer air) + @if command -v air > /dev/null; then \ + echo "$(GREEN)🔥 Rodando com hot reload...$(NC)"; \ + air; \ + else \ + echo "$(RED)❌ Air não instalado. Instale com: go install github.com/cosmtrek/air@latest$(NC)"; \ + exit 1; \ + fi + +##@ Build + +build: ## Compila a aplicação + @echo "$(GREEN)🔨 Compilando $(APP_NAME)...$(NC)" + @mkdir -p $(BUILD_DIR) + $(GO) build $(GOFLAGS) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_PATH) + @echo "$(GREEN)✅ Build completo: $(BUILD_DIR)/$(APP_NAME)$(NC)" + +build-linux: ## Compila para Linux + @echo "$(GREEN)🔨 Compilando para Linux...$(NC)" + @mkdir -p $(BUILD_DIR) + GOOS=linux GOARCH=amd64 $(GO) build $(GOFLAGS) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 $(MAIN_PATH) + @echo "$(GREEN)✅ Build Linux completo$(NC)" + +build-windows: ## Compila para Windows + @echo "$(GREEN)🔨 Compilando para Windows...$(NC)" + @mkdir -p $(BUILD_DIR) + GOOS=windows GOARCH=amd64 $(GO) build $(GOFLAGS) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-windows-amd64.exe $(MAIN_PATH) + @echo "$(GREEN)✅ Build Windows completo$(NC)" + +build-all: build build-linux build-windows ## Compila para todas as plataformas + @echo "$(GREEN)✅ Todos os builds completos$(NC)" + +install: build ## Compila e instala no GOPATH + @echo "$(GREEN)📦 Instalando $(APP_NAME)...$(NC)" + $(GO) install $(MAIN_PATH) + @echo "$(GREEN)✅ Instalado com sucesso$(NC)" + +##@ Testes + +test: ## Roda todos os testes + @echo "$(GREEN)🧪 Rodando testes...$(NC)" + $(GO) test -v ./... + +test-coverage: ## Roda testes com cobertura + @echo "$(GREEN)🧪 Rodando testes com cobertura...$(NC)" + $(GO) test -v -coverprofile=coverage.out ./... + $(GO) tool cover -html=coverage.out -o coverage.html + @echo "$(GREEN)✅ Cobertura gerada: coverage.html$(NC)" + +test-race: ## Roda testes verificando race conditions + @echo "$(GREEN)🧪 Rodando testes com race detector...$(NC)" + $(GO) test -race -v ./... + +bench: ## Roda benchmarks + @echo "$(GREEN)⚡ Rodando benchmarks...$(NC)" + $(GO) test -bench=. -benchmem ./... + +##@ Dependências + +deps: ## Instala dependências + @echo "$(GREEN)📦 Instalando dependências...$(NC)" + $(GO) mod download + $(GO) mod verify + @echo "$(GREEN)✅ Dependências instaladas$(NC)" + +deps-update: ## Atualiza dependências + @echo "$(GREEN)📦 Atualizando dependências...$(NC)" + $(GO) get -u ./... + $(GO) mod tidy + @echo "$(GREEN)✅ Dependências atualizadas$(NC)" + +deps-clean: ## Limpa dependências não utilizadas + @echo "$(GREEN)🧹 Limpando dependências...$(NC)" + $(GO) mod tidy + @echo "$(GREEN)✅ Dependências limpas$(NC)" + +deps-reset: ## Limpa cache e reinstala dependências (força uso do código local) + @echo "$(GREEN)🔄 Resetando dependências e cache...$(NC)" + @echo "$(YELLOW)Limpeza de cache e módulos...$(NC)" + $(GO) clean -cache -modcache -i -r + @echo "$(YELLOW)Download de módulos...$(NC)" + $(GO) mod download + @echo "$(YELLOW)Organizando módulos...$(NC)" + $(GO) mod tidy + @echo "$(GREEN)✅ Dependências resetadas e atualizadas$(NC)" + +##@ Documentação + +swagger: ## Gera documentação Swagger + @echo "$(GREEN)📚 Gerando documentação Swagger...$(NC)" + @if command -v swag > /dev/null; then \ + swag init -g $(MAIN_PATH) -o ./docs; \ + echo "$(GREEN)✅ Swagger gerado com sucesso$(NC)"; \ + else \ + echo "$(RED)❌ Swag não instalado. Instale com: go install github.com/swaggo/swag/cmd/swag@latest$(NC)"; \ + exit 1; \ + fi + +docs: ## Abre a documentação local + @echo "$(GREEN)📖 Abrindo documentação...$(NC)" + @if [ -f "docs/wiki/README.md" ]; then \ + echo "Documentação disponível em: docs/wiki/README.md"; \ + else \ + echo "$(RED)❌ Documentação não encontrada$(NC)"; \ + fi + +##@ Database + +migrate-up: ## Executa migrations do banco de dados + @echo "$(GREEN)🗃️ Executando migrations...$(NC)" + @if [ -d "migrations" ]; then \ + $(GO) run $(MAIN_PATH) migrate up; \ + else \ + echo "$(YELLOW)⚠️ Diretório migrations não encontrado$(NC)"; \ + fi + +migrate-down: ## Reverte migrations do banco de dados + @echo "$(YELLOW)⚠️ Revertendo migrations...$(NC)" + @if [ -d "migrations" ]; then \ + $(GO) run $(MAIN_PATH) migrate down; \ + else \ + echo "$(YELLOW)⚠️ Diretório migrations não encontrado$(NC)"; \ + fi + +##@ Docker + +docker-build: ## Build da imagem Docker + @echo "$(GREEN)🐳 Construindo imagem Docker...$(NC)" + docker build --build-arg VERSION=$(VERSION) -t $(APP_NAME):latest . + @echo "$(GREEN)✅ Imagem Docker construída$(NC)" + +docker-run: ## Roda container Docker + @echo "$(GREEN)🐳 Iniciando container...$(NC)" + docker run -p 4000:4000 --env-file .env $(APP_NAME):latest + +docker-compose-up: ## Sobe todos os serviços com docker-compose + @echo "$(GREEN)🐳 Iniciando serviços com docker-compose...$(NC)" + docker-compose up -d + +docker-compose-down: ## Para todos os serviços do docker-compose + @echo "$(YELLOW)🐳 Parando serviços...$(NC)" + docker-compose down + +docker-compose-logs: ## Exibe logs do docker-compose + docker-compose logs -f + +##@ Linting e Formatação + +fmt: ## Formata o código + @echo "$(GREEN)✨ Formatando código...$(NC)" + $(GO) fmt ./... + @echo "$(GREEN)✅ Código formatado$(NC)" + +lint: ## Executa linter (requer golangci-lint) + @echo "$(GREEN)🔍 Executando linter...$(NC)" + @if command -v golangci-lint > /dev/null; then \ + golangci-lint run ./...; \ + echo "$(GREEN)✅ Lint completo$(NC)"; \ + else \ + echo "$(RED)❌ golangci-lint não instalado. Instale com: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest$(NC)"; \ + exit 1; \ + fi + +vet: ## Executa go vet + @echo "$(GREEN)🔍 Executando go vet...$(NC)" + $(GO) vet ./... + @echo "$(GREEN)✅ Vet completo$(NC)" + +check: fmt vet lint test ## Executa todas as verificações + +##@ Limpeza + +clean: ## Remove arquivos de build + @echo "$(YELLOW)🧹 Limpando arquivos de build...$(NC)" + @rm -rf $(BUILD_DIR) + @rm -f coverage.out coverage.html + @echo "$(GREEN)✅ Limpeza completa$(NC)" + +clean-all: clean ## Remove arquivos de build e cache + @echo "$(YELLOW)🧹 Limpeza completa (incluindo cache)...$(NC)" + $(GO) clean -cache -testcache -modcache + @echo "$(GREEN)✅ Limpeza completa$(NC)" + +##@ Utilitários + +setup: deps swagger ## Setup completo do ambiente de desenvolvimento + @echo "$(GREEN)🎉 Setup completo!$(NC)" + @echo "" + @echo "Para começar a desenvolver, rode:" + @echo " $(YELLOW)make dev$(NC)" + @echo "" + @echo "Outros comandos úteis:" + @echo " $(YELLOW)make help$(NC) - Ver todos os comandos" + @echo " $(YELLOW)make test$(NC) - Rodar testes" + @echo " $(YELLOW)make build$(NC) - Compilar a aplicação" + +logs: ## Exibe logs da aplicação (se estiver rodando) + @echo "$(GREEN)📋 Exibindo logs...$(NC)" + @if [ -f "logs/app.log" ]; then \ + tail -f logs/app.log; \ + else \ + echo "$(YELLOW)⚠️ Arquivo de log não encontrado$(NC)"; \ + fi + +version: ## Exibe versão do Go e dependências + @echo "$(GREEN)📌 Versões:$(NC)" + @$(GO) version + @echo "" + @echo "$(GREEN)Dependências principais:$(NC)" + @$(GO) list -m all | grep -E '(whatsmeow|postgres|minio)' + +status: ## Verifica status da aplicação + @echo "$(GREEN)🔍 Verificando status...$(NC)" + @curl -s http://localhost:4000/health || echo "$(RED)❌ Aplicação não está rodando$(NC)" + +##@ Desenvolvimento Avançado + +profile-cpu: ## Profile de CPU (requer aplicação rodando) + @echo "$(GREEN)📊 Capturando profile de CPU...$(NC)" + curl http://localhost:4000/debug/pprof/profile?seconds=30 > cpu.prof + $(GO) tool pprof -http=:8080 cpu.prof + +profile-mem: ## Profile de memória (requer aplicação rodando) + @echo "$(GREEN)📊 Capturando profile de memória...$(NC)" + curl http://localhost:4000/debug/pprof/heap > mem.prof + $(GO) tool pprof -http=:8080 mem.prof + +generate: ## Roda go generate + @echo "$(GREEN)⚙️ Executando go generate...$(NC)" + $(GO) generate ./... + +mod-graph: ## Exibe gráfico de dependências + @echo "$(GREEN)📊 Gráfico de dependências:$(NC)" + $(GO) mod graph diff --git a/whatsapp-service/NOTICE b/whatsapp-service/NOTICE new file mode 100644 index 0000000000000000000000000000000000000000..29ace80de1c186e97575bb1fb522618e89b35509 --- /dev/null +++ b/whatsapp-service/NOTICE @@ -0,0 +1,7 @@ +AgentDeck Whatsapp Service +Copyright 2026 AgentDeck + +Third-party attributions: + +- whatsmeow (https://github.com/tulir/whatsmeow) by Tulir Asokan, used as the + WhatsApp protocol library. diff --git a/whatsapp-service/VERSION b/whatsapp-service/VERSION new file mode 100644 index 0000000000000000000000000000000000000000..7486fdbc50b3f93d9d04db440483984df5d1dcce --- /dev/null +++ b/whatsapp-service/VERSION @@ -0,0 +1 @@ +0.7.2 diff --git a/whatsapp-service/cmd/agentdeck-whatsapp-service/main.go b/whatsapp-service/cmd/agentdeck-whatsapp-service/main.go new file mode 100644 index 0000000000000000000000000000000000000000..d1bf7b6dce6ce4b9fe5332b0115c319c555938c7 --- /dev/null +++ b/whatsapp-service/cmd/agentdeck-whatsapp-service/main.go @@ -0,0 +1,397 @@ +package main + +import ( + "context" + "database/sql" + "flag" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/gin-gonic/gin" + "github.com/gomessguii/logger" + "github.com/joho/godotenv" + "github.com/redis/go-redis/v9" + "go.mau.fi/whatsmeow" + + call_handler "agentdeck-whatsapp-service/pkg/call/handler" + call_service "agentdeck-whatsapp-service/pkg/call/service" + chat_handler "agentdeck-whatsapp-service/pkg/chat/handler" + chat_service "agentdeck-whatsapp-service/pkg/chat/service" + community_handler "agentdeck-whatsapp-service/pkg/community/handler" + community_service "agentdeck-whatsapp-service/pkg/community/service" + config "agentdeck-whatsapp-service/pkg/config" + "agentdeck-whatsapp-service/pkg/core" + producer_interfaces "agentdeck-whatsapp-service/pkg/events/interfaces" + nats_producer "agentdeck-whatsapp-service/pkg/events/nats" + rabbitmq_producer "agentdeck-whatsapp-service/pkg/events/rabbitmq" + webhook_producer "agentdeck-whatsapp-service/pkg/events/webhook" + websocket_producer "agentdeck-whatsapp-service/pkg/events/websocket" + group_handler "agentdeck-whatsapp-service/pkg/group/handler" + group_service "agentdeck-whatsapp-service/pkg/group/service" + instance_handler "agentdeck-whatsapp-service/pkg/instance/handler" + instance_repository "agentdeck-whatsapp-service/pkg/instance/repository" + instance_service "agentdeck-whatsapp-service/pkg/instance/service" + label_handler "agentdeck-whatsapp-service/pkg/label/handler" + label_repository "agentdeck-whatsapp-service/pkg/label/repository" + label_service "agentdeck-whatsapp-service/pkg/label/service" + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + message_handler "agentdeck-whatsapp-service/pkg/message/handler" + message_repository "agentdeck-whatsapp-service/pkg/message/repository" + message_service "agentdeck-whatsapp-service/pkg/message/service" + auth_middleware "agentdeck-whatsapp-service/pkg/middleware" + newsletter_handler "agentdeck-whatsapp-service/pkg/newsletter/handler" + newsletter_service "agentdeck-whatsapp-service/pkg/newsletter/service" + passkey_handler "agentdeck-whatsapp-service/pkg/passkey/handler" + poll_handler "agentdeck-whatsapp-service/pkg/poll/handler" + routes "agentdeck-whatsapp-service/pkg/routes" + send_handler "agentdeck-whatsapp-service/pkg/sendMessage/handler" + send_service "agentdeck-whatsapp-service/pkg/sendMessage/service" + server_handler "agentdeck-whatsapp-service/pkg/server/handler" + storage_interfaces "agentdeck-whatsapp-service/pkg/storage/interfaces" + minio_storage "agentdeck-whatsapp-service/pkg/storage/minio" + "agentdeck-whatsapp-service/pkg/supabase" + user_handler "agentdeck-whatsapp-service/pkg/user/handler" + user_service "agentdeck-whatsapp-service/pkg/user/service" + whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service" + amqp "github.com/rabbitmq/amqp091-go" +) + +var devMode = flag.Bool("dev", false, "Enable development mode") + +var version = "0.0.0" + +func init() { + // ldflags -X main.version= sets this at compile time. + // If not set (or still default), try reading from VERSION file. + if version == "0.0.0" { + if v, err := os.ReadFile("VERSION"); err == nil { + if trimmed := strings.TrimSpace(string(v)); trimmed != "" { + version = trimmed + } + } + } +} + +func setupRouter(supa *supabase.Client, authDB *sql.DB, redisClient *redis.Client, config *config.Config, conn *amqp.Connection, runtimeCtx *core.RuntimeContext) *gin.Engine { + killChannel := make(map[string](chan bool)) + clientPointer := make(map[string]*whatsmeow.Client) + + loggerWrapper := logger_wrapper.NewLoggerManager(config) + + var rabbitmqProducer producer_interfaces.Producer + if conn != nil { + logger.LogInfo("RabbitMQ enabled") + rabbitmqProducer = rabbitmq_producer.NewRabbitMQProducer( + conn, + config.AmqpGlobalEnabled, + config.AmqpGlobalEvents, + config.AmqpSpecificEvents, + config.AmqpUrl, + loggerWrapper, + ) + } else { + // Even if initial connection failed, pass the URL so reconnection can work + rabbitmqProducer = rabbitmq_producer.NewRabbitMQProducer( + nil, + config.AmqpGlobalEnabled, + config.AmqpGlobalEvents, + config.AmqpSpecificEvents, + config.AmqpUrl, // Keep the URL for reconnection attempts + loggerWrapper, + ) + } + + var natsProducer producer_interfaces.Producer + if config.NatsUrl != "" { + logger.LogInfo("NATS enabled") + natsProducer = nats_producer.NewNatsProducer( + config.NatsUrl, + config.NatsGlobalEnabled, + config.NatsGlobalEvents, + loggerWrapper, + ) + } else { + natsProducer = nats_producer.NewNatsProducer( + "", + false, + nil, + loggerWrapper, + ) + } + + webhookProducer := webhook_producer.NewWebhookProducer(config.WebhookUrl, loggerWrapper) + websocketProducer := websocket_producer.NewWebsocketProducer(loggerWrapper) + + // Cria filas globais se o RabbitMQ global estiver habilitado + if config.AmqpGlobalEnabled && conn != nil { + logger.LogInfo("Creating global RabbitMQ queues...") + if err := rabbitmqProducer.CreateGlobalQueues(); err != nil { + logger.LogError("Failed to create global RabbitMQ queues: %v", err) + } else { + logger.LogInfo("Global RabbitMQ queues created successfully") + } + } + + var mediaStorage storage_interfaces.MediaStorage + var err error + if config.MinioEnabled { + mediaStorage, err = minio_storage.NewMinioMediaStorage( + config.MinioEndpoint, + config.MinioAccessKey, + config.MinioSecretKey, + config.MinioBucket, + config.MinioRegion, + config.MinioUseSSL, + ) + if err != nil { + log.Fatal(err) + } + } + + instanceRepository := instance_repository.NewInstanceRepository(supa) + messageRepository := message_repository.NewMessageRepository(supa) + labelRepository := label_repository.NewLabelRepository(supa) + + whatsmeowService := whatsmeow_service.NewWhatsmeowService( + instanceRepository, + authDB, + supa, + messageRepository, + labelRepository, + config, + killChannel, + clientPointer, + rabbitmqProducer, + webhookProducer, + websocketProducer, + redisClient, + mediaStorage, + natsProducer, + loggerWrapper, + ) + instanceService := instance_service.NewInstanceService( + instanceRepository, + killChannel, + clientPointer, + whatsmeowService, + config, + loggerWrapper, + ) + sendMessageService := send_service.NewSendService(clientPointer, whatsmeowService, config, loggerWrapper) + userService := user_service.NewUserService(clientPointer, whatsmeowService, loggerWrapper) + messageService := message_service.NewMessageService(clientPointer, messageRepository, whatsmeowService, loggerWrapper) + chatService := chat_service.NewChatService(clientPointer, whatsmeowService, loggerWrapper) + groupService := group_service.NewGroupService(clientPointer, whatsmeowService, loggerWrapper) + callService := call_service.NewCallService(clientPointer, whatsmeowService, loggerWrapper) + communityService := community_service.NewCommunityService(clientPointer, whatsmeowService, loggerWrapper) + labelService := label_service.NewLabelService(clientPointer, whatsmeowService, labelRepository, loggerWrapper) + newsletterService := newsletter_service.NewNewsletterService(clientPointer, whatsmeowService, loggerWrapper) + + // NOVO: PollHandler usando PollService já inicializado no whatsmeowService (evita dupla inicialização) + pollHandler := poll_handler.NewPollHandler(whatsmeowService.GetPollService(), loggerWrapper) + + r := gin.Default() + + // CORS middleware — must be before everything else + r.Use(func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") + 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") + c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length") + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(200) + return + } + c.Next() + }) + + r.Use(core.GateMiddleware(runtimeCtx)) + + // License routes (always accessible, even without license) + core.LicenseRoutes(r, runtimeCtx) + + // Passkey ceremony routes — PUBLIC (called by the browser extension from the + // web.whatsapp.com origin, gated only by an opaque ephemeral token). + passkey_handler.RegisterRoutes(r, whatsmeowService) + + routes.NewRouter( + auth_middleware.NewMiddleware(config, instanceService), + instance_handler.NewInstanceHandler(instanceService, config), + user_handler.NewUserHandler(userService), + send_handler.NewSendHandler(sendMessageService), + message_handler.NewMessageHandler(messageService), + chat_handler.NewChatHandler(chatService), + group_handler.NewGroupHandler(groupService), + call_handler.NewCallHandler(callService), + community_handler.NewCommunityHandler(communityService), + label_handler.NewLabelHandler(labelService), + newsletter_handler.NewNewsletterHandler(newsletterService), + pollHandler, + server_handler.NewServerHandler(), + ).AssignRoutes(r) + + if config.ConnectOnStartup { + go whatsmeowService.ConnectOnStartup(config.ClientName) + } + + r.GET("/ws", func(c *gin.Context) { + token := c.Query("token") + instanceId := c.Query("instanceId") + + if token != config.GlobalApiKey { + logger.LogError("Token inválido: %s", token) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Token inválido"}) + return + } + + websocket_producer.ServeWs(c.Writer, c.Request, instanceId, websocketProducer) + }) + + return r +} + +// initRedis connects to Redis using the full URL (REDIS_URL). It is used for +// the userInfoCache and processedMessages deduplication cache. +func initRedis(redisURL string) (*redis.Client, error) { + if redisURL == "" { + return nil, fmt.Errorf("REDIS_URL is required for caching") + } + opts, err := redis.ParseURL(redisURL) + if err != nil { + return nil, fmt.Errorf("invalid REDIS_URL: %v", err) + } + client := redis.NewClient(opts) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := client.Ping(ctx).Err(); err != nil { + return nil, fmt.Errorf("failed to ping Redis: %v", err) + } + logger.LogInfo("Connected to Redis (REDIS_URL)") + return client, nil +} + +// @title AgentDeck Whatsapp Service +// @version 1.0 +// @description AgentDeck Whatsapp Service - whatsmeow +func main() { + flag.Parse() + // Configuration is injected through the process environment (the main + // AgentDeck backend is the single source of truth and forwards the + // WhatsApp settings to this service). A local `.env` is only loaded when + // `--dev` is passed AND the file exists — it is never required. + if *devMode { + if _, err := os.Stat(".env"); err == nil { + if err := godotenv.Load(".env"); err != nil { + log.Fatalf("failed to load .env: %v", err) + } + } + } + + cfg := config.Load() + + logger.LogInfo("Starting AgentDeck Whatsapp Service version %s", version) + + startTime := time.Now() + + // Supabase PostgREST client — drives all app repositories (instances, + // messages, labels, polls, runtime configs). + supa := supabase.New(cfg.SupabaseURL, cfg.SupabaseServiceKey) + + // Native Supabase Postgres — used only by the whatsmeow session store and + // the whatsmeow_device lookup (these require a Postgres driver). + authDB, err := cfg.CreateSupabaseDB() + if err != nil { + logger.LogFatal("[STARTUP] %v", err) + } + defer authDB.Close() + + // Redis — userInfoCache and processedMessages deduplication. + redisClient, err := initRedis(cfg.RedisURL) + if err != nil { + logger.LogFatal("[STARTUP] %v", err) + } + defer redisClient.Close() + + // Initialize core DB + license runtime (runtime_configs via PostgREST) + core.SetDB(supa) + if err := core.MigrateDB(); err != nil { + log.Fatal("Failed to migrate runtime_configs: ", err) + } + tier := "agentdeck-whatsapp" + runtimeCtx := core.InitializeRuntime(tier, version, cfg.GlobalApiKey) + + var conn *amqp.Connection + + if cfg.AmqpUrl != "" { + logger.LogInfo("Attempting to connect to RabbitMQ...") + + // Create connection with heartbeat to prevent timeouts + amqpConfig := amqp.Config{ + Heartbeat: 30 * time.Second, // Send heartbeat every 30 seconds + Locale: "en_US", + } + + conn, err = amqp.DialConfig(cfg.AmqpUrl, amqpConfig) + if err != nil { + logger.LogError("Failed to connect to RabbitMQ, err: %v", err) + logger.LogInfo("RabbitMQ producer will be created with reconnection capability") + } else { + logger.LogInfo("Successfully connected to RabbitMQ with heartbeat enabled") + defer func(conn *amqp.Connection) { + err := conn.Close() + if err != nil { + logger.LogError("Failed to close RabbitMQ connection, err: %v", err) + } + }(conn) + } + } else { + logger.LogInfo("RabbitMQ URL not configured, skipping RabbitMQ connection") + } + + r := setupRouter(supa, authDB, redisClient, cfg, conn, runtimeCtx) + + // Graceful shutdown with heartbeat + heartbeatCtx, heartbeatCancel := context.WithCancel(context.Background()) + defer heartbeatCancel() + + core.StartHeartbeat(heartbeatCtx, runtimeCtx, startTime) + + srv := &http.Server{ + Addr: ":" + os.Getenv("SERVER_PORT"), + Handler: r, + } + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + + go func() { + logger.LogInfo("Iniciando servidor na porta %s", os.Getenv("SERVER_PORT")) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("server error: %v", err) + } + }() + + <-quit + logger.LogInfo("[SHUTDOWN] Signal received, shutting down...") + + // Stop heartbeat loop + heartbeatCancel() + + core.Shutdown(runtimeCtx) + + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer shutdownCancel() + + if err := srv.Shutdown(shutdownCtx); err != nil { + logger.LogError("[SHUTDOWN] Server forced to shutdown: %v", err) + } + + logger.LogInfo("[SHUTDOWN] Server exited") +} diff --git a/whatsapp-service/docs/docs.go b/whatsapp-service/docs/docs.go new file mode 100644 index 0000000000000000000000000000000000000000..59f0aee6ee500cc4f8da077875b77122ea90dbac --- /dev/null +++ b/whatsapp-service/docs/docs.go @@ -0,0 +1,14329 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/call/reject": { + "post": { + "description": "Reject call", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Call" + ], + "summary": "Reject call", + "parameters": [ + { + "description": "Call data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_call_service.RejectCallStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/chat/archive": { + "post": { + "description": "Archive a chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chat" + ], + "summary": "Archive a chat", + "parameters": [ + { + "description": "Chat", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/chat/history-sync": { + "post": { + "description": "HistorySyncRequest a chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chat" + ], + "summary": "HistorySyncRequest a chat", + "parameters": [ + { + "description": "Chat", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_chat_service.HistorySyncRequestStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/chat/mute": { + "post": { + "description": "Mute a chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chat" + ], + "summary": "Mute a chat", + "parameters": [ + { + "description": "Chat", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/chat/pin": { + "post": { + "description": "Pin a chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chat" + ], + "summary": "Pin a chat", + "parameters": [ + { + "description": "Chat", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/chat/unarchive": { + "post": { + "description": "Unarchive a chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chat" + ], + "summary": "Unarchive a chat", + "parameters": [ + { + "description": "Chat", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/chat/unmute": { + "post": { + "description": "Unmute a chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chat" + ], + "summary": "Unmute a chat", + "parameters": [ + { + "description": "Chat", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/chat/unpin": { + "post": { + "description": "Unpin a chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chat" + ], + "summary": "Unpin a chat", + "parameters": [ + { + "description": "Chat", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/community/add": { + "post": { + "description": "Add participant to community", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Community" + ], + "summary": "Add participant to community", + "parameters": [ + { + "description": "Participant data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_community_service.AddParticipantStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/community/create": { + "post": { + "description": "Create community", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Community" + ], + "summary": "Create community", + "parameters": [ + { + "description": "Community data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_community_service.CreateCommunityStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/community/remove": { + "post": { + "description": "Remove participant from community", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Community" + ], + "summary": "Remove participant from community", + "parameters": [ + { + "description": "Participant data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_community_service.AddParticipantStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/create": { + "post": { + "description": "Create group", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Create group", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.CreateGroupStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/description": { + "post": { + "description": "Set group description", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Set group description", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.SetGroupDescriptionStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/info": { + "post": { + "description": "Get group info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Get group info", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.GetGroupInfoStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/invitelink": { + "post": { + "description": "Get group invite link", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Get group invite link", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.GetGroupInviteLinkStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/join": { + "post": { + "description": "Join group link", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Join group link", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.JoinGroupStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/leave": { + "post": { + "description": "Leave group", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Leave group", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.LeaveGroupStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/list": { + "get": { + "description": "List groups", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "List groups", + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/myall": { + "get": { + "description": "Get my groups", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Get my groups", + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/name": { + "post": { + "description": "Set group name", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Set group name", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.SetGroupNameStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/participant": { + "post": { + "description": "Update participant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Update participant", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.AddParticipantStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/photo": { + "post": { + "description": "Set group photo", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Set group photo", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.SetGroupPhotoStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/settings": { + "post": { + "description": "Update group settings (announcement, not_announcement, locked, unlocked, approval_on, approval_off, admin_add, all_member_add)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Update group settings", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.UpdateGroupSettingsStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/all": { + "get": { + "description": "Get all instances", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Get all instances", + "responses": { + "200": { + "description": "All instances", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/connect": { + "post": { + "description": "Connect to instance with the provided data", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Connect to instance", + "parameters": [ + { + "description": "Instance data", + "name": "instance", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_service.ConnectStruct" + } + } + ], + "responses": { + "200": { + "description": "Instance connected successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/create": { + "post": { + "description": "Creates a new instance with the provided data including optional advanced settings", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Create a new instance", + "parameters": [ + { + "description": "Instance data with optional advanced settings", + "name": "instance", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_service.CreateStruct" + } + } + ], + "responses": { + "200": { + "description": "Instance created successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/delete/{instanceId}": { + "delete": { + "description": "Delete instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Delete instance", + "parameters": [ + { + "type": "string", + "description": "Instance Id", + "name": "instanceId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Instance deleted successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/disconnect": { + "post": { + "description": "Disconnect from instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Disconnect from instance", + "responses": { + "200": { + "description": "Instance disconnected successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/forcereconnect/{instanceId}": { + "post": { + "description": "Force reconnect", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Force reconnect", + "parameters": [ + { + "type": "string", + "description": "Instance Id", + "name": "instanceId", + "in": "path", + "required": true + }, + { + "description": "Instance data", + "name": "instance", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_service.ForceReconnectStruct" + } + } + ], + "responses": { + "200": { + "description": "Instance force reconnected successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/info/{instanceId}": { + "get": { + "description": "Get instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Get instance", + "parameters": [ + { + "type": "string", + "description": "Instance Id", + "name": "instanceId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Instance", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/logout": { + "delete": { + "description": "Logout from instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Logout from instance", + "responses": { + "200": { + "description": "Instance logged out successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/logs/{instanceId}": { + "get": { + "description": "Returns log entries for an instance, filterable by date range, level and limit", + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Get instance logs", + "parameters": [ + { + "type": "string", + "description": "Instance Id", + "name": "instanceId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Start date (YYYY-MM-DD, defaults to 7 days ago)", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "End date (YYYY-MM-DD, defaults to now)", + "name": "end_date", + "in": "query" + }, + { + "type": "string", + "description": "Log level filter", + "name": "level", + "in": "query" + }, + { + "type": "integer", + "description": "Max number of entries", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Logs", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/pair": { + "post": { + "description": "Request pairing code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Request pairing code", + "parameters": [ + { + "description": "Instance data", + "name": "instance", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_service.PairStruct" + } + } + ], + "responses": { + "200": { + "description": "Pairing code", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/proxy/{instanceId}": { + "post": { + "description": "Set proxy configuration for an instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Set proxy configuration", + "parameters": [ + { + "type": "string", + "description": "Instance id", + "name": "instanceId", + "in": "path", + "required": true + }, + { + "description": "Proxy configuration", + "name": "proxy", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_service.SetProxyStruct" + } + } + ], + "responses": { + "200": { + "description": "Proxy set successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + }, + "delete": { + "description": "Delete proxy", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Delete proxy", + "parameters": [ + { + "type": "string", + "description": "Instance id", + "name": "instanceId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Proxy deleted successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/qr": { + "get": { + "description": "Get instance QR code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Get instance QR code", + "responses": { + "200": { + "description": "Instance QR code", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/reconnect": { + "post": { + "description": "Reconnect to instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Reconnect to instance", + "responses": { + "200": { + "description": "Instance reconnected successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/status": { + "get": { + "description": "Get instance status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Get instance status", + "responses": { + "200": { + "description": "Instance status", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/{instanceId}/advanced-settings": { + "get": { + "description": "Get advanced settings for a specific instance", + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Get advanced settings", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "instanceId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Advanced settings retrieved successfully", + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_model.AdvancedSettings" + } + }, + "400": { + "description": "Invalid instance ID", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "404": { + "description": "Instance not found", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + }, + "put": { + "description": "Update advanced settings for a specific instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Update advanced settings", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "instanceId", + "in": "path", + "required": true + }, + { + "description": "Advanced settings data", + "name": "settings", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_model.AdvancedSettings" + } + } + ], + "responses": { + "200": { + "description": "Advanced settings updated successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Invalid request data", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "404": { + "description": "Instance not found", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/label/chat": { + "post": { + "description": "Add label to chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Label" + ], + "summary": "Add label to chat", + "parameters": [ + { + "description": "Label data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_label_service.ChatLabelStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/label/edit": { + "post": { + "description": "Edit label", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Label" + ], + "summary": "Edit label", + "parameters": [ + { + "description": "Label data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_label_service.EditLabelStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/label/list": { + "get": { + "description": "Get all labels", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Label" + ], + "summary": "Get all labels", + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/label/message": { + "post": { + "description": "Add label to message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Label" + ], + "summary": "Add label to message", + "parameters": [ + { + "description": "Label data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_label_service.MessageLabelStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/license/activate": { + "get": { + "description": "Exchanges an authorization code (from the registration callback) for an api_key and persists it. Provide the code via the query string.", + "produces": [ + "application/json" + ], + "tags": [ + "License" + ], + "summary": "Activate license", + "parameters": [ + { + "type": "string", + "description": "Authorization code from the registration callback", + "name": "code", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Activation result", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Missing code parameter", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/license/register": { + "get": { + "description": "Checks the GLOBAL_API_KEY with the licensing server. If not yet registered, initiates registration and returns a register_url. Accepts an optional redirect_uri for the post-registration redirect.", + "produces": [ + "application/json" + ], + "tags": [ + "License" + ], + "summary": "Register / get registration URL", + "parameters": [ + { + "type": "string", + "description": "Post-registration redirect URI", + "name": "redirect_uri", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Registration state (status/message or register_url)", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/license/status": { + "get": { + "description": "Returns whether the instance license is active, along with the instance id and a masked api key.", + "produces": [ + "application/json" + ], + "tags": [ + "License" + ], + "summary": "Get license status", + "responses": { + "200": { + "description": "License status ({status, instance_id, api_key?})", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/delete": { + "post": { + "description": "Delete a message for everyone", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "Delete a message for everyone", + "parameters": [ + { + "description": "Delete a message for everyone", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.MessageStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/downloadmedia": { + "post": { + "description": "Download the media content of a message (image, video, audio or document)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "Download media", + "parameters": [ + { + "description": "Download media", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.DownloadMediaStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/edit": { + "post": { + "description": "Edit a message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "Edit a message", + "parameters": [ + { + "description": "Edit a message", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.EditMessageStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/markplayed": { + "post": { + "description": "Mark an audio message as played", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "Mark an audio message as played", + "parameters": [ + { + "description": "Mark an audio message as played", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.MarkPlayedStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/markread": { + "post": { + "description": "Mark a message as read", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "Mark a message as read", + "parameters": [ + { + "description": "Mark a message as read", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.MarkReadStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/presence": { + "post": { + "description": "Set chat presence", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "Set chat presence", + "parameters": [ + { + "description": "Set chat presence", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.ChatPresenceStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/react": { + "post": { + "description": "React to a message with support for fromMe field and participant field for group messages", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "React a message", + "parameters": [ + { + "description": "React to a message with fromMe and participant fields", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.ReactStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/status": { + "post": { + "description": "Get message status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "Get message status", + "parameters": [ + { + "description": "Get message status", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.MessageStatusStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/newsletter/create": { + "post": { + "description": "Create newsletter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Newsletter" + ], + "summary": "Create newsletter", + "parameters": [ + { + "description": "Newsletter data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_newsletter_service.CreateNewsletterStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/newsletter/info": { + "post": { + "description": "Get newsletter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Newsletter" + ], + "summary": "Get newsletter", + "parameters": [ + { + "description": "Newsletter data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/newsletter/link": { + "post": { + "description": "Get newsletter invite", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Newsletter" + ], + "summary": "Get newsletter invite", + "parameters": [ + { + "description": "Newsletter data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterInviteStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/newsletter/list": { + "get": { + "description": "List newsletters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Newsletter" + ], + "summary": "List newsletters", + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/newsletter/messages": { + "post": { + "description": "Get newsletter messages", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Newsletter" + ], + "summary": "Get newsletter messages", + "parameters": [ + { + "description": "Newsletter data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterMessagesStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/newsletter/subscribe": { + "post": { + "description": "Subscribe newsletter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Newsletter" + ], + "summary": "Subscribe newsletter", + "parameters": [ + { + "description": "Newsletter data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/passkey-ceremony/{token}": { + "get": { + "description": "Returns the current WebAuthn passkey-pairing ceremony state for a token. PUBLIC endpoint (no apikey) — access is gated by the opaque short-lived ceremony token. Polled by the AgentDeck Passkey Helper browser extension.", + "produces": [ + "application/json" + ], + "tags": [ + "Passkey" + ], + "summary": "Get passkey ceremony state", + "parameters": [ + { + "type": "string", + "description": "Ceremony token", + "name": "token", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Ceremony state ({stage, skipHandoffUX, publicKey?, code?, error?})", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "token is required", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "404": { + "description": "ceremony not found or expired", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "503": { + "description": "passkey ceremony unavailable", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/passkey-ceremony/{token}/confirm": { + "post": { + "description": "Finishes the passkey pairing after the user verified the confirmation code. PUBLIC endpoint (no apikey) — gated by the ceremony token.", + "produces": [ + "application/json" + ], + "tags": [ + "Passkey" + ], + "summary": "Confirm passkey pairing", + "parameters": [ + { + "type": "string", + "description": "Ceremony token", + "name": "token", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "token is required", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "404": { + "description": "ceremony not found or expired", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "503": { + "description": "passkey ceremony unavailable", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/passkey-ceremony/{token}/response": { + "post": { + "description": "Receives the WebAuthn assertion produced by the browser extension and forwards it to WhatsApp. PUBLIC endpoint (no apikey) — gated by the ceremony token. Body is the WebAuthnResponse shape (id, rawId, type, response{clientDataJSON, authenticatorData, signature, userHandle?}), base64url-unpadded.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Passkey" + ], + "summary": "Submit passkey WebAuthn response", + "parameters": [ + { + "type": "string", + "description": "Ceremony token", + "name": "token", + "in": "path", + "required": true + }, + { + "description": "WebAuthn assertion", + "name": "response", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/types.WebAuthnResponse" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "token is required / invalid body", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "404": { + "description": "ceremony not found or expired", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "503": { + "description": "passkey ceremony unavailable", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/polls/{pollMessageId}/results": { + "get": { + "description": "Retorna todos os votos de uma enquete específica", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Polls" + ], + "summary": "Get poll results", + "parameters": [ + { + "type": "string", + "description": "ID da mensagem da enquete", + "name": "pollMessageId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_poll_model.PollResults" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/button": { + "post": { + "description": "Send an interactive message with buttons. Each button has a ` + "`" + `type` + "`" + `: ` + "`" + `reply` + "`" + `, ` + "`" + `copy` + "`" + `, ` + "`" + `url` + "`" + `, ` + "`" + `call` + "`" + ` or ` + "`" + `pix` + "`" + `.\n\nCombination rules enforced by the server:\n- Up to 3 ` + "`" + `reply` + "`" + ` buttons per message.\n- ` + "`" + `reply` + "`" + ` buttons cannot be mixed with any other type.\n- ` + "`" + `pix` + "`" + ` button must be sent ALONE (no other button in the same message).\n\nWhatsApp client rendering quirks (NOT enforced by the server, but verified in the field):\n- WhatsApp Web: only ` + "`" + `reply` + "`" + `-only messages (up to 3) OR CTAs grouped together (` + "`" + `copy` + "`" + ` + ` + "`" + `url` + "`" + ` + ` + "`" + `call` + "`" + `) render correctly.\n- Do NOT mix ` + "`" + `reply` + "`" + ` with CTA buttons (` + "`" + `copy` + "`" + `/` + "`" + `url` + "`" + `/` + "`" + `call` + "`" + `) — the message will not appear on WhatsApp Web.\n\nRequired body fields: ` + "`" + `number` + "`" + `, ` + "`" + `title` + "`" + `, ` + "`" + `description` + "`" + `, ` + "`" + `footer` + "`" + `, ` + "`" + `buttons` + "`" + `.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a button message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.ButtonStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/carousel": { + "post": { + "description": "Send an interactive carousel (multiple swipeable cards). Each card carries its own image or video, body and optional buttons.\n\nCard button ` + "`" + `type` + "`" + ` accepted values (case-insensitive, uppercased internally): ` + "`" + `REPLY` + "`" + ` (default), ` + "`" + `URL` + "`" + `, ` + "`" + `CALL` + "`" + `, ` + "`" + `COPY` + "`" + `.\nThe ` + "`" + `PIX` + "`" + ` button type is NOT supported in carousel cards — use ` + "`" + `/send/button` + "`" + ` for PIX.\n\nIMPORTANT — ` + "`" + `CarouselButtonStruct` + "`" + ` is different from the flat button used in ` + "`" + `/send/button` + "`" + `:\n- URL button: put the link in the ` + "`" + `id` + "`" + ` field (NOT in a ` + "`" + `url` + "`" + ` field).\n- CALL button: put the phone number in the ` + "`" + `id` + "`" + ` field (NOT in a ` + "`" + `phoneNumber` + "`" + ` field).\n- COPY button: put the code to be copied in ` + "`" + `copyCode` + "`" + `.\n- REPLY button: put the payload/callback ID in ` + "`" + `id` + "`" + `.\n\nPer-card combination rules (NOT enforced by the server, but verified in the field):\n- Same WhatsApp Web quirk as ` + "`" + `/send/button` + "`" + `: avoid mixing REPLY with CTA buttons (URL/CALL/COPY) in the same card — mixed sets do not render on Web.\n- Stick to either \"only REPLY\" or \"only CTAs grouped together\" per card.\n\nRequired body fields: ` + "`" + `number` + "`" + `, ` + "`" + `cards` + "`" + ` (at least one). Each card requires ` + "`" + `header` + "`" + ` + ` + "`" + `body` + "`" + `.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a carousel message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/contact": { + "post": { + "description": "Send a contact message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a contact message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.ContactStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/link": { + "post": { + "description": "Send a link message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a link message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.LinkStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/list": { + "post": { + "description": "Send an interactive list message (single-select) rendered as a tappable menu.\n\nRequired body fields: ` + "`" + `number` + "`" + `, ` + "`" + `title` + "`" + `, ` + "`" + `description` + "`" + `, ` + "`" + `footerText` + "`" + `, ` + "`" + `buttonText` + "`" + `, ` + "`" + `sections` + "`" + `.\nEach section must contain one or more ` + "`" + `rows` + "`" + `. When ` + "`" + `rowId` + "`" + ` is omitted, the server generates a fallback ID.\nWhen ` + "`" + `buttonText` + "`" + ` is empty, the server falls back to \"Ver Menu\".\n\nUses legacy ` + "`" + `ListMessage` + "`" + ` format (no ViewOnceMessage wrapper) so it renders on iOS, Android and WhatsApp Web.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a list message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.ListStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/location": { + "post": { + "description": "Send a location message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a location message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.LocationStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/media": { + "post": { + "description": "Send a media message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a media message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.MediaStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/poll": { + "post": { + "description": "Send a poll message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a poll message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.PollStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/status/media": { + "post": { + "description": "Send an image or video status to status@broadcast. Supports JSON (URL) or multipart/form-data (file upload)", + "consumes": [ + "application/json", + " multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a WhatsApp media status (image/video)", + "parameters": [ + { + "type": "string", + "description": "Media type: image or video", + "name": "type", + "in": "formData", + "required": true + }, + { + "type": "file", + "description": "Media file (for multipart upload)", + "name": "file", + "in": "formData" + }, + { + "type": "string", + "description": "Media URL (for JSON upload)", + "name": "url", + "in": "formData" + }, + { + "type": "string", + "description": "Caption for the media", + "name": "caption", + "in": "formData" + }, + { + "type": "string", + "description": "Custom message ID", + "name": "id", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/status/text": { + "post": { + "description": "Send a WhatsApp text status to status@broadcast", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a WhatsApp text status", + "parameters": [ + { + "description": "Status text data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.StatusTextStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/sticker": { + "post": { + "description": "Send a sticker message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a sticker message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.StickerStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/text": { + "post": { + "description": "Send a text message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a text message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.TextStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/unlabel/chat": { + "post": { + "description": "Remove label from chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Label" + ], + "summary": "Remove label from chat", + "parameters": [ + { + "description": "Label data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_label_service.ChatLabelStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/unlabel/message": { + "post": { + "description": "Remove label from message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Label" + ], + "summary": "Remove label from message", + "parameters": [ + { + "description": "Label data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_label_service.MessageLabelStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/avatar": { + "post": { + "description": "Get a user's avatar", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get a user's avatar", + "parameters": [ + { + "description": "Avatar data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.GetAvatarStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/block": { + "post": { + "description": "Block a contact", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Block a contact", + "parameters": [ + { + "description": "Block data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.BlockStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/blocklist": { + "get": { + "description": "Get a user's block list", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get a user's block list", + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/check": { + "post": { + "description": "Check a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Check a user", + "parameters": [ + { + "description": "User data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.CheckUserStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/contacts": { + "get": { + "description": "Get a user's contacts", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get a user's contacts", + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/info": { + "post": { + "description": "Get a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get a user", + "parameters": [ + { + "description": "User data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.CheckUserStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/privacy": { + "get": { + "description": "Get a user's privacy settings", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get a user's privacy settings", + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + }, + "post": { + "description": "Set a user's privacy settings", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Set a user's privacy settings", + "parameters": [ + { + "description": "Privacy data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.PrivacyStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/profileName": { + "post": { + "description": "Set a user's profile name", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Set a user's profile name", + "parameters": [ + { + "description": "Profile name data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.SetProfilePictureStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/profilePicture": { + "post": { + "description": "Set a user's profile picture", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Set a user's profile picture", + "parameters": [ + { + "description": "Profile picture data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.SetProfilePictureStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/profileStatus": { + "post": { + "description": "Set a user's profile status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Set a user's profile status", + "parameters": [ + { + "description": "Profile status data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.SetProfilePictureStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/unblock": { + "post": { + "description": "Unblock a contact", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Unblock a contact", + "parameters": [ + { + "description": "Block data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.BlockStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + } + }, + "definitions": { + "gin.H": { + "type": "object", + "additionalProperties": {} + }, + "agentdeck-whatsapp-service_pkg_call_service.RejectCallStruct": { + "type": "object", + "properties": { + "callCreator": { + "$ref": "#/definitions/types.JID" + }, + "callId": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_chat_service.BodyStruct": { + "type": "object", + "properties": { + "chat": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_chat_service.HistorySyncRequestStruct": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "messageInfo": { + "$ref": "#/definitions/types.MessageInfo" + } + } + }, + "agentdeck-whatsapp-service_pkg_community_service.AddParticipantStruct": { + "type": "object", + "properties": { + "communityJid": { + "type": "string" + }, + "groupJid": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "agentdeck-whatsapp-service_pkg_community_service.CreateCommunityStruct": { + "type": "object", + "properties": { + "communityName": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.AddParticipantStruct": { + "type": "object", + "properties": { + "action": { + "$ref": "#/definitions/whatsmeow.ParticipantChange" + }, + "groupJid": { + "$ref": "#/definitions/types.JID" + }, + "participants": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.CreateGroupStruct": { + "type": "object", + "properties": { + "groupName": { + "type": "string" + }, + "participants": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.GetGroupInfoStruct": { + "type": "object", + "properties": { + "groupJid": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.GetGroupInviteLinkStruct": { + "type": "object", + "properties": { + "groupJid": { + "type": "string" + }, + "reset": { + "type": "boolean" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.JoinGroupStruct": { + "type": "object", + "properties": { + "code": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.LeaveGroupStruct": { + "type": "object", + "properties": { + "groupJid": { + "$ref": "#/definitions/types.JID" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.SetGroupDescriptionStruct": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "groupJid": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.SetGroupNameStruct": { + "type": "object", + "properties": { + "groupJid": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.SetGroupPhotoStruct": { + "type": "object", + "properties": { + "groupJid": { + "type": "string" + }, + "image": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.UpdateGroupSettingsStruct": { + "type": "object", + "properties": { + "action": { + "description": "announcement, not_announcement, locked, unlocked", + "type": "string" + }, + "groupJid": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_instance_model.AdvancedSettings": { + "type": "object", + "properties": { + "alwaysOnline": { + "type": "boolean" + }, + "ignoreGroups": { + "type": "boolean" + }, + "ignoreStatus": { + "type": "boolean" + }, + "msgRejectCall": { + "type": "string" + }, + "readMessages": { + "type": "boolean" + }, + "rejectCall": { + "type": "boolean" + } + } + }, + "agentdeck-whatsapp-service_pkg_instance_service.ConnectStruct": { + "type": "object", + "properties": { + "immediate": { + "type": "boolean" + }, + "natsEnable": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "rabbitmqEnable": { + "type": "string" + }, + "subscribe": { + "type": "array", + "items": { + "type": "string" + } + }, + "webhookUrl": { + "type": "string" + }, + "websocketEnable": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_instance_service.CreateStruct": { + "type": "object", + "properties": { + "advancedSettings": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_model.AdvancedSettings" + }, + "instanceId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "proxy": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_service.ProxyConfig" + }, + "token": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_instance_service.ForceReconnectStruct": { + "type": "object", + "properties": { + "number": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_instance_service.PairStruct": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "subscribe": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "agentdeck-whatsapp-service_pkg_instance_service.ProxyConfig": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "password": { + "type": "string" + }, + "port": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_instance_service.SetProxyStruct": { + "type": "object", + "required": [ + "host", + "port" + ], + "properties": { + "host": { + "type": "string" + }, + "password": { + "type": "string" + }, + "port": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_label_service.ChatLabelStruct": { + "type": "object", + "properties": { + "jid": { + "type": "string" + }, + "labelId": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_label_service.EditLabelStruct": { + "type": "object", + "properties": { + "color": { + "type": "integer" + }, + "deleted": { + "type": "boolean" + }, + "labelId": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_label_service.MessageLabelStruct": { + "type": "object", + "properties": { + "jid": { + "type": "string" + }, + "labelId": { + "type": "string" + }, + "messageId": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.ChatPresenceStruct": { + "type": "object", + "properties": { + "delay": { + "description": "Delay, in milliseconds, keeps the \"composing\"/\"recording\" indicator alive\nfor the given duration (re-sending it periodically) and then sends \"paused\".\nOnly applies when State is \"composing\". 0 = single fire (legacy behaviour).", + "type": "integer" + }, + "isAudio": { + "type": "boolean" + }, + "number": { + "type": "string" + }, + "state": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.DownloadMediaStruct": { + "type": "object", + "properties": { + "message": { + "$ref": "#/definitions/waE2E.Message" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.EditMessageStruct": { + "type": "object", + "properties": { + "chat": { + "type": "string" + }, + "message": { + "type": "string" + }, + "messageId": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.MarkPlayedStruct": { + "type": "object", + "properties": { + "id": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.MarkReadStruct": { + "type": "object", + "properties": { + "id": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.MessageStatusStruct": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.MessageStruct": { + "type": "object", + "properties": { + "chat": { + "type": "string" + }, + "messageId": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.ReactStruct": { + "type": "object", + "properties": { + "fromMe": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "number": { + "type": "string" + }, + "participant": { + "type": "string" + }, + "reaction": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_newsletter_service.CreateNewsletterStruct": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterInviteStruct": { + "type": "object", + "properties": { + "key": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterMessagesStruct": { + "type": "object", + "properties": { + "before_id": { + "type": "integer" + }, + "count": { + "type": "integer" + }, + "jid": { + "$ref": "#/definitions/types.JID" + } + } + }, + "agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterStruct": { + "type": "object", + "properties": { + "jid": { + "$ref": "#/definitions/types.JID" + } + } + }, + "agentdeck-whatsapp-service_pkg_poll_model.PollResults": { + "type": "object", + "properties": { + "optionCounts": { + "description": "hash -\u003e count", + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "pollChatJid": { + "type": "string" + }, + "pollMessageId": { + "type": "string" + }, + "totalVotes": { + "type": "integer" + }, + "voters": { + "type": "array", + "items": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_poll_model.VoterInfo" + } + }, + "votes": { + "type": "array", + "items": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_poll_model.PollVote" + } + } + } + }, + "agentdeck-whatsapp-service_pkg_poll_model.PollVote": { + "type": "object", + "properties": { + "companyId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "instanceId": { + "type": "string" + }, + "pollChatJid": { + "type": "string" + }, + "pollMessageId": { + "type": "string" + }, + "receivedAt": { + "type": "string" + }, + "selectedOptions": { + "description": "SHA-256 hashes", + "type": "array", + "items": { + "type": "string" + } + }, + "voteMessageId": { + "type": "string" + }, + "votedAt": { + "type": "string" + }, + "voterJid": { + "type": "string" + }, + "voterName": { + "type": "string" + }, + "voterPhone": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_poll_model.VoterInfo": { + "type": "object", + "properties": { + "jid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "selectedOptions": { + "type": "array", + "items": { + "type": "string" + } + }, + "votedAt": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.Button": { + "type": "object", + "properties": { + "copyCode": { + "description": "Code placed in the clipboard when type=copy.", + "type": "string", + "example": "PROMO2026" + }, + "currency": { + "description": "ISO currency code for type=pix (e.g. BRL).", + "type": "string", + "example": "BRL" + }, + "displayText": { + "description": "Label rendered inside the button (reply / copy / url / call). Ignored for pix.", + "type": "string", + "example": "Quero saber mais" + }, + "id": { + "description": "Callback payload for ` + "`" + `reply` + "`" + ` or code-to-copy internal id for ` + "`" + `copy` + "`" + `.", + "type": "string", + "example": "btn_info" + }, + "key": { + "description": "Pix key value matching the keyType.", + "type": "string", + "example": "12345678900" + }, + "keyType": { + "description": "Pix key type. One of: phone, email, cpf, cnpj, random.", + "type": "string", + "enum": [ + "phone", + "email", + "cpf", + "cnpj", + "random" + ], + "example": "cpf" + }, + "name": { + "description": "Merchant display name shown on the Pix sheet.", + "type": "string", + "example": "Minha Loja" + }, + "phoneNumber": { + "description": "Destination phone number (E.164) when type=call.", + "type": "string", + "example": "+5582988898565" + }, + "type": { + "description": "Button kind. One of: reply, copy, url, call, pix.", + "type": "string", + "enum": [ + "reply", + "copy", + "url", + "call", + "pix" + ], + "example": "reply" + }, + "url": { + "description": "Target URL when type=url.", + "type": "string", + "example": "https://agentdeck.ai" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.ButtonStruct": { + "type": "object", + "properties": { + "buttons": { + "description": "Buttons array. See combination rules on the parent type description.", + "type": "array", + "items": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.Button" + } + }, + "delay": { + "description": "Typing delay (milliseconds) applied before sending the message.", + "type": "integer", + "example": 1200 + }, + "description": { + "description": "Body description text (required).", + "type": "string", + "example": "Confira as condicoes abaixo" + }, + "footer": { + "description": "Footer text (required).", + "type": "string", + "example": "AgentDeck Whatsapp Service" + }, + "formatJid": { + "description": "If false, skips automatic formatting/validation of ` + "`" + `number` + "`" + ` into a JID.", + "type": "boolean" + }, + "imageUrl": { + "description": "Optional image URL used as header for reply-only buttons.", + "type": "string" + }, + "mentionAll": { + "description": "Mention every participant (groups only).", + "type": "boolean" + }, + "mentionedJid": { + "description": "JIDs to mention inside the body text.", + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "description": "Destination phone number.", + "type": "string", + "example": "5582988898565" + }, + "quoted": { + "description": "Quoted (reply-to) context.", + "allOf": [ + { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + } + ] + }, + "title": { + "description": "Header title (required).", + "type": "string", + "example": "Oferta especial" + }, + "videoUrl": { + "description": "Optional video URL used as header for reply-only buttons.", + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselButtonStruct": { + "type": "object", + "properties": { + "copyCode": { + "description": "Code placed in the clipboard when type=COPY.", + "type": "string", + "example": "PROMO2026" + }, + "displayText": { + "description": "Label rendered inside the button.", + "type": "string", + "example": "Quero saber mais" + }, + "id": { + "description": "Context-dependent: REPLY payload, URL target (type=URL) or phone number (type=CALL).", + "type": "string", + "example": "card1_info" + }, + "type": { + "description": "Button kind (case-insensitive). One of: REPLY (default), URL, CALL, COPY.", + "type": "string", + "enum": [ + "REPLY", + "URL", + "CALL", + "COPY", + "reply", + "url", + "call", + "copy" + ], + "example": "REPLY" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardBodyStruct": { + "type": "object", + "properties": { + "text": { + "description": "Main text of the card.", + "type": "string", + "example": "Card 1 - Oferta especial" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardHeaderStruct": { + "type": "object", + "properties": { + "imageUrl": { + "description": "Public URL to an image. Downloaded, uploaded to WhatsApp servers and used as card media.", + "type": "string", + "example": "https://picsum.photos/seed/card1/600/400" + }, + "subtitle": { + "description": "Optional subtitle rendered below the title.", + "type": "string", + "example": "Somente hoje" + }, + "title": { + "description": "Optional visible title above the media.", + "type": "string", + "example": "Oferta do dia" + }, + "videoUrl": { + "description": "Public URL to a video. Used only when ` + "`" + `imageUrl` + "`" + ` is empty.", + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardStruct": { + "type": "object", + "properties": { + "body": { + "description": "Card body text (required).", + "allOf": [ + { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardBodyStruct" + } + ] + }, + "buttons": { + "description": "Buttons shown on the card. See CarouselButtonStruct for combination rules.", + "type": "array", + "items": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselButtonStruct" + } + }, + "footer": { + "description": "Optional footer rendered under the body.", + "type": "string", + "example": "Por tempo limitado" + }, + "header": { + "description": "Card header (media + title/subtitle).", + "allOf": [ + { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardHeaderStruct" + } + ] + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselStruct": { + "type": "object", + "properties": { + "body": { + "description": "Optional message body shown above the cards.", + "type": "string", + "example": "Confira nossas novidades!" + }, + "cards": { + "description": "Cards displayed in order. At least one card is required.", + "type": "array", + "items": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardStruct" + } + }, + "delay": { + "description": "Typing delay (milliseconds) applied before sending the message.", + "type": "integer", + "example": 1200 + }, + "footer": { + "description": "Optional message footer shown below the cards.", + "type": "string", + "example": "AgentDeck Whatsapp Service" + }, + "formatJid": { + "description": "If false, skips automatic formatting/validation of ` + "`" + `number` + "`" + ` into a JID.", + "type": "boolean" + }, + "number": { + "description": "Destination phone number.", + "type": "string", + "example": "5582988898565" + }, + "quoted": { + "description": "Quoted (reply-to) context.", + "allOf": [ + { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + } + ] + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.ContactStruct": { + "type": "object", + "properties": { + "delay": { + "type": "integer" + }, + "formatJid": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "mentionAll": { + "type": "boolean" + }, + "mentionedJid": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + }, + "quoted": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + }, + "vcard": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_utils.VCardStruct" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.LinkStruct": { + "type": "object", + "properties": { + "delay": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "formatJid": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "imgUrl": { + "type": "string" + }, + "mentionAll": { + "type": "boolean" + }, + "mentionedJid": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + }, + "quoted": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + }, + "text": { + "type": "string" + }, + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.ListStruct": { + "type": "object", + "properties": { + "buttonText": { + "description": "Label of the button that opens the list. Defaults to \"Ver Menu\" when empty.", + "type": "string", + "example": "Abrir cardapio" + }, + "delay": { + "description": "Typing delay (milliseconds) applied before sending the message.", + "type": "integer", + "example": 1200 + }, + "description": { + "description": "Body description text (required).", + "type": "string", + "example": "Escolha o plano ideal para voce" + }, + "footerText": { + "description": "Footer text (required).", + "type": "string", + "example": "AgentDeck Whatsapp Service" + }, + "formatJid": { + "description": "If false, skips automatic formatting/validation of ` + "`" + `number` + "`" + ` into a JID.", + "type": "boolean" + }, + "mentionAll": { + "description": "Mention every participant (groups only).", + "type": "boolean" + }, + "mentionedJid": { + "description": "JIDs to mention inside the body text.", + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "description": "Destination phone number.", + "type": "string", + "example": "5582988898565" + }, + "quoted": { + "description": "Quoted (reply-to) context.", + "allOf": [ + { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + } + ] + }, + "sections": { + "description": "Sections with rows. At least one section with one row is required.", + "type": "array", + "items": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.Section" + } + }, + "title": { + "description": "Header title (required).", + "type": "string", + "example": "Nossos planos" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.LocationStruct": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "delay": { + "type": "integer" + }, + "formatJid": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, + "mentionAll": { + "type": "boolean" + }, + "mentionedJid": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "number": { + "type": "string" + }, + "quoted": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.MediaStruct": { + "type": "object", + "properties": { + "caption": { + "type": "string" + }, + "delay": { + "type": "integer" + }, + "filename": { + "type": "string" + }, + "formatJid": { + "type": "boolean" + }, + "forwardingScore": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "mentionAll": { + "type": "boolean" + }, + "mentionedJid": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + }, + "quoted": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.PollStruct": { + "type": "object", + "properties": { + "delay": { + "type": "integer" + }, + "formatJid": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "maxAnswer": { + "type": "integer" + }, + "mentionAll": { + "type": "boolean" + }, + "mentionedJid": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "string" + } + }, + "question": { + "type": "string" + }, + "quoted": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct": { + "type": "object", + "properties": { + "messageId": { + "type": "string" + }, + "participant": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.Row": { + "type": "object", + "properties": { + "description": { + "description": "Optional secondary line below the title.", + "type": "string", + "example": "R$ 29,90/mes" + }, + "rowId": { + "description": "Callback payload returned when the user taps the row. Auto-generated if empty.", + "type": "string", + "example": "plan_basic" + }, + "title": { + "description": "Row main label.", + "type": "string", + "example": "Plano Basico" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.Section": { + "type": "object", + "properties": { + "rows": { + "description": "Rows inside this section.", + "type": "array", + "items": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.Row" + } + }, + "title": { + "description": "Section heading (optional; rendered as a group separator).", + "type": "string", + "example": "Planos" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.StatusTextStruct": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "text": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.StickerStruct": { + "type": "object", + "properties": { + "delay": { + "type": "integer" + }, + "formatJid": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "mentionAll": { + "type": "boolean" + }, + "mentionedJid": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + }, + "quoted": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + }, + "sticker": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.TextStruct": { + "type": "object", + "properties": { + "delay": { + "type": "integer" + }, + "formatJid": { + "type": "boolean" + }, + "forwardingScore": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "mentionAll": { + "type": "boolean" + }, + "mentionedJid": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + }, + "quoted": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + }, + "text": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_user_service.BlockStruct": { + "type": "object", + "properties": { + "number": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_user_service.CheckUserStruct": { + "type": "object", + "properties": { + "formatJid": { + "type": "boolean" + }, + "number": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "agentdeck-whatsapp-service_pkg_user_service.GetAvatarStruct": { + "type": "object", + "properties": { + "number": { + "type": "string" + }, + "preview": { + "type": "boolean" + } + } + }, + "agentdeck-whatsapp-service_pkg_user_service.PrivacyStruct": { + "type": "object", + "properties": { + "callAdd": { + "$ref": "#/definitions/types.PrivacySetting" + }, + "groupAdd": { + "$ref": "#/definitions/types.PrivacySetting" + }, + "lastSeen": { + "$ref": "#/definitions/types.PrivacySetting" + }, + "online": { + "$ref": "#/definitions/types.PrivacySetting" + }, + "profile": { + "$ref": "#/definitions/types.PrivacySetting" + }, + "readReceipts": { + "$ref": "#/definitions/types.PrivacySetting" + }, + "status": { + "$ref": "#/definitions/types.PrivacySetting" + } + } + }, + "agentdeck-whatsapp-service_pkg_user_service.SetProfilePictureStruct": { + "type": "object", + "properties": { + "image": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_utils.VCardStruct": { + "type": "object", + "properties": { + "fullName": { + "type": "string" + }, + "organization": { + "type": "string" + }, + "phone": { + "type": "string" + } + } + }, + "types.AddressingMode": { + "type": "string", + "enum": [ + "pn", + "lid" + ], + "x-enum-varnames": [ + "AddressingModePN", + "AddressingModeLID" + ] + }, + "types.BotEditType": { + "type": "string", + "enum": [ + "first", + "inner", + "last" + ], + "x-enum-varnames": [ + "EditTypeFirst", + "EditTypeInner", + "EditTypeLast" + ] + }, + "types.BroadcastRecipient": { + "type": "object", + "properties": { + "lid": { + "$ref": "#/definitions/types.JID" + }, + "pn": { + "$ref": "#/definitions/types.JID" + } + } + }, + "types.DeviceSentMeta": { + "type": "object", + "properties": { + "destinationJID": { + "description": "The destination user. This should match the MessageInfo.Recipient field.", + "type": "string" + }, + "phash": { + "type": "string" + } + } + }, + "types.EditAttribute": { + "type": "string", + "enum": [ + "", + "1", + "2", + "3", + "7", + "8" + ], + "x-enum-comments": { + "EditAttributeAdminEdit": "only used in newsletters" + }, + "x-enum-descriptions": [ + "", + "", + "", + "only used in newsletters", + "", + "" + ], + "x-enum-varnames": [ + "EditAttributeEmpty", + "EditAttributeMessageEdit", + "EditAttributePinInChat", + "EditAttributeAdminEdit", + "EditAttributeSenderRevoke", + "EditAttributeAdminRevoke" + ] + }, + "types.JID": { + "type": "object", + "properties": { + "device": { + "type": "integer", + "format": "int32" + }, + "integrator": { + "type": "integer", + "format": "int32" + }, + "rawAgent": { + "type": "integer", + "format": "int32" + }, + "server": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "types.MessageInfo": { + "type": "object", + "properties": { + "addressingMode": { + "description": "The addressing mode of the message (phone number or LID)", + "allOf": [ + { + "$ref": "#/definitions/types.AddressingMode" + } + ] + }, + "broadcastListOwner": { + "description": "When sending a read receipt to a broadcast list message, the Chat is the broadcast list\nand Sender is you, so this field contains the recipient of the read receipt.", + "allOf": [ + { + "$ref": "#/definitions/types.JID" + } + ] + }, + "broadcastRecipients": { + "type": "array", + "items": { + "$ref": "#/definitions/types.BroadcastRecipient" + } + }, + "category": { + "type": "string" + }, + "chat": { + "description": "The chat where the message was sent.", + "allOf": [ + { + "$ref": "#/definitions/types.JID" + } + ] + }, + "deviceSentMeta": { + "description": "Metadata for direct messages sent from another one of the user's own devices.", + "allOf": [ + { + "$ref": "#/definitions/types.DeviceSentMeta" + } + ] + }, + "edit": { + "$ref": "#/definitions/types.EditAttribute" + }, + "id": { + "type": "string" + }, + "isFromMe": { + "description": "Whether the message was sent by the current user instead of someone else.", + "type": "boolean" + }, + "isGroup": { + "description": "Whether the chat is a group chat or broadcast list.", + "type": "boolean" + }, + "mediaType": { + "type": "string" + }, + "msgBotInfo": { + "$ref": "#/definitions/types.MsgBotInfo" + }, + "msgMetaInfo": { + "$ref": "#/definitions/types.MsgMetaInfo" + }, + "multicast": { + "type": "boolean" + }, + "pushName": { + "type": "string" + }, + "recipientAlt": { + "description": "The alternative address of the recipient of the message for DMs.", + "allOf": [ + { + "$ref": "#/definitions/types.JID" + } + ] + }, + "sender": { + "description": "The user who sent the message.", + "allOf": [ + { + "$ref": "#/definitions/types.JID" + } + ] + }, + "senderAlt": { + "description": "The alternative address of the user who sent the message", + "allOf": [ + { + "$ref": "#/definitions/types.JID" + } + ] + }, + "serverID": { + "type": "integer" + }, + "timestamp": { + "type": "string" + }, + "type": { + "type": "string" + }, + "verifiedName": { + "$ref": "#/definitions/types.VerifiedName" + } + } + }, + "types.MsgBotInfo": { + "type": "object", + "properties": { + "editSenderTimestampMS": { + "type": "string" + }, + "editTargetID": { + "type": "string" + }, + "editType": { + "$ref": "#/definitions/types.BotEditType" + } + } + }, + "types.MsgMetaInfo": { + "type": "object", + "properties": { + "deprecatedLIDSession": { + "type": "boolean" + }, + "targetChat": { + "$ref": "#/definitions/types.JID" + }, + "targetID": { + "description": "Bot things", + "type": "string" + }, + "targetSender": { + "$ref": "#/definitions/types.JID" + }, + "threadMessageID": { + "type": "string" + }, + "threadMessageSenderJID": { + "$ref": "#/definitions/types.JID" + } + } + }, + "types.PrivacySetting": { + "type": "string", + "enum": [ + "", + "all", + "contacts", + "contact_allowlist", + "contact_blacklist", + "match_last_seen", + "known", + "none", + "on_standard", + "off" + ], + "x-enum-varnames": [ + "PrivacySettingUndefined", + "PrivacySettingAll", + "PrivacySettingContacts", + "PrivacySettingContactAllowlist", + "PrivacySettingContactBlacklist", + "PrivacySettingMatchLastSeen", + "PrivacySettingKnown", + "PrivacySettingNone", + "PrivacySettingOnStandard", + "PrivacySettingOff" + ] + }, + "types.VerifiedName": { + "type": "object", + "properties": { + "certificate": { + "$ref": "#/definitions/waVnameCert.VerifiedNameCertificate" + }, + "details": { + "$ref": "#/definitions/waVnameCert.VerifiedNameCertificate_Details" + } + } + }, + "types.WebAuthnResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "rawId": { + "type": "array", + "items": { + "type": "integer" + } + }, + "response": { + "$ref": "#/definitions/types.WebAuthnResponseData" + }, + "type": { + "type": "string" + } + } + }, + "types.WebAuthnResponseData": { + "type": "object", + "properties": { + "authenticatorData": { + "type": "array", + "items": { + "type": "integer" + } + }, + "clientDataJSON": { + "type": "array", + "items": { + "type": "integer" + } + }, + "signature": { + "type": "array", + "items": { + "type": "integer" + } + }, + "userHandle": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waAICommon.AIMediaCollectionMessage": { + "type": "object", + "properties": { + "collectionID": { + "type": "string" + }, + "expectedMediaCount": { + "type": "integer" + }, + "hasGlobalCaption": { + "type": "boolean" + } + } + }, + "waAICommon.AIMediaCollectionMetadata": { + "type": "object", + "properties": { + "collectionID": { + "type": "string" + }, + "uploadOrderIndex": { + "type": "integer" + } + } + }, + "waAICommon.AIMetadataOperation": { + "type": "object", + "properties": { + "hatchMetadataSync": { + "$ref": "#/definitions/waAICommon.HatchMetadataSync" + } + } + }, + "waAICommon.AIRegenerateMetadata": { + "type": "object", + "properties": { + "messageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "responseTimestampMS": { + "type": "integer" + } + } + }, + "waAICommon.AIRichResponseUnifiedResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waAICommon.AISubscriptionRequestType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "AISubscriptionRequestType_UNSPECIFIED", + "AISubscriptionRequestType_THINK_HARD", + "AISubscriptionRequestType_IMAGE_GEN", + "AISubscriptionRequestType_VIDEO_GEN" + ] + }, + "waAICommon.AISubscriptionUpsellMetadata": { + "type": "object", + "properties": { + "requestType": { + "$ref": "#/definitions/waAICommon.AISubscriptionRequestType" + } + } + }, + "waAICommon.AIThreadInfo": { + "type": "object", + "properties": { + "clientInfo": { + "$ref": "#/definitions/waAICommon.AIThreadInfo_AIThreadClientInfo" + }, + "serverInfo": { + "$ref": "#/definitions/waAICommon.AIThreadInfo_AIThreadServerInfo" + } + } + }, + "waAICommon.AIThreadInfo_AIThreadClientInfo": { + "type": "object", + "properties": { + "sourceChatJID": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/waAICommon.AIThreadInfo_AIThreadClientInfo_AIThreadType" + } + } + }, + "waAICommon.AIThreadInfo_AIThreadClientInfo_AIThreadType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "AIThreadInfo_AIThreadClientInfo_UNKNOWN", + "AIThreadInfo_AIThreadClientInfo_DEFAULT", + "AIThreadInfo_AIThreadClientInfo_INCOGNITO", + "AIThreadInfo_AIThreadClientInfo_SIDE_CHAT" + ] + }, + "waAICommon.AIThreadInfo_AIThreadServerInfo": { + "type": "object", + "properties": { + "title": { + "type": "string" + } + } + }, + "waAICommon.BotAgeCollectionMetadata": { + "type": "object", + "properties": { + "ageCollectionEligible": { + "type": "boolean" + }, + "ageCollectionType": { + "$ref": "#/definitions/waAICommon.BotAgeCollectionMetadata_AgeCollectionType" + }, + "shouldTriggerAgeCollectionOnClient": { + "type": "boolean" + } + } + }, + "waAICommon.BotAgeCollectionMetadata_AgeCollectionType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "BotAgeCollectionMetadata_O18_BINARY", + "BotAgeCollectionMetadata_WAFFLE" + ] + }, + "waAICommon.BotAgentDeepLinkMetadata": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "waAICommon.BotAgentMetadata": { + "type": "object", + "properties": { + "deepLinkMetadata": { + "$ref": "#/definitions/waAICommon.BotAgentDeepLinkMetadata" + } + } + }, + "waAICommon.BotCapabilityMetadata": { + "type": "object", + "properties": { + "capabilities": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotCapabilityMetadata_BotCapabilityType" + } + } + } + }, + "waAICommon.BotCapabilityMetadata_BotCapabilityType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65 + ], + "x-enum-varnames": [ + "BotCapabilityMetadata_UNKNOWN", + "BotCapabilityMetadata_PROGRESS_INDICATOR", + "BotCapabilityMetadata_RICH_RESPONSE_HEADING", + "BotCapabilityMetadata_RICH_RESPONSE_NESTED_LIST", + "BotCapabilityMetadata_AI_MEMORY", + "BotCapabilityMetadata_RICH_RESPONSE_THREAD_SURFING", + "BotCapabilityMetadata_RICH_RESPONSE_TABLE", + "BotCapabilityMetadata_RICH_RESPONSE_CODE", + "BotCapabilityMetadata_RICH_RESPONSE_STRUCTURED_RESPONSE", + "BotCapabilityMetadata_RICH_RESPONSE_INLINE_IMAGE", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_CONTROL", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_1", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_2", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_3", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_4", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_5", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_6", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_7", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_8", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_9", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_10", + "BotCapabilityMetadata_RICH_RESPONSE_SUB_HEADING", + "BotCapabilityMetadata_RICH_RESPONSE_GRID_IMAGE", + "BotCapabilityMetadata_AI_STUDIO_UGC_MEMORY", + "BotCapabilityMetadata_RICH_RESPONSE_LATEX", + "BotCapabilityMetadata_RICH_RESPONSE_MAPS", + "BotCapabilityMetadata_RICH_RESPONSE_INLINE_REELS", + "BotCapabilityMetadata_AGENTIC_PLANNING", + "BotCapabilityMetadata_ACCOUNT_LINKING", + "BotCapabilityMetadata_STREAMING_DISAGGREGATION", + "BotCapabilityMetadata_RICH_RESPONSE_GRID_IMAGE_3P", + "BotCapabilityMetadata_RICH_RESPONSE_LATEX_INLINE", + "BotCapabilityMetadata_QUERY_PLAN", + "BotCapabilityMetadata_PROACTIVE_MESSAGE", + "BotCapabilityMetadata_RICH_RESPONSE_UNIFIED_RESPONSE", + "BotCapabilityMetadata_PROMOTION_MESSAGE", + "BotCapabilityMetadata_SIMPLIFIED_PROFILE_PAGE", + "BotCapabilityMetadata_RICH_RESPONSE_SOURCES_IN_MESSAGE", + "BotCapabilityMetadata_RICH_RESPONSE_SIDE_BY_SIDE_SURVEY", + "BotCapabilityMetadata_RICH_RESPONSE_UNIFIED_TEXT_COMPONENT", + "BotCapabilityMetadata_AI_SHARED_MEMORY", + "BotCapabilityMetadata_RICH_RESPONSE_UNIFIED_SOURCES", + "BotCapabilityMetadata_RICH_RESPONSE_UNIFIED_DOMAIN_CITATIONS", + "BotCapabilityMetadata_RICH_RESPONSE_UR_INLINE_REELS_ENABLED", + "BotCapabilityMetadata_RICH_RESPONSE_UR_MEDIA_GRID_ENABLED", + "BotCapabilityMetadata_RICH_RESPONSE_UR_TIMESTAMP_PLACEHOLDER", + "BotCapabilityMetadata_RICH_RESPONSE_IN_APP_SURVEY", + "BotCapabilityMetadata_AI_RESPONSE_MODEL_BRANDING", + "BotCapabilityMetadata_SESSION_TRANSPARENCY_SYSTEM_MESSAGE", + "BotCapabilityMetadata_RICH_RESPONSE_UR_REASONING", + "BotCapabilityMetadata_RICH_RESPONSE_UR_ZEITGEIST_CITATIONS", + "BotCapabilityMetadata_RICH_RESPONSE_UR_ZEITGEIST_CAROUSEL", + "BotCapabilityMetadata_AI_IMAGINE_LOADING_INDICATOR", + "BotCapabilityMetadata_RICH_RESPONSE_UR_IMAGINE", + "BotCapabilityMetadata_AI_IMAGINE_UR_TO_NATIVE_LOADING_INDICATOR", + "BotCapabilityMetadata_RICH_RESPONSE_UR_BLOKS_ENABLED", + "BotCapabilityMetadata_RICH_RESPONSE_INLINE_LINKS_ENABLED", + "BotCapabilityMetadata_RICH_RESPONSE_UR_IMAGINE_VIDEO", + "BotCapabilityMetadata_JSON_PATCH_STREAMING", + "BotCapabilityMetadata_AI_TAB_FORCE_CLIPPY", + "BotCapabilityMetadata_UNIFIED_RESPONSE_EMBEDDED_SCREENS", + "BotCapabilityMetadata_AI_SUBSCRIPTION_ENABLED", + "BotCapabilityMetadata_UNIFIED_RESPONSE_AI_CONTENT_SEARCH_ENABLED", + "BotCapabilityMetadata_UNIFIED_RESPONSE_MARKDOWN_LINKS_ENABLED", + "BotCapabilityMetadata_AI_RICH_RESPONSE_MAPS_V2_ENABLED", + "BotCapabilityMetadata_AI_SUBSCRIPTION_METERING_ENABLED" + ] + }, + "waAICommon.BotCommandMetadata": { + "type": "object", + "properties": { + "commandDescription": { + "type": "string" + }, + "commandName": { + "type": "string" + }, + "commandPrompt": { + "type": "string" + } + } + }, + "waAICommon.BotDocumentMessageMetadata": { + "type": "object", + "properties": { + "pluginType": { + "$ref": "#/definitions/waAICommon.BotDocumentMessageMetadata_DocumentPluginType" + } + } + }, + "waAICommon.BotDocumentMessageMetadata_DocumentPluginType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "BotDocumentMessageMetadata_TEXT_EXTRACTION", + "BotDocumentMessageMetadata_OCR_AND_IMAGES" + ] + }, + "waAICommon.BotFeedbackMessage": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_BotFeedbackKind" + }, + "kindNegative": { + "type": "integer" + }, + "kindPositive": { + "type": "integer" + }, + "kindReport": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_ReportKind" + }, + "messageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "sideBySideSurveyMetadata": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata" + }, + "text": { + "type": "string" + } + } + }, + "waAICommon.BotFeedbackMessage_BotFeedbackKind": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14 + ], + "x-enum-varnames": [ + "BotFeedbackMessage_BOT_FEEDBACK_POSITIVE", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_GENERIC", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_HELPFUL", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_INTERESTING", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_ACCURATE", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_SAFE", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_OTHER", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_REFUSED", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_NOT_VISUALLY_APPEALING", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_NOT_RELEVANT_TO_TEXT", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_PERSONALIZED", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_CLARITY", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_DOESNT_LOOK_LIKE_THE_PERSON", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_HALLUCINATION_INTERNAL_ONLY", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE" + ] + }, + "waAICommon.BotFeedbackMessage_ReportKind": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "BotFeedbackMessage_NONE", + "BotFeedbackMessage_GENERIC" + ] + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata": { + "type": "object", + "properties": { + "analyticsData": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SideBySideSurveyAnalyticsData" + }, + "isSelectedResponsePrimary": { + "type": "boolean" + }, + "messageIDToEdit": { + "type": "string" + }, + "metaAiAnalyticsData": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData" + }, + "responseOtid": { + "type": "string" + }, + "responseTimestampMSString": { + "type": "string" + }, + "selectedRequestID": { + "type": "string" + }, + "simonSessionFbid": { + "type": "string" + }, + "surveyID": { + "type": "integer" + } + } + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SideBySideSurveyAnalyticsData": { + "type": "object", + "properties": { + "simonSessionFbid": { + "type": "string" + }, + "tessaEvent": { + "type": "string" + }, + "tessaSessionFbid": { + "type": "string" + } + } + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData": { + "type": "object", + "properties": { + "abandonEvent": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyAbandonEventData" + }, + "cardImpressionEvent": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCardImpressionEventData" + }, + "ctaClickEvent": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCTAClickEventData" + }, + "ctaImpressionEvent": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCTAImpressionEventData" + }, + "primaryResponseID": { + "type": "string" + }, + "responseEvent": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyResponseEventData" + }, + "surveyID": { + "type": "integer" + }, + "testArmName": { + "type": "string" + }, + "timestampMSString": { + "type": "string" + } + } + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyAbandonEventData": { + "type": "object", + "properties": { + "abandonDwellTimeMSString": { + "type": "string" + } + } + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCTAClickEventData": { + "type": "object", + "properties": { + "clickDwellTimeMSString": { + "type": "string" + }, + "isSurveyExpired": { + "type": "boolean" + } + } + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCTAImpressionEventData": { + "type": "object", + "properties": { + "isSurveyExpired": { + "type": "boolean" + } + } + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCardImpressionEventData": { + "type": "object" + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyResponseEventData": { + "type": "object", + "properties": { + "responseDwellTimeMSString": { + "type": "string" + }, + "selectedResponseID": { + "type": "string" + } + } + }, + "waAICommon.BotGroupMetadata": { + "type": "object", + "properties": { + "participantsMetadata": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotGroupParticipantMetadata" + } + } + } + }, + "waAICommon.BotGroupParticipantMetadata": { + "type": "object", + "properties": { + "botFbid": { + "type": "string" + } + } + }, + "waAICommon.BotImagineMetadata": { + "type": "object", + "properties": { + "imagineType": { + "$ref": "#/definitions/waAICommon.BotImagineMetadata_ImagineType" + }, + "shortPrompt": { + "type": "string" + } + } + }, + "waAICommon.BotImagineMetadata_ImagineType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "x-enum-varnames": [ + "BotImagineMetadata_UNKNOWN", + "BotImagineMetadata_IMAGINE", + "BotImagineMetadata_MEMU", + "BotImagineMetadata_FLASH", + "BotImagineMetadata_EDIT" + ] + }, + "waAICommon.BotInfrastructureDiagnostics": { + "type": "object", + "properties": { + "botBackend": { + "$ref": "#/definitions/waAICommon.BotInfrastructureDiagnostics_BotBackend" + }, + "isThinking": { + "type": "boolean" + }, + "toolsUsed": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "waAICommon.BotInfrastructureDiagnostics_BotBackend": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "BotInfrastructureDiagnostics_AAPI", + "BotInfrastructureDiagnostics_CLIPPY" + ] + }, + "waAICommon.BotLinkedAccount": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/waAICommon.BotLinkedAccount_BotLinkedAccountType" + } + } + }, + "waAICommon.BotLinkedAccount_BotLinkedAccountType": { + "type": "integer", + "format": "int32", + "enum": [ + 0 + ], + "x-enum-varnames": [ + "BotLinkedAccount_BOT_LINKED_ACCOUNT_TYPE_1P" + ] + }, + "waAICommon.BotLinkedAccountsMetadata": { + "type": "object", + "properties": { + "acAuthTokens": { + "type": "array", + "items": { + "type": "integer" + } + }, + "acErrorCode": { + "type": "integer" + }, + "accounts": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotLinkedAccount" + } + } + } + }, + "waAICommon.BotMediaMetadata": { + "type": "object", + "properties": { + "directPath": { + "type": "string" + }, + "fileEncSHA256": { + "type": "string" + }, + "fileSHA256": { + "type": "string" + }, + "mediaKey": { + "type": "string" + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "mimetype": { + "type": "string" + }, + "orientationType": { + "$ref": "#/definitions/waAICommon.BotMediaMetadata_OrientationType" + } + } + }, + "waAICommon.BotMediaMetadata_OrientationType": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BotMediaMetadata_CENTER", + "BotMediaMetadata_LEFT", + "BotMediaMetadata_RIGHT" + ] + }, + "waAICommon.BotMemoryFact": { + "type": "object", + "properties": { + "fact": { + "type": "string" + }, + "factID": { + "type": "string" + } + } + }, + "waAICommon.BotMemoryMetadata": { + "type": "object", + "properties": { + "addedFacts": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotMemoryFact" + } + }, + "disclaimer": { + "type": "string" + }, + "removedFacts": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotMemoryFact" + } + } + } + }, + "waAICommon.BotMemuMetadata": { + "type": "object", + "properties": { + "faceImages": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotMediaMetadata" + } + } + } + }, + "waAICommon.BotMessageOrigin": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/waAICommon.BotMessageOrigin_BotMessageOriginType" + } + } + }, + "waAICommon.BotMessageOriginMetadata": { + "type": "object", + "properties": { + "origins": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotMessageOrigin" + } + } + } + }, + "waAICommon.BotMessageOrigin_BotMessageOriginType": { + "type": "integer", + "format": "int32", + "enum": [ + 0 + ], + "x-enum-varnames": [ + "BotMessageOrigin_BOT_MESSAGE_ORIGIN_TYPE_AI_INITIATED" + ] + }, + "waAICommon.BotMessageSharingInfo": { + "type": "object", + "properties": { + "botEntryPointOrigin": { + "$ref": "#/definitions/waAICommon.BotMetricsEntryPoint" + }, + "forwardScore": { + "type": "integer" + } + } + }, + "waAICommon.BotMetadata": { + "type": "object", + "properties": { + "aiConversationContext": { + "type": "array", + "items": { + "type": "integer" + } + }, + "aiMediaCollectionMetadata": { + "$ref": "#/definitions/waAICommon.AIMediaCollectionMetadata" + }, + "botAgeCollectionMetadata": { + "$ref": "#/definitions/waAICommon.BotAgeCollectionMetadata" + }, + "botDocumentMessageMetadata": { + "$ref": "#/definitions/waAICommon.BotDocumentMessageMetadata" + }, + "botGroupMetadata": { + "$ref": "#/definitions/waAICommon.BotGroupMetadata" + }, + "botInfrastructureDiagnostics": { + "$ref": "#/definitions/waAICommon.BotInfrastructureDiagnostics" + }, + "botLinkedAccountsMetadata": { + "$ref": "#/definitions/waAICommon.BotLinkedAccountsMetadata" + }, + "botMessageOriginMetadata": { + "$ref": "#/definitions/waAICommon.BotMessageOriginMetadata" + }, + "botMetricsMetadata": { + "$ref": "#/definitions/waAICommon.BotMetricsMetadata" + }, + "botModeSelectionMetadata": { + "$ref": "#/definitions/waAICommon.BotModeSelectionMetadata" + }, + "botPromotionMessageMetadata": { + "$ref": "#/definitions/waAICommon.BotPromotionMessageMetadata" + }, + "botQuotaMetadata": { + "$ref": "#/definitions/waAICommon.BotQuotaMetadata" + }, + "botRenderingConfigMetadata": { + "$ref": "#/definitions/waAICommon.BotRenderingConfigMetadata" + }, + "botResponseID": { + "type": "string" + }, + "botThreadInfo": { + "$ref": "#/definitions/waAICommon.AIThreadInfo" + }, + "capabilityMetadata": { + "$ref": "#/definitions/waAICommon.BotCapabilityMetadata" + }, + "commandMetadata": { + "$ref": "#/definitions/waAICommon.BotCommandMetadata" + }, + "conversationStarterPromptID": { + "type": "string" + }, + "imagineMetadata": { + "$ref": "#/definitions/waAICommon.BotImagineMetadata" + }, + "inThreadSurveyMetadata": { + "$ref": "#/definitions/waAICommon.InThreadSurveyMetadata" + }, + "internalMetadata": { + "type": "array", + "items": { + "type": "integer" + } + }, + "invokerJID": { + "type": "string" + }, + "memoryMetadata": { + "$ref": "#/definitions/waAICommon.BotMemoryMetadata" + }, + "memuMetadata": { + "$ref": "#/definitions/waAICommon.BotMemuMetadata" + }, + "messageDisclaimerText": { + "type": "string" + }, + "modelMetadata": { + "$ref": "#/definitions/waAICommon.BotModelMetadata" + }, + "personaID": { + "type": "string" + }, + "pluginMetadata": { + "$ref": "#/definitions/waAICommon.BotPluginMetadata" + }, + "progressIndicatorMetadata": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata" + }, + "pttPromptMetadata": { + "$ref": "#/definitions/waAICommon.BotPttPromptMetadata" + }, + "regenerateMetadata": { + "$ref": "#/definitions/waAICommon.AIRegenerateMetadata" + }, + "reminderMetadata": { + "$ref": "#/definitions/waAICommon.BotReminderMetadata" + }, + "renderingMetadata": { + "$ref": "#/definitions/waAICommon.BotRenderingMetadata" + }, + "resolvedToolCallMetadata": { + "$ref": "#/definitions/waAICommon.BotResolvedToolCallMetadata" + }, + "richResponseSourcesMetadata": { + "$ref": "#/definitions/waAICommon.BotSourcesMetadata" + }, + "sessionMetadata": { + "$ref": "#/definitions/waAICommon.BotSessionMetadata" + }, + "sessionTransparencyMetadata": { + "$ref": "#/definitions/waAICommon.SessionTransparencyMetadata" + }, + "subscriptionUpsellMetadata": { + "$ref": "#/definitions/waAICommon.AISubscriptionUpsellMetadata" + }, + "suggestedPromptMetadata": { + "$ref": "#/definitions/waAICommon.BotSuggestedPromptMetadata" + }, + "timezone": { + "type": "string" + }, + "unifiedResponseMutation": { + "$ref": "#/definitions/waAICommon.BotUnifiedResponseMutation" + }, + "verificationMetadata": { + "$ref": "#/definitions/waAICommon.BotSignatureVerificationMetadata" + } + } + }, + "waAICommon.BotMetricsEntryPoint": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 45, + 46, + 47, + 54, + 55, + 56 + ], + "x-enum-varnames": [ + "BotMetricsEntryPoint_UNDEFINED_ENTRY_POINT", + "BotMetricsEntryPoint_FAVICON", + "BotMetricsEntryPoint_CHATLIST", + "BotMetricsEntryPoint_AISEARCH_NULL_STATE_PAPER_PLANE", + "BotMetricsEntryPoint_AISEARCH_NULL_STATE_SUGGESTION", + "BotMetricsEntryPoint_AISEARCH_TYPE_AHEAD_SUGGESTION", + "BotMetricsEntryPoint_AISEARCH_TYPE_AHEAD_PAPER_PLANE", + "BotMetricsEntryPoint_AISEARCH_TYPE_AHEAD_RESULT_CHATLIST", + "BotMetricsEntryPoint_AISEARCH_TYPE_AHEAD_RESULT_MESSAGES", + "BotMetricsEntryPoint_AIVOICE_SEARCH_BAR", + "BotMetricsEntryPoint_AIVOICE_FAVICON", + "BotMetricsEntryPoint_AISTUDIO", + "BotMetricsEntryPoint_DEEPLINK", + "BotMetricsEntryPoint_NOTIFICATION", + "BotMetricsEntryPoint_PROFILE_MESSAGE_BUTTON", + "BotMetricsEntryPoint_FORWARD", + "BotMetricsEntryPoint_APP_SHORTCUT", + "BotMetricsEntryPoint_FF_FAMILY", + "BotMetricsEntryPoint_AI_TAB", + "BotMetricsEntryPoint_AI_HOME", + "BotMetricsEntryPoint_AI_DEEPLINK_IMMERSIVE", + "BotMetricsEntryPoint_AI_DEEPLINK", + "BotMetricsEntryPoint_META_AI_CHAT_SHORTCUT_AI_STUDIO", + "BotMetricsEntryPoint_UGC_CHAT_SHORTCUT_AI_STUDIO", + "BotMetricsEntryPoint_NEW_CHAT_AI_STUDIO", + "BotMetricsEntryPoint_AIVOICE_FAVICON_CALL_HISTORY", + "BotMetricsEntryPoint_ASK_META_AI_CONTEXT_MENU", + "BotMetricsEntryPoint_ASK_META_AI_CONTEXT_MENU_1ON1", + "BotMetricsEntryPoint_ASK_META_AI_CONTEXT_MENU_GROUP", + "BotMetricsEntryPoint_INVOKE_META_AI_1ON1", + "BotMetricsEntryPoint_INVOKE_META_AI_GROUP", + "BotMetricsEntryPoint_META_AI_FORWARD", + "BotMetricsEntryPoint_NEW_CHAT_AI_CONTACT", + "BotMetricsEntryPoint_MESSAGE_QUICK_ACTION_1_ON_1_CHAT", + "BotMetricsEntryPoint_MESSAGE_QUICK_ACTION_GROUP_CHAT", + "BotMetricsEntryPoint_ATTACHMENT_TRAY_1_ON_1_CHAT", + "BotMetricsEntryPoint_ATTACHMENT_TRAY_GROUP_CHAT", + "BotMetricsEntryPoint_ASK_META_AI_MEDIA_VIEWER_1ON1", + "BotMetricsEntryPoint_ASK_META_AI_MEDIA_VIEWER_GROUP", + "BotMetricsEntryPoint_MEDIA_PICKER_1_ON_1_CHAT", + "BotMetricsEntryPoint_MEDIA_PICKER_GROUP_CHAT", + "BotMetricsEntryPoint_ASK_META_AI_NO_SEARCH_RESULTS", + "BotMetricsEntryPoint_META_AI_SETTINGS", + "BotMetricsEntryPoint_WEB_INTRO_PANEL", + "BotMetricsEntryPoint_WEB_NAVIGATION_BAR", + "BotMetricsEntryPoint_GROUP_MEMBER", + "BotMetricsEntryPoint_CHATLIST_SEARCH", + "BotMetricsEntryPoint_NEW_CHAT_LIST" + ] + }, + "waAICommon.BotMetricsMetadata": { + "type": "object", + "properties": { + "destinationEntryPoint": { + "$ref": "#/definitions/waAICommon.BotMetricsEntryPoint" + }, + "destinationID": { + "type": "string" + }, + "threadOrigin": { + "$ref": "#/definitions/waAICommon.BotMetricsThreadEntryPoint" + } + } + }, + "waAICommon.BotMetricsThreadEntryPoint": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "BotMetricsThreadEntryPoint_AI_TAB_THREAD", + "BotMetricsThreadEntryPoint_AI_HOME_THREAD", + "BotMetricsThreadEntryPoint_AI_DEEPLINK_IMMERSIVE_THREAD", + "BotMetricsThreadEntryPoint_AI_DEEPLINK_THREAD", + "BotMetricsThreadEntryPoint_ASK_META_AI_CONTEXT_MENU_THREAD" + ] + }, + "waAICommon.BotModeSelectionMetadata": { + "type": "object", + "properties": { + "mode": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotModeSelectionMetadata_BotUserSelectionMode" + } + }, + "overrideMode": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waAICommon.BotModeSelectionMetadata_BotUserSelectionMode": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "BotModeSelectionMetadata_DEFAULT_MODE", + "BotModeSelectionMetadata_THINK_HARD_MODE" + ] + }, + "waAICommon.BotModelMetadata": { + "type": "object", + "properties": { + "modelNameOverride": { + "type": "string" + }, + "modelType": { + "$ref": "#/definitions/waAICommon.BotModelMetadata_ModelType" + }, + "premiumModelStatus": { + "$ref": "#/definitions/waAICommon.BotModelMetadata_PremiumModelStatus" + } + } + }, + "waAICommon.BotModelMetadata_ModelType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "BotModelMetadata_UNKNOWN_TYPE", + "BotModelMetadata_LLAMA_PROD", + "BotModelMetadata_LLAMA_PROD_PREMIUM" + ] + }, + "waAICommon.BotModelMetadata_PremiumModelStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "BotModelMetadata_UNKNOWN_STATUS", + "BotModelMetadata_AVAILABLE", + "BotModelMetadata_QUOTA_EXCEED_LIMIT" + ] + }, + "waAICommon.BotPluginMetadata": { + "type": "object", + "properties": { + "deprecatedField": { + "$ref": "#/definitions/waAICommon.BotPluginMetadata_PluginType" + }, + "expectedLinksCount": { + "type": "integer" + }, + "faviconCDNURL": { + "type": "string" + }, + "parentPluginMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "parentPluginType": { + "$ref": "#/definitions/waAICommon.BotPluginMetadata_PluginType" + }, + "pluginType": { + "$ref": "#/definitions/waAICommon.BotPluginMetadata_PluginType" + }, + "profilePhotoCDNURL": { + "type": "string" + }, + "provider": { + "$ref": "#/definitions/waAICommon.BotPluginMetadata_SearchProvider" + }, + "referenceIndex": { + "type": "integer" + }, + "searchProviderURL": { + "type": "string" + }, + "searchQuery": { + "type": "string" + }, + "thumbnailCDNURL": { + "type": "string" + } + } + }, + "waAICommon.BotPluginMetadata_PluginType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "BotPluginMetadata_UNKNOWN_PLUGIN", + "BotPluginMetadata_REELS", + "BotPluginMetadata_SEARCH" + ] + }, + "waAICommon.BotPluginMetadata_SearchProvider": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BotPluginMetadata_UNKNOWN", + "BotPluginMetadata_BING", + "BotPluginMetadata_GOOGLE", + "BotPluginMetadata_SUPPORT" + ] + }, + "waAICommon.BotProgressIndicatorMetadata": { + "type": "object", + "properties": { + "estimatedCompletionTime": { + "type": "integer" + }, + "progressDescription": { + "type": "string" + }, + "stepsMetadata": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata" + } + } + } + }, + "waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata": { + "type": "object", + "properties": { + "isEnhancedSearch": { + "type": "boolean" + }, + "isReasoning": { + "type": "boolean" + }, + "sections": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningStepSectionMetadata" + } + }, + "sourcesMetadata": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata" + } + }, + "status": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_PlanningStepStatus" + }, + "statusBody": { + "type": "string" + }, + "statusTitle": { + "type": "string" + } + } + }, + "waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourceMetadata": { + "type": "object", + "properties": { + "favIconURL": { + "type": "string" + }, + "provider": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotSearchSourceProvider" + }, + "sourceURL": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata": { + "type": "object", + "properties": { + "provider": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_BotPlanningSearchSourceProvider" + }, + "sourceTitle": { + "type": "string" + }, + "sourceURL": { + "type": "string" + } + } + }, + "waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_BotPlanningSearchSourceProvider": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_UNKNOWN", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_OTHER", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_GOOGLE", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_BING" + ] + }, + "waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningStepSectionMetadata": { + "type": "object", + "properties": { + "sectionBody": { + "type": "string" + }, + "sectionTitle": { + "type": "string" + }, + "sourcesMetadata": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourceMetadata" + } + } + } + }, + "waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotSearchSourceProvider": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_UNKNOWN_PROVIDER", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_OTHER", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_GOOGLE", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_BING" + ] + }, + "waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_PlanningStepStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_UNKNOWN", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_PLANNED", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_EXECUTING", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_FINISHED" + ] + }, + "waAICommon.BotPromotionMessageMetadata": { + "type": "object", + "properties": { + "buttonTitle": { + "type": "string" + }, + "promotionType": { + "$ref": "#/definitions/waAICommon.BotPromotionMessageMetadata_BotPromotionType" + } + } + }, + "waAICommon.BotPromotionMessageMetadata_BotPromotionType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "BotPromotionMessageMetadata_UNKNOWN_TYPE", + "BotPromotionMessageMetadata_C50", + "BotPromotionMessageMetadata_SURVEY_PLATFORM" + ] + }, + "waAICommon.BotPromptSuggestion": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + }, + "promptID": { + "type": "string" + } + } + }, + "waAICommon.BotPromptSuggestions": { + "type": "object", + "properties": { + "suggestions": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotPromptSuggestion" + } + } + } + }, + "waAICommon.BotPttPromptMetadata": { + "type": "object", + "properties": { + "transcript": { + "type": "string" + } + } + }, + "waAICommon.BotQuotaMetadata": { + "type": "object", + "properties": { + "botFeatureQuotaMetadata": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotQuotaMetadata_BotFeatureQuotaMetadata" + } + } + } + }, + "waAICommon.BotQuotaMetadata_BotFeatureQuotaMetadata": { + "type": "object", + "properties": { + "expirationTimestamp": { + "type": "integer" + }, + "featureType": { + "$ref": "#/definitions/waAICommon.BotQuotaMetadata_BotFeatureQuotaMetadata_BotFeatureType" + }, + "remainingQuota": { + "type": "integer" + } + } + }, + "waAICommon.BotQuotaMetadata_BotFeatureQuotaMetadata_BotFeatureType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "BotQuotaMetadata_BotFeatureQuotaMetadata_UNKNOWN_FEATURE", + "BotQuotaMetadata_BotFeatureQuotaMetadata_REASONING_FEATURE" + ] + }, + "waAICommon.BotReminderMetadata": { + "type": "object", + "properties": { + "action": { + "$ref": "#/definitions/waAICommon.BotReminderMetadata_ReminderAction" + }, + "frequency": { + "$ref": "#/definitions/waAICommon.BotReminderMetadata_ReminderFrequency" + }, + "name": { + "type": "string" + }, + "nextTriggerTimestamp": { + "type": "integer" + }, + "requestMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waAICommon.BotReminderMetadata_ReminderAction": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3, + 4 + ], + "x-enum-varnames": [ + "BotReminderMetadata_NOTIFY", + "BotReminderMetadata_CREATE", + "BotReminderMetadata_DELETE", + "BotReminderMetadata_UPDATE" + ] + }, + "waAICommon.BotReminderMetadata_ReminderFrequency": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "BotReminderMetadata_ONCE", + "BotReminderMetadata_DAILY", + "BotReminderMetadata_WEEKLY", + "BotReminderMetadata_BIWEEKLY", + "BotReminderMetadata_MONTHLY" + ] + }, + "waAICommon.BotRenderingConfigMetadata": { + "type": "object", + "properties": { + "bloksVersioningID": { + "type": "string" + }, + "pixelDensity": { + "type": "number" + } + } + }, + "waAICommon.BotRenderingMetadata": { + "type": "object", + "properties": { + "keywords": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotRenderingMetadata_Keyword" + } + } + } + }, + "waAICommon.BotRenderingMetadata_Keyword": { + "type": "object", + "properties": { + "associatedPrompts": { + "type": "array", + "items": { + "type": "string" + } + }, + "value": { + "type": "string" + } + } + }, + "waAICommon.BotResolvedToolCallMetadata": { + "type": "object", + "properties": { + "resolutionDataSerialized": { + "type": "string" + }, + "toolCallID": { + "type": "string" + } + } + }, + "waAICommon.BotSessionMetadata": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + }, + "sessionSource": { + "$ref": "#/definitions/waAICommon.BotSessionSource" + } + } + }, + "waAICommon.BotSessionSource": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "x-enum-varnames": [ + "BotSessionSource_NONE", + "BotSessionSource_NULL_STATE", + "BotSessionSource_TYPEAHEAD", + "BotSessionSource_USER_INPUT", + "BotSessionSource_EMU_FLASH", + "BotSessionSource_EMU_FLASH_FOLLOWUP", + "BotSessionSource_VOICE", + "BotSessionSource_AI_HOME_SESSION" + ] + }, + "waAICommon.BotSignatureVerificationMetadata": { + "type": "object", + "properties": { + "proofs": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotSignatureVerificationUseCaseProof" + } + } + } + }, + "waAICommon.BotSignatureVerificationUseCaseProof": { + "type": "object", + "properties": { + "certificateChain": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + "signature": { + "type": "array", + "items": { + "type": "integer" + } + }, + "useCase": { + "$ref": "#/definitions/waAICommon.BotSignatureVerificationUseCaseProof_BotSignatureUseCase" + }, + "version": { + "type": "integer" + } + } + }, + "waAICommon.BotSignatureVerificationUseCaseProof_BotSignatureUseCase": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BotSignatureVerificationUseCaseProof_UNSPECIFIED", + "BotSignatureVerificationUseCaseProof_WA_BOT_MSG", + "BotSignatureVerificationUseCaseProof_WA_TEE_BOT_MSG", + "BotSignatureVerificationUseCaseProof_P2P_PILLS" + ] + }, + "waAICommon.BotSourcesMetadata": { + "type": "object", + "properties": { + "sources": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotSourcesMetadata_BotSourceItem" + } + } + } + }, + "waAICommon.BotSourcesMetadata_BotSourceItem": { + "type": "object", + "properties": { + "citationNumber": { + "type": "integer" + }, + "faviconCDNURL": { + "type": "string" + }, + "provider": { + "$ref": "#/definitions/waAICommon.BotSourcesMetadata_BotSourceItem_SourceProvider" + }, + "sourceProviderURL": { + "type": "string" + }, + "sourceQuery": { + "type": "string" + }, + "sourceTitle": { + "type": "string" + }, + "thumbnailCDNURL": { + "type": "string" + } + } + }, + "waAICommon.BotSourcesMetadata_BotSourceItem_SourceProvider": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "x-enum-varnames": [ + "BotSourcesMetadata_BotSourceItem_UNKNOWN", + "BotSourcesMetadata_BotSourceItem_BING", + "BotSourcesMetadata_BotSourceItem_GOOGLE", + "BotSourcesMetadata_BotSourceItem_SUPPORT", + "BotSourcesMetadata_BotSourceItem_OTHER" + ] + }, + "waAICommon.BotSuggestedPromptMetadata": { + "type": "object", + "properties": { + "promptSuggestions": { + "$ref": "#/definitions/waAICommon.BotPromptSuggestions" + }, + "selectedPromptID": { + "type": "string" + }, + "selectedPromptIndex": { + "type": "integer" + }, + "suggestedPrompts": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "waAICommon.BotUnifiedResponseMutation": { + "type": "object", + "properties": { + "mediaDetailsMetadataList": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotUnifiedResponseMutation_MediaDetailsMetadata" + } + }, + "sbsMetadata": { + "$ref": "#/definitions/waAICommon.BotUnifiedResponseMutation_SideBySideMetadata" + } + } + }, + "waAICommon.BotUnifiedResponseMutation_MediaDetailsMetadata": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "highResMedia": { + "$ref": "#/definitions/waAICommon.BotMediaMetadata" + }, + "previewMedia": { + "$ref": "#/definitions/waAICommon.BotMediaMetadata" + } + } + }, + "waAICommon.BotUnifiedResponseMutation_SideBySideMetadata": { + "type": "object", + "properties": { + "primaryResponseID": { + "type": "string" + }, + "surveyCtaHasRendered": { + "type": "boolean" + } + } + }, + "waAICommon.ForwardedAIBotMessageInfo": { + "type": "object", + "properties": { + "botJID": { + "type": "string" + }, + "botName": { + "type": "string" + }, + "creatorName": { + "type": "string" + } + } + }, + "waAICommon.HatchMetadataSync": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + }, + "requestID": { + "type": "string" + }, + "timestampMS": { + "type": "integer" + } + } + }, + "waAICommon.InThreadSurveyMetadata": { + "type": "object", + "properties": { + "feedbackToastText": { + "type": "string" + }, + "invitationBodyText": { + "type": "string" + }, + "invitationCtaText": { + "type": "string" + }, + "invitationCtaURL": { + "type": "string" + }, + "invitationHeaderText": { + "type": "string" + }, + "privacyStatementFull": { + "type": "string" + }, + "privacyStatementParts": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.InThreadSurveyMetadata_InThreadSurveyPrivacyStatementPart" + } + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.InThreadSurveyMetadata_InThreadSurveyQuestion" + } + }, + "requestID": { + "type": "string" + }, + "simonSessionID": { + "type": "string" + }, + "simonSurveyID": { + "type": "string" + }, + "startQuestionIndex": { + "type": "integer" + }, + "surveyContinueButtonText": { + "type": "string" + }, + "surveySubmitButtonText": { + "type": "string" + }, + "surveyTitle": { + "type": "string" + }, + "tessaEvent": { + "type": "string" + }, + "tessaRootID": { + "type": "string" + }, + "tessaSessionID": { + "type": "string" + } + } + }, + "waAICommon.InThreadSurveyMetadata_InThreadSurveyOption": { + "type": "object", + "properties": { + "numericValue": { + "type": "integer" + }, + "stringValue": { + "type": "string" + }, + "textTranslated": { + "type": "string" + } + } + }, + "waAICommon.InThreadSurveyMetadata_InThreadSurveyPrivacyStatementPart": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "text": { + "type": "string" + } + } + }, + "waAICommon.InThreadSurveyMetadata_InThreadSurveyQuestion": { + "type": "object", + "properties": { + "questionID": { + "type": "string" + }, + "questionOptions": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.InThreadSurveyMetadata_InThreadSurveyOption" + } + }, + "questionText": { + "type": "string" + } + } + }, + "waAICommon.SessionTransparencyMetadata": { + "type": "object", + "properties": { + "disclaimerText": { + "type": "string" + }, + "hcaID": { + "type": "string" + }, + "sessionTransparencyType": { + "$ref": "#/definitions/waAICommon.SessionTransparencyType" + } + } + }, + "waAICommon.SessionTransparencyType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "SessionTransparencyType_UNKNOWN_TYPE", + "SessionTransparencyType_NY_AI_SAFETY_DISCLAIMER" + ] + }, + "waAICommonDeprecated.AIRichResponseCodeMetadata": { + "type": "object", + "properties": { + "codeBlocks": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseCodeMetadata_AIRichResponseCodeBlock" + } + }, + "codeLanguage": { + "type": "string" + } + } + }, + "waAICommonDeprecated.AIRichResponseCodeMetadata_AIRichResponseCodeBlock": { + "type": "object", + "properties": { + "codeContent": { + "type": "string" + }, + "highlightType": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseCodeMetadata_AIRichResponseCodeHighlightType" + } + } + }, + "waAICommonDeprecated.AIRichResponseCodeMetadata_AIRichResponseCodeHighlightType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_DEFAULT", + "AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_KEYWORD", + "AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_METHOD", + "AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_STRING", + "AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_NUMBER", + "AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_COMMENT" + ] + }, + "waAICommonDeprecated.AIRichResponseContentItemsMetadata": { + "type": "object", + "properties": { + "contentType": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseContentItemsMetadata_ContentType" + }, + "itemsMetadata": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseContentItemsMetadata_AIRichResponseContentItemMetadata" + } + } + } + }, + "waAICommonDeprecated.AIRichResponseContentItemsMetadata_AIRichResponseContentItemMetadata": { + "type": "object", + "properties": { + "airichResponseContentItem": { + "description": "Types that are valid to be assigned to AIRichResponseContentItem:\n\n\t*AIRichResponseContentItemsMetadata_AIRichResponseContentItemMetadata_ReelItem" + } + } + }, + "waAICommonDeprecated.AIRichResponseContentItemsMetadata_ContentType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "AIRichResponseContentItemsMetadata_DEFAULT", + "AIRichResponseContentItemsMetadata_CAROUSEL" + ] + }, + "waAICommonDeprecated.AIRichResponseDynamicMetadata": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "loopCount": { + "type": "integer" + }, + "type": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseDynamicMetadata_AIRichResponseDynamicMetadataType" + }, + "version": { + "type": "integer" + } + } + }, + "waAICommonDeprecated.AIRichResponseDynamicMetadata_AIRichResponseDynamicMetadataType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "AIRichResponseDynamicMetadata_AI_RICH_RESPONSE_DYNAMIC_METADATA_TYPE_UNKNOWN", + "AIRichResponseDynamicMetadata_AI_RICH_RESPONSE_DYNAMIC_METADATA_TYPE_IMAGE", + "AIRichResponseDynamicMetadata_AI_RICH_RESPONSE_DYNAMIC_METADATA_TYPE_GIF" + ] + }, + "waAICommonDeprecated.AIRichResponseGridImageMetadata": { + "type": "object", + "properties": { + "gridImageURL": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseImageURL" + }, + "imageURLs": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseImageURL" + } + } + } + }, + "waAICommonDeprecated.AIRichResponseImageURL": { + "type": "object", + "properties": { + "imageHighResURL": { + "type": "string" + }, + "imagePreviewURL": { + "type": "string" + }, + "sourceURL": { + "type": "string" + } + } + }, + "waAICommonDeprecated.AIRichResponseInlineImageMetadata": { + "type": "object", + "properties": { + "alignment": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseInlineImageMetadata_AIRichResponseImageAlignment" + }, + "imageText": { + "type": "string" + }, + "imageURL": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseImageURL" + }, + "tapLinkURL": { + "type": "string" + } + } + }, + "waAICommonDeprecated.AIRichResponseInlineImageMetadata_AIRichResponseImageAlignment": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "AIRichResponseInlineImageMetadata_AI_RICH_RESPONSE_IMAGE_LAYOUT_LEADING_ALIGNED", + "AIRichResponseInlineImageMetadata_AI_RICH_RESPONSE_IMAGE_LAYOUT_TRAILING_ALIGNED", + "AIRichResponseInlineImageMetadata_AI_RICH_RESPONSE_IMAGE_LAYOUT_CENTER_ALIGNED" + ] + }, + "waAICommonDeprecated.AIRichResponseLatexMetadata": { + "type": "object", + "properties": { + "expressions": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseLatexMetadata_AIRichResponseLatexExpression" + } + }, + "text": { + "type": "string" + } + } + }, + "waAICommonDeprecated.AIRichResponseLatexMetadata_AIRichResponseLatexExpression": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "fontHeight": { + "type": "number" + }, + "height": { + "type": "number" + }, + "imageBottomPadding": { + "type": "number" + }, + "imageLeadingPadding": { + "type": "number" + }, + "imageTopPadding": { + "type": "number" + }, + "imageTrailingPadding": { + "type": "number" + }, + "latexExpression": { + "type": "string" + }, + "width": { + "type": "number" + } + } + }, + "waAICommonDeprecated.AIRichResponseMapMetadata": { + "type": "object", + "properties": { + "annotations": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseMapMetadata_AIRichResponseMapAnnotation" + } + }, + "centerLatitude": { + "type": "number" + }, + "centerLongitude": { + "type": "number" + }, + "latitudeDelta": { + "type": "number" + }, + "longitudeDelta": { + "type": "number" + }, + "showInfoList": { + "type": "boolean" + } + } + }, + "waAICommonDeprecated.AIRichResponseMapMetadata_AIRichResponseMapAnnotation": { + "type": "object", + "properties": { + "annotationNumber": { + "type": "integer" + }, + "body": { + "type": "string" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, + "title": { + "type": "string" + } + } + }, + "waAICommonDeprecated.AIRichResponseMessageType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "AIRichResponseMessageType_AI_RICH_RESPONSE_TYPE_UNKNOWN", + "AIRichResponseMessageType_AI_RICH_RESPONSE_TYPE_STANDARD" + ] + }, + "waAICommonDeprecated.AIRichResponseSubMessage": { + "type": "object", + "properties": { + "codeMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseCodeMetadata" + }, + "contentItemsMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseContentItemsMetadata" + }, + "dynamicMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseDynamicMetadata" + }, + "gridImageMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseGridImageMetadata" + }, + "imageMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseInlineImageMetadata" + }, + "latexMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseLatexMetadata" + }, + "mapMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseMapMetadata" + }, + "messageText": { + "type": "string" + }, + "messageType": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseSubMessageType" + }, + "tableMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseTableMetadata" + } + } + }, + "waAICommonDeprecated.AIRichResponseSubMessageType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "x-enum-varnames": [ + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_UNKNOWN", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_GRID_IMAGE", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_TEXT", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_INLINE_IMAGE", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_TABLE", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_CODE", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_DYNAMIC", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_MAP", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_LATEX", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_CONTENT_ITEMS" + ] + }, + "waAICommonDeprecated.AIRichResponseTableMetadata": { + "type": "object", + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseTableMetadata_AIRichResponseTableRow" + } + }, + "title": { + "type": "string" + } + } + }, + "waAICommonDeprecated.AIRichResponseTableMetadata_AIRichResponseTableRow": { + "type": "object", + "properties": { + "isHeading": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "waAdv.ADVEncryptionType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ADVEncryptionType_E2EE", + "ADVEncryptionType_HOSTED" + ] + }, + "waCommon.LimitSharing": { + "type": "object", + "properties": { + "initiatedByMe": { + "type": "boolean" + }, + "limitSharingSettingTimestamp": { + "type": "integer" + }, + "sharingLimited": { + "type": "boolean" + }, + "trigger": { + "$ref": "#/definitions/waCommon.LimitSharing_Trigger" + } + } + }, + "waCommon.LimitSharing_Trigger": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "LimitSharing_UNKNOWN", + "LimitSharing_CHAT_SETTING", + "LimitSharing_BIZ_SUPPORTS_FB_HOSTING", + "LimitSharing_UNKNOWN_GROUP" + ] + }, + "waCommon.MessageKey": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "fromMe": { + "type": "boolean" + }, + "participant": { + "type": "string" + }, + "remoteJID": { + "type": "string" + } + } + }, + "waCompanionReg.DeviceProps_HistorySyncConfig": { + "type": "object", + "properties": { + "completeOnDemandReady": { + "type": "boolean" + }, + "fullSyncDaysLimit": { + "type": "integer" + }, + "fullSyncSizeMbLimit": { + "type": "integer" + }, + "initialSyncMaxMessagesPerChat": { + "type": "integer" + }, + "inlineInitialPayloadInE2EeMsg": { + "type": "boolean" + }, + "onDemandReady": { + "type": "boolean" + }, + "recentSyncDaysLimit": { + "type": "integer" + }, + "storageQuotaMb": { + "type": "integer" + }, + "supportAddOnHistorySyncMigration": { + "type": "boolean" + }, + "supportBizHostedMsg": { + "type": "boolean" + }, + "supportBotUserAgentChatHistory": { + "type": "boolean" + }, + "supportCagReactionsAndPolls": { + "type": "boolean" + }, + "supportCallLogHistory": { + "type": "boolean" + }, + "supportFbidBotChatHistory": { + "type": "boolean" + }, + "supportGroupHistory": { + "type": "boolean" + }, + "supportGuestChat": { + "type": "boolean" + }, + "supportHatchHistory": { + "type": "boolean" + }, + "supportHostedGroupMsg": { + "type": "boolean" + }, + "supportInlineContacts": { + "type": "boolean" + }, + "supportManusHistory": { + "type": "boolean" + }, + "supportMessageAssociation": { + "type": "boolean" + }, + "supportRecentSyncChunkMessageCountTuning": { + "type": "boolean" + }, + "supportedBotChannelFbids": { + "type": "array", + "items": { + "type": "string" + } + }, + "thumbnailSyncDaysLimit": { + "type": "integer" + } + } + }, + "waE2E.AIQueryFanout": { + "type": "object", + "properties": { + "message": { + "$ref": "#/definitions/waE2E.Message" + }, + "messageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "timestamp": { + "type": "integer" + } + } + }, + "waE2E.AIRichResponseMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "messageType": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseMessageType" + }, + "submessages": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseSubMessage" + } + }, + "unifiedResponse": { + "$ref": "#/definitions/waAICommon.AIRichResponseUnifiedResponse" + } + } + }, + "waE2E.ActionLink": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "buttonTitle": { + "type": "string" + } + } + }, + "waE2E.AlbumMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "expectedImageCount": { + "type": "integer" + }, + "expectedVideoCount": { + "type": "integer" + } + } + }, + "waE2E.AppStateFatalExceptionNotification": { + "type": "object", + "properties": { + "collectionNames": { + "type": "array", + "items": { + "type": "string" + } + }, + "timestamp": { + "type": "integer" + } + } + }, + "waE2E.AppStateSyncKey": { + "type": "object", + "properties": { + "keyData": { + "$ref": "#/definitions/waE2E.AppStateSyncKeyData" + }, + "keyID": { + "$ref": "#/definitions/waE2E.AppStateSyncKeyId" + } + } + }, + "waE2E.AppStateSyncKeyData": { + "type": "object", + "properties": { + "fingerprint": { + "$ref": "#/definitions/waE2E.AppStateSyncKeyFingerprint" + }, + "keyData": { + "type": "array", + "items": { + "type": "integer" + } + }, + "timestamp": { + "type": "integer" + } + } + }, + "waE2E.AppStateSyncKeyFingerprint": { + "type": "object", + "properties": { + "currentIndex": { + "type": "integer" + }, + "deviceIndexes": { + "type": "array", + "items": { + "type": "integer" + } + }, + "rawID": { + "type": "integer" + } + } + }, + "waE2E.AppStateSyncKeyId": { + "type": "object", + "properties": { + "keyID": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waE2E.AppStateSyncKeyRequest": { + "type": "object", + "properties": { + "keyIDs": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.AppStateSyncKeyId" + } + } + } + }, + "waE2E.AppStateSyncKeyShare": { + "type": "object", + "properties": { + "keys": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.AppStateSyncKey" + } + } + } + }, + "waE2E.AudioMessage": { + "type": "object", + "properties": { + "PTT": { + "type": "boolean" + }, + "URL": { + "type": "string" + }, + "accessibilityLabel": { + "type": "string" + }, + "backgroundArgb": { + "type": "integer" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "directPath": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "mimetype": { + "type": "string" + }, + "seconds": { + "type": "integer" + }, + "streamingSidecar": { + "type": "array", + "items": { + "type": "integer" + } + }, + "viewOnce": { + "type": "boolean" + }, + "waveform": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waE2E.BCallMessage": { + "type": "object", + "properties": { + "caption": { + "type": "string" + }, + "masterKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaType": { + "$ref": "#/definitions/waE2E.BCallMessage_MediaType" + }, + "sessionID": { + "type": "string" + } + } + }, + "waE2E.BCallMessage_MediaType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "BCallMessage_UNKNOWN", + "BCallMessage_AUDIO", + "BCallMessage_VIDEO" + ] + }, + "waE2E.ButtonsMessage": { + "type": "object", + "properties": { + "buttons": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ButtonsMessage_Button" + } + }, + "contentText": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "footerText": { + "type": "string" + }, + "header": { + "description": "Types that are valid to be assigned to Header:\n\n\t*ButtonsMessage_Text\n\t*ButtonsMessage_DocumentMessage\n\t*ButtonsMessage_ImageMessage\n\t*ButtonsMessage_VideoMessage\n\t*ButtonsMessage_LocationMessage" + }, + "headerType": { + "$ref": "#/definitions/waE2E.ButtonsMessage_HeaderType" + } + } + }, + "waE2E.ButtonsMessage_Button": { + "type": "object", + "properties": { + "buttonID": { + "type": "string" + }, + "buttonText": { + "$ref": "#/definitions/waE2E.ButtonsMessage_Button_ButtonText" + }, + "nativeFlowInfo": { + "$ref": "#/definitions/waE2E.ButtonsMessage_Button_NativeFlowInfo" + }, + "type": { + "$ref": "#/definitions/waE2E.ButtonsMessage_Button_Type" + } + } + }, + "waE2E.ButtonsMessage_Button_ButtonText": { + "type": "object", + "properties": { + "displayText": { + "type": "string" + } + } + }, + "waE2E.ButtonsMessage_Button_NativeFlowInfo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "paramsJSON": { + "type": "string" + } + } + }, + "waE2E.ButtonsMessage_Button_Type": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "ButtonsMessage_Button_UNKNOWN", + "ButtonsMessage_Button_RESPONSE", + "ButtonsMessage_Button_NATIVE_FLOW" + ] + }, + "waE2E.ButtonsMessage_HeaderType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "x-enum-varnames": [ + "ButtonsMessage_UNKNOWN", + "ButtonsMessage_EMPTY", + "ButtonsMessage_TEXT", + "ButtonsMessage_DOCUMENT", + "ButtonsMessage_IMAGE", + "ButtonsMessage_VIDEO", + "ButtonsMessage_LOCATION" + ] + }, + "waE2E.ButtonsResponseMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "response": { + "description": "Types that are valid to be assigned to Response:\n\n\t*ButtonsResponseMessage_SelectedDisplayText" + }, + "selectedButtonID": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/waE2E.ButtonsResponseMessage_Type" + } + } + }, + "waE2E.ButtonsResponseMessage_Type": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ButtonsResponseMessage_UNKNOWN", + "ButtonsResponseMessage_DISPLAY_TEXT" + ] + }, + "waE2E.Call": { + "type": "object", + "properties": { + "callEntryPoint": { + "type": "integer" + }, + "callKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "conversionData": { + "type": "array", + "items": { + "type": "integer" + } + }, + "conversionDelaySeconds": { + "type": "integer" + }, + "conversionSource": { + "type": "string" + }, + "ctwaPayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "ctwaSignals": { + "type": "string" + }, + "deeplinkPayload": { + "type": "string" + }, + "messageContextInfo": { + "$ref": "#/definitions/waE2E.MessageContextInfo" + }, + "nativeFlowCallButtonPayload": { + "type": "string" + } + } + }, + "waE2E.CallLogMessage": { + "type": "object", + "properties": { + "callOutcome": { + "$ref": "#/definitions/waE2E.CallLogMessage_CallOutcome" + }, + "callType": { + "$ref": "#/definitions/waE2E.CallLogMessage_CallType" + }, + "durationSecs": { + "type": "integer" + }, + "isVideo": { + "type": "boolean" + }, + "participants": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.CallLogMessage_CallParticipant" + } + } + } + }, + "waE2E.CallLogMessage_CallOutcome": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "x-enum-varnames": [ + "CallLogMessage_CONNECTED", + "CallLogMessage_MISSED", + "CallLogMessage_FAILED", + "CallLogMessage_REJECTED", + "CallLogMessage_ACCEPTED_ELSEWHERE", + "CallLogMessage_ONGOING", + "CallLogMessage_SILENCED_BY_DND", + "CallLogMessage_SILENCED_UNKNOWN_CALLER" + ] + }, + "waE2E.CallLogMessage_CallParticipant": { + "type": "object", + "properties": { + "JID": { + "type": "string" + }, + "callOutcome": { + "$ref": "#/definitions/waE2E.CallLogMessage_CallOutcome" + } + } + }, + "waE2E.CallLogMessage_CallType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "CallLogMessage_REGULAR", + "CallLogMessage_SCHEDULED_CALL", + "CallLogMessage_VOICE_CHAT" + ] + }, + "waE2E.CancelPaymentRequestMessage": { + "type": "object", + "properties": { + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.Chat": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "displayName": { + "type": "string" + } + } + }, + "waE2E.ChatThemeSetting": { + "type": "object", + "properties": { + "clearTheme": { + "type": "boolean" + }, + "colorSchemeID": { + "type": "string" + }, + "settingTimestampMS": { + "type": "integer" + }, + "wallpaper": { + "description": "Types that are valid to be assigned to Wallpaper:\n\n\t*ChatThemeSetting_DefaultWallpaper\n\t*ChatThemeSetting_SolidColor\n\t*ChatThemeSetting_StockImage\n\t*ChatThemeSetting_CustomImage" + } + } + }, + "waE2E.CloudAPIThreadControlNotification": { + "type": "object", + "properties": { + "consumerLid": { + "type": "string" + }, + "consumerPhoneNumber": { + "type": "string" + }, + "notificationContent": { + "$ref": "#/definitions/waE2E.CloudAPIThreadControlNotification_CloudAPIThreadControlNotificationContent" + }, + "senderNotificationTimestampMS": { + "type": "integer" + }, + "shouldSuppressNotification": { + "type": "boolean" + }, + "status": { + "$ref": "#/definitions/waE2E.CloudAPIThreadControlNotification_CloudAPIThreadControl" + } + } + }, + "waE2E.CloudAPIThreadControlNotification_CloudAPIThreadControl": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "CloudAPIThreadControlNotification_UNKNOWN", + "CloudAPIThreadControlNotification_CONTROL_PASSED", + "CloudAPIThreadControlNotification_CONTROL_TAKEN", + "CloudAPIThreadControlNotification_INFO" + ] + }, + "waE2E.CloudAPIThreadControlNotification_CloudAPIThreadControlNotificationContent": { + "type": "object", + "properties": { + "extraJSON": { + "type": "string" + }, + "handoffNotificationText": { + "type": "string" + } + } + }, + "waE2E.CommentMessage": { + "type": "object", + "properties": { + "message": { + "$ref": "#/definitions/waE2E.Message" + }, + "targetMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.ConditionalRevealMessage": { + "type": "object", + "properties": { + "conditionalRevealMessageType": { + "$ref": "#/definitions/waE2E.ConditionalRevealMessage_ConditionalRevealMessageType" + }, + "encIV": { + "type": "array", + "items": { + "type": "integer" + } + }, + "encPayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "revealKeyID": { + "type": "string" + } + } + }, + "waE2E.ConditionalRevealMessage_ConditionalRevealMessageType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ConditionalRevealMessage_UNKNOWN", + "ConditionalRevealMessage_SCHEDULED_MESSAGE" + ] + }, + "waE2E.ContactMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "displayName": { + "type": "string" + }, + "isSelfContact": { + "type": "boolean" + }, + "vcard": { + "type": "string" + } + } + }, + "waE2E.ContactsArrayMessage": { + "type": "object", + "properties": { + "contacts": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ContactMessage" + } + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "displayName": { + "type": "string" + } + } + }, + "waE2E.ContextInfo": { + "type": "object", + "properties": { + "actionLink": { + "$ref": "#/definitions/waE2E.ActionLink" + }, + "afterReadDuration": { + "type": "integer" + }, + "alwaysShowAdAttribution": { + "type": "boolean" + }, + "botMessageSharingInfo": { + "$ref": "#/definitions/waAICommon.BotMessageSharingInfo" + }, + "businessInteractionPills": { + "$ref": "#/definitions/waE2E.ContextInfo_BusinessInteractionPills" + }, + "businessMessageForwardInfo": { + "$ref": "#/definitions/waE2E.ContextInfo_BusinessMessageForwardInfo" + }, + "conversionData": { + "type": "array", + "items": { + "type": "integer" + } + }, + "conversionDelaySeconds": { + "type": "integer" + }, + "conversionSource": { + "type": "string" + }, + "crossAppSource": { + "$ref": "#/definitions/waE2E.ContextInfo_CrossAppSource" + }, + "ctwaPayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "ctwaSignals": { + "type": "string" + }, + "dataSharingContext": { + "$ref": "#/definitions/waE2E.ContextInfo_DataSharingContext" + }, + "disappearingMode": { + "$ref": "#/definitions/waE2E.DisappearingMode" + }, + "entryPointConversionApp": { + "type": "string" + }, + "entryPointConversionDelaySeconds": { + "type": "integer" + }, + "entryPointConversionExternalMedium": { + "type": "string" + }, + "entryPointConversionExternalSource": { + "type": "string" + }, + "entryPointConversionSource": { + "type": "string" + }, + "ephemeralSettingTimestamp": { + "type": "integer" + }, + "ephemeralSharedSecret": { + "type": "array", + "items": { + "type": "integer" + } + }, + "expiration": { + "type": "integer" + }, + "externalAdReply": { + "$ref": "#/definitions/waE2E.ContextInfo_ExternalAdReplyInfo" + }, + "featureEligibilities": { + "$ref": "#/definitions/waE2E.ContextInfo_FeatureEligibilities" + }, + "forwardOrigin": { + "$ref": "#/definitions/waE2E.ContextInfo_ForwardOrigin" + }, + "forwardedAiBotMessageInfo": { + "$ref": "#/definitions/waAICommon.ForwardedAIBotMessageInfo" + }, + "forwardedNewsletterMessageInfo": { + "$ref": "#/definitions/waE2E.ContextInfo_ForwardedNewsletterMessageInfo" + }, + "forwardingScore": { + "type": "integer" + }, + "groupMentions": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.GroupMention" + } + }, + "groupSubject": { + "type": "string" + }, + "isForwarded": { + "type": "boolean" + }, + "isGroupStatus": { + "type": "boolean" + }, + "isQuestion": { + "type": "boolean" + }, + "isSampled": { + "type": "boolean" + }, + "isSpoiler": { + "type": "boolean" + }, + "mediaDomainInfo": { + "$ref": "#/definitions/waE2E.MediaDomainInfo" + }, + "memberLabel": { + "$ref": "#/definitions/waE2E.MemberLabel" + }, + "mentionedJID": { + "type": "array", + "items": { + "type": "string" + } + }, + "nonJIDMentions": { + "type": "integer" + }, + "pairedMediaType": { + "$ref": "#/definitions/waE2E.ContextInfo_PairedMediaType" + }, + "parentGroupJID": { + "type": "string" + }, + "partiallySelectedContent": { + "$ref": "#/definitions/waE2E.ContextInfo_PartiallySelectedContent" + }, + "participant": { + "type": "string" + }, + "placeholderKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "posterStatusID": { + "type": "string" + }, + "questionReplyQuotedMessage": { + "$ref": "#/definitions/waE2E.ContextInfo_QuestionReplyQuotedMessage" + }, + "quotedAd": { + "$ref": "#/definitions/waE2E.ContextInfo_AdReplyInfo" + }, + "quotedMessage": { + "$ref": "#/definitions/waE2E.Message" + }, + "quotedType": { + "$ref": "#/definitions/waE2E.ContextInfo_QuotedType" + }, + "rankingVersion": { + "type": "integer" + }, + "remoteJID": { + "type": "string" + }, + "smbClientCampaignID": { + "type": "string" + }, + "smbServerCampaignID": { + "type": "string" + }, + "stanzaID": { + "type": "string" + }, + "statusAttributionType": { + "$ref": "#/definitions/waE2E.ContextInfo_StatusAttributionType" + }, + "statusAttributions": { + "type": "array", + "items": { + "$ref": "#/definitions/waStatusAttributions.StatusAttribution" + } + }, + "statusAudienceMetadata": { + "$ref": "#/definitions/waE2E.ContextInfo_StatusAudienceMetadata" + }, + "statusSourceType": { + "$ref": "#/definitions/waE2E.ContextInfo_StatusSourceType" + }, + "trustBannerAction": { + "type": "integer" + }, + "trustBannerType": { + "type": "string" + }, + "urlTrackingMap": { + "$ref": "#/definitions/waE2E.UrlTrackingMap" + }, + "utm": { + "$ref": "#/definitions/waE2E.ContextInfo_UTMInfo" + } + } + }, + "waE2E.ContextInfo_AdReplyInfo": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "advertiserName": { + "type": "string" + }, + "caption": { + "type": "string" + }, + "mediaType": { + "$ref": "#/definitions/waE2E.ContextInfo_AdReplyInfo_MediaType" + } + } + }, + "waE2E.ContextInfo_AdReplyInfo_MediaType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "ContextInfo_AdReplyInfo_NONE", + "ContextInfo_AdReplyInfo_IMAGE", + "ContextInfo_AdReplyInfo_VIDEO" + ] + }, + "waE2E.ContextInfo_BusinessInteractionPills": { + "type": "object", + "properties": { + "businessJID": { + "type": "string" + }, + "entryPoint": { + "$ref": "#/definitions/waE2E.ContextInfo_BusinessInteractionPills_EntryPoint" + }, + "pills": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ContextInfo_BusinessInteractionPills_Pill" + } + }, + "signatureEnvelope": { + "$ref": "#/definitions/waAICommon.BotSignatureVerificationMetadata" + }, + "signedPayload": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waE2E.ContextInfo_BusinessInteractionPills_EntryPoint": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "ContextInfo_BusinessInteractionPills_ENTRY_POINT_UNKNOWN", + "ContextInfo_BusinessInteractionPills_P2P_LINK_SHARE", + "ContextInfo_BusinessInteractionPills_CONTACT_CARD_SHARING", + "ContextInfo_BusinessInteractionPills_PHONE_NUMBER", + "ContextInfo_BusinessInteractionPills_STATUS", + "ContextInfo_BusinessInteractionPills_IN_THREAD_CONTEXT_CARD" + ] + }, + "waE2E.ContextInfo_BusinessInteractionPills_Pill": { + "type": "object", + "properties": { + "actionURL": { + "type": "string" + }, + "pillType": { + "$ref": "#/definitions/waE2E.ContextInfo_BusinessInteractionPills_PillType" + } + } + }, + "waE2E.ContextInfo_BusinessInteractionPills_PillType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12 + ], + "x-enum-varnames": [ + "ContextInfo_BusinessInteractionPills_UNKNOWN", + "ContextInfo_BusinessInteractionPills_VIEW_BUSINESS", + "ContextInfo_BusinessInteractionPills_CHAT", + "ContextInfo_BusinessInteractionPills_CALL", + "ContextInfo_BusinessInteractionPills_CATALOG", + "ContextInfo_BusinessInteractionPills_CHANNEL", + "ContextInfo_BusinessInteractionPills_BOOK_APPOINTMENT", + "ContextInfo_BusinessInteractionPills_OFFERS", + "ContextInfo_BusinessInteractionPills_BESTSELLERS", + "ContextInfo_BusinessInteractionPills_MENU", + "ContextInfo_BusinessInteractionPills_ABOUT", + "ContextInfo_BusinessInteractionPills_SHOP", + "ContextInfo_BusinessInteractionPills_ORDER" + ] + }, + "waE2E.ContextInfo_BusinessMessageForwardInfo": { + "type": "object", + "properties": { + "businessOwnerJID": { + "type": "string" + } + } + }, + "waE2E.ContextInfo_CrossAppSource": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "ContextInfo_CROSS_APP_SOURCE_UNKNOWN", + "ContextInfo_CROSS_APP_SOURCE_INSTAGRAM", + "ContextInfo_CROSS_APP_SOURCE_FACEBOOK" + ] + }, + "waE2E.ContextInfo_DataSharingContext": { + "type": "object", + "properties": { + "dataSharingFlags": { + "type": "integer" + }, + "encryptedSignalTokenConsented": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ContextInfo_DataSharingContext_Parameters" + } + }, + "showMmDisclosure": { + "type": "boolean" + } + } + }, + "waE2E.ContextInfo_DataSharingContext_Parameters": { + "type": "object", + "properties": { + "contents": { + "$ref": "#/definitions/waE2E.ContextInfo_DataSharingContext_Parameters" + }, + "floatData": { + "type": "number" + }, + "intData": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "stringData": { + "type": "string" + } + } + }, + "waE2E.ContextInfo_ExternalAdReplyInfo": { + "type": "object", + "properties": { + "adContextPreviewDismissed": { + "type": "boolean" + }, + "adPreviewURL": { + "type": "string" + }, + "adType": { + "$ref": "#/definitions/waE2E.ContextInfo_ExternalAdReplyInfo_AdType" + }, + "agmHeaderInteractionStrategy": { + "type": "integer" + }, + "agmSubtitleStrategy": { + "type": "integer" + }, + "agmThumbnailStrategy": { + "type": "integer" + }, + "agmTitleStrategy": { + "type": "integer" + }, + "automatedGreetingMessageCtaType": { + "type": "string" + }, + "automatedGreetingMessageShown": { + "type": "boolean" + }, + "body": { + "type": "string" + }, + "clickToWhatsappCall": { + "type": "boolean" + }, + "containsAutoReply": { + "type": "boolean" + }, + "containsCtwaFlowsAutoReply": { + "type": "boolean" + }, + "ctaPayload": { + "type": "string" + }, + "ctwaClid": { + "type": "string" + }, + "disableNudge": { + "type": "boolean" + }, + "greetingMessageBody": { + "type": "string" + }, + "mediaType": { + "$ref": "#/definitions/waE2E.ContextInfo_ExternalAdReplyInfo_MediaType" + }, + "mediaURL": { + "type": "string" + }, + "originalImageURL": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "renderLargerThumbnail": { + "type": "boolean" + }, + "showAdAttribution": { + "type": "boolean" + }, + "sourceApp": { + "type": "string" + }, + "sourceID": { + "type": "string" + }, + "sourceType": { + "type": "string" + }, + "sourceURL": { + "type": "string" + }, + "thumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailURL": { + "type": "string" + }, + "title": { + "type": "string" + }, + "wtwaAdFormat": { + "type": "boolean" + }, + "wtwaWebsiteURL": { + "type": "string" + } + } + }, + "waE2E.ContextInfo_ExternalAdReplyInfo_AdType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ContextInfo_ExternalAdReplyInfo_CTWA", + "ContextInfo_ExternalAdReplyInfo_CAWC" + ] + }, + "waE2E.ContextInfo_ExternalAdReplyInfo_MediaType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "ContextInfo_ExternalAdReplyInfo_NONE", + "ContextInfo_ExternalAdReplyInfo_IMAGE", + "ContextInfo_ExternalAdReplyInfo_VIDEO" + ] + }, + "waE2E.ContextInfo_FeatureEligibilities": { + "type": "object", + "properties": { + "canBeReshared": { + "type": "boolean" + }, + "canReceiveMultiReact": { + "type": "boolean" + }, + "canRequestFeedback": { + "type": "boolean" + }, + "cannotBeRanked": { + "type": "boolean" + }, + "cannotBeReactedTo": { + "type": "boolean" + } + } + }, + "waE2E.ContextInfo_ForwardOrigin": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "ContextInfo_UNKNOWN", + "ContextInfo_CHAT", + "ContextInfo_STATUS", + "ContextInfo_CHANNELS", + "ContextInfo_META_AI", + "ContextInfo_UGC" + ] + }, + "waE2E.ContextInfo_ForwardedNewsletterMessageInfo": { + "type": "object", + "properties": { + "accessibilityText": { + "type": "string" + }, + "contentType": { + "$ref": "#/definitions/waE2E.ContextInfo_ForwardedNewsletterMessageInfo_ContentType" + }, + "newsletterJID": { + "type": "string" + }, + "newsletterName": { + "type": "string" + }, + "profileName": { + "type": "string" + }, + "serverMessageID": { + "type": "integer" + } + } + }, + "waE2E.ContextInfo_ForwardedNewsletterMessageInfo_ContentType": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "ContextInfo_ForwardedNewsletterMessageInfo_UPDATE", + "ContextInfo_ForwardedNewsletterMessageInfo_UPDATE_CARD", + "ContextInfo_ForwardedNewsletterMessageInfo_LINK_CARD" + ] + }, + "waE2E.ContextInfo_PairedMediaType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "x-enum-varnames": [ + "ContextInfo_NOT_PAIRED_MEDIA", + "ContextInfo_SD_VIDEO_PARENT", + "ContextInfo_HD_VIDEO_CHILD", + "ContextInfo_SD_IMAGE_PARENT", + "ContextInfo_HD_IMAGE_CHILD", + "ContextInfo_MOTION_PHOTO_PARENT", + "ContextInfo_MOTION_PHOTO_CHILD", + "ContextInfo_HEVC_VIDEO_PARENT", + "ContextInfo_HEVC_VIDEO_CHILD" + ] + }, + "waE2E.ContextInfo_PartiallySelectedContent": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + } + }, + "waE2E.ContextInfo_QuestionReplyQuotedMessage": { + "type": "object", + "properties": { + "quotedQuestion": { + "$ref": "#/definitions/waE2E.Message" + }, + "quotedResponse": { + "$ref": "#/definitions/waE2E.Message" + }, + "serverQuestionID": { + "type": "integer" + } + } + }, + "waE2E.ContextInfo_QuotedType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ContextInfo_EXPLICIT", + "ContextInfo_AUTO" + ] + }, + "waE2E.ContextInfo_StatusAttributionType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "x-enum-varnames": [ + "ContextInfo_NONE", + "ContextInfo_RESHARED_FROM_MENTION", + "ContextInfo_RESHARED_FROM_POST", + "ContextInfo_RESHARED_FROM_POST_MANY_TIMES", + "ContextInfo_FORWARDED_FROM_STATUS" + ] + }, + "waE2E.ContextInfo_StatusAudienceMetadata": { + "type": "object", + "properties": { + "audienceType": { + "$ref": "#/definitions/waE2E.ContextInfo_StatusAudienceMetadata_AudienceType" + }, + "listEmoji": { + "type": "string" + }, + "listName": { + "type": "string" + } + } + }, + "waE2E.ContextInfo_StatusAudienceMetadata_AudienceType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ContextInfo_StatusAudienceMetadata_UNKNOWN", + "ContextInfo_StatusAudienceMetadata_CLOSE_FRIENDS" + ] + }, + "waE2E.ContextInfo_StatusSourceType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "ContextInfo_IMAGE", + "ContextInfo_VIDEO", + "ContextInfo_GIF", + "ContextInfo_AUDIO", + "ContextInfo_TEXT", + "ContextInfo_MUSIC_STANDALONE" + ] + }, + "waE2E.ContextInfo_UTMInfo": { + "type": "object", + "properties": { + "utmCampaign": { + "type": "string" + }, + "utmSource": { + "type": "string" + } + } + }, + "waE2E.DeclinePaymentRequestMessage": { + "type": "object", + "properties": { + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.DeviceListMetadata": { + "type": "object", + "properties": { + "receiverAccountType": { + "$ref": "#/definitions/waAdv.ADVEncryptionType" + }, + "recipientKeyHash": { + "type": "array", + "items": { + "type": "integer" + } + }, + "recipientKeyIndexes": { + "type": "array", + "items": { + "type": "integer" + } + }, + "recipientTimestamp": { + "type": "integer" + }, + "senderAccountType": { + "$ref": "#/definitions/waAdv.ADVEncryptionType" + }, + "senderKeyHash": { + "type": "array", + "items": { + "type": "integer" + } + }, + "senderKeyIndexes": { + "type": "array", + "items": { + "type": "integer" + } + }, + "senderTimestamp": { + "type": "integer" + } + } + }, + "waE2E.DeviceSentMessage": { + "type": "object", + "properties": { + "destinationJID": { + "type": "string" + }, + "message": { + "$ref": "#/definitions/waE2E.Message" + }, + "phash": { + "type": "string" + } + } + }, + "waE2E.DisappearingMode": { + "type": "object", + "properties": { + "initiatedByMe": { + "type": "boolean" + }, + "initiator": { + "$ref": "#/definitions/waE2E.DisappearingMode_Initiator" + }, + "initiatorDeviceJID": { + "type": "string" + }, + "trigger": { + "$ref": "#/definitions/waE2E.DisappearingMode_Trigger" + } + } + }, + "waE2E.DisappearingMode_Initiator": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "DisappearingMode_CHANGED_IN_CHAT", + "DisappearingMode_INITIATED_BY_ME", + "DisappearingMode_INITIATED_BY_OTHER", + "DisappearingMode_BIZ_UPGRADE_FB_HOSTING" + ] + }, + "waE2E.DisappearingMode_Trigger": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "DisappearingMode_UNKNOWN", + "DisappearingMode_CHAT_SETTING", + "DisappearingMode_ACCOUNT_SETTING", + "DisappearingMode_BULK_CHANGE", + "DisappearingMode_BIZ_SUPPORTS_FB_HOSTING", + "DisappearingMode_UNKNOWN_GROUPS" + ] + }, + "waE2E.DocumentMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "URL": { + "type": "string" + }, + "accessibilityLabel": { + "type": "string" + }, + "caption": { + "type": "string" + }, + "contactVcard": { + "type": "boolean" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "directPath": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileName": { + "type": "string" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "mimetype": { + "type": "string" + }, + "pageCount": { + "type": "integer" + }, + "thumbnailDirectPath": { + "type": "string" + }, + "thumbnailEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailHeight": { + "type": "integer" + }, + "thumbnailSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailWidth": { + "type": "integer" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.EmbeddedContent": { + "type": "object", + "properties": { + "content": { + "description": "Types that are valid to be assigned to Content:\n\n\t*EmbeddedContent_EmbeddedMessage\n\t*EmbeddedContent_EmbeddedMusic" + } + } + }, + "waE2E.EmbeddedMusic": { + "type": "object", + "properties": { + "artistAttribution": { + "type": "string" + }, + "artworkDirectPath": { + "type": "string" + }, + "artworkEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "artworkMediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "artworkSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "author": { + "type": "string" + }, + "countryBlocklist": { + "type": "array", + "items": { + "type": "integer" + } + }, + "derivedContentStartTimeInMS": { + "type": "integer" + }, + "isExplicit": { + "type": "boolean" + }, + "musicContentMediaID": { + "type": "string" + }, + "musicSongStartTimeInMS": { + "type": "integer" + }, + "overlapDurationInMS": { + "type": "integer" + }, + "songID": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.EncCommentMessage": { + "type": "object", + "properties": { + "encIV": { + "type": "array", + "items": { + "type": "integer" + } + }, + "encPayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "targetMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.EncEventResponseMessage": { + "type": "object", + "properties": { + "encIV": { + "type": "array", + "items": { + "type": "integer" + } + }, + "encPayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "eventCreationMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.EncReactionMessage": { + "type": "object", + "properties": { + "encIV": { + "type": "array", + "items": { + "type": "integer" + } + }, + "encPayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "targetMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.EventInviteMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "callLink": { + "type": "string" + }, + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "endTime": { + "type": "integer" + }, + "eventID": { + "type": "string" + }, + "eventTitle": { + "type": "string" + }, + "isCanceled": { + "type": "boolean" + }, + "startTime": { + "type": "integer" + } + } + }, + "waE2E.EventMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "description": { + "type": "string" + }, + "endTime": { + "type": "integer" + }, + "extraGuestsAllowed": { + "type": "boolean" + }, + "hasReminder": { + "type": "boolean" + }, + "isCanceled": { + "type": "boolean" + }, + "isScheduleCall": { + "type": "boolean" + }, + "joinLink": { + "type": "string" + }, + "location": { + "$ref": "#/definitions/waE2E.LocationMessage" + }, + "name": { + "type": "string" + }, + "reminderOffsetSec": { + "type": "integer" + }, + "startTime": { + "type": "integer" + } + } + }, + "waE2E.ExtendedTextMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "backgroundArgb": { + "type": "integer" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "description": { + "type": "string" + }, + "doNotPlayInline": { + "type": "boolean" + }, + "endCardTiles": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.VideoEndCard" + } + }, + "faviconMMSMetadata": { + "$ref": "#/definitions/waE2E.MMSThumbnailMetadata" + }, + "font": { + "$ref": "#/definitions/waE2E.ExtendedTextMessage_FontType" + }, + "inviteLinkGroupType": { + "$ref": "#/definitions/waE2E.ExtendedTextMessage_InviteLinkGroupType" + }, + "inviteLinkGroupTypeV2": { + "$ref": "#/definitions/waE2E.ExtendedTextMessage_InviteLinkGroupType" + }, + "inviteLinkParentGroupSubjectV2": { + "type": "string" + }, + "inviteLinkParentGroupThumbnailV2": { + "type": "array", + "items": { + "type": "integer" + } + }, + "linkPreviewMetadata": { + "$ref": "#/definitions/waE2E.LinkPreviewMetadata" + }, + "matchedText": { + "type": "string" + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "musicMetadata": { + "$ref": "#/definitions/waE2E.EmbeddedMusic" + }, + "paymentExtendedMetadata": { + "$ref": "#/definitions/waE2E.PaymentExtendedMetadata" + }, + "paymentLinkMetadata": { + "$ref": "#/definitions/waE2E.PaymentLinkMetadata" + }, + "previewType": { + "$ref": "#/definitions/waE2E.ExtendedTextMessage_PreviewType" + }, + "text": { + "type": "string" + }, + "textArgb": { + "type": "integer" + }, + "thumbnailDirectPath": { + "type": "string" + }, + "thumbnailEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailHeight": { + "type": "integer" + }, + "thumbnailSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailWidth": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "videoContentURL": { + "type": "string" + }, + "videoHeight": { + "type": "integer" + }, + "videoWidth": { + "type": "integer" + }, + "viewOnce": { + "type": "boolean" + } + } + }, + "waE2E.ExtendedTextMessage_FontType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 6, + 7, + 8, + 9, + 10 + ], + "x-enum-varnames": [ + "ExtendedTextMessage_SYSTEM", + "ExtendedTextMessage_SYSTEM_TEXT", + "ExtendedTextMessage_FB_SCRIPT", + "ExtendedTextMessage_SYSTEM_BOLD", + "ExtendedTextMessage_MORNINGBREEZE_REGULAR", + "ExtendedTextMessage_CALISTOGA_REGULAR", + "ExtendedTextMessage_EXO2_EXTRABOLD", + "ExtendedTextMessage_COURIERPRIME_BOLD" + ] + }, + "waE2E.ExtendedTextMessage_InviteLinkGroupType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "ExtendedTextMessage_DEFAULT", + "ExtendedTextMessage_PARENT", + "ExtendedTextMessage_SUB", + "ExtendedTextMessage_DEFAULT_SUB" + ] + }, + "waE2E.ExtendedTextMessage_PreviewType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 4, + 5, + 6, + 7 + ], + "x-enum-varnames": [ + "ExtendedTextMessage_NONE", + "ExtendedTextMessage_VIDEO", + "ExtendedTextMessage_PLACEHOLDER", + "ExtendedTextMessage_IMAGE", + "ExtendedTextMessage_PAYMENT_LINKS", + "ExtendedTextMessage_PROFILE" + ] + }, + "waE2E.FullHistorySyncOnDemandConfig": { + "type": "object", + "properties": { + "historyDurationDays": { + "type": "integer" + }, + "historyFromTimestamp": { + "type": "integer" + } + } + }, + "waE2E.FullHistorySyncOnDemandRequestMetadata": { + "type": "object", + "properties": { + "businessProduct": { + "type": "string" + }, + "opaqueClientData": { + "type": "array", + "items": { + "type": "integer" + } + }, + "requestID": { + "type": "string" + } + } + }, + "waE2E.FutureProofMessage": { + "type": "object", + "properties": { + "message": { + "$ref": "#/definitions/waE2E.Message" + } + } + }, + "waE2E.GroupInviteMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "groupJID": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "groupType": { + "$ref": "#/definitions/waE2E.GroupInviteMessage_GroupType" + }, + "inviteCode": { + "type": "string" + }, + "inviteExpiration": { + "type": "integer" + } + } + }, + "waE2E.GroupInviteMessage_GroupType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "GroupInviteMessage_DEFAULT", + "GroupInviteMessage_PARENT" + ] + }, + "waE2E.GroupMention": { + "type": "object", + "properties": { + "groupJID": { + "type": "string" + }, + "groupSubject": { + "type": "string" + } + } + }, + "waE2E.GroupRootKeyShare": { + "type": "object", + "properties": { + "keys": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.GroupRootKeyShareEntry" + } + } + } + }, + "waE2E.GroupRootKeyShareEntry": { + "type": "object", + "properties": { + "createdTimestampMS": { + "type": "integer" + }, + "expiryTimestampMS": { + "type": "integer" + }, + "groupRootKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "keyID": { + "type": "string" + } + } + }, + "waE2E.HighlyStructuredMessage": { + "type": "object", + "properties": { + "deterministicLc": { + "type": "string" + }, + "deterministicLg": { + "type": "string" + }, + "elementName": { + "type": "string" + }, + "fallbackLc": { + "type": "string" + }, + "fallbackLg": { + "type": "string" + }, + "hydratedHsm": { + "$ref": "#/definitions/waE2E.TemplateMessage" + }, + "localizableParams": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.HighlyStructuredMessage_HSMLocalizableParameter" + } + }, + "namespace": { + "type": "string" + }, + "params": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "waE2E.HighlyStructuredMessage_HSMLocalizableParameter": { + "type": "object", + "properties": { + "default": { + "type": "string" + }, + "paramOneof": { + "description": "Types that are valid to be assigned to ParamOneof:\n\n\t*HighlyStructuredMessage_HSMLocalizableParameter_Currency\n\t*HighlyStructuredMessage_HSMLocalizableParameter_DateTime" + } + } + }, + "waE2E.HistorySyncMessageAccessStatus": { + "type": "object", + "properties": { + "completeAccessGranted": { + "type": "boolean" + } + } + }, + "waE2E.HistorySyncNotification": { + "type": "object", + "properties": { + "chunkOrder": { + "type": "integer" + }, + "directPath": { + "type": "string" + }, + "encHandle": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fullHistorySyncOnDemandRequestMetadata": { + "$ref": "#/definitions/waE2E.FullHistorySyncOnDemandRequestMetadata" + }, + "initialHistBootstrapInlinePayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "messageAccessStatus": { + "$ref": "#/definitions/waE2E.HistorySyncMessageAccessStatus" + }, + "oldestMsgInChunkTimestampSec": { + "type": "integer" + }, + "originalMessageID": { + "type": "string" + }, + "peerDataRequestSessionID": { + "type": "string" + }, + "progress": { + "type": "integer" + }, + "syncType": { + "$ref": "#/definitions/waE2E.HistorySyncType" + } + } + }, + "waE2E.HistorySyncType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "x-enum-varnames": [ + "HistorySyncType_INITIAL_BOOTSTRAP", + "HistorySyncType_INITIAL_STATUS_V3", + "HistorySyncType_FULL", + "HistorySyncType_RECENT", + "HistorySyncType_PUSH_NAME", + "HistorySyncType_NON_BLOCKING_DATA", + "HistorySyncType_ON_DEMAND", + "HistorySyncType_NO_HISTORY", + "HistorySyncType_MESSAGE_ACCESS_STATUS" + ] + }, + "waE2E.HydratedTemplateButton": { + "type": "object", + "properties": { + "hydratedButton": { + "description": "Types that are valid to be assigned to HydratedButton:\n\n\t*HydratedTemplateButton_QuickReplyButton\n\t*HydratedTemplateButton_UrlButton\n\t*HydratedTemplateButton_CallButton" + }, + "index": { + "type": "integer" + } + } + }, + "waE2E.ImageMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "URL": { + "type": "string" + }, + "accessibilityLabel": { + "type": "string" + }, + "annotations": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.InteractiveAnnotation" + } + }, + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "directPath": { + "type": "string" + }, + "experimentGroupID": { + "type": "integer" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "firstScanLength": { + "type": "integer" + }, + "firstScanSidecar": { + "type": "array", + "items": { + "type": "integer" + } + }, + "height": { + "type": "integer" + }, + "imageSourceType": { + "$ref": "#/definitions/waE2E.ImageMessage_ImageSourceType" + }, + "interactiveAnnotations": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.InteractiveAnnotation" + } + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "midQualityFileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "midQualityFileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mimetype": { + "type": "string" + }, + "qrURL": { + "type": "string" + }, + "scanLengths": { + "type": "array", + "items": { + "type": "integer" + } + }, + "scansSidecar": { + "type": "array", + "items": { + "type": "integer" + } + }, + "staticURL": { + "type": "string" + }, + "thumbnailDirectPath": { + "type": "string" + }, + "thumbnailEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "viewOnce": { + "type": "boolean" + }, + "width": { + "type": "integer" + } + } + }, + "waE2E.ImageMessage_ImageSourceType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "ImageMessage_USER_IMAGE", + "ImageMessage_AI_GENERATED", + "ImageMessage_AI_MODIFIED", + "ImageMessage_RASTERIZED_TEXT_STATUS" + ] + }, + "waE2E.InitialSecurityNotificationSettingSync": { + "type": "object", + "properties": { + "securityNotificationEnabled": { + "type": "boolean" + } + } + }, + "waE2E.InsightDeliveryState": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "x-enum-varnames": [ + "InsightDeliveryState_SENT", + "InsightDeliveryState_DELIVERED", + "InsightDeliveryState_READ", + "InsightDeliveryState_REPLIED", + "InsightDeliveryState_QUICK_REPLIED" + ] + }, + "waE2E.InteractiveAnnotation": { + "type": "object", + "properties": { + "action": { + "description": "Types that are valid to be assigned to Action:\n\n\t*InteractiveAnnotation_Location\n\t*InteractiveAnnotation_Newsletter\n\t*InteractiveAnnotation_EmbeddedAction\n\t*InteractiveAnnotation_TapAction" + }, + "embeddedContent": { + "$ref": "#/definitions/waE2E.EmbeddedContent" + }, + "polygonVertices": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.Point" + } + }, + "shouldSkipConfirmation": { + "type": "boolean" + }, + "statusLinkType": { + "$ref": "#/definitions/waE2E.InteractiveAnnotation_StatusLinkType" + } + } + }, + "waE2E.InteractiveAnnotation_StatusLinkType": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "InteractiveAnnotation_RASTERIZED_LINK_PREVIEW", + "InteractiveAnnotation_RASTERIZED_LINK_TRUNCATED", + "InteractiveAnnotation_RASTERIZED_LINK_FULL_URL" + ] + }, + "waE2E.InteractiveMessage": { + "type": "object", + "properties": { + "bloksWidget": { + "$ref": "#/definitions/waE2E.InteractiveMessage_BloksWidget" + }, + "body": { + "$ref": "#/definitions/waE2E.InteractiveMessage_Body" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "footer": { + "$ref": "#/definitions/waE2E.InteractiveMessage_Footer" + }, + "header": { + "$ref": "#/definitions/waE2E.InteractiveMessage_Header" + }, + "interactiveMessage": { + "description": "Types that are valid to be assigned to InteractiveMessage:\n\n\t*InteractiveMessage_ShopStorefrontMessage\n\t*InteractiveMessage_CollectionMessage_\n\t*InteractiveMessage_NativeFlowMessage_\n\t*InteractiveMessage_CarouselMessage_" + }, + "urlTrackingMap": { + "$ref": "#/definitions/waE2E.UrlTrackingMap" + } + } + }, + "waE2E.InteractiveMessage_BloksWidget": { + "type": "object", + "properties": { + "data": { + "type": "string" + }, + "fallback": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "waE2E.InteractiveMessage_Body": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + } + }, + "waE2E.InteractiveMessage_Footer": { + "type": "object", + "properties": { + "hasMediaAttachment": { + "type": "boolean" + }, + "media": { + "description": "Types that are valid to be assigned to Media:\n\n\t*InteractiveMessage_Footer_AudioMessage" + }, + "text": { + "type": "string" + } + } + }, + "waE2E.InteractiveMessage_Header": { + "type": "object", + "properties": { + "bloksWidget": { + "$ref": "#/definitions/waE2E.InteractiveMessage_BloksWidget" + }, + "hasMediaAttachment": { + "type": "boolean" + }, + "media": { + "description": "Types that are valid to be assigned to Media:\n\n\t*InteractiveMessage_Header_DocumentMessage\n\t*InteractiveMessage_Header_ImageMessage\n\t*InteractiveMessage_Header_JPEGThumbnail\n\t*InteractiveMessage_Header_VideoMessage\n\t*InteractiveMessage_Header_LocationMessage\n\t*InteractiveMessage_Header_ProductMessage" + }, + "subtitle": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.InteractiveResponseMessage": { + "type": "object", + "properties": { + "body": { + "$ref": "#/definitions/waE2E.InteractiveResponseMessage_Body" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "interactiveResponseMessage": { + "description": "Types that are valid to be assigned to InteractiveResponseMessage:\n\n\t*InteractiveResponseMessage_NativeFlowResponseMessage_" + } + } + }, + "waE2E.InteractiveResponseMessage_Body": { + "type": "object", + "properties": { + "format": { + "$ref": "#/definitions/waE2E.InteractiveResponseMessage_Body_Format" + }, + "text": { + "type": "string" + } + } + }, + "waE2E.InteractiveResponseMessage_Body_Format": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "InteractiveResponseMessage_Body_DEFAULT", + "InteractiveResponseMessage_Body_EXTENSIONS_1" + ] + }, + "waE2E.InvoiceMessage": { + "type": "object", + "properties": { + "attachmentDirectPath": { + "type": "string" + }, + "attachmentFileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "attachmentFileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "attachmentJPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "attachmentMediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "attachmentMediaKeyTimestamp": { + "type": "integer" + }, + "attachmentMimetype": { + "type": "string" + }, + "attachmentType": { + "$ref": "#/definitions/waE2E.InvoiceMessage_AttachmentType" + }, + "note": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "waE2E.InvoiceMessage_AttachmentType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "InvoiceMessage_IMAGE", + "InvoiceMessage_PDF" + ] + }, + "waE2E.KeepInChatMessage": { + "type": "object", + "properties": { + "keepType": { + "$ref": "#/definitions/waE2E.KeepType" + }, + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "timestampMS": { + "type": "integer" + } + } + }, + "waE2E.KeepType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "KeepType_UNKNOWN_KEEP_TYPE", + "KeepType_KEEP_FOR_ALL", + "KeepType_UNDO_KEEP_FOR_ALL" + ] + }, + "waE2E.LIDMigrationMappingSyncMessage": { + "type": "object", + "properties": { + "encodedMappingPayload": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waE2E.LinkPreviewMetadata": { + "type": "object", + "properties": { + "fbExperimentID": { + "type": "integer" + }, + "linkInlineVideoMuted": { + "type": "boolean" + }, + "linkMediaDuration": { + "type": "integer" + }, + "musicMetadata": { + "$ref": "#/definitions/waE2E.EmbeddedMusic" + }, + "paymentLinkMetadata": { + "$ref": "#/definitions/waE2E.PaymentLinkMetadata" + }, + "socialMediaPostType": { + "$ref": "#/definitions/waE2E.LinkPreviewMetadata_SocialMediaPostType" + }, + "urlMetadata": { + "$ref": "#/definitions/waE2E.URLMetadata" + }, + "videoContentCaption": { + "type": "string" + }, + "videoContentURL": { + "type": "string" + } + } + }, + "waE2E.LinkPreviewMetadata_SocialMediaPostType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "LinkPreviewMetadata_NONE", + "LinkPreviewMetadata_REEL", + "LinkPreviewMetadata_LIVE_VIDEO", + "LinkPreviewMetadata_LONG_VIDEO", + "LinkPreviewMetadata_SINGLE_IMAGE", + "LinkPreviewMetadata_CAROUSEL" + ] + }, + "waE2E.ListMessage": { + "type": "object", + "properties": { + "buttonText": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "description": { + "type": "string" + }, + "footerText": { + "type": "string" + }, + "listType": { + "$ref": "#/definitions/waE2E.ListMessage_ListType" + }, + "productListInfo": { + "$ref": "#/definitions/waE2E.ListMessage_ProductListInfo" + }, + "sections": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ListMessage_Section" + } + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ListMessage_ListType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "ListMessage_UNKNOWN", + "ListMessage_SINGLE_SELECT", + "ListMessage_PRODUCT_LIST" + ] + }, + "waE2E.ListMessage_Product": { + "type": "object", + "properties": { + "productID": { + "type": "string" + } + } + }, + "waE2E.ListMessage_ProductListHeaderImage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "productID": { + "type": "string" + } + } + }, + "waE2E.ListMessage_ProductListInfo": { + "type": "object", + "properties": { + "businessOwnerJID": { + "type": "string" + }, + "headerImage": { + "$ref": "#/definitions/waE2E.ListMessage_ProductListHeaderImage" + }, + "productSections": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ListMessage_ProductSection" + } + } + } + }, + "waE2E.ListMessage_ProductSection": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ListMessage_Product" + } + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ListMessage_Row": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "rowID": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ListMessage_Section": { + "type": "object", + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ListMessage_Row" + } + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ListResponseMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "description": { + "type": "string" + }, + "listType": { + "$ref": "#/definitions/waE2E.ListResponseMessage_ListType" + }, + "singleSelectReply": { + "$ref": "#/definitions/waE2E.ListResponseMessage_SingleSelectReply" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ListResponseMessage_ListType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ListResponseMessage_UNKNOWN", + "ListResponseMessage_SINGLE_SELECT" + ] + }, + "waE2E.ListResponseMessage_SingleSelectReply": { + "type": "object", + "properties": { + "selectedRowID": { + "type": "string" + } + } + }, + "waE2E.LiveLocationMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "accuracyInMeters": { + "type": "integer" + }, + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "degreesClockwiseFromMagneticNorth": { + "type": "integer" + }, + "degreesLatitude": { + "type": "number" + }, + "degreesLongitude": { + "type": "number" + }, + "sequenceNumber": { + "type": "integer" + }, + "speedInMps": { + "type": "number" + }, + "timeOffset": { + "type": "integer" + } + } + }, + "waE2E.LocationMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "URL": { + "type": "string" + }, + "accuracyInMeters": { + "type": "integer" + }, + "address": { + "type": "string" + }, + "comment": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "degreesClockwiseFromMagneticNorth": { + "type": "integer" + }, + "degreesLatitude": { + "type": "number" + }, + "degreesLongitude": { + "type": "number" + }, + "isLive": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "speedInMps": { + "type": "number" + } + } + }, + "waE2E.MMSThumbnailMetadata": { + "type": "object", + "properties": { + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "thumbnailDirectPath": { + "type": "string" + }, + "thumbnailEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailHeight": { + "type": "integer" + }, + "thumbnailSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailWidth": { + "type": "integer" + } + } + }, + "waE2E.MediaDomainInfo": { + "type": "object", + "properties": { + "e2EeMediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyDomain": { + "$ref": "#/definitions/waE2E.MediaKeyDomain" + } + } + }, + "waE2E.MediaKeyDomain": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "MediaKeyDomain_MEDIA_KEY_DOMAIN_UNKNOWN", + "MediaKeyDomain_MEDIA_KEY_DOMAIN_E2EE", + "MediaKeyDomain_MEDIA_KEY_DOMAIN_NON_E2EE" + ] + }, + "waE2E.MediaNotifyMessage": { + "type": "object", + "properties": { + "expressPathURL": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + } + } + }, + "waE2E.MemberLabel": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "labelTimestamp": { + "type": "integer" + } + } + }, + "waE2E.Message": { + "type": "object", + "properties": { + "albumMessage": { + "$ref": "#/definitions/waE2E.AlbumMessage" + }, + "associatedChildMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "audioMessage": { + "$ref": "#/definitions/waE2E.AudioMessage" + }, + "bcallMessage": { + "$ref": "#/definitions/waE2E.BCallMessage" + }, + "botForwardedMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "botInvokeMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "botTaskMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "buttonsMessage": { + "$ref": "#/definitions/waE2E.ButtonsMessage" + }, + "buttonsResponseMessage": { + "$ref": "#/definitions/waE2E.ButtonsResponseMessage" + }, + "call": { + "$ref": "#/definitions/waE2E.Call" + }, + "callLogMesssage": { + "$ref": "#/definitions/waE2E.CallLogMessage" + }, + "cancelPaymentRequestMessage": { + "$ref": "#/definitions/waE2E.CancelPaymentRequestMessage" + }, + "chat": { + "$ref": "#/definitions/waE2E.Chat" + }, + "commentMessage": { + "$ref": "#/definitions/waE2E.CommentMessage" + }, + "conditionalRevealMessage": { + "$ref": "#/definitions/waE2E.ConditionalRevealMessage" + }, + "contactMessage": { + "$ref": "#/definitions/waE2E.ContactMessage" + }, + "contactsArrayMessage": { + "$ref": "#/definitions/waE2E.ContactsArrayMessage" + }, + "conversation": { + "type": "string" + }, + "declinePaymentRequestMessage": { + "$ref": "#/definitions/waE2E.DeclinePaymentRequestMessage" + }, + "deviceSentMessage": { + "$ref": "#/definitions/waE2E.DeviceSentMessage" + }, + "documentMessage": { + "$ref": "#/definitions/waE2E.DocumentMessage" + }, + "documentWithCaptionMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "editedMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "encCommentMessage": { + "$ref": "#/definitions/waE2E.EncCommentMessage" + }, + "encEventResponseMessage": { + "$ref": "#/definitions/waE2E.EncEventResponseMessage" + }, + "encReactionMessage": { + "$ref": "#/definitions/waE2E.EncReactionMessage" + }, + "ephemeralMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "eventCoverImage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "eventInviteMessage": { + "$ref": "#/definitions/waE2E.EventInviteMessage" + }, + "eventMessage": { + "$ref": "#/definitions/waE2E.EventMessage" + }, + "extendedTextMessage": { + "$ref": "#/definitions/waE2E.ExtendedTextMessage" + }, + "fastRatchetKeySenderKeyDistributionMessage": { + "$ref": "#/definitions/waE2E.SenderKeyDistributionMessage" + }, + "groupInviteMessage": { + "$ref": "#/definitions/waE2E.GroupInviteMessage" + }, + "groupMentionedMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "groupRootKeyShare": { + "$ref": "#/definitions/waE2E.GroupRootKeyShare" + }, + "groupStatusMentionMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "groupStatusMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "groupStatusMessageV2": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "highlyStructuredMessage": { + "$ref": "#/definitions/waE2E.HighlyStructuredMessage" + }, + "imageMessage": { + "$ref": "#/definitions/waE2E.ImageMessage" + }, + "interactiveMessage": { + "$ref": "#/definitions/waE2E.InteractiveMessage" + }, + "interactiveResponseMessage": { + "$ref": "#/definitions/waE2E.InteractiveResponseMessage" + }, + "invoiceMessage": { + "$ref": "#/definitions/waE2E.InvoiceMessage" + }, + "keepInChatMessage": { + "$ref": "#/definitions/waE2E.KeepInChatMessage" + }, + "limitSharingMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "listMessage": { + "$ref": "#/definitions/waE2E.ListMessage" + }, + "listResponseMessage": { + "$ref": "#/definitions/waE2E.ListResponseMessage" + }, + "liveLocationMessage": { + "$ref": "#/definitions/waE2E.LiveLocationMessage" + }, + "locationMessage": { + "$ref": "#/definitions/waE2E.LocationMessage" + }, + "lottieStickerMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "messageContextInfo": { + "$ref": "#/definitions/waE2E.MessageContextInfo" + }, + "messageHistoryBundle": { + "$ref": "#/definitions/waE2E.MessageHistoryBundle" + }, + "messageHistoryNotice": { + "$ref": "#/definitions/waE2E.MessageHistoryNotice" + }, + "newsletterAdminInviteMessage": { + "$ref": "#/definitions/waE2E.NewsletterAdminInviteMessage" + }, + "newsletterAdminProfileMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "newsletterAdminProfileMessageV2": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "newsletterAdminProfileStatusMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "newsletterFollowerInviteMessageV2": { + "$ref": "#/definitions/waE2E.NewsletterFollowerInviteMessage" + }, + "orderMessage": { + "$ref": "#/definitions/waE2E.OrderMessage" + }, + "paymentInviteMessage": { + "$ref": "#/definitions/waE2E.PaymentInviteMessage" + }, + "paymentReminderMessage": { + "$ref": "#/definitions/waE2E.PaymentReminderMessage" + }, + "pinInChatMessage": { + "$ref": "#/definitions/waE2E.PinInChatMessage" + }, + "placeholderMessage": { + "$ref": "#/definitions/waE2E.PlaceholderMessage" + }, + "pollAddOptionMessage": { + "$ref": "#/definitions/waE2E.PollAddOptionMessage" + }, + "pollCreationMessage": { + "$ref": "#/definitions/waE2E.PollCreationMessage" + }, + "pollCreationMessageV2": { + "$ref": "#/definitions/waE2E.PollCreationMessage" + }, + "pollCreationMessageV3": { + "$ref": "#/definitions/waE2E.PollCreationMessage" + }, + "pollCreationMessageV4": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "pollCreationMessageV5": { + "$ref": "#/definitions/waE2E.PollCreationMessage" + }, + "pollCreationMessageV6": { + "$ref": "#/definitions/waE2E.PollCreationMessage" + }, + "pollCreationOptionImageMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "pollResultSnapshotMessage": { + "$ref": "#/definitions/waE2E.PollResultSnapshotMessage" + }, + "pollResultSnapshotMessageV3": { + "$ref": "#/definitions/waE2E.PollResultSnapshotMessage" + }, + "pollUpdateMessage": { + "$ref": "#/definitions/waE2E.PollUpdateMessage" + }, + "productMessage": { + "$ref": "#/definitions/waE2E.ProductMessage" + }, + "protocolMessage": { + "$ref": "#/definitions/waE2E.ProtocolMessage" + }, + "ptvMessage": { + "$ref": "#/definitions/waE2E.VideoMessage" + }, + "questionMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "questionReplyMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "questionResponseMessage": { + "$ref": "#/definitions/waE2E.QuestionResponseMessage" + }, + "reactionMessage": { + "$ref": "#/definitions/waE2E.ReactionMessage" + }, + "requestPaymentMessage": { + "$ref": "#/definitions/waE2E.RequestPaymentMessage" + }, + "requestPhoneNumberMessage": { + "$ref": "#/definitions/waE2E.RequestPhoneNumberMessage" + }, + "richResponseMessage": { + "$ref": "#/definitions/waE2E.AIRichResponseMessage" + }, + "rootSecretDistributeMessage": { + "$ref": "#/definitions/waE2E.RootSecretDistributeMessage" + }, + "scheduledCallCreationMessage": { + "$ref": "#/definitions/waE2E.ScheduledCallCreationMessage" + }, + "scheduledCallEditMessage": { + "$ref": "#/definitions/waE2E.ScheduledCallEditMessage" + }, + "secretEncryptedMessage": { + "$ref": "#/definitions/waE2E.SecretEncryptedMessage" + }, + "sendPaymentMessage": { + "$ref": "#/definitions/waE2E.SendPaymentMessage" + }, + "senderKeyDistributionMessage": { + "$ref": "#/definitions/waE2E.SenderKeyDistributionMessage" + }, + "splitPaymentMessage": { + "$ref": "#/definitions/waE2E.SplitPaymentMessage" + }, + "spoilerMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "statusAddYours": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "statusMentionMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "statusNotificationMessage": { + "$ref": "#/definitions/waE2E.StatusNotificationMessage" + }, + "statusQuestionAnswerMessage": { + "$ref": "#/definitions/waE2E.StatusQuestionAnswerMessage" + }, + "statusQuotedMessage": { + "$ref": "#/definitions/waE2E.StatusQuotedMessage" + }, + "statusStickerInteractionMessage": { + "$ref": "#/definitions/waE2E.StatusStickerInteractionMessage" + }, + "stickerMessage": { + "$ref": "#/definitions/waE2E.StickerMessage" + }, + "stickerPackMessage": { + "$ref": "#/definitions/waE2E.StickerPackMessage" + }, + "stickerSyncRmrMessage": { + "$ref": "#/definitions/waE2E.StickerSyncRMRMessage" + }, + "templateButtonReplyMessage": { + "$ref": "#/definitions/waE2E.TemplateButtonReplyMessage" + }, + "templateMessage": { + "$ref": "#/definitions/waE2E.TemplateMessage" + }, + "videoMessage": { + "$ref": "#/definitions/waE2E.VideoMessage" + }, + "viewOnceMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "viewOnceMessageV2": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "viewOnceMessageV2Extension": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + } + } + }, + "waE2E.MessageAssociation": { + "type": "object", + "properties": { + "associationType": { + "$ref": "#/definitions/waE2E.MessageAssociation_AssociationType" + }, + "messageIndex": { + "type": "integer" + }, + "parentMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.MessageAssociation_AssociationType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20 + ], + "x-enum-varnames": [ + "MessageAssociation_UNKNOWN", + "MessageAssociation_MEDIA_ALBUM", + "MessageAssociation_BOT_PLUGIN", + "MessageAssociation_EVENT_COVER_IMAGE", + "MessageAssociation_STATUS_POLL", + "MessageAssociation_HD_VIDEO_DUAL_UPLOAD", + "MessageAssociation_STATUS_EXTERNAL_RESHARE", + "MessageAssociation_MEDIA_POLL", + "MessageAssociation_STATUS_ADD_YOURS", + "MessageAssociation_STATUS_NOTIFICATION", + "MessageAssociation_HD_IMAGE_DUAL_UPLOAD", + "MessageAssociation_STICKER_ANNOTATION", + "MessageAssociation_MOTION_PHOTO", + "MessageAssociation_STATUS_LINK_ACTION", + "MessageAssociation_VIEW_ALL_REPLIES", + "MessageAssociation_STATUS_ADD_YOURS_AI_IMAGINE", + "MessageAssociation_STATUS_QUESTION", + "MessageAssociation_STATUS_ADD_YOURS_DIWALI", + "MessageAssociation_STATUS_REACTION", + "MessageAssociation_HEVC_VIDEO_DUAL_UPLOAD", + "MessageAssociation_POLL_ADD_OPTION" + ] + }, + "waE2E.MessageContextInfo": { + "type": "object", + "properties": { + "botMessageSecret": { + "type": "array", + "items": { + "type": "integer" + } + }, + "botMetadata": { + "$ref": "#/definitions/waAICommon.BotMetadata" + }, + "capiCreatedGroup": { + "type": "boolean" + }, + "deviceListMetadata": { + "$ref": "#/definitions/waE2E.DeviceListMetadata" + }, + "deviceListMetadataVersion": { + "type": "integer" + }, + "limitSharing": { + "$ref": "#/definitions/waCommon.LimitSharing" + }, + "limitSharingV2": { + "$ref": "#/definitions/waCommon.LimitSharing" + }, + "messageAddOnDurationInSecs": { + "type": "integer" + }, + "messageAddOnExpiryType": { + "$ref": "#/definitions/waE2E.MessageContextInfo_MessageAddonExpiryType" + }, + "messageAssociation": { + "$ref": "#/definitions/waE2E.MessageAssociation" + }, + "messageSecret": { + "type": "array", + "items": { + "type": "integer" + } + }, + "paddingBytes": { + "type": "array", + "items": { + "type": "integer" + } + }, + "reportingTokenVersion": { + "type": "integer" + }, + "supportPayload": { + "type": "string" + }, + "teeBotMetadata": { + "type": "array", + "items": { + "type": "integer" + } + }, + "threadID": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ThreadID" + } + }, + "weblinkRenderConfig": { + "$ref": "#/definitions/waE2E.WebLinkRenderConfig" + } + } + }, + "waE2E.MessageContextInfo_MessageAddonExpiryType": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2 + ], + "x-enum-varnames": [ + "MessageContextInfo_STATIC", + "MessageContextInfo_DEPENDENT_ON_PARENT" + ] + }, + "waE2E.MessageHistoryBundle": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "directPath": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "messageHistoryMetadata": { + "$ref": "#/definitions/waE2E.MessageHistoryMetadata" + }, + "mimetype": { + "type": "string" + } + } + }, + "waE2E.MessageHistoryMetadata": { + "type": "object", + "properties": { + "historyReceivers": { + "type": "array", + "items": { + "type": "string" + } + }, + "messageCount": { + "type": "integer" + }, + "nonHistoryReceivers": { + "type": "array", + "items": { + "type": "string" + } + }, + "oldestMessageTimestampInBundle": { + "type": "integer" + }, + "oldestMessageTimestampInWindow": { + "type": "integer" + } + } + }, + "waE2E.MessageHistoryNotice": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "messageHistoryMetadata": { + "$ref": "#/definitions/waE2E.MessageHistoryMetadata" + } + } + }, + "waE2E.Money": { + "type": "object", + "properties": { + "currencyCode": { + "type": "string" + }, + "offset": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "waE2E.NewsletterAdminInviteMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "inviteExpiration": { + "type": "integer" + }, + "newsletterJID": { + "type": "string" + }, + "newsletterName": { + "type": "string" + } + } + }, + "waE2E.NewsletterFollowerInviteMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "newsletterJID": { + "type": "string" + }, + "newsletterName": { + "type": "string" + } + } + }, + "waE2E.OrderMessage": { + "type": "object", + "properties": { + "catalogType": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "itemCount": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "messageVersion": { + "type": "integer" + }, + "orderID": { + "type": "string" + }, + "orderRequestMessageID": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "orderTitle": { + "type": "string" + }, + "sellerJID": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/waE2E.OrderMessage_OrderStatus" + }, + "surface": { + "$ref": "#/definitions/waE2E.OrderMessage_OrderSurface" + }, + "thumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "token": { + "type": "string" + }, + "totalAmount1000": { + "type": "integer" + }, + "totalCurrencyCode": { + "type": "string" + } + } + }, + "waE2E.OrderMessage_OrderStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "OrderMessage_INQUIRY", + "OrderMessage_ACCEPTED", + "OrderMessage_DECLINED" + ] + }, + "waE2E.OrderMessage_OrderSurface": { + "type": "integer", + "format": "int32", + "enum": [ + 1 + ], + "x-enum-varnames": [ + "OrderMessage_CATALOG" + ] + }, + "waE2E.PaymentBackground": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "fileLength": { + "type": "integer" + }, + "height": { + "type": "integer" + }, + "mediaData": { + "$ref": "#/definitions/waE2E.PaymentBackground_MediaData" + }, + "mimetype": { + "type": "string" + }, + "placeholderArgb": { + "type": "integer" + }, + "subtextArgb": { + "type": "integer" + }, + "textArgb": { + "type": "integer" + }, + "type": { + "$ref": "#/definitions/waE2E.PaymentBackground_Type" + }, + "width": { + "type": "integer" + } + } + }, + "waE2E.PaymentBackground_MediaData": { + "type": "object", + "properties": { + "directPath": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + } + } + }, + "waE2E.PaymentBackground_Type": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "PaymentBackground_UNKNOWN", + "PaymentBackground_DEFAULT" + ] + }, + "waE2E.PaymentExtendedMetadata": { + "type": "object", + "properties": { + "platform": { + "type": "string" + }, + "type": { + "type": "integer" + } + } + }, + "waE2E.PaymentInviteMessage": { + "type": "object", + "properties": { + "expiryTimestamp": { + "type": "integer" + }, + "incentiveEligible": { + "type": "boolean" + }, + "inviteType": { + "$ref": "#/definitions/waE2E.PaymentInviteMessage_InviteType" + }, + "referralID": { + "type": "string" + }, + "serviceType": { + "$ref": "#/definitions/waE2E.PaymentInviteMessage_ServiceType" + } + } + }, + "waE2E.PaymentInviteMessage_InviteType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "PaymentInviteMessage_DEFAULT", + "PaymentInviteMessage_MAPPER" + ] + }, + "waE2E.PaymentInviteMessage_ServiceType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "PaymentInviteMessage_UNKNOWN", + "PaymentInviteMessage_FBPAY", + "PaymentInviteMessage_NOVI", + "PaymentInviteMessage_UPI" + ] + }, + "waE2E.PaymentLinkMetadata": { + "type": "object", + "properties": { + "button": { + "$ref": "#/definitions/waE2E.PaymentLinkMetadata_PaymentLinkButton" + }, + "header": { + "$ref": "#/definitions/waE2E.PaymentLinkMetadata_PaymentLinkHeader" + }, + "provider": { + "$ref": "#/definitions/waE2E.PaymentLinkMetadata_PaymentLinkProvider" + } + } + }, + "waE2E.PaymentLinkMetadata_PaymentLinkButton": { + "type": "object", + "properties": { + "displayText": { + "type": "string" + } + } + }, + "waE2E.PaymentLinkMetadata_PaymentLinkHeader": { + "type": "object", + "properties": { + "headerType": { + "$ref": "#/definitions/waE2E.PaymentLinkMetadata_PaymentLinkHeader_PaymentLinkHeaderType" + } + } + }, + "waE2E.PaymentLinkMetadata_PaymentLinkHeader_PaymentLinkHeaderType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "PaymentLinkMetadata_PaymentLinkHeader_LINK_PREVIEW", + "PaymentLinkMetadata_PaymentLinkHeader_ORDER" + ] + }, + "waE2E.PaymentLinkMetadata_PaymentLinkProvider": { + "type": "object", + "properties": { + "paramsJSON": { + "type": "string" + } + } + }, + "waE2E.PaymentReminderMessage": { + "type": "object", + "properties": { + "amount": { + "$ref": "#/definitions/waE2E.Money" + }, + "description": { + "type": "string" + }, + "frequency": { + "$ref": "#/definitions/waE2E.PaymentReminderMessage_ReminderFrequency" + }, + "instanceID": { + "type": "string" + }, + "payeeJID": { + "type": "string" + }, + "payeeVpa": { + "type": "string" + }, + "payerJID": { + "type": "string" + }, + "reminderID": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/waE2E.PaymentReminderMessage_ReminderStatus" + } + } + }, + "waE2E.PaymentReminderMessage_ReminderFrequency": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "x-enum-varnames": [ + "PaymentReminderMessage_REMINDER_FREQUENCY_UNKNOWN", + "PaymentReminderMessage_WEEKLY", + "PaymentReminderMessage_BI_WEEKLY", + "PaymentReminderMessage_MONTHLY", + "PaymentReminderMessage_QUARTERLY" + ] + }, + "waE2E.PaymentReminderMessage_ReminderStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "PaymentReminderMessage_REMINDER_STATUS_UNKNOWN", + "PaymentReminderMessage_ACTIVE", + "PaymentReminderMessage_CANCELLED_BY_CREATOR", + "PaymentReminderMessage_STOPPED_BY_RECEIVER", + "PaymentReminderMessage_EXPIRED", + "PaymentReminderMessage_PAID" + ] + }, + "waE2E.PeerDataOperationRequestMessage": { + "type": "object", + "properties": { + "bizBroadcastInsightsContactListRequest": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_BizBroadcastInsightsContactListRequest" + }, + "bizBroadcastInsightsRefreshRequest": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_BizBroadcastInsightsRefreshRequest" + }, + "companionCanonicalUserNonceFetchRequest": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_CompanionCanonicalUserNonceFetchRequest" + }, + "fullHistorySyncOnDemandRequest": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_FullHistorySyncOnDemandRequest" + }, + "galaxyFlowAction": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_GalaxyFlowAction" + }, + "historySyncChunkRetryRequest": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_HistorySyncChunkRetryRequest" + }, + "historySyncOnDemandRequest": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_HistorySyncOnDemandRequest" + }, + "peerDataOperationRequestType": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestType" + }, + "placeholderMessageResendRequest": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_PlaceholderMessageResendRequest" + } + }, + "requestStickerReupload": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_RequestStickerReupload" + } + }, + "requestURLPreview": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_RequestUrlPreview" + } + }, + "syncdCollectionFatalRecoveryRequest": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_SyncDCollectionFatalRecoveryRequest" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_BizBroadcastInsightsContactListRequest": { + "type": "object", + "properties": { + "campaignID": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_BizBroadcastInsightsRefreshRequest": { + "type": "object", + "properties": { + "campaignID": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_CompanionCanonicalUserNonceFetchRequest": { + "type": "object", + "properties": { + "registrationTraceID": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_FullHistorySyncOnDemandRequest": { + "type": "object", + "properties": { + "fullHistorySyncOnDemandConfig": { + "$ref": "#/definitions/waE2E.FullHistorySyncOnDemandConfig" + }, + "historySyncConfig": { + "$ref": "#/definitions/waCompanionReg.DeviceProps_HistorySyncConfig" + }, + "requestMetadata": { + "$ref": "#/definitions/waE2E.FullHistorySyncOnDemandRequestMetadata" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_GalaxyFlowAction": { + "type": "object", + "properties": { + "agmID": { + "type": "string" + }, + "flowID": { + "type": "string" + }, + "galaxyFlowDownloadRequestID": { + "type": "string" + }, + "stanzaID": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_GalaxyFlowAction_GalaxyFlowActionType" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_GalaxyFlowAction_GalaxyFlowActionType": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2 + ], + "x-enum-varnames": [ + "PeerDataOperationRequestMessage_GalaxyFlowAction_NOTIFY_LAUNCH", + "PeerDataOperationRequestMessage_GalaxyFlowAction_DOWNLOAD_RESPONSES" + ] + }, + "waE2E.PeerDataOperationRequestMessage_HistorySyncChunkRetryRequest": { + "type": "object", + "properties": { + "chunkNotificationID": { + "type": "string" + }, + "chunkOrder": { + "type": "integer" + }, + "regenerateChunk": { + "type": "boolean" + }, + "syncType": { + "$ref": "#/definitions/waE2E.HistorySyncType" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_HistorySyncOnDemandRequest": { + "type": "object", + "properties": { + "accountLid": { + "type": "string" + }, + "chatJID": { + "type": "string" + }, + "oldestMsgFromMe": { + "type": "boolean" + }, + "oldestMsgID": { + "type": "string" + }, + "oldestMsgTimestampMS": { + "type": "integer" + }, + "onDemandMsgCount": { + "type": "integer" + }, + "supportInlineResponse": { + "type": "boolean" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_PlaceholderMessageResendRequest": { + "type": "object", + "properties": { + "messageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_RequestStickerReupload": { + "type": "object", + "properties": { + "fileSHA256": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_RequestUrlPreview": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "includeHqThumbnail": { + "type": "boolean" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_SyncDCollectionFatalRecoveryRequest": { + "type": "object", + "properties": { + "collectionName": { + "type": "string" + }, + "timestamp": { + "type": "integer" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage": { + "type": "object", + "properties": { + "peerDataOperationRequestType": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestType" + }, + "peerDataOperationResult": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult" + } + }, + "stanzaID": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult": { + "type": "object", + "properties": { + "bizBroadcastInsightsContactListResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_BizBroadcastInsightsContactListResponse" + }, + "companionCanonicalUserNonceFetchRequestResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CompanionCanonicalUserNonceFetchResponse" + }, + "companionMetaNonceFetchRequestResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CompanionMetaNonceFetchResponse" + }, + "flowResponsesCsvBundle": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FlowResponsesCsvBundle" + }, + "fullHistorySyncOnDemandRequestResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FullHistorySyncOnDemandRequestResponse" + }, + "historySyncChunkRetryResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_HistorySyncChunkRetryResponse" + }, + "linkPreviewResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse" + }, + "mediaUploadResult": { + "$ref": "#/definitions/waMmsRetry.MediaRetryNotification_ResultType" + }, + "placeholderMessageResendResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_PlaceholderMessageResendResponse" + }, + "stickerMessage": { + "$ref": "#/definitions/waE2E.StickerMessage" + }, + "syncdSnapshotFatalRecoveryResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_SyncDSnapshotFatalRecoveryResponse" + }, + "waffleNonceFetchRequestResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_WaffleNonceFetchResponse" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_BizBroadcastInsightsContactListResponse": { + "type": "object", + "properties": { + "campaignID": { + "type": "string" + }, + "contacts": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_BizBroadcastInsightsContactState" + } + }, + "timestampMS": { + "type": "integer" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_BizBroadcastInsightsContactState": { + "type": "object", + "properties": { + "contactJID": { + "type": "string" + }, + "state": { + "$ref": "#/definitions/waE2E.InsightDeliveryState" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CompanionCanonicalUserNonceFetchResponse": { + "type": "object", + "properties": { + "forceRefresh": { + "type": "boolean" + }, + "nonce": { + "type": "string" + }, + "waFbid": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CompanionMetaNonceFetchResponse": { + "type": "object", + "properties": { + "nonce": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FlowResponsesCsvBundle": { + "type": "object", + "properties": { + "directPath": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileName": { + "type": "string" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "flowID": { + "type": "string" + }, + "galaxyFlowDownloadRequestID": { + "type": "string" + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "mimetype": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FullHistorySyncOnDemandRequestResponse": { + "type": "object", + "properties": { + "requestMetadata": { + "$ref": "#/definitions/waE2E.FullHistorySyncOnDemandRequestMetadata" + }, + "responseCode": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FullHistorySyncOnDemandResponseCode" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FullHistorySyncOnDemandResponseCode": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "x-enum-varnames": [ + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_REQUEST_SUCCESS", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_REQUEST_TIME_EXPIRED", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_DECLINED_SHARING_HISTORY", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_GENERIC_ERROR", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_ERROR_REQUEST_ON_NON_SMB_PRIMARY", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_ERROR_HOSTED_DEVICE_NOT_CONNECTED", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_ERROR_HOSTED_DEVICE_LOGIN_TIME_NOT_SET", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_ERROR_MULTI_PROVIDER_NOT_CONFIGURED" + ] + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_HistorySyncChunkRetryResponse": { + "type": "object", + "properties": { + "canRecover": { + "type": "boolean" + }, + "chunkOrder": { + "type": "integer" + }, + "requestID": { + "type": "string" + }, + "responseCode": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_HistorySyncChunkRetryResponseCode" + }, + "syncType": { + "$ref": "#/definitions/waE2E.HistorySyncType" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_HistorySyncChunkRetryResponseCode": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "x-enum-varnames": [ + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_GENERATION_ERROR", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CHUNK_CONSUMED", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_TIMEOUT", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_SESSION_EXHAUSTED", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CHUNK_EXHAUSTED", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_DUPLICATED_REQUEST" + ] + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hqThumbnail": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse_LinkPreviewHighQualityThumbnail" + }, + "matchText": { + "type": "string" + }, + "previewMetadata": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse_PaymentLinkPreviewMetadata" + }, + "previewType": { + "type": "string" + }, + "thumbData": { + "type": "array", + "items": { + "type": "integer" + } + }, + "title": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse_LinkPreviewHighQualityThumbnail": { + "type": "object", + "properties": { + "directPath": { + "type": "string" + }, + "encThumbHash": { + "type": "string" + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestampMS": { + "type": "integer" + }, + "thumbHash": { + "type": "string" + }, + "thumbHeight": { + "type": "integer" + }, + "thumbWidth": { + "type": "integer" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse_PaymentLinkPreviewMetadata": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "isBusinessVerified": { + "type": "boolean" + }, + "offset": { + "type": "string" + }, + "providerName": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_PlaceholderMessageResendResponse": { + "type": "object", + "properties": { + "webMessageInfoBytes": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_SyncDSnapshotFatalRecoveryResponse": { + "type": "object", + "properties": { + "collectionSnapshot": { + "type": "array", + "items": { + "type": "integer" + } + }, + "isCompressed": { + "type": "boolean" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_WaffleNonceFetchResponse": { + "type": "object", + "properties": { + "nonce": { + "type": "string" + }, + "waEntFbid": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13 + ], + "x-enum-varnames": [ + "PeerDataOperationRequestType_UPLOAD_STICKER", + "PeerDataOperationRequestType_SEND_RECENT_STICKER_BOOTSTRAP", + "PeerDataOperationRequestType_GENERATE_LINK_PREVIEW", + "PeerDataOperationRequestType_HISTORY_SYNC_ON_DEMAND", + "PeerDataOperationRequestType_PLACEHOLDER_MESSAGE_RESEND", + "PeerDataOperationRequestType_WAFFLE_LINKING_NONCE_FETCH", + "PeerDataOperationRequestType_FULL_HISTORY_SYNC_ON_DEMAND", + "PeerDataOperationRequestType_COMPANION_META_NONCE_FETCH", + "PeerDataOperationRequestType_COMPANION_SYNCD_SNAPSHOT_FATAL_RECOVERY", + "PeerDataOperationRequestType_COMPANION_CANONICAL_USER_NONCE_FETCH", + "PeerDataOperationRequestType_HISTORY_SYNC_CHUNK_RETRY", + "PeerDataOperationRequestType_GALAXY_FLOW_ACTION", + "PeerDataOperationRequestType_BUSINESS_BROADCAST_INSIGHTS_DELIVERED_TO", + "PeerDataOperationRequestType_BUSINESS_BROADCAST_INSIGHTS_REFRESH" + ] + }, + "waE2E.PinInChatMessage": { + "type": "object", + "properties": { + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "senderTimestampMS": { + "type": "integer" + }, + "type": { + "$ref": "#/definitions/waE2E.PinInChatMessage_Type" + } + } + }, + "waE2E.PinInChatMessage_Type": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "PinInChatMessage_UNKNOWN_TYPE", + "PinInChatMessage_PIN_FOR_ALL", + "PinInChatMessage_UNPIN_FOR_ALL" + ] + }, + "waE2E.PlaceholderMessage": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/waE2E.PlaceholderMessage_PlaceholderType" + } + } + }, + "waE2E.PlaceholderMessage_PlaceholderType": { + "type": "integer", + "format": "int32", + "enum": [ + 0 + ], + "x-enum-varnames": [ + "PlaceholderMessage_MASK_LINKED_DEVICES" + ] + }, + "waE2E.Point": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "xDeprecated": { + "type": "integer" + }, + "y": { + "type": "number" + }, + "yDeprecated": { + "type": "integer" + } + } + }, + "waE2E.PollAddOptionMessage": { + "type": "object", + "properties": { + "addOption": { + "$ref": "#/definitions/waE2E.PollCreationMessage_Option" + }, + "metadata": { + "$ref": "#/definitions/waE2E.PollUpdateMessageMetadata" + }, + "pollCreationMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.PollContentType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "PollContentType_UNKNOWN_POLL_CONTENT_TYPE", + "PollContentType_TEXT", + "PollContentType_IMAGE" + ] + }, + "waE2E.PollCreationMessage": { + "type": "object", + "properties": { + "allowAddOption": { + "type": "boolean" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "correctAnswer": { + "$ref": "#/definitions/waE2E.PollCreationMessage_Option" + }, + "encKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "endTime": { + "type": "integer" + }, + "hideParticipantName": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.PollCreationMessage_Option" + } + }, + "pollContentType": { + "$ref": "#/definitions/waE2E.PollContentType" + }, + "pollType": { + "$ref": "#/definitions/waE2E.PollType" + }, + "selectableOptionsCount": { + "type": "integer" + } + } + }, + "waE2E.PollCreationMessage_Option": { + "type": "object", + "properties": { + "optionHash": { + "type": "string" + }, + "optionName": { + "type": "string" + } + } + }, + "waE2E.PollEncValue": { + "type": "object", + "properties": { + "encIV": { + "type": "array", + "items": { + "type": "integer" + } + }, + "encPayload": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waE2E.PollResultSnapshotMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "name": { + "type": "string" + }, + "pollType": { + "$ref": "#/definitions/waE2E.PollType" + }, + "pollVotes": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.PollResultSnapshotMessage_PollVote" + } + } + } + }, + "waE2E.PollResultSnapshotMessage_PollVote": { + "type": "object", + "properties": { + "optionName": { + "type": "string" + }, + "optionVoteCount": { + "type": "integer" + } + } + }, + "waE2E.PollType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "PollType_POLL", + "PollType_QUIZ" + ] + }, + "waE2E.PollUpdateMessage": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/waE2E.PollUpdateMessageMetadata" + }, + "pollCreationMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "senderTimestampMS": { + "type": "integer" + }, + "vote": { + "$ref": "#/definitions/waE2E.PollEncValue" + } + } + }, + "waE2E.PollUpdateMessageMetadata": { + "type": "object", + "properties": { + "lastEditStanzaID": { + "type": "string" + }, + "pollNameHash": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waE2E.ProcessedVideo": { + "type": "object", + "properties": { + "bitrate": { + "type": "integer" + }, + "capabilities": { + "type": "array", + "items": { + "type": "string" + } + }, + "directPath": { + "type": "string" + }, + "fileLength": { + "type": "integer" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "height": { + "type": "integer" + }, + "quality": { + "$ref": "#/definitions/waE2E.ProcessedVideo_VideoQuality" + }, + "width": { + "type": "integer" + } + } + }, + "waE2E.ProcessedVideo_VideoQuality": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "ProcessedVideo_UNDEFINED", + "ProcessedVideo_LOW", + "ProcessedVideo_MID", + "ProcessedVideo_HIGH" + ] + }, + "waE2E.ProductMessage": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "businessOwnerJID": { + "type": "string" + }, + "catalog": { + "$ref": "#/definitions/waE2E.ProductMessage_CatalogSnapshot" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "footer": { + "type": "string" + }, + "product": { + "$ref": "#/definitions/waE2E.ProductMessage_ProductSnapshot" + } + } + }, + "waE2E.ProductMessage_CatalogSnapshot": { + "type": "object", + "properties": { + "catalogImage": { + "$ref": "#/definitions/waE2E.ImageMessage" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ProductMessage_ProductSnapshot": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "currencyCode": { + "type": "string" + }, + "description": { + "type": "string" + }, + "firstImageID": { + "type": "string" + }, + "priceAmount1000": { + "type": "integer" + }, + "productID": { + "type": "string" + }, + "productImage": { + "$ref": "#/definitions/waE2E.ImageMessage" + }, + "productImageCount": { + "type": "integer" + }, + "retailerID": { + "type": "string" + }, + "salePriceAmount1000": { + "type": "integer" + }, + "signedURL": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ProtocolMessage": { + "type": "object", + "properties": { + "afterReadDuration": { + "type": "integer" + }, + "aiMediaCollectionMessage": { + "$ref": "#/definitions/waAICommon.AIMediaCollectionMessage" + }, + "aiMetadataOperation": { + "$ref": "#/definitions/waAICommon.AIMetadataOperation" + }, + "aiPsiMetadata": { + "type": "array", + "items": { + "type": "integer" + } + }, + "aiQueryFanout": { + "$ref": "#/definitions/waE2E.AIQueryFanout" + }, + "appStateFatalExceptionNotification": { + "$ref": "#/definitions/waE2E.AppStateFatalExceptionNotification" + }, + "appStateSyncKeyRequest": { + "$ref": "#/definitions/waE2E.AppStateSyncKeyRequest" + }, + "appStateSyncKeyShare": { + "$ref": "#/definitions/waE2E.AppStateSyncKeyShare" + }, + "botFeedbackMessage": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage" + }, + "chatThemeSetting": { + "$ref": "#/definitions/waE2E.ChatThemeSetting" + }, + "cloudApiThreadControlNotification": { + "$ref": "#/definitions/waE2E.CloudAPIThreadControlNotification" + }, + "disappearingMode": { + "$ref": "#/definitions/waE2E.DisappearingMode" + }, + "editedMessage": { + "$ref": "#/definitions/waE2E.Message" + }, + "ephemeralExpiration": { + "type": "integer" + }, + "ephemeralSettingTimestamp": { + "type": "integer" + }, + "historySyncNotification": { + "$ref": "#/definitions/waE2E.HistorySyncNotification" + }, + "initialSecurityNotificationSettingSync": { + "$ref": "#/definitions/waE2E.InitialSecurityNotificationSettingSync" + }, + "invokerJID": { + "type": "string" + }, + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "lidMigrationMappingSyncMessage": { + "$ref": "#/definitions/waE2E.LIDMigrationMappingSyncMessage" + }, + "limitSharing": { + "$ref": "#/definitions/waCommon.LimitSharing" + }, + "mediaNotifyMessage": { + "$ref": "#/definitions/waE2E.MediaNotifyMessage" + }, + "memberLabel": { + "$ref": "#/definitions/waE2E.MemberLabel" + }, + "peerDataOperationRequestMessage": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage" + }, + "peerDataOperationRequestResponseMessage": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage" + }, + "requestWelcomeMessageMetadata": { + "$ref": "#/definitions/waE2E.RequestWelcomeMessageMetadata" + }, + "timestampMS": { + "type": "integer" + }, + "type": { + "$ref": "#/definitions/waE2E.ProtocolMessage_Type" + } + } + }, + "waE2E.ProtocolMessage_Type": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 14, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 34, + 35 + ], + "x-enum-varnames": [ + "ProtocolMessage_REVOKE", + "ProtocolMessage_EPHEMERAL_SETTING", + "ProtocolMessage_EPHEMERAL_SYNC_RESPONSE", + "ProtocolMessage_HISTORY_SYNC_NOTIFICATION", + "ProtocolMessage_APP_STATE_SYNC_KEY_SHARE", + "ProtocolMessage_APP_STATE_SYNC_KEY_REQUEST", + "ProtocolMessage_MSG_FANOUT_BACKFILL_REQUEST", + "ProtocolMessage_INITIAL_SECURITY_NOTIFICATION_SETTING_SYNC", + "ProtocolMessage_APP_STATE_FATAL_EXCEPTION_NOTIFICATION", + "ProtocolMessage_SHARE_PHONE_NUMBER", + "ProtocolMessage_MESSAGE_EDIT", + "ProtocolMessage_PEER_DATA_OPERATION_REQUEST_MESSAGE", + "ProtocolMessage_PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE", + "ProtocolMessage_REQUEST_WELCOME_MESSAGE", + "ProtocolMessage_BOT_FEEDBACK_MESSAGE", + "ProtocolMessage_MEDIA_NOTIFY_MESSAGE", + "ProtocolMessage_CLOUD_API_THREAD_CONTROL_NOTIFICATION", + "ProtocolMessage_LID_MIGRATION_MAPPING_SYNC", + "ProtocolMessage_REMINDER_MESSAGE", + "ProtocolMessage_BOT_MEMU_ONBOARDING_MESSAGE", + "ProtocolMessage_STATUS_MENTION_MESSAGE", + "ProtocolMessage_STOP_GENERATION_MESSAGE", + "ProtocolMessage_LIMIT_SHARING", + "ProtocolMessage_AI_PSI_METADATA", + "ProtocolMessage_AI_QUERY_FANOUT", + "ProtocolMessage_GROUP_MEMBER_LABEL_CHANGE", + "ProtocolMessage_AI_MEDIA_COLLECTION_MESSAGE", + "ProtocolMessage_MESSAGE_UNSCHEDULE", + "ProtocolMessage_CHAT_THEME_SETTING", + "ProtocolMessage_AI_METADATA_OPERATION" + ] + }, + "waE2E.QuestionResponseMessage": { + "type": "object", + "properties": { + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "text": { + "type": "string" + } + } + }, + "waE2E.ReactionMessage": { + "type": "object", + "properties": { + "groupingKey": { + "type": "string" + }, + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "senderTimestampMS": { + "type": "integer" + }, + "text": { + "type": "string" + } + } + }, + "waE2E.RequestPaymentMessage": { + "type": "object", + "properties": { + "amount": { + "$ref": "#/definitions/waE2E.Money" + }, + "amount1000": { + "type": "integer" + }, + "background": { + "$ref": "#/definitions/waE2E.PaymentBackground" + }, + "currencyCodeIso4217": { + "type": "string" + }, + "expiryTimestamp": { + "type": "integer" + }, + "noteMessage": { + "$ref": "#/definitions/waE2E.Message" + }, + "requestFrom": { + "type": "string" + } + } + }, + "waE2E.RequestPhoneNumberMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + } + } + }, + "waE2E.RequestWelcomeMessageMetadata": { + "type": "object", + "properties": { + "botAgentMetadata": { + "$ref": "#/definitions/waAICommon.BotAgentMetadata" + }, + "localChatState": { + "$ref": "#/definitions/waE2E.RequestWelcomeMessageMetadata_LocalChatState" + }, + "welcomeTrigger": { + "$ref": "#/definitions/waE2E.RequestWelcomeMessageMetadata_WelcomeTrigger" + } + } + }, + "waE2E.RequestWelcomeMessageMetadata_LocalChatState": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "RequestWelcomeMessageMetadata_EMPTY", + "RequestWelcomeMessageMetadata_NON_EMPTY" + ] + }, + "waE2E.RequestWelcomeMessageMetadata_WelcomeTrigger": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "RequestWelcomeMessageMetadata_CHAT_OPEN", + "RequestWelcomeMessageMetadata_COMPANION_PAIRING" + ] + }, + "waE2E.RootSecretDistributeMessage": { + "type": "object", + "properties": { + "chatJID": { + "type": "string" + } + } + }, + "waE2E.ScheduledCallCreationMessage": { + "type": "object", + "properties": { + "callType": { + "$ref": "#/definitions/waE2E.ScheduledCallCreationMessage_CallType" + }, + "scheduledTimestampMS": { + "type": "integer" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ScheduledCallCreationMessage_CallType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "ScheduledCallCreationMessage_UNKNOWN", + "ScheduledCallCreationMessage_VOICE", + "ScheduledCallCreationMessage_VIDEO" + ] + }, + "waE2E.ScheduledCallEditMessage": { + "type": "object", + "properties": { + "editType": { + "$ref": "#/definitions/waE2E.ScheduledCallEditMessage_EditType" + }, + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.ScheduledCallEditMessage_EditType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ScheduledCallEditMessage_UNKNOWN", + "ScheduledCallEditMessage_CANCEL" + ] + }, + "waE2E.SecretEncryptedMessage": { + "type": "object", + "properties": { + "encIV": { + "type": "array", + "items": { + "type": "integer" + } + }, + "encPayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "remoteKeyID": { + "type": "string" + }, + "secretEncType": { + "$ref": "#/definitions/waE2E.SecretEncryptedMessage_SecretEncType" + }, + "targetMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.SecretEncryptedMessage_SecretEncType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "SecretEncryptedMessage_UNKNOWN", + "SecretEncryptedMessage_EVENT_EDIT", + "SecretEncryptedMessage_MESSAGE_EDIT", + "SecretEncryptedMessage_MESSAGE_SCHEDULE", + "SecretEncryptedMessage_POLL_EDIT", + "SecretEncryptedMessage_POLL_ADD_OPTION" + ] + }, + "waE2E.SendPaymentMessage": { + "type": "object", + "properties": { + "background": { + "$ref": "#/definitions/waE2E.PaymentBackground" + }, + "noteMessage": { + "$ref": "#/definitions/waE2E.Message" + }, + "requestMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "transactionData": { + "type": "string" + } + } + }, + "waE2E.SenderKeyDistributionMessage": { + "type": "object", + "properties": { + "axolotlSenderKeyDistributionMessage": { + "type": "array", + "items": { + "type": "integer" + } + }, + "groupID": { + "type": "string" + } + } + }, + "waE2E.SplitPaymentMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "createdAtMS": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "participants": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.SplitPaymentParticipant" + } + }, + "requesterJID": { + "type": "string" + }, + "splitID": { + "type": "string" + }, + "totalAmount": { + "$ref": "#/definitions/waE2E.Money" + } + } + }, + "waE2E.SplitPaymentParticipant": { + "type": "object", + "properties": { + "JID": { + "type": "string" + }, + "amount": { + "$ref": "#/definitions/waE2E.Money" + }, + "status": { + "$ref": "#/definitions/waE2E.SplitPaymentParticipant_SplitPaymentStatus" + } + } + }, + "waE2E.SplitPaymentParticipant_SplitPaymentStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "SplitPaymentParticipant_PENDING", + "SplitPaymentParticipant_PAID" + ] + }, + "waE2E.StatusNotificationMessage": { + "type": "object", + "properties": { + "originalMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "responseMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "type": { + "$ref": "#/definitions/waE2E.StatusNotificationMessage_StatusNotificationType" + } + } + }, + "waE2E.StatusNotificationMessage_StatusNotificationType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "StatusNotificationMessage_UNKNOWN", + "StatusNotificationMessage_STATUS_ADD_YOURS", + "StatusNotificationMessage_STATUS_RESHARE", + "StatusNotificationMessage_STATUS_QUESTION_ANSWER_RESHARE" + ] + }, + "waE2E.StatusQuestionAnswerMessage": { + "type": "object", + "properties": { + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "text": { + "type": "string" + } + } + }, + "waE2E.StatusQuotedMessage": { + "type": "object", + "properties": { + "originalStatusID": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "text": { + "type": "string" + }, + "thumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "type": { + "$ref": "#/definitions/waE2E.StatusQuotedMessage_StatusQuotedMessageType" + } + } + }, + "waE2E.StatusQuotedMessage_StatusQuotedMessageType": { + "type": "integer", + "format": "int32", + "enum": [ + 1 + ], + "x-enum-varnames": [ + "StatusQuotedMessage_QUESTION_ANSWER" + ] + }, + "waE2E.StatusStickerInteractionMessage": { + "type": "object", + "properties": { + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "stickerKey": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/waE2E.StatusStickerInteractionMessage_StatusStickerType" + } + } + }, + "waE2E.StatusStickerInteractionMessage_StatusStickerType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "StatusStickerInteractionMessage_UNKNOWN", + "StatusStickerInteractionMessage_REACTION" + ] + }, + "waE2E.StickerMessage": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "accessibilityLabel": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "directPath": { + "type": "string" + }, + "emojis": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "firstFrameLength": { + "type": "integer" + }, + "firstFrameSidecar": { + "type": "array", + "items": { + "type": "integer" + } + }, + "height": { + "type": "integer" + }, + "isAiSticker": { + "type": "boolean" + }, + "isAnimated": { + "type": "boolean" + }, + "isAvatar": { + "type": "boolean" + }, + "isLottie": { + "type": "boolean" + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "mimetype": { + "type": "string" + }, + "pngThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "premium": { + "type": "integer" + }, + "stickerSentTS": { + "type": "integer" + }, + "width": { + "type": "integer" + } + } + }, + "waE2E.StickerPackMessage": { + "type": "object", + "properties": { + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "directPath": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "imageDataHash": { + "type": "string" + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "packDescription": { + "type": "string" + }, + "publisher": { + "type": "string" + }, + "stickerPackID": { + "type": "string" + }, + "stickerPackOrigin": { + "$ref": "#/definitions/waE2E.StickerPackMessage_StickerPackOrigin" + }, + "stickerPackSize": { + "type": "integer" + }, + "stickers": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.StickerPackMessage_Sticker" + } + }, + "thumbnailDirectPath": { + "type": "string" + }, + "thumbnailEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailHeight": { + "type": "integer" + }, + "thumbnailSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailWidth": { + "type": "integer" + }, + "trayIconFileName": { + "type": "string" + } + } + }, + "waE2E.StickerPackMessage_Sticker": { + "type": "object", + "properties": { + "accessibilityLabel": { + "type": "string" + }, + "emojis": { + "type": "array", + "items": { + "type": "string" + } + }, + "fileName": { + "type": "string" + }, + "isAnimated": { + "type": "boolean" + }, + "isLottie": { + "type": "boolean" + }, + "mimetype": { + "type": "string" + }, + "premium": { + "type": "integer" + } + } + }, + "waE2E.StickerPackMessage_StickerPackOrigin": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "StickerPackMessage_FIRST_PARTY", + "StickerPackMessage_THIRD_PARTY", + "StickerPackMessage_USER_CREATED" + ] + }, + "waE2E.StickerSyncRMRMessage": { + "type": "object", + "properties": { + "filehash": { + "type": "array", + "items": { + "type": "string" + } + }, + "requestTimestamp": { + "type": "integer" + }, + "rmrSource": { + "type": "string" + } + } + }, + "waE2E.TemplateButtonReplyMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "selectedCarouselCardIndex": { + "type": "integer" + }, + "selectedDisplayText": { + "type": "string" + }, + "selectedID": { + "type": "string" + }, + "selectedIndex": { + "type": "integer" + } + } + }, + "waE2E.TemplateMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "format": { + "description": "Types that are valid to be assigned to Format:\n\n\t*TemplateMessage_FourRowTemplate_\n\t*TemplateMessage_HydratedFourRowTemplate_\n\t*TemplateMessage_InteractiveMessageTemplate" + }, + "hydratedTemplate": { + "$ref": "#/definitions/waE2E.TemplateMessage_HydratedFourRowTemplate" + }, + "templateID": { + "type": "string" + } + } + }, + "waE2E.TemplateMessage_HydratedFourRowTemplate": { + "type": "object", + "properties": { + "hydratedButtons": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.HydratedTemplateButton" + } + }, + "hydratedContentText": { + "type": "string" + }, + "hydratedFooterText": { + "type": "string" + }, + "maskLinkedDevices": { + "type": "boolean" + }, + "templateID": { + "type": "string" + }, + "title": { + "description": "Types that are valid to be assigned to Title:\n\n\t*TemplateMessage_HydratedFourRowTemplate_DocumentMessage\n\t*TemplateMessage_HydratedFourRowTemplate_HydratedTitleText\n\t*TemplateMessage_HydratedFourRowTemplate_ImageMessage\n\t*TemplateMessage_HydratedFourRowTemplate_VideoMessage\n\t*TemplateMessage_HydratedFourRowTemplate_LocationMessage" + } + } + }, + "waE2E.ThreadID": { + "type": "object", + "properties": { + "threadKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "threadType": { + "$ref": "#/definitions/waE2E.ThreadID_ThreadType" + } + } + }, + "waE2E.ThreadID_ThreadType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "ThreadID_UNKNOWN", + "ThreadID_VIEW_REPLIES", + "ThreadID_AI_THREAD" + ] + }, + "waE2E.URLMetadata": { + "type": "object", + "properties": { + "fbExperimentID": { + "type": "integer" + } + } + }, + "waE2E.UrlTrackingMap": { + "type": "object", + "properties": { + "urlTrackingMapElements": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.UrlTrackingMap_UrlTrackingMapElement" + } + } + } + }, + "waE2E.UrlTrackingMap_UrlTrackingMapElement": { + "type": "object", + "properties": { + "cardIndex": { + "type": "integer" + }, + "consentedUsersURL": { + "type": "string" + }, + "originalURL": { + "type": "string" + }, + "unconsentedUsersURL": { + "type": "string" + } + } + }, + "waE2E.VideoEndCard": { + "type": "object", + "properties": { + "caption": { + "type": "string" + }, + "profilePictureURL": { + "type": "string" + }, + "thumbnailImageURL": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "waE2E.VideoMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "URL": { + "type": "string" + }, + "accessibilityLabel": { + "type": "string" + }, + "annotations": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.InteractiveAnnotation" + } + }, + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "directPath": { + "type": "string" + }, + "externalShareFullVideoDurationInSeconds": { + "type": "integer" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "gifAttribution": { + "$ref": "#/definitions/waE2E.VideoMessage_Attribution" + }, + "gifPlayback": { + "type": "boolean" + }, + "height": { + "type": "integer" + }, + "interactiveAnnotations": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.InteractiveAnnotation" + } + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "metadataURL": { + "type": "string" + }, + "mimetype": { + "type": "string" + }, + "motionPhotoPresentationOffsetMS": { + "type": "integer" + }, + "processedVideos": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ProcessedVideo" + } + }, + "seconds": { + "type": "integer" + }, + "staticURL": { + "type": "string" + }, + "streamingSidecar": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailDirectPath": { + "type": "string" + }, + "thumbnailEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "videoSourceType": { + "$ref": "#/definitions/waE2E.VideoMessage_VideoSourceType" + }, + "viewOnce": { + "type": "boolean" + }, + "width": { + "type": "integer" + } + } + }, + "waE2E.VideoMessage_Attribution": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "VideoMessage_NONE", + "VideoMessage_GIPHY", + "VideoMessage_TENOR", + "VideoMessage_KLIPY" + ] + }, + "waE2E.VideoMessage_VideoSourceType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "VideoMessage_USER_VIDEO", + "VideoMessage_AI_GENERATED" + ] + }, + "waE2E.WebLinkRenderConfig": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "WebLinkRenderConfig_WEBVIEW", + "WebLinkRenderConfig_SYSTEM" + ] + }, + "waMmsRetry.MediaRetryNotification_ResultType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "MediaRetryNotification_GENERAL_ERROR", + "MediaRetryNotification_SUCCESS", + "MediaRetryNotification_NOT_FOUND", + "MediaRetryNotification_DECRYPTION_ERROR" + ] + }, + "waStatusAttributions.StatusAttribution": { + "type": "object", + "properties": { + "actionURL": { + "type": "string" + }, + "attributionData": { + "description": "Types that are valid to be assigned to AttributionData:\n\n\t*StatusAttribution_StatusReshare_\n\t*StatusAttribution_ExternalShare_\n\t*StatusAttribution_Music_\n\t*StatusAttribution_GroupStatus_\n\t*StatusAttribution_RlAttribution\n\t*StatusAttribution_AiCreatedAttribution_" + }, + "type": { + "$ref": "#/definitions/waStatusAttributions.StatusAttribution_Type" + } + } + }, + "waStatusAttributions.StatusAttribution_Type": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + ], + "x-enum-varnames": [ + "StatusAttribution_UNKNOWN", + "StatusAttribution_RESHARE", + "StatusAttribution_EXTERNAL_SHARE", + "StatusAttribution_MUSIC", + "StatusAttribution_STATUS_MENTION", + "StatusAttribution_GROUP_STATUS", + "StatusAttribution_RL_ATTRIBUTION", + "StatusAttribution_AI_CREATED", + "StatusAttribution_LAYOUTS", + "StatusAttribution_NEWSLETTER_STATUS", + "StatusAttribution_STATUS_CLOSE_SHARING" + ] + }, + "waVnameCert.LocalizedName": { + "type": "object", + "properties": { + "lc": { + "type": "string" + }, + "lg": { + "type": "string" + }, + "verifiedName": { + "type": "string" + } + } + }, + "waVnameCert.VerifiedNameCertificate": { + "type": "object", + "properties": { + "details": { + "type": "array", + "items": { + "type": "integer" + } + }, + "serverSignature": { + "type": "array", + "items": { + "type": "integer" + } + }, + "signature": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waVnameCert.VerifiedNameCertificate_Details": { + "type": "object", + "properties": { + "issueTime": { + "type": "integer" + }, + "issuer": { + "type": "string" + }, + "localizedNames": { + "type": "array", + "items": { + "$ref": "#/definitions/waVnameCert.LocalizedName" + } + }, + "serial": { + "type": "integer" + }, + "verifiedName": { + "type": "string" + } + } + }, + "whatsmeow.ParticipantChange": { + "type": "string", + "enum": [ + "add", + "remove", + "promote", + "demote" + ], + "x-enum-varnames": [ + "ParticipantChangeAdd", + "ParticipantChangeRemove", + "ParticipantChangePromote", + "ParticipantChangeDemote" + ] + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "AgentDeck Whatsapp Service", + Description: "AgentDeck Whatsapp Service - whatsmeow", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/whatsapp-service/docs/swagger.json b/whatsapp-service/docs/swagger.json new file mode 100644 index 0000000000000000000000000000000000000000..40f179b141ff46fa95291195928487bf34b3c696 --- /dev/null +++ b/whatsapp-service/docs/swagger.json @@ -0,0 +1,14303 @@ +{ + "swagger": "2.0", + "info": { + "description": "AgentDeck Whatsapp Service - whatsmeow", + "title": "AgentDeck Whatsapp Service", + "contact": {}, + "version": "1.0" + }, + "paths": { + "/call/reject": { + "post": { + "description": "Reject call", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Call" + ], + "summary": "Reject call", + "parameters": [ + { + "description": "Call data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_call_service.RejectCallStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/chat/archive": { + "post": { + "description": "Archive a chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chat" + ], + "summary": "Archive a chat", + "parameters": [ + { + "description": "Chat", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/chat/history-sync": { + "post": { + "description": "HistorySyncRequest a chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chat" + ], + "summary": "HistorySyncRequest a chat", + "parameters": [ + { + "description": "Chat", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_chat_service.HistorySyncRequestStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/chat/mute": { + "post": { + "description": "Mute a chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chat" + ], + "summary": "Mute a chat", + "parameters": [ + { + "description": "Chat", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/chat/pin": { + "post": { + "description": "Pin a chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chat" + ], + "summary": "Pin a chat", + "parameters": [ + { + "description": "Chat", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/chat/unarchive": { + "post": { + "description": "Unarchive a chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chat" + ], + "summary": "Unarchive a chat", + "parameters": [ + { + "description": "Chat", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/chat/unmute": { + "post": { + "description": "Unmute a chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chat" + ], + "summary": "Unmute a chat", + "parameters": [ + { + "description": "Chat", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/chat/unpin": { + "post": { + "description": "Unpin a chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chat" + ], + "summary": "Unpin a chat", + "parameters": [ + { + "description": "Chat", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/community/add": { + "post": { + "description": "Add participant to community", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Community" + ], + "summary": "Add participant to community", + "parameters": [ + { + "description": "Participant data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_community_service.AddParticipantStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/community/create": { + "post": { + "description": "Create community", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Community" + ], + "summary": "Create community", + "parameters": [ + { + "description": "Community data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_community_service.CreateCommunityStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/community/remove": { + "post": { + "description": "Remove participant from community", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Community" + ], + "summary": "Remove participant from community", + "parameters": [ + { + "description": "Participant data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_community_service.AddParticipantStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/create": { + "post": { + "description": "Create group", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Create group", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.CreateGroupStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/description": { + "post": { + "description": "Set group description", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Set group description", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.SetGroupDescriptionStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/info": { + "post": { + "description": "Get group info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Get group info", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.GetGroupInfoStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/invitelink": { + "post": { + "description": "Get group invite link", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Get group invite link", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.GetGroupInviteLinkStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/join": { + "post": { + "description": "Join group link", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Join group link", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.JoinGroupStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/leave": { + "post": { + "description": "Leave group", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Leave group", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.LeaveGroupStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/list": { + "get": { + "description": "List groups", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "List groups", + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/myall": { + "get": { + "description": "Get my groups", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Get my groups", + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/name": { + "post": { + "description": "Set group name", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Set group name", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.SetGroupNameStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/participant": { + "post": { + "description": "Update participant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Update participant", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.AddParticipantStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/photo": { + "post": { + "description": "Set group photo", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Set group photo", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.SetGroupPhotoStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/group/settings": { + "post": { + "description": "Update group settings (announcement, not_announcement, locked, unlocked, approval_on, approval_off, admin_add, all_member_add)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Update group settings", + "parameters": [ + { + "description": "Group data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_group_service.UpdateGroupSettingsStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/all": { + "get": { + "description": "Get all instances", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Get all instances", + "responses": { + "200": { + "description": "All instances", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/connect": { + "post": { + "description": "Connect to instance with the provided data", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Connect to instance", + "parameters": [ + { + "description": "Instance data", + "name": "instance", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_service.ConnectStruct" + } + } + ], + "responses": { + "200": { + "description": "Instance connected successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/create": { + "post": { + "description": "Creates a new instance with the provided data including optional advanced settings", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Create a new instance", + "parameters": [ + { + "description": "Instance data with optional advanced settings", + "name": "instance", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_service.CreateStruct" + } + } + ], + "responses": { + "200": { + "description": "Instance created successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/delete/{instanceId}": { + "delete": { + "description": "Delete instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Delete instance", + "parameters": [ + { + "type": "string", + "description": "Instance Id", + "name": "instanceId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Instance deleted successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/disconnect": { + "post": { + "description": "Disconnect from instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Disconnect from instance", + "responses": { + "200": { + "description": "Instance disconnected successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/forcereconnect/{instanceId}": { + "post": { + "description": "Force reconnect", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Force reconnect", + "parameters": [ + { + "type": "string", + "description": "Instance Id", + "name": "instanceId", + "in": "path", + "required": true + }, + { + "description": "Instance data", + "name": "instance", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_service.ForceReconnectStruct" + } + } + ], + "responses": { + "200": { + "description": "Instance force reconnected successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/info/{instanceId}": { + "get": { + "description": "Get instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Get instance", + "parameters": [ + { + "type": "string", + "description": "Instance Id", + "name": "instanceId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Instance", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/logout": { + "delete": { + "description": "Logout from instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Logout from instance", + "responses": { + "200": { + "description": "Instance logged out successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/logs/{instanceId}": { + "get": { + "description": "Returns log entries for an instance, filterable by date range, level and limit", + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Get instance logs", + "parameters": [ + { + "type": "string", + "description": "Instance Id", + "name": "instanceId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Start date (YYYY-MM-DD, defaults to 7 days ago)", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "End date (YYYY-MM-DD, defaults to now)", + "name": "end_date", + "in": "query" + }, + { + "type": "string", + "description": "Log level filter", + "name": "level", + "in": "query" + }, + { + "type": "integer", + "description": "Max number of entries", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Logs", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/pair": { + "post": { + "description": "Request pairing code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Request pairing code", + "parameters": [ + { + "description": "Instance data", + "name": "instance", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_service.PairStruct" + } + } + ], + "responses": { + "200": { + "description": "Pairing code", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/proxy/{instanceId}": { + "post": { + "description": "Set proxy configuration for an instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Set proxy configuration", + "parameters": [ + { + "type": "string", + "description": "Instance id", + "name": "instanceId", + "in": "path", + "required": true + }, + { + "description": "Proxy configuration", + "name": "proxy", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_service.SetProxyStruct" + } + } + ], + "responses": { + "200": { + "description": "Proxy set successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + }, + "delete": { + "description": "Delete proxy", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Delete proxy", + "parameters": [ + { + "type": "string", + "description": "Instance id", + "name": "instanceId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Proxy deleted successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/qr": { + "get": { + "description": "Get instance QR code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Get instance QR code", + "responses": { + "200": { + "description": "Instance QR code", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/reconnect": { + "post": { + "description": "Reconnect to instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Reconnect to instance", + "responses": { + "200": { + "description": "Instance reconnected successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/status": { + "get": { + "description": "Get instance status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Get instance status", + "responses": { + "200": { + "description": "Instance status", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/instance/{instanceId}/advanced-settings": { + "get": { + "description": "Get advanced settings for a specific instance", + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Get advanced settings", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "instanceId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Advanced settings retrieved successfully", + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_model.AdvancedSettings" + } + }, + "400": { + "description": "Invalid instance ID", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "404": { + "description": "Instance not found", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + }, + "put": { + "description": "Update advanced settings for a specific instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instance" + ], + "summary": "Update advanced settings", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "instanceId", + "in": "path", + "required": true + }, + { + "description": "Advanced settings data", + "name": "settings", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_model.AdvancedSettings" + } + } + ], + "responses": { + "200": { + "description": "Advanced settings updated successfully", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Invalid request data", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "404": { + "description": "Instance not found", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/label/chat": { + "post": { + "description": "Add label to chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Label" + ], + "summary": "Add label to chat", + "parameters": [ + { + "description": "Label data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_label_service.ChatLabelStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/label/edit": { + "post": { + "description": "Edit label", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Label" + ], + "summary": "Edit label", + "parameters": [ + { + "description": "Label data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_label_service.EditLabelStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/label/list": { + "get": { + "description": "Get all labels", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Label" + ], + "summary": "Get all labels", + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/label/message": { + "post": { + "description": "Add label to message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Label" + ], + "summary": "Add label to message", + "parameters": [ + { + "description": "Label data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_label_service.MessageLabelStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/license/activate": { + "get": { + "description": "Exchanges an authorization code (from the registration callback) for an api_key and persists it. Provide the code via the query string.", + "produces": [ + "application/json" + ], + "tags": [ + "License" + ], + "summary": "Activate license", + "parameters": [ + { + "type": "string", + "description": "Authorization code from the registration callback", + "name": "code", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Activation result", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Missing code parameter", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/license/register": { + "get": { + "description": "Checks the GLOBAL_API_KEY with the licensing server. If not yet registered, initiates registration and returns a register_url. Accepts an optional redirect_uri for the post-registration redirect.", + "produces": [ + "application/json" + ], + "tags": [ + "License" + ], + "summary": "Register / get registration URL", + "parameters": [ + { + "type": "string", + "description": "Post-registration redirect URI", + "name": "redirect_uri", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Registration state (status/message or register_url)", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/license/status": { + "get": { + "description": "Returns whether the instance license is active, along with the instance id and a masked api key.", + "produces": [ + "application/json" + ], + "tags": [ + "License" + ], + "summary": "Get license status", + "responses": { + "200": { + "description": "License status ({status, instance_id, api_key?})", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/delete": { + "post": { + "description": "Delete a message for everyone", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "Delete a message for everyone", + "parameters": [ + { + "description": "Delete a message for everyone", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.MessageStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/downloadmedia": { + "post": { + "description": "Download the media content of a message (image, video, audio or document)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "Download media", + "parameters": [ + { + "description": "Download media", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.DownloadMediaStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/edit": { + "post": { + "description": "Edit a message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "Edit a message", + "parameters": [ + { + "description": "Edit a message", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.EditMessageStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/markplayed": { + "post": { + "description": "Mark an audio message as played", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "Mark an audio message as played", + "parameters": [ + { + "description": "Mark an audio message as played", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.MarkPlayedStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/markread": { + "post": { + "description": "Mark a message as read", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "Mark a message as read", + "parameters": [ + { + "description": "Mark a message as read", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.MarkReadStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/presence": { + "post": { + "description": "Set chat presence", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "Set chat presence", + "parameters": [ + { + "description": "Set chat presence", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.ChatPresenceStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/react": { + "post": { + "description": "React to a message with support for fromMe field and participant field for group messages", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "React a message", + "parameters": [ + { + "description": "React to a message with fromMe and participant fields", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.ReactStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/message/status": { + "post": { + "description": "Get message status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Message" + ], + "summary": "Get message status", + "parameters": [ + { + "description": "Get message status", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_message_service.MessageStatusStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/newsletter/create": { + "post": { + "description": "Create newsletter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Newsletter" + ], + "summary": "Create newsletter", + "parameters": [ + { + "description": "Newsletter data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_newsletter_service.CreateNewsletterStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/newsletter/info": { + "post": { + "description": "Get newsletter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Newsletter" + ], + "summary": "Get newsletter", + "parameters": [ + { + "description": "Newsletter data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/newsletter/link": { + "post": { + "description": "Get newsletter invite", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Newsletter" + ], + "summary": "Get newsletter invite", + "parameters": [ + { + "description": "Newsletter data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterInviteStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/newsletter/list": { + "get": { + "description": "List newsletters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Newsletter" + ], + "summary": "List newsletters", + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/newsletter/messages": { + "post": { + "description": "Get newsletter messages", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Newsletter" + ], + "summary": "Get newsletter messages", + "parameters": [ + { + "description": "Newsletter data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterMessagesStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/newsletter/subscribe": { + "post": { + "description": "Subscribe newsletter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Newsletter" + ], + "summary": "Subscribe newsletter", + "parameters": [ + { + "description": "Newsletter data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/passkey-ceremony/{token}": { + "get": { + "description": "Returns the current WebAuthn passkey-pairing ceremony state for a token. PUBLIC endpoint (no apikey) — access is gated by the opaque short-lived ceremony token. Polled by the AgentDeck Passkey Helper browser extension.", + "produces": [ + "application/json" + ], + "tags": [ + "Passkey" + ], + "summary": "Get passkey ceremony state", + "parameters": [ + { + "type": "string", + "description": "Ceremony token", + "name": "token", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Ceremony state ({stage, skipHandoffUX, publicKey?, code?, error?})", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "token is required", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "404": { + "description": "ceremony not found or expired", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "503": { + "description": "passkey ceremony unavailable", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/passkey-ceremony/{token}/confirm": { + "post": { + "description": "Finishes the passkey pairing after the user verified the confirmation code. PUBLIC endpoint (no apikey) — gated by the ceremony token.", + "produces": [ + "application/json" + ], + "tags": [ + "Passkey" + ], + "summary": "Confirm passkey pairing", + "parameters": [ + { + "type": "string", + "description": "Ceremony token", + "name": "token", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "token is required", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "404": { + "description": "ceremony not found or expired", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "503": { + "description": "passkey ceremony unavailable", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/passkey-ceremony/{token}/response": { + "post": { + "description": "Receives the WebAuthn assertion produced by the browser extension and forwards it to WhatsApp. PUBLIC endpoint (no apikey) — gated by the ceremony token. Body is the WebAuthnResponse shape (id, rawId, type, response{clientDataJSON, authenticatorData, signature, userHandle?}), base64url-unpadded.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Passkey" + ], + "summary": "Submit passkey WebAuthn response", + "parameters": [ + { + "type": "string", + "description": "Ceremony token", + "name": "token", + "in": "path", + "required": true + }, + { + "description": "WebAuthn assertion", + "name": "response", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/types.WebAuthnResponse" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "token is required / invalid body", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "404": { + "description": "ceremony not found or expired", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "503": { + "description": "passkey ceremony unavailable", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/polls/{pollMessageId}/results": { + "get": { + "description": "Retorna todos os votos de uma enquete específica", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Polls" + ], + "summary": "Get poll results", + "parameters": [ + { + "type": "string", + "description": "ID da mensagem da enquete", + "name": "pollMessageId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_poll_model.PollResults" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/button": { + "post": { + "description": "Send an interactive message with buttons. Each button has a `type`: `reply`, `copy`, `url`, `call` or `pix`.\n\nCombination rules enforced by the server:\n- Up to 3 `reply` buttons per message.\n- `reply` buttons cannot be mixed with any other type.\n- `pix` button must be sent ALONE (no other button in the same message).\n\nWhatsApp client rendering quirks (NOT enforced by the server, but verified in the field):\n- WhatsApp Web: only `reply`-only messages (up to 3) OR CTAs grouped together (`copy` + `url` + `call`) render correctly.\n- Do NOT mix `reply` with CTA buttons (`copy`/`url`/`call`) — the message will not appear on WhatsApp Web.\n\nRequired body fields: `number`, `title`, `description`, `footer`, `buttons`.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a button message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.ButtonStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/carousel": { + "post": { + "description": "Send an interactive carousel (multiple swipeable cards). Each card carries its own image or video, body and optional buttons.\n\nCard button `type` accepted values (case-insensitive, uppercased internally): `REPLY` (default), `URL`, `CALL`, `COPY`.\nThe `PIX` button type is NOT supported in carousel cards — use `/send/button` for PIX.\n\nIMPORTANT — `CarouselButtonStruct` is different from the flat button used in `/send/button`:\n- URL button: put the link in the `id` field (NOT in a `url` field).\n- CALL button: put the phone number in the `id` field (NOT in a `phoneNumber` field).\n- COPY button: put the code to be copied in `copyCode`.\n- REPLY button: put the payload/callback ID in `id`.\n\nPer-card combination rules (NOT enforced by the server, but verified in the field):\n- Same WhatsApp Web quirk as `/send/button`: avoid mixing REPLY with CTA buttons (URL/CALL/COPY) in the same card — mixed sets do not render on Web.\n- Stick to either \"only REPLY\" or \"only CTAs grouped together\" per card.\n\nRequired body fields: `number`, `cards` (at least one). Each card requires `header` + `body`.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a carousel message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/contact": { + "post": { + "description": "Send a contact message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a contact message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.ContactStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/link": { + "post": { + "description": "Send a link message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a link message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.LinkStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/list": { + "post": { + "description": "Send an interactive list message (single-select) rendered as a tappable menu.\n\nRequired body fields: `number`, `title`, `description`, `footerText`, `buttonText`, `sections`.\nEach section must contain one or more `rows`. When `rowId` is omitted, the server generates a fallback ID.\nWhen `buttonText` is empty, the server falls back to \"Ver Menu\".\n\nUses legacy `ListMessage` format (no ViewOnceMessage wrapper) so it renders on iOS, Android and WhatsApp Web.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a list message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.ListStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/location": { + "post": { + "description": "Send a location message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a location message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.LocationStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/media": { + "post": { + "description": "Send a media message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a media message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.MediaStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/poll": { + "post": { + "description": "Send a poll message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a poll message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.PollStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/status/media": { + "post": { + "description": "Send an image or video status to status@broadcast. Supports JSON (URL) or multipart/form-data (file upload)", + "consumes": [ + "application/json", + " multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a WhatsApp media status (image/video)", + "parameters": [ + { + "type": "string", + "description": "Media type: image or video", + "name": "type", + "in": "formData", + "required": true + }, + { + "type": "file", + "description": "Media file (for multipart upload)", + "name": "file", + "in": "formData" + }, + { + "type": "string", + "description": "Media URL (for JSON upload)", + "name": "url", + "in": "formData" + }, + { + "type": "string", + "description": "Caption for the media", + "name": "caption", + "in": "formData" + }, + { + "type": "string", + "description": "Custom message ID", + "name": "id", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/status/text": { + "post": { + "description": "Send a WhatsApp text status to status@broadcast", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a WhatsApp text status", + "parameters": [ + { + "description": "Status text data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.StatusTextStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/sticker": { + "post": { + "description": "Send a sticker message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a sticker message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.StickerStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/send/text": { + "post": { + "description": "Send a text message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Send Message" + ], + "summary": "Send a text message", + "parameters": [ + { + "description": "Message data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.TextStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/unlabel/chat": { + "post": { + "description": "Remove label from chat", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Label" + ], + "summary": "Remove label from chat", + "parameters": [ + { + "description": "Label data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_label_service.ChatLabelStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/unlabel/message": { + "post": { + "description": "Remove label from message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Label" + ], + "summary": "Remove label from message", + "parameters": [ + { + "description": "Label data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_label_service.MessageLabelStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/avatar": { + "post": { + "description": "Get a user's avatar", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get a user's avatar", + "parameters": [ + { + "description": "Avatar data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.GetAvatarStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/block": { + "post": { + "description": "Block a contact", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Block a contact", + "parameters": [ + { + "description": "Block data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.BlockStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/blocklist": { + "get": { + "description": "Get a user's block list", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get a user's block list", + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/check": { + "post": { + "description": "Check a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Check a user", + "parameters": [ + { + "description": "User data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.CheckUserStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/contacts": { + "get": { + "description": "Get a user's contacts", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get a user's contacts", + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/info": { + "post": { + "description": "Get a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get a user", + "parameters": [ + { + "description": "User data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.CheckUserStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/privacy": { + "get": { + "description": "Get a user's privacy settings", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get a user's privacy settings", + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + }, + "post": { + "description": "Set a user's privacy settings", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Set a user's privacy settings", + "parameters": [ + { + "description": "Privacy data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.PrivacyStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/profileName": { + "post": { + "description": "Set a user's profile name", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Set a user's profile name", + "parameters": [ + { + "description": "Profile name data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.SetProfilePictureStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/profilePicture": { + "post": { + "description": "Set a user's profile picture", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Set a user's profile picture", + "parameters": [ + { + "description": "Profile picture data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.SetProfilePictureStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/profileStatus": { + "post": { + "description": "Set a user's profile status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Set a user's profile status", + "parameters": [ + { + "description": "Profile status data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.SetProfilePictureStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/user/unblock": { + "post": { + "description": "Unblock a contact", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Unblock a contact", + "parameters": [ + { + "description": "Block data", + "name": "message", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_user_service.BlockStruct" + } + } + ], + "responses": { + "200": { + "description": "success", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Error on validation", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + } + }, + "definitions": { + "gin.H": { + "type": "object", + "additionalProperties": {} + }, + "agentdeck-whatsapp-service_pkg_call_service.RejectCallStruct": { + "type": "object", + "properties": { + "callCreator": { + "$ref": "#/definitions/types.JID" + }, + "callId": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_chat_service.BodyStruct": { + "type": "object", + "properties": { + "chat": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_chat_service.HistorySyncRequestStruct": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "messageInfo": { + "$ref": "#/definitions/types.MessageInfo" + } + } + }, + "agentdeck-whatsapp-service_pkg_community_service.AddParticipantStruct": { + "type": "object", + "properties": { + "communityJid": { + "type": "string" + }, + "groupJid": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "agentdeck-whatsapp-service_pkg_community_service.CreateCommunityStruct": { + "type": "object", + "properties": { + "communityName": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.AddParticipantStruct": { + "type": "object", + "properties": { + "action": { + "$ref": "#/definitions/whatsmeow.ParticipantChange" + }, + "groupJid": { + "$ref": "#/definitions/types.JID" + }, + "participants": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.CreateGroupStruct": { + "type": "object", + "properties": { + "groupName": { + "type": "string" + }, + "participants": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.GetGroupInfoStruct": { + "type": "object", + "properties": { + "groupJid": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.GetGroupInviteLinkStruct": { + "type": "object", + "properties": { + "groupJid": { + "type": "string" + }, + "reset": { + "type": "boolean" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.JoinGroupStruct": { + "type": "object", + "properties": { + "code": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.LeaveGroupStruct": { + "type": "object", + "properties": { + "groupJid": { + "$ref": "#/definitions/types.JID" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.SetGroupDescriptionStruct": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "groupJid": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.SetGroupNameStruct": { + "type": "object", + "properties": { + "groupJid": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.SetGroupPhotoStruct": { + "type": "object", + "properties": { + "groupJid": { + "type": "string" + }, + "image": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_group_service.UpdateGroupSettingsStruct": { + "type": "object", + "properties": { + "action": { + "description": "announcement, not_announcement, locked, unlocked", + "type": "string" + }, + "groupJid": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_instance_model.AdvancedSettings": { + "type": "object", + "properties": { + "alwaysOnline": { + "type": "boolean" + }, + "ignoreGroups": { + "type": "boolean" + }, + "ignoreStatus": { + "type": "boolean" + }, + "msgRejectCall": { + "type": "string" + }, + "readMessages": { + "type": "boolean" + }, + "rejectCall": { + "type": "boolean" + } + } + }, + "agentdeck-whatsapp-service_pkg_instance_service.ConnectStruct": { + "type": "object", + "properties": { + "immediate": { + "type": "boolean" + }, + "natsEnable": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "rabbitmqEnable": { + "type": "string" + }, + "subscribe": { + "type": "array", + "items": { + "type": "string" + } + }, + "webhookUrl": { + "type": "string" + }, + "websocketEnable": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_instance_service.CreateStruct": { + "type": "object", + "properties": { + "advancedSettings": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_model.AdvancedSettings" + }, + "instanceId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "proxy": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_instance_service.ProxyConfig" + }, + "token": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_instance_service.ForceReconnectStruct": { + "type": "object", + "properties": { + "number": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_instance_service.PairStruct": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "subscribe": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "agentdeck-whatsapp-service_pkg_instance_service.ProxyConfig": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "password": { + "type": "string" + }, + "port": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_instance_service.SetProxyStruct": { + "type": "object", + "required": [ + "host", + "port" + ], + "properties": { + "host": { + "type": "string" + }, + "password": { + "type": "string" + }, + "port": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_label_service.ChatLabelStruct": { + "type": "object", + "properties": { + "jid": { + "type": "string" + }, + "labelId": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_label_service.EditLabelStruct": { + "type": "object", + "properties": { + "color": { + "type": "integer" + }, + "deleted": { + "type": "boolean" + }, + "labelId": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_label_service.MessageLabelStruct": { + "type": "object", + "properties": { + "jid": { + "type": "string" + }, + "labelId": { + "type": "string" + }, + "messageId": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.ChatPresenceStruct": { + "type": "object", + "properties": { + "delay": { + "description": "Delay, in milliseconds, keeps the \"composing\"/\"recording\" indicator alive\nfor the given duration (re-sending it periodically) and then sends \"paused\".\nOnly applies when State is \"composing\". 0 = single fire (legacy behaviour).", + "type": "integer" + }, + "isAudio": { + "type": "boolean" + }, + "number": { + "type": "string" + }, + "state": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.DownloadMediaStruct": { + "type": "object", + "properties": { + "message": { + "$ref": "#/definitions/waE2E.Message" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.EditMessageStruct": { + "type": "object", + "properties": { + "chat": { + "type": "string" + }, + "message": { + "type": "string" + }, + "messageId": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.MarkPlayedStruct": { + "type": "object", + "properties": { + "id": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.MarkReadStruct": { + "type": "object", + "properties": { + "id": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.MessageStatusStruct": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.MessageStruct": { + "type": "object", + "properties": { + "chat": { + "type": "string" + }, + "messageId": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_message_service.ReactStruct": { + "type": "object", + "properties": { + "fromMe": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "number": { + "type": "string" + }, + "participant": { + "type": "string" + }, + "reaction": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_newsletter_service.CreateNewsletterStruct": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterInviteStruct": { + "type": "object", + "properties": { + "key": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterMessagesStruct": { + "type": "object", + "properties": { + "before_id": { + "type": "integer" + }, + "count": { + "type": "integer" + }, + "jid": { + "$ref": "#/definitions/types.JID" + } + } + }, + "agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterStruct": { + "type": "object", + "properties": { + "jid": { + "$ref": "#/definitions/types.JID" + } + } + }, + "agentdeck-whatsapp-service_pkg_poll_model.PollResults": { + "type": "object", + "properties": { + "optionCounts": { + "description": "hash -\u003e count", + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "pollChatJid": { + "type": "string" + }, + "pollMessageId": { + "type": "string" + }, + "totalVotes": { + "type": "integer" + }, + "voters": { + "type": "array", + "items": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_poll_model.VoterInfo" + } + }, + "votes": { + "type": "array", + "items": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_poll_model.PollVote" + } + } + } + }, + "agentdeck-whatsapp-service_pkg_poll_model.PollVote": { + "type": "object", + "properties": { + "companyId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "instanceId": { + "type": "string" + }, + "pollChatJid": { + "type": "string" + }, + "pollMessageId": { + "type": "string" + }, + "receivedAt": { + "type": "string" + }, + "selectedOptions": { + "description": "SHA-256 hashes", + "type": "array", + "items": { + "type": "string" + } + }, + "voteMessageId": { + "type": "string" + }, + "votedAt": { + "type": "string" + }, + "voterJid": { + "type": "string" + }, + "voterName": { + "type": "string" + }, + "voterPhone": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_poll_model.VoterInfo": { + "type": "object", + "properties": { + "jid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "selectedOptions": { + "type": "array", + "items": { + "type": "string" + } + }, + "votedAt": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.Button": { + "type": "object", + "properties": { + "copyCode": { + "description": "Code placed in the clipboard when type=copy.", + "type": "string", + "example": "PROMO2026" + }, + "currency": { + "description": "ISO currency code for type=pix (e.g. BRL).", + "type": "string", + "example": "BRL" + }, + "displayText": { + "description": "Label rendered inside the button (reply / copy / url / call). Ignored for pix.", + "type": "string", + "example": "Quero saber mais" + }, + "id": { + "description": "Callback payload for `reply` or code-to-copy internal id for `copy`.", + "type": "string", + "example": "btn_info" + }, + "key": { + "description": "Pix key value matching the keyType.", + "type": "string", + "example": "12345678900" + }, + "keyType": { + "description": "Pix key type. One of: phone, email, cpf, cnpj, random.", + "type": "string", + "enum": [ + "phone", + "email", + "cpf", + "cnpj", + "random" + ], + "example": "cpf" + }, + "name": { + "description": "Merchant display name shown on the Pix sheet.", + "type": "string", + "example": "Minha Loja" + }, + "phoneNumber": { + "description": "Destination phone number (E.164) when type=call.", + "type": "string", + "example": "+5582988898565" + }, + "type": { + "description": "Button kind. One of: reply, copy, url, call, pix.", + "type": "string", + "enum": [ + "reply", + "copy", + "url", + "call", + "pix" + ], + "example": "reply" + }, + "url": { + "description": "Target URL when type=url.", + "type": "string", + "example": "https://agentdeck.ai" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.ButtonStruct": { + "type": "object", + "properties": { + "buttons": { + "description": "Buttons array. See combination rules on the parent type description.", + "type": "array", + "items": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.Button" + } + }, + "delay": { + "description": "Typing delay (milliseconds) applied before sending the message.", + "type": "integer", + "example": 1200 + }, + "description": { + "description": "Body description text (required).", + "type": "string", + "example": "Confira as condicoes abaixo" + }, + "footer": { + "description": "Footer text (required).", + "type": "string", + "example": "AgentDeck Whatsapp Service" + }, + "formatJid": { + "description": "If false, skips automatic formatting/validation of `number` into a JID.", + "type": "boolean" + }, + "imageUrl": { + "description": "Optional image URL used as header for reply-only buttons.", + "type": "string" + }, + "mentionAll": { + "description": "Mention every participant (groups only).", + "type": "boolean" + }, + "mentionedJid": { + "description": "JIDs to mention inside the body text.", + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "description": "Destination phone number.", + "type": "string", + "example": "5582988898565" + }, + "quoted": { + "description": "Quoted (reply-to) context.", + "allOf": [ + { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + } + ] + }, + "title": { + "description": "Header title (required).", + "type": "string", + "example": "Oferta especial" + }, + "videoUrl": { + "description": "Optional video URL used as header for reply-only buttons.", + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselButtonStruct": { + "type": "object", + "properties": { + "copyCode": { + "description": "Code placed in the clipboard when type=COPY.", + "type": "string", + "example": "PROMO2026" + }, + "displayText": { + "description": "Label rendered inside the button.", + "type": "string", + "example": "Quero saber mais" + }, + "id": { + "description": "Context-dependent: REPLY payload, URL target (type=URL) or phone number (type=CALL).", + "type": "string", + "example": "card1_info" + }, + "type": { + "description": "Button kind (case-insensitive). One of: REPLY (default), URL, CALL, COPY.", + "type": "string", + "enum": [ + "REPLY", + "URL", + "CALL", + "COPY", + "reply", + "url", + "call", + "copy" + ], + "example": "REPLY" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardBodyStruct": { + "type": "object", + "properties": { + "text": { + "description": "Main text of the card.", + "type": "string", + "example": "Card 1 - Oferta especial" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardHeaderStruct": { + "type": "object", + "properties": { + "imageUrl": { + "description": "Public URL to an image. Downloaded, uploaded to WhatsApp servers and used as card media.", + "type": "string", + "example": "https://picsum.photos/seed/card1/600/400" + }, + "subtitle": { + "description": "Optional subtitle rendered below the title.", + "type": "string", + "example": "Somente hoje" + }, + "title": { + "description": "Optional visible title above the media.", + "type": "string", + "example": "Oferta do dia" + }, + "videoUrl": { + "description": "Public URL to a video. Used only when `imageUrl` is empty.", + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardStruct": { + "type": "object", + "properties": { + "body": { + "description": "Card body text (required).", + "allOf": [ + { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardBodyStruct" + } + ] + }, + "buttons": { + "description": "Buttons shown on the card. See CarouselButtonStruct for combination rules.", + "type": "array", + "items": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselButtonStruct" + } + }, + "footer": { + "description": "Optional footer rendered under the body.", + "type": "string", + "example": "Por tempo limitado" + }, + "header": { + "description": "Card header (media + title/subtitle).", + "allOf": [ + { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardHeaderStruct" + } + ] + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselStruct": { + "type": "object", + "properties": { + "body": { + "description": "Optional message body shown above the cards.", + "type": "string", + "example": "Confira nossas novidades!" + }, + "cards": { + "description": "Cards displayed in order. At least one card is required.", + "type": "array", + "items": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardStruct" + } + }, + "delay": { + "description": "Typing delay (milliseconds) applied before sending the message.", + "type": "integer", + "example": 1200 + }, + "footer": { + "description": "Optional message footer shown below the cards.", + "type": "string", + "example": "AgentDeck Whatsapp Service" + }, + "formatJid": { + "description": "If false, skips automatic formatting/validation of `number` into a JID.", + "type": "boolean" + }, + "number": { + "description": "Destination phone number.", + "type": "string", + "example": "5582988898565" + }, + "quoted": { + "description": "Quoted (reply-to) context.", + "allOf": [ + { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + } + ] + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.ContactStruct": { + "type": "object", + "properties": { + "delay": { + "type": "integer" + }, + "formatJid": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "mentionAll": { + "type": "boolean" + }, + "mentionedJid": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + }, + "quoted": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + }, + "vcard": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_utils.VCardStruct" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.LinkStruct": { + "type": "object", + "properties": { + "delay": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "formatJid": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "imgUrl": { + "type": "string" + }, + "mentionAll": { + "type": "boolean" + }, + "mentionedJid": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + }, + "quoted": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + }, + "text": { + "type": "string" + }, + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.ListStruct": { + "type": "object", + "properties": { + "buttonText": { + "description": "Label of the button that opens the list. Defaults to \"Ver Menu\" when empty.", + "type": "string", + "example": "Abrir cardapio" + }, + "delay": { + "description": "Typing delay (milliseconds) applied before sending the message.", + "type": "integer", + "example": 1200 + }, + "description": { + "description": "Body description text (required).", + "type": "string", + "example": "Escolha o plano ideal para voce" + }, + "footerText": { + "description": "Footer text (required).", + "type": "string", + "example": "AgentDeck Whatsapp Service" + }, + "formatJid": { + "description": "If false, skips automatic formatting/validation of `number` into a JID.", + "type": "boolean" + }, + "mentionAll": { + "description": "Mention every participant (groups only).", + "type": "boolean" + }, + "mentionedJid": { + "description": "JIDs to mention inside the body text.", + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "description": "Destination phone number.", + "type": "string", + "example": "5582988898565" + }, + "quoted": { + "description": "Quoted (reply-to) context.", + "allOf": [ + { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + } + ] + }, + "sections": { + "description": "Sections with rows. At least one section with one row is required.", + "type": "array", + "items": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.Section" + } + }, + "title": { + "description": "Header title (required).", + "type": "string", + "example": "Nossos planos" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.LocationStruct": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "delay": { + "type": "integer" + }, + "formatJid": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, + "mentionAll": { + "type": "boolean" + }, + "mentionedJid": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "number": { + "type": "string" + }, + "quoted": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.MediaStruct": { + "type": "object", + "properties": { + "caption": { + "type": "string" + }, + "delay": { + "type": "integer" + }, + "filename": { + "type": "string" + }, + "formatJid": { + "type": "boolean" + }, + "forwardingScore": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "mentionAll": { + "type": "boolean" + }, + "mentionedJid": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + }, + "quoted": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.PollStruct": { + "type": "object", + "properties": { + "delay": { + "type": "integer" + }, + "formatJid": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "maxAnswer": { + "type": "integer" + }, + "mentionAll": { + "type": "boolean" + }, + "mentionedJid": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "string" + } + }, + "question": { + "type": "string" + }, + "quoted": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct": { + "type": "object", + "properties": { + "messageId": { + "type": "string" + }, + "participant": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.Row": { + "type": "object", + "properties": { + "description": { + "description": "Optional secondary line below the title.", + "type": "string", + "example": "R$ 29,90/mes" + }, + "rowId": { + "description": "Callback payload returned when the user taps the row. Auto-generated if empty.", + "type": "string", + "example": "plan_basic" + }, + "title": { + "description": "Row main label.", + "type": "string", + "example": "Plano Basico" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.Section": { + "type": "object", + "properties": { + "rows": { + "description": "Rows inside this section.", + "type": "array", + "items": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.Row" + } + }, + "title": { + "description": "Section heading (optional; rendered as a group separator).", + "type": "string", + "example": "Planos" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.StatusTextStruct": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "text": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.StickerStruct": { + "type": "object", + "properties": { + "delay": { + "type": "integer" + }, + "formatJid": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "mentionAll": { + "type": "boolean" + }, + "mentionedJid": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + }, + "quoted": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + }, + "sticker": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_sendMessage_service.TextStruct": { + "type": "object", + "properties": { + "delay": { + "type": "integer" + }, + "formatJid": { + "type": "boolean" + }, + "forwardingScore": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "mentionAll": { + "type": "boolean" + }, + "mentionedJid": { + "type": "array", + "items": { + "type": "string" + } + }, + "number": { + "type": "string" + }, + "quoted": { + "$ref": "#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct" + }, + "text": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_user_service.BlockStruct": { + "type": "object", + "properties": { + "number": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_user_service.CheckUserStruct": { + "type": "object", + "properties": { + "formatJid": { + "type": "boolean" + }, + "number": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "agentdeck-whatsapp-service_pkg_user_service.GetAvatarStruct": { + "type": "object", + "properties": { + "number": { + "type": "string" + }, + "preview": { + "type": "boolean" + } + } + }, + "agentdeck-whatsapp-service_pkg_user_service.PrivacyStruct": { + "type": "object", + "properties": { + "callAdd": { + "$ref": "#/definitions/types.PrivacySetting" + }, + "groupAdd": { + "$ref": "#/definitions/types.PrivacySetting" + }, + "lastSeen": { + "$ref": "#/definitions/types.PrivacySetting" + }, + "online": { + "$ref": "#/definitions/types.PrivacySetting" + }, + "profile": { + "$ref": "#/definitions/types.PrivacySetting" + }, + "readReceipts": { + "$ref": "#/definitions/types.PrivacySetting" + }, + "status": { + "$ref": "#/definitions/types.PrivacySetting" + } + } + }, + "agentdeck-whatsapp-service_pkg_user_service.SetProfilePictureStruct": { + "type": "object", + "properties": { + "image": { + "type": "string" + } + } + }, + "agentdeck-whatsapp-service_pkg_utils.VCardStruct": { + "type": "object", + "properties": { + "fullName": { + "type": "string" + }, + "organization": { + "type": "string" + }, + "phone": { + "type": "string" + } + } + }, + "types.AddressingMode": { + "type": "string", + "enum": [ + "pn", + "lid" + ], + "x-enum-varnames": [ + "AddressingModePN", + "AddressingModeLID" + ] + }, + "types.BotEditType": { + "type": "string", + "enum": [ + "first", + "inner", + "last" + ], + "x-enum-varnames": [ + "EditTypeFirst", + "EditTypeInner", + "EditTypeLast" + ] + }, + "types.BroadcastRecipient": { + "type": "object", + "properties": { + "lid": { + "$ref": "#/definitions/types.JID" + }, + "pn": { + "$ref": "#/definitions/types.JID" + } + } + }, + "types.DeviceSentMeta": { + "type": "object", + "properties": { + "destinationJID": { + "description": "The destination user. This should match the MessageInfo.Recipient field.", + "type": "string" + }, + "phash": { + "type": "string" + } + } + }, + "types.EditAttribute": { + "type": "string", + "enum": [ + "", + "1", + "2", + "3", + "7", + "8" + ], + "x-enum-comments": { + "EditAttributeAdminEdit": "only used in newsletters" + }, + "x-enum-descriptions": [ + "", + "", + "", + "only used in newsletters", + "", + "" + ], + "x-enum-varnames": [ + "EditAttributeEmpty", + "EditAttributeMessageEdit", + "EditAttributePinInChat", + "EditAttributeAdminEdit", + "EditAttributeSenderRevoke", + "EditAttributeAdminRevoke" + ] + }, + "types.JID": { + "type": "object", + "properties": { + "device": { + "type": "integer", + "format": "int32" + }, + "integrator": { + "type": "integer", + "format": "int32" + }, + "rawAgent": { + "type": "integer", + "format": "int32" + }, + "server": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "types.MessageInfo": { + "type": "object", + "properties": { + "addressingMode": { + "description": "The addressing mode of the message (phone number or LID)", + "allOf": [ + { + "$ref": "#/definitions/types.AddressingMode" + } + ] + }, + "broadcastListOwner": { + "description": "When sending a read receipt to a broadcast list message, the Chat is the broadcast list\nand Sender is you, so this field contains the recipient of the read receipt.", + "allOf": [ + { + "$ref": "#/definitions/types.JID" + } + ] + }, + "broadcastRecipients": { + "type": "array", + "items": { + "$ref": "#/definitions/types.BroadcastRecipient" + } + }, + "category": { + "type": "string" + }, + "chat": { + "description": "The chat where the message was sent.", + "allOf": [ + { + "$ref": "#/definitions/types.JID" + } + ] + }, + "deviceSentMeta": { + "description": "Metadata for direct messages sent from another one of the user's own devices.", + "allOf": [ + { + "$ref": "#/definitions/types.DeviceSentMeta" + } + ] + }, + "edit": { + "$ref": "#/definitions/types.EditAttribute" + }, + "id": { + "type": "string" + }, + "isFromMe": { + "description": "Whether the message was sent by the current user instead of someone else.", + "type": "boolean" + }, + "isGroup": { + "description": "Whether the chat is a group chat or broadcast list.", + "type": "boolean" + }, + "mediaType": { + "type": "string" + }, + "msgBotInfo": { + "$ref": "#/definitions/types.MsgBotInfo" + }, + "msgMetaInfo": { + "$ref": "#/definitions/types.MsgMetaInfo" + }, + "multicast": { + "type": "boolean" + }, + "pushName": { + "type": "string" + }, + "recipientAlt": { + "description": "The alternative address of the recipient of the message for DMs.", + "allOf": [ + { + "$ref": "#/definitions/types.JID" + } + ] + }, + "sender": { + "description": "The user who sent the message.", + "allOf": [ + { + "$ref": "#/definitions/types.JID" + } + ] + }, + "senderAlt": { + "description": "The alternative address of the user who sent the message", + "allOf": [ + { + "$ref": "#/definitions/types.JID" + } + ] + }, + "serverID": { + "type": "integer" + }, + "timestamp": { + "type": "string" + }, + "type": { + "type": "string" + }, + "verifiedName": { + "$ref": "#/definitions/types.VerifiedName" + } + } + }, + "types.MsgBotInfo": { + "type": "object", + "properties": { + "editSenderTimestampMS": { + "type": "string" + }, + "editTargetID": { + "type": "string" + }, + "editType": { + "$ref": "#/definitions/types.BotEditType" + } + } + }, + "types.MsgMetaInfo": { + "type": "object", + "properties": { + "deprecatedLIDSession": { + "type": "boolean" + }, + "targetChat": { + "$ref": "#/definitions/types.JID" + }, + "targetID": { + "description": "Bot things", + "type": "string" + }, + "targetSender": { + "$ref": "#/definitions/types.JID" + }, + "threadMessageID": { + "type": "string" + }, + "threadMessageSenderJID": { + "$ref": "#/definitions/types.JID" + } + } + }, + "types.PrivacySetting": { + "type": "string", + "enum": [ + "", + "all", + "contacts", + "contact_allowlist", + "contact_blacklist", + "match_last_seen", + "known", + "none", + "on_standard", + "off" + ], + "x-enum-varnames": [ + "PrivacySettingUndefined", + "PrivacySettingAll", + "PrivacySettingContacts", + "PrivacySettingContactAllowlist", + "PrivacySettingContactBlacklist", + "PrivacySettingMatchLastSeen", + "PrivacySettingKnown", + "PrivacySettingNone", + "PrivacySettingOnStandard", + "PrivacySettingOff" + ] + }, + "types.VerifiedName": { + "type": "object", + "properties": { + "certificate": { + "$ref": "#/definitions/waVnameCert.VerifiedNameCertificate" + }, + "details": { + "$ref": "#/definitions/waVnameCert.VerifiedNameCertificate_Details" + } + } + }, + "types.WebAuthnResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "rawId": { + "type": "array", + "items": { + "type": "integer" + } + }, + "response": { + "$ref": "#/definitions/types.WebAuthnResponseData" + }, + "type": { + "type": "string" + } + } + }, + "types.WebAuthnResponseData": { + "type": "object", + "properties": { + "authenticatorData": { + "type": "array", + "items": { + "type": "integer" + } + }, + "clientDataJSON": { + "type": "array", + "items": { + "type": "integer" + } + }, + "signature": { + "type": "array", + "items": { + "type": "integer" + } + }, + "userHandle": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waAICommon.AIMediaCollectionMessage": { + "type": "object", + "properties": { + "collectionID": { + "type": "string" + }, + "expectedMediaCount": { + "type": "integer" + }, + "hasGlobalCaption": { + "type": "boolean" + } + } + }, + "waAICommon.AIMediaCollectionMetadata": { + "type": "object", + "properties": { + "collectionID": { + "type": "string" + }, + "uploadOrderIndex": { + "type": "integer" + } + } + }, + "waAICommon.AIMetadataOperation": { + "type": "object", + "properties": { + "hatchMetadataSync": { + "$ref": "#/definitions/waAICommon.HatchMetadataSync" + } + } + }, + "waAICommon.AIRegenerateMetadata": { + "type": "object", + "properties": { + "messageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "responseTimestampMS": { + "type": "integer" + } + } + }, + "waAICommon.AIRichResponseUnifiedResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waAICommon.AISubscriptionRequestType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "AISubscriptionRequestType_UNSPECIFIED", + "AISubscriptionRequestType_THINK_HARD", + "AISubscriptionRequestType_IMAGE_GEN", + "AISubscriptionRequestType_VIDEO_GEN" + ] + }, + "waAICommon.AISubscriptionUpsellMetadata": { + "type": "object", + "properties": { + "requestType": { + "$ref": "#/definitions/waAICommon.AISubscriptionRequestType" + } + } + }, + "waAICommon.AIThreadInfo": { + "type": "object", + "properties": { + "clientInfo": { + "$ref": "#/definitions/waAICommon.AIThreadInfo_AIThreadClientInfo" + }, + "serverInfo": { + "$ref": "#/definitions/waAICommon.AIThreadInfo_AIThreadServerInfo" + } + } + }, + "waAICommon.AIThreadInfo_AIThreadClientInfo": { + "type": "object", + "properties": { + "sourceChatJID": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/waAICommon.AIThreadInfo_AIThreadClientInfo_AIThreadType" + } + } + }, + "waAICommon.AIThreadInfo_AIThreadClientInfo_AIThreadType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "AIThreadInfo_AIThreadClientInfo_UNKNOWN", + "AIThreadInfo_AIThreadClientInfo_DEFAULT", + "AIThreadInfo_AIThreadClientInfo_INCOGNITO", + "AIThreadInfo_AIThreadClientInfo_SIDE_CHAT" + ] + }, + "waAICommon.AIThreadInfo_AIThreadServerInfo": { + "type": "object", + "properties": { + "title": { + "type": "string" + } + } + }, + "waAICommon.BotAgeCollectionMetadata": { + "type": "object", + "properties": { + "ageCollectionEligible": { + "type": "boolean" + }, + "ageCollectionType": { + "$ref": "#/definitions/waAICommon.BotAgeCollectionMetadata_AgeCollectionType" + }, + "shouldTriggerAgeCollectionOnClient": { + "type": "boolean" + } + } + }, + "waAICommon.BotAgeCollectionMetadata_AgeCollectionType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "BotAgeCollectionMetadata_O18_BINARY", + "BotAgeCollectionMetadata_WAFFLE" + ] + }, + "waAICommon.BotAgentDeepLinkMetadata": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "waAICommon.BotAgentMetadata": { + "type": "object", + "properties": { + "deepLinkMetadata": { + "$ref": "#/definitions/waAICommon.BotAgentDeepLinkMetadata" + } + } + }, + "waAICommon.BotCapabilityMetadata": { + "type": "object", + "properties": { + "capabilities": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotCapabilityMetadata_BotCapabilityType" + } + } + } + }, + "waAICommon.BotCapabilityMetadata_BotCapabilityType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65 + ], + "x-enum-varnames": [ + "BotCapabilityMetadata_UNKNOWN", + "BotCapabilityMetadata_PROGRESS_INDICATOR", + "BotCapabilityMetadata_RICH_RESPONSE_HEADING", + "BotCapabilityMetadata_RICH_RESPONSE_NESTED_LIST", + "BotCapabilityMetadata_AI_MEMORY", + "BotCapabilityMetadata_RICH_RESPONSE_THREAD_SURFING", + "BotCapabilityMetadata_RICH_RESPONSE_TABLE", + "BotCapabilityMetadata_RICH_RESPONSE_CODE", + "BotCapabilityMetadata_RICH_RESPONSE_STRUCTURED_RESPONSE", + "BotCapabilityMetadata_RICH_RESPONSE_INLINE_IMAGE", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_CONTROL", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_1", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_2", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_3", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_4", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_5", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_6", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_7", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_8", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_9", + "BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_10", + "BotCapabilityMetadata_RICH_RESPONSE_SUB_HEADING", + "BotCapabilityMetadata_RICH_RESPONSE_GRID_IMAGE", + "BotCapabilityMetadata_AI_STUDIO_UGC_MEMORY", + "BotCapabilityMetadata_RICH_RESPONSE_LATEX", + "BotCapabilityMetadata_RICH_RESPONSE_MAPS", + "BotCapabilityMetadata_RICH_RESPONSE_INLINE_REELS", + "BotCapabilityMetadata_AGENTIC_PLANNING", + "BotCapabilityMetadata_ACCOUNT_LINKING", + "BotCapabilityMetadata_STREAMING_DISAGGREGATION", + "BotCapabilityMetadata_RICH_RESPONSE_GRID_IMAGE_3P", + "BotCapabilityMetadata_RICH_RESPONSE_LATEX_INLINE", + "BotCapabilityMetadata_QUERY_PLAN", + "BotCapabilityMetadata_PROACTIVE_MESSAGE", + "BotCapabilityMetadata_RICH_RESPONSE_UNIFIED_RESPONSE", + "BotCapabilityMetadata_PROMOTION_MESSAGE", + "BotCapabilityMetadata_SIMPLIFIED_PROFILE_PAGE", + "BotCapabilityMetadata_RICH_RESPONSE_SOURCES_IN_MESSAGE", + "BotCapabilityMetadata_RICH_RESPONSE_SIDE_BY_SIDE_SURVEY", + "BotCapabilityMetadata_RICH_RESPONSE_UNIFIED_TEXT_COMPONENT", + "BotCapabilityMetadata_AI_SHARED_MEMORY", + "BotCapabilityMetadata_RICH_RESPONSE_UNIFIED_SOURCES", + "BotCapabilityMetadata_RICH_RESPONSE_UNIFIED_DOMAIN_CITATIONS", + "BotCapabilityMetadata_RICH_RESPONSE_UR_INLINE_REELS_ENABLED", + "BotCapabilityMetadata_RICH_RESPONSE_UR_MEDIA_GRID_ENABLED", + "BotCapabilityMetadata_RICH_RESPONSE_UR_TIMESTAMP_PLACEHOLDER", + "BotCapabilityMetadata_RICH_RESPONSE_IN_APP_SURVEY", + "BotCapabilityMetadata_AI_RESPONSE_MODEL_BRANDING", + "BotCapabilityMetadata_SESSION_TRANSPARENCY_SYSTEM_MESSAGE", + "BotCapabilityMetadata_RICH_RESPONSE_UR_REASONING", + "BotCapabilityMetadata_RICH_RESPONSE_UR_ZEITGEIST_CITATIONS", + "BotCapabilityMetadata_RICH_RESPONSE_UR_ZEITGEIST_CAROUSEL", + "BotCapabilityMetadata_AI_IMAGINE_LOADING_INDICATOR", + "BotCapabilityMetadata_RICH_RESPONSE_UR_IMAGINE", + "BotCapabilityMetadata_AI_IMAGINE_UR_TO_NATIVE_LOADING_INDICATOR", + "BotCapabilityMetadata_RICH_RESPONSE_UR_BLOKS_ENABLED", + "BotCapabilityMetadata_RICH_RESPONSE_INLINE_LINKS_ENABLED", + "BotCapabilityMetadata_RICH_RESPONSE_UR_IMAGINE_VIDEO", + "BotCapabilityMetadata_JSON_PATCH_STREAMING", + "BotCapabilityMetadata_AI_TAB_FORCE_CLIPPY", + "BotCapabilityMetadata_UNIFIED_RESPONSE_EMBEDDED_SCREENS", + "BotCapabilityMetadata_AI_SUBSCRIPTION_ENABLED", + "BotCapabilityMetadata_UNIFIED_RESPONSE_AI_CONTENT_SEARCH_ENABLED", + "BotCapabilityMetadata_UNIFIED_RESPONSE_MARKDOWN_LINKS_ENABLED", + "BotCapabilityMetadata_AI_RICH_RESPONSE_MAPS_V2_ENABLED", + "BotCapabilityMetadata_AI_SUBSCRIPTION_METERING_ENABLED" + ] + }, + "waAICommon.BotCommandMetadata": { + "type": "object", + "properties": { + "commandDescription": { + "type": "string" + }, + "commandName": { + "type": "string" + }, + "commandPrompt": { + "type": "string" + } + } + }, + "waAICommon.BotDocumentMessageMetadata": { + "type": "object", + "properties": { + "pluginType": { + "$ref": "#/definitions/waAICommon.BotDocumentMessageMetadata_DocumentPluginType" + } + } + }, + "waAICommon.BotDocumentMessageMetadata_DocumentPluginType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "BotDocumentMessageMetadata_TEXT_EXTRACTION", + "BotDocumentMessageMetadata_OCR_AND_IMAGES" + ] + }, + "waAICommon.BotFeedbackMessage": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_BotFeedbackKind" + }, + "kindNegative": { + "type": "integer" + }, + "kindPositive": { + "type": "integer" + }, + "kindReport": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_ReportKind" + }, + "messageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "sideBySideSurveyMetadata": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata" + }, + "text": { + "type": "string" + } + } + }, + "waAICommon.BotFeedbackMessage_BotFeedbackKind": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14 + ], + "x-enum-varnames": [ + "BotFeedbackMessage_BOT_FEEDBACK_POSITIVE", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_GENERIC", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_HELPFUL", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_INTERESTING", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_ACCURATE", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_SAFE", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_OTHER", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_REFUSED", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_NOT_VISUALLY_APPEALING", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_NOT_RELEVANT_TO_TEXT", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_PERSONALIZED", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_CLARITY", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_DOESNT_LOOK_LIKE_THE_PERSON", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_HALLUCINATION_INTERNAL_ONLY", + "BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE" + ] + }, + "waAICommon.BotFeedbackMessage_ReportKind": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "BotFeedbackMessage_NONE", + "BotFeedbackMessage_GENERIC" + ] + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata": { + "type": "object", + "properties": { + "analyticsData": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SideBySideSurveyAnalyticsData" + }, + "isSelectedResponsePrimary": { + "type": "boolean" + }, + "messageIDToEdit": { + "type": "string" + }, + "metaAiAnalyticsData": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData" + }, + "responseOtid": { + "type": "string" + }, + "responseTimestampMSString": { + "type": "string" + }, + "selectedRequestID": { + "type": "string" + }, + "simonSessionFbid": { + "type": "string" + }, + "surveyID": { + "type": "integer" + } + } + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SideBySideSurveyAnalyticsData": { + "type": "object", + "properties": { + "simonSessionFbid": { + "type": "string" + }, + "tessaEvent": { + "type": "string" + }, + "tessaSessionFbid": { + "type": "string" + } + } + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData": { + "type": "object", + "properties": { + "abandonEvent": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyAbandonEventData" + }, + "cardImpressionEvent": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCardImpressionEventData" + }, + "ctaClickEvent": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCTAClickEventData" + }, + "ctaImpressionEvent": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCTAImpressionEventData" + }, + "primaryResponseID": { + "type": "string" + }, + "responseEvent": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyResponseEventData" + }, + "surveyID": { + "type": "integer" + }, + "testArmName": { + "type": "string" + }, + "timestampMSString": { + "type": "string" + } + } + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyAbandonEventData": { + "type": "object", + "properties": { + "abandonDwellTimeMSString": { + "type": "string" + } + } + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCTAClickEventData": { + "type": "object", + "properties": { + "clickDwellTimeMSString": { + "type": "string" + }, + "isSurveyExpired": { + "type": "boolean" + } + } + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCTAImpressionEventData": { + "type": "object", + "properties": { + "isSurveyExpired": { + "type": "boolean" + } + } + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCardImpressionEventData": { + "type": "object" + }, + "waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyResponseEventData": { + "type": "object", + "properties": { + "responseDwellTimeMSString": { + "type": "string" + }, + "selectedResponseID": { + "type": "string" + } + } + }, + "waAICommon.BotGroupMetadata": { + "type": "object", + "properties": { + "participantsMetadata": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotGroupParticipantMetadata" + } + } + } + }, + "waAICommon.BotGroupParticipantMetadata": { + "type": "object", + "properties": { + "botFbid": { + "type": "string" + } + } + }, + "waAICommon.BotImagineMetadata": { + "type": "object", + "properties": { + "imagineType": { + "$ref": "#/definitions/waAICommon.BotImagineMetadata_ImagineType" + }, + "shortPrompt": { + "type": "string" + } + } + }, + "waAICommon.BotImagineMetadata_ImagineType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "x-enum-varnames": [ + "BotImagineMetadata_UNKNOWN", + "BotImagineMetadata_IMAGINE", + "BotImagineMetadata_MEMU", + "BotImagineMetadata_FLASH", + "BotImagineMetadata_EDIT" + ] + }, + "waAICommon.BotInfrastructureDiagnostics": { + "type": "object", + "properties": { + "botBackend": { + "$ref": "#/definitions/waAICommon.BotInfrastructureDiagnostics_BotBackend" + }, + "isThinking": { + "type": "boolean" + }, + "toolsUsed": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "waAICommon.BotInfrastructureDiagnostics_BotBackend": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "BotInfrastructureDiagnostics_AAPI", + "BotInfrastructureDiagnostics_CLIPPY" + ] + }, + "waAICommon.BotLinkedAccount": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/waAICommon.BotLinkedAccount_BotLinkedAccountType" + } + } + }, + "waAICommon.BotLinkedAccount_BotLinkedAccountType": { + "type": "integer", + "format": "int32", + "enum": [ + 0 + ], + "x-enum-varnames": [ + "BotLinkedAccount_BOT_LINKED_ACCOUNT_TYPE_1P" + ] + }, + "waAICommon.BotLinkedAccountsMetadata": { + "type": "object", + "properties": { + "acAuthTokens": { + "type": "array", + "items": { + "type": "integer" + } + }, + "acErrorCode": { + "type": "integer" + }, + "accounts": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotLinkedAccount" + } + } + } + }, + "waAICommon.BotMediaMetadata": { + "type": "object", + "properties": { + "directPath": { + "type": "string" + }, + "fileEncSHA256": { + "type": "string" + }, + "fileSHA256": { + "type": "string" + }, + "mediaKey": { + "type": "string" + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "mimetype": { + "type": "string" + }, + "orientationType": { + "$ref": "#/definitions/waAICommon.BotMediaMetadata_OrientationType" + } + } + }, + "waAICommon.BotMediaMetadata_OrientationType": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BotMediaMetadata_CENTER", + "BotMediaMetadata_LEFT", + "BotMediaMetadata_RIGHT" + ] + }, + "waAICommon.BotMemoryFact": { + "type": "object", + "properties": { + "fact": { + "type": "string" + }, + "factID": { + "type": "string" + } + } + }, + "waAICommon.BotMemoryMetadata": { + "type": "object", + "properties": { + "addedFacts": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotMemoryFact" + } + }, + "disclaimer": { + "type": "string" + }, + "removedFacts": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotMemoryFact" + } + } + } + }, + "waAICommon.BotMemuMetadata": { + "type": "object", + "properties": { + "faceImages": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotMediaMetadata" + } + } + } + }, + "waAICommon.BotMessageOrigin": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/waAICommon.BotMessageOrigin_BotMessageOriginType" + } + } + }, + "waAICommon.BotMessageOriginMetadata": { + "type": "object", + "properties": { + "origins": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotMessageOrigin" + } + } + } + }, + "waAICommon.BotMessageOrigin_BotMessageOriginType": { + "type": "integer", + "format": "int32", + "enum": [ + 0 + ], + "x-enum-varnames": [ + "BotMessageOrigin_BOT_MESSAGE_ORIGIN_TYPE_AI_INITIATED" + ] + }, + "waAICommon.BotMessageSharingInfo": { + "type": "object", + "properties": { + "botEntryPointOrigin": { + "$ref": "#/definitions/waAICommon.BotMetricsEntryPoint" + }, + "forwardScore": { + "type": "integer" + } + } + }, + "waAICommon.BotMetadata": { + "type": "object", + "properties": { + "aiConversationContext": { + "type": "array", + "items": { + "type": "integer" + } + }, + "aiMediaCollectionMetadata": { + "$ref": "#/definitions/waAICommon.AIMediaCollectionMetadata" + }, + "botAgeCollectionMetadata": { + "$ref": "#/definitions/waAICommon.BotAgeCollectionMetadata" + }, + "botDocumentMessageMetadata": { + "$ref": "#/definitions/waAICommon.BotDocumentMessageMetadata" + }, + "botGroupMetadata": { + "$ref": "#/definitions/waAICommon.BotGroupMetadata" + }, + "botInfrastructureDiagnostics": { + "$ref": "#/definitions/waAICommon.BotInfrastructureDiagnostics" + }, + "botLinkedAccountsMetadata": { + "$ref": "#/definitions/waAICommon.BotLinkedAccountsMetadata" + }, + "botMessageOriginMetadata": { + "$ref": "#/definitions/waAICommon.BotMessageOriginMetadata" + }, + "botMetricsMetadata": { + "$ref": "#/definitions/waAICommon.BotMetricsMetadata" + }, + "botModeSelectionMetadata": { + "$ref": "#/definitions/waAICommon.BotModeSelectionMetadata" + }, + "botPromotionMessageMetadata": { + "$ref": "#/definitions/waAICommon.BotPromotionMessageMetadata" + }, + "botQuotaMetadata": { + "$ref": "#/definitions/waAICommon.BotQuotaMetadata" + }, + "botRenderingConfigMetadata": { + "$ref": "#/definitions/waAICommon.BotRenderingConfigMetadata" + }, + "botResponseID": { + "type": "string" + }, + "botThreadInfo": { + "$ref": "#/definitions/waAICommon.AIThreadInfo" + }, + "capabilityMetadata": { + "$ref": "#/definitions/waAICommon.BotCapabilityMetadata" + }, + "commandMetadata": { + "$ref": "#/definitions/waAICommon.BotCommandMetadata" + }, + "conversationStarterPromptID": { + "type": "string" + }, + "imagineMetadata": { + "$ref": "#/definitions/waAICommon.BotImagineMetadata" + }, + "inThreadSurveyMetadata": { + "$ref": "#/definitions/waAICommon.InThreadSurveyMetadata" + }, + "internalMetadata": { + "type": "array", + "items": { + "type": "integer" + } + }, + "invokerJID": { + "type": "string" + }, + "memoryMetadata": { + "$ref": "#/definitions/waAICommon.BotMemoryMetadata" + }, + "memuMetadata": { + "$ref": "#/definitions/waAICommon.BotMemuMetadata" + }, + "messageDisclaimerText": { + "type": "string" + }, + "modelMetadata": { + "$ref": "#/definitions/waAICommon.BotModelMetadata" + }, + "personaID": { + "type": "string" + }, + "pluginMetadata": { + "$ref": "#/definitions/waAICommon.BotPluginMetadata" + }, + "progressIndicatorMetadata": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata" + }, + "pttPromptMetadata": { + "$ref": "#/definitions/waAICommon.BotPttPromptMetadata" + }, + "regenerateMetadata": { + "$ref": "#/definitions/waAICommon.AIRegenerateMetadata" + }, + "reminderMetadata": { + "$ref": "#/definitions/waAICommon.BotReminderMetadata" + }, + "renderingMetadata": { + "$ref": "#/definitions/waAICommon.BotRenderingMetadata" + }, + "resolvedToolCallMetadata": { + "$ref": "#/definitions/waAICommon.BotResolvedToolCallMetadata" + }, + "richResponseSourcesMetadata": { + "$ref": "#/definitions/waAICommon.BotSourcesMetadata" + }, + "sessionMetadata": { + "$ref": "#/definitions/waAICommon.BotSessionMetadata" + }, + "sessionTransparencyMetadata": { + "$ref": "#/definitions/waAICommon.SessionTransparencyMetadata" + }, + "subscriptionUpsellMetadata": { + "$ref": "#/definitions/waAICommon.AISubscriptionUpsellMetadata" + }, + "suggestedPromptMetadata": { + "$ref": "#/definitions/waAICommon.BotSuggestedPromptMetadata" + }, + "timezone": { + "type": "string" + }, + "unifiedResponseMutation": { + "$ref": "#/definitions/waAICommon.BotUnifiedResponseMutation" + }, + "verificationMetadata": { + "$ref": "#/definitions/waAICommon.BotSignatureVerificationMetadata" + } + } + }, + "waAICommon.BotMetricsEntryPoint": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 45, + 46, + 47, + 54, + 55, + 56 + ], + "x-enum-varnames": [ + "BotMetricsEntryPoint_UNDEFINED_ENTRY_POINT", + "BotMetricsEntryPoint_FAVICON", + "BotMetricsEntryPoint_CHATLIST", + "BotMetricsEntryPoint_AISEARCH_NULL_STATE_PAPER_PLANE", + "BotMetricsEntryPoint_AISEARCH_NULL_STATE_SUGGESTION", + "BotMetricsEntryPoint_AISEARCH_TYPE_AHEAD_SUGGESTION", + "BotMetricsEntryPoint_AISEARCH_TYPE_AHEAD_PAPER_PLANE", + "BotMetricsEntryPoint_AISEARCH_TYPE_AHEAD_RESULT_CHATLIST", + "BotMetricsEntryPoint_AISEARCH_TYPE_AHEAD_RESULT_MESSAGES", + "BotMetricsEntryPoint_AIVOICE_SEARCH_BAR", + "BotMetricsEntryPoint_AIVOICE_FAVICON", + "BotMetricsEntryPoint_AISTUDIO", + "BotMetricsEntryPoint_DEEPLINK", + "BotMetricsEntryPoint_NOTIFICATION", + "BotMetricsEntryPoint_PROFILE_MESSAGE_BUTTON", + "BotMetricsEntryPoint_FORWARD", + "BotMetricsEntryPoint_APP_SHORTCUT", + "BotMetricsEntryPoint_FF_FAMILY", + "BotMetricsEntryPoint_AI_TAB", + "BotMetricsEntryPoint_AI_HOME", + "BotMetricsEntryPoint_AI_DEEPLINK_IMMERSIVE", + "BotMetricsEntryPoint_AI_DEEPLINK", + "BotMetricsEntryPoint_META_AI_CHAT_SHORTCUT_AI_STUDIO", + "BotMetricsEntryPoint_UGC_CHAT_SHORTCUT_AI_STUDIO", + "BotMetricsEntryPoint_NEW_CHAT_AI_STUDIO", + "BotMetricsEntryPoint_AIVOICE_FAVICON_CALL_HISTORY", + "BotMetricsEntryPoint_ASK_META_AI_CONTEXT_MENU", + "BotMetricsEntryPoint_ASK_META_AI_CONTEXT_MENU_1ON1", + "BotMetricsEntryPoint_ASK_META_AI_CONTEXT_MENU_GROUP", + "BotMetricsEntryPoint_INVOKE_META_AI_1ON1", + "BotMetricsEntryPoint_INVOKE_META_AI_GROUP", + "BotMetricsEntryPoint_META_AI_FORWARD", + "BotMetricsEntryPoint_NEW_CHAT_AI_CONTACT", + "BotMetricsEntryPoint_MESSAGE_QUICK_ACTION_1_ON_1_CHAT", + "BotMetricsEntryPoint_MESSAGE_QUICK_ACTION_GROUP_CHAT", + "BotMetricsEntryPoint_ATTACHMENT_TRAY_1_ON_1_CHAT", + "BotMetricsEntryPoint_ATTACHMENT_TRAY_GROUP_CHAT", + "BotMetricsEntryPoint_ASK_META_AI_MEDIA_VIEWER_1ON1", + "BotMetricsEntryPoint_ASK_META_AI_MEDIA_VIEWER_GROUP", + "BotMetricsEntryPoint_MEDIA_PICKER_1_ON_1_CHAT", + "BotMetricsEntryPoint_MEDIA_PICKER_GROUP_CHAT", + "BotMetricsEntryPoint_ASK_META_AI_NO_SEARCH_RESULTS", + "BotMetricsEntryPoint_META_AI_SETTINGS", + "BotMetricsEntryPoint_WEB_INTRO_PANEL", + "BotMetricsEntryPoint_WEB_NAVIGATION_BAR", + "BotMetricsEntryPoint_GROUP_MEMBER", + "BotMetricsEntryPoint_CHATLIST_SEARCH", + "BotMetricsEntryPoint_NEW_CHAT_LIST" + ] + }, + "waAICommon.BotMetricsMetadata": { + "type": "object", + "properties": { + "destinationEntryPoint": { + "$ref": "#/definitions/waAICommon.BotMetricsEntryPoint" + }, + "destinationID": { + "type": "string" + }, + "threadOrigin": { + "$ref": "#/definitions/waAICommon.BotMetricsThreadEntryPoint" + } + } + }, + "waAICommon.BotMetricsThreadEntryPoint": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "BotMetricsThreadEntryPoint_AI_TAB_THREAD", + "BotMetricsThreadEntryPoint_AI_HOME_THREAD", + "BotMetricsThreadEntryPoint_AI_DEEPLINK_IMMERSIVE_THREAD", + "BotMetricsThreadEntryPoint_AI_DEEPLINK_THREAD", + "BotMetricsThreadEntryPoint_ASK_META_AI_CONTEXT_MENU_THREAD" + ] + }, + "waAICommon.BotModeSelectionMetadata": { + "type": "object", + "properties": { + "mode": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotModeSelectionMetadata_BotUserSelectionMode" + } + }, + "overrideMode": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waAICommon.BotModeSelectionMetadata_BotUserSelectionMode": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "BotModeSelectionMetadata_DEFAULT_MODE", + "BotModeSelectionMetadata_THINK_HARD_MODE" + ] + }, + "waAICommon.BotModelMetadata": { + "type": "object", + "properties": { + "modelNameOverride": { + "type": "string" + }, + "modelType": { + "$ref": "#/definitions/waAICommon.BotModelMetadata_ModelType" + }, + "premiumModelStatus": { + "$ref": "#/definitions/waAICommon.BotModelMetadata_PremiumModelStatus" + } + } + }, + "waAICommon.BotModelMetadata_ModelType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "BotModelMetadata_UNKNOWN_TYPE", + "BotModelMetadata_LLAMA_PROD", + "BotModelMetadata_LLAMA_PROD_PREMIUM" + ] + }, + "waAICommon.BotModelMetadata_PremiumModelStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "BotModelMetadata_UNKNOWN_STATUS", + "BotModelMetadata_AVAILABLE", + "BotModelMetadata_QUOTA_EXCEED_LIMIT" + ] + }, + "waAICommon.BotPluginMetadata": { + "type": "object", + "properties": { + "deprecatedField": { + "$ref": "#/definitions/waAICommon.BotPluginMetadata_PluginType" + }, + "expectedLinksCount": { + "type": "integer" + }, + "faviconCDNURL": { + "type": "string" + }, + "parentPluginMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "parentPluginType": { + "$ref": "#/definitions/waAICommon.BotPluginMetadata_PluginType" + }, + "pluginType": { + "$ref": "#/definitions/waAICommon.BotPluginMetadata_PluginType" + }, + "profilePhotoCDNURL": { + "type": "string" + }, + "provider": { + "$ref": "#/definitions/waAICommon.BotPluginMetadata_SearchProvider" + }, + "referenceIndex": { + "type": "integer" + }, + "searchProviderURL": { + "type": "string" + }, + "searchQuery": { + "type": "string" + }, + "thumbnailCDNURL": { + "type": "string" + } + } + }, + "waAICommon.BotPluginMetadata_PluginType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "BotPluginMetadata_UNKNOWN_PLUGIN", + "BotPluginMetadata_REELS", + "BotPluginMetadata_SEARCH" + ] + }, + "waAICommon.BotPluginMetadata_SearchProvider": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BotPluginMetadata_UNKNOWN", + "BotPluginMetadata_BING", + "BotPluginMetadata_GOOGLE", + "BotPluginMetadata_SUPPORT" + ] + }, + "waAICommon.BotProgressIndicatorMetadata": { + "type": "object", + "properties": { + "estimatedCompletionTime": { + "type": "integer" + }, + "progressDescription": { + "type": "string" + }, + "stepsMetadata": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata" + } + } + } + }, + "waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata": { + "type": "object", + "properties": { + "isEnhancedSearch": { + "type": "boolean" + }, + "isReasoning": { + "type": "boolean" + }, + "sections": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningStepSectionMetadata" + } + }, + "sourcesMetadata": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata" + } + }, + "status": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_PlanningStepStatus" + }, + "statusBody": { + "type": "string" + }, + "statusTitle": { + "type": "string" + } + } + }, + "waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourceMetadata": { + "type": "object", + "properties": { + "favIconURL": { + "type": "string" + }, + "provider": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotSearchSourceProvider" + }, + "sourceURL": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata": { + "type": "object", + "properties": { + "provider": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_BotPlanningSearchSourceProvider" + }, + "sourceTitle": { + "type": "string" + }, + "sourceURL": { + "type": "string" + } + } + }, + "waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_BotPlanningSearchSourceProvider": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_UNKNOWN", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_OTHER", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_GOOGLE", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_BING" + ] + }, + "waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningStepSectionMetadata": { + "type": "object", + "properties": { + "sectionBody": { + "type": "string" + }, + "sectionTitle": { + "type": "string" + }, + "sourcesMetadata": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourceMetadata" + } + } + } + }, + "waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotSearchSourceProvider": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_UNKNOWN_PROVIDER", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_OTHER", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_GOOGLE", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_BING" + ] + }, + "waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_PlanningStepStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_UNKNOWN", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_PLANNED", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_EXECUTING", + "BotProgressIndicatorMetadata_BotPlanningStepMetadata_FINISHED" + ] + }, + "waAICommon.BotPromotionMessageMetadata": { + "type": "object", + "properties": { + "buttonTitle": { + "type": "string" + }, + "promotionType": { + "$ref": "#/definitions/waAICommon.BotPromotionMessageMetadata_BotPromotionType" + } + } + }, + "waAICommon.BotPromotionMessageMetadata_BotPromotionType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "BotPromotionMessageMetadata_UNKNOWN_TYPE", + "BotPromotionMessageMetadata_C50", + "BotPromotionMessageMetadata_SURVEY_PLATFORM" + ] + }, + "waAICommon.BotPromptSuggestion": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + }, + "promptID": { + "type": "string" + } + } + }, + "waAICommon.BotPromptSuggestions": { + "type": "object", + "properties": { + "suggestions": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotPromptSuggestion" + } + } + } + }, + "waAICommon.BotPttPromptMetadata": { + "type": "object", + "properties": { + "transcript": { + "type": "string" + } + } + }, + "waAICommon.BotQuotaMetadata": { + "type": "object", + "properties": { + "botFeatureQuotaMetadata": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotQuotaMetadata_BotFeatureQuotaMetadata" + } + } + } + }, + "waAICommon.BotQuotaMetadata_BotFeatureQuotaMetadata": { + "type": "object", + "properties": { + "expirationTimestamp": { + "type": "integer" + }, + "featureType": { + "$ref": "#/definitions/waAICommon.BotQuotaMetadata_BotFeatureQuotaMetadata_BotFeatureType" + }, + "remainingQuota": { + "type": "integer" + } + } + }, + "waAICommon.BotQuotaMetadata_BotFeatureQuotaMetadata_BotFeatureType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "BotQuotaMetadata_BotFeatureQuotaMetadata_UNKNOWN_FEATURE", + "BotQuotaMetadata_BotFeatureQuotaMetadata_REASONING_FEATURE" + ] + }, + "waAICommon.BotReminderMetadata": { + "type": "object", + "properties": { + "action": { + "$ref": "#/definitions/waAICommon.BotReminderMetadata_ReminderAction" + }, + "frequency": { + "$ref": "#/definitions/waAICommon.BotReminderMetadata_ReminderFrequency" + }, + "name": { + "type": "string" + }, + "nextTriggerTimestamp": { + "type": "integer" + }, + "requestMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waAICommon.BotReminderMetadata_ReminderAction": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3, + 4 + ], + "x-enum-varnames": [ + "BotReminderMetadata_NOTIFY", + "BotReminderMetadata_CREATE", + "BotReminderMetadata_DELETE", + "BotReminderMetadata_UPDATE" + ] + }, + "waAICommon.BotReminderMetadata_ReminderFrequency": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "BotReminderMetadata_ONCE", + "BotReminderMetadata_DAILY", + "BotReminderMetadata_WEEKLY", + "BotReminderMetadata_BIWEEKLY", + "BotReminderMetadata_MONTHLY" + ] + }, + "waAICommon.BotRenderingConfigMetadata": { + "type": "object", + "properties": { + "bloksVersioningID": { + "type": "string" + }, + "pixelDensity": { + "type": "number" + } + } + }, + "waAICommon.BotRenderingMetadata": { + "type": "object", + "properties": { + "keywords": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotRenderingMetadata_Keyword" + } + } + } + }, + "waAICommon.BotRenderingMetadata_Keyword": { + "type": "object", + "properties": { + "associatedPrompts": { + "type": "array", + "items": { + "type": "string" + } + }, + "value": { + "type": "string" + } + } + }, + "waAICommon.BotResolvedToolCallMetadata": { + "type": "object", + "properties": { + "resolutionDataSerialized": { + "type": "string" + }, + "toolCallID": { + "type": "string" + } + } + }, + "waAICommon.BotSessionMetadata": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + }, + "sessionSource": { + "$ref": "#/definitions/waAICommon.BotSessionSource" + } + } + }, + "waAICommon.BotSessionSource": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "x-enum-varnames": [ + "BotSessionSource_NONE", + "BotSessionSource_NULL_STATE", + "BotSessionSource_TYPEAHEAD", + "BotSessionSource_USER_INPUT", + "BotSessionSource_EMU_FLASH", + "BotSessionSource_EMU_FLASH_FOLLOWUP", + "BotSessionSource_VOICE", + "BotSessionSource_AI_HOME_SESSION" + ] + }, + "waAICommon.BotSignatureVerificationMetadata": { + "type": "object", + "properties": { + "proofs": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotSignatureVerificationUseCaseProof" + } + } + } + }, + "waAICommon.BotSignatureVerificationUseCaseProof": { + "type": "object", + "properties": { + "certificateChain": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + "signature": { + "type": "array", + "items": { + "type": "integer" + } + }, + "useCase": { + "$ref": "#/definitions/waAICommon.BotSignatureVerificationUseCaseProof_BotSignatureUseCase" + }, + "version": { + "type": "integer" + } + } + }, + "waAICommon.BotSignatureVerificationUseCaseProof_BotSignatureUseCase": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BotSignatureVerificationUseCaseProof_UNSPECIFIED", + "BotSignatureVerificationUseCaseProof_WA_BOT_MSG", + "BotSignatureVerificationUseCaseProof_WA_TEE_BOT_MSG", + "BotSignatureVerificationUseCaseProof_P2P_PILLS" + ] + }, + "waAICommon.BotSourcesMetadata": { + "type": "object", + "properties": { + "sources": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotSourcesMetadata_BotSourceItem" + } + } + } + }, + "waAICommon.BotSourcesMetadata_BotSourceItem": { + "type": "object", + "properties": { + "citationNumber": { + "type": "integer" + }, + "faviconCDNURL": { + "type": "string" + }, + "provider": { + "$ref": "#/definitions/waAICommon.BotSourcesMetadata_BotSourceItem_SourceProvider" + }, + "sourceProviderURL": { + "type": "string" + }, + "sourceQuery": { + "type": "string" + }, + "sourceTitle": { + "type": "string" + }, + "thumbnailCDNURL": { + "type": "string" + } + } + }, + "waAICommon.BotSourcesMetadata_BotSourceItem_SourceProvider": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "x-enum-varnames": [ + "BotSourcesMetadata_BotSourceItem_UNKNOWN", + "BotSourcesMetadata_BotSourceItem_BING", + "BotSourcesMetadata_BotSourceItem_GOOGLE", + "BotSourcesMetadata_BotSourceItem_SUPPORT", + "BotSourcesMetadata_BotSourceItem_OTHER" + ] + }, + "waAICommon.BotSuggestedPromptMetadata": { + "type": "object", + "properties": { + "promptSuggestions": { + "$ref": "#/definitions/waAICommon.BotPromptSuggestions" + }, + "selectedPromptID": { + "type": "string" + }, + "selectedPromptIndex": { + "type": "integer" + }, + "suggestedPrompts": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "waAICommon.BotUnifiedResponseMutation": { + "type": "object", + "properties": { + "mediaDetailsMetadataList": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.BotUnifiedResponseMutation_MediaDetailsMetadata" + } + }, + "sbsMetadata": { + "$ref": "#/definitions/waAICommon.BotUnifiedResponseMutation_SideBySideMetadata" + } + } + }, + "waAICommon.BotUnifiedResponseMutation_MediaDetailsMetadata": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "highResMedia": { + "$ref": "#/definitions/waAICommon.BotMediaMetadata" + }, + "previewMedia": { + "$ref": "#/definitions/waAICommon.BotMediaMetadata" + } + } + }, + "waAICommon.BotUnifiedResponseMutation_SideBySideMetadata": { + "type": "object", + "properties": { + "primaryResponseID": { + "type": "string" + }, + "surveyCtaHasRendered": { + "type": "boolean" + } + } + }, + "waAICommon.ForwardedAIBotMessageInfo": { + "type": "object", + "properties": { + "botJID": { + "type": "string" + }, + "botName": { + "type": "string" + }, + "creatorName": { + "type": "string" + } + } + }, + "waAICommon.HatchMetadataSync": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + }, + "requestID": { + "type": "string" + }, + "timestampMS": { + "type": "integer" + } + } + }, + "waAICommon.InThreadSurveyMetadata": { + "type": "object", + "properties": { + "feedbackToastText": { + "type": "string" + }, + "invitationBodyText": { + "type": "string" + }, + "invitationCtaText": { + "type": "string" + }, + "invitationCtaURL": { + "type": "string" + }, + "invitationHeaderText": { + "type": "string" + }, + "privacyStatementFull": { + "type": "string" + }, + "privacyStatementParts": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.InThreadSurveyMetadata_InThreadSurveyPrivacyStatementPart" + } + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.InThreadSurveyMetadata_InThreadSurveyQuestion" + } + }, + "requestID": { + "type": "string" + }, + "simonSessionID": { + "type": "string" + }, + "simonSurveyID": { + "type": "string" + }, + "startQuestionIndex": { + "type": "integer" + }, + "surveyContinueButtonText": { + "type": "string" + }, + "surveySubmitButtonText": { + "type": "string" + }, + "surveyTitle": { + "type": "string" + }, + "tessaEvent": { + "type": "string" + }, + "tessaRootID": { + "type": "string" + }, + "tessaSessionID": { + "type": "string" + } + } + }, + "waAICommon.InThreadSurveyMetadata_InThreadSurveyOption": { + "type": "object", + "properties": { + "numericValue": { + "type": "integer" + }, + "stringValue": { + "type": "string" + }, + "textTranslated": { + "type": "string" + } + } + }, + "waAICommon.InThreadSurveyMetadata_InThreadSurveyPrivacyStatementPart": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "text": { + "type": "string" + } + } + }, + "waAICommon.InThreadSurveyMetadata_InThreadSurveyQuestion": { + "type": "object", + "properties": { + "questionID": { + "type": "string" + }, + "questionOptions": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommon.InThreadSurveyMetadata_InThreadSurveyOption" + } + }, + "questionText": { + "type": "string" + } + } + }, + "waAICommon.SessionTransparencyMetadata": { + "type": "object", + "properties": { + "disclaimerText": { + "type": "string" + }, + "hcaID": { + "type": "string" + }, + "sessionTransparencyType": { + "$ref": "#/definitions/waAICommon.SessionTransparencyType" + } + } + }, + "waAICommon.SessionTransparencyType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "SessionTransparencyType_UNKNOWN_TYPE", + "SessionTransparencyType_NY_AI_SAFETY_DISCLAIMER" + ] + }, + "waAICommonDeprecated.AIRichResponseCodeMetadata": { + "type": "object", + "properties": { + "codeBlocks": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseCodeMetadata_AIRichResponseCodeBlock" + } + }, + "codeLanguage": { + "type": "string" + } + } + }, + "waAICommonDeprecated.AIRichResponseCodeMetadata_AIRichResponseCodeBlock": { + "type": "object", + "properties": { + "codeContent": { + "type": "string" + }, + "highlightType": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseCodeMetadata_AIRichResponseCodeHighlightType" + } + } + }, + "waAICommonDeprecated.AIRichResponseCodeMetadata_AIRichResponseCodeHighlightType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_DEFAULT", + "AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_KEYWORD", + "AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_METHOD", + "AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_STRING", + "AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_NUMBER", + "AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_COMMENT" + ] + }, + "waAICommonDeprecated.AIRichResponseContentItemsMetadata": { + "type": "object", + "properties": { + "contentType": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseContentItemsMetadata_ContentType" + }, + "itemsMetadata": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseContentItemsMetadata_AIRichResponseContentItemMetadata" + } + } + } + }, + "waAICommonDeprecated.AIRichResponseContentItemsMetadata_AIRichResponseContentItemMetadata": { + "type": "object", + "properties": { + "airichResponseContentItem": { + "description": "Types that are valid to be assigned to AIRichResponseContentItem:\n\n\t*AIRichResponseContentItemsMetadata_AIRichResponseContentItemMetadata_ReelItem" + } + } + }, + "waAICommonDeprecated.AIRichResponseContentItemsMetadata_ContentType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "AIRichResponseContentItemsMetadata_DEFAULT", + "AIRichResponseContentItemsMetadata_CAROUSEL" + ] + }, + "waAICommonDeprecated.AIRichResponseDynamicMetadata": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "loopCount": { + "type": "integer" + }, + "type": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseDynamicMetadata_AIRichResponseDynamicMetadataType" + }, + "version": { + "type": "integer" + } + } + }, + "waAICommonDeprecated.AIRichResponseDynamicMetadata_AIRichResponseDynamicMetadataType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "AIRichResponseDynamicMetadata_AI_RICH_RESPONSE_DYNAMIC_METADATA_TYPE_UNKNOWN", + "AIRichResponseDynamicMetadata_AI_RICH_RESPONSE_DYNAMIC_METADATA_TYPE_IMAGE", + "AIRichResponseDynamicMetadata_AI_RICH_RESPONSE_DYNAMIC_METADATA_TYPE_GIF" + ] + }, + "waAICommonDeprecated.AIRichResponseGridImageMetadata": { + "type": "object", + "properties": { + "gridImageURL": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseImageURL" + }, + "imageURLs": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseImageURL" + } + } + } + }, + "waAICommonDeprecated.AIRichResponseImageURL": { + "type": "object", + "properties": { + "imageHighResURL": { + "type": "string" + }, + "imagePreviewURL": { + "type": "string" + }, + "sourceURL": { + "type": "string" + } + } + }, + "waAICommonDeprecated.AIRichResponseInlineImageMetadata": { + "type": "object", + "properties": { + "alignment": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseInlineImageMetadata_AIRichResponseImageAlignment" + }, + "imageText": { + "type": "string" + }, + "imageURL": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseImageURL" + }, + "tapLinkURL": { + "type": "string" + } + } + }, + "waAICommonDeprecated.AIRichResponseInlineImageMetadata_AIRichResponseImageAlignment": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "AIRichResponseInlineImageMetadata_AI_RICH_RESPONSE_IMAGE_LAYOUT_LEADING_ALIGNED", + "AIRichResponseInlineImageMetadata_AI_RICH_RESPONSE_IMAGE_LAYOUT_TRAILING_ALIGNED", + "AIRichResponseInlineImageMetadata_AI_RICH_RESPONSE_IMAGE_LAYOUT_CENTER_ALIGNED" + ] + }, + "waAICommonDeprecated.AIRichResponseLatexMetadata": { + "type": "object", + "properties": { + "expressions": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseLatexMetadata_AIRichResponseLatexExpression" + } + }, + "text": { + "type": "string" + } + } + }, + "waAICommonDeprecated.AIRichResponseLatexMetadata_AIRichResponseLatexExpression": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "fontHeight": { + "type": "number" + }, + "height": { + "type": "number" + }, + "imageBottomPadding": { + "type": "number" + }, + "imageLeadingPadding": { + "type": "number" + }, + "imageTopPadding": { + "type": "number" + }, + "imageTrailingPadding": { + "type": "number" + }, + "latexExpression": { + "type": "string" + }, + "width": { + "type": "number" + } + } + }, + "waAICommonDeprecated.AIRichResponseMapMetadata": { + "type": "object", + "properties": { + "annotations": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseMapMetadata_AIRichResponseMapAnnotation" + } + }, + "centerLatitude": { + "type": "number" + }, + "centerLongitude": { + "type": "number" + }, + "latitudeDelta": { + "type": "number" + }, + "longitudeDelta": { + "type": "number" + }, + "showInfoList": { + "type": "boolean" + } + } + }, + "waAICommonDeprecated.AIRichResponseMapMetadata_AIRichResponseMapAnnotation": { + "type": "object", + "properties": { + "annotationNumber": { + "type": "integer" + }, + "body": { + "type": "string" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, + "title": { + "type": "string" + } + } + }, + "waAICommonDeprecated.AIRichResponseMessageType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "AIRichResponseMessageType_AI_RICH_RESPONSE_TYPE_UNKNOWN", + "AIRichResponseMessageType_AI_RICH_RESPONSE_TYPE_STANDARD" + ] + }, + "waAICommonDeprecated.AIRichResponseSubMessage": { + "type": "object", + "properties": { + "codeMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseCodeMetadata" + }, + "contentItemsMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseContentItemsMetadata" + }, + "dynamicMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseDynamicMetadata" + }, + "gridImageMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseGridImageMetadata" + }, + "imageMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseInlineImageMetadata" + }, + "latexMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseLatexMetadata" + }, + "mapMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseMapMetadata" + }, + "messageText": { + "type": "string" + }, + "messageType": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseSubMessageType" + }, + "tableMetadata": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseTableMetadata" + } + } + }, + "waAICommonDeprecated.AIRichResponseSubMessageType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "x-enum-varnames": [ + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_UNKNOWN", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_GRID_IMAGE", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_TEXT", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_INLINE_IMAGE", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_TABLE", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_CODE", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_DYNAMIC", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_MAP", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_LATEX", + "AIRichResponseSubMessageType_AI_RICH_RESPONSE_CONTENT_ITEMS" + ] + }, + "waAICommonDeprecated.AIRichResponseTableMetadata": { + "type": "object", + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseTableMetadata_AIRichResponseTableRow" + } + }, + "title": { + "type": "string" + } + } + }, + "waAICommonDeprecated.AIRichResponseTableMetadata_AIRichResponseTableRow": { + "type": "object", + "properties": { + "isHeading": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "waAdv.ADVEncryptionType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ADVEncryptionType_E2EE", + "ADVEncryptionType_HOSTED" + ] + }, + "waCommon.LimitSharing": { + "type": "object", + "properties": { + "initiatedByMe": { + "type": "boolean" + }, + "limitSharingSettingTimestamp": { + "type": "integer" + }, + "sharingLimited": { + "type": "boolean" + }, + "trigger": { + "$ref": "#/definitions/waCommon.LimitSharing_Trigger" + } + } + }, + "waCommon.LimitSharing_Trigger": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "LimitSharing_UNKNOWN", + "LimitSharing_CHAT_SETTING", + "LimitSharing_BIZ_SUPPORTS_FB_HOSTING", + "LimitSharing_UNKNOWN_GROUP" + ] + }, + "waCommon.MessageKey": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "fromMe": { + "type": "boolean" + }, + "participant": { + "type": "string" + }, + "remoteJID": { + "type": "string" + } + } + }, + "waCompanionReg.DeviceProps_HistorySyncConfig": { + "type": "object", + "properties": { + "completeOnDemandReady": { + "type": "boolean" + }, + "fullSyncDaysLimit": { + "type": "integer" + }, + "fullSyncSizeMbLimit": { + "type": "integer" + }, + "initialSyncMaxMessagesPerChat": { + "type": "integer" + }, + "inlineInitialPayloadInE2EeMsg": { + "type": "boolean" + }, + "onDemandReady": { + "type": "boolean" + }, + "recentSyncDaysLimit": { + "type": "integer" + }, + "storageQuotaMb": { + "type": "integer" + }, + "supportAddOnHistorySyncMigration": { + "type": "boolean" + }, + "supportBizHostedMsg": { + "type": "boolean" + }, + "supportBotUserAgentChatHistory": { + "type": "boolean" + }, + "supportCagReactionsAndPolls": { + "type": "boolean" + }, + "supportCallLogHistory": { + "type": "boolean" + }, + "supportFbidBotChatHistory": { + "type": "boolean" + }, + "supportGroupHistory": { + "type": "boolean" + }, + "supportGuestChat": { + "type": "boolean" + }, + "supportHatchHistory": { + "type": "boolean" + }, + "supportHostedGroupMsg": { + "type": "boolean" + }, + "supportInlineContacts": { + "type": "boolean" + }, + "supportManusHistory": { + "type": "boolean" + }, + "supportMessageAssociation": { + "type": "boolean" + }, + "supportRecentSyncChunkMessageCountTuning": { + "type": "boolean" + }, + "supportedBotChannelFbids": { + "type": "array", + "items": { + "type": "string" + } + }, + "thumbnailSyncDaysLimit": { + "type": "integer" + } + } + }, + "waE2E.AIQueryFanout": { + "type": "object", + "properties": { + "message": { + "$ref": "#/definitions/waE2E.Message" + }, + "messageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "timestamp": { + "type": "integer" + } + } + }, + "waE2E.AIRichResponseMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "messageType": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseMessageType" + }, + "submessages": { + "type": "array", + "items": { + "$ref": "#/definitions/waAICommonDeprecated.AIRichResponseSubMessage" + } + }, + "unifiedResponse": { + "$ref": "#/definitions/waAICommon.AIRichResponseUnifiedResponse" + } + } + }, + "waE2E.ActionLink": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "buttonTitle": { + "type": "string" + } + } + }, + "waE2E.AlbumMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "expectedImageCount": { + "type": "integer" + }, + "expectedVideoCount": { + "type": "integer" + } + } + }, + "waE2E.AppStateFatalExceptionNotification": { + "type": "object", + "properties": { + "collectionNames": { + "type": "array", + "items": { + "type": "string" + } + }, + "timestamp": { + "type": "integer" + } + } + }, + "waE2E.AppStateSyncKey": { + "type": "object", + "properties": { + "keyData": { + "$ref": "#/definitions/waE2E.AppStateSyncKeyData" + }, + "keyID": { + "$ref": "#/definitions/waE2E.AppStateSyncKeyId" + } + } + }, + "waE2E.AppStateSyncKeyData": { + "type": "object", + "properties": { + "fingerprint": { + "$ref": "#/definitions/waE2E.AppStateSyncKeyFingerprint" + }, + "keyData": { + "type": "array", + "items": { + "type": "integer" + } + }, + "timestamp": { + "type": "integer" + } + } + }, + "waE2E.AppStateSyncKeyFingerprint": { + "type": "object", + "properties": { + "currentIndex": { + "type": "integer" + }, + "deviceIndexes": { + "type": "array", + "items": { + "type": "integer" + } + }, + "rawID": { + "type": "integer" + } + } + }, + "waE2E.AppStateSyncKeyId": { + "type": "object", + "properties": { + "keyID": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waE2E.AppStateSyncKeyRequest": { + "type": "object", + "properties": { + "keyIDs": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.AppStateSyncKeyId" + } + } + } + }, + "waE2E.AppStateSyncKeyShare": { + "type": "object", + "properties": { + "keys": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.AppStateSyncKey" + } + } + } + }, + "waE2E.AudioMessage": { + "type": "object", + "properties": { + "PTT": { + "type": "boolean" + }, + "URL": { + "type": "string" + }, + "accessibilityLabel": { + "type": "string" + }, + "backgroundArgb": { + "type": "integer" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "directPath": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "mimetype": { + "type": "string" + }, + "seconds": { + "type": "integer" + }, + "streamingSidecar": { + "type": "array", + "items": { + "type": "integer" + } + }, + "viewOnce": { + "type": "boolean" + }, + "waveform": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waE2E.BCallMessage": { + "type": "object", + "properties": { + "caption": { + "type": "string" + }, + "masterKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaType": { + "$ref": "#/definitions/waE2E.BCallMessage_MediaType" + }, + "sessionID": { + "type": "string" + } + } + }, + "waE2E.BCallMessage_MediaType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "BCallMessage_UNKNOWN", + "BCallMessage_AUDIO", + "BCallMessage_VIDEO" + ] + }, + "waE2E.ButtonsMessage": { + "type": "object", + "properties": { + "buttons": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ButtonsMessage_Button" + } + }, + "contentText": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "footerText": { + "type": "string" + }, + "header": { + "description": "Types that are valid to be assigned to Header:\n\n\t*ButtonsMessage_Text\n\t*ButtonsMessage_DocumentMessage\n\t*ButtonsMessage_ImageMessage\n\t*ButtonsMessage_VideoMessage\n\t*ButtonsMessage_LocationMessage" + }, + "headerType": { + "$ref": "#/definitions/waE2E.ButtonsMessage_HeaderType" + } + } + }, + "waE2E.ButtonsMessage_Button": { + "type": "object", + "properties": { + "buttonID": { + "type": "string" + }, + "buttonText": { + "$ref": "#/definitions/waE2E.ButtonsMessage_Button_ButtonText" + }, + "nativeFlowInfo": { + "$ref": "#/definitions/waE2E.ButtonsMessage_Button_NativeFlowInfo" + }, + "type": { + "$ref": "#/definitions/waE2E.ButtonsMessage_Button_Type" + } + } + }, + "waE2E.ButtonsMessage_Button_ButtonText": { + "type": "object", + "properties": { + "displayText": { + "type": "string" + } + } + }, + "waE2E.ButtonsMessage_Button_NativeFlowInfo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "paramsJSON": { + "type": "string" + } + } + }, + "waE2E.ButtonsMessage_Button_Type": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "ButtonsMessage_Button_UNKNOWN", + "ButtonsMessage_Button_RESPONSE", + "ButtonsMessage_Button_NATIVE_FLOW" + ] + }, + "waE2E.ButtonsMessage_HeaderType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "x-enum-varnames": [ + "ButtonsMessage_UNKNOWN", + "ButtonsMessage_EMPTY", + "ButtonsMessage_TEXT", + "ButtonsMessage_DOCUMENT", + "ButtonsMessage_IMAGE", + "ButtonsMessage_VIDEO", + "ButtonsMessage_LOCATION" + ] + }, + "waE2E.ButtonsResponseMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "response": { + "description": "Types that are valid to be assigned to Response:\n\n\t*ButtonsResponseMessage_SelectedDisplayText" + }, + "selectedButtonID": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/waE2E.ButtonsResponseMessage_Type" + } + } + }, + "waE2E.ButtonsResponseMessage_Type": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ButtonsResponseMessage_UNKNOWN", + "ButtonsResponseMessage_DISPLAY_TEXT" + ] + }, + "waE2E.Call": { + "type": "object", + "properties": { + "callEntryPoint": { + "type": "integer" + }, + "callKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "conversionData": { + "type": "array", + "items": { + "type": "integer" + } + }, + "conversionDelaySeconds": { + "type": "integer" + }, + "conversionSource": { + "type": "string" + }, + "ctwaPayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "ctwaSignals": { + "type": "string" + }, + "deeplinkPayload": { + "type": "string" + }, + "messageContextInfo": { + "$ref": "#/definitions/waE2E.MessageContextInfo" + }, + "nativeFlowCallButtonPayload": { + "type": "string" + } + } + }, + "waE2E.CallLogMessage": { + "type": "object", + "properties": { + "callOutcome": { + "$ref": "#/definitions/waE2E.CallLogMessage_CallOutcome" + }, + "callType": { + "$ref": "#/definitions/waE2E.CallLogMessage_CallType" + }, + "durationSecs": { + "type": "integer" + }, + "isVideo": { + "type": "boolean" + }, + "participants": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.CallLogMessage_CallParticipant" + } + } + } + }, + "waE2E.CallLogMessage_CallOutcome": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "x-enum-varnames": [ + "CallLogMessage_CONNECTED", + "CallLogMessage_MISSED", + "CallLogMessage_FAILED", + "CallLogMessage_REJECTED", + "CallLogMessage_ACCEPTED_ELSEWHERE", + "CallLogMessage_ONGOING", + "CallLogMessage_SILENCED_BY_DND", + "CallLogMessage_SILENCED_UNKNOWN_CALLER" + ] + }, + "waE2E.CallLogMessage_CallParticipant": { + "type": "object", + "properties": { + "JID": { + "type": "string" + }, + "callOutcome": { + "$ref": "#/definitions/waE2E.CallLogMessage_CallOutcome" + } + } + }, + "waE2E.CallLogMessage_CallType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "CallLogMessage_REGULAR", + "CallLogMessage_SCHEDULED_CALL", + "CallLogMessage_VOICE_CHAT" + ] + }, + "waE2E.CancelPaymentRequestMessage": { + "type": "object", + "properties": { + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.Chat": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "displayName": { + "type": "string" + } + } + }, + "waE2E.ChatThemeSetting": { + "type": "object", + "properties": { + "clearTheme": { + "type": "boolean" + }, + "colorSchemeID": { + "type": "string" + }, + "settingTimestampMS": { + "type": "integer" + }, + "wallpaper": { + "description": "Types that are valid to be assigned to Wallpaper:\n\n\t*ChatThemeSetting_DefaultWallpaper\n\t*ChatThemeSetting_SolidColor\n\t*ChatThemeSetting_StockImage\n\t*ChatThemeSetting_CustomImage" + } + } + }, + "waE2E.CloudAPIThreadControlNotification": { + "type": "object", + "properties": { + "consumerLid": { + "type": "string" + }, + "consumerPhoneNumber": { + "type": "string" + }, + "notificationContent": { + "$ref": "#/definitions/waE2E.CloudAPIThreadControlNotification_CloudAPIThreadControlNotificationContent" + }, + "senderNotificationTimestampMS": { + "type": "integer" + }, + "shouldSuppressNotification": { + "type": "boolean" + }, + "status": { + "$ref": "#/definitions/waE2E.CloudAPIThreadControlNotification_CloudAPIThreadControl" + } + } + }, + "waE2E.CloudAPIThreadControlNotification_CloudAPIThreadControl": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "CloudAPIThreadControlNotification_UNKNOWN", + "CloudAPIThreadControlNotification_CONTROL_PASSED", + "CloudAPIThreadControlNotification_CONTROL_TAKEN", + "CloudAPIThreadControlNotification_INFO" + ] + }, + "waE2E.CloudAPIThreadControlNotification_CloudAPIThreadControlNotificationContent": { + "type": "object", + "properties": { + "extraJSON": { + "type": "string" + }, + "handoffNotificationText": { + "type": "string" + } + } + }, + "waE2E.CommentMessage": { + "type": "object", + "properties": { + "message": { + "$ref": "#/definitions/waE2E.Message" + }, + "targetMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.ConditionalRevealMessage": { + "type": "object", + "properties": { + "conditionalRevealMessageType": { + "$ref": "#/definitions/waE2E.ConditionalRevealMessage_ConditionalRevealMessageType" + }, + "encIV": { + "type": "array", + "items": { + "type": "integer" + } + }, + "encPayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "revealKeyID": { + "type": "string" + } + } + }, + "waE2E.ConditionalRevealMessage_ConditionalRevealMessageType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ConditionalRevealMessage_UNKNOWN", + "ConditionalRevealMessage_SCHEDULED_MESSAGE" + ] + }, + "waE2E.ContactMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "displayName": { + "type": "string" + }, + "isSelfContact": { + "type": "boolean" + }, + "vcard": { + "type": "string" + } + } + }, + "waE2E.ContactsArrayMessage": { + "type": "object", + "properties": { + "contacts": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ContactMessage" + } + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "displayName": { + "type": "string" + } + } + }, + "waE2E.ContextInfo": { + "type": "object", + "properties": { + "actionLink": { + "$ref": "#/definitions/waE2E.ActionLink" + }, + "afterReadDuration": { + "type": "integer" + }, + "alwaysShowAdAttribution": { + "type": "boolean" + }, + "botMessageSharingInfo": { + "$ref": "#/definitions/waAICommon.BotMessageSharingInfo" + }, + "businessInteractionPills": { + "$ref": "#/definitions/waE2E.ContextInfo_BusinessInteractionPills" + }, + "businessMessageForwardInfo": { + "$ref": "#/definitions/waE2E.ContextInfo_BusinessMessageForwardInfo" + }, + "conversionData": { + "type": "array", + "items": { + "type": "integer" + } + }, + "conversionDelaySeconds": { + "type": "integer" + }, + "conversionSource": { + "type": "string" + }, + "crossAppSource": { + "$ref": "#/definitions/waE2E.ContextInfo_CrossAppSource" + }, + "ctwaPayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "ctwaSignals": { + "type": "string" + }, + "dataSharingContext": { + "$ref": "#/definitions/waE2E.ContextInfo_DataSharingContext" + }, + "disappearingMode": { + "$ref": "#/definitions/waE2E.DisappearingMode" + }, + "entryPointConversionApp": { + "type": "string" + }, + "entryPointConversionDelaySeconds": { + "type": "integer" + }, + "entryPointConversionExternalMedium": { + "type": "string" + }, + "entryPointConversionExternalSource": { + "type": "string" + }, + "entryPointConversionSource": { + "type": "string" + }, + "ephemeralSettingTimestamp": { + "type": "integer" + }, + "ephemeralSharedSecret": { + "type": "array", + "items": { + "type": "integer" + } + }, + "expiration": { + "type": "integer" + }, + "externalAdReply": { + "$ref": "#/definitions/waE2E.ContextInfo_ExternalAdReplyInfo" + }, + "featureEligibilities": { + "$ref": "#/definitions/waE2E.ContextInfo_FeatureEligibilities" + }, + "forwardOrigin": { + "$ref": "#/definitions/waE2E.ContextInfo_ForwardOrigin" + }, + "forwardedAiBotMessageInfo": { + "$ref": "#/definitions/waAICommon.ForwardedAIBotMessageInfo" + }, + "forwardedNewsletterMessageInfo": { + "$ref": "#/definitions/waE2E.ContextInfo_ForwardedNewsletterMessageInfo" + }, + "forwardingScore": { + "type": "integer" + }, + "groupMentions": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.GroupMention" + } + }, + "groupSubject": { + "type": "string" + }, + "isForwarded": { + "type": "boolean" + }, + "isGroupStatus": { + "type": "boolean" + }, + "isQuestion": { + "type": "boolean" + }, + "isSampled": { + "type": "boolean" + }, + "isSpoiler": { + "type": "boolean" + }, + "mediaDomainInfo": { + "$ref": "#/definitions/waE2E.MediaDomainInfo" + }, + "memberLabel": { + "$ref": "#/definitions/waE2E.MemberLabel" + }, + "mentionedJID": { + "type": "array", + "items": { + "type": "string" + } + }, + "nonJIDMentions": { + "type": "integer" + }, + "pairedMediaType": { + "$ref": "#/definitions/waE2E.ContextInfo_PairedMediaType" + }, + "parentGroupJID": { + "type": "string" + }, + "partiallySelectedContent": { + "$ref": "#/definitions/waE2E.ContextInfo_PartiallySelectedContent" + }, + "participant": { + "type": "string" + }, + "placeholderKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "posterStatusID": { + "type": "string" + }, + "questionReplyQuotedMessage": { + "$ref": "#/definitions/waE2E.ContextInfo_QuestionReplyQuotedMessage" + }, + "quotedAd": { + "$ref": "#/definitions/waE2E.ContextInfo_AdReplyInfo" + }, + "quotedMessage": { + "$ref": "#/definitions/waE2E.Message" + }, + "quotedType": { + "$ref": "#/definitions/waE2E.ContextInfo_QuotedType" + }, + "rankingVersion": { + "type": "integer" + }, + "remoteJID": { + "type": "string" + }, + "smbClientCampaignID": { + "type": "string" + }, + "smbServerCampaignID": { + "type": "string" + }, + "stanzaID": { + "type": "string" + }, + "statusAttributionType": { + "$ref": "#/definitions/waE2E.ContextInfo_StatusAttributionType" + }, + "statusAttributions": { + "type": "array", + "items": { + "$ref": "#/definitions/waStatusAttributions.StatusAttribution" + } + }, + "statusAudienceMetadata": { + "$ref": "#/definitions/waE2E.ContextInfo_StatusAudienceMetadata" + }, + "statusSourceType": { + "$ref": "#/definitions/waE2E.ContextInfo_StatusSourceType" + }, + "trustBannerAction": { + "type": "integer" + }, + "trustBannerType": { + "type": "string" + }, + "urlTrackingMap": { + "$ref": "#/definitions/waE2E.UrlTrackingMap" + }, + "utm": { + "$ref": "#/definitions/waE2E.ContextInfo_UTMInfo" + } + } + }, + "waE2E.ContextInfo_AdReplyInfo": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "advertiserName": { + "type": "string" + }, + "caption": { + "type": "string" + }, + "mediaType": { + "$ref": "#/definitions/waE2E.ContextInfo_AdReplyInfo_MediaType" + } + } + }, + "waE2E.ContextInfo_AdReplyInfo_MediaType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "ContextInfo_AdReplyInfo_NONE", + "ContextInfo_AdReplyInfo_IMAGE", + "ContextInfo_AdReplyInfo_VIDEO" + ] + }, + "waE2E.ContextInfo_BusinessInteractionPills": { + "type": "object", + "properties": { + "businessJID": { + "type": "string" + }, + "entryPoint": { + "$ref": "#/definitions/waE2E.ContextInfo_BusinessInteractionPills_EntryPoint" + }, + "pills": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ContextInfo_BusinessInteractionPills_Pill" + } + }, + "signatureEnvelope": { + "$ref": "#/definitions/waAICommon.BotSignatureVerificationMetadata" + }, + "signedPayload": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waE2E.ContextInfo_BusinessInteractionPills_EntryPoint": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "ContextInfo_BusinessInteractionPills_ENTRY_POINT_UNKNOWN", + "ContextInfo_BusinessInteractionPills_P2P_LINK_SHARE", + "ContextInfo_BusinessInteractionPills_CONTACT_CARD_SHARING", + "ContextInfo_BusinessInteractionPills_PHONE_NUMBER", + "ContextInfo_BusinessInteractionPills_STATUS", + "ContextInfo_BusinessInteractionPills_IN_THREAD_CONTEXT_CARD" + ] + }, + "waE2E.ContextInfo_BusinessInteractionPills_Pill": { + "type": "object", + "properties": { + "actionURL": { + "type": "string" + }, + "pillType": { + "$ref": "#/definitions/waE2E.ContextInfo_BusinessInteractionPills_PillType" + } + } + }, + "waE2E.ContextInfo_BusinessInteractionPills_PillType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12 + ], + "x-enum-varnames": [ + "ContextInfo_BusinessInteractionPills_UNKNOWN", + "ContextInfo_BusinessInteractionPills_VIEW_BUSINESS", + "ContextInfo_BusinessInteractionPills_CHAT", + "ContextInfo_BusinessInteractionPills_CALL", + "ContextInfo_BusinessInteractionPills_CATALOG", + "ContextInfo_BusinessInteractionPills_CHANNEL", + "ContextInfo_BusinessInteractionPills_BOOK_APPOINTMENT", + "ContextInfo_BusinessInteractionPills_OFFERS", + "ContextInfo_BusinessInteractionPills_BESTSELLERS", + "ContextInfo_BusinessInteractionPills_MENU", + "ContextInfo_BusinessInteractionPills_ABOUT", + "ContextInfo_BusinessInteractionPills_SHOP", + "ContextInfo_BusinessInteractionPills_ORDER" + ] + }, + "waE2E.ContextInfo_BusinessMessageForwardInfo": { + "type": "object", + "properties": { + "businessOwnerJID": { + "type": "string" + } + } + }, + "waE2E.ContextInfo_CrossAppSource": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "ContextInfo_CROSS_APP_SOURCE_UNKNOWN", + "ContextInfo_CROSS_APP_SOURCE_INSTAGRAM", + "ContextInfo_CROSS_APP_SOURCE_FACEBOOK" + ] + }, + "waE2E.ContextInfo_DataSharingContext": { + "type": "object", + "properties": { + "dataSharingFlags": { + "type": "integer" + }, + "encryptedSignalTokenConsented": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ContextInfo_DataSharingContext_Parameters" + } + }, + "showMmDisclosure": { + "type": "boolean" + } + } + }, + "waE2E.ContextInfo_DataSharingContext_Parameters": { + "type": "object", + "properties": { + "contents": { + "$ref": "#/definitions/waE2E.ContextInfo_DataSharingContext_Parameters" + }, + "floatData": { + "type": "number" + }, + "intData": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "stringData": { + "type": "string" + } + } + }, + "waE2E.ContextInfo_ExternalAdReplyInfo": { + "type": "object", + "properties": { + "adContextPreviewDismissed": { + "type": "boolean" + }, + "adPreviewURL": { + "type": "string" + }, + "adType": { + "$ref": "#/definitions/waE2E.ContextInfo_ExternalAdReplyInfo_AdType" + }, + "agmHeaderInteractionStrategy": { + "type": "integer" + }, + "agmSubtitleStrategy": { + "type": "integer" + }, + "agmThumbnailStrategy": { + "type": "integer" + }, + "agmTitleStrategy": { + "type": "integer" + }, + "automatedGreetingMessageCtaType": { + "type": "string" + }, + "automatedGreetingMessageShown": { + "type": "boolean" + }, + "body": { + "type": "string" + }, + "clickToWhatsappCall": { + "type": "boolean" + }, + "containsAutoReply": { + "type": "boolean" + }, + "containsCtwaFlowsAutoReply": { + "type": "boolean" + }, + "ctaPayload": { + "type": "string" + }, + "ctwaClid": { + "type": "string" + }, + "disableNudge": { + "type": "boolean" + }, + "greetingMessageBody": { + "type": "string" + }, + "mediaType": { + "$ref": "#/definitions/waE2E.ContextInfo_ExternalAdReplyInfo_MediaType" + }, + "mediaURL": { + "type": "string" + }, + "originalImageURL": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "renderLargerThumbnail": { + "type": "boolean" + }, + "showAdAttribution": { + "type": "boolean" + }, + "sourceApp": { + "type": "string" + }, + "sourceID": { + "type": "string" + }, + "sourceType": { + "type": "string" + }, + "sourceURL": { + "type": "string" + }, + "thumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailURL": { + "type": "string" + }, + "title": { + "type": "string" + }, + "wtwaAdFormat": { + "type": "boolean" + }, + "wtwaWebsiteURL": { + "type": "string" + } + } + }, + "waE2E.ContextInfo_ExternalAdReplyInfo_AdType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ContextInfo_ExternalAdReplyInfo_CTWA", + "ContextInfo_ExternalAdReplyInfo_CAWC" + ] + }, + "waE2E.ContextInfo_ExternalAdReplyInfo_MediaType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "ContextInfo_ExternalAdReplyInfo_NONE", + "ContextInfo_ExternalAdReplyInfo_IMAGE", + "ContextInfo_ExternalAdReplyInfo_VIDEO" + ] + }, + "waE2E.ContextInfo_FeatureEligibilities": { + "type": "object", + "properties": { + "canBeReshared": { + "type": "boolean" + }, + "canReceiveMultiReact": { + "type": "boolean" + }, + "canRequestFeedback": { + "type": "boolean" + }, + "cannotBeRanked": { + "type": "boolean" + }, + "cannotBeReactedTo": { + "type": "boolean" + } + } + }, + "waE2E.ContextInfo_ForwardOrigin": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "ContextInfo_UNKNOWN", + "ContextInfo_CHAT", + "ContextInfo_STATUS", + "ContextInfo_CHANNELS", + "ContextInfo_META_AI", + "ContextInfo_UGC" + ] + }, + "waE2E.ContextInfo_ForwardedNewsletterMessageInfo": { + "type": "object", + "properties": { + "accessibilityText": { + "type": "string" + }, + "contentType": { + "$ref": "#/definitions/waE2E.ContextInfo_ForwardedNewsletterMessageInfo_ContentType" + }, + "newsletterJID": { + "type": "string" + }, + "newsletterName": { + "type": "string" + }, + "profileName": { + "type": "string" + }, + "serverMessageID": { + "type": "integer" + } + } + }, + "waE2E.ContextInfo_ForwardedNewsletterMessageInfo_ContentType": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "ContextInfo_ForwardedNewsletterMessageInfo_UPDATE", + "ContextInfo_ForwardedNewsletterMessageInfo_UPDATE_CARD", + "ContextInfo_ForwardedNewsletterMessageInfo_LINK_CARD" + ] + }, + "waE2E.ContextInfo_PairedMediaType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "x-enum-varnames": [ + "ContextInfo_NOT_PAIRED_MEDIA", + "ContextInfo_SD_VIDEO_PARENT", + "ContextInfo_HD_VIDEO_CHILD", + "ContextInfo_SD_IMAGE_PARENT", + "ContextInfo_HD_IMAGE_CHILD", + "ContextInfo_MOTION_PHOTO_PARENT", + "ContextInfo_MOTION_PHOTO_CHILD", + "ContextInfo_HEVC_VIDEO_PARENT", + "ContextInfo_HEVC_VIDEO_CHILD" + ] + }, + "waE2E.ContextInfo_PartiallySelectedContent": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + } + }, + "waE2E.ContextInfo_QuestionReplyQuotedMessage": { + "type": "object", + "properties": { + "quotedQuestion": { + "$ref": "#/definitions/waE2E.Message" + }, + "quotedResponse": { + "$ref": "#/definitions/waE2E.Message" + }, + "serverQuestionID": { + "type": "integer" + } + } + }, + "waE2E.ContextInfo_QuotedType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ContextInfo_EXPLICIT", + "ContextInfo_AUTO" + ] + }, + "waE2E.ContextInfo_StatusAttributionType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "x-enum-varnames": [ + "ContextInfo_NONE", + "ContextInfo_RESHARED_FROM_MENTION", + "ContextInfo_RESHARED_FROM_POST", + "ContextInfo_RESHARED_FROM_POST_MANY_TIMES", + "ContextInfo_FORWARDED_FROM_STATUS" + ] + }, + "waE2E.ContextInfo_StatusAudienceMetadata": { + "type": "object", + "properties": { + "audienceType": { + "$ref": "#/definitions/waE2E.ContextInfo_StatusAudienceMetadata_AudienceType" + }, + "listEmoji": { + "type": "string" + }, + "listName": { + "type": "string" + } + } + }, + "waE2E.ContextInfo_StatusAudienceMetadata_AudienceType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ContextInfo_StatusAudienceMetadata_UNKNOWN", + "ContextInfo_StatusAudienceMetadata_CLOSE_FRIENDS" + ] + }, + "waE2E.ContextInfo_StatusSourceType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "ContextInfo_IMAGE", + "ContextInfo_VIDEO", + "ContextInfo_GIF", + "ContextInfo_AUDIO", + "ContextInfo_TEXT", + "ContextInfo_MUSIC_STANDALONE" + ] + }, + "waE2E.ContextInfo_UTMInfo": { + "type": "object", + "properties": { + "utmCampaign": { + "type": "string" + }, + "utmSource": { + "type": "string" + } + } + }, + "waE2E.DeclinePaymentRequestMessage": { + "type": "object", + "properties": { + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.DeviceListMetadata": { + "type": "object", + "properties": { + "receiverAccountType": { + "$ref": "#/definitions/waAdv.ADVEncryptionType" + }, + "recipientKeyHash": { + "type": "array", + "items": { + "type": "integer" + } + }, + "recipientKeyIndexes": { + "type": "array", + "items": { + "type": "integer" + } + }, + "recipientTimestamp": { + "type": "integer" + }, + "senderAccountType": { + "$ref": "#/definitions/waAdv.ADVEncryptionType" + }, + "senderKeyHash": { + "type": "array", + "items": { + "type": "integer" + } + }, + "senderKeyIndexes": { + "type": "array", + "items": { + "type": "integer" + } + }, + "senderTimestamp": { + "type": "integer" + } + } + }, + "waE2E.DeviceSentMessage": { + "type": "object", + "properties": { + "destinationJID": { + "type": "string" + }, + "message": { + "$ref": "#/definitions/waE2E.Message" + }, + "phash": { + "type": "string" + } + } + }, + "waE2E.DisappearingMode": { + "type": "object", + "properties": { + "initiatedByMe": { + "type": "boolean" + }, + "initiator": { + "$ref": "#/definitions/waE2E.DisappearingMode_Initiator" + }, + "initiatorDeviceJID": { + "type": "string" + }, + "trigger": { + "$ref": "#/definitions/waE2E.DisappearingMode_Trigger" + } + } + }, + "waE2E.DisappearingMode_Initiator": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "DisappearingMode_CHANGED_IN_CHAT", + "DisappearingMode_INITIATED_BY_ME", + "DisappearingMode_INITIATED_BY_OTHER", + "DisappearingMode_BIZ_UPGRADE_FB_HOSTING" + ] + }, + "waE2E.DisappearingMode_Trigger": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "DisappearingMode_UNKNOWN", + "DisappearingMode_CHAT_SETTING", + "DisappearingMode_ACCOUNT_SETTING", + "DisappearingMode_BULK_CHANGE", + "DisappearingMode_BIZ_SUPPORTS_FB_HOSTING", + "DisappearingMode_UNKNOWN_GROUPS" + ] + }, + "waE2E.DocumentMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "URL": { + "type": "string" + }, + "accessibilityLabel": { + "type": "string" + }, + "caption": { + "type": "string" + }, + "contactVcard": { + "type": "boolean" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "directPath": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileName": { + "type": "string" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "mimetype": { + "type": "string" + }, + "pageCount": { + "type": "integer" + }, + "thumbnailDirectPath": { + "type": "string" + }, + "thumbnailEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailHeight": { + "type": "integer" + }, + "thumbnailSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailWidth": { + "type": "integer" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.EmbeddedContent": { + "type": "object", + "properties": { + "content": { + "description": "Types that are valid to be assigned to Content:\n\n\t*EmbeddedContent_EmbeddedMessage\n\t*EmbeddedContent_EmbeddedMusic" + } + } + }, + "waE2E.EmbeddedMusic": { + "type": "object", + "properties": { + "artistAttribution": { + "type": "string" + }, + "artworkDirectPath": { + "type": "string" + }, + "artworkEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "artworkMediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "artworkSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "author": { + "type": "string" + }, + "countryBlocklist": { + "type": "array", + "items": { + "type": "integer" + } + }, + "derivedContentStartTimeInMS": { + "type": "integer" + }, + "isExplicit": { + "type": "boolean" + }, + "musicContentMediaID": { + "type": "string" + }, + "musicSongStartTimeInMS": { + "type": "integer" + }, + "overlapDurationInMS": { + "type": "integer" + }, + "songID": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.EncCommentMessage": { + "type": "object", + "properties": { + "encIV": { + "type": "array", + "items": { + "type": "integer" + } + }, + "encPayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "targetMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.EncEventResponseMessage": { + "type": "object", + "properties": { + "encIV": { + "type": "array", + "items": { + "type": "integer" + } + }, + "encPayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "eventCreationMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.EncReactionMessage": { + "type": "object", + "properties": { + "encIV": { + "type": "array", + "items": { + "type": "integer" + } + }, + "encPayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "targetMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.EventInviteMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "callLink": { + "type": "string" + }, + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "endTime": { + "type": "integer" + }, + "eventID": { + "type": "string" + }, + "eventTitle": { + "type": "string" + }, + "isCanceled": { + "type": "boolean" + }, + "startTime": { + "type": "integer" + } + } + }, + "waE2E.EventMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "description": { + "type": "string" + }, + "endTime": { + "type": "integer" + }, + "extraGuestsAllowed": { + "type": "boolean" + }, + "hasReminder": { + "type": "boolean" + }, + "isCanceled": { + "type": "boolean" + }, + "isScheduleCall": { + "type": "boolean" + }, + "joinLink": { + "type": "string" + }, + "location": { + "$ref": "#/definitions/waE2E.LocationMessage" + }, + "name": { + "type": "string" + }, + "reminderOffsetSec": { + "type": "integer" + }, + "startTime": { + "type": "integer" + } + } + }, + "waE2E.ExtendedTextMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "backgroundArgb": { + "type": "integer" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "description": { + "type": "string" + }, + "doNotPlayInline": { + "type": "boolean" + }, + "endCardTiles": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.VideoEndCard" + } + }, + "faviconMMSMetadata": { + "$ref": "#/definitions/waE2E.MMSThumbnailMetadata" + }, + "font": { + "$ref": "#/definitions/waE2E.ExtendedTextMessage_FontType" + }, + "inviteLinkGroupType": { + "$ref": "#/definitions/waE2E.ExtendedTextMessage_InviteLinkGroupType" + }, + "inviteLinkGroupTypeV2": { + "$ref": "#/definitions/waE2E.ExtendedTextMessage_InviteLinkGroupType" + }, + "inviteLinkParentGroupSubjectV2": { + "type": "string" + }, + "inviteLinkParentGroupThumbnailV2": { + "type": "array", + "items": { + "type": "integer" + } + }, + "linkPreviewMetadata": { + "$ref": "#/definitions/waE2E.LinkPreviewMetadata" + }, + "matchedText": { + "type": "string" + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "musicMetadata": { + "$ref": "#/definitions/waE2E.EmbeddedMusic" + }, + "paymentExtendedMetadata": { + "$ref": "#/definitions/waE2E.PaymentExtendedMetadata" + }, + "paymentLinkMetadata": { + "$ref": "#/definitions/waE2E.PaymentLinkMetadata" + }, + "previewType": { + "$ref": "#/definitions/waE2E.ExtendedTextMessage_PreviewType" + }, + "text": { + "type": "string" + }, + "textArgb": { + "type": "integer" + }, + "thumbnailDirectPath": { + "type": "string" + }, + "thumbnailEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailHeight": { + "type": "integer" + }, + "thumbnailSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailWidth": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "videoContentURL": { + "type": "string" + }, + "videoHeight": { + "type": "integer" + }, + "videoWidth": { + "type": "integer" + }, + "viewOnce": { + "type": "boolean" + } + } + }, + "waE2E.ExtendedTextMessage_FontType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 6, + 7, + 8, + 9, + 10 + ], + "x-enum-varnames": [ + "ExtendedTextMessage_SYSTEM", + "ExtendedTextMessage_SYSTEM_TEXT", + "ExtendedTextMessage_FB_SCRIPT", + "ExtendedTextMessage_SYSTEM_BOLD", + "ExtendedTextMessage_MORNINGBREEZE_REGULAR", + "ExtendedTextMessage_CALISTOGA_REGULAR", + "ExtendedTextMessage_EXO2_EXTRABOLD", + "ExtendedTextMessage_COURIERPRIME_BOLD" + ] + }, + "waE2E.ExtendedTextMessage_InviteLinkGroupType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "ExtendedTextMessage_DEFAULT", + "ExtendedTextMessage_PARENT", + "ExtendedTextMessage_SUB", + "ExtendedTextMessage_DEFAULT_SUB" + ] + }, + "waE2E.ExtendedTextMessage_PreviewType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 4, + 5, + 6, + 7 + ], + "x-enum-varnames": [ + "ExtendedTextMessage_NONE", + "ExtendedTextMessage_VIDEO", + "ExtendedTextMessage_PLACEHOLDER", + "ExtendedTextMessage_IMAGE", + "ExtendedTextMessage_PAYMENT_LINKS", + "ExtendedTextMessage_PROFILE" + ] + }, + "waE2E.FullHistorySyncOnDemandConfig": { + "type": "object", + "properties": { + "historyDurationDays": { + "type": "integer" + }, + "historyFromTimestamp": { + "type": "integer" + } + } + }, + "waE2E.FullHistorySyncOnDemandRequestMetadata": { + "type": "object", + "properties": { + "businessProduct": { + "type": "string" + }, + "opaqueClientData": { + "type": "array", + "items": { + "type": "integer" + } + }, + "requestID": { + "type": "string" + } + } + }, + "waE2E.FutureProofMessage": { + "type": "object", + "properties": { + "message": { + "$ref": "#/definitions/waE2E.Message" + } + } + }, + "waE2E.GroupInviteMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "groupJID": { + "type": "string" + }, + "groupName": { + "type": "string" + }, + "groupType": { + "$ref": "#/definitions/waE2E.GroupInviteMessage_GroupType" + }, + "inviteCode": { + "type": "string" + }, + "inviteExpiration": { + "type": "integer" + } + } + }, + "waE2E.GroupInviteMessage_GroupType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "GroupInviteMessage_DEFAULT", + "GroupInviteMessage_PARENT" + ] + }, + "waE2E.GroupMention": { + "type": "object", + "properties": { + "groupJID": { + "type": "string" + }, + "groupSubject": { + "type": "string" + } + } + }, + "waE2E.GroupRootKeyShare": { + "type": "object", + "properties": { + "keys": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.GroupRootKeyShareEntry" + } + } + } + }, + "waE2E.GroupRootKeyShareEntry": { + "type": "object", + "properties": { + "createdTimestampMS": { + "type": "integer" + }, + "expiryTimestampMS": { + "type": "integer" + }, + "groupRootKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "keyID": { + "type": "string" + } + } + }, + "waE2E.HighlyStructuredMessage": { + "type": "object", + "properties": { + "deterministicLc": { + "type": "string" + }, + "deterministicLg": { + "type": "string" + }, + "elementName": { + "type": "string" + }, + "fallbackLc": { + "type": "string" + }, + "fallbackLg": { + "type": "string" + }, + "hydratedHsm": { + "$ref": "#/definitions/waE2E.TemplateMessage" + }, + "localizableParams": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.HighlyStructuredMessage_HSMLocalizableParameter" + } + }, + "namespace": { + "type": "string" + }, + "params": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "waE2E.HighlyStructuredMessage_HSMLocalizableParameter": { + "type": "object", + "properties": { + "default": { + "type": "string" + }, + "paramOneof": { + "description": "Types that are valid to be assigned to ParamOneof:\n\n\t*HighlyStructuredMessage_HSMLocalizableParameter_Currency\n\t*HighlyStructuredMessage_HSMLocalizableParameter_DateTime" + } + } + }, + "waE2E.HistorySyncMessageAccessStatus": { + "type": "object", + "properties": { + "completeAccessGranted": { + "type": "boolean" + } + } + }, + "waE2E.HistorySyncNotification": { + "type": "object", + "properties": { + "chunkOrder": { + "type": "integer" + }, + "directPath": { + "type": "string" + }, + "encHandle": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fullHistorySyncOnDemandRequestMetadata": { + "$ref": "#/definitions/waE2E.FullHistorySyncOnDemandRequestMetadata" + }, + "initialHistBootstrapInlinePayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "messageAccessStatus": { + "$ref": "#/definitions/waE2E.HistorySyncMessageAccessStatus" + }, + "oldestMsgInChunkTimestampSec": { + "type": "integer" + }, + "originalMessageID": { + "type": "string" + }, + "peerDataRequestSessionID": { + "type": "string" + }, + "progress": { + "type": "integer" + }, + "syncType": { + "$ref": "#/definitions/waE2E.HistorySyncType" + } + } + }, + "waE2E.HistorySyncType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "x-enum-varnames": [ + "HistorySyncType_INITIAL_BOOTSTRAP", + "HistorySyncType_INITIAL_STATUS_V3", + "HistorySyncType_FULL", + "HistorySyncType_RECENT", + "HistorySyncType_PUSH_NAME", + "HistorySyncType_NON_BLOCKING_DATA", + "HistorySyncType_ON_DEMAND", + "HistorySyncType_NO_HISTORY", + "HistorySyncType_MESSAGE_ACCESS_STATUS" + ] + }, + "waE2E.HydratedTemplateButton": { + "type": "object", + "properties": { + "hydratedButton": { + "description": "Types that are valid to be assigned to HydratedButton:\n\n\t*HydratedTemplateButton_QuickReplyButton\n\t*HydratedTemplateButton_UrlButton\n\t*HydratedTemplateButton_CallButton" + }, + "index": { + "type": "integer" + } + } + }, + "waE2E.ImageMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "URL": { + "type": "string" + }, + "accessibilityLabel": { + "type": "string" + }, + "annotations": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.InteractiveAnnotation" + } + }, + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "directPath": { + "type": "string" + }, + "experimentGroupID": { + "type": "integer" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "firstScanLength": { + "type": "integer" + }, + "firstScanSidecar": { + "type": "array", + "items": { + "type": "integer" + } + }, + "height": { + "type": "integer" + }, + "imageSourceType": { + "$ref": "#/definitions/waE2E.ImageMessage_ImageSourceType" + }, + "interactiveAnnotations": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.InteractiveAnnotation" + } + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "midQualityFileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "midQualityFileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mimetype": { + "type": "string" + }, + "qrURL": { + "type": "string" + }, + "scanLengths": { + "type": "array", + "items": { + "type": "integer" + } + }, + "scansSidecar": { + "type": "array", + "items": { + "type": "integer" + } + }, + "staticURL": { + "type": "string" + }, + "thumbnailDirectPath": { + "type": "string" + }, + "thumbnailEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "viewOnce": { + "type": "boolean" + }, + "width": { + "type": "integer" + } + } + }, + "waE2E.ImageMessage_ImageSourceType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "ImageMessage_USER_IMAGE", + "ImageMessage_AI_GENERATED", + "ImageMessage_AI_MODIFIED", + "ImageMessage_RASTERIZED_TEXT_STATUS" + ] + }, + "waE2E.InitialSecurityNotificationSettingSync": { + "type": "object", + "properties": { + "securityNotificationEnabled": { + "type": "boolean" + } + } + }, + "waE2E.InsightDeliveryState": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "x-enum-varnames": [ + "InsightDeliveryState_SENT", + "InsightDeliveryState_DELIVERED", + "InsightDeliveryState_READ", + "InsightDeliveryState_REPLIED", + "InsightDeliveryState_QUICK_REPLIED" + ] + }, + "waE2E.InteractiveAnnotation": { + "type": "object", + "properties": { + "action": { + "description": "Types that are valid to be assigned to Action:\n\n\t*InteractiveAnnotation_Location\n\t*InteractiveAnnotation_Newsletter\n\t*InteractiveAnnotation_EmbeddedAction\n\t*InteractiveAnnotation_TapAction" + }, + "embeddedContent": { + "$ref": "#/definitions/waE2E.EmbeddedContent" + }, + "polygonVertices": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.Point" + } + }, + "shouldSkipConfirmation": { + "type": "boolean" + }, + "statusLinkType": { + "$ref": "#/definitions/waE2E.InteractiveAnnotation_StatusLinkType" + } + } + }, + "waE2E.InteractiveAnnotation_StatusLinkType": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "InteractiveAnnotation_RASTERIZED_LINK_PREVIEW", + "InteractiveAnnotation_RASTERIZED_LINK_TRUNCATED", + "InteractiveAnnotation_RASTERIZED_LINK_FULL_URL" + ] + }, + "waE2E.InteractiveMessage": { + "type": "object", + "properties": { + "bloksWidget": { + "$ref": "#/definitions/waE2E.InteractiveMessage_BloksWidget" + }, + "body": { + "$ref": "#/definitions/waE2E.InteractiveMessage_Body" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "footer": { + "$ref": "#/definitions/waE2E.InteractiveMessage_Footer" + }, + "header": { + "$ref": "#/definitions/waE2E.InteractiveMessage_Header" + }, + "interactiveMessage": { + "description": "Types that are valid to be assigned to InteractiveMessage:\n\n\t*InteractiveMessage_ShopStorefrontMessage\n\t*InteractiveMessage_CollectionMessage_\n\t*InteractiveMessage_NativeFlowMessage_\n\t*InteractiveMessage_CarouselMessage_" + }, + "urlTrackingMap": { + "$ref": "#/definitions/waE2E.UrlTrackingMap" + } + } + }, + "waE2E.InteractiveMessage_BloksWidget": { + "type": "object", + "properties": { + "data": { + "type": "string" + }, + "fallback": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "waE2E.InteractiveMessage_Body": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + } + }, + "waE2E.InteractiveMessage_Footer": { + "type": "object", + "properties": { + "hasMediaAttachment": { + "type": "boolean" + }, + "media": { + "description": "Types that are valid to be assigned to Media:\n\n\t*InteractiveMessage_Footer_AudioMessage" + }, + "text": { + "type": "string" + } + } + }, + "waE2E.InteractiveMessage_Header": { + "type": "object", + "properties": { + "bloksWidget": { + "$ref": "#/definitions/waE2E.InteractiveMessage_BloksWidget" + }, + "hasMediaAttachment": { + "type": "boolean" + }, + "media": { + "description": "Types that are valid to be assigned to Media:\n\n\t*InteractiveMessage_Header_DocumentMessage\n\t*InteractiveMessage_Header_ImageMessage\n\t*InteractiveMessage_Header_JPEGThumbnail\n\t*InteractiveMessage_Header_VideoMessage\n\t*InteractiveMessage_Header_LocationMessage\n\t*InteractiveMessage_Header_ProductMessage" + }, + "subtitle": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.InteractiveResponseMessage": { + "type": "object", + "properties": { + "body": { + "$ref": "#/definitions/waE2E.InteractiveResponseMessage_Body" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "interactiveResponseMessage": { + "description": "Types that are valid to be assigned to InteractiveResponseMessage:\n\n\t*InteractiveResponseMessage_NativeFlowResponseMessage_" + } + } + }, + "waE2E.InteractiveResponseMessage_Body": { + "type": "object", + "properties": { + "format": { + "$ref": "#/definitions/waE2E.InteractiveResponseMessage_Body_Format" + }, + "text": { + "type": "string" + } + } + }, + "waE2E.InteractiveResponseMessage_Body_Format": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "InteractiveResponseMessage_Body_DEFAULT", + "InteractiveResponseMessage_Body_EXTENSIONS_1" + ] + }, + "waE2E.InvoiceMessage": { + "type": "object", + "properties": { + "attachmentDirectPath": { + "type": "string" + }, + "attachmentFileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "attachmentFileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "attachmentJPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "attachmentMediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "attachmentMediaKeyTimestamp": { + "type": "integer" + }, + "attachmentMimetype": { + "type": "string" + }, + "attachmentType": { + "$ref": "#/definitions/waE2E.InvoiceMessage_AttachmentType" + }, + "note": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "waE2E.InvoiceMessage_AttachmentType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "InvoiceMessage_IMAGE", + "InvoiceMessage_PDF" + ] + }, + "waE2E.KeepInChatMessage": { + "type": "object", + "properties": { + "keepType": { + "$ref": "#/definitions/waE2E.KeepType" + }, + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "timestampMS": { + "type": "integer" + } + } + }, + "waE2E.KeepType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "KeepType_UNKNOWN_KEEP_TYPE", + "KeepType_KEEP_FOR_ALL", + "KeepType_UNDO_KEEP_FOR_ALL" + ] + }, + "waE2E.LIDMigrationMappingSyncMessage": { + "type": "object", + "properties": { + "encodedMappingPayload": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waE2E.LinkPreviewMetadata": { + "type": "object", + "properties": { + "fbExperimentID": { + "type": "integer" + }, + "linkInlineVideoMuted": { + "type": "boolean" + }, + "linkMediaDuration": { + "type": "integer" + }, + "musicMetadata": { + "$ref": "#/definitions/waE2E.EmbeddedMusic" + }, + "paymentLinkMetadata": { + "$ref": "#/definitions/waE2E.PaymentLinkMetadata" + }, + "socialMediaPostType": { + "$ref": "#/definitions/waE2E.LinkPreviewMetadata_SocialMediaPostType" + }, + "urlMetadata": { + "$ref": "#/definitions/waE2E.URLMetadata" + }, + "videoContentCaption": { + "type": "string" + }, + "videoContentURL": { + "type": "string" + } + } + }, + "waE2E.LinkPreviewMetadata_SocialMediaPostType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "LinkPreviewMetadata_NONE", + "LinkPreviewMetadata_REEL", + "LinkPreviewMetadata_LIVE_VIDEO", + "LinkPreviewMetadata_LONG_VIDEO", + "LinkPreviewMetadata_SINGLE_IMAGE", + "LinkPreviewMetadata_CAROUSEL" + ] + }, + "waE2E.ListMessage": { + "type": "object", + "properties": { + "buttonText": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "description": { + "type": "string" + }, + "footerText": { + "type": "string" + }, + "listType": { + "$ref": "#/definitions/waE2E.ListMessage_ListType" + }, + "productListInfo": { + "$ref": "#/definitions/waE2E.ListMessage_ProductListInfo" + }, + "sections": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ListMessage_Section" + } + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ListMessage_ListType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "ListMessage_UNKNOWN", + "ListMessage_SINGLE_SELECT", + "ListMessage_PRODUCT_LIST" + ] + }, + "waE2E.ListMessage_Product": { + "type": "object", + "properties": { + "productID": { + "type": "string" + } + } + }, + "waE2E.ListMessage_ProductListHeaderImage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "productID": { + "type": "string" + } + } + }, + "waE2E.ListMessage_ProductListInfo": { + "type": "object", + "properties": { + "businessOwnerJID": { + "type": "string" + }, + "headerImage": { + "$ref": "#/definitions/waE2E.ListMessage_ProductListHeaderImage" + }, + "productSections": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ListMessage_ProductSection" + } + } + } + }, + "waE2E.ListMessage_ProductSection": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ListMessage_Product" + } + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ListMessage_Row": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "rowID": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ListMessage_Section": { + "type": "object", + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ListMessage_Row" + } + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ListResponseMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "description": { + "type": "string" + }, + "listType": { + "$ref": "#/definitions/waE2E.ListResponseMessage_ListType" + }, + "singleSelectReply": { + "$ref": "#/definitions/waE2E.ListResponseMessage_SingleSelectReply" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ListResponseMessage_ListType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ListResponseMessage_UNKNOWN", + "ListResponseMessage_SINGLE_SELECT" + ] + }, + "waE2E.ListResponseMessage_SingleSelectReply": { + "type": "object", + "properties": { + "selectedRowID": { + "type": "string" + } + } + }, + "waE2E.LiveLocationMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "accuracyInMeters": { + "type": "integer" + }, + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "degreesClockwiseFromMagneticNorth": { + "type": "integer" + }, + "degreesLatitude": { + "type": "number" + }, + "degreesLongitude": { + "type": "number" + }, + "sequenceNumber": { + "type": "integer" + }, + "speedInMps": { + "type": "number" + }, + "timeOffset": { + "type": "integer" + } + } + }, + "waE2E.LocationMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "URL": { + "type": "string" + }, + "accuracyInMeters": { + "type": "integer" + }, + "address": { + "type": "string" + }, + "comment": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "degreesClockwiseFromMagneticNorth": { + "type": "integer" + }, + "degreesLatitude": { + "type": "number" + }, + "degreesLongitude": { + "type": "number" + }, + "isLive": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "speedInMps": { + "type": "number" + } + } + }, + "waE2E.MMSThumbnailMetadata": { + "type": "object", + "properties": { + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "thumbnailDirectPath": { + "type": "string" + }, + "thumbnailEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailHeight": { + "type": "integer" + }, + "thumbnailSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailWidth": { + "type": "integer" + } + } + }, + "waE2E.MediaDomainInfo": { + "type": "object", + "properties": { + "e2EeMediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyDomain": { + "$ref": "#/definitions/waE2E.MediaKeyDomain" + } + } + }, + "waE2E.MediaKeyDomain": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "MediaKeyDomain_MEDIA_KEY_DOMAIN_UNKNOWN", + "MediaKeyDomain_MEDIA_KEY_DOMAIN_E2EE", + "MediaKeyDomain_MEDIA_KEY_DOMAIN_NON_E2EE" + ] + }, + "waE2E.MediaNotifyMessage": { + "type": "object", + "properties": { + "expressPathURL": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + } + } + }, + "waE2E.MemberLabel": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "labelTimestamp": { + "type": "integer" + } + } + }, + "waE2E.Message": { + "type": "object", + "properties": { + "albumMessage": { + "$ref": "#/definitions/waE2E.AlbumMessage" + }, + "associatedChildMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "audioMessage": { + "$ref": "#/definitions/waE2E.AudioMessage" + }, + "bcallMessage": { + "$ref": "#/definitions/waE2E.BCallMessage" + }, + "botForwardedMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "botInvokeMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "botTaskMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "buttonsMessage": { + "$ref": "#/definitions/waE2E.ButtonsMessage" + }, + "buttonsResponseMessage": { + "$ref": "#/definitions/waE2E.ButtonsResponseMessage" + }, + "call": { + "$ref": "#/definitions/waE2E.Call" + }, + "callLogMesssage": { + "$ref": "#/definitions/waE2E.CallLogMessage" + }, + "cancelPaymentRequestMessage": { + "$ref": "#/definitions/waE2E.CancelPaymentRequestMessage" + }, + "chat": { + "$ref": "#/definitions/waE2E.Chat" + }, + "commentMessage": { + "$ref": "#/definitions/waE2E.CommentMessage" + }, + "conditionalRevealMessage": { + "$ref": "#/definitions/waE2E.ConditionalRevealMessage" + }, + "contactMessage": { + "$ref": "#/definitions/waE2E.ContactMessage" + }, + "contactsArrayMessage": { + "$ref": "#/definitions/waE2E.ContactsArrayMessage" + }, + "conversation": { + "type": "string" + }, + "declinePaymentRequestMessage": { + "$ref": "#/definitions/waE2E.DeclinePaymentRequestMessage" + }, + "deviceSentMessage": { + "$ref": "#/definitions/waE2E.DeviceSentMessage" + }, + "documentMessage": { + "$ref": "#/definitions/waE2E.DocumentMessage" + }, + "documentWithCaptionMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "editedMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "encCommentMessage": { + "$ref": "#/definitions/waE2E.EncCommentMessage" + }, + "encEventResponseMessage": { + "$ref": "#/definitions/waE2E.EncEventResponseMessage" + }, + "encReactionMessage": { + "$ref": "#/definitions/waE2E.EncReactionMessage" + }, + "ephemeralMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "eventCoverImage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "eventInviteMessage": { + "$ref": "#/definitions/waE2E.EventInviteMessage" + }, + "eventMessage": { + "$ref": "#/definitions/waE2E.EventMessage" + }, + "extendedTextMessage": { + "$ref": "#/definitions/waE2E.ExtendedTextMessage" + }, + "fastRatchetKeySenderKeyDistributionMessage": { + "$ref": "#/definitions/waE2E.SenderKeyDistributionMessage" + }, + "groupInviteMessage": { + "$ref": "#/definitions/waE2E.GroupInviteMessage" + }, + "groupMentionedMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "groupRootKeyShare": { + "$ref": "#/definitions/waE2E.GroupRootKeyShare" + }, + "groupStatusMentionMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "groupStatusMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "groupStatusMessageV2": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "highlyStructuredMessage": { + "$ref": "#/definitions/waE2E.HighlyStructuredMessage" + }, + "imageMessage": { + "$ref": "#/definitions/waE2E.ImageMessage" + }, + "interactiveMessage": { + "$ref": "#/definitions/waE2E.InteractiveMessage" + }, + "interactiveResponseMessage": { + "$ref": "#/definitions/waE2E.InteractiveResponseMessage" + }, + "invoiceMessage": { + "$ref": "#/definitions/waE2E.InvoiceMessage" + }, + "keepInChatMessage": { + "$ref": "#/definitions/waE2E.KeepInChatMessage" + }, + "limitSharingMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "listMessage": { + "$ref": "#/definitions/waE2E.ListMessage" + }, + "listResponseMessage": { + "$ref": "#/definitions/waE2E.ListResponseMessage" + }, + "liveLocationMessage": { + "$ref": "#/definitions/waE2E.LiveLocationMessage" + }, + "locationMessage": { + "$ref": "#/definitions/waE2E.LocationMessage" + }, + "lottieStickerMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "messageContextInfo": { + "$ref": "#/definitions/waE2E.MessageContextInfo" + }, + "messageHistoryBundle": { + "$ref": "#/definitions/waE2E.MessageHistoryBundle" + }, + "messageHistoryNotice": { + "$ref": "#/definitions/waE2E.MessageHistoryNotice" + }, + "newsletterAdminInviteMessage": { + "$ref": "#/definitions/waE2E.NewsletterAdminInviteMessage" + }, + "newsletterAdminProfileMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "newsletterAdminProfileMessageV2": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "newsletterAdminProfileStatusMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "newsletterFollowerInviteMessageV2": { + "$ref": "#/definitions/waE2E.NewsletterFollowerInviteMessage" + }, + "orderMessage": { + "$ref": "#/definitions/waE2E.OrderMessage" + }, + "paymentInviteMessage": { + "$ref": "#/definitions/waE2E.PaymentInviteMessage" + }, + "paymentReminderMessage": { + "$ref": "#/definitions/waE2E.PaymentReminderMessage" + }, + "pinInChatMessage": { + "$ref": "#/definitions/waE2E.PinInChatMessage" + }, + "placeholderMessage": { + "$ref": "#/definitions/waE2E.PlaceholderMessage" + }, + "pollAddOptionMessage": { + "$ref": "#/definitions/waE2E.PollAddOptionMessage" + }, + "pollCreationMessage": { + "$ref": "#/definitions/waE2E.PollCreationMessage" + }, + "pollCreationMessageV2": { + "$ref": "#/definitions/waE2E.PollCreationMessage" + }, + "pollCreationMessageV3": { + "$ref": "#/definitions/waE2E.PollCreationMessage" + }, + "pollCreationMessageV4": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "pollCreationMessageV5": { + "$ref": "#/definitions/waE2E.PollCreationMessage" + }, + "pollCreationMessageV6": { + "$ref": "#/definitions/waE2E.PollCreationMessage" + }, + "pollCreationOptionImageMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "pollResultSnapshotMessage": { + "$ref": "#/definitions/waE2E.PollResultSnapshotMessage" + }, + "pollResultSnapshotMessageV3": { + "$ref": "#/definitions/waE2E.PollResultSnapshotMessage" + }, + "pollUpdateMessage": { + "$ref": "#/definitions/waE2E.PollUpdateMessage" + }, + "productMessage": { + "$ref": "#/definitions/waE2E.ProductMessage" + }, + "protocolMessage": { + "$ref": "#/definitions/waE2E.ProtocolMessage" + }, + "ptvMessage": { + "$ref": "#/definitions/waE2E.VideoMessage" + }, + "questionMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "questionReplyMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "questionResponseMessage": { + "$ref": "#/definitions/waE2E.QuestionResponseMessage" + }, + "reactionMessage": { + "$ref": "#/definitions/waE2E.ReactionMessage" + }, + "requestPaymentMessage": { + "$ref": "#/definitions/waE2E.RequestPaymentMessage" + }, + "requestPhoneNumberMessage": { + "$ref": "#/definitions/waE2E.RequestPhoneNumberMessage" + }, + "richResponseMessage": { + "$ref": "#/definitions/waE2E.AIRichResponseMessage" + }, + "rootSecretDistributeMessage": { + "$ref": "#/definitions/waE2E.RootSecretDistributeMessage" + }, + "scheduledCallCreationMessage": { + "$ref": "#/definitions/waE2E.ScheduledCallCreationMessage" + }, + "scheduledCallEditMessage": { + "$ref": "#/definitions/waE2E.ScheduledCallEditMessage" + }, + "secretEncryptedMessage": { + "$ref": "#/definitions/waE2E.SecretEncryptedMessage" + }, + "sendPaymentMessage": { + "$ref": "#/definitions/waE2E.SendPaymentMessage" + }, + "senderKeyDistributionMessage": { + "$ref": "#/definitions/waE2E.SenderKeyDistributionMessage" + }, + "splitPaymentMessage": { + "$ref": "#/definitions/waE2E.SplitPaymentMessage" + }, + "spoilerMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "statusAddYours": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "statusMentionMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "statusNotificationMessage": { + "$ref": "#/definitions/waE2E.StatusNotificationMessage" + }, + "statusQuestionAnswerMessage": { + "$ref": "#/definitions/waE2E.StatusQuestionAnswerMessage" + }, + "statusQuotedMessage": { + "$ref": "#/definitions/waE2E.StatusQuotedMessage" + }, + "statusStickerInteractionMessage": { + "$ref": "#/definitions/waE2E.StatusStickerInteractionMessage" + }, + "stickerMessage": { + "$ref": "#/definitions/waE2E.StickerMessage" + }, + "stickerPackMessage": { + "$ref": "#/definitions/waE2E.StickerPackMessage" + }, + "stickerSyncRmrMessage": { + "$ref": "#/definitions/waE2E.StickerSyncRMRMessage" + }, + "templateButtonReplyMessage": { + "$ref": "#/definitions/waE2E.TemplateButtonReplyMessage" + }, + "templateMessage": { + "$ref": "#/definitions/waE2E.TemplateMessage" + }, + "videoMessage": { + "$ref": "#/definitions/waE2E.VideoMessage" + }, + "viewOnceMessage": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "viewOnceMessageV2": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + }, + "viewOnceMessageV2Extension": { + "$ref": "#/definitions/waE2E.FutureProofMessage" + } + } + }, + "waE2E.MessageAssociation": { + "type": "object", + "properties": { + "associationType": { + "$ref": "#/definitions/waE2E.MessageAssociation_AssociationType" + }, + "messageIndex": { + "type": "integer" + }, + "parentMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.MessageAssociation_AssociationType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20 + ], + "x-enum-varnames": [ + "MessageAssociation_UNKNOWN", + "MessageAssociation_MEDIA_ALBUM", + "MessageAssociation_BOT_PLUGIN", + "MessageAssociation_EVENT_COVER_IMAGE", + "MessageAssociation_STATUS_POLL", + "MessageAssociation_HD_VIDEO_DUAL_UPLOAD", + "MessageAssociation_STATUS_EXTERNAL_RESHARE", + "MessageAssociation_MEDIA_POLL", + "MessageAssociation_STATUS_ADD_YOURS", + "MessageAssociation_STATUS_NOTIFICATION", + "MessageAssociation_HD_IMAGE_DUAL_UPLOAD", + "MessageAssociation_STICKER_ANNOTATION", + "MessageAssociation_MOTION_PHOTO", + "MessageAssociation_STATUS_LINK_ACTION", + "MessageAssociation_VIEW_ALL_REPLIES", + "MessageAssociation_STATUS_ADD_YOURS_AI_IMAGINE", + "MessageAssociation_STATUS_QUESTION", + "MessageAssociation_STATUS_ADD_YOURS_DIWALI", + "MessageAssociation_STATUS_REACTION", + "MessageAssociation_HEVC_VIDEO_DUAL_UPLOAD", + "MessageAssociation_POLL_ADD_OPTION" + ] + }, + "waE2E.MessageContextInfo": { + "type": "object", + "properties": { + "botMessageSecret": { + "type": "array", + "items": { + "type": "integer" + } + }, + "botMetadata": { + "$ref": "#/definitions/waAICommon.BotMetadata" + }, + "capiCreatedGroup": { + "type": "boolean" + }, + "deviceListMetadata": { + "$ref": "#/definitions/waE2E.DeviceListMetadata" + }, + "deviceListMetadataVersion": { + "type": "integer" + }, + "limitSharing": { + "$ref": "#/definitions/waCommon.LimitSharing" + }, + "limitSharingV2": { + "$ref": "#/definitions/waCommon.LimitSharing" + }, + "messageAddOnDurationInSecs": { + "type": "integer" + }, + "messageAddOnExpiryType": { + "$ref": "#/definitions/waE2E.MessageContextInfo_MessageAddonExpiryType" + }, + "messageAssociation": { + "$ref": "#/definitions/waE2E.MessageAssociation" + }, + "messageSecret": { + "type": "array", + "items": { + "type": "integer" + } + }, + "paddingBytes": { + "type": "array", + "items": { + "type": "integer" + } + }, + "reportingTokenVersion": { + "type": "integer" + }, + "supportPayload": { + "type": "string" + }, + "teeBotMetadata": { + "type": "array", + "items": { + "type": "integer" + } + }, + "threadID": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ThreadID" + } + }, + "weblinkRenderConfig": { + "$ref": "#/definitions/waE2E.WebLinkRenderConfig" + } + } + }, + "waE2E.MessageContextInfo_MessageAddonExpiryType": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2 + ], + "x-enum-varnames": [ + "MessageContextInfo_STATIC", + "MessageContextInfo_DEPENDENT_ON_PARENT" + ] + }, + "waE2E.MessageHistoryBundle": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "directPath": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "messageHistoryMetadata": { + "$ref": "#/definitions/waE2E.MessageHistoryMetadata" + }, + "mimetype": { + "type": "string" + } + } + }, + "waE2E.MessageHistoryMetadata": { + "type": "object", + "properties": { + "historyReceivers": { + "type": "array", + "items": { + "type": "string" + } + }, + "messageCount": { + "type": "integer" + }, + "nonHistoryReceivers": { + "type": "array", + "items": { + "type": "string" + } + }, + "oldestMessageTimestampInBundle": { + "type": "integer" + }, + "oldestMessageTimestampInWindow": { + "type": "integer" + } + } + }, + "waE2E.MessageHistoryNotice": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "messageHistoryMetadata": { + "$ref": "#/definitions/waE2E.MessageHistoryMetadata" + } + } + }, + "waE2E.Money": { + "type": "object", + "properties": { + "currencyCode": { + "type": "string" + }, + "offset": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "waE2E.NewsletterAdminInviteMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "inviteExpiration": { + "type": "integer" + }, + "newsletterJID": { + "type": "string" + }, + "newsletterName": { + "type": "string" + } + } + }, + "waE2E.NewsletterFollowerInviteMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "newsletterJID": { + "type": "string" + }, + "newsletterName": { + "type": "string" + } + } + }, + "waE2E.OrderMessage": { + "type": "object", + "properties": { + "catalogType": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "itemCount": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "messageVersion": { + "type": "integer" + }, + "orderID": { + "type": "string" + }, + "orderRequestMessageID": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "orderTitle": { + "type": "string" + }, + "sellerJID": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/waE2E.OrderMessage_OrderStatus" + }, + "surface": { + "$ref": "#/definitions/waE2E.OrderMessage_OrderSurface" + }, + "thumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "token": { + "type": "string" + }, + "totalAmount1000": { + "type": "integer" + }, + "totalCurrencyCode": { + "type": "string" + } + } + }, + "waE2E.OrderMessage_OrderStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "OrderMessage_INQUIRY", + "OrderMessage_ACCEPTED", + "OrderMessage_DECLINED" + ] + }, + "waE2E.OrderMessage_OrderSurface": { + "type": "integer", + "format": "int32", + "enum": [ + 1 + ], + "x-enum-varnames": [ + "OrderMessage_CATALOG" + ] + }, + "waE2E.PaymentBackground": { + "type": "object", + "properties": { + "ID": { + "type": "string" + }, + "fileLength": { + "type": "integer" + }, + "height": { + "type": "integer" + }, + "mediaData": { + "$ref": "#/definitions/waE2E.PaymentBackground_MediaData" + }, + "mimetype": { + "type": "string" + }, + "placeholderArgb": { + "type": "integer" + }, + "subtextArgb": { + "type": "integer" + }, + "textArgb": { + "type": "integer" + }, + "type": { + "$ref": "#/definitions/waE2E.PaymentBackground_Type" + }, + "width": { + "type": "integer" + } + } + }, + "waE2E.PaymentBackground_MediaData": { + "type": "object", + "properties": { + "directPath": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + } + } + }, + "waE2E.PaymentBackground_Type": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "PaymentBackground_UNKNOWN", + "PaymentBackground_DEFAULT" + ] + }, + "waE2E.PaymentExtendedMetadata": { + "type": "object", + "properties": { + "platform": { + "type": "string" + }, + "type": { + "type": "integer" + } + } + }, + "waE2E.PaymentInviteMessage": { + "type": "object", + "properties": { + "expiryTimestamp": { + "type": "integer" + }, + "incentiveEligible": { + "type": "boolean" + }, + "inviteType": { + "$ref": "#/definitions/waE2E.PaymentInviteMessage_InviteType" + }, + "referralID": { + "type": "string" + }, + "serviceType": { + "$ref": "#/definitions/waE2E.PaymentInviteMessage_ServiceType" + } + } + }, + "waE2E.PaymentInviteMessage_InviteType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "PaymentInviteMessage_DEFAULT", + "PaymentInviteMessage_MAPPER" + ] + }, + "waE2E.PaymentInviteMessage_ServiceType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "PaymentInviteMessage_UNKNOWN", + "PaymentInviteMessage_FBPAY", + "PaymentInviteMessage_NOVI", + "PaymentInviteMessage_UPI" + ] + }, + "waE2E.PaymentLinkMetadata": { + "type": "object", + "properties": { + "button": { + "$ref": "#/definitions/waE2E.PaymentLinkMetadata_PaymentLinkButton" + }, + "header": { + "$ref": "#/definitions/waE2E.PaymentLinkMetadata_PaymentLinkHeader" + }, + "provider": { + "$ref": "#/definitions/waE2E.PaymentLinkMetadata_PaymentLinkProvider" + } + } + }, + "waE2E.PaymentLinkMetadata_PaymentLinkButton": { + "type": "object", + "properties": { + "displayText": { + "type": "string" + } + } + }, + "waE2E.PaymentLinkMetadata_PaymentLinkHeader": { + "type": "object", + "properties": { + "headerType": { + "$ref": "#/definitions/waE2E.PaymentLinkMetadata_PaymentLinkHeader_PaymentLinkHeaderType" + } + } + }, + "waE2E.PaymentLinkMetadata_PaymentLinkHeader_PaymentLinkHeaderType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "PaymentLinkMetadata_PaymentLinkHeader_LINK_PREVIEW", + "PaymentLinkMetadata_PaymentLinkHeader_ORDER" + ] + }, + "waE2E.PaymentLinkMetadata_PaymentLinkProvider": { + "type": "object", + "properties": { + "paramsJSON": { + "type": "string" + } + } + }, + "waE2E.PaymentReminderMessage": { + "type": "object", + "properties": { + "amount": { + "$ref": "#/definitions/waE2E.Money" + }, + "description": { + "type": "string" + }, + "frequency": { + "$ref": "#/definitions/waE2E.PaymentReminderMessage_ReminderFrequency" + }, + "instanceID": { + "type": "string" + }, + "payeeJID": { + "type": "string" + }, + "payeeVpa": { + "type": "string" + }, + "payerJID": { + "type": "string" + }, + "reminderID": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/waE2E.PaymentReminderMessage_ReminderStatus" + } + } + }, + "waE2E.PaymentReminderMessage_ReminderFrequency": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "x-enum-varnames": [ + "PaymentReminderMessage_REMINDER_FREQUENCY_UNKNOWN", + "PaymentReminderMessage_WEEKLY", + "PaymentReminderMessage_BI_WEEKLY", + "PaymentReminderMessage_MONTHLY", + "PaymentReminderMessage_QUARTERLY" + ] + }, + "waE2E.PaymentReminderMessage_ReminderStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "PaymentReminderMessage_REMINDER_STATUS_UNKNOWN", + "PaymentReminderMessage_ACTIVE", + "PaymentReminderMessage_CANCELLED_BY_CREATOR", + "PaymentReminderMessage_STOPPED_BY_RECEIVER", + "PaymentReminderMessage_EXPIRED", + "PaymentReminderMessage_PAID" + ] + }, + "waE2E.PeerDataOperationRequestMessage": { + "type": "object", + "properties": { + "bizBroadcastInsightsContactListRequest": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_BizBroadcastInsightsContactListRequest" + }, + "bizBroadcastInsightsRefreshRequest": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_BizBroadcastInsightsRefreshRequest" + }, + "companionCanonicalUserNonceFetchRequest": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_CompanionCanonicalUserNonceFetchRequest" + }, + "fullHistorySyncOnDemandRequest": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_FullHistorySyncOnDemandRequest" + }, + "galaxyFlowAction": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_GalaxyFlowAction" + }, + "historySyncChunkRetryRequest": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_HistorySyncChunkRetryRequest" + }, + "historySyncOnDemandRequest": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_HistorySyncOnDemandRequest" + }, + "peerDataOperationRequestType": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestType" + }, + "placeholderMessageResendRequest": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_PlaceholderMessageResendRequest" + } + }, + "requestStickerReupload": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_RequestStickerReupload" + } + }, + "requestURLPreview": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_RequestUrlPreview" + } + }, + "syncdCollectionFatalRecoveryRequest": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_SyncDCollectionFatalRecoveryRequest" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_BizBroadcastInsightsContactListRequest": { + "type": "object", + "properties": { + "campaignID": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_BizBroadcastInsightsRefreshRequest": { + "type": "object", + "properties": { + "campaignID": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_CompanionCanonicalUserNonceFetchRequest": { + "type": "object", + "properties": { + "registrationTraceID": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_FullHistorySyncOnDemandRequest": { + "type": "object", + "properties": { + "fullHistorySyncOnDemandConfig": { + "$ref": "#/definitions/waE2E.FullHistorySyncOnDemandConfig" + }, + "historySyncConfig": { + "$ref": "#/definitions/waCompanionReg.DeviceProps_HistorySyncConfig" + }, + "requestMetadata": { + "$ref": "#/definitions/waE2E.FullHistorySyncOnDemandRequestMetadata" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_GalaxyFlowAction": { + "type": "object", + "properties": { + "agmID": { + "type": "string" + }, + "flowID": { + "type": "string" + }, + "galaxyFlowDownloadRequestID": { + "type": "string" + }, + "stanzaID": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage_GalaxyFlowAction_GalaxyFlowActionType" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_GalaxyFlowAction_GalaxyFlowActionType": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2 + ], + "x-enum-varnames": [ + "PeerDataOperationRequestMessage_GalaxyFlowAction_NOTIFY_LAUNCH", + "PeerDataOperationRequestMessage_GalaxyFlowAction_DOWNLOAD_RESPONSES" + ] + }, + "waE2E.PeerDataOperationRequestMessage_HistorySyncChunkRetryRequest": { + "type": "object", + "properties": { + "chunkNotificationID": { + "type": "string" + }, + "chunkOrder": { + "type": "integer" + }, + "regenerateChunk": { + "type": "boolean" + }, + "syncType": { + "$ref": "#/definitions/waE2E.HistorySyncType" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_HistorySyncOnDemandRequest": { + "type": "object", + "properties": { + "accountLid": { + "type": "string" + }, + "chatJID": { + "type": "string" + }, + "oldestMsgFromMe": { + "type": "boolean" + }, + "oldestMsgID": { + "type": "string" + }, + "oldestMsgTimestampMS": { + "type": "integer" + }, + "onDemandMsgCount": { + "type": "integer" + }, + "supportInlineResponse": { + "type": "boolean" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_PlaceholderMessageResendRequest": { + "type": "object", + "properties": { + "messageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_RequestStickerReupload": { + "type": "object", + "properties": { + "fileSHA256": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_RequestUrlPreview": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "includeHqThumbnail": { + "type": "boolean" + } + } + }, + "waE2E.PeerDataOperationRequestMessage_SyncDCollectionFatalRecoveryRequest": { + "type": "object", + "properties": { + "collectionName": { + "type": "string" + }, + "timestamp": { + "type": "integer" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage": { + "type": "object", + "properties": { + "peerDataOperationRequestType": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestType" + }, + "peerDataOperationResult": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult" + } + }, + "stanzaID": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult": { + "type": "object", + "properties": { + "bizBroadcastInsightsContactListResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_BizBroadcastInsightsContactListResponse" + }, + "companionCanonicalUserNonceFetchRequestResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CompanionCanonicalUserNonceFetchResponse" + }, + "companionMetaNonceFetchRequestResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CompanionMetaNonceFetchResponse" + }, + "flowResponsesCsvBundle": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FlowResponsesCsvBundle" + }, + "fullHistorySyncOnDemandRequestResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FullHistorySyncOnDemandRequestResponse" + }, + "historySyncChunkRetryResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_HistorySyncChunkRetryResponse" + }, + "linkPreviewResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse" + }, + "mediaUploadResult": { + "$ref": "#/definitions/waMmsRetry.MediaRetryNotification_ResultType" + }, + "placeholderMessageResendResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_PlaceholderMessageResendResponse" + }, + "stickerMessage": { + "$ref": "#/definitions/waE2E.StickerMessage" + }, + "syncdSnapshotFatalRecoveryResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_SyncDSnapshotFatalRecoveryResponse" + }, + "waffleNonceFetchRequestResponse": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_WaffleNonceFetchResponse" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_BizBroadcastInsightsContactListResponse": { + "type": "object", + "properties": { + "campaignID": { + "type": "string" + }, + "contacts": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_BizBroadcastInsightsContactState" + } + }, + "timestampMS": { + "type": "integer" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_BizBroadcastInsightsContactState": { + "type": "object", + "properties": { + "contactJID": { + "type": "string" + }, + "state": { + "$ref": "#/definitions/waE2E.InsightDeliveryState" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CompanionCanonicalUserNonceFetchResponse": { + "type": "object", + "properties": { + "forceRefresh": { + "type": "boolean" + }, + "nonce": { + "type": "string" + }, + "waFbid": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CompanionMetaNonceFetchResponse": { + "type": "object", + "properties": { + "nonce": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FlowResponsesCsvBundle": { + "type": "object", + "properties": { + "directPath": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileName": { + "type": "string" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "flowID": { + "type": "string" + }, + "galaxyFlowDownloadRequestID": { + "type": "string" + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "mimetype": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FullHistorySyncOnDemandRequestResponse": { + "type": "object", + "properties": { + "requestMetadata": { + "$ref": "#/definitions/waE2E.FullHistorySyncOnDemandRequestMetadata" + }, + "responseCode": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FullHistorySyncOnDemandResponseCode" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FullHistorySyncOnDemandResponseCode": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "x-enum-varnames": [ + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_REQUEST_SUCCESS", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_REQUEST_TIME_EXPIRED", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_DECLINED_SHARING_HISTORY", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_GENERIC_ERROR", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_ERROR_REQUEST_ON_NON_SMB_PRIMARY", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_ERROR_HOSTED_DEVICE_NOT_CONNECTED", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_ERROR_HOSTED_DEVICE_LOGIN_TIME_NOT_SET", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_ERROR_MULTI_PROVIDER_NOT_CONFIGURED" + ] + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_HistorySyncChunkRetryResponse": { + "type": "object", + "properties": { + "canRecover": { + "type": "boolean" + }, + "chunkOrder": { + "type": "integer" + }, + "requestID": { + "type": "string" + }, + "responseCode": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_HistorySyncChunkRetryResponseCode" + }, + "syncType": { + "$ref": "#/definitions/waE2E.HistorySyncType" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_HistorySyncChunkRetryResponseCode": { + "type": "integer", + "format": "int32", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "x-enum-varnames": [ + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_GENERATION_ERROR", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CHUNK_CONSUMED", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_TIMEOUT", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_SESSION_EXHAUSTED", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CHUNK_EXHAUSTED", + "PeerDataOperationRequestResponseMessage_PeerDataOperationResult_DUPLICATED_REQUEST" + ] + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hqThumbnail": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse_LinkPreviewHighQualityThumbnail" + }, + "matchText": { + "type": "string" + }, + "previewMetadata": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse_PaymentLinkPreviewMetadata" + }, + "previewType": { + "type": "string" + }, + "thumbData": { + "type": "array", + "items": { + "type": "integer" + } + }, + "title": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse_LinkPreviewHighQualityThumbnail": { + "type": "object", + "properties": { + "directPath": { + "type": "string" + }, + "encThumbHash": { + "type": "string" + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestampMS": { + "type": "integer" + }, + "thumbHash": { + "type": "string" + }, + "thumbHeight": { + "type": "integer" + }, + "thumbWidth": { + "type": "integer" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse_PaymentLinkPreviewMetadata": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "isBusinessVerified": { + "type": "boolean" + }, + "offset": { + "type": "string" + }, + "providerName": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_PlaceholderMessageResendResponse": { + "type": "object", + "properties": { + "webMessageInfoBytes": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_SyncDSnapshotFatalRecoveryResponse": { + "type": "object", + "properties": { + "collectionSnapshot": { + "type": "array", + "items": { + "type": "integer" + } + }, + "isCompressed": { + "type": "boolean" + } + } + }, + "waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_WaffleNonceFetchResponse": { + "type": "object", + "properties": { + "nonce": { + "type": "string" + }, + "waEntFbid": { + "type": "string" + } + } + }, + "waE2E.PeerDataOperationRequestType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13 + ], + "x-enum-varnames": [ + "PeerDataOperationRequestType_UPLOAD_STICKER", + "PeerDataOperationRequestType_SEND_RECENT_STICKER_BOOTSTRAP", + "PeerDataOperationRequestType_GENERATE_LINK_PREVIEW", + "PeerDataOperationRequestType_HISTORY_SYNC_ON_DEMAND", + "PeerDataOperationRequestType_PLACEHOLDER_MESSAGE_RESEND", + "PeerDataOperationRequestType_WAFFLE_LINKING_NONCE_FETCH", + "PeerDataOperationRequestType_FULL_HISTORY_SYNC_ON_DEMAND", + "PeerDataOperationRequestType_COMPANION_META_NONCE_FETCH", + "PeerDataOperationRequestType_COMPANION_SYNCD_SNAPSHOT_FATAL_RECOVERY", + "PeerDataOperationRequestType_COMPANION_CANONICAL_USER_NONCE_FETCH", + "PeerDataOperationRequestType_HISTORY_SYNC_CHUNK_RETRY", + "PeerDataOperationRequestType_GALAXY_FLOW_ACTION", + "PeerDataOperationRequestType_BUSINESS_BROADCAST_INSIGHTS_DELIVERED_TO", + "PeerDataOperationRequestType_BUSINESS_BROADCAST_INSIGHTS_REFRESH" + ] + }, + "waE2E.PinInChatMessage": { + "type": "object", + "properties": { + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "senderTimestampMS": { + "type": "integer" + }, + "type": { + "$ref": "#/definitions/waE2E.PinInChatMessage_Type" + } + } + }, + "waE2E.PinInChatMessage_Type": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "PinInChatMessage_UNKNOWN_TYPE", + "PinInChatMessage_PIN_FOR_ALL", + "PinInChatMessage_UNPIN_FOR_ALL" + ] + }, + "waE2E.PlaceholderMessage": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/waE2E.PlaceholderMessage_PlaceholderType" + } + } + }, + "waE2E.PlaceholderMessage_PlaceholderType": { + "type": "integer", + "format": "int32", + "enum": [ + 0 + ], + "x-enum-varnames": [ + "PlaceholderMessage_MASK_LINKED_DEVICES" + ] + }, + "waE2E.Point": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "xDeprecated": { + "type": "integer" + }, + "y": { + "type": "number" + }, + "yDeprecated": { + "type": "integer" + } + } + }, + "waE2E.PollAddOptionMessage": { + "type": "object", + "properties": { + "addOption": { + "$ref": "#/definitions/waE2E.PollCreationMessage_Option" + }, + "metadata": { + "$ref": "#/definitions/waE2E.PollUpdateMessageMetadata" + }, + "pollCreationMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.PollContentType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "PollContentType_UNKNOWN_POLL_CONTENT_TYPE", + "PollContentType_TEXT", + "PollContentType_IMAGE" + ] + }, + "waE2E.PollCreationMessage": { + "type": "object", + "properties": { + "allowAddOption": { + "type": "boolean" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "correctAnswer": { + "$ref": "#/definitions/waE2E.PollCreationMessage_Option" + }, + "encKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "endTime": { + "type": "integer" + }, + "hideParticipantName": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.PollCreationMessage_Option" + } + }, + "pollContentType": { + "$ref": "#/definitions/waE2E.PollContentType" + }, + "pollType": { + "$ref": "#/definitions/waE2E.PollType" + }, + "selectableOptionsCount": { + "type": "integer" + } + } + }, + "waE2E.PollCreationMessage_Option": { + "type": "object", + "properties": { + "optionHash": { + "type": "string" + }, + "optionName": { + "type": "string" + } + } + }, + "waE2E.PollEncValue": { + "type": "object", + "properties": { + "encIV": { + "type": "array", + "items": { + "type": "integer" + } + }, + "encPayload": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waE2E.PollResultSnapshotMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "name": { + "type": "string" + }, + "pollType": { + "$ref": "#/definitions/waE2E.PollType" + }, + "pollVotes": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.PollResultSnapshotMessage_PollVote" + } + } + } + }, + "waE2E.PollResultSnapshotMessage_PollVote": { + "type": "object", + "properties": { + "optionName": { + "type": "string" + }, + "optionVoteCount": { + "type": "integer" + } + } + }, + "waE2E.PollType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "PollType_POLL", + "PollType_QUIZ" + ] + }, + "waE2E.PollUpdateMessage": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/waE2E.PollUpdateMessageMetadata" + }, + "pollCreationMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "senderTimestampMS": { + "type": "integer" + }, + "vote": { + "$ref": "#/definitions/waE2E.PollEncValue" + } + } + }, + "waE2E.PollUpdateMessageMetadata": { + "type": "object", + "properties": { + "lastEditStanzaID": { + "type": "string" + }, + "pollNameHash": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waE2E.ProcessedVideo": { + "type": "object", + "properties": { + "bitrate": { + "type": "integer" + }, + "capabilities": { + "type": "array", + "items": { + "type": "string" + } + }, + "directPath": { + "type": "string" + }, + "fileLength": { + "type": "integer" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "height": { + "type": "integer" + }, + "quality": { + "$ref": "#/definitions/waE2E.ProcessedVideo_VideoQuality" + }, + "width": { + "type": "integer" + } + } + }, + "waE2E.ProcessedVideo_VideoQuality": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "ProcessedVideo_UNDEFINED", + "ProcessedVideo_LOW", + "ProcessedVideo_MID", + "ProcessedVideo_HIGH" + ] + }, + "waE2E.ProductMessage": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "businessOwnerJID": { + "type": "string" + }, + "catalog": { + "$ref": "#/definitions/waE2E.ProductMessage_CatalogSnapshot" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "footer": { + "type": "string" + }, + "product": { + "$ref": "#/definitions/waE2E.ProductMessage_ProductSnapshot" + } + } + }, + "waE2E.ProductMessage_CatalogSnapshot": { + "type": "object", + "properties": { + "catalogImage": { + "$ref": "#/definitions/waE2E.ImageMessage" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ProductMessage_ProductSnapshot": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "currencyCode": { + "type": "string" + }, + "description": { + "type": "string" + }, + "firstImageID": { + "type": "string" + }, + "priceAmount1000": { + "type": "integer" + }, + "productID": { + "type": "string" + }, + "productImage": { + "$ref": "#/definitions/waE2E.ImageMessage" + }, + "productImageCount": { + "type": "integer" + }, + "retailerID": { + "type": "string" + }, + "salePriceAmount1000": { + "type": "integer" + }, + "signedURL": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ProtocolMessage": { + "type": "object", + "properties": { + "afterReadDuration": { + "type": "integer" + }, + "aiMediaCollectionMessage": { + "$ref": "#/definitions/waAICommon.AIMediaCollectionMessage" + }, + "aiMetadataOperation": { + "$ref": "#/definitions/waAICommon.AIMetadataOperation" + }, + "aiPsiMetadata": { + "type": "array", + "items": { + "type": "integer" + } + }, + "aiQueryFanout": { + "$ref": "#/definitions/waE2E.AIQueryFanout" + }, + "appStateFatalExceptionNotification": { + "$ref": "#/definitions/waE2E.AppStateFatalExceptionNotification" + }, + "appStateSyncKeyRequest": { + "$ref": "#/definitions/waE2E.AppStateSyncKeyRequest" + }, + "appStateSyncKeyShare": { + "$ref": "#/definitions/waE2E.AppStateSyncKeyShare" + }, + "botFeedbackMessage": { + "$ref": "#/definitions/waAICommon.BotFeedbackMessage" + }, + "chatThemeSetting": { + "$ref": "#/definitions/waE2E.ChatThemeSetting" + }, + "cloudApiThreadControlNotification": { + "$ref": "#/definitions/waE2E.CloudAPIThreadControlNotification" + }, + "disappearingMode": { + "$ref": "#/definitions/waE2E.DisappearingMode" + }, + "editedMessage": { + "$ref": "#/definitions/waE2E.Message" + }, + "ephemeralExpiration": { + "type": "integer" + }, + "ephemeralSettingTimestamp": { + "type": "integer" + }, + "historySyncNotification": { + "$ref": "#/definitions/waE2E.HistorySyncNotification" + }, + "initialSecurityNotificationSettingSync": { + "$ref": "#/definitions/waE2E.InitialSecurityNotificationSettingSync" + }, + "invokerJID": { + "type": "string" + }, + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "lidMigrationMappingSyncMessage": { + "$ref": "#/definitions/waE2E.LIDMigrationMappingSyncMessage" + }, + "limitSharing": { + "$ref": "#/definitions/waCommon.LimitSharing" + }, + "mediaNotifyMessage": { + "$ref": "#/definitions/waE2E.MediaNotifyMessage" + }, + "memberLabel": { + "$ref": "#/definitions/waE2E.MemberLabel" + }, + "peerDataOperationRequestMessage": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestMessage" + }, + "peerDataOperationRequestResponseMessage": { + "$ref": "#/definitions/waE2E.PeerDataOperationRequestResponseMessage" + }, + "requestWelcomeMessageMetadata": { + "$ref": "#/definitions/waE2E.RequestWelcomeMessageMetadata" + }, + "timestampMS": { + "type": "integer" + }, + "type": { + "$ref": "#/definitions/waE2E.ProtocolMessage_Type" + } + } + }, + "waE2E.ProtocolMessage_Type": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 14, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 34, + 35 + ], + "x-enum-varnames": [ + "ProtocolMessage_REVOKE", + "ProtocolMessage_EPHEMERAL_SETTING", + "ProtocolMessage_EPHEMERAL_SYNC_RESPONSE", + "ProtocolMessage_HISTORY_SYNC_NOTIFICATION", + "ProtocolMessage_APP_STATE_SYNC_KEY_SHARE", + "ProtocolMessage_APP_STATE_SYNC_KEY_REQUEST", + "ProtocolMessage_MSG_FANOUT_BACKFILL_REQUEST", + "ProtocolMessage_INITIAL_SECURITY_NOTIFICATION_SETTING_SYNC", + "ProtocolMessage_APP_STATE_FATAL_EXCEPTION_NOTIFICATION", + "ProtocolMessage_SHARE_PHONE_NUMBER", + "ProtocolMessage_MESSAGE_EDIT", + "ProtocolMessage_PEER_DATA_OPERATION_REQUEST_MESSAGE", + "ProtocolMessage_PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE", + "ProtocolMessage_REQUEST_WELCOME_MESSAGE", + "ProtocolMessage_BOT_FEEDBACK_MESSAGE", + "ProtocolMessage_MEDIA_NOTIFY_MESSAGE", + "ProtocolMessage_CLOUD_API_THREAD_CONTROL_NOTIFICATION", + "ProtocolMessage_LID_MIGRATION_MAPPING_SYNC", + "ProtocolMessage_REMINDER_MESSAGE", + "ProtocolMessage_BOT_MEMU_ONBOARDING_MESSAGE", + "ProtocolMessage_STATUS_MENTION_MESSAGE", + "ProtocolMessage_STOP_GENERATION_MESSAGE", + "ProtocolMessage_LIMIT_SHARING", + "ProtocolMessage_AI_PSI_METADATA", + "ProtocolMessage_AI_QUERY_FANOUT", + "ProtocolMessage_GROUP_MEMBER_LABEL_CHANGE", + "ProtocolMessage_AI_MEDIA_COLLECTION_MESSAGE", + "ProtocolMessage_MESSAGE_UNSCHEDULE", + "ProtocolMessage_CHAT_THEME_SETTING", + "ProtocolMessage_AI_METADATA_OPERATION" + ] + }, + "waE2E.QuestionResponseMessage": { + "type": "object", + "properties": { + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "text": { + "type": "string" + } + } + }, + "waE2E.ReactionMessage": { + "type": "object", + "properties": { + "groupingKey": { + "type": "string" + }, + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "senderTimestampMS": { + "type": "integer" + }, + "text": { + "type": "string" + } + } + }, + "waE2E.RequestPaymentMessage": { + "type": "object", + "properties": { + "amount": { + "$ref": "#/definitions/waE2E.Money" + }, + "amount1000": { + "type": "integer" + }, + "background": { + "$ref": "#/definitions/waE2E.PaymentBackground" + }, + "currencyCodeIso4217": { + "type": "string" + }, + "expiryTimestamp": { + "type": "integer" + }, + "noteMessage": { + "$ref": "#/definitions/waE2E.Message" + }, + "requestFrom": { + "type": "string" + } + } + }, + "waE2E.RequestPhoneNumberMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + } + } + }, + "waE2E.RequestWelcomeMessageMetadata": { + "type": "object", + "properties": { + "botAgentMetadata": { + "$ref": "#/definitions/waAICommon.BotAgentMetadata" + }, + "localChatState": { + "$ref": "#/definitions/waE2E.RequestWelcomeMessageMetadata_LocalChatState" + }, + "welcomeTrigger": { + "$ref": "#/definitions/waE2E.RequestWelcomeMessageMetadata_WelcomeTrigger" + } + } + }, + "waE2E.RequestWelcomeMessageMetadata_LocalChatState": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "RequestWelcomeMessageMetadata_EMPTY", + "RequestWelcomeMessageMetadata_NON_EMPTY" + ] + }, + "waE2E.RequestWelcomeMessageMetadata_WelcomeTrigger": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "RequestWelcomeMessageMetadata_CHAT_OPEN", + "RequestWelcomeMessageMetadata_COMPANION_PAIRING" + ] + }, + "waE2E.RootSecretDistributeMessage": { + "type": "object", + "properties": { + "chatJID": { + "type": "string" + } + } + }, + "waE2E.ScheduledCallCreationMessage": { + "type": "object", + "properties": { + "callType": { + "$ref": "#/definitions/waE2E.ScheduledCallCreationMessage_CallType" + }, + "scheduledTimestampMS": { + "type": "integer" + }, + "title": { + "type": "string" + } + } + }, + "waE2E.ScheduledCallCreationMessage_CallType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "ScheduledCallCreationMessage_UNKNOWN", + "ScheduledCallCreationMessage_VOICE", + "ScheduledCallCreationMessage_VIDEO" + ] + }, + "waE2E.ScheduledCallEditMessage": { + "type": "object", + "properties": { + "editType": { + "$ref": "#/definitions/waE2E.ScheduledCallEditMessage_EditType" + }, + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.ScheduledCallEditMessage_EditType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ScheduledCallEditMessage_UNKNOWN", + "ScheduledCallEditMessage_CANCEL" + ] + }, + "waE2E.SecretEncryptedMessage": { + "type": "object", + "properties": { + "encIV": { + "type": "array", + "items": { + "type": "integer" + } + }, + "encPayload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "remoteKeyID": { + "type": "string" + }, + "secretEncType": { + "$ref": "#/definitions/waE2E.SecretEncryptedMessage_SecretEncType" + }, + "targetMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + } + } + }, + "waE2E.SecretEncryptedMessage_SecretEncType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-varnames": [ + "SecretEncryptedMessage_UNKNOWN", + "SecretEncryptedMessage_EVENT_EDIT", + "SecretEncryptedMessage_MESSAGE_EDIT", + "SecretEncryptedMessage_MESSAGE_SCHEDULE", + "SecretEncryptedMessage_POLL_EDIT", + "SecretEncryptedMessage_POLL_ADD_OPTION" + ] + }, + "waE2E.SendPaymentMessage": { + "type": "object", + "properties": { + "background": { + "$ref": "#/definitions/waE2E.PaymentBackground" + }, + "noteMessage": { + "$ref": "#/definitions/waE2E.Message" + }, + "requestMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "transactionData": { + "type": "string" + } + } + }, + "waE2E.SenderKeyDistributionMessage": { + "type": "object", + "properties": { + "axolotlSenderKeyDistributionMessage": { + "type": "array", + "items": { + "type": "integer" + } + }, + "groupID": { + "type": "string" + } + } + }, + "waE2E.SplitPaymentMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "createdAtMS": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "participants": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.SplitPaymentParticipant" + } + }, + "requesterJID": { + "type": "string" + }, + "splitID": { + "type": "string" + }, + "totalAmount": { + "$ref": "#/definitions/waE2E.Money" + } + } + }, + "waE2E.SplitPaymentParticipant": { + "type": "object", + "properties": { + "JID": { + "type": "string" + }, + "amount": { + "$ref": "#/definitions/waE2E.Money" + }, + "status": { + "$ref": "#/definitions/waE2E.SplitPaymentParticipant_SplitPaymentStatus" + } + } + }, + "waE2E.SplitPaymentParticipant_SplitPaymentStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "SplitPaymentParticipant_PENDING", + "SplitPaymentParticipant_PAID" + ] + }, + "waE2E.StatusNotificationMessage": { + "type": "object", + "properties": { + "originalMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "responseMessageKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "type": { + "$ref": "#/definitions/waE2E.StatusNotificationMessage_StatusNotificationType" + } + } + }, + "waE2E.StatusNotificationMessage_StatusNotificationType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "StatusNotificationMessage_UNKNOWN", + "StatusNotificationMessage_STATUS_ADD_YOURS", + "StatusNotificationMessage_STATUS_RESHARE", + "StatusNotificationMessage_STATUS_QUESTION_ANSWER_RESHARE" + ] + }, + "waE2E.StatusQuestionAnswerMessage": { + "type": "object", + "properties": { + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "text": { + "type": "string" + } + } + }, + "waE2E.StatusQuotedMessage": { + "type": "object", + "properties": { + "originalStatusID": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "text": { + "type": "string" + }, + "thumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "type": { + "$ref": "#/definitions/waE2E.StatusQuotedMessage_StatusQuotedMessageType" + } + } + }, + "waE2E.StatusQuotedMessage_StatusQuotedMessageType": { + "type": "integer", + "format": "int32", + "enum": [ + 1 + ], + "x-enum-varnames": [ + "StatusQuotedMessage_QUESTION_ANSWER" + ] + }, + "waE2E.StatusStickerInteractionMessage": { + "type": "object", + "properties": { + "key": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "stickerKey": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/waE2E.StatusStickerInteractionMessage_StatusStickerType" + } + } + }, + "waE2E.StatusStickerInteractionMessage_StatusStickerType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "StatusStickerInteractionMessage_UNKNOWN", + "StatusStickerInteractionMessage_REACTION" + ] + }, + "waE2E.StickerMessage": { + "type": "object", + "properties": { + "URL": { + "type": "string" + }, + "accessibilityLabel": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "directPath": { + "type": "string" + }, + "emojis": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "firstFrameLength": { + "type": "integer" + }, + "firstFrameSidecar": { + "type": "array", + "items": { + "type": "integer" + } + }, + "height": { + "type": "integer" + }, + "isAiSticker": { + "type": "boolean" + }, + "isAnimated": { + "type": "boolean" + }, + "isAvatar": { + "type": "boolean" + }, + "isLottie": { + "type": "boolean" + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "mimetype": { + "type": "string" + }, + "pngThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "premium": { + "type": "integer" + }, + "stickerSentTS": { + "type": "integer" + }, + "width": { + "type": "integer" + } + } + }, + "waE2E.StickerPackMessage": { + "type": "object", + "properties": { + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "directPath": { + "type": "string" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "imageDataHash": { + "type": "string" + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "packDescription": { + "type": "string" + }, + "publisher": { + "type": "string" + }, + "stickerPackID": { + "type": "string" + }, + "stickerPackOrigin": { + "$ref": "#/definitions/waE2E.StickerPackMessage_StickerPackOrigin" + }, + "stickerPackSize": { + "type": "integer" + }, + "stickers": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.StickerPackMessage_Sticker" + } + }, + "thumbnailDirectPath": { + "type": "string" + }, + "thumbnailEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailHeight": { + "type": "integer" + }, + "thumbnailSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailWidth": { + "type": "integer" + }, + "trayIconFileName": { + "type": "string" + } + } + }, + "waE2E.StickerPackMessage_Sticker": { + "type": "object", + "properties": { + "accessibilityLabel": { + "type": "string" + }, + "emojis": { + "type": "array", + "items": { + "type": "string" + } + }, + "fileName": { + "type": "string" + }, + "isAnimated": { + "type": "boolean" + }, + "isLottie": { + "type": "boolean" + }, + "mimetype": { + "type": "string" + }, + "premium": { + "type": "integer" + } + } + }, + "waE2E.StickerPackMessage_StickerPackOrigin": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "StickerPackMessage_FIRST_PARTY", + "StickerPackMessage_THIRD_PARTY", + "StickerPackMessage_USER_CREATED" + ] + }, + "waE2E.StickerSyncRMRMessage": { + "type": "object", + "properties": { + "filehash": { + "type": "array", + "items": { + "type": "string" + } + }, + "requestTimestamp": { + "type": "integer" + }, + "rmrSource": { + "type": "string" + } + } + }, + "waE2E.TemplateButtonReplyMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "selectedCarouselCardIndex": { + "type": "integer" + }, + "selectedDisplayText": { + "type": "string" + }, + "selectedID": { + "type": "string" + }, + "selectedIndex": { + "type": "integer" + } + } + }, + "waE2E.TemplateMessage": { + "type": "object", + "properties": { + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "format": { + "description": "Types that are valid to be assigned to Format:\n\n\t*TemplateMessage_FourRowTemplate_\n\t*TemplateMessage_HydratedFourRowTemplate_\n\t*TemplateMessage_InteractiveMessageTemplate" + }, + "hydratedTemplate": { + "$ref": "#/definitions/waE2E.TemplateMessage_HydratedFourRowTemplate" + }, + "templateID": { + "type": "string" + } + } + }, + "waE2E.TemplateMessage_HydratedFourRowTemplate": { + "type": "object", + "properties": { + "hydratedButtons": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.HydratedTemplateButton" + } + }, + "hydratedContentText": { + "type": "string" + }, + "hydratedFooterText": { + "type": "string" + }, + "maskLinkedDevices": { + "type": "boolean" + }, + "templateID": { + "type": "string" + }, + "title": { + "description": "Types that are valid to be assigned to Title:\n\n\t*TemplateMessage_HydratedFourRowTemplate_DocumentMessage\n\t*TemplateMessage_HydratedFourRowTemplate_HydratedTitleText\n\t*TemplateMessage_HydratedFourRowTemplate_ImageMessage\n\t*TemplateMessage_HydratedFourRowTemplate_VideoMessage\n\t*TemplateMessage_HydratedFourRowTemplate_LocationMessage" + } + } + }, + "waE2E.ThreadID": { + "type": "object", + "properties": { + "threadKey": { + "$ref": "#/definitions/waCommon.MessageKey" + }, + "threadType": { + "$ref": "#/definitions/waE2E.ThreadID_ThreadType" + } + } + }, + "waE2E.ThreadID_ThreadType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "ThreadID_UNKNOWN", + "ThreadID_VIEW_REPLIES", + "ThreadID_AI_THREAD" + ] + }, + "waE2E.URLMetadata": { + "type": "object", + "properties": { + "fbExperimentID": { + "type": "integer" + } + } + }, + "waE2E.UrlTrackingMap": { + "type": "object", + "properties": { + "urlTrackingMapElements": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.UrlTrackingMap_UrlTrackingMapElement" + } + } + } + }, + "waE2E.UrlTrackingMap_UrlTrackingMapElement": { + "type": "object", + "properties": { + "cardIndex": { + "type": "integer" + }, + "consentedUsersURL": { + "type": "string" + }, + "originalURL": { + "type": "string" + }, + "unconsentedUsersURL": { + "type": "string" + } + } + }, + "waE2E.VideoEndCard": { + "type": "object", + "properties": { + "caption": { + "type": "string" + }, + "profilePictureURL": { + "type": "string" + }, + "thumbnailImageURL": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "waE2E.VideoMessage": { + "type": "object", + "properties": { + "JPEGThumbnail": { + "type": "array", + "items": { + "type": "integer" + } + }, + "URL": { + "type": "string" + }, + "accessibilityLabel": { + "type": "string" + }, + "annotations": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.InteractiveAnnotation" + } + }, + "caption": { + "type": "string" + }, + "contextInfo": { + "$ref": "#/definitions/waE2E.ContextInfo" + }, + "directPath": { + "type": "string" + }, + "externalShareFullVideoDurationInSeconds": { + "type": "integer" + }, + "fileEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "fileLength": { + "type": "integer" + }, + "fileSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "gifAttribution": { + "$ref": "#/definitions/waE2E.VideoMessage_Attribution" + }, + "gifPlayback": { + "type": "boolean" + }, + "height": { + "type": "integer" + }, + "interactiveAnnotations": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.InteractiveAnnotation" + } + }, + "mediaKey": { + "type": "array", + "items": { + "type": "integer" + } + }, + "mediaKeyTimestamp": { + "type": "integer" + }, + "metadataURL": { + "type": "string" + }, + "mimetype": { + "type": "string" + }, + "motionPhotoPresentationOffsetMS": { + "type": "integer" + }, + "processedVideos": { + "type": "array", + "items": { + "$ref": "#/definitions/waE2E.ProcessedVideo" + } + }, + "seconds": { + "type": "integer" + }, + "staticURL": { + "type": "string" + }, + "streamingSidecar": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailDirectPath": { + "type": "string" + }, + "thumbnailEncSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "thumbnailSHA256": { + "type": "array", + "items": { + "type": "integer" + } + }, + "videoSourceType": { + "$ref": "#/definitions/waE2E.VideoMessage_VideoSourceType" + }, + "viewOnce": { + "type": "boolean" + }, + "width": { + "type": "integer" + } + } + }, + "waE2E.VideoMessage_Attribution": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "VideoMessage_NONE", + "VideoMessage_GIPHY", + "VideoMessage_TENOR", + "VideoMessage_KLIPY" + ] + }, + "waE2E.VideoMessage_VideoSourceType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "VideoMessage_USER_VIDEO", + "VideoMessage_AI_GENERATED" + ] + }, + "waE2E.WebLinkRenderConfig": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "WebLinkRenderConfig_WEBVIEW", + "WebLinkRenderConfig_SYSTEM" + ] + }, + "waMmsRetry.MediaRetryNotification_ResultType": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "MediaRetryNotification_GENERAL_ERROR", + "MediaRetryNotification_SUCCESS", + "MediaRetryNotification_NOT_FOUND", + "MediaRetryNotification_DECRYPTION_ERROR" + ] + }, + "waStatusAttributions.StatusAttribution": { + "type": "object", + "properties": { + "actionURL": { + "type": "string" + }, + "attributionData": { + "description": "Types that are valid to be assigned to AttributionData:\n\n\t*StatusAttribution_StatusReshare_\n\t*StatusAttribution_ExternalShare_\n\t*StatusAttribution_Music_\n\t*StatusAttribution_GroupStatus_\n\t*StatusAttribution_RlAttribution\n\t*StatusAttribution_AiCreatedAttribution_" + }, + "type": { + "$ref": "#/definitions/waStatusAttributions.StatusAttribution_Type" + } + } + }, + "waStatusAttributions.StatusAttribution_Type": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + ], + "x-enum-varnames": [ + "StatusAttribution_UNKNOWN", + "StatusAttribution_RESHARE", + "StatusAttribution_EXTERNAL_SHARE", + "StatusAttribution_MUSIC", + "StatusAttribution_STATUS_MENTION", + "StatusAttribution_GROUP_STATUS", + "StatusAttribution_RL_ATTRIBUTION", + "StatusAttribution_AI_CREATED", + "StatusAttribution_LAYOUTS", + "StatusAttribution_NEWSLETTER_STATUS", + "StatusAttribution_STATUS_CLOSE_SHARING" + ] + }, + "waVnameCert.LocalizedName": { + "type": "object", + "properties": { + "lc": { + "type": "string" + }, + "lg": { + "type": "string" + }, + "verifiedName": { + "type": "string" + } + } + }, + "waVnameCert.VerifiedNameCertificate": { + "type": "object", + "properties": { + "details": { + "type": "array", + "items": { + "type": "integer" + } + }, + "serverSignature": { + "type": "array", + "items": { + "type": "integer" + } + }, + "signature": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "waVnameCert.VerifiedNameCertificate_Details": { + "type": "object", + "properties": { + "issueTime": { + "type": "integer" + }, + "issuer": { + "type": "string" + }, + "localizedNames": { + "type": "array", + "items": { + "$ref": "#/definitions/waVnameCert.LocalizedName" + } + }, + "serial": { + "type": "integer" + }, + "verifiedName": { + "type": "string" + } + } + }, + "whatsmeow.ParticipantChange": { + "type": "string", + "enum": [ + "add", + "remove", + "promote", + "demote" + ], + "x-enum-varnames": [ + "ParticipantChangeAdd", + "ParticipantChangeRemove", + "ParticipantChangePromote", + "ParticipantChangeDemote" + ] + } + } +} \ No newline at end of file diff --git a/whatsapp-service/docs/swagger.yaml b/whatsapp-service/docs/swagger.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9fc09badd7509e653aff6bec02a1ba7dc3d9ac88 --- /dev/null +++ b/whatsapp-service/docs/swagger.yaml @@ -0,0 +1,9832 @@ +definitions: + gin.H: + additionalProperties: {} + type: object + agentdeck-whatsapp-service_pkg_call_service.RejectCallStruct: + properties: + callCreator: + $ref: '#/definitions/types.JID' + callId: + type: string + type: object + agentdeck-whatsapp-service_pkg_chat_service.BodyStruct: + properties: + chat: + type: string + type: object + agentdeck-whatsapp-service_pkg_chat_service.HistorySyncRequestStruct: + properties: + count: + type: integer + messageInfo: + $ref: '#/definitions/types.MessageInfo' + type: object + agentdeck-whatsapp-service_pkg_community_service.AddParticipantStruct: + properties: + communityJid: + type: string + groupJid: + items: + type: string + type: array + type: object + agentdeck-whatsapp-service_pkg_community_service.CreateCommunityStruct: + properties: + communityName: + type: string + type: object + agentdeck-whatsapp-service_pkg_group_service.AddParticipantStruct: + properties: + action: + $ref: '#/definitions/whatsmeow.ParticipantChange' + groupJid: + $ref: '#/definitions/types.JID' + participants: + items: + type: string + type: array + type: object + agentdeck-whatsapp-service_pkg_group_service.CreateGroupStruct: + properties: + groupName: + type: string + participants: + items: + type: string + type: array + type: object + agentdeck-whatsapp-service_pkg_group_service.GetGroupInfoStruct: + properties: + groupJid: + type: string + type: object + agentdeck-whatsapp-service_pkg_group_service.GetGroupInviteLinkStruct: + properties: + groupJid: + type: string + reset: + type: boolean + type: object + agentdeck-whatsapp-service_pkg_group_service.JoinGroupStruct: + properties: + code: + type: string + type: object + agentdeck-whatsapp-service_pkg_group_service.LeaveGroupStruct: + properties: + groupJid: + $ref: '#/definitions/types.JID' + type: object + agentdeck-whatsapp-service_pkg_group_service.SetGroupDescriptionStruct: + properties: + description: + type: string + groupJid: + type: string + type: object + agentdeck-whatsapp-service_pkg_group_service.SetGroupNameStruct: + properties: + groupJid: + type: string + name: + type: string + type: object + agentdeck-whatsapp-service_pkg_group_service.SetGroupPhotoStruct: + properties: + groupJid: + type: string + image: + type: string + type: object + agentdeck-whatsapp-service_pkg_group_service.UpdateGroupSettingsStruct: + properties: + action: + description: announcement, not_announcement, locked, unlocked + type: string + groupJid: + type: string + type: object + agentdeck-whatsapp-service_pkg_instance_model.AdvancedSettings: + properties: + alwaysOnline: + type: boolean + ignoreGroups: + type: boolean + ignoreStatus: + type: boolean + msgRejectCall: + type: string + readMessages: + type: boolean + rejectCall: + type: boolean + type: object + agentdeck-whatsapp-service_pkg_instance_service.ConnectStruct: + properties: + immediate: + type: boolean + natsEnable: + type: string + phone: + type: string + rabbitmqEnable: + type: string + subscribe: + items: + type: string + type: array + webhookUrl: + type: string + websocketEnable: + type: string + type: object + agentdeck-whatsapp-service_pkg_instance_service.CreateStruct: + properties: + advancedSettings: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_instance_model.AdvancedSettings' + instanceId: + type: string + name: + type: string + proxy: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_instance_service.ProxyConfig' + token: + type: string + type: object + agentdeck-whatsapp-service_pkg_instance_service.ForceReconnectStruct: + properties: + number: + type: string + type: object + agentdeck-whatsapp-service_pkg_instance_service.PairStruct: + properties: + phone: + type: string + subscribe: + items: + type: string + type: array + type: object + agentdeck-whatsapp-service_pkg_instance_service.ProxyConfig: + properties: + host: + type: string + password: + type: string + port: + type: string + protocol: + type: string + username: + type: string + type: object + agentdeck-whatsapp-service_pkg_instance_service.SetProxyStruct: + properties: + host: + type: string + password: + type: string + port: + type: string + protocol: + type: string + username: + type: string + required: + - host + - port + type: object + agentdeck-whatsapp-service_pkg_label_service.ChatLabelStruct: + properties: + jid: + type: string + labelId: + type: string + type: object + agentdeck-whatsapp-service_pkg_label_service.EditLabelStruct: + properties: + color: + type: integer + deleted: + type: boolean + labelId: + type: string + name: + type: string + type: object + agentdeck-whatsapp-service_pkg_label_service.MessageLabelStruct: + properties: + jid: + type: string + labelId: + type: string + messageId: + type: string + type: object + agentdeck-whatsapp-service_pkg_message_service.ChatPresenceStruct: + properties: + delay: + description: |- + Delay, in milliseconds, keeps the "composing"/"recording" indicator alive + for the given duration (re-sending it periodically) and then sends "paused". + Only applies when State is "composing". 0 = single fire (legacy behaviour). + type: integer + isAudio: + type: boolean + number: + type: string + state: + type: string + type: object + agentdeck-whatsapp-service_pkg_message_service.DownloadMediaStruct: + properties: + message: + $ref: '#/definitions/waE2E.Message' + type: object + agentdeck-whatsapp-service_pkg_message_service.EditMessageStruct: + properties: + chat: + type: string + message: + type: string + messageId: + type: string + type: object + agentdeck-whatsapp-service_pkg_message_service.MarkPlayedStruct: + properties: + id: + items: + type: string + type: array + number: + type: string + type: object + agentdeck-whatsapp-service_pkg_message_service.MarkReadStruct: + properties: + id: + items: + type: string + type: array + number: + type: string + type: object + agentdeck-whatsapp-service_pkg_message_service.MessageStatusStruct: + properties: + id: + type: string + type: object + agentdeck-whatsapp-service_pkg_message_service.MessageStruct: + properties: + chat: + type: string + messageId: + type: string + type: object + agentdeck-whatsapp-service_pkg_message_service.ReactStruct: + properties: + fromMe: + type: boolean + id: + type: string + number: + type: string + participant: + type: string + reaction: + type: string + type: object + agentdeck-whatsapp-service_pkg_newsletter_service.CreateNewsletterStruct: + properties: + description: + type: string + name: + type: string + type: object + agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterInviteStruct: + properties: + key: + type: string + type: object + agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterMessagesStruct: + properties: + before_id: + type: integer + count: + type: integer + jid: + $ref: '#/definitions/types.JID' + type: object + agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterStruct: + properties: + jid: + $ref: '#/definitions/types.JID' + type: object + agentdeck-whatsapp-service_pkg_poll_model.PollResults: + properties: + optionCounts: + additionalProperties: + type: integer + description: hash -> count + type: object + pollChatJid: + type: string + pollMessageId: + type: string + totalVotes: + type: integer + voters: + items: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_poll_model.VoterInfo' + type: array + votes: + items: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_poll_model.PollVote' + type: array + type: object + agentdeck-whatsapp-service_pkg_poll_model.PollVote: + properties: + companyId: + type: string + id: + type: string + instanceId: + type: string + pollChatJid: + type: string + pollMessageId: + type: string + receivedAt: + type: string + selectedOptions: + description: SHA-256 hashes + items: + type: string + type: array + voteMessageId: + type: string + votedAt: + type: string + voterJid: + type: string + voterName: + type: string + voterPhone: + type: string + type: object + agentdeck-whatsapp-service_pkg_poll_model.VoterInfo: + properties: + jid: + type: string + name: + type: string + phone: + type: string + selectedOptions: + items: + type: string + type: array + votedAt: + type: string + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.Button: + properties: + copyCode: + description: Code placed in the clipboard when type=copy. + example: PROMO2026 + type: string + currency: + description: ISO currency code for type=pix (e.g. BRL). + example: BRL + type: string + displayText: + description: Label rendered inside the button (reply / copy / url / call). + Ignored for pix. + example: Quero saber mais + type: string + id: + description: Callback payload for `reply` or code-to-copy internal id for + `copy`. + example: btn_info + type: string + key: + description: Pix key value matching the keyType. + example: "12345678900" + type: string + keyType: + description: 'Pix key type. One of: phone, email, cpf, cnpj, random.' + enum: + - phone + - email + - cpf + - cnpj + - random + example: cpf + type: string + name: + description: Merchant display name shown on the Pix sheet. + example: Minha Loja + type: string + phoneNumber: + description: Destination phone number (E.164) when type=call. + example: "+5582988898565" + type: string + type: + description: 'Button kind. One of: reply, copy, url, call, pix.' + enum: + - reply + - copy + - url + - call + - pix + example: reply + type: string + url: + description: Target URL when type=url. + example: https://agentdeck.ai + type: string + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.ButtonStruct: + properties: + buttons: + description: Buttons array. See combination rules on the parent type description. + items: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.Button' + type: array + delay: + description: Typing delay (milliseconds) applied before sending the message. + example: 1200 + type: integer + description: + description: Body description text (required). + example: Confira as condicoes abaixo + type: string + footer: + description: Footer text (required). + example: AgentDeck Whatsapp Service + type: string + formatJid: + description: If false, skips automatic formatting/validation of `number` into + a JID. + type: boolean + imageUrl: + description: Optional image URL used as header for reply-only buttons. + type: string + mentionAll: + description: Mention every participant (groups only). + type: boolean + mentionedJid: + description: JIDs to mention inside the body text. + items: + type: string + type: array + number: + description: Destination phone number. + example: "5582988898565" + type: string + quoted: + allOf: + - $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct' + description: Quoted (reply-to) context. + title: + description: Header title (required). + example: Oferta especial + type: string + videoUrl: + description: Optional video URL used as header for reply-only buttons. + type: string + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselButtonStruct: + properties: + copyCode: + description: Code placed in the clipboard when type=COPY. + example: PROMO2026 + type: string + displayText: + description: Label rendered inside the button. + example: Quero saber mais + type: string + id: + description: 'Context-dependent: REPLY payload, URL target (type=URL) or phone + number (type=CALL).' + example: card1_info + type: string + type: + description: 'Button kind (case-insensitive). One of: REPLY (default), URL, + CALL, COPY.' + enum: + - REPLY + - URL + - CALL + - COPY + - reply + - url + - call + - copy + example: REPLY + type: string + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardBodyStruct: + properties: + text: + description: Main text of the card. + example: Card 1 - Oferta especial + type: string + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardHeaderStruct: + properties: + imageUrl: + description: Public URL to an image. Downloaded, uploaded to WhatsApp servers + and used as card media. + example: https://picsum.photos/seed/card1/600/400 + type: string + subtitle: + description: Optional subtitle rendered below the title. + example: Somente hoje + type: string + title: + description: Optional visible title above the media. + example: Oferta do dia + type: string + videoUrl: + description: Public URL to a video. Used only when `imageUrl` is empty. + type: string + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardStruct: + properties: + body: + allOf: + - $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardBodyStruct' + description: Card body text (required). + buttons: + description: Buttons shown on the card. See CarouselButtonStruct for combination + rules. + items: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselButtonStruct' + type: array + footer: + description: Optional footer rendered under the body. + example: Por tempo limitado + type: string + header: + allOf: + - $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardHeaderStruct' + description: Card header (media + title/subtitle). + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselStruct: + properties: + body: + description: Optional message body shown above the cards. + example: Confira nossas novidades! + type: string + cards: + description: Cards displayed in order. At least one card is required. + items: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselCardStruct' + type: array + delay: + description: Typing delay (milliseconds) applied before sending the message. + example: 1200 + type: integer + footer: + description: Optional message footer shown below the cards. + example: AgentDeck Whatsapp Service + type: string + formatJid: + description: If false, skips automatic formatting/validation of `number` into + a JID. + type: boolean + number: + description: Destination phone number. + example: "5582988898565" + type: string + quoted: + allOf: + - $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct' + description: Quoted (reply-to) context. + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.ContactStruct: + properties: + delay: + type: integer + formatJid: + type: boolean + id: + type: string + mentionAll: + type: boolean + mentionedJid: + items: + type: string + type: array + number: + type: string + quoted: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct' + vcard: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_utils.VCardStruct' + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.LinkStruct: + properties: + delay: + type: integer + description: + type: string + formatJid: + type: boolean + id: + type: string + imgUrl: + type: string + mentionAll: + type: boolean + mentionedJid: + items: + type: string + type: array + number: + type: string + quoted: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct' + text: + type: string + title: + type: string + url: + type: string + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.ListStruct: + properties: + buttonText: + description: Label of the button that opens the list. Defaults to "Ver Menu" + when empty. + example: Abrir cardapio + type: string + delay: + description: Typing delay (milliseconds) applied before sending the message. + example: 1200 + type: integer + description: + description: Body description text (required). + example: Escolha o plano ideal para voce + type: string + footerText: + description: Footer text (required). + example: AgentDeck Whatsapp Service + type: string + formatJid: + description: If false, skips automatic formatting/validation of `number` into + a JID. + type: boolean + mentionAll: + description: Mention every participant (groups only). + type: boolean + mentionedJid: + description: JIDs to mention inside the body text. + items: + type: string + type: array + number: + description: Destination phone number. + example: "5582988898565" + type: string + quoted: + allOf: + - $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct' + description: Quoted (reply-to) context. + sections: + description: Sections with rows. At least one section with one row is required. + items: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.Section' + type: array + title: + description: Header title (required). + example: Nossos planos + type: string + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.LocationStruct: + properties: + address: + type: string + delay: + type: integer + formatJid: + type: boolean + id: + type: string + latitude: + type: number + longitude: + type: number + mentionAll: + type: boolean + mentionedJid: + items: + type: string + type: array + name: + type: string + number: + type: string + quoted: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct' + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.MediaStruct: + properties: + caption: + type: string + delay: + type: integer + filename: + type: string + formatJid: + type: boolean + forwardingScore: + type: integer + id: + type: string + mentionAll: + type: boolean + mentionedJid: + items: + type: string + type: array + number: + type: string + quoted: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct' + type: + type: string + url: + type: string + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.PollStruct: + properties: + delay: + type: integer + formatJid: + type: boolean + id: + type: string + maxAnswer: + type: integer + mentionAll: + type: boolean + mentionedJid: + items: + type: string + type: array + number: + type: string + options: + items: + type: string + type: array + question: + type: string + quoted: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct' + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct: + properties: + messageId: + type: string + participant: + type: string + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.Row: + properties: + description: + description: Optional secondary line below the title. + example: R$ 29,90/mes + type: string + rowId: + description: Callback payload returned when the user taps the row. Auto-generated + if empty. + example: plan_basic + type: string + title: + description: Row main label. + example: Plano Basico + type: string + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.Section: + properties: + rows: + description: Rows inside this section. + items: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.Row' + type: array + title: + description: Section heading (optional; rendered as a group separator). + example: Planos + type: string + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.StatusTextStruct: + properties: + id: + type: string + text: + type: string + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.StickerStruct: + properties: + delay: + type: integer + formatJid: + type: boolean + id: + type: string + mentionAll: + type: boolean + mentionedJid: + items: + type: string + type: array + number: + type: string + quoted: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct' + sticker: + type: string + type: object + agentdeck-whatsapp-service_pkg_sendMessage_service.TextStruct: + properties: + delay: + type: integer + formatJid: + type: boolean + forwardingScore: + type: integer + id: + type: string + mentionAll: + type: boolean + mentionedJid: + items: + type: string + type: array + number: + type: string + quoted: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.QuotedStruct' + text: + type: string + type: object + agentdeck-whatsapp-service_pkg_user_service.BlockStruct: + properties: + number: + type: string + type: object + agentdeck-whatsapp-service_pkg_user_service.CheckUserStruct: + properties: + formatJid: + type: boolean + number: + items: + type: string + type: array + type: object + agentdeck-whatsapp-service_pkg_user_service.GetAvatarStruct: + properties: + number: + type: string + preview: + type: boolean + type: object + agentdeck-whatsapp-service_pkg_user_service.PrivacyStruct: + properties: + callAdd: + $ref: '#/definitions/types.PrivacySetting' + groupAdd: + $ref: '#/definitions/types.PrivacySetting' + lastSeen: + $ref: '#/definitions/types.PrivacySetting' + online: + $ref: '#/definitions/types.PrivacySetting' + profile: + $ref: '#/definitions/types.PrivacySetting' + readReceipts: + $ref: '#/definitions/types.PrivacySetting' + status: + $ref: '#/definitions/types.PrivacySetting' + type: object + agentdeck-whatsapp-service_pkg_user_service.SetProfilePictureStruct: + properties: + image: + type: string + type: object + agentdeck-whatsapp-service_pkg_utils.VCardStruct: + properties: + fullName: + type: string + organization: + type: string + phone: + type: string + type: object + types.AddressingMode: + enum: + - pn + - lid + type: string + x-enum-varnames: + - AddressingModePN + - AddressingModeLID + types.BotEditType: + enum: + - first + - inner + - last + type: string + x-enum-varnames: + - EditTypeFirst + - EditTypeInner + - EditTypeLast + types.BroadcastRecipient: + properties: + lid: + $ref: '#/definitions/types.JID' + pn: + $ref: '#/definitions/types.JID' + type: object + types.DeviceSentMeta: + properties: + destinationJID: + description: The destination user. This should match the MessageInfo.Recipient + field. + type: string + phash: + type: string + type: object + types.EditAttribute: + enum: + - "" + - "1" + - "2" + - "3" + - "7" + - "8" + type: string + x-enum-comments: + EditAttributeAdminEdit: only used in newsletters + x-enum-descriptions: + - "" + - "" + - "" + - only used in newsletters + - "" + - "" + x-enum-varnames: + - EditAttributeEmpty + - EditAttributeMessageEdit + - EditAttributePinInChat + - EditAttributeAdminEdit + - EditAttributeSenderRevoke + - EditAttributeAdminRevoke + types.JID: + properties: + device: + format: int32 + type: integer + integrator: + format: int32 + type: integer + rawAgent: + format: int32 + type: integer + server: + type: string + user: + type: string + type: object + types.MessageInfo: + properties: + addressingMode: + allOf: + - $ref: '#/definitions/types.AddressingMode' + description: The addressing mode of the message (phone number or LID) + broadcastListOwner: + allOf: + - $ref: '#/definitions/types.JID' + description: |- + When sending a read receipt to a broadcast list message, the Chat is the broadcast list + and Sender is you, so this field contains the recipient of the read receipt. + broadcastRecipients: + items: + $ref: '#/definitions/types.BroadcastRecipient' + type: array + category: + type: string + chat: + allOf: + - $ref: '#/definitions/types.JID' + description: The chat where the message was sent. + deviceSentMeta: + allOf: + - $ref: '#/definitions/types.DeviceSentMeta' + description: Metadata for direct messages sent from another one of the user's + own devices. + edit: + $ref: '#/definitions/types.EditAttribute' + id: + type: string + isFromMe: + description: Whether the message was sent by the current user instead of someone + else. + type: boolean + isGroup: + description: Whether the chat is a group chat or broadcast list. + type: boolean + mediaType: + type: string + msgBotInfo: + $ref: '#/definitions/types.MsgBotInfo' + msgMetaInfo: + $ref: '#/definitions/types.MsgMetaInfo' + multicast: + type: boolean + pushName: + type: string + recipientAlt: + allOf: + - $ref: '#/definitions/types.JID' + description: The alternative address of the recipient of the message for DMs. + sender: + allOf: + - $ref: '#/definitions/types.JID' + description: The user who sent the message. + senderAlt: + allOf: + - $ref: '#/definitions/types.JID' + description: The alternative address of the user who sent the message + serverID: + type: integer + timestamp: + type: string + type: + type: string + verifiedName: + $ref: '#/definitions/types.VerifiedName' + type: object + types.MsgBotInfo: + properties: + editSenderTimestampMS: + type: string + editTargetID: + type: string + editType: + $ref: '#/definitions/types.BotEditType' + type: object + types.MsgMetaInfo: + properties: + deprecatedLIDSession: + type: boolean + targetChat: + $ref: '#/definitions/types.JID' + targetID: + description: Bot things + type: string + targetSender: + $ref: '#/definitions/types.JID' + threadMessageID: + type: string + threadMessageSenderJID: + $ref: '#/definitions/types.JID' + type: object + types.PrivacySetting: + enum: + - "" + - all + - contacts + - contact_allowlist + - contact_blacklist + - match_last_seen + - known + - none + - on_standard + - "off" + type: string + x-enum-varnames: + - PrivacySettingUndefined + - PrivacySettingAll + - PrivacySettingContacts + - PrivacySettingContactAllowlist + - PrivacySettingContactBlacklist + - PrivacySettingMatchLastSeen + - PrivacySettingKnown + - PrivacySettingNone + - PrivacySettingOnStandard + - PrivacySettingOff + types.VerifiedName: + properties: + certificate: + $ref: '#/definitions/waVnameCert.VerifiedNameCertificate' + details: + $ref: '#/definitions/waVnameCert.VerifiedNameCertificate_Details' + type: object + types.WebAuthnResponse: + properties: + id: + type: string + rawId: + items: + type: integer + type: array + response: + $ref: '#/definitions/types.WebAuthnResponseData' + type: + type: string + type: object + types.WebAuthnResponseData: + properties: + authenticatorData: + items: + type: integer + type: array + clientDataJSON: + items: + type: integer + type: array + signature: + items: + type: integer + type: array + userHandle: + items: + type: integer + type: array + type: object + waAICommon.AIMediaCollectionMessage: + properties: + collectionID: + type: string + expectedMediaCount: + type: integer + hasGlobalCaption: + type: boolean + type: object + waAICommon.AIMediaCollectionMetadata: + properties: + collectionID: + type: string + uploadOrderIndex: + type: integer + type: object + waAICommon.AIMetadataOperation: + properties: + hatchMetadataSync: + $ref: '#/definitions/waAICommon.HatchMetadataSync' + type: object + waAICommon.AIRegenerateMetadata: + properties: + messageKey: + $ref: '#/definitions/waCommon.MessageKey' + responseTimestampMS: + type: integer + type: object + waAICommon.AIRichResponseUnifiedResponse: + properties: + data: + items: + type: integer + type: array + type: object + waAICommon.AISubscriptionRequestType: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - AISubscriptionRequestType_UNSPECIFIED + - AISubscriptionRequestType_THINK_HARD + - AISubscriptionRequestType_IMAGE_GEN + - AISubscriptionRequestType_VIDEO_GEN + waAICommon.AISubscriptionUpsellMetadata: + properties: + requestType: + $ref: '#/definitions/waAICommon.AISubscriptionRequestType' + type: object + waAICommon.AIThreadInfo: + properties: + clientInfo: + $ref: '#/definitions/waAICommon.AIThreadInfo_AIThreadClientInfo' + serverInfo: + $ref: '#/definitions/waAICommon.AIThreadInfo_AIThreadServerInfo' + type: object + waAICommon.AIThreadInfo_AIThreadClientInfo: + properties: + sourceChatJID: + type: string + type: + $ref: '#/definitions/waAICommon.AIThreadInfo_AIThreadClientInfo_AIThreadType' + type: object + waAICommon.AIThreadInfo_AIThreadClientInfo_AIThreadType: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - AIThreadInfo_AIThreadClientInfo_UNKNOWN + - AIThreadInfo_AIThreadClientInfo_DEFAULT + - AIThreadInfo_AIThreadClientInfo_INCOGNITO + - AIThreadInfo_AIThreadClientInfo_SIDE_CHAT + waAICommon.AIThreadInfo_AIThreadServerInfo: + properties: + title: + type: string + type: object + waAICommon.BotAgeCollectionMetadata: + properties: + ageCollectionEligible: + type: boolean + ageCollectionType: + $ref: '#/definitions/waAICommon.BotAgeCollectionMetadata_AgeCollectionType' + shouldTriggerAgeCollectionOnClient: + type: boolean + type: object + waAICommon.BotAgeCollectionMetadata_AgeCollectionType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - BotAgeCollectionMetadata_O18_BINARY + - BotAgeCollectionMetadata_WAFFLE + waAICommon.BotAgentDeepLinkMetadata: + properties: + token: + type: string + type: object + waAICommon.BotAgentMetadata: + properties: + deepLinkMetadata: + $ref: '#/definitions/waAICommon.BotAgentDeepLinkMetadata' + type: object + waAICommon.BotCapabilityMetadata: + properties: + capabilities: + items: + $ref: '#/definitions/waAICommon.BotCapabilityMetadata_BotCapabilityType' + type: array + type: object + waAICommon.BotCapabilityMetadata_BotCapabilityType: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + - 13 + - 14 + - 15 + - 16 + - 17 + - 18 + - 19 + - 20 + - 21 + - 22 + - 23 + - 24 + - 25 + - 26 + - 27 + - 28 + - 29 + - 30 + - 31 + - 32 + - 33 + - 34 + - 35 + - 36 + - 37 + - 38 + - 39 + - 40 + - 41 + - 42 + - 43 + - 44 + - 45 + - 46 + - 47 + - 48 + - 49 + - 50 + - 51 + - 52 + - 53 + - 54 + - 55 + - 56 + - 57 + - 58 + - 59 + - 60 + - 61 + - 62 + - 63 + - 64 + - 65 + format: int32 + type: integer + x-enum-varnames: + - BotCapabilityMetadata_UNKNOWN + - BotCapabilityMetadata_PROGRESS_INDICATOR + - BotCapabilityMetadata_RICH_RESPONSE_HEADING + - BotCapabilityMetadata_RICH_RESPONSE_NESTED_LIST + - BotCapabilityMetadata_AI_MEMORY + - BotCapabilityMetadata_RICH_RESPONSE_THREAD_SURFING + - BotCapabilityMetadata_RICH_RESPONSE_TABLE + - BotCapabilityMetadata_RICH_RESPONSE_CODE + - BotCapabilityMetadata_RICH_RESPONSE_STRUCTURED_RESPONSE + - BotCapabilityMetadata_RICH_RESPONSE_INLINE_IMAGE + - BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_CONTROL + - BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_1 + - BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_2 + - BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_3 + - BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_4 + - BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_5 + - BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_6 + - BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_7 + - BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_8 + - BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_9 + - BotCapabilityMetadata_WA_IG_1P_PLUGIN_RANKING_UPDATE_10 + - BotCapabilityMetadata_RICH_RESPONSE_SUB_HEADING + - BotCapabilityMetadata_RICH_RESPONSE_GRID_IMAGE + - BotCapabilityMetadata_AI_STUDIO_UGC_MEMORY + - BotCapabilityMetadata_RICH_RESPONSE_LATEX + - BotCapabilityMetadata_RICH_RESPONSE_MAPS + - BotCapabilityMetadata_RICH_RESPONSE_INLINE_REELS + - BotCapabilityMetadata_AGENTIC_PLANNING + - BotCapabilityMetadata_ACCOUNT_LINKING + - BotCapabilityMetadata_STREAMING_DISAGGREGATION + - BotCapabilityMetadata_RICH_RESPONSE_GRID_IMAGE_3P + - BotCapabilityMetadata_RICH_RESPONSE_LATEX_INLINE + - BotCapabilityMetadata_QUERY_PLAN + - BotCapabilityMetadata_PROACTIVE_MESSAGE + - BotCapabilityMetadata_RICH_RESPONSE_UNIFIED_RESPONSE + - BotCapabilityMetadata_PROMOTION_MESSAGE + - BotCapabilityMetadata_SIMPLIFIED_PROFILE_PAGE + - BotCapabilityMetadata_RICH_RESPONSE_SOURCES_IN_MESSAGE + - BotCapabilityMetadata_RICH_RESPONSE_SIDE_BY_SIDE_SURVEY + - BotCapabilityMetadata_RICH_RESPONSE_UNIFIED_TEXT_COMPONENT + - BotCapabilityMetadata_AI_SHARED_MEMORY + - BotCapabilityMetadata_RICH_RESPONSE_UNIFIED_SOURCES + - BotCapabilityMetadata_RICH_RESPONSE_UNIFIED_DOMAIN_CITATIONS + - BotCapabilityMetadata_RICH_RESPONSE_UR_INLINE_REELS_ENABLED + - BotCapabilityMetadata_RICH_RESPONSE_UR_MEDIA_GRID_ENABLED + - BotCapabilityMetadata_RICH_RESPONSE_UR_TIMESTAMP_PLACEHOLDER + - BotCapabilityMetadata_RICH_RESPONSE_IN_APP_SURVEY + - BotCapabilityMetadata_AI_RESPONSE_MODEL_BRANDING + - BotCapabilityMetadata_SESSION_TRANSPARENCY_SYSTEM_MESSAGE + - BotCapabilityMetadata_RICH_RESPONSE_UR_REASONING + - BotCapabilityMetadata_RICH_RESPONSE_UR_ZEITGEIST_CITATIONS + - BotCapabilityMetadata_RICH_RESPONSE_UR_ZEITGEIST_CAROUSEL + - BotCapabilityMetadata_AI_IMAGINE_LOADING_INDICATOR + - BotCapabilityMetadata_RICH_RESPONSE_UR_IMAGINE + - BotCapabilityMetadata_AI_IMAGINE_UR_TO_NATIVE_LOADING_INDICATOR + - BotCapabilityMetadata_RICH_RESPONSE_UR_BLOKS_ENABLED + - BotCapabilityMetadata_RICH_RESPONSE_INLINE_LINKS_ENABLED + - BotCapabilityMetadata_RICH_RESPONSE_UR_IMAGINE_VIDEO + - BotCapabilityMetadata_JSON_PATCH_STREAMING + - BotCapabilityMetadata_AI_TAB_FORCE_CLIPPY + - BotCapabilityMetadata_UNIFIED_RESPONSE_EMBEDDED_SCREENS + - BotCapabilityMetadata_AI_SUBSCRIPTION_ENABLED + - BotCapabilityMetadata_UNIFIED_RESPONSE_AI_CONTENT_SEARCH_ENABLED + - BotCapabilityMetadata_UNIFIED_RESPONSE_MARKDOWN_LINKS_ENABLED + - BotCapabilityMetadata_AI_RICH_RESPONSE_MAPS_V2_ENABLED + - BotCapabilityMetadata_AI_SUBSCRIPTION_METERING_ENABLED + waAICommon.BotCommandMetadata: + properties: + commandDescription: + type: string + commandName: + type: string + commandPrompt: + type: string + type: object + waAICommon.BotDocumentMessageMetadata: + properties: + pluginType: + $ref: '#/definitions/waAICommon.BotDocumentMessageMetadata_DocumentPluginType' + type: object + waAICommon.BotDocumentMessageMetadata_DocumentPluginType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - BotDocumentMessageMetadata_TEXT_EXTRACTION + - BotDocumentMessageMetadata_OCR_AND_IMAGES + waAICommon.BotFeedbackMessage: + properties: + kind: + $ref: '#/definitions/waAICommon.BotFeedbackMessage_BotFeedbackKind' + kindNegative: + type: integer + kindPositive: + type: integer + kindReport: + $ref: '#/definitions/waAICommon.BotFeedbackMessage_ReportKind' + messageKey: + $ref: '#/definitions/waCommon.MessageKey' + sideBySideSurveyMetadata: + $ref: '#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata' + text: + type: string + type: object + waAICommon.BotFeedbackMessage_BotFeedbackKind: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + - 13 + - 14 + format: int32 + type: integer + x-enum-varnames: + - BotFeedbackMessage_BOT_FEEDBACK_POSITIVE + - BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_GENERIC + - BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_HELPFUL + - BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_INTERESTING + - BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_ACCURATE + - BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_SAFE + - BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_OTHER + - BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_REFUSED + - BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_NOT_VISUALLY_APPEALING + - BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_NOT_RELEVANT_TO_TEXT + - BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_PERSONALIZED + - BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_CLARITY + - BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_DOESNT_LOOK_LIKE_THE_PERSON + - BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE_HALLUCINATION_INTERNAL_ONLY + - BotFeedbackMessage_BOT_FEEDBACK_NEGATIVE + waAICommon.BotFeedbackMessage_ReportKind: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - BotFeedbackMessage_NONE + - BotFeedbackMessage_GENERIC + waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata: + properties: + analyticsData: + $ref: '#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SideBySideSurveyAnalyticsData' + isSelectedResponsePrimary: + type: boolean + messageIDToEdit: + type: string + metaAiAnalyticsData: + $ref: '#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData' + responseOtid: + type: string + responseTimestampMSString: + type: string + selectedRequestID: + type: string + simonSessionFbid: + type: string + surveyID: + type: integer + type: object + waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SideBySideSurveyAnalyticsData: + properties: + simonSessionFbid: + type: string + tessaEvent: + type: string + tessaSessionFbid: + type: string + type: object + waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData: + properties: + abandonEvent: + $ref: '#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyAbandonEventData' + cardImpressionEvent: + $ref: '#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCardImpressionEventData' + ctaClickEvent: + $ref: '#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCTAClickEventData' + ctaImpressionEvent: + $ref: '#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCTAImpressionEventData' + primaryResponseID: + type: string + responseEvent: + $ref: '#/definitions/waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyResponseEventData' + surveyID: + type: integer + testArmName: + type: string + timestampMSString: + type: string + type: object + waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyAbandonEventData: + properties: + abandonDwellTimeMSString: + type: string + type: object + waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCTAClickEventData: + properties: + clickDwellTimeMSString: + type: string + isSurveyExpired: + type: boolean + type: object + ? waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCTAImpressionEventData + : properties: + isSurveyExpired: + type: boolean + type: object + ? waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyCardImpressionEventData + : type: object + waAICommon.BotFeedbackMessage_SideBySideSurveyMetadata_SidebySideSurveyMetaAiAnalyticsData_SideBySideSurveyResponseEventData: + properties: + responseDwellTimeMSString: + type: string + selectedResponseID: + type: string + type: object + waAICommon.BotGroupMetadata: + properties: + participantsMetadata: + items: + $ref: '#/definitions/waAICommon.BotGroupParticipantMetadata' + type: array + type: object + waAICommon.BotGroupParticipantMetadata: + properties: + botFbid: + type: string + type: object + waAICommon.BotImagineMetadata: + properties: + imagineType: + $ref: '#/definitions/waAICommon.BotImagineMetadata_ImagineType' + shortPrompt: + type: string + type: object + waAICommon.BotImagineMetadata_ImagineType: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + format: int32 + type: integer + x-enum-varnames: + - BotImagineMetadata_UNKNOWN + - BotImagineMetadata_IMAGINE + - BotImagineMetadata_MEMU + - BotImagineMetadata_FLASH + - BotImagineMetadata_EDIT + waAICommon.BotInfrastructureDiagnostics: + properties: + botBackend: + $ref: '#/definitions/waAICommon.BotInfrastructureDiagnostics_BotBackend' + isThinking: + type: boolean + toolsUsed: + items: + type: string + type: array + type: object + waAICommon.BotInfrastructureDiagnostics_BotBackend: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - BotInfrastructureDiagnostics_AAPI + - BotInfrastructureDiagnostics_CLIPPY + waAICommon.BotLinkedAccount: + properties: + type: + $ref: '#/definitions/waAICommon.BotLinkedAccount_BotLinkedAccountType' + type: object + waAICommon.BotLinkedAccount_BotLinkedAccountType: + enum: + - 0 + format: int32 + type: integer + x-enum-varnames: + - BotLinkedAccount_BOT_LINKED_ACCOUNT_TYPE_1P + waAICommon.BotLinkedAccountsMetadata: + properties: + acAuthTokens: + items: + type: integer + type: array + acErrorCode: + type: integer + accounts: + items: + $ref: '#/definitions/waAICommon.BotLinkedAccount' + type: array + type: object + waAICommon.BotMediaMetadata: + properties: + directPath: + type: string + fileEncSHA256: + type: string + fileSHA256: + type: string + mediaKey: + type: string + mediaKeyTimestamp: + type: integer + mimetype: + type: string + orientationType: + $ref: '#/definitions/waAICommon.BotMediaMetadata_OrientationType' + type: object + waAICommon.BotMediaMetadata_OrientationType: + enum: + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - BotMediaMetadata_CENTER + - BotMediaMetadata_LEFT + - BotMediaMetadata_RIGHT + waAICommon.BotMemoryFact: + properties: + fact: + type: string + factID: + type: string + type: object + waAICommon.BotMemoryMetadata: + properties: + addedFacts: + items: + $ref: '#/definitions/waAICommon.BotMemoryFact' + type: array + disclaimer: + type: string + removedFacts: + items: + $ref: '#/definitions/waAICommon.BotMemoryFact' + type: array + type: object + waAICommon.BotMemuMetadata: + properties: + faceImages: + items: + $ref: '#/definitions/waAICommon.BotMediaMetadata' + type: array + type: object + waAICommon.BotMessageOrigin: + properties: + type: + $ref: '#/definitions/waAICommon.BotMessageOrigin_BotMessageOriginType' + type: object + waAICommon.BotMessageOrigin_BotMessageOriginType: + enum: + - 0 + format: int32 + type: integer + x-enum-varnames: + - BotMessageOrigin_BOT_MESSAGE_ORIGIN_TYPE_AI_INITIATED + waAICommon.BotMessageOriginMetadata: + properties: + origins: + items: + $ref: '#/definitions/waAICommon.BotMessageOrigin' + type: array + type: object + waAICommon.BotMessageSharingInfo: + properties: + botEntryPointOrigin: + $ref: '#/definitions/waAICommon.BotMetricsEntryPoint' + forwardScore: + type: integer + type: object + waAICommon.BotMetadata: + properties: + aiConversationContext: + items: + type: integer + type: array + aiMediaCollectionMetadata: + $ref: '#/definitions/waAICommon.AIMediaCollectionMetadata' + botAgeCollectionMetadata: + $ref: '#/definitions/waAICommon.BotAgeCollectionMetadata' + botDocumentMessageMetadata: + $ref: '#/definitions/waAICommon.BotDocumentMessageMetadata' + botGroupMetadata: + $ref: '#/definitions/waAICommon.BotGroupMetadata' + botInfrastructureDiagnostics: + $ref: '#/definitions/waAICommon.BotInfrastructureDiagnostics' + botLinkedAccountsMetadata: + $ref: '#/definitions/waAICommon.BotLinkedAccountsMetadata' + botMessageOriginMetadata: + $ref: '#/definitions/waAICommon.BotMessageOriginMetadata' + botMetricsMetadata: + $ref: '#/definitions/waAICommon.BotMetricsMetadata' + botModeSelectionMetadata: + $ref: '#/definitions/waAICommon.BotModeSelectionMetadata' + botPromotionMessageMetadata: + $ref: '#/definitions/waAICommon.BotPromotionMessageMetadata' + botQuotaMetadata: + $ref: '#/definitions/waAICommon.BotQuotaMetadata' + botRenderingConfigMetadata: + $ref: '#/definitions/waAICommon.BotRenderingConfigMetadata' + botResponseID: + type: string + botThreadInfo: + $ref: '#/definitions/waAICommon.AIThreadInfo' + capabilityMetadata: + $ref: '#/definitions/waAICommon.BotCapabilityMetadata' + commandMetadata: + $ref: '#/definitions/waAICommon.BotCommandMetadata' + conversationStarterPromptID: + type: string + imagineMetadata: + $ref: '#/definitions/waAICommon.BotImagineMetadata' + inThreadSurveyMetadata: + $ref: '#/definitions/waAICommon.InThreadSurveyMetadata' + internalMetadata: + items: + type: integer + type: array + invokerJID: + type: string + memoryMetadata: + $ref: '#/definitions/waAICommon.BotMemoryMetadata' + memuMetadata: + $ref: '#/definitions/waAICommon.BotMemuMetadata' + messageDisclaimerText: + type: string + modelMetadata: + $ref: '#/definitions/waAICommon.BotModelMetadata' + personaID: + type: string + pluginMetadata: + $ref: '#/definitions/waAICommon.BotPluginMetadata' + progressIndicatorMetadata: + $ref: '#/definitions/waAICommon.BotProgressIndicatorMetadata' + pttPromptMetadata: + $ref: '#/definitions/waAICommon.BotPttPromptMetadata' + regenerateMetadata: + $ref: '#/definitions/waAICommon.AIRegenerateMetadata' + reminderMetadata: + $ref: '#/definitions/waAICommon.BotReminderMetadata' + renderingMetadata: + $ref: '#/definitions/waAICommon.BotRenderingMetadata' + resolvedToolCallMetadata: + $ref: '#/definitions/waAICommon.BotResolvedToolCallMetadata' + richResponseSourcesMetadata: + $ref: '#/definitions/waAICommon.BotSourcesMetadata' + sessionMetadata: + $ref: '#/definitions/waAICommon.BotSessionMetadata' + sessionTransparencyMetadata: + $ref: '#/definitions/waAICommon.SessionTransparencyMetadata' + subscriptionUpsellMetadata: + $ref: '#/definitions/waAICommon.AISubscriptionUpsellMetadata' + suggestedPromptMetadata: + $ref: '#/definitions/waAICommon.BotSuggestedPromptMetadata' + timezone: + type: string + unifiedResponseMutation: + $ref: '#/definitions/waAICommon.BotUnifiedResponseMutation' + verificationMetadata: + $ref: '#/definitions/waAICommon.BotSignatureVerificationMetadata' + type: object + waAICommon.BotMetricsEntryPoint: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + - 13 + - 14 + - 15 + - 16 + - 17 + - 18 + - 19 + - 20 + - 21 + - 22 + - 23 + - 24 + - 25 + - 26 + - 27 + - 28 + - 29 + - 30 + - 31 + - 32 + - 33 + - 34 + - 35 + - 36 + - 37 + - 38 + - 39 + - 40 + - 41 + - 45 + - 46 + - 47 + - 54 + - 55 + - 56 + format: int32 + type: integer + x-enum-varnames: + - BotMetricsEntryPoint_UNDEFINED_ENTRY_POINT + - BotMetricsEntryPoint_FAVICON + - BotMetricsEntryPoint_CHATLIST + - BotMetricsEntryPoint_AISEARCH_NULL_STATE_PAPER_PLANE + - BotMetricsEntryPoint_AISEARCH_NULL_STATE_SUGGESTION + - BotMetricsEntryPoint_AISEARCH_TYPE_AHEAD_SUGGESTION + - BotMetricsEntryPoint_AISEARCH_TYPE_AHEAD_PAPER_PLANE + - BotMetricsEntryPoint_AISEARCH_TYPE_AHEAD_RESULT_CHATLIST + - BotMetricsEntryPoint_AISEARCH_TYPE_AHEAD_RESULT_MESSAGES + - BotMetricsEntryPoint_AIVOICE_SEARCH_BAR + - BotMetricsEntryPoint_AIVOICE_FAVICON + - BotMetricsEntryPoint_AISTUDIO + - BotMetricsEntryPoint_DEEPLINK + - BotMetricsEntryPoint_NOTIFICATION + - BotMetricsEntryPoint_PROFILE_MESSAGE_BUTTON + - BotMetricsEntryPoint_FORWARD + - BotMetricsEntryPoint_APP_SHORTCUT + - BotMetricsEntryPoint_FF_FAMILY + - BotMetricsEntryPoint_AI_TAB + - BotMetricsEntryPoint_AI_HOME + - BotMetricsEntryPoint_AI_DEEPLINK_IMMERSIVE + - BotMetricsEntryPoint_AI_DEEPLINK + - BotMetricsEntryPoint_META_AI_CHAT_SHORTCUT_AI_STUDIO + - BotMetricsEntryPoint_UGC_CHAT_SHORTCUT_AI_STUDIO + - BotMetricsEntryPoint_NEW_CHAT_AI_STUDIO + - BotMetricsEntryPoint_AIVOICE_FAVICON_CALL_HISTORY + - BotMetricsEntryPoint_ASK_META_AI_CONTEXT_MENU + - BotMetricsEntryPoint_ASK_META_AI_CONTEXT_MENU_1ON1 + - BotMetricsEntryPoint_ASK_META_AI_CONTEXT_MENU_GROUP + - BotMetricsEntryPoint_INVOKE_META_AI_1ON1 + - BotMetricsEntryPoint_INVOKE_META_AI_GROUP + - BotMetricsEntryPoint_META_AI_FORWARD + - BotMetricsEntryPoint_NEW_CHAT_AI_CONTACT + - BotMetricsEntryPoint_MESSAGE_QUICK_ACTION_1_ON_1_CHAT + - BotMetricsEntryPoint_MESSAGE_QUICK_ACTION_GROUP_CHAT + - BotMetricsEntryPoint_ATTACHMENT_TRAY_1_ON_1_CHAT + - BotMetricsEntryPoint_ATTACHMENT_TRAY_GROUP_CHAT + - BotMetricsEntryPoint_ASK_META_AI_MEDIA_VIEWER_1ON1 + - BotMetricsEntryPoint_ASK_META_AI_MEDIA_VIEWER_GROUP + - BotMetricsEntryPoint_MEDIA_PICKER_1_ON_1_CHAT + - BotMetricsEntryPoint_MEDIA_PICKER_GROUP_CHAT + - BotMetricsEntryPoint_ASK_META_AI_NO_SEARCH_RESULTS + - BotMetricsEntryPoint_META_AI_SETTINGS + - BotMetricsEntryPoint_WEB_INTRO_PANEL + - BotMetricsEntryPoint_WEB_NAVIGATION_BAR + - BotMetricsEntryPoint_GROUP_MEMBER + - BotMetricsEntryPoint_CHATLIST_SEARCH + - BotMetricsEntryPoint_NEW_CHAT_LIST + waAICommon.BotMetricsMetadata: + properties: + destinationEntryPoint: + $ref: '#/definitions/waAICommon.BotMetricsEntryPoint' + destinationID: + type: string + threadOrigin: + $ref: '#/definitions/waAICommon.BotMetricsThreadEntryPoint' + type: object + waAICommon.BotMetricsThreadEntryPoint: + enum: + - 1 + - 2 + - 3 + - 4 + - 5 + format: int32 + type: integer + x-enum-varnames: + - BotMetricsThreadEntryPoint_AI_TAB_THREAD + - BotMetricsThreadEntryPoint_AI_HOME_THREAD + - BotMetricsThreadEntryPoint_AI_DEEPLINK_IMMERSIVE_THREAD + - BotMetricsThreadEntryPoint_AI_DEEPLINK_THREAD + - BotMetricsThreadEntryPoint_ASK_META_AI_CONTEXT_MENU_THREAD + waAICommon.BotModeSelectionMetadata: + properties: + mode: + items: + $ref: '#/definitions/waAICommon.BotModeSelectionMetadata_BotUserSelectionMode' + type: array + overrideMode: + items: + type: integer + type: array + type: object + waAICommon.BotModeSelectionMetadata_BotUserSelectionMode: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - BotModeSelectionMetadata_DEFAULT_MODE + - BotModeSelectionMetadata_THINK_HARD_MODE + waAICommon.BotModelMetadata: + properties: + modelNameOverride: + type: string + modelType: + $ref: '#/definitions/waAICommon.BotModelMetadata_ModelType' + premiumModelStatus: + $ref: '#/definitions/waAICommon.BotModelMetadata_PremiumModelStatus' + type: object + waAICommon.BotModelMetadata_ModelType: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - BotModelMetadata_UNKNOWN_TYPE + - BotModelMetadata_LLAMA_PROD + - BotModelMetadata_LLAMA_PROD_PREMIUM + waAICommon.BotModelMetadata_PremiumModelStatus: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - BotModelMetadata_UNKNOWN_STATUS + - BotModelMetadata_AVAILABLE + - BotModelMetadata_QUOTA_EXCEED_LIMIT + waAICommon.BotPluginMetadata: + properties: + deprecatedField: + $ref: '#/definitions/waAICommon.BotPluginMetadata_PluginType' + expectedLinksCount: + type: integer + faviconCDNURL: + type: string + parentPluginMessageKey: + $ref: '#/definitions/waCommon.MessageKey' + parentPluginType: + $ref: '#/definitions/waAICommon.BotPluginMetadata_PluginType' + pluginType: + $ref: '#/definitions/waAICommon.BotPluginMetadata_PluginType' + profilePhotoCDNURL: + type: string + provider: + $ref: '#/definitions/waAICommon.BotPluginMetadata_SearchProvider' + referenceIndex: + type: integer + searchProviderURL: + type: string + searchQuery: + type: string + thumbnailCDNURL: + type: string + type: object + waAICommon.BotPluginMetadata_PluginType: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - BotPluginMetadata_UNKNOWN_PLUGIN + - BotPluginMetadata_REELS + - BotPluginMetadata_SEARCH + waAICommon.BotPluginMetadata_SearchProvider: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - BotPluginMetadata_UNKNOWN + - BotPluginMetadata_BING + - BotPluginMetadata_GOOGLE + - BotPluginMetadata_SUPPORT + waAICommon.BotProgressIndicatorMetadata: + properties: + estimatedCompletionTime: + type: integer + progressDescription: + type: string + stepsMetadata: + items: + $ref: '#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata' + type: array + type: object + waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata: + properties: + isEnhancedSearch: + type: boolean + isReasoning: + type: boolean + sections: + items: + $ref: '#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningStepSectionMetadata' + type: array + sourcesMetadata: + items: + $ref: '#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata' + type: array + status: + $ref: '#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_PlanningStepStatus' + statusBody: + type: string + statusTitle: + type: string + type: object + waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourceMetadata: + properties: + favIconURL: + type: string + provider: + $ref: '#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotSearchSourceProvider' + sourceURL: + type: string + title: + type: string + type: object + waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata: + properties: + provider: + $ref: '#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_BotPlanningSearchSourceProvider' + sourceTitle: + type: string + sourceURL: + type: string + type: object + waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_BotPlanningSearchSourceProvider: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_UNKNOWN + - BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_OTHER + - BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_GOOGLE + - BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourcesMetadata_BING + waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningStepSectionMetadata: + properties: + sectionBody: + type: string + sectionTitle: + type: string + sourcesMetadata: + items: + $ref: '#/definitions/waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotPlanningSearchSourceMetadata' + type: array + type: object + waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_BotSearchSourceProvider: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - BotProgressIndicatorMetadata_BotPlanningStepMetadata_UNKNOWN_PROVIDER + - BotProgressIndicatorMetadata_BotPlanningStepMetadata_OTHER + - BotProgressIndicatorMetadata_BotPlanningStepMetadata_GOOGLE + - BotProgressIndicatorMetadata_BotPlanningStepMetadata_BING + waAICommon.BotProgressIndicatorMetadata_BotPlanningStepMetadata_PlanningStepStatus: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - BotProgressIndicatorMetadata_BotPlanningStepMetadata_UNKNOWN + - BotProgressIndicatorMetadata_BotPlanningStepMetadata_PLANNED + - BotProgressIndicatorMetadata_BotPlanningStepMetadata_EXECUTING + - BotProgressIndicatorMetadata_BotPlanningStepMetadata_FINISHED + waAICommon.BotPromotionMessageMetadata: + properties: + buttonTitle: + type: string + promotionType: + $ref: '#/definitions/waAICommon.BotPromotionMessageMetadata_BotPromotionType' + type: object + waAICommon.BotPromotionMessageMetadata_BotPromotionType: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - BotPromotionMessageMetadata_UNKNOWN_TYPE + - BotPromotionMessageMetadata_C50 + - BotPromotionMessageMetadata_SURVEY_PLATFORM + waAICommon.BotPromptSuggestion: + properties: + prompt: + type: string + promptID: + type: string + type: object + waAICommon.BotPromptSuggestions: + properties: + suggestions: + items: + $ref: '#/definitions/waAICommon.BotPromptSuggestion' + type: array + type: object + waAICommon.BotPttPromptMetadata: + properties: + transcript: + type: string + type: object + waAICommon.BotQuotaMetadata: + properties: + botFeatureQuotaMetadata: + items: + $ref: '#/definitions/waAICommon.BotQuotaMetadata_BotFeatureQuotaMetadata' + type: array + type: object + waAICommon.BotQuotaMetadata_BotFeatureQuotaMetadata: + properties: + expirationTimestamp: + type: integer + featureType: + $ref: '#/definitions/waAICommon.BotQuotaMetadata_BotFeatureQuotaMetadata_BotFeatureType' + remainingQuota: + type: integer + type: object + waAICommon.BotQuotaMetadata_BotFeatureQuotaMetadata_BotFeatureType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - BotQuotaMetadata_BotFeatureQuotaMetadata_UNKNOWN_FEATURE + - BotQuotaMetadata_BotFeatureQuotaMetadata_REASONING_FEATURE + waAICommon.BotReminderMetadata: + properties: + action: + $ref: '#/definitions/waAICommon.BotReminderMetadata_ReminderAction' + frequency: + $ref: '#/definitions/waAICommon.BotReminderMetadata_ReminderFrequency' + name: + type: string + nextTriggerTimestamp: + type: integer + requestMessageKey: + $ref: '#/definitions/waCommon.MessageKey' + type: object + waAICommon.BotReminderMetadata_ReminderAction: + enum: + - 1 + - 2 + - 3 + - 4 + format: int32 + type: integer + x-enum-varnames: + - BotReminderMetadata_NOTIFY + - BotReminderMetadata_CREATE + - BotReminderMetadata_DELETE + - BotReminderMetadata_UPDATE + waAICommon.BotReminderMetadata_ReminderFrequency: + enum: + - 1 + - 2 + - 3 + - 4 + - 5 + format: int32 + type: integer + x-enum-varnames: + - BotReminderMetadata_ONCE + - BotReminderMetadata_DAILY + - BotReminderMetadata_WEEKLY + - BotReminderMetadata_BIWEEKLY + - BotReminderMetadata_MONTHLY + waAICommon.BotRenderingConfigMetadata: + properties: + bloksVersioningID: + type: string + pixelDensity: + type: number + type: object + waAICommon.BotRenderingMetadata: + properties: + keywords: + items: + $ref: '#/definitions/waAICommon.BotRenderingMetadata_Keyword' + type: array + type: object + waAICommon.BotRenderingMetadata_Keyword: + properties: + associatedPrompts: + items: + type: string + type: array + value: + type: string + type: object + waAICommon.BotResolvedToolCallMetadata: + properties: + resolutionDataSerialized: + type: string + toolCallID: + type: string + type: object + waAICommon.BotSessionMetadata: + properties: + sessionID: + type: string + sessionSource: + $ref: '#/definitions/waAICommon.BotSessionSource' + type: object + waAICommon.BotSessionSource: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + format: int32 + type: integer + x-enum-varnames: + - BotSessionSource_NONE + - BotSessionSource_NULL_STATE + - BotSessionSource_TYPEAHEAD + - BotSessionSource_USER_INPUT + - BotSessionSource_EMU_FLASH + - BotSessionSource_EMU_FLASH_FOLLOWUP + - BotSessionSource_VOICE + - BotSessionSource_AI_HOME_SESSION + waAICommon.BotSignatureVerificationMetadata: + properties: + proofs: + items: + $ref: '#/definitions/waAICommon.BotSignatureVerificationUseCaseProof' + type: array + type: object + waAICommon.BotSignatureVerificationUseCaseProof: + properties: + certificateChain: + items: + items: + format: int32 + type: integer + type: array + type: array + signature: + items: + type: integer + type: array + useCase: + $ref: '#/definitions/waAICommon.BotSignatureVerificationUseCaseProof_BotSignatureUseCase' + version: + type: integer + type: object + waAICommon.BotSignatureVerificationUseCaseProof_BotSignatureUseCase: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - BotSignatureVerificationUseCaseProof_UNSPECIFIED + - BotSignatureVerificationUseCaseProof_WA_BOT_MSG + - BotSignatureVerificationUseCaseProof_WA_TEE_BOT_MSG + - BotSignatureVerificationUseCaseProof_P2P_PILLS + waAICommon.BotSourcesMetadata: + properties: + sources: + items: + $ref: '#/definitions/waAICommon.BotSourcesMetadata_BotSourceItem' + type: array + type: object + waAICommon.BotSourcesMetadata_BotSourceItem: + properties: + citationNumber: + type: integer + faviconCDNURL: + type: string + provider: + $ref: '#/definitions/waAICommon.BotSourcesMetadata_BotSourceItem_SourceProvider' + sourceProviderURL: + type: string + sourceQuery: + type: string + sourceTitle: + type: string + thumbnailCDNURL: + type: string + type: object + waAICommon.BotSourcesMetadata_BotSourceItem_SourceProvider: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + format: int32 + type: integer + x-enum-varnames: + - BotSourcesMetadata_BotSourceItem_UNKNOWN + - BotSourcesMetadata_BotSourceItem_BING + - BotSourcesMetadata_BotSourceItem_GOOGLE + - BotSourcesMetadata_BotSourceItem_SUPPORT + - BotSourcesMetadata_BotSourceItem_OTHER + waAICommon.BotSuggestedPromptMetadata: + properties: + promptSuggestions: + $ref: '#/definitions/waAICommon.BotPromptSuggestions' + selectedPromptID: + type: string + selectedPromptIndex: + type: integer + suggestedPrompts: + items: + type: string + type: array + type: object + waAICommon.BotUnifiedResponseMutation: + properties: + mediaDetailsMetadataList: + items: + $ref: '#/definitions/waAICommon.BotUnifiedResponseMutation_MediaDetailsMetadata' + type: array + sbsMetadata: + $ref: '#/definitions/waAICommon.BotUnifiedResponseMutation_SideBySideMetadata' + type: object + waAICommon.BotUnifiedResponseMutation_MediaDetailsMetadata: + properties: + ID: + type: string + highResMedia: + $ref: '#/definitions/waAICommon.BotMediaMetadata' + previewMedia: + $ref: '#/definitions/waAICommon.BotMediaMetadata' + type: object + waAICommon.BotUnifiedResponseMutation_SideBySideMetadata: + properties: + primaryResponseID: + type: string + surveyCtaHasRendered: + type: boolean + type: object + waAICommon.ForwardedAIBotMessageInfo: + properties: + botJID: + type: string + botName: + type: string + creatorName: + type: string + type: object + waAICommon.HatchMetadataSync: + properties: + data: + items: + type: integer + type: array + requestID: + type: string + timestampMS: + type: integer + type: object + waAICommon.InThreadSurveyMetadata: + properties: + feedbackToastText: + type: string + invitationBodyText: + type: string + invitationCtaText: + type: string + invitationCtaURL: + type: string + invitationHeaderText: + type: string + privacyStatementFull: + type: string + privacyStatementParts: + items: + $ref: '#/definitions/waAICommon.InThreadSurveyMetadata_InThreadSurveyPrivacyStatementPart' + type: array + questions: + items: + $ref: '#/definitions/waAICommon.InThreadSurveyMetadata_InThreadSurveyQuestion' + type: array + requestID: + type: string + simonSessionID: + type: string + simonSurveyID: + type: string + startQuestionIndex: + type: integer + surveyContinueButtonText: + type: string + surveySubmitButtonText: + type: string + surveyTitle: + type: string + tessaEvent: + type: string + tessaRootID: + type: string + tessaSessionID: + type: string + type: object + waAICommon.InThreadSurveyMetadata_InThreadSurveyOption: + properties: + numericValue: + type: integer + stringValue: + type: string + textTranslated: + type: string + type: object + waAICommon.InThreadSurveyMetadata_InThreadSurveyPrivacyStatementPart: + properties: + URL: + type: string + text: + type: string + type: object + waAICommon.InThreadSurveyMetadata_InThreadSurveyQuestion: + properties: + questionID: + type: string + questionOptions: + items: + $ref: '#/definitions/waAICommon.InThreadSurveyMetadata_InThreadSurveyOption' + type: array + questionText: + type: string + type: object + waAICommon.SessionTransparencyMetadata: + properties: + disclaimerText: + type: string + hcaID: + type: string + sessionTransparencyType: + $ref: '#/definitions/waAICommon.SessionTransparencyType' + type: object + waAICommon.SessionTransparencyType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - SessionTransparencyType_UNKNOWN_TYPE + - SessionTransparencyType_NY_AI_SAFETY_DISCLAIMER + waAICommonDeprecated.AIRichResponseCodeMetadata: + properties: + codeBlocks: + items: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseCodeMetadata_AIRichResponseCodeBlock' + type: array + codeLanguage: + type: string + type: object + waAICommonDeprecated.AIRichResponseCodeMetadata_AIRichResponseCodeBlock: + properties: + codeContent: + type: string + highlightType: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseCodeMetadata_AIRichResponseCodeHighlightType' + type: object + waAICommonDeprecated.AIRichResponseCodeMetadata_AIRichResponseCodeHighlightType: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + format: int32 + type: integer + x-enum-varnames: + - AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_DEFAULT + - AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_KEYWORD + - AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_METHOD + - AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_STRING + - AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_NUMBER + - AIRichResponseCodeMetadata_AI_RICH_RESPONSE_CODE_HIGHLIGHT_COMMENT + waAICommonDeprecated.AIRichResponseContentItemsMetadata: + properties: + contentType: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseContentItemsMetadata_ContentType' + itemsMetadata: + items: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseContentItemsMetadata_AIRichResponseContentItemMetadata' + type: array + type: object + waAICommonDeprecated.AIRichResponseContentItemsMetadata_AIRichResponseContentItemMetadata: + properties: + airichResponseContentItem: + description: "Types that are valid to be assigned to AIRichResponseContentItem:\n\n\t*AIRichResponseContentItemsMetadata_AIRichResponseContentItemMetadata_ReelItem" + type: object + waAICommonDeprecated.AIRichResponseContentItemsMetadata_ContentType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - AIRichResponseContentItemsMetadata_DEFAULT + - AIRichResponseContentItemsMetadata_CAROUSEL + waAICommonDeprecated.AIRichResponseDynamicMetadata: + properties: + URL: + type: string + loopCount: + type: integer + type: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseDynamicMetadata_AIRichResponseDynamicMetadataType' + version: + type: integer + type: object + waAICommonDeprecated.AIRichResponseDynamicMetadata_AIRichResponseDynamicMetadataType: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - AIRichResponseDynamicMetadata_AI_RICH_RESPONSE_DYNAMIC_METADATA_TYPE_UNKNOWN + - AIRichResponseDynamicMetadata_AI_RICH_RESPONSE_DYNAMIC_METADATA_TYPE_IMAGE + - AIRichResponseDynamicMetadata_AI_RICH_RESPONSE_DYNAMIC_METADATA_TYPE_GIF + waAICommonDeprecated.AIRichResponseGridImageMetadata: + properties: + gridImageURL: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseImageURL' + imageURLs: + items: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseImageURL' + type: array + type: object + waAICommonDeprecated.AIRichResponseImageURL: + properties: + imageHighResURL: + type: string + imagePreviewURL: + type: string + sourceURL: + type: string + type: object + waAICommonDeprecated.AIRichResponseInlineImageMetadata: + properties: + alignment: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseInlineImageMetadata_AIRichResponseImageAlignment' + imageText: + type: string + imageURL: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseImageURL' + tapLinkURL: + type: string + type: object + waAICommonDeprecated.AIRichResponseInlineImageMetadata_AIRichResponseImageAlignment: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - AIRichResponseInlineImageMetadata_AI_RICH_RESPONSE_IMAGE_LAYOUT_LEADING_ALIGNED + - AIRichResponseInlineImageMetadata_AI_RICH_RESPONSE_IMAGE_LAYOUT_TRAILING_ALIGNED + - AIRichResponseInlineImageMetadata_AI_RICH_RESPONSE_IMAGE_LAYOUT_CENTER_ALIGNED + waAICommonDeprecated.AIRichResponseLatexMetadata: + properties: + expressions: + items: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseLatexMetadata_AIRichResponseLatexExpression' + type: array + text: + type: string + type: object + waAICommonDeprecated.AIRichResponseLatexMetadata_AIRichResponseLatexExpression: + properties: + URL: + type: string + fontHeight: + type: number + height: + type: number + imageBottomPadding: + type: number + imageLeadingPadding: + type: number + imageTopPadding: + type: number + imageTrailingPadding: + type: number + latexExpression: + type: string + width: + type: number + type: object + waAICommonDeprecated.AIRichResponseMapMetadata: + properties: + annotations: + items: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseMapMetadata_AIRichResponseMapAnnotation' + type: array + centerLatitude: + type: number + centerLongitude: + type: number + latitudeDelta: + type: number + longitudeDelta: + type: number + showInfoList: + type: boolean + type: object + waAICommonDeprecated.AIRichResponseMapMetadata_AIRichResponseMapAnnotation: + properties: + annotationNumber: + type: integer + body: + type: string + latitude: + type: number + longitude: + type: number + title: + type: string + type: object + waAICommonDeprecated.AIRichResponseMessageType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - AIRichResponseMessageType_AI_RICH_RESPONSE_TYPE_UNKNOWN + - AIRichResponseMessageType_AI_RICH_RESPONSE_TYPE_STANDARD + waAICommonDeprecated.AIRichResponseSubMessage: + properties: + codeMetadata: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseCodeMetadata' + contentItemsMetadata: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseContentItemsMetadata' + dynamicMetadata: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseDynamicMetadata' + gridImageMetadata: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseGridImageMetadata' + imageMetadata: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseInlineImageMetadata' + latexMetadata: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseLatexMetadata' + mapMetadata: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseMapMetadata' + messageText: + type: string + messageType: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseSubMessageType' + tableMetadata: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseTableMetadata' + type: object + waAICommonDeprecated.AIRichResponseSubMessageType: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + format: int32 + type: integer + x-enum-varnames: + - AIRichResponseSubMessageType_AI_RICH_RESPONSE_UNKNOWN + - AIRichResponseSubMessageType_AI_RICH_RESPONSE_GRID_IMAGE + - AIRichResponseSubMessageType_AI_RICH_RESPONSE_TEXT + - AIRichResponseSubMessageType_AI_RICH_RESPONSE_INLINE_IMAGE + - AIRichResponseSubMessageType_AI_RICH_RESPONSE_TABLE + - AIRichResponseSubMessageType_AI_RICH_RESPONSE_CODE + - AIRichResponseSubMessageType_AI_RICH_RESPONSE_DYNAMIC + - AIRichResponseSubMessageType_AI_RICH_RESPONSE_MAP + - AIRichResponseSubMessageType_AI_RICH_RESPONSE_LATEX + - AIRichResponseSubMessageType_AI_RICH_RESPONSE_CONTENT_ITEMS + waAICommonDeprecated.AIRichResponseTableMetadata: + properties: + rows: + items: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseTableMetadata_AIRichResponseTableRow' + type: array + title: + type: string + type: object + waAICommonDeprecated.AIRichResponseTableMetadata_AIRichResponseTableRow: + properties: + isHeading: + type: boolean + items: + items: + type: string + type: array + type: object + waAdv.ADVEncryptionType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - ADVEncryptionType_E2EE + - ADVEncryptionType_HOSTED + waCommon.LimitSharing: + properties: + initiatedByMe: + type: boolean + limitSharingSettingTimestamp: + type: integer + sharingLimited: + type: boolean + trigger: + $ref: '#/definitions/waCommon.LimitSharing_Trigger' + type: object + waCommon.LimitSharing_Trigger: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - LimitSharing_UNKNOWN + - LimitSharing_CHAT_SETTING + - LimitSharing_BIZ_SUPPORTS_FB_HOSTING + - LimitSharing_UNKNOWN_GROUP + waCommon.MessageKey: + properties: + ID: + type: string + fromMe: + type: boolean + participant: + type: string + remoteJID: + type: string + type: object + waCompanionReg.DeviceProps_HistorySyncConfig: + properties: + completeOnDemandReady: + type: boolean + fullSyncDaysLimit: + type: integer + fullSyncSizeMbLimit: + type: integer + initialSyncMaxMessagesPerChat: + type: integer + inlineInitialPayloadInE2EeMsg: + type: boolean + onDemandReady: + type: boolean + recentSyncDaysLimit: + type: integer + storageQuotaMb: + type: integer + supportAddOnHistorySyncMigration: + type: boolean + supportBizHostedMsg: + type: boolean + supportBotUserAgentChatHistory: + type: boolean + supportCagReactionsAndPolls: + type: boolean + supportCallLogHistory: + type: boolean + supportFbidBotChatHistory: + type: boolean + supportGroupHistory: + type: boolean + supportGuestChat: + type: boolean + supportHatchHistory: + type: boolean + supportHostedGroupMsg: + type: boolean + supportInlineContacts: + type: boolean + supportManusHistory: + type: boolean + supportMessageAssociation: + type: boolean + supportRecentSyncChunkMessageCountTuning: + type: boolean + supportedBotChannelFbids: + items: + type: string + type: array + thumbnailSyncDaysLimit: + type: integer + type: object + waE2E.AIQueryFanout: + properties: + message: + $ref: '#/definitions/waE2E.Message' + messageKey: + $ref: '#/definitions/waCommon.MessageKey' + timestamp: + type: integer + type: object + waE2E.AIRichResponseMessage: + properties: + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + messageType: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseMessageType' + submessages: + items: + $ref: '#/definitions/waAICommonDeprecated.AIRichResponseSubMessage' + type: array + unifiedResponse: + $ref: '#/definitions/waAICommon.AIRichResponseUnifiedResponse' + type: object + waE2E.ActionLink: + properties: + URL: + type: string + buttonTitle: + type: string + type: object + waE2E.AlbumMessage: + properties: + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + expectedImageCount: + type: integer + expectedVideoCount: + type: integer + type: object + waE2E.AppStateFatalExceptionNotification: + properties: + collectionNames: + items: + type: string + type: array + timestamp: + type: integer + type: object + waE2E.AppStateSyncKey: + properties: + keyData: + $ref: '#/definitions/waE2E.AppStateSyncKeyData' + keyID: + $ref: '#/definitions/waE2E.AppStateSyncKeyId' + type: object + waE2E.AppStateSyncKeyData: + properties: + fingerprint: + $ref: '#/definitions/waE2E.AppStateSyncKeyFingerprint' + keyData: + items: + type: integer + type: array + timestamp: + type: integer + type: object + waE2E.AppStateSyncKeyFingerprint: + properties: + currentIndex: + type: integer + deviceIndexes: + items: + type: integer + type: array + rawID: + type: integer + type: object + waE2E.AppStateSyncKeyId: + properties: + keyID: + items: + type: integer + type: array + type: object + waE2E.AppStateSyncKeyRequest: + properties: + keyIDs: + items: + $ref: '#/definitions/waE2E.AppStateSyncKeyId' + type: array + type: object + waE2E.AppStateSyncKeyShare: + properties: + keys: + items: + $ref: '#/definitions/waE2E.AppStateSyncKey' + type: array + type: object + waE2E.AudioMessage: + properties: + PTT: + type: boolean + URL: + type: string + accessibilityLabel: + type: string + backgroundArgb: + type: integer + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + directPath: + type: string + fileEncSHA256: + items: + type: integer + type: array + fileLength: + type: integer + fileSHA256: + items: + type: integer + type: array + mediaKey: + items: + type: integer + type: array + mediaKeyTimestamp: + type: integer + mimetype: + type: string + seconds: + type: integer + streamingSidecar: + items: + type: integer + type: array + viewOnce: + type: boolean + waveform: + items: + type: integer + type: array + type: object + waE2E.BCallMessage: + properties: + caption: + type: string + masterKey: + items: + type: integer + type: array + mediaType: + $ref: '#/definitions/waE2E.BCallMessage_MediaType' + sessionID: + type: string + type: object + waE2E.BCallMessage_MediaType: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - BCallMessage_UNKNOWN + - BCallMessage_AUDIO + - BCallMessage_VIDEO + waE2E.ButtonsMessage: + properties: + buttons: + items: + $ref: '#/definitions/waE2E.ButtonsMessage_Button' + type: array + contentText: + type: string + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + footerText: + type: string + header: + description: "Types that are valid to be assigned to Header:\n\n\t*ButtonsMessage_Text\n\t*ButtonsMessage_DocumentMessage\n\t*ButtonsMessage_ImageMessage\n\t*ButtonsMessage_VideoMessage\n\t*ButtonsMessage_LocationMessage" + headerType: + $ref: '#/definitions/waE2E.ButtonsMessage_HeaderType' + type: object + waE2E.ButtonsMessage_Button: + properties: + buttonID: + type: string + buttonText: + $ref: '#/definitions/waE2E.ButtonsMessage_Button_ButtonText' + nativeFlowInfo: + $ref: '#/definitions/waE2E.ButtonsMessage_Button_NativeFlowInfo' + type: + $ref: '#/definitions/waE2E.ButtonsMessage_Button_Type' + type: object + waE2E.ButtonsMessage_Button_ButtonText: + properties: + displayText: + type: string + type: object + waE2E.ButtonsMessage_Button_NativeFlowInfo: + properties: + name: + type: string + paramsJSON: + type: string + type: object + waE2E.ButtonsMessage_Button_Type: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - ButtonsMessage_Button_UNKNOWN + - ButtonsMessage_Button_RESPONSE + - ButtonsMessage_Button_NATIVE_FLOW + waE2E.ButtonsMessage_HeaderType: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + format: int32 + type: integer + x-enum-varnames: + - ButtonsMessage_UNKNOWN + - ButtonsMessage_EMPTY + - ButtonsMessage_TEXT + - ButtonsMessage_DOCUMENT + - ButtonsMessage_IMAGE + - ButtonsMessage_VIDEO + - ButtonsMessage_LOCATION + waE2E.ButtonsResponseMessage: + properties: + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + response: + description: "Types that are valid to be assigned to Response:\n\n\t*ButtonsResponseMessage_SelectedDisplayText" + selectedButtonID: + type: string + type: + $ref: '#/definitions/waE2E.ButtonsResponseMessage_Type' + type: object + waE2E.ButtonsResponseMessage_Type: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - ButtonsResponseMessage_UNKNOWN + - ButtonsResponseMessage_DISPLAY_TEXT + waE2E.Call: + properties: + callEntryPoint: + type: integer + callKey: + items: + type: integer + type: array + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + conversionData: + items: + type: integer + type: array + conversionDelaySeconds: + type: integer + conversionSource: + type: string + ctwaPayload: + items: + type: integer + type: array + ctwaSignals: + type: string + deeplinkPayload: + type: string + messageContextInfo: + $ref: '#/definitions/waE2E.MessageContextInfo' + nativeFlowCallButtonPayload: + type: string + type: object + waE2E.CallLogMessage: + properties: + callOutcome: + $ref: '#/definitions/waE2E.CallLogMessage_CallOutcome' + callType: + $ref: '#/definitions/waE2E.CallLogMessage_CallType' + durationSecs: + type: integer + isVideo: + type: boolean + participants: + items: + $ref: '#/definitions/waE2E.CallLogMessage_CallParticipant' + type: array + type: object + waE2E.CallLogMessage_CallOutcome: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + format: int32 + type: integer + x-enum-varnames: + - CallLogMessage_CONNECTED + - CallLogMessage_MISSED + - CallLogMessage_FAILED + - CallLogMessage_REJECTED + - CallLogMessage_ACCEPTED_ELSEWHERE + - CallLogMessage_ONGOING + - CallLogMessage_SILENCED_BY_DND + - CallLogMessage_SILENCED_UNKNOWN_CALLER + waE2E.CallLogMessage_CallParticipant: + properties: + JID: + type: string + callOutcome: + $ref: '#/definitions/waE2E.CallLogMessage_CallOutcome' + type: object + waE2E.CallLogMessage_CallType: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - CallLogMessage_REGULAR + - CallLogMessage_SCHEDULED_CALL + - CallLogMessage_VOICE_CHAT + waE2E.CancelPaymentRequestMessage: + properties: + key: + $ref: '#/definitions/waCommon.MessageKey' + type: object + waE2E.Chat: + properties: + ID: + type: string + displayName: + type: string + type: object + waE2E.ChatThemeSetting: + properties: + clearTheme: + type: boolean + colorSchemeID: + type: string + settingTimestampMS: + type: integer + wallpaper: + description: "Types that are valid to be assigned to Wallpaper:\n\n\t*ChatThemeSetting_DefaultWallpaper\n\t*ChatThemeSetting_SolidColor\n\t*ChatThemeSetting_StockImage\n\t*ChatThemeSetting_CustomImage" + type: object + waE2E.CloudAPIThreadControlNotification: + properties: + consumerLid: + type: string + consumerPhoneNumber: + type: string + notificationContent: + $ref: '#/definitions/waE2E.CloudAPIThreadControlNotification_CloudAPIThreadControlNotificationContent' + senderNotificationTimestampMS: + type: integer + shouldSuppressNotification: + type: boolean + status: + $ref: '#/definitions/waE2E.CloudAPIThreadControlNotification_CloudAPIThreadControl' + type: object + waE2E.CloudAPIThreadControlNotification_CloudAPIThreadControl: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - CloudAPIThreadControlNotification_UNKNOWN + - CloudAPIThreadControlNotification_CONTROL_PASSED + - CloudAPIThreadControlNotification_CONTROL_TAKEN + - CloudAPIThreadControlNotification_INFO + waE2E.CloudAPIThreadControlNotification_CloudAPIThreadControlNotificationContent: + properties: + extraJSON: + type: string + handoffNotificationText: + type: string + type: object + waE2E.CommentMessage: + properties: + message: + $ref: '#/definitions/waE2E.Message' + targetMessageKey: + $ref: '#/definitions/waCommon.MessageKey' + type: object + waE2E.ConditionalRevealMessage: + properties: + conditionalRevealMessageType: + $ref: '#/definitions/waE2E.ConditionalRevealMessage_ConditionalRevealMessageType' + encIV: + items: + type: integer + type: array + encPayload: + items: + type: integer + type: array + revealKeyID: + type: string + type: object + waE2E.ConditionalRevealMessage_ConditionalRevealMessageType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - ConditionalRevealMessage_UNKNOWN + - ConditionalRevealMessage_SCHEDULED_MESSAGE + waE2E.ContactMessage: + properties: + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + displayName: + type: string + isSelfContact: + type: boolean + vcard: + type: string + type: object + waE2E.ContactsArrayMessage: + properties: + contacts: + items: + $ref: '#/definitions/waE2E.ContactMessage' + type: array + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + displayName: + type: string + type: object + waE2E.ContextInfo: + properties: + actionLink: + $ref: '#/definitions/waE2E.ActionLink' + afterReadDuration: + type: integer + alwaysShowAdAttribution: + type: boolean + botMessageSharingInfo: + $ref: '#/definitions/waAICommon.BotMessageSharingInfo' + businessInteractionPills: + $ref: '#/definitions/waE2E.ContextInfo_BusinessInteractionPills' + businessMessageForwardInfo: + $ref: '#/definitions/waE2E.ContextInfo_BusinessMessageForwardInfo' + conversionData: + items: + type: integer + type: array + conversionDelaySeconds: + type: integer + conversionSource: + type: string + crossAppSource: + $ref: '#/definitions/waE2E.ContextInfo_CrossAppSource' + ctwaPayload: + items: + type: integer + type: array + ctwaSignals: + type: string + dataSharingContext: + $ref: '#/definitions/waE2E.ContextInfo_DataSharingContext' + disappearingMode: + $ref: '#/definitions/waE2E.DisappearingMode' + entryPointConversionApp: + type: string + entryPointConversionDelaySeconds: + type: integer + entryPointConversionExternalMedium: + type: string + entryPointConversionExternalSource: + type: string + entryPointConversionSource: + type: string + ephemeralSettingTimestamp: + type: integer + ephemeralSharedSecret: + items: + type: integer + type: array + expiration: + type: integer + externalAdReply: + $ref: '#/definitions/waE2E.ContextInfo_ExternalAdReplyInfo' + featureEligibilities: + $ref: '#/definitions/waE2E.ContextInfo_FeatureEligibilities' + forwardOrigin: + $ref: '#/definitions/waE2E.ContextInfo_ForwardOrigin' + forwardedAiBotMessageInfo: + $ref: '#/definitions/waAICommon.ForwardedAIBotMessageInfo' + forwardedNewsletterMessageInfo: + $ref: '#/definitions/waE2E.ContextInfo_ForwardedNewsletterMessageInfo' + forwardingScore: + type: integer + groupMentions: + items: + $ref: '#/definitions/waE2E.GroupMention' + type: array + groupSubject: + type: string + isForwarded: + type: boolean + isGroupStatus: + type: boolean + isQuestion: + type: boolean + isSampled: + type: boolean + isSpoiler: + type: boolean + mediaDomainInfo: + $ref: '#/definitions/waE2E.MediaDomainInfo' + memberLabel: + $ref: '#/definitions/waE2E.MemberLabel' + mentionedJID: + items: + type: string + type: array + nonJIDMentions: + type: integer + pairedMediaType: + $ref: '#/definitions/waE2E.ContextInfo_PairedMediaType' + parentGroupJID: + type: string + partiallySelectedContent: + $ref: '#/definitions/waE2E.ContextInfo_PartiallySelectedContent' + participant: + type: string + placeholderKey: + $ref: '#/definitions/waCommon.MessageKey' + posterStatusID: + type: string + questionReplyQuotedMessage: + $ref: '#/definitions/waE2E.ContextInfo_QuestionReplyQuotedMessage' + quotedAd: + $ref: '#/definitions/waE2E.ContextInfo_AdReplyInfo' + quotedMessage: + $ref: '#/definitions/waE2E.Message' + quotedType: + $ref: '#/definitions/waE2E.ContextInfo_QuotedType' + rankingVersion: + type: integer + remoteJID: + type: string + smbClientCampaignID: + type: string + smbServerCampaignID: + type: string + stanzaID: + type: string + statusAttributionType: + $ref: '#/definitions/waE2E.ContextInfo_StatusAttributionType' + statusAttributions: + items: + $ref: '#/definitions/waStatusAttributions.StatusAttribution' + type: array + statusAudienceMetadata: + $ref: '#/definitions/waE2E.ContextInfo_StatusAudienceMetadata' + statusSourceType: + $ref: '#/definitions/waE2E.ContextInfo_StatusSourceType' + trustBannerAction: + type: integer + trustBannerType: + type: string + urlTrackingMap: + $ref: '#/definitions/waE2E.UrlTrackingMap' + utm: + $ref: '#/definitions/waE2E.ContextInfo_UTMInfo' + type: object + waE2E.ContextInfo_AdReplyInfo: + properties: + JPEGThumbnail: + items: + type: integer + type: array + advertiserName: + type: string + caption: + type: string + mediaType: + $ref: '#/definitions/waE2E.ContextInfo_AdReplyInfo_MediaType' + type: object + waE2E.ContextInfo_AdReplyInfo_MediaType: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - ContextInfo_AdReplyInfo_NONE + - ContextInfo_AdReplyInfo_IMAGE + - ContextInfo_AdReplyInfo_VIDEO + waE2E.ContextInfo_BusinessInteractionPills: + properties: + businessJID: + type: string + entryPoint: + $ref: '#/definitions/waE2E.ContextInfo_BusinessInteractionPills_EntryPoint' + pills: + items: + $ref: '#/definitions/waE2E.ContextInfo_BusinessInteractionPills_Pill' + type: array + signatureEnvelope: + $ref: '#/definitions/waAICommon.BotSignatureVerificationMetadata' + signedPayload: + items: + type: integer + type: array + type: object + waE2E.ContextInfo_BusinessInteractionPills_EntryPoint: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + format: int32 + type: integer + x-enum-varnames: + - ContextInfo_BusinessInteractionPills_ENTRY_POINT_UNKNOWN + - ContextInfo_BusinessInteractionPills_P2P_LINK_SHARE + - ContextInfo_BusinessInteractionPills_CONTACT_CARD_SHARING + - ContextInfo_BusinessInteractionPills_PHONE_NUMBER + - ContextInfo_BusinessInteractionPills_STATUS + - ContextInfo_BusinessInteractionPills_IN_THREAD_CONTEXT_CARD + waE2E.ContextInfo_BusinessInteractionPills_Pill: + properties: + actionURL: + type: string + pillType: + $ref: '#/definitions/waE2E.ContextInfo_BusinessInteractionPills_PillType' + type: object + waE2E.ContextInfo_BusinessInteractionPills_PillType: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + format: int32 + type: integer + x-enum-varnames: + - ContextInfo_BusinessInteractionPills_UNKNOWN + - ContextInfo_BusinessInteractionPills_VIEW_BUSINESS + - ContextInfo_BusinessInteractionPills_CHAT + - ContextInfo_BusinessInteractionPills_CALL + - ContextInfo_BusinessInteractionPills_CATALOG + - ContextInfo_BusinessInteractionPills_CHANNEL + - ContextInfo_BusinessInteractionPills_BOOK_APPOINTMENT + - ContextInfo_BusinessInteractionPills_OFFERS + - ContextInfo_BusinessInteractionPills_BESTSELLERS + - ContextInfo_BusinessInteractionPills_MENU + - ContextInfo_BusinessInteractionPills_ABOUT + - ContextInfo_BusinessInteractionPills_SHOP + - ContextInfo_BusinessInteractionPills_ORDER + waE2E.ContextInfo_BusinessMessageForwardInfo: + properties: + businessOwnerJID: + type: string + type: object + waE2E.ContextInfo_CrossAppSource: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - ContextInfo_CROSS_APP_SOURCE_UNKNOWN + - ContextInfo_CROSS_APP_SOURCE_INSTAGRAM + - ContextInfo_CROSS_APP_SOURCE_FACEBOOK + waE2E.ContextInfo_DataSharingContext: + properties: + dataSharingFlags: + type: integer + encryptedSignalTokenConsented: + type: string + parameters: + items: + $ref: '#/definitions/waE2E.ContextInfo_DataSharingContext_Parameters' + type: array + showMmDisclosure: + type: boolean + type: object + waE2E.ContextInfo_DataSharingContext_Parameters: + properties: + contents: + $ref: '#/definitions/waE2E.ContextInfo_DataSharingContext_Parameters' + floatData: + type: number + intData: + type: integer + key: + type: string + stringData: + type: string + type: object + waE2E.ContextInfo_ExternalAdReplyInfo: + properties: + adContextPreviewDismissed: + type: boolean + adPreviewURL: + type: string + adType: + $ref: '#/definitions/waE2E.ContextInfo_ExternalAdReplyInfo_AdType' + agmHeaderInteractionStrategy: + type: integer + agmSubtitleStrategy: + type: integer + agmThumbnailStrategy: + type: integer + agmTitleStrategy: + type: integer + automatedGreetingMessageCtaType: + type: string + automatedGreetingMessageShown: + type: boolean + body: + type: string + clickToWhatsappCall: + type: boolean + containsAutoReply: + type: boolean + containsCtwaFlowsAutoReply: + type: boolean + ctaPayload: + type: string + ctwaClid: + type: string + disableNudge: + type: boolean + greetingMessageBody: + type: string + mediaType: + $ref: '#/definitions/waE2E.ContextInfo_ExternalAdReplyInfo_MediaType' + mediaURL: + type: string + originalImageURL: + type: string + ref: + type: string + renderLargerThumbnail: + type: boolean + showAdAttribution: + type: boolean + sourceApp: + type: string + sourceID: + type: string + sourceType: + type: string + sourceURL: + type: string + thumbnail: + items: + type: integer + type: array + thumbnailURL: + type: string + title: + type: string + wtwaAdFormat: + type: boolean + wtwaWebsiteURL: + type: string + type: object + waE2E.ContextInfo_ExternalAdReplyInfo_AdType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - ContextInfo_ExternalAdReplyInfo_CTWA + - ContextInfo_ExternalAdReplyInfo_CAWC + waE2E.ContextInfo_ExternalAdReplyInfo_MediaType: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - ContextInfo_ExternalAdReplyInfo_NONE + - ContextInfo_ExternalAdReplyInfo_IMAGE + - ContextInfo_ExternalAdReplyInfo_VIDEO + waE2E.ContextInfo_FeatureEligibilities: + properties: + canBeReshared: + type: boolean + canReceiveMultiReact: + type: boolean + canRequestFeedback: + type: boolean + cannotBeRanked: + type: boolean + cannotBeReactedTo: + type: boolean + type: object + waE2E.ContextInfo_ForwardOrigin: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + format: int32 + type: integer + x-enum-varnames: + - ContextInfo_UNKNOWN + - ContextInfo_CHAT + - ContextInfo_STATUS + - ContextInfo_CHANNELS + - ContextInfo_META_AI + - ContextInfo_UGC + waE2E.ContextInfo_ForwardedNewsletterMessageInfo: + properties: + accessibilityText: + type: string + contentType: + $ref: '#/definitions/waE2E.ContextInfo_ForwardedNewsletterMessageInfo_ContentType' + newsletterJID: + type: string + newsletterName: + type: string + profileName: + type: string + serverMessageID: + type: integer + type: object + waE2E.ContextInfo_ForwardedNewsletterMessageInfo_ContentType: + enum: + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - ContextInfo_ForwardedNewsletterMessageInfo_UPDATE + - ContextInfo_ForwardedNewsletterMessageInfo_UPDATE_CARD + - ContextInfo_ForwardedNewsletterMessageInfo_LINK_CARD + waE2E.ContextInfo_PairedMediaType: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + format: int32 + type: integer + x-enum-varnames: + - ContextInfo_NOT_PAIRED_MEDIA + - ContextInfo_SD_VIDEO_PARENT + - ContextInfo_HD_VIDEO_CHILD + - ContextInfo_SD_IMAGE_PARENT + - ContextInfo_HD_IMAGE_CHILD + - ContextInfo_MOTION_PHOTO_PARENT + - ContextInfo_MOTION_PHOTO_CHILD + - ContextInfo_HEVC_VIDEO_PARENT + - ContextInfo_HEVC_VIDEO_CHILD + waE2E.ContextInfo_PartiallySelectedContent: + properties: + text: + type: string + type: object + waE2E.ContextInfo_QuestionReplyQuotedMessage: + properties: + quotedQuestion: + $ref: '#/definitions/waE2E.Message' + quotedResponse: + $ref: '#/definitions/waE2E.Message' + serverQuestionID: + type: integer + type: object + waE2E.ContextInfo_QuotedType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - ContextInfo_EXPLICIT + - ContextInfo_AUTO + waE2E.ContextInfo_StatusAttributionType: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + format: int32 + type: integer + x-enum-varnames: + - ContextInfo_NONE + - ContextInfo_RESHARED_FROM_MENTION + - ContextInfo_RESHARED_FROM_POST + - ContextInfo_RESHARED_FROM_POST_MANY_TIMES + - ContextInfo_FORWARDED_FROM_STATUS + waE2E.ContextInfo_StatusAudienceMetadata: + properties: + audienceType: + $ref: '#/definitions/waE2E.ContextInfo_StatusAudienceMetadata_AudienceType' + listEmoji: + type: string + listName: + type: string + type: object + waE2E.ContextInfo_StatusAudienceMetadata_AudienceType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - ContextInfo_StatusAudienceMetadata_UNKNOWN + - ContextInfo_StatusAudienceMetadata_CLOSE_FRIENDS + waE2E.ContextInfo_StatusSourceType: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + format: int32 + type: integer + x-enum-varnames: + - ContextInfo_IMAGE + - ContextInfo_VIDEO + - ContextInfo_GIF + - ContextInfo_AUDIO + - ContextInfo_TEXT + - ContextInfo_MUSIC_STANDALONE + waE2E.ContextInfo_UTMInfo: + properties: + utmCampaign: + type: string + utmSource: + type: string + type: object + waE2E.DeclinePaymentRequestMessage: + properties: + key: + $ref: '#/definitions/waCommon.MessageKey' + type: object + waE2E.DeviceListMetadata: + properties: + receiverAccountType: + $ref: '#/definitions/waAdv.ADVEncryptionType' + recipientKeyHash: + items: + type: integer + type: array + recipientKeyIndexes: + items: + type: integer + type: array + recipientTimestamp: + type: integer + senderAccountType: + $ref: '#/definitions/waAdv.ADVEncryptionType' + senderKeyHash: + items: + type: integer + type: array + senderKeyIndexes: + items: + type: integer + type: array + senderTimestamp: + type: integer + type: object + waE2E.DeviceSentMessage: + properties: + destinationJID: + type: string + message: + $ref: '#/definitions/waE2E.Message' + phash: + type: string + type: object + waE2E.DisappearingMode: + properties: + initiatedByMe: + type: boolean + initiator: + $ref: '#/definitions/waE2E.DisappearingMode_Initiator' + initiatorDeviceJID: + type: string + trigger: + $ref: '#/definitions/waE2E.DisappearingMode_Trigger' + type: object + waE2E.DisappearingMode_Initiator: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - DisappearingMode_CHANGED_IN_CHAT + - DisappearingMode_INITIATED_BY_ME + - DisappearingMode_INITIATED_BY_OTHER + - DisappearingMode_BIZ_UPGRADE_FB_HOSTING + waE2E.DisappearingMode_Trigger: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + format: int32 + type: integer + x-enum-varnames: + - DisappearingMode_UNKNOWN + - DisappearingMode_CHAT_SETTING + - DisappearingMode_ACCOUNT_SETTING + - DisappearingMode_BULK_CHANGE + - DisappearingMode_BIZ_SUPPORTS_FB_HOSTING + - DisappearingMode_UNKNOWN_GROUPS + waE2E.DocumentMessage: + properties: + JPEGThumbnail: + items: + type: integer + type: array + URL: + type: string + accessibilityLabel: + type: string + caption: + type: string + contactVcard: + type: boolean + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + directPath: + type: string + fileEncSHA256: + items: + type: integer + type: array + fileLength: + type: integer + fileName: + type: string + fileSHA256: + items: + type: integer + type: array + mediaKey: + items: + type: integer + type: array + mediaKeyTimestamp: + type: integer + mimetype: + type: string + pageCount: + type: integer + thumbnailDirectPath: + type: string + thumbnailEncSHA256: + items: + type: integer + type: array + thumbnailHeight: + type: integer + thumbnailSHA256: + items: + type: integer + type: array + thumbnailWidth: + type: integer + title: + type: string + type: object + waE2E.EmbeddedContent: + properties: + content: + description: "Types that are valid to be assigned to Content:\n\n\t*EmbeddedContent_EmbeddedMessage\n\t*EmbeddedContent_EmbeddedMusic" + type: object + waE2E.EmbeddedMusic: + properties: + artistAttribution: + type: string + artworkDirectPath: + type: string + artworkEncSHA256: + items: + type: integer + type: array + artworkMediaKey: + items: + type: integer + type: array + artworkSHA256: + items: + type: integer + type: array + author: + type: string + countryBlocklist: + items: + type: integer + type: array + derivedContentStartTimeInMS: + type: integer + isExplicit: + type: boolean + musicContentMediaID: + type: string + musicSongStartTimeInMS: + type: integer + overlapDurationInMS: + type: integer + songID: + type: string + title: + type: string + type: object + waE2E.EncCommentMessage: + properties: + encIV: + items: + type: integer + type: array + encPayload: + items: + type: integer + type: array + targetMessageKey: + $ref: '#/definitions/waCommon.MessageKey' + type: object + waE2E.EncEventResponseMessage: + properties: + encIV: + items: + type: integer + type: array + encPayload: + items: + type: integer + type: array + eventCreationMessageKey: + $ref: '#/definitions/waCommon.MessageKey' + type: object + waE2E.EncReactionMessage: + properties: + encIV: + items: + type: integer + type: array + encPayload: + items: + type: integer + type: array + targetMessageKey: + $ref: '#/definitions/waCommon.MessageKey' + type: object + waE2E.EventInviteMessage: + properties: + JPEGThumbnail: + items: + type: integer + type: array + callLink: + type: string + caption: + type: string + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + endTime: + type: integer + eventID: + type: string + eventTitle: + type: string + isCanceled: + type: boolean + startTime: + type: integer + type: object + waE2E.EventMessage: + properties: + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + description: + type: string + endTime: + type: integer + extraGuestsAllowed: + type: boolean + hasReminder: + type: boolean + isCanceled: + type: boolean + isScheduleCall: + type: boolean + joinLink: + type: string + location: + $ref: '#/definitions/waE2E.LocationMessage' + name: + type: string + reminderOffsetSec: + type: integer + startTime: + type: integer + type: object + waE2E.ExtendedTextMessage: + properties: + JPEGThumbnail: + items: + type: integer + type: array + backgroundArgb: + type: integer + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + description: + type: string + doNotPlayInline: + type: boolean + endCardTiles: + items: + $ref: '#/definitions/waE2E.VideoEndCard' + type: array + faviconMMSMetadata: + $ref: '#/definitions/waE2E.MMSThumbnailMetadata' + font: + $ref: '#/definitions/waE2E.ExtendedTextMessage_FontType' + inviteLinkGroupType: + $ref: '#/definitions/waE2E.ExtendedTextMessage_InviteLinkGroupType' + inviteLinkGroupTypeV2: + $ref: '#/definitions/waE2E.ExtendedTextMessage_InviteLinkGroupType' + inviteLinkParentGroupSubjectV2: + type: string + inviteLinkParentGroupThumbnailV2: + items: + type: integer + type: array + linkPreviewMetadata: + $ref: '#/definitions/waE2E.LinkPreviewMetadata' + matchedText: + type: string + mediaKey: + items: + type: integer + type: array + mediaKeyTimestamp: + type: integer + musicMetadata: + $ref: '#/definitions/waE2E.EmbeddedMusic' + paymentExtendedMetadata: + $ref: '#/definitions/waE2E.PaymentExtendedMetadata' + paymentLinkMetadata: + $ref: '#/definitions/waE2E.PaymentLinkMetadata' + previewType: + $ref: '#/definitions/waE2E.ExtendedTextMessage_PreviewType' + text: + type: string + textArgb: + type: integer + thumbnailDirectPath: + type: string + thumbnailEncSHA256: + items: + type: integer + type: array + thumbnailHeight: + type: integer + thumbnailSHA256: + items: + type: integer + type: array + thumbnailWidth: + type: integer + title: + type: string + videoContentURL: + type: string + videoHeight: + type: integer + videoWidth: + type: integer + viewOnce: + type: boolean + type: object + waE2E.ExtendedTextMessage_FontType: + enum: + - 0 + - 1 + - 2 + - 6 + - 7 + - 8 + - 9 + - 10 + format: int32 + type: integer + x-enum-varnames: + - ExtendedTextMessage_SYSTEM + - ExtendedTextMessage_SYSTEM_TEXT + - ExtendedTextMessage_FB_SCRIPT + - ExtendedTextMessage_SYSTEM_BOLD + - ExtendedTextMessage_MORNINGBREEZE_REGULAR + - ExtendedTextMessage_CALISTOGA_REGULAR + - ExtendedTextMessage_EXO2_EXTRABOLD + - ExtendedTextMessage_COURIERPRIME_BOLD + waE2E.ExtendedTextMessage_InviteLinkGroupType: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - ExtendedTextMessage_DEFAULT + - ExtendedTextMessage_PARENT + - ExtendedTextMessage_SUB + - ExtendedTextMessage_DEFAULT_SUB + waE2E.ExtendedTextMessage_PreviewType: + enum: + - 0 + - 1 + - 4 + - 5 + - 6 + - 7 + format: int32 + type: integer + x-enum-varnames: + - ExtendedTextMessage_NONE + - ExtendedTextMessage_VIDEO + - ExtendedTextMessage_PLACEHOLDER + - ExtendedTextMessage_IMAGE + - ExtendedTextMessage_PAYMENT_LINKS + - ExtendedTextMessage_PROFILE + waE2E.FullHistorySyncOnDemandConfig: + properties: + historyDurationDays: + type: integer + historyFromTimestamp: + type: integer + type: object + waE2E.FullHistorySyncOnDemandRequestMetadata: + properties: + businessProduct: + type: string + opaqueClientData: + items: + type: integer + type: array + requestID: + type: string + type: object + waE2E.FutureProofMessage: + properties: + message: + $ref: '#/definitions/waE2E.Message' + type: object + waE2E.GroupInviteMessage: + properties: + JPEGThumbnail: + items: + type: integer + type: array + caption: + type: string + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + groupJID: + type: string + groupName: + type: string + groupType: + $ref: '#/definitions/waE2E.GroupInviteMessage_GroupType' + inviteCode: + type: string + inviteExpiration: + type: integer + type: object + waE2E.GroupInviteMessage_GroupType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - GroupInviteMessage_DEFAULT + - GroupInviteMessage_PARENT + waE2E.GroupMention: + properties: + groupJID: + type: string + groupSubject: + type: string + type: object + waE2E.GroupRootKeyShare: + properties: + keys: + items: + $ref: '#/definitions/waE2E.GroupRootKeyShareEntry' + type: array + type: object + waE2E.GroupRootKeyShareEntry: + properties: + createdTimestampMS: + type: integer + expiryTimestampMS: + type: integer + groupRootKey: + items: + type: integer + type: array + keyID: + type: string + type: object + waE2E.HighlyStructuredMessage: + properties: + deterministicLc: + type: string + deterministicLg: + type: string + elementName: + type: string + fallbackLc: + type: string + fallbackLg: + type: string + hydratedHsm: + $ref: '#/definitions/waE2E.TemplateMessage' + localizableParams: + items: + $ref: '#/definitions/waE2E.HighlyStructuredMessage_HSMLocalizableParameter' + type: array + namespace: + type: string + params: + items: + type: string + type: array + type: object + waE2E.HighlyStructuredMessage_HSMLocalizableParameter: + properties: + default: + type: string + paramOneof: + description: "Types that are valid to be assigned to ParamOneof:\n\n\t*HighlyStructuredMessage_HSMLocalizableParameter_Currency\n\t*HighlyStructuredMessage_HSMLocalizableParameter_DateTime" + type: object + waE2E.HistorySyncMessageAccessStatus: + properties: + completeAccessGranted: + type: boolean + type: object + waE2E.HistorySyncNotification: + properties: + chunkOrder: + type: integer + directPath: + type: string + encHandle: + type: string + fileEncSHA256: + items: + type: integer + type: array + fileLength: + type: integer + fileSHA256: + items: + type: integer + type: array + fullHistorySyncOnDemandRequestMetadata: + $ref: '#/definitions/waE2E.FullHistorySyncOnDemandRequestMetadata' + initialHistBootstrapInlinePayload: + items: + type: integer + type: array + mediaKey: + items: + type: integer + type: array + messageAccessStatus: + $ref: '#/definitions/waE2E.HistorySyncMessageAccessStatus' + oldestMsgInChunkTimestampSec: + type: integer + originalMessageID: + type: string + peerDataRequestSessionID: + type: string + progress: + type: integer + syncType: + $ref: '#/definitions/waE2E.HistorySyncType' + type: object + waE2E.HistorySyncType: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + format: int32 + type: integer + x-enum-varnames: + - HistorySyncType_INITIAL_BOOTSTRAP + - HistorySyncType_INITIAL_STATUS_V3 + - HistorySyncType_FULL + - HistorySyncType_RECENT + - HistorySyncType_PUSH_NAME + - HistorySyncType_NON_BLOCKING_DATA + - HistorySyncType_ON_DEMAND + - HistorySyncType_NO_HISTORY + - HistorySyncType_MESSAGE_ACCESS_STATUS + waE2E.HydratedTemplateButton: + properties: + hydratedButton: + description: "Types that are valid to be assigned to HydratedButton:\n\n\t*HydratedTemplateButton_QuickReplyButton\n\t*HydratedTemplateButton_UrlButton\n\t*HydratedTemplateButton_CallButton" + index: + type: integer + type: object + waE2E.ImageMessage: + properties: + JPEGThumbnail: + items: + type: integer + type: array + URL: + type: string + accessibilityLabel: + type: string + annotations: + items: + $ref: '#/definitions/waE2E.InteractiveAnnotation' + type: array + caption: + type: string + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + directPath: + type: string + experimentGroupID: + type: integer + fileEncSHA256: + items: + type: integer + type: array + fileLength: + type: integer + fileSHA256: + items: + type: integer + type: array + firstScanLength: + type: integer + firstScanSidecar: + items: + type: integer + type: array + height: + type: integer + imageSourceType: + $ref: '#/definitions/waE2E.ImageMessage_ImageSourceType' + interactiveAnnotations: + items: + $ref: '#/definitions/waE2E.InteractiveAnnotation' + type: array + mediaKey: + items: + type: integer + type: array + mediaKeyTimestamp: + type: integer + midQualityFileEncSHA256: + items: + type: integer + type: array + midQualityFileSHA256: + items: + type: integer + type: array + mimetype: + type: string + qrURL: + type: string + scanLengths: + items: + type: integer + type: array + scansSidecar: + items: + type: integer + type: array + staticURL: + type: string + thumbnailDirectPath: + type: string + thumbnailEncSHA256: + items: + type: integer + type: array + thumbnailSHA256: + items: + type: integer + type: array + viewOnce: + type: boolean + width: + type: integer + type: object + waE2E.ImageMessage_ImageSourceType: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - ImageMessage_USER_IMAGE + - ImageMessage_AI_GENERATED + - ImageMessage_AI_MODIFIED + - ImageMessage_RASTERIZED_TEXT_STATUS + waE2E.InitialSecurityNotificationSettingSync: + properties: + securityNotificationEnabled: + type: boolean + type: object + waE2E.InsightDeliveryState: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + format: int32 + type: integer + x-enum-varnames: + - InsightDeliveryState_SENT + - InsightDeliveryState_DELIVERED + - InsightDeliveryState_READ + - InsightDeliveryState_REPLIED + - InsightDeliveryState_QUICK_REPLIED + waE2E.InteractiveAnnotation: + properties: + action: + description: "Types that are valid to be assigned to Action:\n\n\t*InteractiveAnnotation_Location\n\t*InteractiveAnnotation_Newsletter\n\t*InteractiveAnnotation_EmbeddedAction\n\t*InteractiveAnnotation_TapAction" + embeddedContent: + $ref: '#/definitions/waE2E.EmbeddedContent' + polygonVertices: + items: + $ref: '#/definitions/waE2E.Point' + type: array + shouldSkipConfirmation: + type: boolean + statusLinkType: + $ref: '#/definitions/waE2E.InteractiveAnnotation_StatusLinkType' + type: object + waE2E.InteractiveAnnotation_StatusLinkType: + enum: + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - InteractiveAnnotation_RASTERIZED_LINK_PREVIEW + - InteractiveAnnotation_RASTERIZED_LINK_TRUNCATED + - InteractiveAnnotation_RASTERIZED_LINK_FULL_URL + waE2E.InteractiveMessage: + properties: + bloksWidget: + $ref: '#/definitions/waE2E.InteractiveMessage_BloksWidget' + body: + $ref: '#/definitions/waE2E.InteractiveMessage_Body' + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + footer: + $ref: '#/definitions/waE2E.InteractiveMessage_Footer' + header: + $ref: '#/definitions/waE2E.InteractiveMessage_Header' + interactiveMessage: + description: "Types that are valid to be assigned to InteractiveMessage:\n\n\t*InteractiveMessage_ShopStorefrontMessage\n\t*InteractiveMessage_CollectionMessage_\n\t*InteractiveMessage_NativeFlowMessage_\n\t*InteractiveMessage_CarouselMessage_" + urlTrackingMap: + $ref: '#/definitions/waE2E.UrlTrackingMap' + type: object + waE2E.InteractiveMessage_BloksWidget: + properties: + data: + type: string + fallback: + type: string + type: + type: string + uuid: + type: string + type: object + waE2E.InteractiveMessage_Body: + properties: + text: + type: string + type: object + waE2E.InteractiveMessage_Footer: + properties: + hasMediaAttachment: + type: boolean + media: + description: "Types that are valid to be assigned to Media:\n\n\t*InteractiveMessage_Footer_AudioMessage" + text: + type: string + type: object + waE2E.InteractiveMessage_Header: + properties: + bloksWidget: + $ref: '#/definitions/waE2E.InteractiveMessage_BloksWidget' + hasMediaAttachment: + type: boolean + media: + description: "Types that are valid to be assigned to Media:\n\n\t*InteractiveMessage_Header_DocumentMessage\n\t*InteractiveMessage_Header_ImageMessage\n\t*InteractiveMessage_Header_JPEGThumbnail\n\t*InteractiveMessage_Header_VideoMessage\n\t*InteractiveMessage_Header_LocationMessage\n\t*InteractiveMessage_Header_ProductMessage" + subtitle: + type: string + title: + type: string + type: object + waE2E.InteractiveResponseMessage: + properties: + body: + $ref: '#/definitions/waE2E.InteractiveResponseMessage_Body' + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + interactiveResponseMessage: + description: "Types that are valid to be assigned to InteractiveResponseMessage:\n\n\t*InteractiveResponseMessage_NativeFlowResponseMessage_" + type: object + waE2E.InteractiveResponseMessage_Body: + properties: + format: + $ref: '#/definitions/waE2E.InteractiveResponseMessage_Body_Format' + text: + type: string + type: object + waE2E.InteractiveResponseMessage_Body_Format: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - InteractiveResponseMessage_Body_DEFAULT + - InteractiveResponseMessage_Body_EXTENSIONS_1 + waE2E.InvoiceMessage: + properties: + attachmentDirectPath: + type: string + attachmentFileEncSHA256: + items: + type: integer + type: array + attachmentFileSHA256: + items: + type: integer + type: array + attachmentJPEGThumbnail: + items: + type: integer + type: array + attachmentMediaKey: + items: + type: integer + type: array + attachmentMediaKeyTimestamp: + type: integer + attachmentMimetype: + type: string + attachmentType: + $ref: '#/definitions/waE2E.InvoiceMessage_AttachmentType' + note: + type: string + token: + type: string + type: object + waE2E.InvoiceMessage_AttachmentType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - InvoiceMessage_IMAGE + - InvoiceMessage_PDF + waE2E.KeepInChatMessage: + properties: + keepType: + $ref: '#/definitions/waE2E.KeepType' + key: + $ref: '#/definitions/waCommon.MessageKey' + timestampMS: + type: integer + type: object + waE2E.KeepType: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - KeepType_UNKNOWN_KEEP_TYPE + - KeepType_KEEP_FOR_ALL + - KeepType_UNDO_KEEP_FOR_ALL + waE2E.LIDMigrationMappingSyncMessage: + properties: + encodedMappingPayload: + items: + type: integer + type: array + type: object + waE2E.LinkPreviewMetadata: + properties: + fbExperimentID: + type: integer + linkInlineVideoMuted: + type: boolean + linkMediaDuration: + type: integer + musicMetadata: + $ref: '#/definitions/waE2E.EmbeddedMusic' + paymentLinkMetadata: + $ref: '#/definitions/waE2E.PaymentLinkMetadata' + socialMediaPostType: + $ref: '#/definitions/waE2E.LinkPreviewMetadata_SocialMediaPostType' + urlMetadata: + $ref: '#/definitions/waE2E.URLMetadata' + videoContentCaption: + type: string + videoContentURL: + type: string + type: object + waE2E.LinkPreviewMetadata_SocialMediaPostType: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + format: int32 + type: integer + x-enum-varnames: + - LinkPreviewMetadata_NONE + - LinkPreviewMetadata_REEL + - LinkPreviewMetadata_LIVE_VIDEO + - LinkPreviewMetadata_LONG_VIDEO + - LinkPreviewMetadata_SINGLE_IMAGE + - LinkPreviewMetadata_CAROUSEL + waE2E.ListMessage: + properties: + buttonText: + type: string + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + description: + type: string + footerText: + type: string + listType: + $ref: '#/definitions/waE2E.ListMessage_ListType' + productListInfo: + $ref: '#/definitions/waE2E.ListMessage_ProductListInfo' + sections: + items: + $ref: '#/definitions/waE2E.ListMessage_Section' + type: array + title: + type: string + type: object + waE2E.ListMessage_ListType: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - ListMessage_UNKNOWN + - ListMessage_SINGLE_SELECT + - ListMessage_PRODUCT_LIST + waE2E.ListMessage_Product: + properties: + productID: + type: string + type: object + waE2E.ListMessage_ProductListHeaderImage: + properties: + JPEGThumbnail: + items: + type: integer + type: array + productID: + type: string + type: object + waE2E.ListMessage_ProductListInfo: + properties: + businessOwnerJID: + type: string + headerImage: + $ref: '#/definitions/waE2E.ListMessage_ProductListHeaderImage' + productSections: + items: + $ref: '#/definitions/waE2E.ListMessage_ProductSection' + type: array + type: object + waE2E.ListMessage_ProductSection: + properties: + products: + items: + $ref: '#/definitions/waE2E.ListMessage_Product' + type: array + title: + type: string + type: object + waE2E.ListMessage_Row: + properties: + description: + type: string + rowID: + type: string + title: + type: string + type: object + waE2E.ListMessage_Section: + properties: + rows: + items: + $ref: '#/definitions/waE2E.ListMessage_Row' + type: array + title: + type: string + type: object + waE2E.ListResponseMessage: + properties: + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + description: + type: string + listType: + $ref: '#/definitions/waE2E.ListResponseMessage_ListType' + singleSelectReply: + $ref: '#/definitions/waE2E.ListResponseMessage_SingleSelectReply' + title: + type: string + type: object + waE2E.ListResponseMessage_ListType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - ListResponseMessage_UNKNOWN + - ListResponseMessage_SINGLE_SELECT + waE2E.ListResponseMessage_SingleSelectReply: + properties: + selectedRowID: + type: string + type: object + waE2E.LiveLocationMessage: + properties: + JPEGThumbnail: + items: + type: integer + type: array + accuracyInMeters: + type: integer + caption: + type: string + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + degreesClockwiseFromMagneticNorth: + type: integer + degreesLatitude: + type: number + degreesLongitude: + type: number + sequenceNumber: + type: integer + speedInMps: + type: number + timeOffset: + type: integer + type: object + waE2E.LocationMessage: + properties: + JPEGThumbnail: + items: + type: integer + type: array + URL: + type: string + accuracyInMeters: + type: integer + address: + type: string + comment: + type: string + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + degreesClockwiseFromMagneticNorth: + type: integer + degreesLatitude: + type: number + degreesLongitude: + type: number + isLive: + type: boolean + name: + type: string + speedInMps: + type: number + type: object + waE2E.MMSThumbnailMetadata: + properties: + mediaKey: + items: + type: integer + type: array + mediaKeyTimestamp: + type: integer + thumbnailDirectPath: + type: string + thumbnailEncSHA256: + items: + type: integer + type: array + thumbnailHeight: + type: integer + thumbnailSHA256: + items: + type: integer + type: array + thumbnailWidth: + type: integer + type: object + waE2E.MediaDomainInfo: + properties: + e2EeMediaKey: + items: + type: integer + type: array + mediaKeyDomain: + $ref: '#/definitions/waE2E.MediaKeyDomain' + type: object + waE2E.MediaKeyDomain: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - MediaKeyDomain_MEDIA_KEY_DOMAIN_UNKNOWN + - MediaKeyDomain_MEDIA_KEY_DOMAIN_E2EE + - MediaKeyDomain_MEDIA_KEY_DOMAIN_NON_E2EE + waE2E.MediaNotifyMessage: + properties: + expressPathURL: + type: string + fileEncSHA256: + items: + type: integer + type: array + fileLength: + type: integer + type: object + waE2E.MemberLabel: + properties: + label: + type: string + labelTimestamp: + type: integer + type: object + waE2E.Message: + properties: + albumMessage: + $ref: '#/definitions/waE2E.AlbumMessage' + associatedChildMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + audioMessage: + $ref: '#/definitions/waE2E.AudioMessage' + bcallMessage: + $ref: '#/definitions/waE2E.BCallMessage' + botForwardedMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + botInvokeMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + botTaskMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + buttonsMessage: + $ref: '#/definitions/waE2E.ButtonsMessage' + buttonsResponseMessage: + $ref: '#/definitions/waE2E.ButtonsResponseMessage' + call: + $ref: '#/definitions/waE2E.Call' + callLogMesssage: + $ref: '#/definitions/waE2E.CallLogMessage' + cancelPaymentRequestMessage: + $ref: '#/definitions/waE2E.CancelPaymentRequestMessage' + chat: + $ref: '#/definitions/waE2E.Chat' + commentMessage: + $ref: '#/definitions/waE2E.CommentMessage' + conditionalRevealMessage: + $ref: '#/definitions/waE2E.ConditionalRevealMessage' + contactMessage: + $ref: '#/definitions/waE2E.ContactMessage' + contactsArrayMessage: + $ref: '#/definitions/waE2E.ContactsArrayMessage' + conversation: + type: string + declinePaymentRequestMessage: + $ref: '#/definitions/waE2E.DeclinePaymentRequestMessage' + deviceSentMessage: + $ref: '#/definitions/waE2E.DeviceSentMessage' + documentMessage: + $ref: '#/definitions/waE2E.DocumentMessage' + documentWithCaptionMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + editedMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + encCommentMessage: + $ref: '#/definitions/waE2E.EncCommentMessage' + encEventResponseMessage: + $ref: '#/definitions/waE2E.EncEventResponseMessage' + encReactionMessage: + $ref: '#/definitions/waE2E.EncReactionMessage' + ephemeralMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + eventCoverImage: + $ref: '#/definitions/waE2E.FutureProofMessage' + eventInviteMessage: + $ref: '#/definitions/waE2E.EventInviteMessage' + eventMessage: + $ref: '#/definitions/waE2E.EventMessage' + extendedTextMessage: + $ref: '#/definitions/waE2E.ExtendedTextMessage' + fastRatchetKeySenderKeyDistributionMessage: + $ref: '#/definitions/waE2E.SenderKeyDistributionMessage' + groupInviteMessage: + $ref: '#/definitions/waE2E.GroupInviteMessage' + groupMentionedMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + groupRootKeyShare: + $ref: '#/definitions/waE2E.GroupRootKeyShare' + groupStatusMentionMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + groupStatusMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + groupStatusMessageV2: + $ref: '#/definitions/waE2E.FutureProofMessage' + highlyStructuredMessage: + $ref: '#/definitions/waE2E.HighlyStructuredMessage' + imageMessage: + $ref: '#/definitions/waE2E.ImageMessage' + interactiveMessage: + $ref: '#/definitions/waE2E.InteractiveMessage' + interactiveResponseMessage: + $ref: '#/definitions/waE2E.InteractiveResponseMessage' + invoiceMessage: + $ref: '#/definitions/waE2E.InvoiceMessage' + keepInChatMessage: + $ref: '#/definitions/waE2E.KeepInChatMessage' + limitSharingMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + listMessage: + $ref: '#/definitions/waE2E.ListMessage' + listResponseMessage: + $ref: '#/definitions/waE2E.ListResponseMessage' + liveLocationMessage: + $ref: '#/definitions/waE2E.LiveLocationMessage' + locationMessage: + $ref: '#/definitions/waE2E.LocationMessage' + lottieStickerMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + messageContextInfo: + $ref: '#/definitions/waE2E.MessageContextInfo' + messageHistoryBundle: + $ref: '#/definitions/waE2E.MessageHistoryBundle' + messageHistoryNotice: + $ref: '#/definitions/waE2E.MessageHistoryNotice' + newsletterAdminInviteMessage: + $ref: '#/definitions/waE2E.NewsletterAdminInviteMessage' + newsletterAdminProfileMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + newsletterAdminProfileMessageV2: + $ref: '#/definitions/waE2E.FutureProofMessage' + newsletterAdminProfileStatusMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + newsletterFollowerInviteMessageV2: + $ref: '#/definitions/waE2E.NewsletterFollowerInviteMessage' + orderMessage: + $ref: '#/definitions/waE2E.OrderMessage' + paymentInviteMessage: + $ref: '#/definitions/waE2E.PaymentInviteMessage' + paymentReminderMessage: + $ref: '#/definitions/waE2E.PaymentReminderMessage' + pinInChatMessage: + $ref: '#/definitions/waE2E.PinInChatMessage' + placeholderMessage: + $ref: '#/definitions/waE2E.PlaceholderMessage' + pollAddOptionMessage: + $ref: '#/definitions/waE2E.PollAddOptionMessage' + pollCreationMessage: + $ref: '#/definitions/waE2E.PollCreationMessage' + pollCreationMessageV2: + $ref: '#/definitions/waE2E.PollCreationMessage' + pollCreationMessageV3: + $ref: '#/definitions/waE2E.PollCreationMessage' + pollCreationMessageV4: + $ref: '#/definitions/waE2E.FutureProofMessage' + pollCreationMessageV5: + $ref: '#/definitions/waE2E.PollCreationMessage' + pollCreationMessageV6: + $ref: '#/definitions/waE2E.PollCreationMessage' + pollCreationOptionImageMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + pollResultSnapshotMessage: + $ref: '#/definitions/waE2E.PollResultSnapshotMessage' + pollResultSnapshotMessageV3: + $ref: '#/definitions/waE2E.PollResultSnapshotMessage' + pollUpdateMessage: + $ref: '#/definitions/waE2E.PollUpdateMessage' + productMessage: + $ref: '#/definitions/waE2E.ProductMessage' + protocolMessage: + $ref: '#/definitions/waE2E.ProtocolMessage' + ptvMessage: + $ref: '#/definitions/waE2E.VideoMessage' + questionMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + questionReplyMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + questionResponseMessage: + $ref: '#/definitions/waE2E.QuestionResponseMessage' + reactionMessage: + $ref: '#/definitions/waE2E.ReactionMessage' + requestPaymentMessage: + $ref: '#/definitions/waE2E.RequestPaymentMessage' + requestPhoneNumberMessage: + $ref: '#/definitions/waE2E.RequestPhoneNumberMessage' + richResponseMessage: + $ref: '#/definitions/waE2E.AIRichResponseMessage' + rootSecretDistributeMessage: + $ref: '#/definitions/waE2E.RootSecretDistributeMessage' + scheduledCallCreationMessage: + $ref: '#/definitions/waE2E.ScheduledCallCreationMessage' + scheduledCallEditMessage: + $ref: '#/definitions/waE2E.ScheduledCallEditMessage' + secretEncryptedMessage: + $ref: '#/definitions/waE2E.SecretEncryptedMessage' + sendPaymentMessage: + $ref: '#/definitions/waE2E.SendPaymentMessage' + senderKeyDistributionMessage: + $ref: '#/definitions/waE2E.SenderKeyDistributionMessage' + splitPaymentMessage: + $ref: '#/definitions/waE2E.SplitPaymentMessage' + spoilerMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + statusAddYours: + $ref: '#/definitions/waE2E.FutureProofMessage' + statusMentionMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + statusNotificationMessage: + $ref: '#/definitions/waE2E.StatusNotificationMessage' + statusQuestionAnswerMessage: + $ref: '#/definitions/waE2E.StatusQuestionAnswerMessage' + statusQuotedMessage: + $ref: '#/definitions/waE2E.StatusQuotedMessage' + statusStickerInteractionMessage: + $ref: '#/definitions/waE2E.StatusStickerInteractionMessage' + stickerMessage: + $ref: '#/definitions/waE2E.StickerMessage' + stickerPackMessage: + $ref: '#/definitions/waE2E.StickerPackMessage' + stickerSyncRmrMessage: + $ref: '#/definitions/waE2E.StickerSyncRMRMessage' + templateButtonReplyMessage: + $ref: '#/definitions/waE2E.TemplateButtonReplyMessage' + templateMessage: + $ref: '#/definitions/waE2E.TemplateMessage' + videoMessage: + $ref: '#/definitions/waE2E.VideoMessage' + viewOnceMessage: + $ref: '#/definitions/waE2E.FutureProofMessage' + viewOnceMessageV2: + $ref: '#/definitions/waE2E.FutureProofMessage' + viewOnceMessageV2Extension: + $ref: '#/definitions/waE2E.FutureProofMessage' + type: object + waE2E.MessageAssociation: + properties: + associationType: + $ref: '#/definitions/waE2E.MessageAssociation_AssociationType' + messageIndex: + type: integer + parentMessageKey: + $ref: '#/definitions/waCommon.MessageKey' + type: object + waE2E.MessageAssociation_AssociationType: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + - 13 + - 14 + - 15 + - 16 + - 17 + - 18 + - 19 + - 20 + format: int32 + type: integer + x-enum-varnames: + - MessageAssociation_UNKNOWN + - MessageAssociation_MEDIA_ALBUM + - MessageAssociation_BOT_PLUGIN + - MessageAssociation_EVENT_COVER_IMAGE + - MessageAssociation_STATUS_POLL + - MessageAssociation_HD_VIDEO_DUAL_UPLOAD + - MessageAssociation_STATUS_EXTERNAL_RESHARE + - MessageAssociation_MEDIA_POLL + - MessageAssociation_STATUS_ADD_YOURS + - MessageAssociation_STATUS_NOTIFICATION + - MessageAssociation_HD_IMAGE_DUAL_UPLOAD + - MessageAssociation_STICKER_ANNOTATION + - MessageAssociation_MOTION_PHOTO + - MessageAssociation_STATUS_LINK_ACTION + - MessageAssociation_VIEW_ALL_REPLIES + - MessageAssociation_STATUS_ADD_YOURS_AI_IMAGINE + - MessageAssociation_STATUS_QUESTION + - MessageAssociation_STATUS_ADD_YOURS_DIWALI + - MessageAssociation_STATUS_REACTION + - MessageAssociation_HEVC_VIDEO_DUAL_UPLOAD + - MessageAssociation_POLL_ADD_OPTION + waE2E.MessageContextInfo: + properties: + botMessageSecret: + items: + type: integer + type: array + botMetadata: + $ref: '#/definitions/waAICommon.BotMetadata' + capiCreatedGroup: + type: boolean + deviceListMetadata: + $ref: '#/definitions/waE2E.DeviceListMetadata' + deviceListMetadataVersion: + type: integer + limitSharing: + $ref: '#/definitions/waCommon.LimitSharing' + limitSharingV2: + $ref: '#/definitions/waCommon.LimitSharing' + messageAddOnDurationInSecs: + type: integer + messageAddOnExpiryType: + $ref: '#/definitions/waE2E.MessageContextInfo_MessageAddonExpiryType' + messageAssociation: + $ref: '#/definitions/waE2E.MessageAssociation' + messageSecret: + items: + type: integer + type: array + paddingBytes: + items: + type: integer + type: array + reportingTokenVersion: + type: integer + supportPayload: + type: string + teeBotMetadata: + items: + type: integer + type: array + threadID: + items: + $ref: '#/definitions/waE2E.ThreadID' + type: array + weblinkRenderConfig: + $ref: '#/definitions/waE2E.WebLinkRenderConfig' + type: object + waE2E.MessageContextInfo_MessageAddonExpiryType: + enum: + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - MessageContextInfo_STATIC + - MessageContextInfo_DEPENDENT_ON_PARENT + waE2E.MessageHistoryBundle: + properties: + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + directPath: + type: string + fileEncSHA256: + items: + type: integer + type: array + fileSHA256: + items: + type: integer + type: array + mediaKey: + items: + type: integer + type: array + mediaKeyTimestamp: + type: integer + messageHistoryMetadata: + $ref: '#/definitions/waE2E.MessageHistoryMetadata' + mimetype: + type: string + type: object + waE2E.MessageHistoryMetadata: + properties: + historyReceivers: + items: + type: string + type: array + messageCount: + type: integer + nonHistoryReceivers: + items: + type: string + type: array + oldestMessageTimestampInBundle: + type: integer + oldestMessageTimestampInWindow: + type: integer + type: object + waE2E.MessageHistoryNotice: + properties: + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + messageHistoryMetadata: + $ref: '#/definitions/waE2E.MessageHistoryMetadata' + type: object + waE2E.Money: + properties: + currencyCode: + type: string + offset: + type: integer + value: + type: integer + type: object + waE2E.NewsletterAdminInviteMessage: + properties: + JPEGThumbnail: + items: + type: integer + type: array + caption: + type: string + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + inviteExpiration: + type: integer + newsletterJID: + type: string + newsletterName: + type: string + type: object + waE2E.NewsletterFollowerInviteMessage: + properties: + JPEGThumbnail: + items: + type: integer + type: array + caption: + type: string + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + newsletterJID: + type: string + newsletterName: + type: string + type: object + waE2E.OrderMessage: + properties: + catalogType: + type: string + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + itemCount: + type: integer + message: + type: string + messageVersion: + type: integer + orderID: + type: string + orderRequestMessageID: + $ref: '#/definitions/waCommon.MessageKey' + orderTitle: + type: string + sellerJID: + type: string + status: + $ref: '#/definitions/waE2E.OrderMessage_OrderStatus' + surface: + $ref: '#/definitions/waE2E.OrderMessage_OrderSurface' + thumbnail: + items: + type: integer + type: array + token: + type: string + totalAmount1000: + type: integer + totalCurrencyCode: + type: string + type: object + waE2E.OrderMessage_OrderStatus: + enum: + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - OrderMessage_INQUIRY + - OrderMessage_ACCEPTED + - OrderMessage_DECLINED + waE2E.OrderMessage_OrderSurface: + enum: + - 1 + format: int32 + type: integer + x-enum-varnames: + - OrderMessage_CATALOG + waE2E.PaymentBackground: + properties: + ID: + type: string + fileLength: + type: integer + height: + type: integer + mediaData: + $ref: '#/definitions/waE2E.PaymentBackground_MediaData' + mimetype: + type: string + placeholderArgb: + type: integer + subtextArgb: + type: integer + textArgb: + type: integer + type: + $ref: '#/definitions/waE2E.PaymentBackground_Type' + width: + type: integer + type: object + waE2E.PaymentBackground_MediaData: + properties: + directPath: + type: string + fileEncSHA256: + items: + type: integer + type: array + fileSHA256: + items: + type: integer + type: array + mediaKey: + items: + type: integer + type: array + mediaKeyTimestamp: + type: integer + type: object + waE2E.PaymentBackground_Type: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - PaymentBackground_UNKNOWN + - PaymentBackground_DEFAULT + waE2E.PaymentExtendedMetadata: + properties: + platform: + type: string + type: + type: integer + type: object + waE2E.PaymentInviteMessage: + properties: + expiryTimestamp: + type: integer + incentiveEligible: + type: boolean + inviteType: + $ref: '#/definitions/waE2E.PaymentInviteMessage_InviteType' + referralID: + type: string + serviceType: + $ref: '#/definitions/waE2E.PaymentInviteMessage_ServiceType' + type: object + waE2E.PaymentInviteMessage_InviteType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - PaymentInviteMessage_DEFAULT + - PaymentInviteMessage_MAPPER + waE2E.PaymentInviteMessage_ServiceType: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - PaymentInviteMessage_UNKNOWN + - PaymentInviteMessage_FBPAY + - PaymentInviteMessage_NOVI + - PaymentInviteMessage_UPI + waE2E.PaymentLinkMetadata: + properties: + button: + $ref: '#/definitions/waE2E.PaymentLinkMetadata_PaymentLinkButton' + header: + $ref: '#/definitions/waE2E.PaymentLinkMetadata_PaymentLinkHeader' + provider: + $ref: '#/definitions/waE2E.PaymentLinkMetadata_PaymentLinkProvider' + type: object + waE2E.PaymentLinkMetadata_PaymentLinkButton: + properties: + displayText: + type: string + type: object + waE2E.PaymentLinkMetadata_PaymentLinkHeader: + properties: + headerType: + $ref: '#/definitions/waE2E.PaymentLinkMetadata_PaymentLinkHeader_PaymentLinkHeaderType' + type: object + waE2E.PaymentLinkMetadata_PaymentLinkHeader_PaymentLinkHeaderType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - PaymentLinkMetadata_PaymentLinkHeader_LINK_PREVIEW + - PaymentLinkMetadata_PaymentLinkHeader_ORDER + waE2E.PaymentLinkMetadata_PaymentLinkProvider: + properties: + paramsJSON: + type: string + type: object + waE2E.PaymentReminderMessage: + properties: + amount: + $ref: '#/definitions/waE2E.Money' + description: + type: string + frequency: + $ref: '#/definitions/waE2E.PaymentReminderMessage_ReminderFrequency' + instanceID: + type: string + payeeJID: + type: string + payeeVpa: + type: string + payerJID: + type: string + reminderID: + type: string + status: + $ref: '#/definitions/waE2E.PaymentReminderMessage_ReminderStatus' + type: object + waE2E.PaymentReminderMessage_ReminderFrequency: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + format: int32 + type: integer + x-enum-varnames: + - PaymentReminderMessage_REMINDER_FREQUENCY_UNKNOWN + - PaymentReminderMessage_WEEKLY + - PaymentReminderMessage_BI_WEEKLY + - PaymentReminderMessage_MONTHLY + - PaymentReminderMessage_QUARTERLY + waE2E.PaymentReminderMessage_ReminderStatus: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + format: int32 + type: integer + x-enum-varnames: + - PaymentReminderMessage_REMINDER_STATUS_UNKNOWN + - PaymentReminderMessage_ACTIVE + - PaymentReminderMessage_CANCELLED_BY_CREATOR + - PaymentReminderMessage_STOPPED_BY_RECEIVER + - PaymentReminderMessage_EXPIRED + - PaymentReminderMessage_PAID + waE2E.PeerDataOperationRequestMessage: + properties: + bizBroadcastInsightsContactListRequest: + $ref: '#/definitions/waE2E.PeerDataOperationRequestMessage_BizBroadcastInsightsContactListRequest' + bizBroadcastInsightsRefreshRequest: + $ref: '#/definitions/waE2E.PeerDataOperationRequestMessage_BizBroadcastInsightsRefreshRequest' + companionCanonicalUserNonceFetchRequest: + $ref: '#/definitions/waE2E.PeerDataOperationRequestMessage_CompanionCanonicalUserNonceFetchRequest' + fullHistorySyncOnDemandRequest: + $ref: '#/definitions/waE2E.PeerDataOperationRequestMessage_FullHistorySyncOnDemandRequest' + galaxyFlowAction: + $ref: '#/definitions/waE2E.PeerDataOperationRequestMessage_GalaxyFlowAction' + historySyncChunkRetryRequest: + $ref: '#/definitions/waE2E.PeerDataOperationRequestMessage_HistorySyncChunkRetryRequest' + historySyncOnDemandRequest: + $ref: '#/definitions/waE2E.PeerDataOperationRequestMessage_HistorySyncOnDemandRequest' + peerDataOperationRequestType: + $ref: '#/definitions/waE2E.PeerDataOperationRequestType' + placeholderMessageResendRequest: + items: + $ref: '#/definitions/waE2E.PeerDataOperationRequestMessage_PlaceholderMessageResendRequest' + type: array + requestStickerReupload: + items: + $ref: '#/definitions/waE2E.PeerDataOperationRequestMessage_RequestStickerReupload' + type: array + requestURLPreview: + items: + $ref: '#/definitions/waE2E.PeerDataOperationRequestMessage_RequestUrlPreview' + type: array + syncdCollectionFatalRecoveryRequest: + $ref: '#/definitions/waE2E.PeerDataOperationRequestMessage_SyncDCollectionFatalRecoveryRequest' + type: object + waE2E.PeerDataOperationRequestMessage_BizBroadcastInsightsContactListRequest: + properties: + campaignID: + type: string + type: object + waE2E.PeerDataOperationRequestMessage_BizBroadcastInsightsRefreshRequest: + properties: + campaignID: + type: string + type: object + waE2E.PeerDataOperationRequestMessage_CompanionCanonicalUserNonceFetchRequest: + properties: + registrationTraceID: + type: string + type: object + waE2E.PeerDataOperationRequestMessage_FullHistorySyncOnDemandRequest: + properties: + fullHistorySyncOnDemandConfig: + $ref: '#/definitions/waE2E.FullHistorySyncOnDemandConfig' + historySyncConfig: + $ref: '#/definitions/waCompanionReg.DeviceProps_HistorySyncConfig' + requestMetadata: + $ref: '#/definitions/waE2E.FullHistorySyncOnDemandRequestMetadata' + type: object + waE2E.PeerDataOperationRequestMessage_GalaxyFlowAction: + properties: + agmID: + type: string + flowID: + type: string + galaxyFlowDownloadRequestID: + type: string + stanzaID: + type: string + type: + $ref: '#/definitions/waE2E.PeerDataOperationRequestMessage_GalaxyFlowAction_GalaxyFlowActionType' + type: object + waE2E.PeerDataOperationRequestMessage_GalaxyFlowAction_GalaxyFlowActionType: + enum: + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - PeerDataOperationRequestMessage_GalaxyFlowAction_NOTIFY_LAUNCH + - PeerDataOperationRequestMessage_GalaxyFlowAction_DOWNLOAD_RESPONSES + waE2E.PeerDataOperationRequestMessage_HistorySyncChunkRetryRequest: + properties: + chunkNotificationID: + type: string + chunkOrder: + type: integer + regenerateChunk: + type: boolean + syncType: + $ref: '#/definitions/waE2E.HistorySyncType' + type: object + waE2E.PeerDataOperationRequestMessage_HistorySyncOnDemandRequest: + properties: + accountLid: + type: string + chatJID: + type: string + oldestMsgFromMe: + type: boolean + oldestMsgID: + type: string + oldestMsgTimestampMS: + type: integer + onDemandMsgCount: + type: integer + supportInlineResponse: + type: boolean + type: object + waE2E.PeerDataOperationRequestMessage_PlaceholderMessageResendRequest: + properties: + messageKey: + $ref: '#/definitions/waCommon.MessageKey' + type: object + waE2E.PeerDataOperationRequestMessage_RequestStickerReupload: + properties: + fileSHA256: + type: string + type: object + waE2E.PeerDataOperationRequestMessage_RequestUrlPreview: + properties: + URL: + type: string + includeHqThumbnail: + type: boolean + type: object + waE2E.PeerDataOperationRequestMessage_SyncDCollectionFatalRecoveryRequest: + properties: + collectionName: + type: string + timestamp: + type: integer + type: object + waE2E.PeerDataOperationRequestResponseMessage: + properties: + peerDataOperationRequestType: + $ref: '#/definitions/waE2E.PeerDataOperationRequestType' + peerDataOperationResult: + items: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult' + type: array + stanzaID: + type: string + type: object + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult: + properties: + bizBroadcastInsightsContactListResponse: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_BizBroadcastInsightsContactListResponse' + companionCanonicalUserNonceFetchRequestResponse: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CompanionCanonicalUserNonceFetchResponse' + companionMetaNonceFetchRequestResponse: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CompanionMetaNonceFetchResponse' + flowResponsesCsvBundle: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FlowResponsesCsvBundle' + fullHistorySyncOnDemandRequestResponse: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FullHistorySyncOnDemandRequestResponse' + historySyncChunkRetryResponse: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_HistorySyncChunkRetryResponse' + linkPreviewResponse: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse' + mediaUploadResult: + $ref: '#/definitions/waMmsRetry.MediaRetryNotification_ResultType' + placeholderMessageResendResponse: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_PlaceholderMessageResendResponse' + stickerMessage: + $ref: '#/definitions/waE2E.StickerMessage' + syncdSnapshotFatalRecoveryResponse: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_SyncDSnapshotFatalRecoveryResponse' + waffleNonceFetchRequestResponse: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_WaffleNonceFetchResponse' + type: object + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_BizBroadcastInsightsContactListResponse: + properties: + campaignID: + type: string + contacts: + items: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_BizBroadcastInsightsContactState' + type: array + timestampMS: + type: integer + type: object + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_BizBroadcastInsightsContactState: + properties: + contactJID: + type: string + state: + $ref: '#/definitions/waE2E.InsightDeliveryState' + type: object + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CompanionCanonicalUserNonceFetchResponse: + properties: + forceRefresh: + type: boolean + nonce: + type: string + waFbid: + type: string + type: object + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CompanionMetaNonceFetchResponse: + properties: + nonce: + type: string + type: object + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FlowResponsesCsvBundle: + properties: + directPath: + type: string + fileEncSHA256: + items: + type: integer + type: array + fileLength: + type: integer + fileName: + type: string + fileSHA256: + items: + type: integer + type: array + flowID: + type: string + galaxyFlowDownloadRequestID: + type: string + mediaKey: + items: + type: integer + type: array + mediaKeyTimestamp: + type: integer + mimetype: + type: string + type: object + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FullHistorySyncOnDemandRequestResponse: + properties: + requestMetadata: + $ref: '#/definitions/waE2E.FullHistorySyncOnDemandRequestMetadata' + responseCode: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FullHistorySyncOnDemandResponseCode' + type: object + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_FullHistorySyncOnDemandResponseCode: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + format: int32 + type: integer + x-enum-varnames: + - PeerDataOperationRequestResponseMessage_PeerDataOperationResult_REQUEST_SUCCESS + - PeerDataOperationRequestResponseMessage_PeerDataOperationResult_REQUEST_TIME_EXPIRED + - PeerDataOperationRequestResponseMessage_PeerDataOperationResult_DECLINED_SHARING_HISTORY + - PeerDataOperationRequestResponseMessage_PeerDataOperationResult_GENERIC_ERROR + - PeerDataOperationRequestResponseMessage_PeerDataOperationResult_ERROR_REQUEST_ON_NON_SMB_PRIMARY + - PeerDataOperationRequestResponseMessage_PeerDataOperationResult_ERROR_HOSTED_DEVICE_NOT_CONNECTED + - PeerDataOperationRequestResponseMessage_PeerDataOperationResult_ERROR_HOSTED_DEVICE_LOGIN_TIME_NOT_SET + - PeerDataOperationRequestResponseMessage_PeerDataOperationResult_ERROR_MULTI_PROVIDER_NOT_CONFIGURED + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_HistorySyncChunkRetryResponse: + properties: + canRecover: + type: boolean + chunkOrder: + type: integer + requestID: + type: string + responseCode: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_HistorySyncChunkRetryResponseCode' + syncType: + $ref: '#/definitions/waE2E.HistorySyncType' + type: object + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_HistorySyncChunkRetryResponseCode: + enum: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + format: int32 + type: integer + x-enum-varnames: + - PeerDataOperationRequestResponseMessage_PeerDataOperationResult_GENERATION_ERROR + - PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CHUNK_CONSUMED + - PeerDataOperationRequestResponseMessage_PeerDataOperationResult_TIMEOUT + - PeerDataOperationRequestResponseMessage_PeerDataOperationResult_SESSION_EXHAUSTED + - PeerDataOperationRequestResponseMessage_PeerDataOperationResult_CHUNK_EXHAUSTED + - PeerDataOperationRequestResponseMessage_PeerDataOperationResult_DUPLICATED_REQUEST + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse: + properties: + URL: + type: string + description: + type: string + hqThumbnail: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse_LinkPreviewHighQualityThumbnail' + matchText: + type: string + previewMetadata: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse_PaymentLinkPreviewMetadata' + previewType: + type: string + thumbData: + items: + type: integer + type: array + title: + type: string + type: object + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse_LinkPreviewHighQualityThumbnail: + properties: + directPath: + type: string + encThumbHash: + type: string + mediaKey: + items: + type: integer + type: array + mediaKeyTimestampMS: + type: integer + thumbHash: + type: string + thumbHeight: + type: integer + thumbWidth: + type: integer + type: object + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_LinkPreviewResponse_PaymentLinkPreviewMetadata: + properties: + amount: + type: string + currency: + type: string + isBusinessVerified: + type: boolean + offset: + type: string + providerName: + type: string + type: object + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_PlaceholderMessageResendResponse: + properties: + webMessageInfoBytes: + items: + type: integer + type: array + type: object + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_SyncDSnapshotFatalRecoveryResponse: + properties: + collectionSnapshot: + items: + type: integer + type: array + isCompressed: + type: boolean + type: object + waE2E.PeerDataOperationRequestResponseMessage_PeerDataOperationResult_WaffleNonceFetchResponse: + properties: + nonce: + type: string + waEntFbid: + type: string + type: object + waE2E.PeerDataOperationRequestType: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + - 13 + format: int32 + type: integer + x-enum-varnames: + - PeerDataOperationRequestType_UPLOAD_STICKER + - PeerDataOperationRequestType_SEND_RECENT_STICKER_BOOTSTRAP + - PeerDataOperationRequestType_GENERATE_LINK_PREVIEW + - PeerDataOperationRequestType_HISTORY_SYNC_ON_DEMAND + - PeerDataOperationRequestType_PLACEHOLDER_MESSAGE_RESEND + - PeerDataOperationRequestType_WAFFLE_LINKING_NONCE_FETCH + - PeerDataOperationRequestType_FULL_HISTORY_SYNC_ON_DEMAND + - PeerDataOperationRequestType_COMPANION_META_NONCE_FETCH + - PeerDataOperationRequestType_COMPANION_SYNCD_SNAPSHOT_FATAL_RECOVERY + - PeerDataOperationRequestType_COMPANION_CANONICAL_USER_NONCE_FETCH + - PeerDataOperationRequestType_HISTORY_SYNC_CHUNK_RETRY + - PeerDataOperationRequestType_GALAXY_FLOW_ACTION + - PeerDataOperationRequestType_BUSINESS_BROADCAST_INSIGHTS_DELIVERED_TO + - PeerDataOperationRequestType_BUSINESS_BROADCAST_INSIGHTS_REFRESH + waE2E.PinInChatMessage: + properties: + key: + $ref: '#/definitions/waCommon.MessageKey' + senderTimestampMS: + type: integer + type: + $ref: '#/definitions/waE2E.PinInChatMessage_Type' + type: object + waE2E.PinInChatMessage_Type: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - PinInChatMessage_UNKNOWN_TYPE + - PinInChatMessage_PIN_FOR_ALL + - PinInChatMessage_UNPIN_FOR_ALL + waE2E.PlaceholderMessage: + properties: + type: + $ref: '#/definitions/waE2E.PlaceholderMessage_PlaceholderType' + type: object + waE2E.PlaceholderMessage_PlaceholderType: + enum: + - 0 + format: int32 + type: integer + x-enum-varnames: + - PlaceholderMessage_MASK_LINKED_DEVICES + waE2E.Point: + properties: + x: + type: number + xDeprecated: + type: integer + "y": + type: number + yDeprecated: + type: integer + type: object + waE2E.PollAddOptionMessage: + properties: + addOption: + $ref: '#/definitions/waE2E.PollCreationMessage_Option' + metadata: + $ref: '#/definitions/waE2E.PollUpdateMessageMetadata' + pollCreationMessageKey: + $ref: '#/definitions/waCommon.MessageKey' + type: object + waE2E.PollContentType: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - PollContentType_UNKNOWN_POLL_CONTENT_TYPE + - PollContentType_TEXT + - PollContentType_IMAGE + waE2E.PollCreationMessage: + properties: + allowAddOption: + type: boolean + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + correctAnswer: + $ref: '#/definitions/waE2E.PollCreationMessage_Option' + encKey: + items: + type: integer + type: array + endTime: + type: integer + hideParticipantName: + type: boolean + name: + type: string + options: + items: + $ref: '#/definitions/waE2E.PollCreationMessage_Option' + type: array + pollContentType: + $ref: '#/definitions/waE2E.PollContentType' + pollType: + $ref: '#/definitions/waE2E.PollType' + selectableOptionsCount: + type: integer + type: object + waE2E.PollCreationMessage_Option: + properties: + optionHash: + type: string + optionName: + type: string + type: object + waE2E.PollEncValue: + properties: + encIV: + items: + type: integer + type: array + encPayload: + items: + type: integer + type: array + type: object + waE2E.PollResultSnapshotMessage: + properties: + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + name: + type: string + pollType: + $ref: '#/definitions/waE2E.PollType' + pollVotes: + items: + $ref: '#/definitions/waE2E.PollResultSnapshotMessage_PollVote' + type: array + type: object + waE2E.PollResultSnapshotMessage_PollVote: + properties: + optionName: + type: string + optionVoteCount: + type: integer + type: object + waE2E.PollType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - PollType_POLL + - PollType_QUIZ + waE2E.PollUpdateMessage: + properties: + metadata: + $ref: '#/definitions/waE2E.PollUpdateMessageMetadata' + pollCreationMessageKey: + $ref: '#/definitions/waCommon.MessageKey' + senderTimestampMS: + type: integer + vote: + $ref: '#/definitions/waE2E.PollEncValue' + type: object + waE2E.PollUpdateMessageMetadata: + properties: + lastEditStanzaID: + type: string + pollNameHash: + items: + type: integer + type: array + type: object + waE2E.ProcessedVideo: + properties: + bitrate: + type: integer + capabilities: + items: + type: string + type: array + directPath: + type: string + fileLength: + type: integer + fileSHA256: + items: + type: integer + type: array + height: + type: integer + quality: + $ref: '#/definitions/waE2E.ProcessedVideo_VideoQuality' + width: + type: integer + type: object + waE2E.ProcessedVideo_VideoQuality: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - ProcessedVideo_UNDEFINED + - ProcessedVideo_LOW + - ProcessedVideo_MID + - ProcessedVideo_HIGH + waE2E.ProductMessage: + properties: + body: + type: string + businessOwnerJID: + type: string + catalog: + $ref: '#/definitions/waE2E.ProductMessage_CatalogSnapshot' + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + footer: + type: string + product: + $ref: '#/definitions/waE2E.ProductMessage_ProductSnapshot' + type: object + waE2E.ProductMessage_CatalogSnapshot: + properties: + catalogImage: + $ref: '#/definitions/waE2E.ImageMessage' + description: + type: string + title: + type: string + type: object + waE2E.ProductMessage_ProductSnapshot: + properties: + URL: + type: string + currencyCode: + type: string + description: + type: string + firstImageID: + type: string + priceAmount1000: + type: integer + productID: + type: string + productImage: + $ref: '#/definitions/waE2E.ImageMessage' + productImageCount: + type: integer + retailerID: + type: string + salePriceAmount1000: + type: integer + signedURL: + type: string + title: + type: string + type: object + waE2E.ProtocolMessage: + properties: + afterReadDuration: + type: integer + aiMediaCollectionMessage: + $ref: '#/definitions/waAICommon.AIMediaCollectionMessage' + aiMetadataOperation: + $ref: '#/definitions/waAICommon.AIMetadataOperation' + aiPsiMetadata: + items: + type: integer + type: array + aiQueryFanout: + $ref: '#/definitions/waE2E.AIQueryFanout' + appStateFatalExceptionNotification: + $ref: '#/definitions/waE2E.AppStateFatalExceptionNotification' + appStateSyncKeyRequest: + $ref: '#/definitions/waE2E.AppStateSyncKeyRequest' + appStateSyncKeyShare: + $ref: '#/definitions/waE2E.AppStateSyncKeyShare' + botFeedbackMessage: + $ref: '#/definitions/waAICommon.BotFeedbackMessage' + chatThemeSetting: + $ref: '#/definitions/waE2E.ChatThemeSetting' + cloudApiThreadControlNotification: + $ref: '#/definitions/waE2E.CloudAPIThreadControlNotification' + disappearingMode: + $ref: '#/definitions/waE2E.DisappearingMode' + editedMessage: + $ref: '#/definitions/waE2E.Message' + ephemeralExpiration: + type: integer + ephemeralSettingTimestamp: + type: integer + historySyncNotification: + $ref: '#/definitions/waE2E.HistorySyncNotification' + initialSecurityNotificationSettingSync: + $ref: '#/definitions/waE2E.InitialSecurityNotificationSettingSync' + invokerJID: + type: string + key: + $ref: '#/definitions/waCommon.MessageKey' + lidMigrationMappingSyncMessage: + $ref: '#/definitions/waE2E.LIDMigrationMappingSyncMessage' + limitSharing: + $ref: '#/definitions/waCommon.LimitSharing' + mediaNotifyMessage: + $ref: '#/definitions/waE2E.MediaNotifyMessage' + memberLabel: + $ref: '#/definitions/waE2E.MemberLabel' + peerDataOperationRequestMessage: + $ref: '#/definitions/waE2E.PeerDataOperationRequestMessage' + peerDataOperationRequestResponseMessage: + $ref: '#/definitions/waE2E.PeerDataOperationRequestResponseMessage' + requestWelcomeMessageMetadata: + $ref: '#/definitions/waE2E.RequestWelcomeMessageMetadata' + timestampMS: + type: integer + type: + $ref: '#/definitions/waE2E.ProtocolMessage_Type' + type: object + waE2E.ProtocolMessage_Type: + enum: + - 0 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 14 + - 16 + - 17 + - 18 + - 19 + - 20 + - 21 + - 22 + - 23 + - 24 + - 25 + - 26 + - 27 + - 28 + - 29 + - 30 + - 31 + - 32 + - 34 + - 35 + format: int32 + type: integer + x-enum-varnames: + - ProtocolMessage_REVOKE + - ProtocolMessage_EPHEMERAL_SETTING + - ProtocolMessage_EPHEMERAL_SYNC_RESPONSE + - ProtocolMessage_HISTORY_SYNC_NOTIFICATION + - ProtocolMessage_APP_STATE_SYNC_KEY_SHARE + - ProtocolMessage_APP_STATE_SYNC_KEY_REQUEST + - ProtocolMessage_MSG_FANOUT_BACKFILL_REQUEST + - ProtocolMessage_INITIAL_SECURITY_NOTIFICATION_SETTING_SYNC + - ProtocolMessage_APP_STATE_FATAL_EXCEPTION_NOTIFICATION + - ProtocolMessage_SHARE_PHONE_NUMBER + - ProtocolMessage_MESSAGE_EDIT + - ProtocolMessage_PEER_DATA_OPERATION_REQUEST_MESSAGE + - ProtocolMessage_PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE + - ProtocolMessage_REQUEST_WELCOME_MESSAGE + - ProtocolMessage_BOT_FEEDBACK_MESSAGE + - ProtocolMessage_MEDIA_NOTIFY_MESSAGE + - ProtocolMessage_CLOUD_API_THREAD_CONTROL_NOTIFICATION + - ProtocolMessage_LID_MIGRATION_MAPPING_SYNC + - ProtocolMessage_REMINDER_MESSAGE + - ProtocolMessage_BOT_MEMU_ONBOARDING_MESSAGE + - ProtocolMessage_STATUS_MENTION_MESSAGE + - ProtocolMessage_STOP_GENERATION_MESSAGE + - ProtocolMessage_LIMIT_SHARING + - ProtocolMessage_AI_PSI_METADATA + - ProtocolMessage_AI_QUERY_FANOUT + - ProtocolMessage_GROUP_MEMBER_LABEL_CHANGE + - ProtocolMessage_AI_MEDIA_COLLECTION_MESSAGE + - ProtocolMessage_MESSAGE_UNSCHEDULE + - ProtocolMessage_CHAT_THEME_SETTING + - ProtocolMessage_AI_METADATA_OPERATION + waE2E.QuestionResponseMessage: + properties: + key: + $ref: '#/definitions/waCommon.MessageKey' + text: + type: string + type: object + waE2E.ReactionMessage: + properties: + groupingKey: + type: string + key: + $ref: '#/definitions/waCommon.MessageKey' + senderTimestampMS: + type: integer + text: + type: string + type: object + waE2E.RequestPaymentMessage: + properties: + amount: + $ref: '#/definitions/waE2E.Money' + amount1000: + type: integer + background: + $ref: '#/definitions/waE2E.PaymentBackground' + currencyCodeIso4217: + type: string + expiryTimestamp: + type: integer + noteMessage: + $ref: '#/definitions/waE2E.Message' + requestFrom: + type: string + type: object + waE2E.RequestPhoneNumberMessage: + properties: + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + type: object + waE2E.RequestWelcomeMessageMetadata: + properties: + botAgentMetadata: + $ref: '#/definitions/waAICommon.BotAgentMetadata' + localChatState: + $ref: '#/definitions/waE2E.RequestWelcomeMessageMetadata_LocalChatState' + welcomeTrigger: + $ref: '#/definitions/waE2E.RequestWelcomeMessageMetadata_WelcomeTrigger' + type: object + waE2E.RequestWelcomeMessageMetadata_LocalChatState: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - RequestWelcomeMessageMetadata_EMPTY + - RequestWelcomeMessageMetadata_NON_EMPTY + waE2E.RequestWelcomeMessageMetadata_WelcomeTrigger: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - RequestWelcomeMessageMetadata_CHAT_OPEN + - RequestWelcomeMessageMetadata_COMPANION_PAIRING + waE2E.RootSecretDistributeMessage: + properties: + chatJID: + type: string + type: object + waE2E.ScheduledCallCreationMessage: + properties: + callType: + $ref: '#/definitions/waE2E.ScheduledCallCreationMessage_CallType' + scheduledTimestampMS: + type: integer + title: + type: string + type: object + waE2E.ScheduledCallCreationMessage_CallType: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - ScheduledCallCreationMessage_UNKNOWN + - ScheduledCallCreationMessage_VOICE + - ScheduledCallCreationMessage_VIDEO + waE2E.ScheduledCallEditMessage: + properties: + editType: + $ref: '#/definitions/waE2E.ScheduledCallEditMessage_EditType' + key: + $ref: '#/definitions/waCommon.MessageKey' + type: object + waE2E.ScheduledCallEditMessage_EditType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - ScheduledCallEditMessage_UNKNOWN + - ScheduledCallEditMessage_CANCEL + waE2E.SecretEncryptedMessage: + properties: + encIV: + items: + type: integer + type: array + encPayload: + items: + type: integer + type: array + remoteKeyID: + type: string + secretEncType: + $ref: '#/definitions/waE2E.SecretEncryptedMessage_SecretEncType' + targetMessageKey: + $ref: '#/definitions/waCommon.MessageKey' + type: object + waE2E.SecretEncryptedMessage_SecretEncType: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + format: int32 + type: integer + x-enum-varnames: + - SecretEncryptedMessage_UNKNOWN + - SecretEncryptedMessage_EVENT_EDIT + - SecretEncryptedMessage_MESSAGE_EDIT + - SecretEncryptedMessage_MESSAGE_SCHEDULE + - SecretEncryptedMessage_POLL_EDIT + - SecretEncryptedMessage_POLL_ADD_OPTION + waE2E.SendPaymentMessage: + properties: + background: + $ref: '#/definitions/waE2E.PaymentBackground' + noteMessage: + $ref: '#/definitions/waE2E.Message' + requestMessageKey: + $ref: '#/definitions/waCommon.MessageKey' + transactionData: + type: string + type: object + waE2E.SenderKeyDistributionMessage: + properties: + axolotlSenderKeyDistributionMessage: + items: + type: integer + type: array + groupID: + type: string + type: object + waE2E.SplitPaymentMessage: + properties: + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + createdAtMS: + type: integer + description: + type: string + participants: + items: + $ref: '#/definitions/waE2E.SplitPaymentParticipant' + type: array + requesterJID: + type: string + splitID: + type: string + totalAmount: + $ref: '#/definitions/waE2E.Money' + type: object + waE2E.SplitPaymentParticipant: + properties: + JID: + type: string + amount: + $ref: '#/definitions/waE2E.Money' + status: + $ref: '#/definitions/waE2E.SplitPaymentParticipant_SplitPaymentStatus' + type: object + waE2E.SplitPaymentParticipant_SplitPaymentStatus: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - SplitPaymentParticipant_PENDING + - SplitPaymentParticipant_PAID + waE2E.StatusNotificationMessage: + properties: + originalMessageKey: + $ref: '#/definitions/waCommon.MessageKey' + responseMessageKey: + $ref: '#/definitions/waCommon.MessageKey' + type: + $ref: '#/definitions/waE2E.StatusNotificationMessage_StatusNotificationType' + type: object + waE2E.StatusNotificationMessage_StatusNotificationType: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - StatusNotificationMessage_UNKNOWN + - StatusNotificationMessage_STATUS_ADD_YOURS + - StatusNotificationMessage_STATUS_RESHARE + - StatusNotificationMessage_STATUS_QUESTION_ANSWER_RESHARE + waE2E.StatusQuestionAnswerMessage: + properties: + key: + $ref: '#/definitions/waCommon.MessageKey' + text: + type: string + type: object + waE2E.StatusQuotedMessage: + properties: + originalStatusID: + $ref: '#/definitions/waCommon.MessageKey' + text: + type: string + thumbnail: + items: + type: integer + type: array + type: + $ref: '#/definitions/waE2E.StatusQuotedMessage_StatusQuotedMessageType' + type: object + waE2E.StatusQuotedMessage_StatusQuotedMessageType: + enum: + - 1 + format: int32 + type: integer + x-enum-varnames: + - StatusQuotedMessage_QUESTION_ANSWER + waE2E.StatusStickerInteractionMessage: + properties: + key: + $ref: '#/definitions/waCommon.MessageKey' + stickerKey: + type: string + type: + $ref: '#/definitions/waE2E.StatusStickerInteractionMessage_StatusStickerType' + type: object + waE2E.StatusStickerInteractionMessage_StatusStickerType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - StatusStickerInteractionMessage_UNKNOWN + - StatusStickerInteractionMessage_REACTION + waE2E.StickerMessage: + properties: + URL: + type: string + accessibilityLabel: + type: string + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + directPath: + type: string + emojis: + type: string + fileEncSHA256: + items: + type: integer + type: array + fileLength: + type: integer + fileSHA256: + items: + type: integer + type: array + firstFrameLength: + type: integer + firstFrameSidecar: + items: + type: integer + type: array + height: + type: integer + isAiSticker: + type: boolean + isAnimated: + type: boolean + isAvatar: + type: boolean + isLottie: + type: boolean + mediaKey: + items: + type: integer + type: array + mediaKeyTimestamp: + type: integer + mimetype: + type: string + pngThumbnail: + items: + type: integer + type: array + premium: + type: integer + stickerSentTS: + type: integer + width: + type: integer + type: object + waE2E.StickerPackMessage: + properties: + caption: + type: string + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + directPath: + type: string + fileEncSHA256: + items: + type: integer + type: array + fileLength: + type: integer + fileSHA256: + items: + type: integer + type: array + imageDataHash: + type: string + mediaKey: + items: + type: integer + type: array + mediaKeyTimestamp: + type: integer + name: + type: string + packDescription: + type: string + publisher: + type: string + stickerPackID: + type: string + stickerPackOrigin: + $ref: '#/definitions/waE2E.StickerPackMessage_StickerPackOrigin' + stickerPackSize: + type: integer + stickers: + items: + $ref: '#/definitions/waE2E.StickerPackMessage_Sticker' + type: array + thumbnailDirectPath: + type: string + thumbnailEncSHA256: + items: + type: integer + type: array + thumbnailHeight: + type: integer + thumbnailSHA256: + items: + type: integer + type: array + thumbnailWidth: + type: integer + trayIconFileName: + type: string + type: object + waE2E.StickerPackMessage_Sticker: + properties: + accessibilityLabel: + type: string + emojis: + items: + type: string + type: array + fileName: + type: string + isAnimated: + type: boolean + isLottie: + type: boolean + mimetype: + type: string + premium: + type: integer + type: object + waE2E.StickerPackMessage_StickerPackOrigin: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - StickerPackMessage_FIRST_PARTY + - StickerPackMessage_THIRD_PARTY + - StickerPackMessage_USER_CREATED + waE2E.StickerSyncRMRMessage: + properties: + filehash: + items: + type: string + type: array + requestTimestamp: + type: integer + rmrSource: + type: string + type: object + waE2E.TemplateButtonReplyMessage: + properties: + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + selectedCarouselCardIndex: + type: integer + selectedDisplayText: + type: string + selectedID: + type: string + selectedIndex: + type: integer + type: object + waE2E.TemplateMessage: + properties: + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + format: + description: "Types that are valid to be assigned to Format:\n\n\t*TemplateMessage_FourRowTemplate_\n\t*TemplateMessage_HydratedFourRowTemplate_\n\t*TemplateMessage_InteractiveMessageTemplate" + hydratedTemplate: + $ref: '#/definitions/waE2E.TemplateMessage_HydratedFourRowTemplate' + templateID: + type: string + type: object + waE2E.TemplateMessage_HydratedFourRowTemplate: + properties: + hydratedButtons: + items: + $ref: '#/definitions/waE2E.HydratedTemplateButton' + type: array + hydratedContentText: + type: string + hydratedFooterText: + type: string + maskLinkedDevices: + type: boolean + templateID: + type: string + title: + description: "Types that are valid to be assigned to Title:\n\n\t*TemplateMessage_HydratedFourRowTemplate_DocumentMessage\n\t*TemplateMessage_HydratedFourRowTemplate_HydratedTitleText\n\t*TemplateMessage_HydratedFourRowTemplate_ImageMessage\n\t*TemplateMessage_HydratedFourRowTemplate_VideoMessage\n\t*TemplateMessage_HydratedFourRowTemplate_LocationMessage" + type: object + waE2E.ThreadID: + properties: + threadKey: + $ref: '#/definitions/waCommon.MessageKey' + threadType: + $ref: '#/definitions/waE2E.ThreadID_ThreadType' + type: object + waE2E.ThreadID_ThreadType: + enum: + - 0 + - 1 + - 2 + format: int32 + type: integer + x-enum-varnames: + - ThreadID_UNKNOWN + - ThreadID_VIEW_REPLIES + - ThreadID_AI_THREAD + waE2E.URLMetadata: + properties: + fbExperimentID: + type: integer + type: object + waE2E.UrlTrackingMap: + properties: + urlTrackingMapElements: + items: + $ref: '#/definitions/waE2E.UrlTrackingMap_UrlTrackingMapElement' + type: array + type: object + waE2E.UrlTrackingMap_UrlTrackingMapElement: + properties: + cardIndex: + type: integer + consentedUsersURL: + type: string + originalURL: + type: string + unconsentedUsersURL: + type: string + type: object + waE2E.VideoEndCard: + properties: + caption: + type: string + profilePictureURL: + type: string + thumbnailImageURL: + type: string + username: + type: string + type: object + waE2E.VideoMessage: + properties: + JPEGThumbnail: + items: + type: integer + type: array + URL: + type: string + accessibilityLabel: + type: string + annotations: + items: + $ref: '#/definitions/waE2E.InteractiveAnnotation' + type: array + caption: + type: string + contextInfo: + $ref: '#/definitions/waE2E.ContextInfo' + directPath: + type: string + externalShareFullVideoDurationInSeconds: + type: integer + fileEncSHA256: + items: + type: integer + type: array + fileLength: + type: integer + fileSHA256: + items: + type: integer + type: array + gifAttribution: + $ref: '#/definitions/waE2E.VideoMessage_Attribution' + gifPlayback: + type: boolean + height: + type: integer + interactiveAnnotations: + items: + $ref: '#/definitions/waE2E.InteractiveAnnotation' + type: array + mediaKey: + items: + type: integer + type: array + mediaKeyTimestamp: + type: integer + metadataURL: + type: string + mimetype: + type: string + motionPhotoPresentationOffsetMS: + type: integer + processedVideos: + items: + $ref: '#/definitions/waE2E.ProcessedVideo' + type: array + seconds: + type: integer + staticURL: + type: string + streamingSidecar: + items: + type: integer + type: array + thumbnailDirectPath: + type: string + thumbnailEncSHA256: + items: + type: integer + type: array + thumbnailSHA256: + items: + type: integer + type: array + videoSourceType: + $ref: '#/definitions/waE2E.VideoMessage_VideoSourceType' + viewOnce: + type: boolean + width: + type: integer + type: object + waE2E.VideoMessage_Attribution: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - VideoMessage_NONE + - VideoMessage_GIPHY + - VideoMessage_TENOR + - VideoMessage_KLIPY + waE2E.VideoMessage_VideoSourceType: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - VideoMessage_USER_VIDEO + - VideoMessage_AI_GENERATED + waE2E.WebLinkRenderConfig: + enum: + - 0 + - 1 + format: int32 + type: integer + x-enum-varnames: + - WebLinkRenderConfig_WEBVIEW + - WebLinkRenderConfig_SYSTEM + waMmsRetry.MediaRetryNotification_ResultType: + enum: + - 0 + - 1 + - 2 + - 3 + format: int32 + type: integer + x-enum-varnames: + - MediaRetryNotification_GENERAL_ERROR + - MediaRetryNotification_SUCCESS + - MediaRetryNotification_NOT_FOUND + - MediaRetryNotification_DECRYPTION_ERROR + waStatusAttributions.StatusAttribution: + properties: + actionURL: + type: string + attributionData: + description: "Types that are valid to be assigned to AttributionData:\n\n\t*StatusAttribution_StatusReshare_\n\t*StatusAttribution_ExternalShare_\n\t*StatusAttribution_Music_\n\t*StatusAttribution_GroupStatus_\n\t*StatusAttribution_RlAttribution\n\t*StatusAttribution_AiCreatedAttribution_" + type: + $ref: '#/definitions/waStatusAttributions.StatusAttribution_Type' + type: object + waStatusAttributions.StatusAttribution_Type: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + format: int32 + type: integer + x-enum-varnames: + - StatusAttribution_UNKNOWN + - StatusAttribution_RESHARE + - StatusAttribution_EXTERNAL_SHARE + - StatusAttribution_MUSIC + - StatusAttribution_STATUS_MENTION + - StatusAttribution_GROUP_STATUS + - StatusAttribution_RL_ATTRIBUTION + - StatusAttribution_AI_CREATED + - StatusAttribution_LAYOUTS + - StatusAttribution_NEWSLETTER_STATUS + - StatusAttribution_STATUS_CLOSE_SHARING + waVnameCert.LocalizedName: + properties: + lc: + type: string + lg: + type: string + verifiedName: + type: string + type: object + waVnameCert.VerifiedNameCertificate: + properties: + details: + items: + type: integer + type: array + serverSignature: + items: + type: integer + type: array + signature: + items: + type: integer + type: array + type: object + waVnameCert.VerifiedNameCertificate_Details: + properties: + issueTime: + type: integer + issuer: + type: string + localizedNames: + items: + $ref: '#/definitions/waVnameCert.LocalizedName' + type: array + serial: + type: integer + verifiedName: + type: string + type: object + whatsmeow.ParticipantChange: + enum: + - add + - remove + - promote + - demote + type: string + x-enum-varnames: + - ParticipantChangeAdd + - ParticipantChangeRemove + - ParticipantChangePromote + - ParticipantChangeDemote +info: + contact: {} + description: AgentDeck Whatsapp Service - whatsmeow + title: AgentDeck Whatsapp Service + version: "1.0" +paths: + /call/reject: + post: + consumes: + - application/json + description: Reject call + parameters: + - description: Call data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_call_service.RejectCallStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Reject call + tags: + - Call + /chat/archive: + post: + consumes: + - application/json + description: Archive a chat + parameters: + - description: Chat + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Archive a chat + tags: + - Chat + /chat/history-sync: + post: + consumes: + - application/json + description: HistorySyncRequest a chat + parameters: + - description: Chat + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_chat_service.HistorySyncRequestStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: HistorySyncRequest a chat + tags: + - Chat + /chat/mute: + post: + consumes: + - application/json + description: Mute a chat + parameters: + - description: Chat + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Mute a chat + tags: + - Chat + /chat/pin: + post: + consumes: + - application/json + description: Pin a chat + parameters: + - description: Chat + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Pin a chat + tags: + - Chat + /chat/unarchive: + post: + consumes: + - application/json + description: Unarchive a chat + parameters: + - description: Chat + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Unarchive a chat + tags: + - Chat + /chat/unmute: + post: + consumes: + - application/json + description: Unmute a chat + parameters: + - description: Chat + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Unmute a chat + tags: + - Chat + /chat/unpin: + post: + consumes: + - application/json + description: Unpin a chat + parameters: + - description: Chat + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_chat_service.BodyStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Unpin a chat + tags: + - Chat + /community/add: + post: + consumes: + - application/json + description: Add participant to community + parameters: + - description: Participant data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_community_service.AddParticipantStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Add participant to community + tags: + - Community + /community/create: + post: + consumes: + - application/json + description: Create community + parameters: + - description: Community data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_community_service.CreateCommunityStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Create community + tags: + - Community + /community/remove: + post: + consumes: + - application/json + description: Remove participant from community + parameters: + - description: Participant data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_community_service.AddParticipantStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Remove participant from community + tags: + - Community + /group/create: + post: + consumes: + - application/json + description: Create group + parameters: + - description: Group data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_group_service.CreateGroupStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Create group + tags: + - Group + /group/description: + post: + consumes: + - application/json + description: Set group description + parameters: + - description: Group data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_group_service.SetGroupDescriptionStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Set group description + tags: + - Group + /group/info: + post: + consumes: + - application/json + description: Get group info + parameters: + - description: Group data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_group_service.GetGroupInfoStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get group info + tags: + - Group + /group/invitelink: + post: + consumes: + - application/json + description: Get group invite link + parameters: + - description: Group data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_group_service.GetGroupInviteLinkStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get group invite link + tags: + - Group + /group/join: + post: + consumes: + - application/json + description: Join group link + parameters: + - description: Group data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_group_service.JoinGroupStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Join group link + tags: + - Group + /group/leave: + post: + consumes: + - application/json + description: Leave group + parameters: + - description: Group data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_group_service.LeaveGroupStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Leave group + tags: + - Group + /group/list: + get: + consumes: + - application/json + description: List groups + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: List groups + tags: + - Group + /group/myall: + get: + consumes: + - application/json + description: Get my groups + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get my groups + tags: + - Group + /group/name: + post: + consumes: + - application/json + description: Set group name + parameters: + - description: Group data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_group_service.SetGroupNameStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Set group name + tags: + - Group + /group/participant: + post: + consumes: + - application/json + description: Update participant + parameters: + - description: Group data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_group_service.AddParticipantStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Update participant + tags: + - Group + /group/photo: + post: + consumes: + - application/json + description: Set group photo + parameters: + - description: Group data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_group_service.SetGroupPhotoStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Set group photo + tags: + - Group + /group/settings: + post: + consumes: + - application/json + description: Update group settings (announcement, not_announcement, locked, + unlocked, approval_on, approval_off, admin_add, all_member_add) + parameters: + - description: Group data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_group_service.UpdateGroupSettingsStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Update group settings + tags: + - Group + /instance/{instanceId}/advanced-settings: + get: + description: Get advanced settings for a specific instance + parameters: + - description: Instance ID + in: path + name: instanceId + required: true + type: string + produces: + - application/json + responses: + "200": + description: Advanced settings retrieved successfully + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_instance_model.AdvancedSettings' + "400": + description: Invalid instance ID + schema: + $ref: '#/definitions/gin.H' + "404": + description: Instance not found + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get advanced settings + tags: + - Instance + put: + consumes: + - application/json + description: Update advanced settings for a specific instance + parameters: + - description: Instance ID + in: path + name: instanceId + required: true + type: string + - description: Advanced settings data + in: body + name: settings + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_instance_model.AdvancedSettings' + produces: + - application/json + responses: + "200": + description: Advanced settings updated successfully + schema: + $ref: '#/definitions/gin.H' + "400": + description: Invalid request data + schema: + $ref: '#/definitions/gin.H' + "404": + description: Instance not found + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Update advanced settings + tags: + - Instance + /instance/all: + get: + consumes: + - application/json + description: Get all instances + produces: + - application/json + responses: + "200": + description: All instances + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get all instances + tags: + - Instance + /instance/connect: + post: + consumes: + - application/json + description: Connect to instance with the provided data + parameters: + - description: Instance data + in: body + name: instance + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_instance_service.ConnectStruct' + produces: + - application/json + responses: + "200": + description: Instance connected successfully + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Connect to instance + tags: + - Instance + /instance/create: + post: + consumes: + - application/json + description: Creates a new instance with the provided data including optional + advanced settings + parameters: + - description: Instance data with optional advanced settings + in: body + name: instance + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_instance_service.CreateStruct' + produces: + - application/json + responses: + "200": + description: Instance created successfully + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Create a new instance + tags: + - Instance + /instance/delete/{instanceId}: + delete: + consumes: + - application/json + description: Delete instance + parameters: + - description: Instance Id + in: path + name: instanceId + required: true + type: string + produces: + - application/json + responses: + "200": + description: Instance deleted successfully + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Delete instance + tags: + - Instance + /instance/disconnect: + post: + consumes: + - application/json + description: Disconnect from instance + produces: + - application/json + responses: + "200": + description: Instance disconnected successfully + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Disconnect from instance + tags: + - Instance + /instance/forcereconnect/{instanceId}: + post: + consumes: + - application/json + description: Force reconnect + parameters: + - description: Instance Id + in: path + name: instanceId + required: true + type: string + - description: Instance data + in: body + name: instance + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_instance_service.ForceReconnectStruct' + produces: + - application/json + responses: + "200": + description: Instance force reconnected successfully + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Force reconnect + tags: + - Instance + /instance/info/{instanceId}: + get: + consumes: + - application/json + description: Get instance + parameters: + - description: Instance Id + in: path + name: instanceId + required: true + type: string + produces: + - application/json + responses: + "200": + description: Instance + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get instance + tags: + - Instance + /instance/logout: + delete: + consumes: + - application/json + description: Logout from instance + produces: + - application/json + responses: + "200": + description: Instance logged out successfully + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Logout from instance + tags: + - Instance + /instance/logs/{instanceId}: + get: + description: Returns log entries for an instance, filterable by date range, + level and limit + parameters: + - description: Instance Id + in: path + name: instanceId + required: true + type: string + - description: Start date (YYYY-MM-DD, defaults to 7 days ago) + in: query + name: start_date + type: string + - description: End date (YYYY-MM-DD, defaults to now) + in: query + name: end_date + type: string + - description: Log level filter + in: query + name: level + type: string + - description: Max number of entries + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: Logs + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get instance logs + tags: + - Instance + /instance/pair: + post: + consumes: + - application/json + description: Request pairing code + parameters: + - description: Instance data + in: body + name: instance + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_instance_service.PairStruct' + produces: + - application/json + responses: + "200": + description: Pairing code + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Request pairing code + tags: + - Instance + /instance/proxy/{instanceId}: + delete: + consumes: + - application/json + description: Delete proxy + parameters: + - description: Instance id + in: path + name: instanceId + required: true + type: string + produces: + - application/json + responses: + "200": + description: Proxy deleted successfully + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Delete proxy + tags: + - Instance + post: + consumes: + - application/json + description: Set proxy configuration for an instance + parameters: + - description: Instance id + in: path + name: instanceId + required: true + type: string + - description: Proxy configuration + in: body + name: proxy + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_instance_service.SetProxyStruct' + produces: + - application/json + responses: + "200": + description: Proxy set successfully + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Set proxy configuration + tags: + - Instance + /instance/qr: + get: + consumes: + - application/json + description: Get instance QR code + produces: + - application/json + responses: + "200": + description: Instance QR code + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get instance QR code + tags: + - Instance + /instance/reconnect: + post: + consumes: + - application/json + description: Reconnect to instance + produces: + - application/json + responses: + "200": + description: Instance reconnected successfully + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Reconnect to instance + tags: + - Instance + /instance/status: + get: + consumes: + - application/json + description: Get instance status + produces: + - application/json + responses: + "200": + description: Instance status + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get instance status + tags: + - Instance + /label/chat: + post: + consumes: + - application/json + description: Add label to chat + parameters: + - description: Label data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_label_service.ChatLabelStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Add label to chat + tags: + - Label + /label/edit: + post: + consumes: + - application/json + description: Edit label + parameters: + - description: Label data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_label_service.EditLabelStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Edit label + tags: + - Label + /label/list: + get: + consumes: + - application/json + description: Get all labels + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get all labels + tags: + - Label + /label/message: + post: + consumes: + - application/json + description: Add label to message + parameters: + - description: Label data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_label_service.MessageLabelStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Add label to message + tags: + - Label + /license/activate: + get: + description: Exchanges an authorization code (from the registration callback) + for an api_key and persists it. Provide the code via the query string. + parameters: + - description: Authorization code from the registration callback + in: query + name: code + required: true + type: string + produces: + - application/json + responses: + "200": + description: Activation result + schema: + $ref: '#/definitions/gin.H' + "400": + description: Missing code parameter + schema: + $ref: '#/definitions/gin.H' + summary: Activate license + tags: + - License + /license/register: + get: + description: Checks the GLOBAL_API_KEY with the licensing server. If not yet + registered, initiates registration and returns a register_url. Accepts an + optional redirect_uri for the post-registration redirect. + parameters: + - description: Post-registration redirect URI + in: query + name: redirect_uri + type: string + produces: + - application/json + responses: + "200": + description: Registration state (status/message or register_url) + schema: + $ref: '#/definitions/gin.H' + summary: Register / get registration URL + tags: + - License + /license/status: + get: + description: Returns whether the instance license is active, along with the + instance id and a masked api key. + produces: + - application/json + responses: + "200": + description: License status ({status, instance_id, api_key?}) + schema: + $ref: '#/definitions/gin.H' + summary: Get license status + tags: + - License + /message/delete: + post: + consumes: + - application/json + description: Delete a message for everyone + parameters: + - description: Delete a message for everyone + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_message_service.MessageStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Delete a message for everyone + tags: + - Message + /message/downloadmedia: + post: + consumes: + - application/json + description: Download the media content of a message (image, video, audio or + document) + parameters: + - description: Download media + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_message_service.DownloadMediaStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Download media + tags: + - Message + /message/edit: + post: + consumes: + - application/json + description: Edit a message + parameters: + - description: Edit a message + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_message_service.EditMessageStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Edit a message + tags: + - Message + /message/markplayed: + post: + consumes: + - application/json + description: Mark an audio message as played + parameters: + - description: Mark an audio message as played + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_message_service.MarkPlayedStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Mark an audio message as played + tags: + - Message + /message/markread: + post: + consumes: + - application/json + description: Mark a message as read + parameters: + - description: Mark a message as read + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_message_service.MarkReadStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Mark a message as read + tags: + - Message + /message/presence: + post: + consumes: + - application/json + description: Set chat presence + parameters: + - description: Set chat presence + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_message_service.ChatPresenceStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Set chat presence + tags: + - Message + /message/react: + post: + consumes: + - application/json + description: React to a message with support for fromMe field and participant + field for group messages + parameters: + - description: React to a message with fromMe and participant fields + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_message_service.ReactStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: React a message + tags: + - Message + /message/status: + post: + consumes: + - application/json + description: Get message status + parameters: + - description: Get message status + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_message_service.MessageStatusStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get message status + tags: + - Message + /newsletter/create: + post: + consumes: + - application/json + description: Create newsletter + parameters: + - description: Newsletter data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_newsletter_service.CreateNewsletterStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Create newsletter + tags: + - Newsletter + /newsletter/info: + post: + consumes: + - application/json + description: Get newsletter + parameters: + - description: Newsletter data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get newsletter + tags: + - Newsletter + /newsletter/link: + post: + consumes: + - application/json + description: Get newsletter invite + parameters: + - description: Newsletter data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterInviteStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get newsletter invite + tags: + - Newsletter + /newsletter/list: + get: + consumes: + - application/json + description: List newsletters + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: List newsletters + tags: + - Newsletter + /newsletter/messages: + post: + consumes: + - application/json + description: Get newsletter messages + parameters: + - description: Newsletter data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterMessagesStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get newsletter messages + tags: + - Newsletter + /newsletter/subscribe: + post: + consumes: + - application/json + description: Subscribe newsletter + parameters: + - description: Newsletter data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_newsletter_service.GetNewsletterStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Subscribe newsletter + tags: + - Newsletter + /passkey-ceremony/{token}: + get: + description: Returns the current WebAuthn passkey-pairing ceremony state for + a token. PUBLIC endpoint (no apikey) — access is gated by the opaque short-lived + ceremony token. Polled by the AgentDeck Passkey Helper browser extension. + parameters: + - description: Ceremony token + in: path + name: token + required: true + type: string + produces: + - application/json + responses: + "200": + description: Ceremony state ({stage, skipHandoffUX, publicKey?, code?, error?}) + schema: + $ref: '#/definitions/gin.H' + "400": + description: token is required + schema: + $ref: '#/definitions/gin.H' + "404": + description: ceremony not found or expired + schema: + $ref: '#/definitions/gin.H' + "503": + description: passkey ceremony unavailable + schema: + $ref: '#/definitions/gin.H' + summary: Get passkey ceremony state + tags: + - Passkey + /passkey-ceremony/{token}/confirm: + post: + description: Finishes the passkey pairing after the user verified the confirmation + code. PUBLIC endpoint (no apikey) — gated by the ceremony token. + parameters: + - description: Ceremony token + in: path + name: token + required: true + type: string + produces: + - application/json + responses: + "200": + description: ok + schema: + $ref: '#/definitions/gin.H' + "400": + description: token is required + schema: + $ref: '#/definitions/gin.H' + "404": + description: ceremony not found or expired + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + "503": + description: passkey ceremony unavailable + schema: + $ref: '#/definitions/gin.H' + summary: Confirm passkey pairing + tags: + - Passkey + /passkey-ceremony/{token}/response: + post: + consumes: + - application/json + description: Receives the WebAuthn assertion produced by the browser extension + and forwards it to WhatsApp. PUBLIC endpoint (no apikey) — gated by the ceremony + token. Body is the WebAuthnResponse shape (id, rawId, type, response{clientDataJSON, + authenticatorData, signature, userHandle?}), base64url-unpadded. + parameters: + - description: Ceremony token + in: path + name: token + required: true + type: string + - description: WebAuthn assertion + in: body + name: response + required: true + schema: + $ref: '#/definitions/types.WebAuthnResponse' + produces: + - application/json + responses: + "200": + description: ok + schema: + $ref: '#/definitions/gin.H' + "400": + description: token is required / invalid body + schema: + $ref: '#/definitions/gin.H' + "404": + description: ceremony not found or expired + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + "503": + description: passkey ceremony unavailable + schema: + $ref: '#/definitions/gin.H' + summary: Submit passkey WebAuthn response + tags: + - Passkey + /polls/{pollMessageId}/results: + get: + consumes: + - application/json + description: Retorna todos os votos de uma enquete específica + parameters: + - description: ID da mensagem da enquete + in: path + name: pollMessageId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_poll_model.PollResults' + "400": + description: Bad Request + schema: + $ref: '#/definitions/gin.H' + "404": + description: Not Found + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/gin.H' + summary: Get poll results + tags: + - Polls + /send/button: + post: + consumes: + - application/json + description: |- + Send an interactive message with buttons. Each button has a `type`: `reply`, `copy`, `url`, `call` or `pix`. + + Combination rules enforced by the server: + - Up to 3 `reply` buttons per message. + - `reply` buttons cannot be mixed with any other type. + - `pix` button must be sent ALONE (no other button in the same message). + + WhatsApp client rendering quirks (NOT enforced by the server, but verified in the field): + - WhatsApp Web: only `reply`-only messages (up to 3) OR CTAs grouped together (`copy` + `url` + `call`) render correctly. + - Do NOT mix `reply` with CTA buttons (`copy`/`url`/`call`) — the message will not appear on WhatsApp Web. + + Required body fields: `number`, `title`, `description`, `footer`, `buttons`. + parameters: + - description: Message data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.ButtonStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Send a button message + tags: + - Send Message + /send/carousel: + post: + consumes: + - application/json + description: |- + Send an interactive carousel (multiple swipeable cards). Each card carries its own image or video, body and optional buttons. + + Card button `type` accepted values (case-insensitive, uppercased internally): `REPLY` (default), `URL`, `CALL`, `COPY`. + The `PIX` button type is NOT supported in carousel cards — use `/send/button` for PIX. + + IMPORTANT — `CarouselButtonStruct` is different from the flat button used in `/send/button`: + - URL button: put the link in the `id` field (NOT in a `url` field). + - CALL button: put the phone number in the `id` field (NOT in a `phoneNumber` field). + - COPY button: put the code to be copied in `copyCode`. + - REPLY button: put the payload/callback ID in `id`. + + Per-card combination rules (NOT enforced by the server, but verified in the field): + - Same WhatsApp Web quirk as `/send/button`: avoid mixing REPLY with CTA buttons (URL/CALL/COPY) in the same card — mixed sets do not render on Web. + - Stick to either "only REPLY" or "only CTAs grouped together" per card. + + Required body fields: `number`, `cards` (at least one). Each card requires `header` + `body`. + parameters: + - description: Message data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.CarouselStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Send a carousel message + tags: + - Send Message + /send/contact: + post: + consumes: + - application/json + description: Send a contact message + parameters: + - description: Message data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.ContactStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Send a contact message + tags: + - Send Message + /send/link: + post: + consumes: + - application/json + description: Send a link message + parameters: + - description: Message data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.LinkStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Send a link message + tags: + - Send Message + /send/list: + post: + consumes: + - application/json + description: |- + Send an interactive list message (single-select) rendered as a tappable menu. + + Required body fields: `number`, `title`, `description`, `footerText`, `buttonText`, `sections`. + Each section must contain one or more `rows`. When `rowId` is omitted, the server generates a fallback ID. + When `buttonText` is empty, the server falls back to "Ver Menu". + + Uses legacy `ListMessage` format (no ViewOnceMessage wrapper) so it renders on iOS, Android and WhatsApp Web. + parameters: + - description: Message data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.ListStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Send a list message + tags: + - Send Message + /send/location: + post: + consumes: + - application/json + description: Send a location message + parameters: + - description: Message data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.LocationStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Send a location message + tags: + - Send Message + /send/media: + post: + consumes: + - application/json + description: Send a media message + parameters: + - description: Message data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.MediaStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Send a media message + tags: + - Send Message + /send/poll: + post: + consumes: + - application/json + description: Send a poll message + parameters: + - description: Message data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.PollStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Send a poll message + tags: + - Send Message + /send/status/media: + post: + consumes: + - application/json + - ' multipart/form-data' + description: Send an image or video status to status@broadcast. Supports JSON + (URL) or multipart/form-data (file upload) + parameters: + - description: 'Media type: image or video' + in: formData + name: type + required: true + type: string + - description: Media file (for multipart upload) + in: formData + name: file + type: file + - description: Media URL (for JSON upload) + in: formData + name: url + type: string + - description: Caption for the media + in: formData + name: caption + type: string + - description: Custom message ID + in: formData + name: id + type: string + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Send a WhatsApp media status (image/video) + tags: + - Send Message + /send/status/text: + post: + consumes: + - application/json + description: Send a WhatsApp text status to status@broadcast + parameters: + - description: Status text data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.StatusTextStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Send a WhatsApp text status + tags: + - Send Message + /send/sticker: + post: + consumes: + - application/json + description: Send a sticker message + parameters: + - description: Message data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.StickerStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Send a sticker message + tags: + - Send Message + /send/text: + post: + consumes: + - application/json + description: Send a text message + parameters: + - description: Message data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_sendMessage_service.TextStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Send a text message + tags: + - Send Message + /unlabel/chat: + post: + consumes: + - application/json + description: Remove label from chat + parameters: + - description: Label data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_label_service.ChatLabelStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Remove label from chat + tags: + - Label + /unlabel/message: + post: + consumes: + - application/json + description: Remove label from message + parameters: + - description: Label data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_label_service.MessageLabelStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Remove label from message + tags: + - Label + /user/avatar: + post: + consumes: + - application/json + description: Get a user's avatar + parameters: + - description: Avatar data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_user_service.GetAvatarStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get a user's avatar + tags: + - User + /user/block: + post: + consumes: + - application/json + description: Block a contact + parameters: + - description: Block data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_user_service.BlockStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Block a contact + tags: + - User + /user/blocklist: + get: + consumes: + - application/json + description: Get a user's block list + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get a user's block list + tags: + - User + /user/check: + post: + consumes: + - application/json + description: Check a user + parameters: + - description: User data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_user_service.CheckUserStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Check a user + tags: + - User + /user/contacts: + get: + consumes: + - application/json + description: Get a user's contacts + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get a user's contacts + tags: + - User + /user/info: + post: + consumes: + - application/json + description: Get a user + parameters: + - description: User data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_user_service.CheckUserStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get a user + tags: + - User + /user/privacy: + get: + consumes: + - application/json + description: Get a user's privacy settings + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Get a user's privacy settings + tags: + - User + post: + consumes: + - application/json + description: Set a user's privacy settings + parameters: + - description: Privacy data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_user_service.PrivacyStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Set a user's privacy settings + tags: + - User + /user/profileName: + post: + consumes: + - application/json + description: Set a user's profile name + parameters: + - description: Profile name data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_user_service.SetProfilePictureStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Set a user's profile name + tags: + - User + /user/profilePicture: + post: + consumes: + - application/json + description: Set a user's profile picture + parameters: + - description: Profile picture data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_user_service.SetProfilePictureStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Set a user's profile picture + tags: + - User + /user/profileStatus: + post: + consumes: + - application/json + description: Set a user's profile status + parameters: + - description: Profile status data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_user_service.SetProfilePictureStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Set a user's profile status + tags: + - User + /user/unblock: + post: + consumes: + - application/json + description: Unblock a contact + parameters: + - description: Block data + in: body + name: message + required: true + schema: + $ref: '#/definitions/agentdeck-whatsapp-service_pkg_user_service.BlockStruct' + produces: + - application/json + responses: + "200": + description: success + schema: + $ref: '#/definitions/gin.H' + "400": + description: Error on validation + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gin.H' + summary: Unblock a contact + tags: + - User +swagger: "2.0" diff --git a/whatsapp-service/go.mod b/whatsapp-service/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..b971e18d82136a411b25c025fca653ea5d0577c5 --- /dev/null +++ b/whatsapp-service/go.mod @@ -0,0 +1,87 @@ +module agentdeck-whatsapp-service + +go 1.25.0 + +require ( + github.com/chai2010/webp v1.1.1 + github.com/gabriel-vasile/mimetype v1.4.5 + github.com/gin-gonic/gin v1.10.0 + github.com/gomessguii/logger v0.0.3 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 + github.com/minio/minio-go/v7 v7.0.80 + github.com/nats-io/nats.go v1.39.0 + github.com/rabbitmq/amqp091-go v1.10.0 + github.com/redis/go-redis/v9 v9.22.0 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.3 + github.com/vincent-petithory/dataurl v1.0.0 + go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b + golang.org/x/exp v0.0.0-20260611194520-c48552f49976 + golang.org/x/image v0.0.0-20211028202545-6944b10bf410 + golang.org/x/net v0.56.0 + google.golang.org/protobuf v1.36.11 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 + gorm.io/gorm v1.25.10 +) + +require ( + filippo.io/edwards25519 v1.2.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/beeper/argo-go v1.1.2 // indirect + github.com/bytedance/sonic v1.12.2 // indirect + github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/coder/websocket v1.8.15 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/nats-io/nkeys v0.4.9 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/rs/zerolog v1.35.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/vektah/gqlparser/v2 v2.5.27 // indirect + go.mau.fi/libsignal v0.2.2 // indirect + go.mau.fi/util v0.9.10 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/arch v0.10.0 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 // indirect + golang.org/x/tools v0.46.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/whatsapp-service/go.sum b/whatsapp-service/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..f3f765440fcba7d7eac419d1c1b6793c81d24215 --- /dev/null +++ b/whatsapp-service/go.sum @@ -0,0 +1,241 @@ +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs= +github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg= +github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= +github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk= +github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= +github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= +github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= +github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= +github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gomessguii/logger v0.0.3 h1:985MqDkp2Fi6IQ3eQ3NG7VXZo8flbWd+6QUj9hASWJA= +github.com/gomessguii/logger v0.0.3/go.mod h1:JBfDf2h4qUFaIjpE/0T4/jeLpIHopc73k+G9+iu9+ms= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk= +github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.80 h1:2mdUHXEykRdY/BigLt3Iuu1otL0JTogT0Nmltg0wujk= +github.com/minio/minio-go/v7 v7.0.80/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nats-io/nats.go v1.39.0 h1:2/yg2JQjiYYKLwDuBzV0FbB2sIV+eFNkEevlRi4n9lI= +github.com/nats-io/nats.go v1.39.0/go.mod h1:MgRb8oOdigA6cYpEPhXJuRVH6UE/V4jblJ2jQ27IXYM= +github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= +github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM= +github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= +github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= +github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s= +github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= +github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI= +github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU= +go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0= +go.mau.fi/util v0.9.10 h1:wzvz5iDHyqDXB8vgisD4d3SzucLXNM3iNY+1O1RoHtg= +go.mau.fi/util v0.9.10/go.mod h1:YQOxySn+ZE3qSYqNxvyX7Yi3suA8YK17PS6QqBREW7A= +go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b h1:ZUk1ErarDNpnbosXR/MeOz2gkqA4S1bh8zjaSRj7N+Y= +go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= +golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= +golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= +golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= +gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/whatsapp-service/pkg/cache/redis.go b/whatsapp-service/pkg/cache/redis.go new file mode 100644 index 0000000000000000000000000000000000000000..7cfb2cf8e83695c597c568d4904a1a7d7cfd83f6 --- /dev/null +++ b/whatsapp-service/pkg/cache/redis.go @@ -0,0 +1,64 @@ +// Package cache provides a Redis-backed cache with a small API mirroring the +// in-memory cache it replaces (userInfoCache, processedMessages). +package cache + +import ( + "context" + "encoding/json" + "time" + + "github.com/redis/go-redis/v9" +) + +// Cache wraps a go-redis client for key/value storage with TTL. +type Cache struct { + rdb *redis.Client + ctx context.Context +} + +// New creates a Cache backed by the given Redis client. +func New(rdb *redis.Client) *Cache { + return &Cache{rdb: rdb, ctx: context.Background()} +} + +// Set stores a value under key with an optional TTL (0 = no expiration). +func (c *Cache) Set(key string, value interface{}, ttl time.Duration) error { + b, err := json.Marshal(value) + if err != nil { + return err + } + return c.rdb.Set(c.ctx, key, b, ttl).Err() +} + +// Get returns (value, true) if key exists, decoding into out. +func (c *Cache) Get(key string, out interface{}) (bool, error) { + data, err := c.rdb.Get(c.ctx, key).Bytes() + if err != nil { + if err == redis.Nil { + return false, nil + } + return false, err + } + if out == nil { + return true, nil + } + if err := json.Unmarshal(data, out); err != nil { + return false, err + } + return true, nil +} + +// Delete removes a key. +func (c *Cache) Delete(key string) error { + return c.rdb.Del(c.ctx, key).Err() +} + +// SetNX atomically sets a key only if it does not already exist. +// Returns true when the key was newly created. +func (c *Cache) SetNX(key string, value interface{}, ttl time.Duration) (bool, error) { + b, err := json.Marshal(value) + if err != nil { + return false, err + } + return c.rdb.SetNX(c.ctx, key, b, ttl).Result() +} diff --git a/whatsapp-service/pkg/call/handler/call_handler.go b/whatsapp-service/pkg/call/handler/call_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..b6caac1f3c5074464b28875d70c6d34bcc29f47d --- /dev/null +++ b/whatsapp-service/pkg/call/handler/call_handler.go @@ -0,0 +1,60 @@ +package call_handler + +import ( + "net/http" + + call_service "agentdeck-whatsapp-service/pkg/call/service" + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + "github.com/gin-gonic/gin" +) + +type CallHandler interface { + RejectCall(ctx *gin.Context) +} + +type callHandler struct { + callService call_service.CallService +} + +// Reject call +// @Summary Reject call +// @Description Reject call +// @Tags Call +// @Accept json +// @Produce json +// @Param message body call_service.RejectCallStruct true "Call data" +// @Success 200 {object} gin.H "success" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /call/reject [post] +func (g *callHandler) RejectCall(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *call_service.RejectCallStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + err = g.callService.RejectCall(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +func NewCallHandler( + callService call_service.CallService, +) CallHandler { + return &callHandler{ + callService: callService, + } +} diff --git a/whatsapp-service/pkg/call/service/call_service.go b/whatsapp-service/pkg/call/service/call_service.go new file mode 100644 index 0000000000000000000000000000000000000000..d512a60f77d707039ec056b6239e410ab43b6934 --- /dev/null +++ b/whatsapp-service/pkg/call/service/call_service.go @@ -0,0 +1,95 @@ +package call_service + +import ( + "context" + "errors" + "time" + + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service" + "github.com/gomessguii/logger" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/types" +) + +type CallService interface { + RejectCall(data *RejectCallStruct, instance *instance_model.Instance) error +} + +type callService struct { + clientPointer map[string]*whatsmeow.Client + whatsmeowService whatsmeow_service.WhatsmeowService + loggerWrapper *logger_wrapper.LoggerManager +} + +type RejectCallStruct struct { + CallCreator types.JID `json:"callCreator"` + CallID string `json:"callId"` +} + +func (c *callService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + client := c.clientPointer[instanceId] + c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) + + if client == nil { + c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] No client found, attempting to start new instance", instanceId) + err := c.whatsmeowService.StartInstance(instanceId) + if err != nil { + c.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to start instance: %v", instanceId, err) + return nil, errors.New("no active session found") + } + + c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) + time.Sleep(2 * time.Second) + + client = c.clientPointer[instanceId] + c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + + if client == nil || !client.IsConnected() { + c.loggerWrapper.GetLogger(instanceId).LogError("[%s] New client validation failed - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + return nil, errors.New("no active session found") + } + } else if !client.IsConnected() { + c.loggerWrapper.GetLogger(instanceId).LogError("[%s] Existing client is disconnected - Connected status: %v", + instanceId, + client.IsConnected()) + return nil, errors.New("client disconnected") + } + + c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected()) + return client, nil +} + +func (c *callService) RejectCall(data *RejectCallStruct, instance *instance_model.Instance) error { + client, err := c.ensureClientConnected(instance.Id) + if err != nil { + return err + } + + err = client.RejectCall(context.Background(), data.CallCreator, data.CallID) + if err != nil { + logger.LogError("[%s] error reject call: %v", instance.Id, err) + return err + } + + return nil +} + +func NewCallService( + clientPointer map[string]*whatsmeow.Client, + whatsmeowService whatsmeow_service.WhatsmeowService, + loggerWrapper *logger_wrapper.LoggerManager, +) CallService { + return &callService{ + clientPointer: clientPointer, + whatsmeowService: whatsmeowService, + loggerWrapper: loggerWrapper, + } +} diff --git a/whatsapp-service/pkg/chat/handler/chat_handler.go b/whatsapp-service/pkg/chat/handler/chat_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..3d08b500c62b783dc89a57619c00f1a8e09417e1 --- /dev/null +++ b/whatsapp-service/pkg/chat/handler/chat_handler.go @@ -0,0 +1,337 @@ +package chat_handler + +import ( + "net/http" + + chat_service "agentdeck-whatsapp-service/pkg/chat/service" + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + "github.com/gin-gonic/gin" +) + +type ChatHandler interface { + ChatPin(ctx *gin.Context) + ChatUnpin(ctx *gin.Context) + ChatArchive(ctx *gin.Context) + ChatUnarchive(ctx *gin.Context) + ChatMute(ctx *gin.Context) + ChatUnmute(ctx *gin.Context) + HistorySyncRequest(ctx *gin.Context) +} + +type chatHandler struct { + chatService chat_service.ChatService +} + +// Pin a chat +// @Summary Pin a chat +// @Description Pin a chat +// @Tags Chat +// @Accept json +// @Produce json +// @Param message body chat_service.BodyStruct true "Chat" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /chat/pin [post] +func (c *chatHandler) ChatPin(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *chat_service.BodyStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Chat == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"}) + return + } + + ts, err := c.chatService.ChatPin(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + responseData := gin.H{ + "timestamp": ts, + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// Unpin a chat +// @Summary Unpin a chat +// @Description Unpin a chat +// @Tags Chat +// @Accept json +// @Produce json +// @Param message body chat_service.BodyStruct true "Chat" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /chat/unpin [post] +func (c *chatHandler) ChatUnpin(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *chat_service.BodyStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Chat == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"}) + return + } + + ts, err := c.chatService.ChatUnpin(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + responseData := gin.H{ + "timestamp": ts, + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// Archive a chat +// @Summary Archive a chat +// @Description Archive a chat +// @Tags Chat +// @Accept json +// @Produce json +// @Param message body chat_service.BodyStruct true "Chat" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /chat/archive [post] +func (c *chatHandler) ChatArchive(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *chat_service.BodyStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Chat == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"}) + return + } + + ts, err := c.chatService.ChatArchive(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + responseData := gin.H{ + "timestamp": ts, + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// Unarchive a chat +// @Summary Unarchive a chat +// @Description Unarchive a chat +// @Tags Chat +// @Accept json +// @Produce json +// @Param message body chat_service.BodyStruct true "Chat" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /chat/unarchive [post] +func (c *chatHandler) ChatUnarchive(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *chat_service.BodyStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Chat == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"}) + return + } + + ts, err := c.chatService.ChatUnarchive(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + responseData := gin.H{ + "timestamp": ts, + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// Mute a chat +// @Summary Mute a chat +// @Description Mute a chat +// @Tags Chat +// @Accept json +// @Produce json +// @Param message body chat_service.BodyStruct true "Chat" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /chat/mute [post] +func (c *chatHandler) ChatMute(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *chat_service.BodyStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Chat == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"}) + return + } + + ts, err := c.chatService.ChatMute(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + responseData := gin.H{ + "timestamp": ts, + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// Unmute a chat +// @Summary Unmute a chat +// @Description Unmute a chat +// @Tags Chat +// @Accept json +// @Produce json +// @Param message body chat_service.BodyStruct true "Chat" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /chat/unmute [post] +func (c *chatHandler) ChatUnmute(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *chat_service.BodyStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Chat == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"}) + return + } + + ts, err := c.chatService.ChatUnmute(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + responseData := gin.H{ + "timestamp": ts, + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// HistorySyncRequest a chat +// @Summary HistorySyncRequest a chat +// @Description HistorySyncRequest a chat +// @Tags Chat +// @Accept json +// @Produce json +// @Param message body chat_service.HistorySyncRequestStruct true "Chat" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /chat/history-sync [post] +func (c *chatHandler) HistorySyncRequest(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *chat_service.HistorySyncRequestStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + resp, err := c.chatService.HistorySyncRequest(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp}) +} + +func NewChatHandler( + chatService chat_service.ChatService, +) ChatHandler { + return &chatHandler{ + chatService: chatService, + } +} diff --git a/whatsapp-service/pkg/chat/service/chat_service.go b/whatsapp-service/pkg/chat/service/chat_service.go new file mode 100644 index 0000000000000000000000000000000000000000..b0d3d9bbd0192a2797c3172256eba79707f81bbd --- /dev/null +++ b/whatsapp-service/pkg/chat/service/chat_service.go @@ -0,0 +1,256 @@ +package chat_service + +import ( + "context" + "errors" + "time" + + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + "agentdeck-whatsapp-service/pkg/utils" + whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/appstate" + "go.mau.fi/whatsmeow/types" +) + +type ChatService interface { + ChatPin(data *BodyStruct, instance *instance_model.Instance) (string, error) + ChatUnpin(data *BodyStruct, instance *instance_model.Instance) (string, error) + ChatArchive(data *BodyStruct, instance *instance_model.Instance) (string, error) + ChatUnarchive(data *BodyStruct, instance *instance_model.Instance) (string, error) + ChatMute(data *BodyStruct, instance *instance_model.Instance) (string, error) + ChatUnmute(data *BodyStruct, instance *instance_model.Instance) (string, error) + HistorySyncRequest(data *HistorySyncRequestStruct, instance *instance_model.Instance) (*whatsmeow.SendResponse, error) +} + +type chatService struct { + clientPointer map[string]*whatsmeow.Client + whatsmeowService whatsmeow_service.WhatsmeowService + loggerWrapper *logger_wrapper.LoggerManager +} + +type BodyStruct struct { + Chat string `json:"chat"` +} + +type HistorySyncRequestStruct struct { + MessageInfo *types.MessageInfo `json:"messageInfo"` + Count int `json:"count"` +} + +func (c *chatService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + client := c.clientPointer[instanceId] + c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) + + if client == nil { + c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] No client found, attempting to start new instance", instanceId) + err := c.whatsmeowService.StartInstance(instanceId) + if err != nil { + c.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to start instance: %v", instanceId, err) + return nil, errors.New("no active session found") + } + + c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) + time.Sleep(2 * time.Second) + + client = c.clientPointer[instanceId] + c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + + if client == nil || !client.IsConnected() { + c.loggerWrapper.GetLogger(instanceId).LogError("[%s] New client validation failed - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + return nil, errors.New("no active session found") + } + } else if !client.IsConnected() { + c.loggerWrapper.GetLogger(instanceId).LogError("[%s] Existing client is disconnected - Connected status: %v", + instanceId, + client.IsConnected()) + return nil, errors.New("client disconnected") + } + + c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected()) + return client, nil +} + +func (c *chatService) ChatPin(data *BodyStruct, instance *instance_model.Instance) (string, error) { + client, err := c.ensureClientConnected(instance.Id) + if err != nil { + return "", err + } + + var ts time.Time + + recipient, ok := utils.ParseJID(data.Chat) + if !ok { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return "", errors.New("invalid phone number") + } + + err = client.SendAppState(context.Background(), appstate.BuildPin(recipient, true)) + if err != nil { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error pin chat: %v", instance.Id, err) + return "", err + } + + return ts.String(), nil +} + +func (c *chatService) ChatUnpin(data *BodyStruct, instance *instance_model.Instance) (string, error) { + client, err := c.ensureClientConnected(instance.Id) + if err != nil { + return "", err + } + + var ts time.Time + + recipient, ok := utils.ParseJID(data.Chat) + if !ok { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return "", errors.New("invalid phone number") + } + + err = client.SendAppState(context.Background(), appstate.BuildPin(recipient, false)) + if err != nil { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error unpin chat: %v", instance.Id, err) + return "", err + } + + return ts.String(), nil +} + +func (c *chatService) ChatArchive(data *BodyStruct, instance *instance_model.Instance) (string, error) { + client, err := c.ensureClientConnected(instance.Id) + if err != nil { + return "", err + } + + var ts time.Time + + recipient, ok := utils.ParseJID(data.Chat) + if !ok { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return "", errors.New("invalid phone number") + } + + err = client.SendAppState(context.Background(), appstate.BuildArchive(recipient, true, time.Time{}, nil)) + if err != nil { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error archive chat: %v", instance.Id, err) + return "", err + } + + return ts.String(), nil +} + +func (c *chatService) ChatUnarchive(data *BodyStruct, instance *instance_model.Instance) (string, error) { + client, err := c.ensureClientConnected(instance.Id) + if err != nil { + return "", err + } + + var ts time.Time + + recipient, ok := utils.ParseJID(data.Chat) + if !ok { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return "", errors.New("invalid phone number") + } + + err = client.SendAppState(context.Background(), appstate.BuildArchive(recipient, false, time.Time{}, nil)) + if err != nil { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error unarchive chat: %v", instance.Id, err) + return "", err + } + + return ts.String(), nil +} + +func (c *chatService) ChatMute(data *BodyStruct, instance *instance_model.Instance) (string, error) { + client, err := c.ensureClientConnected(instance.Id) + if err != nil { + return "", err + } + + var ts time.Time + + recipient, ok := utils.ParseJID(data.Chat) + if !ok { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return "", errors.New("invalid phone number") + } + + err = client.SendAppState(context.Background(), appstate.BuildMute(recipient, true, 1*time.Hour)) + if err != nil { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error mute chat: %v", instance.Id, err) + return "", err + } + + return ts.String(), nil +} + +func (c *chatService) ChatUnmute(data *BodyStruct, instance *instance_model.Instance) (string, error) { + client, err := c.ensureClientConnected(instance.Id) + if err != nil { + return "", err + } + + var ts time.Time + + recipient, ok := utils.ParseJID(data.Chat) + if !ok { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return "", errors.New("invalid phone number") + } + + err = client.SendAppState(context.Background(), appstate.BuildMute(recipient, false, 0*time.Hour)) + if err != nil { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error unmute chat: %v", instance.Id, err) + return "", err + } + + return ts.String(), nil +} + +func (c *chatService) HistorySyncRequest(data *HistorySyncRequestStruct, instance *instance_model.Instance) (*whatsmeow.SendResponse, error) { + client, err := c.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + messageInfo := types.MessageInfo{ + MessageSource: types.MessageSource{ + Chat: data.MessageInfo.Chat, + IsFromMe: data.MessageInfo.IsFromMe, + IsGroup: data.MessageInfo.IsGroup, + }, + ID: data.MessageInfo.ID, + Timestamp: data.MessageInfo.Timestamp, + } + + histRequest := client.BuildHistorySyncRequest(&messageInfo, data.Count) + + res, err := client.SendMessage(context.Background(), messageInfo.Chat, histRequest, whatsmeow.SendRequestExtra{Peer: true}) + if err != nil { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error history sync request: %v", instance.Id, err) + return nil, err + } + + return &res, nil +} + +func NewChatService( + clientPointer map[string]*whatsmeow.Client, + whatsmeowService whatsmeow_service.WhatsmeowService, + loggerWrapper *logger_wrapper.LoggerManager, +) ChatService { + return &chatService{ + clientPointer: clientPointer, + whatsmeowService: whatsmeowService, + loggerWrapper: loggerWrapper, + } +} diff --git a/whatsapp-service/pkg/community/handler/community_handler.go b/whatsapp-service/pkg/community/handler/community_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..34687411180c160f269582e023bdd013bca691ae --- /dev/null +++ b/whatsapp-service/pkg/community/handler/community_handler.go @@ -0,0 +1,160 @@ +package community_handler + +import ( + "net/http" + + community_service "agentdeck-whatsapp-service/pkg/community/service" + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + "github.com/gin-gonic/gin" +) + +type CommunityHandler interface { + CreateCommunity(ctx *gin.Context) + CommunityAdd(ctx *gin.Context) + CommunityRemove(ctx *gin.Context) +} + +type communityHandler struct { + communityService community_service.CommunityService +} + +// Create community +// @Summary Create community +// @Description Create community +// @Tags Community +// @Accept json +// @Produce json +// @Param message body community_service.CreateCommunityStruct true "Community data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /community/create [post] +func (c *communityHandler) CreateCommunity(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *community_service.CreateCommunityStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.CommunityName == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "community name is required"}) + return + } + + community, err := c.communityService.CreateCommunity(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": community}) +} + +// Add participant to community +// @Summary Add participant to community +// @Description Add participant to community +// @Tags Community +// @Accept json +// @Produce json +// @Param message body community_service.AddParticipantStruct true "Participant data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /community/add [post] +func (c *communityHandler) CommunityAdd(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *community_service.AddParticipantStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.CommunityJID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "community jid is required"}) + return + } + + if len(data.GroupJID) == 0 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "group jid is required"}) + return + } + + resp, err := c.communityService.CommunityAdd(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp}) +} + +// Remove participant from community +// @Summary Remove participant from community +// @Description Remove participant from community +// @Tags Community +// @Accept json +// @Produce json +// @Param message body community_service.AddParticipantStruct true "Participant data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /community/remove [post] +func (c *communityHandler) CommunityRemove(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *community_service.AddParticipantStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.CommunityJID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "community jid is required"}) + return + } + + if len(data.GroupJID) == 0 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "group jid is required"}) + return + } + + resp, err := c.communityService.CommunityRemove(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp}) +} + +func NewCommunityHandler( + communityService community_service.CommunityService, +) CommunityHandler { + return &communityHandler{ + communityService: communityService, + } +} diff --git a/whatsapp-service/pkg/community/service/community_service.go b/whatsapp-service/pkg/community/service/community_service.go new file mode 100644 index 0000000000000000000000000000000000000000..b7a25dd751d5337d6c4ad997b1c94e5228abfbb4 --- /dev/null +++ b/whatsapp-service/pkg/community/service/community_service.go @@ -0,0 +1,169 @@ +package community_service + +import ( + "context" + "errors" + "time" + + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + "agentdeck-whatsapp-service/pkg/utils" + whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service" + "github.com/gin-gonic/gin" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/types" +) + +type CommunityService interface { + CreateCommunity(data *CreateCommunityStruct, instance *instance_model.Instance) (*types.GroupInfo, error) + CommunityAdd(data *AddParticipantStruct, instance *instance_model.Instance) (gin.H, error) + CommunityRemove(data *AddParticipantStruct, instance *instance_model.Instance) (gin.H, error) +} + +type communityService struct { + clientPointer map[string]*whatsmeow.Client + whatsmeowService whatsmeow_service.WhatsmeowService + loggerWrapper *logger_wrapper.LoggerManager +} + +type CreateCommunityStruct struct { + CommunityName string `json:"communityName"` +} + +type AddParticipantStruct struct { + CommunityJID string `json:"communityJid"` + GroupJID []string `json:"groupJid"` +} + +func (c *communityService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + client := c.clientPointer[instanceId] + c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) + + if client == nil { + c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] No client found, attempting to start new instance", instanceId) + err := c.whatsmeowService.StartInstance(instanceId) + if err != nil { + c.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to start instance: %v", instanceId, err) + return nil, errors.New("no active session found") + } + + c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) + time.Sleep(2 * time.Second) + + client = c.clientPointer[instanceId] + c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + + if client == nil || !client.IsConnected() { + c.loggerWrapper.GetLogger(instanceId).LogError("[%s] New client validation failed - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + return nil, errors.New("no active session found") + } + } else if !client.IsConnected() { + c.loggerWrapper.GetLogger(instanceId).LogError("[%s] Existing client is disconnected - Connected status: %v", + instanceId, + client.IsConnected()) + return nil, errors.New("client disconnected") + } + + c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected()) + return client, nil +} + +func (c *communityService) CreateCommunity(data *CreateCommunityStruct, instance *instance_model.Instance) (*types.GroupInfo, error) { + client, err := c.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + resp, err := client.CreateGroup(context.Background(), whatsmeow.ReqCreateGroup{ + Name: data.CommunityName, + GroupParent: types.GroupParent{ + IsParent: true, + }, + }) + if err != nil { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error create community: %v", instance.Id, err) + return nil, err + } + + return resp, nil +} + +func (c *communityService) CommunityAdd(data *AddParticipantStruct, instance *instance_model.Instance) (gin.H, error) { + client, err := c.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + communityJID, ok := utils.ParseJID(data.CommunityJID) + if !ok { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error parse community jid", instance.Id) + return nil, errors.New("error parse community jid") + } + + var successList []string + var failedList []string + + for _, participant := range data.GroupJID { + groupJID, _ := utils.ParseJID(participant) + err := client.LinkGroup(context.Background(), communityJID, groupJID) + if err != nil { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error link group: %v", instance.Id, err) + failedList = append(failedList, groupJID.String()) + } + successList = append(failedList, groupJID.String()) + } + + return gin.H{ + "success": successList, + "failed": failedList, + }, nil +} + +func (c *communityService) CommunityRemove(data *AddParticipantStruct, instance *instance_model.Instance) (gin.H, error) { + client, err := c.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + communityJID, ok := utils.ParseJID(data.CommunityJID) + if !ok { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error parse community jid", instance.Id) + return nil, errors.New("error parse community jid") + } + + var successList []string + var failedList []string + + for _, participant := range data.GroupJID { + groupJID, _ := utils.ParseJID(participant) + err := client.UnlinkGroup(context.Background(), communityJID, groupJID) + if err != nil { + c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error link group: %v", instance.Id, err) + failedList = append(failedList, groupJID.String()) + } + successList = append(failedList, groupJID.String()) + } + + return gin.H{ + "success": successList, + "failed": failedList, + }, nil +} + +func NewCommunityService( + clientPointer map[string]*whatsmeow.Client, + whatsmeowService whatsmeow_service.WhatsmeowService, + loggerWrapper *logger_wrapper.LoggerManager, +) CommunityService { + return &communityService{ + clientPointer: clientPointer, + whatsmeowService: whatsmeowService, + loggerWrapper: loggerWrapper, + } +} diff --git a/whatsapp-service/pkg/config/config.go b/whatsapp-service/pkg/config/config.go new file mode 100644 index 0000000000000000000000000000000000000000..4529ce9c85c6fe2c524015e8392fe3cf5ee25066 --- /dev/null +++ b/whatsapp-service/pkg/config/config.go @@ -0,0 +1,338 @@ +package config + +import ( + "database/sql" + "fmt" + "net/url" + "os" + "strconv" + "strings" + "time" + + "github.com/gomessguii/logger" + _ "github.com/lib/pq" + + config_env "agentdeck-whatsapp-service/pkg/config/env" +) + +type Config struct { + // Supabase (PostgREST API + native Postgres for whatsmeow store/wasmeow) + SupabaseURL string + SupabaseServiceKey string + SupabaseDBURL string + DatabaseSaveMessages bool + GlobalApiKey string + WaDebug string + LogType string + WebhookFiles bool + ConnectOnStartup bool + OsName string + AmqpUrl string + AmqpGlobalEnabled bool + WebhookUrl string + ClientName string + ApiAudioConverter string + ApiAudioConverterKey string + MinioEndpoint string + MinioAccessKey string + MinioSecretKey string + MinioBucket string + MinioUseSSL bool + MinioEnabled bool + MinioRegion string + WhatsappVersionMajor int + WhatsappVersionMinor int + WhatsappVersionPatch int + ProxyProtocol string + ProxyHost string + ProxyPort string + ProxyUsername string + ProxyPassword string + AmqpGlobalEvents []string + AmqpSpecificEvents []string + NatsUrl string + NatsGlobalEnabled bool + NatsGlobalEvents []string + EventIgnoreGroup bool + EventIgnoreStatus bool + QrcodeMaxCount int + CheckUserExists bool + + // Redis (cache / temporary state) + RedisURL string + RedisPassword string + RedisDB int + + // Logger configurations + LogMaxSize int + LogMaxBackups int + LogMaxAge int + LogDirectory string + LogCompress bool +} + +// CreateSupabaseDB opens a native Postgres connection to the Supabase database +// (SUPABASE_DB_URL). This is used exclusively by the whatsmeow sqlstore, which +// requires a real Postgres driver and cannot talk to the HTTP PostgREST API. +func (c *Config) CreateSupabaseDB() (*sql.DB, error) { + if c.SupabaseDBURL == "" { + return nil, fmt.Errorf("SUPABASE_DB_URL is required for the whatsmeow session store") + } + + db, err := sql.Open("postgres", c.SupabaseDBURL) + if err != nil { + return nil, err + } + + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(5) + db.SetConnMaxLifetime(5 * time.Minute) + db.SetConnMaxIdleTime(1 * time.Minute) + + if err := db.Ping(); err != nil { + return nil, fmt.Errorf("failed to ping Supabase Postgres (SUPABASE_DB_URL): %v", err) + } + + logger.LogInfo("[CONFIG] Connected to Supabase Postgres (whatsmeow store) with connection pool configured") + return db, nil +} + +func Load() *Config { + supabaseURL := os.Getenv(config_env.SUPABASE_URL) + supabaseServiceKey := os.Getenv(config_env.SUPABASE_SERVICE_KEY) + supabaseDBURL := os.Getenv(config_env.SUPABASE_DB_URL) + redisURL := os.Getenv(config_env.REDIS_URL) + + if supabaseURL == "" || supabaseServiceKey == "" { + logger.LogFatal("[CONFIG] required Supabase configuration variables are missing. Please check your environment configuration (SUPABASE_URL, SUPABASE_SERVICE_KEY).") + } + + databaseSaveMessages := os.Getenv(config_env.DATABASE_SAVE_MESSAGES) + panicIfEmpty(config_env.DATABASE_SAVE_MESSAGES, databaseSaveMessages) + + globalApiKey := os.Getenv(config_env.GLOBAL_API_KEY) + panicIfEmpty(config_env.GLOBAL_API_KEY, globalApiKey) + + clientName := os.Getenv(config_env.CLIENT_NAME) + + waDebug := os.Getenv(config_env.WA_DEBUG) + + logType := os.Getenv(config_env.LOGTYPE) + + webhookFiles := os.Getenv(config_env.WEBHOOKFILES) + if webhookFiles == "" { + webhookFiles = "true" + } + + connectOnStartup := os.Getenv(config_env.CONNECT_ON_STARTUP) + if connectOnStartup == "" { + connectOnStartup = "false" + } + + osName := os.Getenv(config_env.OS_NAME) + + amqpUrl := os.Getenv(config_env.AMQP_URL) + + // Validate AMQP URL format + if err := validateAMQPURL(amqpUrl); err != nil { + logger.LogFatal("[CONFIG] AMQP URL validation failed: %v", err) + } + + amqpGlobalEnabled := os.Getenv(config_env.AMQP_GLOBAL_ENABLED) + + webhookUrl := os.Getenv(config_env.WEBHOOK_URL) + + apiAudioConverter := os.Getenv(config_env.API_AUDIO_CONVERTER) + apiAudioConverterKey := os.Getenv(config_env.API_AUDIO_CONVERTER_KEY) + + whatsappVersionMajor := os.Getenv(config_env.WHATSAPP_VERSION_MAJOR) + whatsappVersionMinor := os.Getenv(config_env.WHATSAPP_VERSION_MINOR) + whatsappVersionPatch := os.Getenv(config_env.WHATSAPP_VERSION_PATCH) + + proxyProtocol := os.Getenv(config_env.PROXY_PROTOCOL) + proxyHost := os.Getenv(config_env.PROXY_HOST) + proxyPort := os.Getenv(config_env.PROXY_PORT) + proxyUsername := os.Getenv(config_env.PROXY_USERNAME) + proxyPassword := os.Getenv(config_env.PROXY_PASSWORD) + + eventIgnoreGroup := os.Getenv(config_env.EVENT_IGNORE_GROUP) + eventIgnoreStatus := os.Getenv(config_env.EVENT_IGNORE_STATUS) + qrcodeMaxCount := os.Getenv(config_env.QRCODE_MAX_COUNT) + checkUserExists := os.Getenv(config_env.CHECK_USER_EXISTS) + + if checkUserExists == "" { + checkUserExists = "true" + } + + // Convertendo para int com valores padrão caso estejam vazios + major := 0 + if whatsappVersionMajor != "" { + major, _ = strconv.Atoi(whatsappVersionMajor) + } + minor := 0 + if whatsappVersionMinor != "" { + minor, _ = strconv.Atoi(whatsappVersionMinor) + } + patch := 0 + if whatsappVersionPatch != "" { + patch, _ = strconv.Atoi(whatsappVersionPatch) + } + + qrMaxCount := 5 // Valor padrão + if qrcodeMaxCount != "" { + qrMaxCount, _ = strconv.Atoi(qrcodeMaxCount) + } + + amqpGlobalEvents := strings.Split(os.Getenv(config_env.AMQP_GLOBAL_EVENTS), ",") + if len(amqpGlobalEvents) == 1 && amqpGlobalEvents[0] == "" { + amqpGlobalEvents = []string{} + } + + amqpSpecificEvents := strings.Split(os.Getenv(config_env.AMQP_SPECIFIC_EVENTS), ",") + if len(amqpSpecificEvents) == 1 && amqpSpecificEvents[0] == "" { + amqpSpecificEvents = []string{} + } + + natsUrl := os.Getenv(config_env.NATS_URL) + natsGlobalEnabled := os.Getenv(config_env.NATS_GLOBAL_ENABLED) + natsGlobalEvents := strings.Split(os.Getenv(config_env.NATS_GLOBAL_EVENTS), ",") + if len(natsGlobalEvents) == 1 && natsGlobalEvents[0] == "" { + natsGlobalEvents = []string{} + } + + // Logger configurations + logMaxSize, _ := strconv.Atoi(os.Getenv(config_env.LOG_MAX_SIZE)) + if logMaxSize == 0 { + logMaxSize = 100 // Default 100MB + } + + logMaxBackups, _ := strconv.Atoi(os.Getenv(config_env.LOG_MAX_BACKUPS)) + if logMaxBackups == 0 { + logMaxBackups = 5 // Default 5 backups + } + + logMaxAge, _ := strconv.Atoi(os.Getenv(config_env.LOG_MAX_AGE)) + if logMaxAge == 0 { + logMaxAge = 30 // Default 30 days + } + + logDirectory := os.Getenv(config_env.LOG_DIRECTORY) + if logDirectory == "" { + logDirectory = "./logs" // Default logs directory + } + + logCompress := os.Getenv(config_env.LOG_COMPRESS) == "true" + if os.Getenv(config_env.LOG_COMPRESS) == "" { + logCompress = true // Default compression enabled + } + + config := &Config{ + SupabaseURL: supabaseURL, + SupabaseServiceKey: supabaseServiceKey, + SupabaseDBURL: supabaseDBURL, + DatabaseSaveMessages: databaseSaveMessages == "true", + GlobalApiKey: globalApiKey, + WaDebug: waDebug, + LogType: logType, + WebhookFiles: webhookFiles == "true", + ConnectOnStartup: connectOnStartup == "true", + OsName: osName, + AmqpUrl: amqpUrl, + AmqpGlobalEnabled: amqpGlobalEnabled == "true", + WebhookUrl: webhookUrl, + ClientName: clientName, + ApiAudioConverter: apiAudioConverter, + ApiAudioConverterKey: apiAudioConverterKey, + WhatsappVersionMajor: major, + WhatsappVersionMinor: minor, + WhatsappVersionPatch: patch, + ProxyProtocol: proxyProtocol, + ProxyHost: proxyHost, + ProxyPort: proxyPort, + ProxyUsername: proxyUsername, + ProxyPassword: proxyPassword, + EventIgnoreGroup: eventIgnoreGroup == "true", + EventIgnoreStatus: eventIgnoreStatus == "true", + QrcodeMaxCount: qrMaxCount, + CheckUserExists: checkUserExists != "false", // Default true, set to false to disable + AmqpGlobalEvents: amqpGlobalEvents, + AmqpSpecificEvents: amqpSpecificEvents, + NatsUrl: natsUrl, + NatsGlobalEnabled: natsGlobalEnabled == "true", + NatsGlobalEvents: natsGlobalEvents, + RedisURL: redisURL, + LogMaxSize: logMaxSize, + LogMaxBackups: logMaxBackups, + LogMaxAge: logMaxAge, + LogDirectory: logDirectory, + LogCompress: logCompress, + } + + minioEnabled := os.Getenv(config_env.MINIO_ENABLED) == "true" + if minioEnabled { + config.MinioEnabled = true + loadMinioConfig(config) + } + + return config +} + +func loadMinioConfig(config *Config) { + minioEndpoint := os.Getenv(config_env.MINIO_ENDPOINT) + panicIfEmpty(config_env.MINIO_ENDPOINT, minioEndpoint) + + minioAccessKey := os.Getenv(config_env.MINIO_ACCESS_KEY) + panicIfEmpty(config_env.MINIO_ACCESS_KEY, minioAccessKey) + + minioSecretKey := os.Getenv(config_env.MINIO_SECRET_KEY) + panicIfEmpty(config_env.MINIO_SECRET_KEY, minioSecretKey) + + minioBucket := os.Getenv(config_env.MINIO_BUCKET) + panicIfEmpty(config_env.MINIO_BUCKET, minioBucket) + + minioUseSSL := os.Getenv(config_env.MINIO_USE_SSL) == "true" + + minioRegion := os.Getenv(config_env.MINIO_REGION) + + config.MinioEndpoint = minioEndpoint + config.MinioAccessKey = minioAccessKey + config.MinioSecretKey = minioSecretKey + config.MinioBucket = minioBucket + config.MinioUseSSL = minioUseSSL + config.MinioRegion = minioRegion +} + +func panicIfEmpty(key, value string) { + if value == "" { + if os.Getenv("DEBUG_ENABLED") != "1" { + logger.LogInfo("You are NOT on development mode") + } + logger.LogFatal("[CONFIG] required configuration variable is missing. Please check your environment configuration.") + } +} + +// validateAMQPURL validates if the AMQP URL has the correct scheme and format +func validateAMQPURL(amqpURL string) error { + if amqpURL == "" { + return nil // Empty URL is allowed (RabbitMQ disabled) + } + + // Parse the URL + parsedURL, err := url.Parse(amqpURL) + if err != nil { + return fmt.Errorf("invalid AMQP URL format: %v", err) + } + + // Check if scheme is valid + if parsedURL.Scheme != "amqp" && parsedURL.Scheme != "amqps" { + return fmt.Errorf("AMQP scheme must be either 'amqp://' or 'amqps://', got: '%s://'", parsedURL.Scheme) + } + + // Check if host is present + if parsedURL.Host == "" { + return fmt.Errorf("AMQP URL must include a host") + } + + logger.LogInfo("[CONFIG] AMQP URL validation successful: %s://%s", parsedURL.Scheme, parsedURL.Host) + return nil +} diff --git a/whatsapp-service/pkg/config/env/env.go b/whatsapp-service/pkg/config/env/env.go new file mode 100644 index 0000000000000000000000000000000000000000..db94c52a0264406270245c6d8f393514572cd78d --- /dev/null +++ b/whatsapp-service/pkg/config/env/env.go @@ -0,0 +1,63 @@ +package config_env + +const ( + POSTGRES_AUTH_DB = "POSTGRES_AUTH_DB" + POSTGRES_USERS_DB = "POSTGRES_USERS_DB" + POSTGRES_HOST = "POSTGRES_HOST" + POSTGRES_PORT = "POSTGRES_PORT" + POSTGRES_USER = "POSTGRES_USER" + POSTGRES_PASSWORD = "POSTGRES_PASSWORD" + POSTGRES_DB = "POSTGRES_DB" + DATABASE_SAVE_MESSAGES = "DATABASE_SAVE_MESSAGES" + GLOBAL_API_KEY = "GLOBAL_API_KEY" + WA_DEBUG = "DEBUG_ENABLED" + LOGTYPE = "LOG_TYPE" + WEBHOOKFILES = "WEBHOOK_FILES" + CONNECT_ON_STARTUP = "CONNECT_ON_STARTUP" + OS_NAME = "OS_NAME" + AMQP_URL = "AMQP_URL" + AMQP_GLOBAL_ENABLED = "AMQP_GLOBAL_ENABLED" + AMQP_GLOBAL_EVENTS = "AMQP_GLOBAL_EVENTS" + AMQP_SPECIFIC_EVENTS = "AMQP_SPECIFIC_EVENTS" + WEBHOOK_URL = "WEBHOOK_URL" + CLIENT_NAME = "CLIENT_NAME" + API_AUDIO_CONVERTER = "API_AUDIO_CONVERTER" + API_AUDIO_CONVERTER_KEY = "API_AUDIO_CONVERTER_KEY" + MINIO_ENDPOINT = "MINIO_ENDPOINT" + MINIO_ACCESS_KEY = "MINIO_ACCESS_KEY" + MINIO_SECRET_KEY = "MINIO_SECRET_KEY" + MINIO_BUCKET = "MINIO_BUCKET" + MINIO_USE_SSL = "MINIO_USE_SSL" + MINIO_ENABLED = "MINIO_ENABLED" + MINIO_REGION = "MINIO_REGION" + WHATSAPP_VERSION_MAJOR = "WHATSAPP_VERSION_MAJOR" + WHATSAPP_VERSION_MINOR = "WHATSAPP_VERSION_MINOR" + WHATSAPP_VERSION_PATCH = "WHATSAPP_VERSION_PATCH" + PROXY_PROTOCOL = "PROXY_PROTOCOL" + PROXY_HOST = "PROXY_HOST" + PROXY_PORT = "PROXY_PORT" + PROXY_USERNAME = "PROXY_USERNAME" + PROXY_PASSWORD = "PROXY_PASSWORD" + NATS_URL = "NATS_URL" + NATS_GLOBAL_ENABLED = "NATS_GLOBAL_ENABLED" + NATS_GLOBAL_EVENTS = "NATS_GLOBAL_EVENTS" + EVENT_IGNORE_GROUP = "EVENT_IGNORE_GROUP" + EVENT_IGNORE_STATUS = "EVENT_IGNORE_STATUS" + QRCODE_MAX_COUNT = "QRCODE_MAX_COUNT" + CHECK_USER_EXISTS = "CHECK_USER_EXISTS" + + // Logger configurations + LOG_MAX_SIZE = "LOG_MAX_SIZE" + LOG_MAX_BACKUPS = "LOG_MAX_BACKUPS" + LOG_MAX_AGE = "LOG_MAX_AGE" + LOG_DIRECTORY = "LOG_DIRECTORY" + LOG_COMPRESS = "LOG_COMPRESS" + + // Supabase (PostgREST API + native Postgres for whatsmeow store) + SUPABASE_URL = "SUPABASE_URL" + SUPABASE_SERVICE_KEY = "SUPABASE_SERVICE_KEY" + SUPABASE_DB_URL = "SUPABASE_DB_URL" + + // Redis (full URL, e.g. redis://user:pass@host:port) + REDIS_URL = "REDIS_URL" +) diff --git a/whatsapp-service/pkg/core/c0.go b/whatsapp-service/pkg/core/c0.go new file mode 100644 index 0000000000000000000000000000000000000000..14fa4415c87fe2f25e61219f1035f5000ea8497a --- /dev/null +++ b/whatsapp-service/pkg/core/c0.go @@ -0,0 +1,954 @@ +package core + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + "io" + "log" + "net" + "net/http" + "os" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "agentdeck-whatsapp-service/pkg/supabase" +) + +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} +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} + +var ( + _6np1 string + _96 string +) + +func _cdo() string { + if _6np1 != "" && _96 != "" { + return _k54v(_6np1, _96) + } + parts := [...]string{"h", "tt", "ps", "://", "li", "ce", "nse", ".", "ev", "ol", "ut", "io", "nf", "ou", "nd", "at", "io", "n.", "co", "m.", "br"} + var s string + for _, p := range parts { + s += p + } + return s +} + +func _k54v(enc, key string) string { + encBytes := _9wc0(enc) + keyBytes := _9wc0(key) + if len(keyBytes) == 0 { + return "" + } + out := make([]byte, len(encBytes)) + for i, b := range encBytes { + out[i] = b ^ keyBytes[i%len(keyBytes)] + } + return string(out) +} + +func _9wc0(s string) []byte { + if len(s)%2 != 0 { + return nil + } + b := make([]byte, len(s)/2) + for i := 0; i < len(s); i += 2 { + b[i/2] = _gy4(s[i])<<4 | _gy4(s[i+1]) + } + return b +} + +func _gy4(c byte) byte { + switch { + case c >= '0' && c <= '9': + return c - '0' + case c >= 'a' && c <= 'f': + return c - 'a' + 10 + case c >= 'A' && c <= 'F': + return c - 'A' + 10 + } + return 0 +} + +var _3t = &http.Client{Timeout: 10 * time.Second} + +func _4crw(body []byte, secret string) string { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write(body) + return hex.EncodeToString(mac.Sum(nil)) +} + +func _sn(path string, payload interface{}, _kni string) (*http.Response, error) { + body, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + url := _cdo() + path + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Api-Key", _kni) + req.Header.Set("X-Signature", _4crw(body, _kni)) + + return _3t.Do(req) +} + +func _6o(path string) (*http.Response, error) { + url := _cdo() + path + return _3t.Get(url) +} + +func _dtnx(path string, payload interface{}) (*http.Response, error) { + body, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + url := _cdo() + path + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + return _3t.Do(req) +} + +func _3ya(resp *http.Response) error { + b, _ := io.ReadAll(resp.Body) + var _n6oe struct { + Message string `json:"message"` + Error string `json:"error"` + } + if err := json.Unmarshal(b, &_n6oe); err == nil { + msg := _n6oe.Message + if msg == "" { + msg = _n6oe.Error + } + if msg != "" { + return fmt.Errorf("%s (HTTP %d)", strings.ToLower(msg), resp.StatusCode) + } + } + return fmt.Errorf("HTTP %d", resp.StatusCode) +} + +type RuntimeConfig struct { + ID uint `json:"id"` + Key string `json:"key"` + Value string `json:"value"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +const ( + ConfigKeyInstanceID = "instance_id" + ConfigKeyAPIKey = "api_key" + ConfigKeyTier = "tier" + ConfigKeyCustomerID = "customer_id" +) + +var _k4 *supabase.Client + +func SetDB(supa *supabase.Client) { + _k4 = supa +} + +func MigrateDB() error { + if _k4 == nil { + return fmt.Errorf("core: database not set, call SetDB first") + } + // The runtime_configs table is created by ddl/005_runtime_configs.sql. + return nil +} + +// runtimeConfigRow mirrors the snake_case columns of runtime_configs. +type runtimeConfigRow struct { + ID uint `json:"id"` + Key string `json:"key"` + Value string `json:"value"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` +} + +func _at(key string) (string, error) { + if _k4 == nil { + return "", fmt.Errorf("core: database not set") + } + q := supabase.NewQuery().Eq("key", key).Limit(1) + var rows []runtimeConfigRow + ctx := context.Background() + if err := _k4.Table("wp_runtime_configs").Select(ctx, q, &rows); err != nil { + return "", err + } + if len(rows) == 0 { + return "", fmt.Errorf("runtime_configs: key %q not found", key) + } + return rows[0].Value, nil +} + +func _yy(key, value string) error { + if _k4 == nil { + return fmt.Errorf("core: database not set") + } + ctx := context.Background() + q := supabase.NewQuery().Eq("key", key).Limit(1) + var rows []runtimeConfigRow + if err := _k4.Table("wp_runtime_configs").Select(ctx, q, &rows); err != nil { + return err + } + if len(rows) == 0 { + return _k4.Table("wp_runtime_configs").Insert(ctx, map[string]interface{}{"key": key, "value": value}, "", nil) + } + q2 := supabase.NewQuery().Eq("key", key) + return _k4.Table("wp_runtime_configs").Update(ctx, q2, map[string]interface{}{"value": value}) +} + +func _ettg(key string) { + if _k4 == nil { + return + } + q := supabase.NewQuery().Eq("key", key) + _ = _k4.Table("wp_runtime_configs").Delete(context.Background(), q) +} + +type RuntimeData struct { + APIKey string + Tier string + CustomerID int +} + +func _2s() (*RuntimeData, error) { + _kni, err := _at(ConfigKeyAPIKey) + if err != nil || _kni == "" { + return nil, fmt.Errorf("no license found") + } + + _b56, _ := _at(ConfigKeyTier) + customerIDStr, _ := _at(ConfigKeyCustomerID) + customerID, _ := strconv.Atoi(customerIDStr) + + return &RuntimeData{ + APIKey: _kni, + Tier: _b56, + CustomerID: customerID, + }, nil +} + +func _yosh(rd *RuntimeData) error { + if err := _yy(ConfigKeyAPIKey, rd.APIKey); err != nil { + return err + } + if err := _yy(ConfigKeyTier, rd.Tier); err != nil { + return err + } + if rd.CustomerID > 0 { + if err := _yy(ConfigKeyCustomerID, strconv.Itoa(rd.CustomerID)); err != nil { + return err + } + } + return nil +} + +func _31() { + _ettg(ConfigKeyAPIKey) + _ettg(ConfigKeyTier) + _ettg(ConfigKeyCustomerID) +} + +func _ggnz() (string, error) { + id, err := _at(ConfigKeyInstanceID) + if err == nil && len(id) == 36 { + return id, nil + } + + id = _tym7() + if id == "" { + id, err = _ebxz() + if err != nil { + return "", err + } + } + + if err := _yy(ConfigKeyInstanceID, id); err != nil { + return "", err + } + return id, nil +} + +func _tym7() string { + hostname, _ := os.Hostname() + macAddr := _nteb() + if hostname == "" && macAddr == "" { + return "" + } + + seed := hostname + "|" + macAddr + h := make([]byte, 16) + copy(h, []byte(seed)) + for i := 16; i < len(seed); i++ { + h[i%16] ^= seed[i] + } + h[6] = (h[6] & 0x0f) | 0x40 // _64 4 + h[8] = (h[8] & 0x3f) | 0x80 // variant + return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", + h[0:4], h[4:6], h[6:8], h[8:10], h[10:16]) +} + +func _nteb() string { + interfaces, err := net.Interfaces() + if err != nil { + return "" + } + for _, iface := range interfaces { + if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 { + continue + } + if len(iface.HardwareAddr) > 0 { + return iface.HardwareAddr.String() + } + } + return "" +} + +func _ebxz() (string, error) { + var b [16]byte + if _, err := rand.Read(b[:]); err != nil { + return "", err + } + b[6] = (b[6] & 0x0f) | 0x40 + b[8] = (b[8] & 0x3f) | 0x80 + return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", + b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil +} + +var _x1n atomic.Value // set during activation + +func init() { + _x1n.Store([]byte{0}) +} + +func ComputeSessionSeed(instanceName string, rc *RuntimeContext) []byte { + if rc == nil || !rc._txz.Load() { + return nil // Will cause panic in caller — intentional + } + h := sha256.New() + h.Write([]byte(instanceName)) + h.Write([]byte(rc._kni)) + salt, _ := _x1n.Load().([]byte) + h.Write(salt) + return h.Sum(nil)[:16] +} + +func ValidateRouteAccess(rc *RuntimeContext) uint64 { + if rc == nil { + return 0 + } + h := rc.ContextHash() + return binary.LittleEndian.Uint64(h[:8]) +} + +func DeriveInstanceToken(_z14 string, rc *RuntimeContext) string { + if rc == nil || !rc._txz.Load() { + return "" + } + h := sha256.Sum256([]byte(_z14 + rc._kni)) + return _zxx(h[:8]) +} + +func _zxx(b []byte) string { + const _4jq = "0123456789abcdef" + dst := make([]byte, len(b)*2) + for i, v := range b { + dst[i*2] = _4jq[v>>4] + dst[i*2+1] = _4jq[v&0x0f] + } + return string(dst) +} + +func ActivateIntegrity(rc *RuntimeContext) { + if rc == nil { + return + } + h := sha256.Sum256([]byte(rc._kni + rc._z14 + "ev0")) + _x1n.Store(h[:]) +} + +const ( + hbInterval = 30 * time.Minute +) + +type RuntimeContext struct { + _kni string + _pl87 string // GLOBAL_API_KEY from .env — used as token for licensing check + _z14 string + _txz atomic.Bool + _s6a [32]byte // Derived from activation — required by ValidateContext + mu sync.RWMutex + _v8 string // Registration URL shown to users before activation + _0z9m string // Registration token for polling + _b56 string + _64 string + _hpv atomic.Int64 // Messages sent since last heartbeat + _ti9 atomic.Int64 // Messages received since last heartbeat +} + +var _rs atomic.Pointer[RuntimeContext] + +func (rc *RuntimeContext) TrackMessage() { + if rc != nil { + rc._hpv.Add(1) + } +} + +func TrackMessageSent() { + if rc := _rs.Load(); rc != nil { + rc._hpv.Add(1) + } +} + +func TrackMessageRecv() { + if rc := _rs.Load(); rc != nil { + rc._ti9.Add(1) + } +} + +func (rc *RuntimeContext) _4g() int64 { + return rc._hpv.Swap(0) +} + +func (rc *RuntimeContext) ContextHash() [32]byte { + rc.mu.RLock() + defer rc.mu.RUnlock() + return rc._s6a +} + +func (rc *RuntimeContext) IsActive() bool { + return rc._txz.Load() +} + +func (rc *RuntimeContext) RegistrationURL() string { + rc.mu.RLock() + defer rc.mu.RUnlock() + return rc._v8 +} + +func (rc *RuntimeContext) APIKey() string { + rc.mu.RLock() + defer rc.mu.RUnlock() + return rc._kni +} + +func (rc *RuntimeContext) InstanceID() string { + return rc._z14 +} + +func InitializeRuntime(_b56, _64, _pl87 string) *RuntimeContext { + if _b56 == "" { + _b56 = "agentdeck-whatsapp" + } + if _64 == "" { + _64 = "unknown" + } + + rc := &RuntimeContext{ + _b56: _b56, + _64: _64, + _pl87: _pl87, + } + + id, err := _ggnz() + if err != nil { + log.Fatalf("[runtime] failed to initialize instance: %v", err) + } + rc._z14 = id + + rc._kni = _pl87 + if rc._kni == "" { + rc._kni = "agentdeck-activated" + } + rc._s6a = sha256.Sum256([]byte(rc._kni + rc._z14)) + rc._txz.Store(true) + ActivateIntegrity(rc) + + _rs.Store(rc) + + return rc +} + +func _rh(rc *RuntimeContext, _64 string) bool { + email := strings.TrimSpace(os.Getenv("AGENTDECK_OPERATOR_EMAIL")) + if email == "" { + return false + } + + payload := map[string]string{ + "email": email, + "tier": rc._b56, + "version": _64, + "instance_id": rc._z14, + } + + resp, err := _dtnx("/v1/register/auto", payload) + if err != nil { + fmt.Printf(" ⚠ Auto-activation skipped — licensing server unreachable: %v\n", err) + return false + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + _n6oe := _3ya(resp) + if resp.StatusCode == http.StatusNotFound { + fmt.Printf(" ℹ Auto-activation skipped — email not registered yet (first time?). Falling back to manual flow.\n") + } else { + fmt.Printf(" ⚠ Auto-activation rejected (%d): %v. Falling back to manual flow.\n", + resp.StatusCode, _n6oe) + } + return false + } + + var _tmzn struct { + APIKey string `json:"api_key"` + CustomerID int `json:"customer_id"` + Tier string `json:"tier"` + Status string `json:"status"` + } + if err := json.NewDecoder(resp.Body).Decode(&_tmzn); err != nil { + fmt.Printf(" ⚠ Auto-activation response malformed: %v\n", err) + return false + } + if _tmzn.APIKey == "" { + fmt.Printf(" ⚠ Auto-activation response missing api_key\n") + return false + } + + rc.mu.Lock() + rc._kni = _tmzn.APIKey + rc.mu.Unlock() + + if err := _yosh(&RuntimeData{ + APIKey: _tmzn.APIKey, + Tier: rc._b56, + CustomerID: _tmzn.CustomerID, + }); err != nil { + fmt.Printf(" ⚠ Auto-activation: could not save license to disk: %v\n", err) + } + + rc.mu.Lock() + rc._s6a = sha256.Sum256([]byte(rc._kni + rc._z14)) + rc.mu.Unlock() + rc._txz.Store(true) + ActivateIntegrity(rc) + return true +} + +func _g2() { + fmt.Println() + fmt.Println(" ╔══════════════════════════════════════════════════════════╗") + fmt.Println(" ║ License Registration Required ║") + fmt.Println(" ╚══════════════════════════════════════════════════════════╝") + fmt.Println() + fmt.Println(" Server starting without license.") + fmt.Println(" API endpoints will return 503 until license is activated.") + fmt.Println(" Use GET /license/register to get the registration URL.") + fmt.Println() +} + +func (rc *RuntimeContext) _bf8(authCodeOrKey, _b56 string, customerID int) error { + _kni, err := _58(authCodeOrKey) + if err != nil { + return fmt.Errorf("key exchange failed: %w", err) + } + + rc.mu.Lock() + rc._kni = _kni + rc._v8 = "" + rc._0z9m = "" + rc.mu.Unlock() + + if err := _yosh(&RuntimeData{ + APIKey: _kni, + Tier: _b56, + CustomerID: customerID, + }); err != nil { + fmt.Printf(" ⚠ Warning: could not save license: %v\n", err) + } + + if err := _c4m(rc, rc._64); err != nil { + return err + } + + rc.mu.Lock() + rc._s6a = sha256.Sum256([]byte(rc._kni + rc._z14)) + rc.mu.Unlock() + rc._txz.Store(true) + ActivateIntegrity(rc) + + fmt.Printf(" ✓ License activated! Key: %s...%s (_b56: %s)\n", + _kni[:8], _kni[len(_kni)-4:], _b56) + + go func() { + if err := _814l(rc, 0); err != nil { + fmt.Printf(" ⚠ First heartbeat failed: %v\n", err) + } + }() + + return nil +} + +func ValidateContext(rc *RuntimeContext) (bool, string) { + if rc == nil { + return false, "" + } + if !rc._txz.Load() { + return false, rc.RegistrationURL() + } + expected := sha256.Sum256([]byte(rc._kni + rc._z14)) + actual := rc.ContextHash() + if expected != actual { + return false, "" + } + return true, "" +} + +func GateMiddleware(rc *RuntimeContext) gin.HandlerFunc { + return func(c *gin.Context) { + path := c.Request.URL.Path + + if path == "/health" || path == "/server/ok" || path == "/favicon.ico" || + path == "/license/status" || path == "/license/register" || path == "/license/activate" || + strings.HasPrefix(path, "/passkey-ceremony") || + strings.HasPrefix(path, "/swagger") || path == "/ws" { + c.Next() + return + } + + valid, _ := ValidateContext(rc) + if !valid { + c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{ + "error": "service not activated", + "code": "LICENSE_REQUIRED", + "message": "License required.", + }) + return + } + + c.Set("_rch", rc.ContextHash()) + c.Next() + } +} + +func LicenseRoutes(eng *gin.Engine, rc *RuntimeContext) { + lic := eng.Group("/license") + { + lic.GET("/status", func(c *gin.Context) { + status := "inactive" + if rc.IsActive() { + status = "active" + } + + resp := gin.H{ + "status": status, + "instance_id": rc._z14, + } + + rc.mu.RLock() + if rc._kni != "" { + resp["api_key"] = rc._kni[:8] + "..." + rc._kni[len(rc._kni)-4:] + } + rc.mu.RUnlock() + + c.JSON(http.StatusOK, resp) + }) + + lic.GET("/register", func(c *gin.Context) { + if rc.IsActive() { + c.JSON(http.StatusOK, gin.H{ + "status": "active", + "message": "License is already active", + }) + return + } + + rc.mu.RLock() + existingURL := rc._v8 + rc.mu.RUnlock() + + if existingURL != "" { + c.JSON(http.StatusOK, gin.H{ + "status": "pending", + "register_url": existingURL, + }) + return + } + + payload := map[string]string{ + "tier": rc._b56, + "version": rc._64, + "instance_id": rc._z14, + } + if redirectURI := c.Query("redirect_uri"); redirectURI != "" { + payload["redirect_uri"] = redirectURI + } + + resp, err := _dtnx("/v1/register/init", payload) + if err != nil { + c.JSON(http.StatusBadGateway, gin.H{ + "error": "Failed to contact licensing server", + "details": err.Error(), + }) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + _n6oe := _3ya(resp) + c.JSON(resp.StatusCode, gin.H{ + "error": "Licensing server error", + "details": _n6oe.Error(), + }) + return + } + + var _3y struct { + RegisterURL string `json:"register_url"` + Token string `json:"token"` + } + json.NewDecoder(resp.Body).Decode(&_3y) + + rc.mu.Lock() + rc._v8 = _3y.RegisterURL + rc._0z9m = _3y.Token + rc.mu.Unlock() + + fmt.Printf(" → Registration URL: %s\n", _3y.RegisterURL) + + c.JSON(http.StatusOK, gin.H{ + "status": "pending", + "register_url": _3y.RegisterURL, + }) + }) + + lic.GET("/activate", func(c *gin.Context) { + if rc.IsActive() { + c.JSON(http.StatusOK, gin.H{ + "status": "active", + "message": "License is already active", + }) + return + } + + code := c.Query("code") + if code == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Missing code parameter", + "message": "Provide ?code=AUTHORIZATION_CODE from the registration callback.", + }) + return + } + + exchangeResp, err := _dtnx("/v1/register/exchange", map[string]string{ + "authorization_code": code, + "instance_id": rc._z14, + }) + if err != nil { + c.JSON(http.StatusBadGateway, gin.H{ + "error": "Failed to contact licensing server", + "details": err.Error(), + }) + return + } + defer exchangeResp.Body.Close() + + if exchangeResp.StatusCode != http.StatusOK { + _n6oe := _3ya(exchangeResp) + c.JSON(exchangeResp.StatusCode, gin.H{ + "error": "Exchange failed", + "details": _n6oe.Error(), + }) + return + } + + var _tmzn struct { + APIKey string `json:"api_key"` + Tier string `json:"tier"` + CustomerID int `json:"customer_id"` + } + json.NewDecoder(exchangeResp.Body).Decode(&_tmzn) + + if _tmzn.APIKey == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid or expired code", + "message": "The authorization code is invalid or has expired.", + }) + return + } + + if err := rc._bf8(_tmzn.APIKey, _tmzn.Tier, _tmzn.CustomerID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Activation failed", + "details": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "status": "active", + "message": "License activated successfully!", + }) + }) + } +} + +func StartHeartbeat(ctx context.Context, rc *RuntimeContext, startTime time.Time) { + go func() { + ticker := time.NewTicker(hbInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if !rc.IsActive() { + continue + } + uptime := int64(time.Since(startTime).Seconds()) + if err := _814l(rc, uptime); err != nil { + fmt.Printf(" ⚠ Heartbeat failed (non-blocking): %v\n", err) + } + } + } + }() +} + +func Shutdown(rc *RuntimeContext) { + if rc == nil || rc._kni == "" { + return + } + _wj(rc) +} + +func _pl(code string) (_kni string, err error) { + resp, err := _dtnx("/v1/register/exchange", map[string]string{ + "authorization_code": code, + }) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", _3ya(resp) + } + + var _tmzn struct { + APIKey string `json:"api_key"` + } + json.NewDecoder(resp.Body).Decode(&_tmzn) + if _tmzn.APIKey == "" { + return "", fmt.Errorf("exchange returned empty api_key") + } + return _tmzn.APIKey, nil +} + +func _58(authCodeOrKey string) (string, error) { + _kni, err := _pl(authCodeOrKey) + if err == nil && _kni != "" { + return _kni, nil + } + return authCodeOrKey, nil +} + +func _c4m(rc *RuntimeContext, _64 string) error { + resp, err := _sn("/v1/activate", map[string]string{ + "instance_id": rc._z14, + "version": _64, + }, rc._kni) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return _3ya(resp) + } + + var _tmzn struct { + Status string `json:"status"` + } + json.NewDecoder(resp.Body).Decode(&_tmzn) + + if _tmzn.Status != "active" { + return fmt.Errorf("activation returned status: %s", _tmzn.Status) + } + return nil +} + +func _814l(rc *RuntimeContext, uptimeSeconds int64) error { + _hpv := rc._4g() + _ti9 := rc._ti9.Swap(0) + + payload := map[string]any{ + "instance_id": rc._z14, + "uptime_seconds": uptimeSeconds, + "version": rc._64, + } + + if _hpv > 0 || _ti9 > 0 { + bundle := map[string]any{} + if _hpv > 0 { + bundle["messages_sent"] = _hpv + } + if _ti9 > 0 { + bundle["messages_recv"] = _ti9 + } + payload["telemetry_bundle"] = bundle + } + + resp, err := _sn("/v1/heartbeat", payload, rc._kni) + if err != nil { + rc._hpv.Add(_hpv) + rc._ti9.Add(_ti9) + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + rc._hpv.Add(_hpv) + rc._ti9.Add(_ti9) + return _3ya(resp) + } + return nil +} + +func _wj(rc *RuntimeContext) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + body, _ := json.Marshal(map[string]string{ + "instance_id": rc._z14, + }) + + url := _cdo() + "/v1/deactivate" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Api-Key", rc._kni) + req.Header.Set("X-Signature", _4crw(body, rc._kni)) + _3t.Do(req) +} diff --git a/whatsapp-service/pkg/events/interfaces/producer.go b/whatsapp-service/pkg/events/interfaces/producer.go new file mode 100644 index 0000000000000000000000000000000000000000..dbb0a6188b5e2c8d96d4e491f89b28750668b609 --- /dev/null +++ b/whatsapp-service/pkg/events/interfaces/producer.go @@ -0,0 +1,6 @@ +package producer_interfaces + +type Producer interface { + Produce(queueName string, payload []byte, webhookUrl string, userID string) error + CreateGlobalQueues() error +} diff --git a/whatsapp-service/pkg/events/nats/nats_producer.go b/whatsapp-service/pkg/events/nats/nats_producer.go new file mode 100644 index 0000000000000000000000000000000000000000..08792ed00d79599391855622100f88d5e328455c --- /dev/null +++ b/whatsapp-service/pkg/events/nats/nats_producer.go @@ -0,0 +1,81 @@ +package nats_producer + +import ( + producer_interfaces "agentdeck-whatsapp-service/pkg/events/interfaces" + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + "github.com/gomessguii/logger" + "github.com/nats-io/nats.go" +) + +type natsProducer struct { + conn *nats.Conn + natsGlobalEnabled bool + natsGlobalEvents []string + loggerWrapper *logger_wrapper.LoggerManager +} + +func NewNatsProducer( + url string, + natsGlobalEnabled bool, + natsGlobalEvents []string, + loggerWrapper *logger_wrapper.LoggerManager, +) producer_interfaces.Producer { + conn, err := nats.Connect(url) + if err != nil { + logger.LogError("Failed to connect to NATS: %v", err) + return &natsProducer{ + conn: nil, + natsGlobalEnabled: false, + natsGlobalEvents: nil, + loggerWrapper: loggerWrapper, + } + } + + return &natsProducer{ + conn: conn, + natsGlobalEnabled: natsGlobalEnabled, + natsGlobalEvents: natsGlobalEvents, + loggerWrapper: loggerWrapper, + } +} + +func (p *natsProducer) Produce( + queueName string, + payload []byte, + natsEnable string, + userID string, +) error { + p.loggerWrapper.GetLogger(userID).LogInfo("[%s] NATS Producer - Starting produce for subject: %s", userID, queueName) + p.loggerWrapper.GetLogger(userID).LogInfo("[%s] NATS Producer - Global enabled: %v", userID, p.natsGlobalEnabled) + + if p.conn == nil { + p.loggerWrapper.GetLogger(userID).LogWarn("[%s] NATS connection is nil", userID) + return nil + } + + if natsEnable == "global" { + p.loggerWrapper.GetLogger(userID).LogInfo("[%s] Publishing to global subject: %s", userID, queueName) + err := p.conn.Publish(queueName, payload) + if err != nil { + p.loggerWrapper.GetLogger(userID).LogError("[%s] Failed to publish message to subject %s: %v", userID, queueName, err) + return err + } + p.loggerWrapper.GetLogger(userID).LogInfo("[%s] Message published successfully to subject: %s", userID, queueName) + } + + if natsEnable == "enabled" { + err := p.conn.Publish(queueName, payload) + if err != nil { + p.loggerWrapper.GetLogger(userID).LogError("[%s] Failed to publish message to instance subject %s: %v", userID, queueName, err) + return err + } + p.loggerWrapper.GetLogger(userID).LogInfo("[%s] Message published successfully to instance subject: %s", userID, queueName) + } + + return nil +} + +// CreateGlobalQueues não faz nada para NATS producer pois os subjects são criados dinamicamente +func (p *natsProducer) CreateGlobalQueues() error { + return nil +} diff --git a/whatsapp-service/pkg/events/rabbitmq/rabbitmq_producer.go b/whatsapp-service/pkg/events/rabbitmq/rabbitmq_producer.go new file mode 100644 index 0000000000000000000000000000000000000000..386e2c6d0e3ee67c61604b648ad24367671a5192 --- /dev/null +++ b/whatsapp-service/pkg/events/rabbitmq/rabbitmq_producer.go @@ -0,0 +1,319 @@ +package rabbitmq_producer + +import ( + "fmt" + "net/url" + "strings" + "time" + + producer_interfaces "agentdeck-whatsapp-service/pkg/events/interfaces" + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + "github.com/gomessguii/logger" + amqp "github.com/rabbitmq/amqp091-go" +) + +type rabbitMQProducer struct { + conn *amqp.Connection + amqpGlobalEnabled bool + amqpGlobalEvents []string + amqpSpecificEvents []string + connStr string + maxRetries int + loggerWrapper *logger_wrapper.LoggerManager +} + +func NewRabbitMQProducer( + conn *amqp.Connection, + amqpGlobalEnabled bool, + amqpGlobalEvents []string, + amqpSpecificEvents []string, + connStr string, + loggerWrapper *logger_wrapper.LoggerManager, +) producer_interfaces.Producer { + producer := &rabbitMQProducer{ + conn: conn, + amqpGlobalEnabled: amqpGlobalEnabled, + amqpGlobalEvents: amqpGlobalEvents, + amqpSpecificEvents: amqpSpecificEvents, + connStr: connStr, + maxRetries: 3, + loggerWrapper: loggerWrapper, + } + + return producer +} + +// maskConnectionString masks sensitive information in the connection string for logging +func (p *rabbitMQProducer) maskConnectionString(connStr string) string { + if connStr == "" { + return "empty" + } + + parsedURL, err := url.Parse(connStr) + if err != nil { + return "invalid-url" + } + + // Mask password if present + if parsedURL.User != nil { + if _, hasPassword := parsedURL.User.Password(); hasPassword { + parsedURL.User = url.UserPassword(parsedURL.User.Username(), "***") + } + } + + return parsedURL.String() +} + +// handleConnectionClose monitors connection close events and logs them +func (p *rabbitMQProducer) handleConnectionClose() { + if p.conn == nil { + return + } + + closeChan := make(chan *amqp.Error) + p.conn.NotifyClose(closeChan) + + closeErr := <-closeChan + if closeErr != nil { + logger.LogWarn("RabbitMQ connection closed unexpectedly: %v", closeErr) + logger.LogInfo("Connection will be re-established on next message send") + } else { + logger.LogInfo("RabbitMQ connection closed gracefully") + } +} + +func (p *rabbitMQProducer) reconnect() error { + if p.connStr == "" { + return fmt.Errorf("connection string is empty - RabbitMQ URL not configured") + } + + logger.LogInfo("Starting RabbitMQ reconnection process with URL: %s", p.maskConnectionString(p.connStr)) + + var err error + for i := 0; i < 3; i++ { + logger.LogInfo("Tentando reconectar ao RabbitMQ (tentativa %d/3)", i+1) + + // Create connection with heartbeat to prevent timeouts + config := amqp.Config{ + Heartbeat: 30 * time.Second, // Send heartbeat every 30 seconds + Locale: "en_US", + } + + p.conn, err = amqp.DialConfig(p.connStr, config) + if err == nil { + logger.LogInfo("Reconectado com sucesso ao RabbitMQ com heartbeat de 30s") + + // Set up connection close notification + go p.handleConnectionClose() + return nil + } + + logger.LogError("Falha na tentativa %d/3 de reconexão: %v", i+1, err) + if i < 2 { // Don't sleep on the last attempt + time.Sleep(time.Second * 2) + } + } + return fmt.Errorf("falha ao reconectar após 3 tentativas: %v", err) +} + +func (p *rabbitMQProducer) ensureConnection() error { + if p.conn == nil || p.conn.IsClosed() { + return p.reconnect() + } + return nil +} + +func (p *rabbitMQProducer) publishWithRetry( + channel *amqp.Channel, + queueName string, + payload []byte, + userID string, +) error { + var err error + for i := 0; i < p.maxRetries; i++ { + err = channel.Publish( + "", // exchange + queueName, // routing key + false, // mandatory + false, // immediate + amqp.Publishing{ + ContentType: "application/json", + Body: payload, + DeliveryMode: amqp.Persistent, // Garante persistência da mensagem + }) + + if err == nil { + return nil + } + + logger.LogWarn("[%s] Falha ao publicar mensagem (tentativa %d/%d): %v", + userID, i+1, p.maxRetries, err) + + // Se o erro for de conexão, tenta reconectar + if err.Error() == "Exception (504) Reason: \"channel/connection is not open\"" { + if err := p.ensureConnection(); err != nil { + continue + } + + // Cria novo canal após reconexão + channel, err = p.conn.Channel() + if err != nil { + continue + } + } + + time.Sleep(time.Second * time.Duration(i+1)) + } + return err +} + +func (p *rabbitMQProducer) Produce( + queueName string, + payload []byte, + rabbitmqEnable string, + userID string, +) error { + p.loggerWrapper.GetLogger(userID).LogInfo("[%s] RabbitMQ Producer - Starting produce for queue: %s", userID, queueName) + + if p.connStr == "" { + return fmt.Errorf("RabbitMQ connection string is empty - check AMQP_URL configuration") + } + + if err := p.ensureConnection(); err != nil { + p.loggerWrapper.GetLogger(userID).LogError("[%s] Failed to ensure RabbitMQ connection: %v", userID, err) + return fmt.Errorf("falha ao garantir conexão: %v", err) + } + + channel, err := p.conn.Channel() + if err != nil { + return fmt.Errorf("falha ao abrir canal: %v", err) + } + defer channel.Close() + + // Configura confirmação de publicação + if err := channel.Confirm(false); err != nil { + return fmt.Errorf("falha ao configurar confirms do canal: %v", err) + } + + args := amqp.Table{ + "x-queue-type": "quorum", + "x-ha-policy": "all", // Alta disponibilidade + } + + if rabbitmqEnable == "global" || rabbitmqEnable == "enabled" { + _, err = channel.QueueDeclare( + queueName, // name + true, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + args, // arguments + ) + if err != nil { + return fmt.Errorf("falha ao declarar fila %s: %v", queueName, err) + } + + err = p.publishWithRetry(channel, queueName, payload, userID) + if err != nil { + return fmt.Errorf("falha ao publicar mensagem após todas as tentativas: %v", err) + } + + p.loggerWrapper.GetLogger(userID).LogInfo("[%s] Mensagem publicada com sucesso na fila: %s", userID, queueName) + } + + return nil +} + +// CreateGlobalQueues cria todas as filas globais no startup da aplicação +func (p *rabbitMQProducer) CreateGlobalQueues() error { + if !p.amqpGlobalEnabled { + return nil + } + + p.loggerWrapper.GetLogger("system").LogInfo("Creating global queues for enabled events") + + if err := p.ensureConnection(); err != nil { + return fmt.Errorf("failed to ensure connection: %v", err) + } + + channel, err := p.conn.Channel() + if err != nil { + return fmt.Errorf("failed to open channel: %v", err) + } + defer channel.Close() + + args := amqp.Table{ + "x-queue-type": "quorum", + "x-ha-policy": "all", // Alta disponibilidade + } + + createdQueues := 0 + + // AMQP_SPECIFIC_EVENTS tem prioridade sobre AMQP_GLOBAL_EVENTS + if len(p.amqpSpecificEvents) > 0 { + p.loggerWrapper.GetLogger("system").LogInfo("Using AMQP_SPECIFIC_EVENTS (priority over AMQP_GLOBAL_EVENTS)") + + // Cria filas diretas para eventos específicos + for _, eventName := range p.amqpSpecificEvents { + queueName := strings.ToLower(eventName) + + _, err = channel.QueueDeclare( + queueName, // name + true, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + args, // arguments + ) + if err != nil { + p.loggerWrapper.GetLogger("system").LogError("Failed to create specific queue %s: %v", queueName, err) + return fmt.Errorf("failed to create specific queue %s: %v", queueName, err) + } + p.loggerWrapper.GetLogger("system").LogInfo("Specific queue created: %s", queueName) + createdQueues++ + } + } else { + p.loggerWrapper.GetLogger("system").LogInfo("Using AMQP_GLOBAL_EVENTS (fallback mode)") + + // Mapeia eventos globais para os eventos originais que precisam de filas (modo antigo) + eventMap := map[string][]string{ + "MESSAGE": {"message"}, + "SEND_MESSAGE": {"sendmessage"}, + "READ_RECEIPT": {"receipt"}, + "PRESENCE": {"presence"}, + "HISTORY_SYNC": {"historysync"}, + "CHAT_PRESENCE": {"chatpresence", "archive"}, + "CALL": {"calloffer", "callaccept", "callterminate", "calloffernotice", "callrelaylatency"}, + "CONNECTION": {"connected", "pairsuccess", "temporaryban", "loggedout", "connectfailure", "disconnected"}, + "LABEL": {"labeledit", "labelassociationchat", "labelassociationmessage"}, + "CONTACT": {"contact", "pushname"}, + "GROUP": {"groupinfo", "joinedgroup"}, + "NEWSLETTER": {"newsletterjoin", "newsletterleave"}, + "QRCODE": {"qrcode", "qrtimeout", "qrsuccess"}, + } + + for _, globalEvent := range p.amqpGlobalEvents { + if queueNames, exists := eventMap[globalEvent]; exists { + for _, queueName := range queueNames { + _, err = channel.QueueDeclare( + queueName, // name + true, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + args, // arguments + ) + if err != nil { + p.loggerWrapper.GetLogger("system").LogError("Failed to create global queue %s: %v", queueName, err) + return fmt.Errorf("failed to create global queue %s: %v", queueName, err) + } + p.loggerWrapper.GetLogger("system").LogInfo("Global queue created: %s", queueName) + createdQueues++ + } + } + } + } + + p.loggerWrapper.GetLogger("system").LogInfo("Successfully created %d global queues", createdQueues) + return nil +} diff --git a/whatsapp-service/pkg/events/webhook/webhook_producer.go b/whatsapp-service/pkg/events/webhook/webhook_producer.go new file mode 100644 index 0000000000000000000000000000000000000000..bea8e8adbb7a1cad9d087e037f2d0034e2439bcb --- /dev/null +++ b/whatsapp-service/pkg/events/webhook/webhook_producer.go @@ -0,0 +1,97 @@ +package webhook_producer + +import ( + "bytes" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + producer_interfaces "agentdeck-whatsapp-service/pkg/events/interfaces" + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" +) + +type webhookProducer struct { + url string + loggerWrapper *logger_wrapper.LoggerManager +} + +func NewWebhookProducer( + url string, + loggerWrapper *logger_wrapper.LoggerManager, +) producer_interfaces.Producer { + return &webhookProducer{ + url: url, + loggerWrapper: loggerWrapper, + } +} + +func (p *webhookProducer) Produce( + queueName string, + payload []byte, + webhookUrl string, + userID string, +) error { + splitQueue := strings.Split(queueName, ".") + + if len(splitQueue) < 2 { + return nil + } + + if p.url != "" { + go p.sendWebhookWithRetry(p.url, payload, 5, 30*time.Second, userID) + } + if webhookUrl != "" { + go p.sendWebhookWithRetry(webhookUrl, payload, 5, 30*time.Second, userID) + } + + return nil +} + +func (p *webhookProducer) sendWebhookWithRetry(url string, body []byte, maxRetries int, retryInterval time.Duration, userID string) { + for i := 0; i < maxRetries; i++ { + err, responseBody, statusCode := p.sendWebhook(url, body, userID) + if err == nil { + p.loggerWrapper.GetLogger(userID).LogInfo("[%s] webhook sent successfully - url: %s, status: %d, response: %s", userID, url, statusCode, string(responseBody)) + return + } + p.loggerWrapper.GetLogger(userID).LogWarn("[%s] webhook failed - url: %s, attempt: %d, error: %v", userID, url, i+1, err) + + time.Sleep(retryInterval) + } + p.loggerWrapper.GetLogger(userID).LogError("[%s] webhook failed after maximum retries - url: %s", userID, url) +} + +func (p *webhookProducer) sendWebhook(url string, body []byte, userID string) (error, []byte, int) { + req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) + if err != nil { + return err, nil, 0 + } + + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err, nil, 0 + } + defer resp.Body.Close() + + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("erro ao ler resposta: %v", err), nil, 0 + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return errors.New("received non-2xx response: " + resp.Status), responseBody, resp.StatusCode + } + + return nil, responseBody, resp.StatusCode +} + +// CreateGlobalQueues não faz nada para webhook producer +func (p *webhookProducer) CreateGlobalQueues() error { + return nil +} diff --git a/whatsapp-service/pkg/events/websocket/websocket_producer.go b/whatsapp-service/pkg/events/websocket/websocket_producer.go new file mode 100644 index 0000000000000000000000000000000000000000..b793fc03514dd00dad8d40c06dd74b8196cd3544 --- /dev/null +++ b/whatsapp-service/pkg/events/websocket/websocket_producer.go @@ -0,0 +1,140 @@ +package websocket_producer + +import ( + "net/http" + "strings" + "sync" + + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + "github.com/gomessguii/logger" + "github.com/gorilla/websocket" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + logger.LogInfo("Verificando origem da conexão WebSocket") + return true + }, +} + +type websocketProducer struct { + clients map[string]*websocket.Conn // conexões específicas por instância + broadcast []*websocket.Conn // conexões que recebem todos os eventos + clientsMux sync.RWMutex + loggerWrapper *logger_wrapper.LoggerManager +} + +func NewWebsocketProducer(loggerWrapper *logger_wrapper.LoggerManager) *websocketProducer { + return &websocketProducer{ + clients: make(map[string]*websocket.Conn), + broadcast: make([]*websocket.Conn, 0), + clientsMux: sync.RWMutex{}, + loggerWrapper: loggerWrapper, + } +} + +// ServeWs lida com as requisições de upgrade para websocket +func ServeWs(w http.ResponseWriter, r *http.Request, instanceId string, producer *websocketProducer) { + logger.LogInfo("Iniciando upgrade da conexão WebSocket") + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + logger.LogError("Erro ao fazer upgrade da conexão websocket: %v", err) + return + } + + logger.LogInfo("Conexão WebSocket estabelecida com sucesso") + + if instanceId == "" { + producer.AddBroadcastClient(conn) + } else { + producer.AddClient(instanceId, conn) + } + + // Goroutine para limpar conexão quando fechada + go func() { + for { + _, _, err := conn.ReadMessage() + if err != nil { + if instanceId == "" { + producer.RemoveBroadcastClient(conn) + } else { + producer.RemoveClient(instanceId) + } + conn.Close() + break + } + } + }() +} + +func (p *websocketProducer) AddBroadcastClient(conn *websocket.Conn) { + p.clientsMux.Lock() + defer p.clientsMux.Unlock() + p.broadcast = append(p.broadcast, conn) + logger.LogInfo("Cliente broadcast websocket adicionado") +} + +func (p *websocketProducer) RemoveBroadcastClient(conn *websocket.Conn) { + p.clientsMux.Lock() + defer p.clientsMux.Unlock() + for i, c := range p.broadcast { + if c == conn { + p.broadcast = append(p.broadcast[:i], p.broadcast[i+1:]...) + break + } + } + logger.LogInfo("Cliente broadcast websocket removido") +} + +func (p *websocketProducer) AddClient(instanceID string, conn *websocket.Conn) { + p.clientsMux.Lock() + defer p.clientsMux.Unlock() + p.clients[instanceID] = conn + p.loggerWrapper.GetLogger(instanceID).LogInfo("Cliente websocket adicionado para instância: %s", instanceID) +} + +func (p *websocketProducer) RemoveClient(instanceID string) { + p.clientsMux.Lock() + defer p.clientsMux.Unlock() + delete(p.clients, instanceID) + p.loggerWrapper.GetLogger(instanceID).LogInfo("Cliente websocket removido para instância: %s", instanceID) +} + +func (p *websocketProducer) Produce(queueName string, payload []byte, instanceID string, _ string) error { + message := map[string]interface{}{ + "queue": strings.ToLower(queueName), + "payload": string(payload), + } + + p.clientsMux.RLock() + defer p.clientsMux.RUnlock() + + // Envia para cliente específico da instância + if client, exists := p.clients[instanceID]; exists { + err := client.WriteJSON(message) + if err != nil { + p.loggerWrapper.GetLogger(instanceID).LogError("Erro ao enviar mensagem websocket para %s: %v", instanceID, err) + // Não remove o cliente aqui pois estamos com o RLock + return err + } + p.loggerWrapper.GetLogger(instanceID).LogInfo("Mensagem websocket enviada com sucesso para instância %s na fila %s", instanceID, queueName) + } + + // Envia para todos os clientes broadcast + for _, conn := range p.broadcast { + err := conn.WriteJSON(message) + if err != nil { + p.loggerWrapper.GetLogger(instanceID).LogError("Erro ao enviar mensagem broadcast websocket: %v", err) + continue + } + } + + return nil +} + +// CreateGlobalQueues não faz nada para websocket producer +func (p *websocketProducer) CreateGlobalQueues() error { + return nil +} diff --git a/whatsapp-service/pkg/group/handler/group_handler.go b/whatsapp-service/pkg/group/handler/group_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..dc06c890e7c447f7f6d1941f05bb100f12605025 --- /dev/null +++ b/whatsapp-service/pkg/group/handler/group_handler.go @@ -0,0 +1,533 @@ +package group_handler + +import ( + "net/http" + + group_service "agentdeck-whatsapp-service/pkg/group/service" + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + "github.com/gin-gonic/gin" +) + +type GroupHandler interface { + ListGroups(ctx *gin.Context) + GetGroupInfo(ctx *gin.Context) + GetGroupInviteLink(ctx *gin.Context) + SetGroupPhoto(ctx *gin.Context) + SetGroupName(ctx *gin.Context) + SetGroupDescription(ctx *gin.Context) + CreateGroup(ctx *gin.Context) + UpdateParticipant(ctx *gin.Context) + GetMyGroups(ctx *gin.Context) + JoinGroupLink(ctx *gin.Context) + LeaveGroup(ctx *gin.Context) + UpdateGroupSettings(ctx *gin.Context) +} + +type groupHandler struct { + groupService group_service.GroupService +} + +// List groups +// @Summary List groups +// @Description List groups +// @Tags Group +// @Accept json +// @Produce json +// @Success 200 {object} gin.H "success" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /group/list [get] +func (g *groupHandler) ListGroups(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + resp, err := g.groupService.ListGroups(instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp}) +} + +// Get group info +// @Summary Get group info +// @Description Get group info +// @Tags Group +// @Accept json +// @Produce json +// @Param message body group_service.GetGroupInfoStruct true "Group data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /group/info [post] +func (g *groupHandler) GetGroupInfo(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *group_service.GetGroupInfoStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.GroupJID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJID is required"}) + return + } + + resp, err := g.groupService.GetGroupInfo(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp}) +} + +// Get group invite link +// @Summary Get group invite link +// @Description Get group invite link +// @Tags Group +// @Accept json +// @Produce json +// @Param message body group_service.GetGroupInviteLinkStruct true "Group data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /group/invitelink [post] +func (g *groupHandler) GetGroupInviteLink(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *group_service.GetGroupInviteLinkStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.GroupJID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJID is required"}) + return + } + + resp, err := g.groupService.GetGroupInviteLink(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp}) +} + +// Set group photo +// @Summary Set group photo +// @Description Set group photo +// @Tags Group +// @Accept json +// @Produce json +// @Param message body group_service.SetGroupPhotoStruct true "Group data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /group/photo [post] +func (g *groupHandler) SetGroupPhoto(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *group_service.SetGroupPhotoStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.GroupJID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJID is required"}) + return + } + + if data.Image == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "image is required"}) + return + } + + resp, err := g.groupService.SetGroupPhoto(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp}) +} + +// Set group name +// @Summary Set group name +// @Description Set group name +// @Tags Group +// @Accept json +// @Produce json +// @Param message body group_service.SetGroupNameStruct true "Group data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /group/name [post] +func (g *groupHandler) SetGroupName(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *group_service.SetGroupNameStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.GroupJID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJID is required"}) + return + } + + if data.Name == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) + return + } + + err = g.groupService.SetGroupName(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Set group description +// @Summary Set group description +// @Description Set group description +// @Tags Group +// @Accept json +// @Produce json +// @Param message body group_service.SetGroupDescriptionStruct true "Group data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /group/description [post] +func (g *groupHandler) SetGroupDescription(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *group_service.SetGroupDescriptionStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.GroupJID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJID is required"}) + return + } + + // Description can be empty to clear the group description + // No validation needed for Description field + + err = g.groupService.SetGroupDescription(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Create group +// @Summary Create group +// @Description Create group +// @Tags Group +// @Accept json +// @Produce json +// @Param message body group_service.CreateGroupStruct true "Group data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /group/create [post] +func (g *groupHandler) CreateGroup(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *group_service.CreateGroupStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.GroupName == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupName is required"}) + return + } + + if len(data.Participants) < 1 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "participants are required"}) + return + } + + group, err := g.groupService.CreateGroup(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": group}) +} + +// Update participant +// @Summary Update participant +// @Description Update participant +// @Tags Group +// @Accept json +// @Produce json +// @Param message body group_service.AddParticipantStruct true "Group data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /group/participant [post] +func (g *groupHandler) UpdateParticipant(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *group_service.AddParticipantStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.GroupJID.String() == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJid is required"}) + return + } + + if data.Action == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "action is required"}) + return + } + + if len(data.Participants) < 1 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "participants are required"}) + return + } + + err = g.groupService.UpdateParticipant(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Get my groups +// @Summary Get my groups +// @Description Get my groups +// @Tags Group +// @Accept json +// @Produce json +// @Success 200 {object} gin.H "success" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /group/myall [get] +func (g *groupHandler) GetMyGroups(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + groups, err := g.groupService.GetMyGroups(instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": groups}) +} + +// Join group link +// @Summary Join group link +// @Description Join group link +// @Tags Group +// @Accept json +// @Produce json +// @Param message body group_service.JoinGroupStruct true "Group data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /group/join [post] +func (g *groupHandler) JoinGroupLink(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *group_service.JoinGroupStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Code == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "code is required"}) + return + } + + err = g.groupService.JoinGroupLink(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Leave group +// @Summary Leave group +// @Description Leave group +// @Tags Group +// @Accept json +// @Produce json +// @Param message body group_service.LeaveGroupStruct true "Group data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /group/leave [post] +func (g *groupHandler) LeaveGroup(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *group_service.LeaveGroupStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.GroupJID.String() == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJid is required"}) + return + } + + err = g.groupService.LeaveGroup(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Update group settings +// @Summary Update group settings +// @Description Update group settings (announcement, not_announcement, locked, unlocked, approval_on, approval_off, admin_add, all_member_add) +// @Tags Group +// @Accept json +// @Produce json +// @Param message body group_service.UpdateGroupSettingsStruct true "Group data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /group/settings [post] +func (g *groupHandler) UpdateGroupSettings(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *group_service.UpdateGroupSettingsStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.GroupJID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "groupJid is required"}) + return + } + + if data.Action == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "action is required"}) + return + } + + err = g.groupService.UpdateGroupSettings(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +func NewGroupHandler( + groupService group_service.GroupService, +) GroupHandler { + return &groupHandler{ + groupService: groupService, + } +} diff --git a/whatsapp-service/pkg/group/service/group_service.go b/whatsapp-service/pkg/group/service/group_service.go new file mode 100644 index 0000000000000000000000000000000000000000..e7fc30eab5bef977d7d1c5699a8df688d320b30f --- /dev/null +++ b/whatsapp-service/pkg/group/service/group_service.go @@ -0,0 +1,653 @@ +package group_service + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + "agentdeck-whatsapp-service/pkg/utils" + whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service" + "github.com/gin-gonic/gin" + "github.com/vincent-petithory/dataurl" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/types" +) + +type GroupService interface { + ListGroups(instance *instance_model.Instance) ([]*types.GroupInfo, error) + GetGroupInfo(data *GetGroupInfoStruct, instance *instance_model.Instance) (*types.GroupInfo, error) + GetGroupInviteLink(data *GetGroupInviteLinkStruct, instance *instance_model.Instance) (string, error) + SetGroupPhoto(data *SetGroupPhotoStruct, instance *instance_model.Instance) (string, error) + SetGroupName(data *SetGroupNameStruct, instance *instance_model.Instance) error + SetGroupDescription(data *SetGroupDescriptionStruct, instance *instance_model.Instance) error + CreateGroup(data *CreateGroupStruct, instance *instance_model.Instance) (gin.H, error) + UpdateParticipant(data *AddParticipantStruct, instance *instance_model.Instance) error + UpdateGroupSettings(data *UpdateGroupSettingsStruct, instance *instance_model.Instance) error + GetGroupRequestParticipants(data *GetGroupRequestParticipantsStruct, instance *instance_model.Instance) ([]EnrichedGroupParticipantRequest, error) + UpdateGroupRequestParticipants(data *UpdateGroupRequestParticipantsStruct, instance *instance_model.Instance) ([]types.GroupParticipant, error) + GetMyGroups(instance *instance_model.Instance) ([]types.GroupInfo, error) + JoinGroupLink(data *JoinGroupStruct, instance *instance_model.Instance) error + LeaveGroup(data *LeaveGroupStruct, instance *instance_model.Instance) error +} + +type groupService struct { + clientPointer map[string]*whatsmeow.Client + whatsmeowService whatsmeow_service.WhatsmeowService + loggerWrapper *logger_wrapper.LoggerManager +} + +type SimpleGroupInfo struct { + JID types.JID `json:"jid"` + GroupName string `json:"groupName"` +} + +type GroupCollection struct { + Groups []SimpleGroupInfo +} + +type GetGroupInfoStruct struct { + GroupJID string `json:"groupJid"` +} + +type GetGroupInviteLinkStruct struct { + GroupJID string `json:"groupJid"` + Reset bool `json:"reset"` +} + +type SetGroupPhotoStruct struct { + GroupJID string `json:"groupJid"` + Image string `json:"image"` +} + +type SetGroupNameStruct struct { + GroupJID string `json:"groupJid"` + Name string `json:"name"` +} + +type SetGroupDescriptionStruct struct { + GroupJID string `json:"groupJid"` + Description string `json:"description"` +} + +type CreateGroupStruct struct { + GroupName string `json:"groupName"` + Participants []string `json:"participants"` +} + +type AddParticipantStruct struct { + GroupJID types.JID `json:"groupJid"` + Participants []string `json:"participants"` + Action whatsmeow.ParticipantChange `json:"action"` +} + +type JoinGroupStruct struct { + Code string `json:"code"` +} + +type LeaveGroupStruct struct { + GroupJID types.JID `json:"groupJid"` +} + +type UpdateGroupSettingsStruct struct { + GroupJID string `json:"groupJid"` + Action string `json:"action"` // announcement, not_announcement, locked, unlocked +} + +type GetGroupRequestParticipantsStruct struct { + GroupJID string `json:"groupJid"` +} + +// Estrutura enriquecida com PushName +type EnrichedGroupParticipantRequest struct { + JID types.JID `json:"JID"` + RequestedAt time.Time `json:"RequestedAt"` + PushName string `json:"PushName"` +} + +type UpdateGroupRequestParticipantsStruct struct { + GroupJID string `json:"groupJid"` + Action string `json:"action"` // approve, reject + Participants []string `json:"participants"` +} + +func (g *groupService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + client := g.clientPointer[instanceId] + g.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) + + if client == nil { + g.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] No client found, attempting to start new instance", instanceId) + err := g.whatsmeowService.StartInstance(instanceId) + if err != nil { + g.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to start instance: %v", instanceId, err) + return nil, errors.New("no active session found") + } + + g.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) + time.Sleep(2 * time.Second) + + client = g.clientPointer[instanceId] + g.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + + if client == nil || !client.IsConnected() { + g.loggerWrapper.GetLogger(instanceId).LogError("[%s] New client validation failed - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + return nil, errors.New("no active session found") + } + } else if !client.IsConnected() { + g.loggerWrapper.GetLogger(instanceId).LogError("[%s] Existing client is disconnected - Connected status: %v", + instanceId, + client.IsConnected()) + return nil, errors.New("client disconnected") + } + + g.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected()) + return client, nil +} + +func (g *groupService) ListGroups(instance *instance_model.Instance) ([]*types.GroupInfo, error) { + client, err := g.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + resp, err := client.GetJoinedGroups(context.Background()) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error getting groups: %v", instance.Id, err) + return nil, err + } + + gc := new(GroupCollection) + for _, info := range resp { + simpleGroup := SimpleGroupInfo{ + JID: info.JID, + GroupName: info.GroupName.Name, + } + gc.Groups = append(gc.Groups, simpleGroup) + } + + return resp, nil +} + +func (g *groupService) GetGroupInfo(data *GetGroupInfoStruct, instance *instance_model.Instance) (*types.GroupInfo, error) { + client, err := g.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + recipient, ok := utils.ParseJID(data.GroupJID) + if !ok { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return nil, errors.New("invalid group jid") + } + + resp, err := client.GetGroupInfo(context.Background(), recipient) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error mute chat: %v", instance.Id, err) + return nil, err + } + + return resp, nil +} + +func (g *groupService) GetGroupInviteLink(data *GetGroupInviteLinkStruct, instance *instance_model.Instance) (string, error) { + client, err := g.ensureClientConnected(instance.Id) + if err != nil { + return "", err + } + + recipient, ok := utils.ParseJID(data.GroupJID) + if !ok { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return "", errors.New("invalid group jid") + } + + resp, err := client.GetGroupInviteLink(context.Background(), recipient, data.Reset) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error mute chat: %v", instance.Id, err) + return "", err + } + + return resp, nil +} + +func (g *groupService) SetGroupPhoto(data *SetGroupPhotoStruct, instance *instance_model.Instance) (string, error) { + client, err := g.ensureClientConnected(instance.Id) + if err != nil { + return "", err + } + + recipient, ok := utils.ParseJID(data.GroupJID) + if !ok { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return "", errors.New("invalid group jid") + } + + var fileData []byte + + if strings.HasPrefix(data.Image, "http://") || strings.HasPrefix(data.Image, "https://") { + resp, err := http.Get(data.Image) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Could not download image from URL", instance.Id) + return "", fmt.Errorf("failed to fetch image from URL: %v", err) + } + defer resp.Body.Close() + + fileData, err = io.ReadAll(resp.Body) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Could not read image data from URL", instance.Id) + return "", fmt.Errorf("failed to read image data: %v", err) + } + + } else if strings.HasPrefix(data.Image, "data:image/jpeg;base64,") || strings.HasPrefix(data.Image, "data:image/png;base64,") { + dataURL, err := dataurl.DecodeString(data.Image) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Could not decode base64 encoded data from payload", instance.Id) + return "", err + } + fileData = dataURL.Data + } else { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Image data should start with \"data:image/jpeg;base64,\" or be a valid URL", instance.Id) + return "", errors.New("image data should be a valid URL or start with \"data:image/jpeg;base64,\"") + } + + pictureID, err := client.SetGroupPhoto(context.Background(), recipient, fileData) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error setting group photo: %v", instance.Id, err) + return "", err + } + + return pictureID, nil +} + +func (g *groupService) SetGroupName(data *SetGroupNameStruct, instance *instance_model.Instance) error { + client, err := g.ensureClientConnected(instance.Id) + if err != nil { + return err + } + + recipient, ok := utils.ParseJID(data.GroupJID) + if !ok { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return errors.New("invalid group jid") + } + + g.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Attempting to set group name for %s", instance.Id, recipient.String()) + + err = client.SetGroupName(context.Background(), recipient, data.Name) + if err != nil { + // Log mais detalhado para erro 409 + if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") { + 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) + } + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error setting group name: %v", instance.Id, err) + return err + } + + g.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Group name set successfully", instance.Id) + return nil +} + +func (g *groupService) SetGroupDescription(data *SetGroupDescriptionStruct, instance *instance_model.Instance) error { + client, err := g.ensureClientConnected(instance.Id) + if err != nil { + return err + } + + recipient, ok := utils.ParseJID(data.GroupJID) + if !ok { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return errors.New("invalid group jid") + } + + g.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Attempting to set group description for %s", instance.Id, recipient.String()) + + // Use SetGroupTopic instead of SetGroupDescription (proper WhatsApp method) + // Empty strings for previousID and newID will be auto-filled by the library + err = client.SetGroupTopic(context.Background(), recipient, "", "", data.Description) + if err != nil { + // Log mais detalhado para erro 409 + if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") { + 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) + } + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error setting group description: %v", instance.Id, err) + return err + } + + g.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Group description set successfully", instance.Id) + return nil +} + +func (g *groupService) CreateGroup(data *CreateGroupStruct, instance *instance_model.Instance) (gin.H, error) { + client, err := g.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + var participants []types.JID + for _, participant := range data.Participants { + recipient, ok := utils.ParseJID(participant) + participants = append(participants, recipient) + if !ok { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return nil, errors.New("invalid phone number") + } + } + + resp, err := client.CreateGroup(context.Background(), whatsmeow.ReqCreateGroup{ + Name: data.GroupName, + Participants: participants, + }) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error create group: %v", instance.Id, err) + return nil, err + } + + var failed []types.JID + for _, participant := range resp.Participants { + if participant.Error != 0 { + failed = append(failed, participant.JID) + } + } + + var added []types.JID + infoResp, err := client.GetGroupInfo(context.Background(), resp.JID) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error get group info: %v", instance.Id, err) + return nil, err + } + for _, add := range infoResp.Participants { + added = append(added, add.JID) + } + + response := gin.H{ + "jid": resp.JID, + "name": resp.Name, + "owner": resp.OwnerJID, + "added": added, + "failed": failed, + } + + return response, nil +} + +func (g *groupService) UpdateParticipant(data *AddParticipantStruct, instance *instance_model.Instance) error { + client, err := g.ensureClientConnected(instance.Id) + if err != nil { + return err + } + + var participants []types.JID + for _, participant := range data.Participants { + recipient, ok := utils.ParseJID(participant) + participants = append(participants, recipient) + if !ok { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return errors.New("invalid phone number") + } + } + + _, err = client.UpdateGroupParticipants(context.Background(), data.GroupJID, participants, data.Action) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error create group: %v", instance.Id, err) + return err + } + + return nil +} + +func (g *groupService) GetMyGroups(instance *instance_model.Instance) ([]types.GroupInfo, error) { + client, err := g.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + resp, err := client.GetJoinedGroups(context.Background()) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error create group: %v", instance.Id, err) + return nil, err + } + + var jid string = client.Store.ID.String() + var jidClear = strings.Split(jid, ".")[0] + jidOfAdmin, ok := utils.ParseJID(jidClear) + if !ok { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return nil, errors.New("invalid phone number") + } + var adminGroups []types.GroupInfo + for _, group := range resp { + if group.OwnerJID == jidOfAdmin { + adminGroups = append(adminGroups, *group) + _ = adminGroups + } + } + + return adminGroups, nil +} + +func (g *groupService) JoinGroupLink(data *JoinGroupStruct, instance *instance_model.Instance) error { + client, err := g.ensureClientConnected(instance.Id) + if err != nil { + return err + } + + _, err = client.JoinGroupWithLink(context.Background(), data.Code) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error create group: %v", instance.Id, err) + return err + } + + return nil +} + +func (g *groupService) LeaveGroup(data *LeaveGroupStruct, instance *instance_model.Instance) error { + client, err := g.ensureClientConnected(instance.Id) + if err != nil { + return err + } + + err = client.LeaveGroup(context.Background(), data.GroupJID) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error leave group: %v", instance.Id, err) + return err + } + + return nil +} + +func (g *groupService) UpdateGroupSettings(data *UpdateGroupSettingsStruct, instance *instance_model.Instance) error { + client, err := g.ensureClientConnected(instance.Id) + if err != nil { + return err + } + + recipient, ok := utils.ParseJID(data.GroupJID) + if !ok { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating group jid", instance.Id) + return errors.New("invalid group jid") + } + + // Validate action + validActions := map[string]bool{ + "announcement": true, + "not_announcement": true, + "locked": true, + "unlocked": true, + "approval_on": true, + "approval_off": true, + "admin_add": true, + "all_member_add": true, + } + + if !validActions[data.Action] { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Invalid action: %s", instance.Id, data.Action) + return errors.New("invalid action. Valid actions: announcement, not_announcement, locked, unlocked, approval_on, approval_off, admin_add, all_member_add") + } + + // Apply settings based on action + switch data.Action { + case "announcement": + err = client.SetGroupAnnounce(context.Background(), recipient, true) + case "not_announcement": + err = client.SetGroupAnnounce(context.Background(), recipient, false) + case "locked": + err = client.SetGroupLocked(context.Background(), recipient, true) + case "unlocked": + err = client.SetGroupLocked(context.Background(), recipient, false) + case "approval_on": + err = client.SetGroupJoinApprovalMode(context.Background(), recipient, true) + case "approval_off": + err = client.SetGroupJoinApprovalMode(context.Background(), recipient, false) + case "admin_add": + err = client.SetGroupMemberAddMode(context.Background(), recipient, "admin_add") + case "all_member_add": + err = client.SetGroupMemberAddMode(context.Background(), recipient, "all_member_add") + } + + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error updating group settings: %v", instance.Id, err) + return err + } + + g.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Group settings updated successfully: %s", instance.Id, data.Action) + return nil +} + +func (g *groupService) GetGroupRequestParticipants(data *GetGroupRequestParticipantsStruct, instance *instance_model.Instance) ([]EnrichedGroupParticipantRequest, error) { + client, err := g.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + recipient, ok := utils.ParseJID(data.GroupJID) + if !ok { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating group jid", instance.Id) + return nil, errors.New("invalid group jid") + } + + requests, err := client.GetGroupRequestParticipants(context.Background(), recipient) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error getting group request participants: %v", instance.Id, err) + return nil, err + } + + g.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Retrieved %d pending group requests", instance.Id, len(requests)) + + // Enriquecer com informações de usuário (PushName) + enrichedRequests := make([]EnrichedGroupParticipantRequest, len(requests)) + jidsToFetch := make([]types.JID, 0, len(requests)) + + for _, req := range requests { + if req.JID.User != "" { + jidsToFetch = append(jidsToFetch, req.JID) + } + } + + // Buscar informações de usuário em lote + userInfoMap := make(map[types.JID]types.UserInfo) + if len(jidsToFetch) > 0 { + userInfoMap, err = client.GetUserInfo(context.Background(), jidsToFetch) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Could not fetch user info: %v", instance.Id, err) + // Continuar sem pushName se falhar + } + } + + // Montar resposta enriquecida + for i, req := range requests { + enrichedRequests[i] = EnrichedGroupParticipantRequest{ + JID: req.JID, + RequestedAt: req.RequestedAt, + PushName: "", + } + + // Tentar obter PushName + lookupJID := req.JID + + if userInfo, found := userInfoMap[lookupJID]; found { + // VerifiedName é ponteiro, verificar se não é nil + if userInfo.VerifiedName != nil && userInfo.VerifiedName.Details.GetVerifiedName() != "" { + enrichedRequests[i].PushName = userInfo.VerifiedName.Details.GetVerifiedName() + } + } + + // Tentar obter do store de contatos se não tiver VerifiedName + if enrichedRequests[i].PushName == "" && client.Store.Contacts != nil { + if contactInfo, err := client.Store.Contacts.GetContact(context.Background(), lookupJID); err == nil && contactInfo.PushName != "" { + enrichedRequests[i].PushName = contactInfo.PushName + } else if contactInfo.FullName != "" { + enrichedRequests[i].PushName = contactInfo.FullName + } + } + } + + return enrichedRequests, nil +} + +func (g *groupService) UpdateGroupRequestParticipants(data *UpdateGroupRequestParticipantsStruct, instance *instance_model.Instance) ([]types.GroupParticipant, error) { + client, err := g.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + recipient, ok := utils.ParseJID(data.GroupJID) + if !ok { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating group jid", instance.Id) + return nil, errors.New("invalid group jid") + } + + // Validate action + var action whatsmeow.ParticipantRequestChange + switch data.Action { + case "approve": + action = whatsmeow.ParticipantChangeApprove + case "reject": + action = whatsmeow.ParticipantChangeReject + default: + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Invalid action: %s", instance.Id, data.Action) + return nil, errors.New("invalid action. Valid actions: approve, reject") + } + + // Parse participants JIDs + var participants []types.JID + for _, participant := range data.Participants { + participantJID, ok := utils.ParseJID(participant) + if !ok { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating participant jid: %s", instance.Id, participant) + return nil, errors.New("invalid participant jid: " + participant) + } + participants = append(participants, participantJID) + } + + results, err := client.UpdateGroupRequestParticipants(context.Background(), recipient, participants, action) + if err != nil { + g.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error updating group request participants: %v", instance.Id, err) + return nil, err + } + + g.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Successfully %sd %d participants", instance.Id, data.Action, len(participants)) + return results, nil +} + +func NewGroupService( + clientPointer map[string]*whatsmeow.Client, + whatsmeowService whatsmeow_service.WhatsmeowService, + loggerWrapper *logger_wrapper.LoggerManager, +) GroupService { + return &groupService{ + clientPointer: clientPointer, + whatsmeowService: whatsmeowService, + loggerWrapper: loggerWrapper, + } +} diff --git a/whatsapp-service/pkg/instance/handler/instance_handler.go b/whatsapp-service/pkg/instance/handler/instance_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..d249a8f211d738ad63ddce86777d5f0876f707b6 --- /dev/null +++ b/whatsapp-service/pkg/instance/handler/instance_handler.go @@ -0,0 +1,660 @@ +package instance_handler + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + + config "agentdeck-whatsapp-service/pkg/config" + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + instance_service "agentdeck-whatsapp-service/pkg/instance/service" + "agentdeck-whatsapp-service/pkg/utils" +) + +type InstanceHandler interface { + Create(ctx *gin.Context) + Connect(ctx *gin.Context) + Reconnect(ctx *gin.Context) + Disconnect(ctx *gin.Context) + Logout(ctx *gin.Context) + Delete(ctx *gin.Context) + Status(ctx *gin.Context) + Qr(ctx *gin.Context) + All(ctx *gin.Context) + Info(ctx *gin.Context) + Pair(ctx *gin.Context) + SetProxy(ctx *gin.Context) + DeleteProxy(ctx *gin.Context) + ForceReconnect(ctx *gin.Context) + GetLogs(ctx *gin.Context) + GetAdvancedSettings(ctx *gin.Context) + UpdateAdvancedSettings(ctx *gin.Context) +} + +type instanceHandler struct { + config *config.Config + instanceService instance_service.InstanceService +} + +// Create a new instance +// @Summary Create a new instance +// @Description Creates a new instance with the provided data including optional advanced settings +// @Tags Instance +// @Accept json +// @Produce json +// @Param instance body instance_service.CreateStruct true "Instance data with optional advanced settings" +// @Success 200 {object} gin.H "Instance created successfully" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/create [post] +func (i *instanceHandler) Create(ctx *gin.Context) { + var data *instance_service.CreateStruct + err := ctx.ShouldBindJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Name == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) + return + } + + if data.Token == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "token is required"}) + return + } + + if data.Proxy != nil { + if data.Proxy.Port == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "proxy port is required"}) + return + } + + if data.Proxy.Password == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "proxy password is required"}) + return + } + + if data.Proxy.Username == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "proxy username is required"}) + return + } + + if data.Proxy.Host == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "proxy host is required"}) + return + } + } else { + if i.config.ProxyHost != "" && i.config.ProxyPort != "" && i.config.ProxyUsername != "" && i.config.ProxyPassword != "" { + data.Proxy = &instance_service.ProxyConfig{ + Host: i.config.ProxyHost, + Port: i.config.ProxyPort, + Username: i.config.ProxyUsername, + Password: i.config.ProxyPassword, + } + } + } + + createdInstance, err := i.instanceService.Create(data) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": createdInstance}) +} + +// Connect to instance +// @Summary Connect to instance +// @Description Connect to instance with the provided data +// @Tags Instance +// @Accept json +// @Produce json +// @Param instance body instance_service.ConnectStruct true "Instance data" +// @Success 200 {object} gin.H "Instance connected successfully" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/connect [post] +func (i *instanceHandler) Connect(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *instance_service.ConnectStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + instance, jid, eventString, err := i.instanceService.Connect(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.Set("instance", instance) + + responseData := gin.H{ + "jid": jid, + "webhookUrl": instance.Webhook, + "eventString": eventString, + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// Reconnect to instance +// @Summary Reconnect to instance +// @Description Reconnect to instance +// @Tags Instance +// @Accept json +// @Produce json +// @Success 200 {object} gin.H "Instance reconnected successfully" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/reconnect [post] +func (i *instanceHandler) Reconnect(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + err := i.instanceService.Reconnect(instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Disconnect from instance +// @Summary Disconnect from instance +// @Description Disconnect from instance +// @Tags Instance +// @Accept json +// @Produce json +// @Success 200 {object} gin.H "Instance disconnected successfully" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/disconnect [post] +func (i *instanceHandler) Disconnect(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + updateInstance, err := i.instanceService.Disconnect(instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.Set("instance", updateInstance) + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Logout from instance +// @Summary Logout from instance +// @Description Logout from instance +// @Tags Instance +// @Accept json +// @Produce json +// @Success 200 {object} gin.H "Instance logged out successfully" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/logout [delete] +func (i *instanceHandler) Logout(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusNotFound, gin.H{"error": "instance not found"}) + return + } + + updateInstance, err := i.instanceService.Logout(instance) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + ctx.Set("instance", updateInstance) + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Get instance status +// @Summary Get instance status +// @Description Get instance status +// @Tags Instance +// @Accept json +// @Produce json +// @Success 200 {object} gin.H "Instance status" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/status [get] +func (i *instanceHandler) Status(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusNotFound, gin.H{"error": "instance not found"}) + return + } + + status, err := i.instanceService.Status(instance) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": status}) +} + +// Get instance QR code +// @Summary Get instance QR code +// @Description Get instance QR code +// @Tags Instance +// @Accept json +// @Produce json +// @Success 200 {object} gin.H "Instance QR code" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/qr [get] +func (i *instanceHandler) Qr(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusNotFound, gin.H{"error": "instance not found"}) + return + } + + qrcode, err := i.instanceService.GetQr(instance) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": qrcode}) +} + +// Request pairing code +// @Summary Request pairing code +// @Description Request pairing code +// @Tags Instance +// @Accept json +// @Produce json +// @Param instance body instance_service.PairStruct true "Instance data" +// @Success 200 {object} gin.H "Pairing code" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/pair [post] +func (i *instanceHandler) Pair(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *instance_service.PairStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Phone == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone is required"}) + return + } + + pairingCode, err := i.instanceService.Pair(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": pairingCode}) +} + +// Get all instances +// @Summary Get all instances +// @Description Get all instances +// @Tags Instance +// @Accept json +// @Produce json +// @Success 200 {object} gin.H "All instances" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/all [get] +func (i *instanceHandler) All(ctx *gin.Context) { + instances, err := i.instanceService.GetAll() + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": instances}) +} + +// Get instance +// @Summary Get instance +// @Description Get instance +// @Tags Instance +// @Accept json +// @Produce json +// @Param instanceId path string true "Instance Id" +// @Success 200 {object} gin.H "Instance" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/info/{instanceId} [get] +func (i *instanceHandler) Info(ctx *gin.Context) { + instanceId := ctx.Param("instanceId") + + if instanceId == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "instanceId is required"}) + return + } + + instance, err := i.instanceService.Info(instanceId) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": instance}) +} + +// Delete instance +// @Summary Delete instance +// @Description Delete instance +// @Tags Instance +// @Accept json +// @Produce json +// @Param instanceId path string true "Instance Id" +// @Success 200 {object} gin.H "Instance deleted successfully" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/delete/{instanceId} [delete] +func (i *instanceHandler) Delete(ctx *gin.Context) { + instanceId := ctx.Param("instanceId") + + if instanceId == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "instanceId is required"}) + return + } + + err := i.instanceService.Delete(instanceId) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Set proxy +// @Summary Set proxy configuration +// @Description Set proxy configuration for an instance +// @Tags Instance +// @Accept json +// @Produce json +// @Param instanceId path string true "Instance id" +// @Param proxy body instance_service.SetProxyStruct true "Proxy configuration" +// @Success 200 {object} gin.H "Proxy set successfully" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/proxy/{instanceId} [post] +func (i *instanceHandler) SetProxy(ctx *gin.Context) { + instanceId := ctx.Param("instanceId") + + if instanceId == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "instanceId is required"}) + return + } + + var data *instance_service.SetProxyStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Validate required fields + if data.Host == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "host is required"}) + return + } + + if data.Port == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "port is required"}) + return + } + + err = i.instanceService.SetProxyFromStruct(instanceId, data) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + responseData := gin.H{ + "protocol": utils.NormalizeProxyProtocol(data.Protocol, data.Port), + "host": data.Host, + "port": data.Port, + "hasAuth": data.Username != "" && data.Password != "", + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// Delete proxy +// @Summary Delete proxy +// @Description Delete proxy +// @Tags Instance +// @Accept json +// @Produce json +// @Param instanceId path string true "Instance id" +// @Success 200 {object} gin.H "Proxy deleted successfully" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/proxy/{instanceId} [delete] +func (i *instanceHandler) DeleteProxy(ctx *gin.Context) { + instanceId := ctx.Param("instanceId") + + if instanceId == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "id is required"}) + return + } + + err := i.instanceService.RemoveProxy(instanceId) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Force reconnect +// @Summary Force reconnect +// @Description Force reconnect +// @Tags Instance +// @Accept json +// @Produce json +// @Param instanceId path string true "Instance Id" +// @Param instance body instance_service.ForceReconnectStruct true "Instance data" +// @Success 200 {object} gin.H "Instance force reconnected successfully" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/forcereconnect/{instanceId} [post] +func (i *instanceHandler) ForceReconnect(ctx *gin.Context) { + instanceId := ctx.Param("instanceId") + + if instanceId == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "instanceId is required"}) + return + } + + var data *instance_service.ForceReconnectStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var number string + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "number is required"}) + return + } + + number = data.Number + + err = i.instanceService.ForceReconnect(instanceId, number) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +type GetLogsQuery struct { + StartDate string `form:"start_date"` + EndDate string `form:"end_date"` + Level string `form:"level"` + Limit int `form:"limit"` +} + +// GetLogs returns the log entries for an instance +// @Summary Get instance logs +// @Description Returns log entries for an instance, filterable by date range, level and limit +// @Tags Instance +// @Produce json +// @Param instanceId path string true "Instance Id" +// @Param start_date query string false "Start date (YYYY-MM-DD, defaults to 7 days ago)" +// @Param end_date query string false "End date (YYYY-MM-DD, defaults to now)" +// @Param level query string false "Log level filter" +// @Param limit query int false "Max number of entries" +// @Success 200 {object} gin.H "Logs" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/logs/{instanceId} [get] +func (h *instanceHandler) GetLogs(c *gin.Context) { + instanceId := c.Param("instanceId") + + var query GetLogsQuery + if err := c.ShouldBindQuery(&query); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Converte as datas + startDate, err := time.Parse("2006-01-02", query.StartDate) + if err != nil { + startDate = time.Now().AddDate(0, 0, -7) // Default: 7 dias atrás + } + + endDate, err := time.Parse("2006-01-02", query.EndDate) + if err != nil { + endDate = time.Now() + } + + // Ajusta o endDate para o final do dia + endDate = time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 999999999, time.UTC) + + if query.Limit == 0 { + query.Limit = 100 // Default: 100 registros + } + + logs, err := h.instanceService.GetLogs(instanceId, startDate, endDate, query.Level, query.Limit) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, logs) +} + +// GetAdvancedSettings retrieves advanced settings for an instance +// @Summary Get advanced settings +// @Description Get advanced settings for a specific instance +// @Tags Instance +// @Produce json +// @Param instanceId path string true "Instance ID" +// @Success 200 {object} instance_model.AdvancedSettings "Advanced settings retrieved successfully" +// @Failure 400 {object} gin.H "Invalid instance ID" +// @Failure 404 {object} gin.H "Instance not found" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/{instanceId}/advanced-settings [get] +func (h *instanceHandler) GetAdvancedSettings(c *gin.Context) { + instanceId := c.Param("instanceId") + + if instanceId == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "instanceId is required"}) + return + } + + settings, err := h.instanceService.GetAdvancedSettings(instanceId) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, settings) +} + +// UpdateAdvancedSettings updates advanced settings for an instance +// @Summary Update advanced settings +// @Description Update advanced settings for a specific instance +// @Tags Instance +// @Accept json +// @Produce json +// @Param instanceId path string true "Instance ID" +// @Param settings body instance_model.AdvancedSettings true "Advanced settings data" +// @Success 200 {object} gin.H "Advanced settings updated successfully" +// @Failure 400 {object} gin.H "Invalid request data" +// @Failure 404 {object} gin.H "Instance not found" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /instance/{instanceId}/advanced-settings [put] +func (h *instanceHandler) UpdateAdvancedSettings(c *gin.Context) { + instanceId := c.Param("instanceId") + + if instanceId == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "instanceId is required"}) + return + } + + var settings instance_model.AdvancedSettings + if err := c.ShouldBindJSON(&settings); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + err := h.instanceService.UpdateAdvancedSettings(instanceId, &settings) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Advanced settings updated successfully", + "settings": settings, + }) +} + +func NewInstanceHandler(instanceService instance_service.InstanceService, config *config.Config) InstanceHandler { + return &instanceHandler{instanceService: instanceService, config: config} +} diff --git a/whatsapp-service/pkg/instance/model/instance_model.go b/whatsapp-service/pkg/instance/model/instance_model.go new file mode 100644 index 0000000000000000000000000000000000000000..1182da3dd5708cbcffcf184371e24ea24ff9de57 --- /dev/null +++ b/whatsapp-service/pkg/instance/model/instance_model.go @@ -0,0 +1,52 @@ +package instance_model + +import ( + "time" + + "github.com/google/uuid" +) + +type Instance struct { + Id string `json:"id"` + Name string `json:"name"` + Token string `json:"token"` + Webhook string `json:"webhook"` + RabbitmqEnable string `json:"rabbitmqEnable"` + WebSocketEnable string `json:"websocketEnable"` + NatsEnable string `json:"natsEnable"` + Jid string `json:"jid"` + Qrcode string `json:"qrcode"` + Connected bool `json:"connected"` + Expiration int64 `json:"expiration"` + DisconnectReason string `json:"disconnect_reason"` + Events string `json:"events"` + OsName string `json:"os_name"` + Proxy string `json:"proxy"` + ClientName string `json:"client_name"` + CreatedAt time.Time `json:"createdAt"` + + // Advanced Settings + AlwaysOnline bool `json:"alwaysOnline"` + RejectCall bool `json:"rejectCall"` + MsgRejectCall string `json:"msgRejectCall"` + ReadMessages bool `json:"readMessages"` + IgnoreGroups bool `json:"ignoreGroups"` + IgnoreStatus bool `json:"ignoreStatus"` +} + +// AdvancedSettings representa as configurações avançadas de uma instância +type AdvancedSettings struct { + AlwaysOnline bool `json:"alwaysOnline"` + RejectCall bool `json:"rejectCall"` + MsgRejectCall string `json:"msgRejectCall"` + ReadMessages bool `json:"readMessages"` + IgnoreGroups bool `json:"ignoreGroups"` + IgnoreStatus bool `json:"ignoreStatus"` +} + +// EnsureID assigns a UUID when not already set. +func (m *Instance) EnsureID() { + if m.Id == "" { + m.Id = uuid.New().String() + } +} diff --git a/whatsapp-service/pkg/instance/repository/instance_repository.go b/whatsapp-service/pkg/instance/repository/instance_repository.go new file mode 100644 index 0000000000000000000000000000000000000000..dfd196b2d94fdf3f447259d9700e0994c439446e --- /dev/null +++ b/whatsapp-service/pkg/instance/repository/instance_repository.go @@ -0,0 +1,333 @@ +package instance_repository + +import ( + "context" + "fmt" + "time" + + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + "agentdeck-whatsapp-service/pkg/supabase" + "github.com/google/uuid" +) + +type InstanceRepository interface { + Create(instance instance_model.Instance) (*instance_model.Instance, error) + GetInstanceByID(instanceId string) (*instance_model.Instance, error) + GetConnectedInstanceByID(instanceId string) (*instance_model.Instance, error) + GetInstanceByToken(token string) (*instance_model.Instance, error) + GetInstanceByName(name string) (*instance_model.Instance, error) + Update(*instance_model.Instance) error + UpdateConnected(userId string, status bool, disconnectReason string) error + UpdateQrcode(userId string, qr string) error + UpdateProxy(userId string, proxy string) error + UpdateJid(userId string, jid string) error + GetAllConnectedInstances() ([]*instance_model.Instance, error) + GetAllConnectedInstancesByClientName(clientName string) ([]*instance_model.Instance, error) + GetAll(clientName string) ([]*instance_model.Instance, error) + Delete(instanceId string) error + GetAdvancedSettings(instanceId string) (*instance_model.AdvancedSettings, error) + UpdateAdvancedSettings(instanceId string, settings *instance_model.AdvancedSettings) error +} + +// instanceRow is the snake_case PostgREST representation of an instance row. +// The public API model keeps its camelCase JSON tags; this row maps only the DB. +type instanceRow struct { + Id string `json:"id"` + Name string `json:"name"` + Token string `json:"token"` + Webhook string `json:"webhook"` + RabbitmqEnable string `json:"rabbitmq_enable"` + WebSocketEnable string `json:"web_socket_enable"` + NatsEnable string `json:"nats_enable"` + Jid string `json:"jid"` + Qrcode string `json:"qrcode"` + Connected bool `json:"connected"` + Expiration int64 `json:"expiration"` + DisconnectReason string `json:"disconnect_reason"` + Events string `json:"events"` + OsName string `json:"os_name"` + Proxy string `json:"proxy"` + ClientName string `json:"client_name"` + CreatedAt *string `json:"created_at,omitempty"` + + AlwaysOnline bool `json:"always_online"` + RejectCall bool `json:"reject_call"` + MsgRejectCall string `json:"msg_reject_call"` + ReadMessages bool `json:"read_messages"` + IgnoreGroups bool `json:"ignore_groups"` + IgnoreStatus bool `json:"ignore_status"` +} + +type instanceRepository struct { + supa *supabase.Client +} + +func toRow(instance instance_model.Instance) instanceRow { + return instanceRow{ + Id: instance.Id, + Name: instance.Name, + Token: instance.Token, + Webhook: instance.Webhook, + RabbitmqEnable: instance.RabbitmqEnable, + WebSocketEnable: instance.WebSocketEnable, + NatsEnable: instance.NatsEnable, + Jid: instance.Jid, + Qrcode: instance.Qrcode, + Connected: instance.Connected, + Expiration: instance.Expiration, + DisconnectReason: instance.DisconnectReason, + Events: instance.Events, + OsName: instance.OsName, + Proxy: instance.Proxy, + ClientName: instance.ClientName, + AlwaysOnline: instance.AlwaysOnline, + RejectCall: instance.RejectCall, + MsgRejectCall: instance.MsgRejectCall, + ReadMessages: instance.ReadMessages, + IgnoreGroups: instance.IgnoreGroups, + IgnoreStatus: instance.IgnoreStatus, + } +} + +func fromRow(r *instanceRow) *instance_model.Instance { + instance := &instance_model.Instance{ + Id: r.Id, + Name: r.Name, + Token: r.Token, + Webhook: r.Webhook, + RabbitmqEnable: r.RabbitmqEnable, + WebSocketEnable: r.WebSocketEnable, + NatsEnable: r.NatsEnable, + Jid: r.Jid, + Qrcode: r.Qrcode, + Connected: r.Connected, + Expiration: r.Expiration, + DisconnectReason: r.DisconnectReason, + Events: r.Events, + OsName: r.OsName, + Proxy: r.Proxy, + ClientName: r.ClientName, + AlwaysOnline: r.AlwaysOnline, + RejectCall: r.RejectCall, + MsgRejectCall: r.MsgRejectCall, + ReadMessages: r.ReadMessages, + IgnoreGroups: r.IgnoreGroups, + IgnoreStatus: r.IgnoreStatus, + } + if r.CreatedAt != nil { + if t, err := time.Parse(time.RFC3339, *r.CreatedAt); err == nil { + instance.CreatedAt = t + } + } + return instance +} + +func (i *instanceRepository) Create(instance instance_model.Instance) (*instance_model.Instance, error) { + if instance.Id == "" { + instance.Id = uuid.New().String() + } + row := toRow(instance) + ctx := context.Background() + var created []instanceRow + if err := i.supa.Table("wp_instances").Insert(ctx, row, "return=representation", &created); err != nil { + return nil, err + } + if len(created) == 0 { + return nil, fmt.Errorf("no instance row returned") + } + return fromRow(&created[0]), nil +} + +func (i *instanceRepository) GetInstanceByToken(token string) (*instance_model.Instance, error) { + q := supabase.NewQuery().Eq("token", token).Limit(1) + var rows []instanceRow + ctx := context.Background() + if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil { + return nil, err + } + if len(rows) == 0 { + return nil, fmt.Errorf("instances: no row for token") + } + return fromRow(&rows[0]), nil +} + +func (i *instanceRepository) GetInstanceByName(name string) (*instance_model.Instance, error) { + q := supabase.NewQuery().Eq("name", name).Limit(1) + var rows []instanceRow + ctx := context.Background() + if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil { + return nil, err + } + if len(rows) == 0 { + return nil, fmt.Errorf("instances not found for name") + } + return fromRow(&rows[0]), nil +} + +func (i *instanceRepository) GetInstanceByID(instanceId string) (*instance_model.Instance, error) { + if _, err := uuid.Parse(instanceId); err != nil { + return nil, fmt.Errorf("invalid UUID format: %v", err) + } + q := supabase.NewQuery().Eq("id", instanceId).Limit(1) + var rows []instanceRow + ctx := context.Background() + if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil { + return nil, err + } + if len(rows) == 0 { + return nil, fmt.Errorf("instance not found") + } + return fromRow(&rows[0]), nil +} + +func (i *instanceRepository) GetConnectedInstanceByID(instanceId string) (*instance_model.Instance, error) { + q := supabase.NewQuery().Eq("id", instanceId).Eq("connected", "true").Limit(1) + var rows []instanceRow + ctx := context.Background() + if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil { + return nil, err + } + if len(rows) == 0 { + return nil, fmt.Errorf("connected instance not found") + } + return fromRow(&rows[0]), nil +} + +func (i *instanceRepository) Update(instance *instance_model.Instance) error { + row := toRow(*instance) + ctx := context.Background() + q := supabase.NewQuery().Eq("id", instance.Id) + return i.supa.Table("wp_instances").Update(ctx, q, row) +} + +func (i *instanceRepository) UpdateConnected(userId string, connected bool, disconnectReason string) error { + ctx := context.Background() + q := supabase.NewQuery().Eq("id", userId) + body := map[string]interface{}{ + "connected": connected, + "disconnect_reason": disconnectReason, + } + return i.supa.Table("wp_instances").Update(ctx, q, body) +} + +func (i *instanceRepository) UpdateQrcode(userId string, qr string) error { + ctx := context.Background() + q := supabase.NewQuery().Eq("id", userId) + return i.supa.Table("wp_instances").Update(ctx, q, map[string]interface{}{"qrcode": qr}) +} + +func (i *instanceRepository) UpdateProxy(userId string, proxy string) error { + ctx := context.Background() + q := supabase.NewQuery().Eq("id", userId) + return i.supa.Table("wp_instances").Update(ctx, q, map[string]interface{}{"proxy": proxy}) +} + +func (i *instanceRepository) UpdateJid(userId string, jid string) error { + ctx := context.Background() + q := supabase.NewQuery().Eq("id", userId) + return i.supa.Table("wp_instances").Update(ctx, q, map[string]interface{}{"jid": jid}) +} + +func (i *instanceRepository) GetAllConnectedInstances() ([]*instance_model.Instance, error) { + q := supabase.NewQuery().Eq("connected", "true") + var rows []instanceRow + ctx := context.Background() + if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil { + return nil, err + } + out := make([]*instance_model.Instance, 0, len(rows)) + for idx := range rows { + out = append(out, fromRow(&rows[idx])) + } + return out, nil +} + +func (i *instanceRepository) GetAllConnectedInstancesByClientName(clientName string) ([]*instance_model.Instance, error) { + q := supabase.NewQuery().Eq("connected", "true").Eq("client_name", clientName) + var rows []instanceRow + ctx := context.Background() + if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil { + return nil, err + } + out := make([]*instance_model.Instance, 0, len(rows)) + for idx := range rows { + out = append(out, fromRow(&rows[idx])) + } + return out, nil +} + +func (i *instanceRepository) GetAll(clientName string) ([]*instance_model.Instance, error) { + q := supabase.NewQuery().Eq("client_name", clientName) + var rows []instanceRow + ctx := context.Background() + if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil { + return nil, err + } + out := make([]*instance_model.Instance, 0, len(rows)) + for idx := range rows { + out = append(out, fromRow(&rows[idx])) + } + return out, nil +} + +func (i *instanceRepository) Delete(instanceId string) error { + ctx := context.Background() + // Cascade delete via a Supabase RPC, which deletes related rows and the + // instance atomically (see ddl/006_functions.sql). + var result interface{} + if err := i.supa.RPC(ctx, "wp_delete_instance", map[string]interface{}{"p_instance_id": instanceId}, &result); err != nil { + // Fallback: delete rows individually if the RPC is unavailable. + q := supabase.NewQuery().Eq("id", instanceId) + if derr := i.supa.Table("wp_instances").Delete(ctx, q); derr != nil { + return fmt.Errorf("failed to delete instance: %v", derr) + } + } + return nil +} + +func (i *instanceRepository) GetAdvancedSettings(instanceId string) (*instance_model.AdvancedSettings, error) { + if _, err := uuid.Parse(instanceId); err != nil { + return nil, fmt.Errorf("invalid UUID format: %v", err) + } + q := supabase.NewQuery(). + Eq("id", instanceId). + Select("always_online, reject_call, msg_reject_call, read_messages, ignore_groups, ignore_status"). + Limit(1) + var rows []instanceRow + ctx := context.Background() + if err := i.supa.Table("wp_instances").Select(ctx, q, &rows); err != nil { + return nil, err + } + if len(rows) == 0 { + return nil, fmt.Errorf("instance not found") + } + return &instance_model.AdvancedSettings{ + AlwaysOnline: rows[0].AlwaysOnline, + RejectCall: rows[0].RejectCall, + MsgRejectCall: rows[0].MsgRejectCall, + ReadMessages: rows[0].ReadMessages, + IgnoreGroups: rows[0].IgnoreGroups, + IgnoreStatus: rows[0].IgnoreStatus, + }, nil +} + +func (i *instanceRepository) UpdateAdvancedSettings(instanceId string, settings *instance_model.AdvancedSettings) error { + if _, err := uuid.Parse(instanceId); err != nil { + return fmt.Errorf("invalid UUID format: %v", err) + } + ctx := context.Background() + q := supabase.NewQuery().Eq("id", instanceId) + body := map[string]interface{}{ + "always_online": settings.AlwaysOnline, + "reject_call": settings.RejectCall, + "msg_reject_call": settings.MsgRejectCall, + "read_messages": settings.ReadMessages, + "ignore_groups": settings.IgnoreGroups, + "ignore_status": settings.IgnoreStatus, + } + return i.supa.Table("wp_instances").Update(ctx, q, body) +} + +func NewInstanceRepository(supa *supabase.Client) InstanceRepository { + return &instanceRepository{supa: supa} +} \ No newline at end of file diff --git a/whatsapp-service/pkg/instance/service/instance_service.go b/whatsapp-service/pkg/instance/service/instance_service.go new file mode 100644 index 0000000000000000000000000000000000000000..6f21ab307534be8a6bb7bb181f9c59564204fb65 --- /dev/null +++ b/whatsapp-service/pkg/instance/service/instance_service.go @@ -0,0 +1,929 @@ +package instance_service + +import ( + "bufio" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "sort" + "strings" + "time" + + "agentdeck-whatsapp-service/pkg/config" + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + instance_repository "agentdeck-whatsapp-service/pkg/instance/repository" + event_types "agentdeck-whatsapp-service/pkg/internal/event_types" + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + "agentdeck-whatsapp-service/pkg/utils" + whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/types" +) + +type InstanceService interface { + Create(data *CreateStruct) (*instance_model.Instance, error) + Connect(data *ConnectStruct, instance *instance_model.Instance) (*instance_model.Instance, string, string, error) + Reconnect(instance *instance_model.Instance) error + Disconnect(instance *instance_model.Instance) (*instance_model.Instance, error) + Logout(instance *instance_model.Instance) (*instance_model.Instance, error) + Status(instance *instance_model.Instance) (*StatusStruct, error) + GetQr(instance *instance_model.Instance) (*QrcodeStruct, error) + Pair(data *PairStruct, instance *instance_model.Instance) (*PairReturnStruct, error) + GetAll() ([]*instance_model.Instance, error) + Info(instanceId string) (*instance_model.Instance, error) + Delete(id string) error + SetProxy(id string, proxyConfig *ProxyConfig) error + SetProxyFromStruct(id string, data *SetProxyStruct) error + RemoveProxy(id string) error + ForceReconnect(instanceId string, number string) error + GetInstanceByToken(token string) (*instance_model.Instance, error) + GetLogs(instanceId string, startDate, endDate time.Time, level string, limit int) ([]logger_wrapper.LogEntry, error) + GetAdvancedSettings(instanceId string) (*instance_model.AdvancedSettings, error) + UpdateAdvancedSettings(instanceId string, settings *instance_model.AdvancedSettings) error +} + +type instances struct { + instanceRepository instance_repository.InstanceRepository + config *config.Config + killChannel map[string](chan bool) + clientPointer map[string]*whatsmeow.Client + whatsmeowService whatsmeow_service.WhatsmeowService + loggerWrapper *logger_wrapper.LoggerManager +} + +type ProxyConfig struct { + Protocol string `json:"protocol,omitempty"` + Port string `json:"port"` + Password string `json:"password"` + Username string `json:"username"` + Host string `json:"host"` +} + +type CreateStruct struct { + InstanceId string `json:"instanceId"` + Name string `json:"name"` + Token string `json:"token"` + Proxy *ProxyConfig `json:"proxy"` + AdvancedSettings *instance_model.AdvancedSettings `json:"advancedSettings"` +} + +type ConnectStruct struct { + WebhookUrl string `json:"webhookUrl"` + Subscribe []string `json:"subscribe"` + Immediate bool `json:"immediate"` + Phone string `json:"phone"` + RabbitmqEnable string `json:"rabbitmqEnable"` + WebSocketEnable string `json:"websocketEnable"` + NatsEnable string `json:"natsEnable"` +} + +type StatusStruct struct { + Connected bool + LoggedIn bool + myJid *types.JID + Name string +} + +type QrcodeStruct struct { + Qrcode string `json:"qrcode"` + Code string `json:"code"` + // Passkey ceremony fields. Populated when the account requires a WebAuthn + // passkey to finish linking (no QR to scan at that point). The manager uses + // PasskeyStage to switch its UI and PasskeyOpenUrl for the + // "Abrir WhatsApp Web" button that launches the passkey ceremony. + PasskeyStage string `json:"passkeyStage,omitempty"` + PasskeyOpenURL string `json:"passkeyOpenUrl,omitempty"` + PasskeyCode string `json:"passkeyCode,omitempty"` +} + +type PairStruct struct { + Subscribe []string `json:"subscribe"` + Phone string `json:"phone"` +} + +type PairReturnStruct struct { + PairingCode string +} + +type SetProxyStruct struct { + Protocol string `json:"protocol,omitempty"` + Host string `json:"host" validate:"required"` + Port string `json:"port" validate:"required"` + Username string `json:"username"` + Password string `json:"password"` +} + +type ForceReconnectStruct struct { + Number string `json:"number"` +} + +func (i *instances) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + logger := i.loggerWrapper.GetLogger(instanceId) + client := i.clientPointer[instanceId] + logger.LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) + + if client == nil { + logger.LogInfo("[%s] No client found, attempting to start new instance", instanceId) + err := i.whatsmeowService.StartInstance(instanceId) + if err != nil { + logger.LogError("[%s] Failed to start instance: %v", instanceId, err) + return nil, errors.New("no active session found") + } + + logger.LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) + time.Sleep(2 * time.Second) + + client = i.clientPointer[instanceId] + logger.LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + + if client == nil || !client.IsConnected() { + logger.LogError("[%s] New client validation failed - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + return nil, errors.New("no active session found") + } + } else if !client.IsConnected() { + logger.LogError("[%s] Existing client is disconnected - Connected status: %v", + instanceId, + client.IsConnected()) + return nil, errors.New("client disconnected") + } + + logger.LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected()) + return client, nil +} + +func (i instances) Create(data *CreateStruct) (*instance_model.Instance, error) { + if data.Proxy != nil { + data.Proxy.Protocol = utils.NormalizeProxyProtocol(data.Proxy.Protocol, data.Proxy.Port) + } + + proxyJson, err := json.Marshal(data.Proxy) + if err != nil { + return nil, err + } + + findInstance, _ := i.instanceRepository.GetInstanceByName(data.Name) + + if findInstance != nil { + return nil, fmt.Errorf("instance already exists") + } + + instance := instance_model.Instance{ + Id: data.InstanceId, + Name: data.Name, + Token: data.Token, + OsName: i.config.OsName, + Proxy: string(proxyJson), + Connected: false, + ClientName: i.config.ClientName, + } + + // Set advanced settings if provided + if data.AdvancedSettings != nil { + instance.AlwaysOnline = data.AdvancedSettings.AlwaysOnline + instance.RejectCall = data.AdvancedSettings.RejectCall + instance.MsgRejectCall = data.AdvancedSettings.MsgRejectCall + instance.ReadMessages = data.AdvancedSettings.ReadMessages + instance.IgnoreGroups = data.AdvancedSettings.IgnoreGroups + instance.IgnoreStatus = data.AdvancedSettings.IgnoreStatus + } + + createdInstance, err := i.instanceRepository.Create(instance) + if err != nil { + return nil, err + } + + return createdInstance, nil +} + +func (i instances) Connect(data *ConnectStruct, instance *instance_model.Instance) (*instance_model.Instance, string, string, error) { + var subscribedEvents []string + + i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Processing subscribe events: %v", instance.Id, data.Subscribe) + + if len(data.Subscribe) == 0 { + subscribedEvents = append(subscribedEvents, event_types.MESSAGE) + } else if len(data.Subscribe) > 0 && data.Subscribe[0] == "ALL" { + for _, event := range event_types.AllEventTypes { + subscribedEvents = append(subscribedEvents, event) + } + } else { + for _, arg := range data.Subscribe { + if !event_types.IsEventType(arg) { + i.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Message type discarded '%s'", instance.Id, arg) + continue + } + subscribedEvents = append(subscribedEvents, arg) + } + } + + eventString := strings.Join(subscribedEvents, ",") + + instance.Events = eventString + instance.Webhook = data.WebhookUrl + instance.RabbitmqEnable = data.RabbitmqEnable + instance.NatsEnable = data.NatsEnable + instance.WebSocketEnable = data.WebSocketEnable + + err := i.instanceRepository.Update(instance) + if err != nil { + i.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error updating instance: %s", instance.Id, err) + return nil, "", "", err + } + + // Verifica se a instância já está rodando + isInstanceRunning := i.clientPointer[instance.Id] != nil + + // Sincroniza as configurações na instância em execução (se já estiver conectada) + err = i.whatsmeowService.UpdateInstanceSettings(instance.Id) + if err != nil { + i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Instance not in runtime yet, will be updated when connected", instance.Id) + isInstanceRunning = false + } else { + i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Instance settings updated successfully in runtime", instance.Id) + isInstanceRunning = true + } + + // Se a instância não estiver rodando, inicia uma nova + if !isInstanceRunning { + i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Starting new client instance", instance.Id) + + i.killChannel[instance.Id] = make(chan bool) + + clientData := &whatsmeow_service.ClientData{ + Instance: instance, + Subscriptions: subscribedEvents, + Phone: data.Phone, + IsProxy: false, + } + + if instance.Proxy != "" || i.config.ProxyHost != "" { + var proxyConfig ProxyConfig + err := json.Unmarshal([]byte(instance.Proxy), &proxyConfig) + if err != nil { + i.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error unmarshalling proxy config: %v", instance.Id, err) + return nil, "", "", err + } + + if proxyConfig.Host != "" || i.config.ProxyHost != "" { + clientData.IsProxy = true + } + } + + go i.whatsmeowService.StartClient(clientData) + } else { + i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Instance already running, settings updated without restarting client", instance.Id) + } + + // logger.LogInfo("Waiting 1 seconds") + // time.Sleep(1000 * time.Millisecond) + + // if i.clientPointer[instance.Id] != nil { + // if !i.clientPointer[instance.Id].IsConnected() { + // return instance, "", "", fmt.Errorf("failed to connect") + // } + // } else { + // return instance, "", "", fmt.Errorf("failed to connect") + // } + + return instance, instance.Jid, eventString, nil +} + +func (i instances) Reconnect(instance *instance_model.Instance) error { + _, err := i.ensureClientConnected(instance.Id) + if err != nil { + return err + } + + return i.whatsmeowService.ReconnectClient(instance.Id) +} + +func (i instances) Disconnect(instance *instance_model.Instance) (*instance_model.Instance, error) { + client, err := i.ensureClientConnected(instance.Id) + if err != nil { + return instance, err + } + + if client.IsConnected() { + if client.IsLoggedIn() { + i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Disconnection successful", instance.Id) + i.killChannel[instance.Id] <- true + + instance.Events = "" + + err := i.instanceRepository.Update(instance) + if err != nil { + return instance, err + } + + return instance, nil + } + } + + i.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Ignoring disconnect as it was not connected", instance.Id) + return instance, nil +} + +func (i instances) Logout(instance *instance_model.Instance) (*instance_model.Instance, error) { + client, err := i.ensureClientConnected(instance.Id) + if err != nil { + return instance, err + } + + if client.IsLoggedIn() && client.IsConnected() { + err := client.Logout(context.Background()) + if err != nil { + return instance, err + } + + instance.Connected = false + err = i.instanceRepository.Update(instance) + if err != nil { + return instance, err + } + + select { + case i.killChannel[instance.Id] <- true: + case <-time.After(5 * time.Second): + } + + delete(i.clientPointer, instance.Id) + delete(i.killChannel, instance.Id) + + i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Logout successful", instance.Id) + return instance, nil + } + + if client.IsConnected() { + client.Disconnect() + + select { + case i.killChannel[instance.Id] <- true: + case <-time.After(5 * time.Second): + } + + delete(i.clientPointer, instance.Id) + delete(i.killChannel, instance.Id) + + i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Disconnection successful", instance.Id) + return instance, nil + } + + i.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Ignoring logout as it was not connected", instance.Id) + return instance, fmt.Errorf("ignoring logout as it was not connected") +} + +func (i instances) Status(instance *instance_model.Instance) (*StatusStruct, error) { + client := i.clientPointer[instance.Id] + + if client == nil { + return &StatusStruct{ + Connected: false, + LoggedIn: false, + }, nil + } + + isConnected := client.IsConnected() + isLoggedIn := client.IsLoggedIn() + + var myJid *types.JID + var name string + if isLoggedIn { + myJid = client.Store.ID + name = client.Store.PushName + } + + return &StatusStruct{ + Connected: isConnected, + LoggedIn: isLoggedIn, + myJid: myJid, + Name: name, + }, nil +} + +func (i instances) GetQr(instance *instance_model.Instance) (*QrcodeStruct, error) { + logger := i.loggerWrapper.GetLogger(instance.Id) + client := i.clientPointer[instance.Id] + + // Se não há cliente ou o cliente está logado, precisamos iniciar um novo cliente + if client == nil || client.IsLoggedIn() { + if client != nil && client.IsLoggedIn() { + logger.LogInfo("[%s] Client is logged in, starting new instance for QR code", instance.Id) + } else { + logger.LogInfo("[%s] No client found, starting new instance for QR code", instance.Id) + } + + // Iniciar nova instância para gerar QR code + err := i.whatsmeowService.StartInstance(instance.Id) + if err != nil { + logger.LogError("[%s] Failed to start instance: %v", instance.Id, err) + return nil, fmt.Errorf("failed to start instance: %w", err) + } + + // Aguardar um pouco para o cliente iniciar e gerar QR code + logger.LogInfo("[%s] Waiting for QR code generation...", instance.Id) + time.Sleep(3 * time.Second) + + // Verificar novamente se há cliente + client = i.clientPointer[instance.Id] + if client != nil && client.IsLoggedIn() { + return nil, fmt.Errorf("session already logged in") + } + } else if !client.IsConnected() { + // Se o cliente existe mas não está conectado, pode estar aguardando QR code + logger.LogInfo("[%s] Client exists but not connected, checking for existing QR code", instance.Id) + } + + // Buscar instância atualizada do banco para pegar o QR code mais recente + instance, err := i.instanceRepository.GetInstanceByID(instance.Id) + if err != nil { + return nil, err + } + + // If a passkey ceremony is in progress, there is no QR to scan — return the + // passkey stage + the #wapk openUrl so the manager can render the + // "Abrir WhatsApp Web" button. Checked before the empty-QR branch because + // during a passkey ceremony instance.Qrcode is empty. + if store := i.whatsmeowService.PasskeyCeremonyStore(); store != nil { + if token, state, ok := store.StateByInstance(instance.Id); ok { + logger.LogInfo("[%s] Passkey ceremony active (stage=%s) — returning passkey info instead of QR", instance.Id, state.Stage) + return &QrcodeStruct{ + PasskeyStage: state.Stage, + PasskeyCode: state.Code, + PasskeyOpenURL: buildPasskeyOpenURL(token), + }, nil + } + } + + code := instance.Qrcode + if code == "" { + // Se não há QR code ainda, aguardar um pouco mais e tentar novamente + logger.LogInfo("[%s] No QR code available yet, waiting a bit more...", instance.Id) + time.Sleep(2 * time.Second) + + instance, err = i.instanceRepository.GetInstanceByID(instance.Id) + if err != nil { + return nil, err + } + + code = instance.Qrcode + if code == "" { + return nil, fmt.Errorf("no QR code available. Please wait a moment and try again") + } + } + + parts := strings.Split(code, "|") + if len(parts) < 2 { + return nil, fmt.Errorf("invalid QR code format") + } + + qr := &QrcodeStruct{ + Qrcode: parts[0], + Code: parts[1], + } + + return qr, nil +} + +// buildPasskeyOpenURL builds the URL the manager opens to start the passkey +// ceremony: https://web.whatsapp.com/#wapk=. +// publicBase must be the PUBLICLY reachable API base the browser can hit; set it +// via PASSKEY_PUBLIC_URL. Kept in sync with the event handler in whatsmeow.go. +func buildPasskeyOpenURL(token string) string { + publicBase := os.Getenv("PASSKEY_PUBLIC_URL") + if publicBase == "" { + publicBase = "" + } + payload := fmt.Sprintf(`{"t":%q,"b":%q}`, token, publicBase) + wapk := base64.RawURLEncoding.EncodeToString([]byte(payload)) + return "https://web.whatsapp.com/#wapk=" + wapk +} + +func (i instances) Pair(data *PairStruct, instance *instance_model.Instance) (*PairReturnStruct, error) { + logger := i.loggerWrapper.GetLogger(instance.Id) + client := i.clientPointer[instance.Id] + + if client == nil || !client.IsConnected() { + if client != nil && client.IsLoggedIn() { + return nil, fmt.Errorf("instance is already authenticated") + } + logger.LogInfo("[%s] No active connection, starting instance for phone pairing", instance.Id) + if err := i.whatsmeowService.StartInstance(instance.Id); err != nil { + logger.LogError("[%s] Failed to start instance for pairing: %v", instance.Id, err) + return nil, fmt.Errorf("failed to start instance: %w", err) + } + // Wait for the WA websocket connection and initial QR generation to establish. + // PairPhone must be called after the QR event is received per whatsmeow docs. + time.Sleep(3 * time.Second) + client = i.clientPointer[instance.Id] + if client == nil { + return nil, fmt.Errorf("failed to initialize client for pairing") + } + } + + if client.IsLoggedIn() { + return nil, fmt.Errorf("instance is already authenticated") + } + + code, err := client.PairPhone(context.Background(), data.Phone, true, whatsmeow.PairClientChrome, "Chrome (Linux)") + if err != nil { + logger.LogError("[%s] PairPhone failed: %v", instance.Id, err) + return nil, fmt.Errorf("pairing failed: %w", err) + } + + return &PairReturnStruct{PairingCode: code}, nil +} + +func (i instances) GetAll() ([]*instance_model.Instance, error) { + instances, err := i.instanceRepository.GetAll(i.config.ClientName) + if err != nil { + return nil, err + } + + for _, instance := range instances { + if client := i.clientPointer[instance.Id]; client != nil { + instance.Connected = client.IsLoggedIn() + } else { + instance.Connected = false + } + + instance.Proxy = "" + } + + return instances, nil +} + +func (i instances) Info(instanceId string) (*instance_model.Instance, error) { + instance, err := i.instanceRepository.GetInstanceByID(instanceId) + if err != nil { + return nil, err + } + + // Atualiza o status connected com base no estado real do cliente + if client := i.clientPointer[instance.Id]; client != nil { + instance.Connected = client.IsLoggedIn() + } else { + instance.Connected = false + } + + instance.Proxy = "" + + return instance, nil +} + +func (i instances) Delete(id string) error { + instance, err := i.instanceRepository.GetInstanceByID(id) + if err != nil { + return err + } + + if i.clientPointer[instance.Id] != nil && i.clientPointer[instance.Id].IsConnected() { + if i.clientPointer[instance.Id].IsLoggedIn() { + i.clientPointer[instance.Id].Logout(context.Background()) + } + i.clientPointer[instance.Id].Disconnect() + } + + // Limpar todos os recursos da instância antes de deletar + delete(i.clientPointer, instance.Id) + if i.killChannel[instance.Id] != nil { + close(i.killChannel[instance.Id]) + delete(i.killChannel, instance.Id) + } + + // Limpar cache via whatsmeow service + err = i.whatsmeowService.ClearInstanceCache(instance.Id, instance.Token) + if err != nil { + i.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Failed to clear instance cache: %v", instance.Id, err) + } + + err = i.instanceRepository.Delete(id) + if err != nil { + return err + } + + return nil +} + +func (i instances) SetProxy(id string, proxyConfig *ProxyConfig) error { + instance, err := i.instanceRepository.GetInstanceByID(id) + if err != nil { + return err + } + + // Validate proxy configuration + if proxyConfig == nil { + return fmt.Errorf("proxy configuration cannot be nil") + } + + if proxyConfig.Host == "" { + return fmt.Errorf("proxy host is required") + } + + if proxyConfig.Port == "" { + return fmt.Errorf("proxy port is required") + } + + proxyConfig.Protocol = utils.NormalizeProxyProtocol(proxyConfig.Protocol, proxyConfig.Port) + + // Convert proxy config to JSON + proxyJSON, err := json.Marshal(proxyConfig) + if err != nil { + i.loggerWrapper.GetLogger(id).LogError("[%s] Failed to marshal proxy config: %v", id, err) + return fmt.Errorf("failed to marshal proxy configuration: %v", err) + } + + instance.Proxy = string(proxyJSON) + + // Update instance in database + err = i.instanceRepository.Update(instance) + if err != nil { + i.loggerWrapper.GetLogger(id).LogError("[%s] Failed to update instance with proxy: %v", id, err) + return err + } + + i.loggerWrapper.GetLogger(id).LogInfo("[%s] Proxy configuration updated: %s://%s:%s", id, proxyConfig.Protocol, proxyConfig.Host, proxyConfig.Port) + + // Reconnect to apply proxy changes + go i.Reconnect(instance) + + return nil +} + +func (i instances) SetProxyFromStruct(id string, data *SetProxyStruct) error { + if data == nil { + return fmt.Errorf("proxy data cannot be nil") + } + + proxyConfig := &ProxyConfig{ + Protocol: data.Protocol, + Host: data.Host, + Port: data.Port, + Username: data.Username, + Password: data.Password, + } + + return i.SetProxy(id, proxyConfig) +} + +func (i instances) RemoveProxy(id string) error { + instance, err := i.instanceRepository.GetInstanceByID(id) + if err != nil { + return err + } + + instance.Proxy = "" + + err = i.instanceRepository.Update(instance) + if err != nil { + return err + } + + i.loggerWrapper.GetLogger(id).LogInfo("[%s] Proxy configuration removed", id) + + go i.Reconnect(instance) + + return nil +} + +func (i instances) ForceReconnect(instanceId string, number string) error { + if i.clientPointer[instanceId].IsConnected() && i.clientPointer[instanceId].IsLoggedIn() { + return fmt.Errorf("client already connected") + } + + err := i.whatsmeowService.ForceUpdateJid(instanceId, number) + if err != nil { + return err + } + + instance, err := i.instanceRepository.GetInstanceByID(instanceId) + if err != nil { + return err + } + + subscribedEvents := strings.Split(instance.Events, ",") + + i.killChannel[instance.Id] = make(chan bool) + + clientData := &whatsmeow_service.ClientData{ + Instance: instance, + Subscriptions: subscribedEvents, + Phone: "", + IsProxy: false, + } + + if instance.Proxy != "" || i.config.ProxyHost != "" { + var proxyConfig ProxyConfig + err := json.Unmarshal([]byte(instance.Proxy), &proxyConfig) + if err != nil { + i.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error unmarshalling proxy config: %v", instance.Id, err) + return err + } + + if proxyConfig.Host != "" || i.config.ProxyHost != "" { + clientData.IsProxy = true + } + } + + if i.clientPointer[instance.Id] != nil { + client := i.clientPointer[instance.Id] + client.Disconnect() + + select { + case i.killChannel[instance.Id] <- true: + case <-time.After(5 * time.Second): + } + + delete(i.clientPointer, instance.Id) + delete(i.killChannel, instance.Id) + } + + go i.whatsmeowService.StartClient(clientData) + + time.Sleep(2 * time.Second) + + if i.clientPointer[instance.Id] != nil { + if !i.clientPointer[instance.Id].IsConnected() { + return fmt.Errorf("failed to connect") + } + + if !i.clientPointer[instance.Id].IsLoggedIn() { + return fmt.Errorf("failed to login") + } + } else { + return fmt.Errorf("failed to connect") + } + + return nil +} + +func (i instances) GetInstanceByToken(token string) (*instance_model.Instance, error) { + return i.instanceRepository.GetInstanceByToken(token) +} + +func (i instances) GetLogs(instanceId string, startDate, endDate time.Time, level string, limit int) ([]logger_wrapper.LogEntry, error) { + // Inicializa o slice vazio para garantir que nunca retorne null + logs := make([]logger_wrapper.LogEntry, 0) + + // Define valores padrão + if limit <= 0 { + limit = 100 // Limite padrão de 100 registros + } + + // Se não foi fornecida data inicial, usa 7 dias atrás + if startDate.IsZero() { + startDate = time.Now().AddDate(0, 0, -7) + } + + // Se não foi fornecida data final, usa data atual + if endDate.IsZero() { + endDate = time.Now() + } + + // Ajusta as datas para início e fim do dia + startDate = time.Date(startDate.Year(), startDate.Month(), startDate.Day(), 0, 0, 0, 0, time.UTC) + endDate = time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 999999999, time.UTC) + + // Garante que a data inicial não seja posterior à data final + if startDate.After(endDate) { + return logs, fmt.Errorf("data inicial não pode ser posterior à data final") + } + + // Níveis de log válidos + validLevels := map[string]bool{ + "INFO": true, + "ERROR": true, + "WARN": true, + "DEBUG": true, + } + + var levelArray []string + if level == "" { + // Se nenhum nível foi especificado, usa todos + levelArray = []string{"INFO", "ERROR", "WARN", "DEBUG"} + } else { + // Divide e normaliza os níveis fornecidos + for _, l := range strings.Split(level, ",") { + l = strings.TrimSpace(strings.ToUpper(l)) + if !validLevels[l] { + return logs, fmt.Errorf("nível de log inválido: %s", l) + } + levelArray = append(levelArray, l) + } + } + + // Lê os logs do arquivo + logPath := filepath.Join(i.config.LogDirectory, instanceId, "instance.log") + file, err := os.Open(logPath) + if err != nil { + if os.IsNotExist(err) { + return logs, nil // Retorna array vazio se arquivo não existir + } + return logs, fmt.Errorf("erro ao abrir arquivo de log: %v", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + + // Aumenta o buffer do scanner para lidar com linhas grandes + const maxCapacity = 1024 * 1024 // 1MB + buf := make([]byte, maxCapacity) + scanner.Buffer(buf, maxCapacity) + + for scanner.Scan() { + var entry logger_wrapper.LogEntry + if err := json.Unmarshal(scanner.Bytes(), &entry); err != nil { + continue // Ignora linhas inválidas + } + + // Ajusta o timestamp da entrada para UTC para comparação correta + entry.Timestamp = entry.Timestamp.UTC() + + // Aplica os filtros + if entry.Timestamp.Before(startDate) || entry.Timestamp.After(endDate) { + continue + } + + if !slices.Contains(levelArray, entry.Level) { + continue + } + + logs = append(logs, entry) + + // Verifica o limite + if len(logs) >= limit { + break + } + } + + if err := scanner.Err(); err != nil { + return logs, fmt.Errorf("erro ao ler arquivo de log: %v", err) + } + + // Ordena os logs por timestamp em ordem decrescente + sort.Slice(logs, func(i, j int) bool { + return logs[i].Timestamp.After(logs[j].Timestamp) + }) + + return logs, nil +} + +func (i instances) GetAdvancedSettings(instanceId string) (*instance_model.AdvancedSettings, error) { + i.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Getting advanced settings", instanceId) + + settings, err := i.instanceRepository.GetAdvancedSettings(instanceId) + if err != nil { + i.loggerWrapper.GetLogger(instanceId).LogError("[%s] Error getting advanced settings: %v", instanceId, err) + return nil, err + } + + return settings, nil +} + +func (i instances) UpdateAdvancedSettings(instanceId string, settings *instance_model.AdvancedSettings) error { + i.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Updating advanced settings", instanceId) + + err := i.instanceRepository.UpdateAdvancedSettings(instanceId, settings) + if err != nil { + i.loggerWrapper.GetLogger(instanceId).LogError("[%s] Error updating advanced settings: %v", instanceId, err) + return err + } + + // Sincroniza as configurações na instância em execução + err = i.whatsmeowService.UpdateInstanceAdvancedSettings(instanceId) + if err != nil { + i.loggerWrapper.GetLogger(instanceId).LogWarn("[%s] Error syncing advanced settings to runtime: %v", instanceId, err) + // Não falha a operação, apenas loga o warning + } + + i.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Advanced settings updated successfully", instanceId) + return nil +} + +func NewInstanceService( + instanceRepository instance_repository.InstanceRepository, + killChannel map[string](chan bool), + clientPointer map[string]*whatsmeow.Client, + whatsmeowService whatsmeow_service.WhatsmeowService, + config *config.Config, + loggerWrapper *logger_wrapper.LoggerManager, +) InstanceService { + return &instances{ + instanceRepository: instanceRepository, + killChannel: killChannel, + clientPointer: clientPointer, + whatsmeowService: whatsmeowService, + config: config, + loggerWrapper: loggerWrapper, + } +} diff --git a/whatsapp-service/pkg/internal/event_types/event_types.go b/whatsapp-service/pkg/internal/event_types/event_types.go new file mode 100644 index 0000000000000000000000000000000000000000..1eabb85d5610d227f5bbfd448724c42c953dec87 --- /dev/null +++ b/whatsapp-service/pkg/internal/event_types/event_types.go @@ -0,0 +1,64 @@ +package event_types + +const ( + ALL = "ALL" + MESSAGE = "MESSAGE" + SEND_MESSAGE = "SEND_MESSAGE" + READ_RECEIPT = "READ_RECEIPT" + PRESENCE = "PRESENCE" + HISTORY_SYNC = "HISTORY_SYNC" + CHAT_PRESENCE = "CHAT_PRESENCE" + CALL = "CALL" + CONNECTION = "CONNECTION" + LABEL = "LABEL" + CONTACT = "CONTACT" + GROUP = "GROUP" + NEWSLETTER = "NEWSLETTER" + QRCODE = "QRCODE" + BUTTON_CLICK = "BUTTON_CLICK" + PICTURE = "PICTURE" + USER_ABOUT = "USER_ABOUT" +) + +var AllEventTypes = []string{ + MESSAGE, + SEND_MESSAGE, + READ_RECEIPT, + PRESENCE, + HISTORY_SYNC, + CHAT_PRESENCE, + CALL, + CONNECTION, + LABEL, + CONTACT, + GROUP, + NEWSLETTER, + QRCODE, + BUTTON_CLICK, + PICTURE, + USER_ABOUT, +} + +var validEventTypes = map[string]bool{ + ALL: true, + MESSAGE: true, + SEND_MESSAGE: true, + READ_RECEIPT: true, + PRESENCE: true, + HISTORY_SYNC: true, + CHAT_PRESENCE: true, + CALL: true, + CONNECTION: true, + LABEL: true, + CONTACT: true, + GROUP: true, + NEWSLETTER: true, + QRCODE: true, + BUTTON_CLICK: true, + PICTURE: true, + USER_ABOUT: true, +} + +func IsEventType(eventType string) bool { + return validEventTypes[eventType] +} diff --git a/whatsapp-service/pkg/label/handler/label_handler.go b/whatsapp-service/pkg/label/handler/label_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..3c1269290f588eac62709356a1ec3f81d31fe1f2 --- /dev/null +++ b/whatsapp-service/pkg/label/handler/label_handler.go @@ -0,0 +1,297 @@ +package label_handler + +import ( + "net/http" + + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + label_service "agentdeck-whatsapp-service/pkg/label/service" + "github.com/gin-gonic/gin" +) + +type LabelHandler interface { + ChatLabel(ctx *gin.Context) + MessageLabel(ctx *gin.Context) + EditLabel(ctx *gin.Context) + ChatUnlabel(ctx *gin.Context) + MessageUnlabel(ctx *gin.Context) + GetLabels(ctx *gin.Context) +} + +type labelHandler struct { + labelService label_service.LabelService +} + +// Add label to chat +// @Summary Add label to chat +// @Description Add label to chat +// @Tags Label +// @Accept json +// @Produce json +// @Param message body label_service.ChatLabelStruct true "Label data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /label/chat [post] +func (l *labelHandler) ChatLabel(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *label_service.ChatLabelStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.JID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "jid is required"}) + return + } + + if data.LabelID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "label id is required"}) + return + } + + err = l.labelService.ChatLabel(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Add label to message +// @Summary Add label to message +// @Description Add label to message +// @Tags Label +// @Accept json +// @Produce json +// @Param message body label_service.MessageLabelStruct true "Label data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /label/message [post] +func (l *labelHandler) MessageLabel(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *label_service.MessageLabelStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.JID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "jid is required"}) + return + } + + if data.LabelID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "label id is required"}) + return + } + + if data.MessageID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "message id is required"}) + return + } + + err = l.labelService.MessageLabel(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Edit label +// @Summary Edit label +// @Description Edit label +// @Tags Label +// @Accept json +// @Produce json +// @Param message body label_service.EditLabelStruct true "Label data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /label/edit [post] +func (l *labelHandler) EditLabel(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *label_service.EditLabelStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.LabelID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "label id is required"}) + return + } + + if data.Name == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) + return + } + + err = l.labelService.EditLabel(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Remove label from chat +// @Summary Remove label from chat +// @Description Remove label from chat +// @Tags Label +// @Accept json +// @Produce json +// @Param message body label_service.ChatLabelStruct true "Label data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /unlabel/chat [post] +func (l *labelHandler) ChatUnlabel(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *label_service.ChatLabelStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.JID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "jid is required"}) + return + } + + if data.LabelID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "label id is required"}) + return + } + + err = l.labelService.ChatUnlabel(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Remove label from message +// @Summary Remove label from message +// @Description Remove label from message +// @Tags Label +// @Accept json +// @Produce json +// @Param message body label_service.MessageLabelStruct true "Label data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /unlabel/message [post] +func (l *labelHandler) MessageUnlabel(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *label_service.MessageLabelStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.JID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "jid is required"}) + return + } + + if data.LabelID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "label id is required"}) + return + } + + if data.MessageID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "message id is required"}) + return + } + + err = l.labelService.MessageUnlabel(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Get all labels +// @Summary Get all labels +// @Description Get all labels +// @Tags Label +// @Accept json +// @Produce json +// @Success 200 {object} gin.H "success" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /label/list [get] +func (l *labelHandler) GetLabels(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + labels, err := l.labelService.GetLabels(instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, labels) +} + +func NewLabelHandler( + labelService label_service.LabelService, +) LabelHandler { + return &labelHandler{ + labelService: labelService, + } +} diff --git a/whatsapp-service/pkg/label/model/label_model.go b/whatsapp-service/pkg/label/model/label_model.go new file mode 100644 index 0000000000000000000000000000000000000000..6509938a0a47ab1bca7220540f036fade91991a1 --- /dev/null +++ b/whatsapp-service/pkg/label/model/label_model.go @@ -0,0 +1,10 @@ +package label_model + +type Label struct { + Id string `json:"id"` + InstanceID string `json:"instance_id"` + LabelID string `json:"label_id"` + LabelName string `json:"label_name"` + LabelColor string `json:"label_color"` + PredefinedId string `json:"predefined_id"` +} diff --git a/whatsapp-service/pkg/label/repository/label_repository.go b/whatsapp-service/pkg/label/repository/label_repository.go new file mode 100644 index 0000000000000000000000000000000000000000..f031c12cd7842218acec14ea66ec084843ce21c4 --- /dev/null +++ b/whatsapp-service/pkg/label/repository/label_repository.go @@ -0,0 +1,144 @@ +package label_repository + +import ( + "context" + + label_model "agentdeck-whatsapp-service/pkg/label/model" + "agentdeck-whatsapp-service/pkg/supabase" + "github.com/google/uuid" +) + +type LabelRepository interface { + InsertLabel(label label_model.Label) error + UpdateLabel(label label_model.Label) error + GetLabelByID(id string) (*label_model.Label, error) + DeleteLabel(id string) error + GetAllLabelsByInstanceID(instanceID string) ([]label_model.Label, error) + UpsertLabel(label label_model.Label) error +} + +type labelRepository struct { + supa *supabase.Client +} + +// labelRow is the snake_case PostgREST representation of a labels row. +type labelRow struct { + Id string `json:"id"` + InstanceID string `json:"instance_id"` + LabelID string `json:"label_id"` + LabelName string `json:"label_name"` + LabelColor string `json:"label_color"` + PredefinedId string `json:"predefined_id"` +} + +func (l *labelRepository) InsertLabel(label label_model.Label) error { + if label.Id == "" { + label.Id = uuid.New().String() + } + ctx := context.Background() + return l.supa.Table("wp_labels").Insert(ctx, labelRow{ + Id: label.Id, + InstanceID: label.InstanceID, + LabelID: label.LabelID, + LabelName: label.LabelName, + LabelColor: label.LabelColor, + PredefinedId: label.PredefinedId, + }, "", nil) +} + +func (l *labelRepository) UpdateLabel(label label_model.Label) error { + ctx := context.Background() + row := labelRow{ + Id: label.Id, + InstanceID: label.InstanceID, + LabelID: label.LabelID, + LabelName: label.LabelName, + LabelColor: label.LabelColor, + PredefinedId: label.PredefinedId, + } + q := supabase.NewQuery().Eq("id", label.Id) + return l.supa.Table("wp_labels").Update(ctx, q, row) +} + +func (l *labelRepository) GetLabelByID(id string) (*label_model.Label, error) { + q := supabase.NewQuery().Eq("id", id).Limit(1) + var rows []labelRow + ctx := context.Background() + if err := l.supa.Table("wp_labels").Select(ctx, q, &rows); err != nil { + return nil, err + } + if len(rows) == 0 { + return nil, nil + } + return &label_model.Label{ + Id: rows[0].Id, + InstanceID: rows[0].InstanceID, + LabelID: rows[0].LabelID, + LabelName: rows[0].LabelName, + LabelColor: rows[0].LabelColor, + PredefinedId: rows[0].PredefinedId, + }, nil +} + +func (l *labelRepository) DeleteLabel(id string) error { + ctx := context.Background() + q := supabase.NewQuery().Eq("id", id) + return l.supa.Table("wp_labels").Delete(ctx, q) +} + +func (l *labelRepository) GetAllLabelsByInstanceID(instanceID string) ([]label_model.Label, error) { + q := supabase.NewQuery().Eq("instance_id", instanceID) + var rows []labelRow + ctx := context.Background() + if err := l.supa.Table("wp_labels").Select(ctx, q, &rows); err != nil { + return nil, err + } + out := make([]label_model.Label, 0, len(rows)) + for _, r := range rows { + out = append(out, label_model.Label{ + Id: r.Id, + InstanceID: r.InstanceID, + LabelID: r.LabelID, + LabelName: r.LabelName, + LabelColor: r.LabelColor, + PredefinedId: r.PredefinedId, + }) + } + return out, nil +} + +func (l *labelRepository) UpsertLabel(label label_model.Label) error { + ctx := context.Background() + // PostgREST merge-duplicates uses the table's unique constraint; the labels + // table's practical uniqueness is (instance_id, label_id) but the PK is id. + // Keep the original FirstOrCreate semantics: look it up first. + q := supabase.NewQuery().Eq("instance_id", label.InstanceID).Eq("label_id", label.LabelID).Limit(1) + var rows []labelRow + if err := l.supa.Table("wp_labels").Select(ctx, q, &rows); err != nil { + return err + } + if len(rows) == 0 { + if label.Id == "" { + label.Id = uuid.New().String() + } + return l.supa.Table("wp_labels").Insert(ctx, labelRow{ + Id: label.Id, + InstanceID: label.InstanceID, + LabelID: label.LabelID, + LabelName: label.LabelName, + LabelColor: label.LabelColor, + PredefinedId: label.PredefinedId, + }, "", nil) + } + + row := rows[0] + row.LabelName = label.LabelName + row.LabelColor = label.LabelColor + row.PredefinedId = label.PredefinedId + q2 := supabase.NewQuery().Eq("id", rows[0].Id) + return l.supa.Table("wp_labels").Update(ctx, q2, row) +} + +func NewLabelRepository(supa *supabase.Client) LabelRepository { + return &labelRepository{supa: supa} +} \ No newline at end of file diff --git a/whatsapp-service/pkg/label/service/label_service.go b/whatsapp-service/pkg/label/service/label_service.go new file mode 100644 index 0000000000000000000000000000000000000000..95baf32a63803bd087b0b42f03561fe23baf0321 --- /dev/null +++ b/whatsapp-service/pkg/label/service/label_service.go @@ -0,0 +1,240 @@ +package label_service + +import ( + "context" + "errors" + "time" + + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + label_model "agentdeck-whatsapp-service/pkg/label/model" + label_repository "agentdeck-whatsapp-service/pkg/label/repository" + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + "agentdeck-whatsapp-service/pkg/utils" + whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/appstate" +) + +type LabelService interface { + ChatLabel(data *ChatLabelStruct, instance *instance_model.Instance) error + MessageLabel(data *MessageLabelStruct, instance *instance_model.Instance) error + EditLabel(data *EditLabelStruct, instance *instance_model.Instance) error + ChatUnlabel(data *ChatLabelStruct, instance *instance_model.Instance) error + MessageUnlabel(data *MessageLabelStruct, instance *instance_model.Instance) error + GetLabels(instance *instance_model.Instance) ([]label_model.Label, error) +} + +type labelService struct { + clientPointer map[string]*whatsmeow.Client + whatsmeowService whatsmeow_service.WhatsmeowService + labelRepository label_repository.LabelRepository + loggerWrapper *logger_wrapper.LoggerManager +} + +type ChatLabelStruct struct { + JID string `json:"jid"` + LabelID string `json:"labelId"` +} + +type MessageLabelStruct struct { + JID string `json:"jid"` + LabelID string `json:"labelId"` + MessageID string `json:"messageId"` +} + +type EditLabelStruct struct { + LabelID string `json:"labelId"` + Name string `json:"name"` + Color int `json:"color"` + Deleted bool `json:"deleted"` +} + +func (l *labelService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + client := l.clientPointer[instanceId] + l.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) + + if client == nil { + l.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] No client found, attempting to start new instance", instanceId) + err := l.whatsmeowService.StartInstance(instanceId) + if err != nil { + l.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to start instance: %v", instanceId, err) + return nil, errors.New("no active session found") + } + + l.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) + time.Sleep(2 * time.Second) + + client = l.clientPointer[instanceId] + l.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + + if client == nil || !client.IsConnected() { + l.loggerWrapper.GetLogger(instanceId).LogError("[%s] New client validation failed - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + return nil, errors.New("no active session found") + } + } else if !client.IsConnected() { + l.loggerWrapper.GetLogger(instanceId).LogError("[%s] Existing client is disconnected - Connected status: %v", + instanceId, + client.IsConnected()) + return nil, errors.New("client disconnected") + } + + l.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected()) + return client, nil +} + +func (l *labelService) ChatLabel(data *ChatLabelStruct, instance *instance_model.Instance) error { + client, err := l.ensureClientConnected(instance.Id) + if err != nil { + return err + } + + jid, ok := utils.ParseJID(data.JID) + if !ok { + l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error parse community jid", instance.Id) + return errors.New("error parse community jid") + } + + err = client.SendAppState(context.Background(), appstate.BuildLabelChat( + jid, + data.LabelID, + true, + )) + if err != nil { + l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error label chat: %v", instance.Id, err) + return err + } + + return nil +} + +func (l *labelService) MessageLabel(data *MessageLabelStruct, instance *instance_model.Instance) error { + client, err := l.ensureClientConnected(instance.Id) + if err != nil { + return err + } + + jid, ok := utils.ParseJID(data.JID) + if !ok { + l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error parse community jid", instance.Id) + return errors.New("error parse community jid") + } + + err = client.SendAppState(context.Background(), appstate.BuildLabelMessage( + jid, + data.LabelID, + data.MessageID, + true, + )) + if err != nil { + l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error label message: %v", instance.Id, err) + return err + } + + return nil +} + +func (l *labelService) EditLabel(data *EditLabelStruct, instance *instance_model.Instance) error { + client, err := l.ensureClientConnected(instance.Id) + if err != nil { + return err + } + + err = client.SendAppState(context.Background(), appstate.BuildLabelEdit( + data.LabelID, + data.Name, + int32(data.Color), + data.Deleted, + )) + if err != nil { + l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error label message: %v", instance.Id, err) + return err + } + + return nil +} + +func (l *labelService) ChatUnlabel(data *ChatLabelStruct, instance *instance_model.Instance) error { + client, err := l.ensureClientConnected(instance.Id) + if err != nil { + return err + } + + jid, ok := utils.ParseJID(data.JID) + if !ok { + l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error parse community jid", instance.Id) + return errors.New("error parse community jid") + } + + err = client.SendAppState(context.Background(), appstate.BuildLabelChat( + jid, + data.LabelID, + false, + )) + if err != nil { + l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error label chat: %v", instance.Id, err) + return err + } + + return nil +} + +func (l *labelService) MessageUnlabel(data *MessageLabelStruct, instance *instance_model.Instance) error { + client, err := l.ensureClientConnected(instance.Id) + if err != nil { + return err + } + + jid, ok := utils.ParseJID(data.JID) + if !ok { + l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error parse community jid", instance.Id) + return errors.New("error parse community jid") + } + + err = client.SendAppState(context.Background(), appstate.BuildLabelMessage( + jid, + data.LabelID, + data.MessageID, + false, + )) + if err != nil { + l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error label message: %v", instance.Id, err) + return err + } + + return nil +} + +func (l *labelService) GetLabels(instance *instance_model.Instance) ([]label_model.Label, error) { + _, err := l.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + labels, err := l.labelRepository.GetAllLabelsByInstanceID(instance.Id) + if err != nil { + l.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error fetching labels from database: %v", instance.Id, err) + return nil, err + } + + return labels, nil +} + +func NewLabelService( + clientPointer map[string]*whatsmeow.Client, + whatsmeowService whatsmeow_service.WhatsmeowService, + labelRepository label_repository.LabelRepository, + loggerWrapper *logger_wrapper.LoggerManager, +) LabelService { + return &labelService{ + clientPointer: clientPointer, + whatsmeowService: whatsmeowService, + labelRepository: labelRepository, + loggerWrapper: loggerWrapper, + } +} diff --git a/whatsapp-service/pkg/logger/logger.go b/whatsapp-service/pkg/logger/logger.go new file mode 100644 index 0000000000000000000000000000000000000000..150de9d0cc9efd3ea69e5ad5e411c8aefefec4f0 --- /dev/null +++ b/whatsapp-service/pkg/logger/logger.go @@ -0,0 +1,146 @@ +package logger + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "agentdeck-whatsapp-service/pkg/config" + "github.com/gomessguii/logger" + "gopkg.in/natefinch/lumberjack.v2" +) + +type LoggerManager struct { + config *config.Config + loggers map[string]*Logger + mu sync.RWMutex +} + +type Logger struct { + config *config.Config + instanceId string + mu sync.Mutex + writer *lumberjack.Logger +} + +type LogEntry struct { + Timestamp time.Time `json:"timestamp"` + Level string `json:"level"` + InstanceId string `json:"instance_id"` + Message string `json:"message"` + Metadata json.RawMessage `json:"metadata,omitempty"` +} + +func NewLoggerManager(config *config.Config) *LoggerManager { + // Garante que o diretório base de logs existe + if err := os.MkdirAll(config.LogDirectory, 0755); err != nil { + logger.LogError("Falha ao criar diretório base de logs: %v", err) + } + + return &LoggerManager{ + config: config, + loggers: make(map[string]*Logger), + } +} + +func (lm *LoggerManager) GetLogger(instanceId string) *Logger { + lm.mu.RLock() + logger, exists := lm.loggers[instanceId] + lm.mu.RUnlock() + + if exists { + return logger + } + + lm.mu.Lock() + defer lm.mu.Unlock() + + // Verificar novamente após obter o lock de escrita + if logger, exists = lm.loggers[instanceId]; exists { + return logger + } + + // Criar novo logger para a instância + logger = newLogger(instanceId, lm.config) + lm.loggers[instanceId] = logger + return logger +} + +func newLogger(instanceId string, config *config.Config) *Logger { + // Garante que o diretório existe + logPath := filepath.Join(config.LogDirectory, instanceId) + os.MkdirAll(logPath, 0755) + + logFile := filepath.Join(logPath, "instance.log") + + writer := &lumberjack.Logger{ + Filename: logFile, + MaxSize: config.LogMaxSize, + MaxBackups: config.LogMaxBackups, + MaxAge: config.LogMaxAge, + Compress: config.LogCompress, + } + + return &Logger{ + config: config, + instanceId: instanceId, + writer: writer, + } +} + +func (l *Logger) LogInfo(format string, args ...interface{}) { + l.log("INFO", format, args...) + logger.LogInfo(format, args...) +} + +func (l *Logger) LogError(format string, args ...interface{}) { + l.log("ERROR", format, args...) + logger.LogError(format, args...) +} + +func (l *Logger) LogWarn(format string, args ...interface{}) { + l.log("WARN", format, args...) + logger.LogWarn(format, args...) +} + +func (l *Logger) LogDebug(format string, args ...interface{}) { + l.log("DEBUG", format, args...) + logger.LogDebug(format, args...) +} + +func (l *Logger) log(level string, format string, args ...interface{}) { + l.mu.Lock() + defer l.mu.Unlock() + + entry := LogEntry{ + Timestamp: time.Now(), + Level: level, + InstanceId: l.instanceId, + Message: fmt.Sprintf(format, args...), + } + + jsonEntry, err := json.Marshal(entry) + if err != nil { + logger.LogError("Failed to marshal log entry: %v", err) + return + } + + if _, err := l.writer.Write(append(jsonEntry, '\n')); err != nil { + logger.LogError("Failed to write log: %v", err) + } +} + +func (l *Logger) Close() error { + l.mu.Lock() + defer l.mu.Unlock() + return l.writer.Close() +} + +// GetLogs retorna os logs da instância com filtros opcionais +func (l *Logger) GetLogs(startDate, endDate time.Time, level string, limit int) ([]LogEntry, error) { + // Implementação movida para o service + return nil, fmt.Errorf("método movido para instance_service") +} diff --git a/whatsapp-service/pkg/message/handler/message_handler.go b/whatsapp-service/pkg/message/handler/message_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..f67dc6c7cc6dec6150a29fb8e61e2adddbbf17bf --- /dev/null +++ b/whatsapp-service/pkg/message/handler/message_handler.go @@ -0,0 +1,422 @@ +package message_handler + +import ( + "net/http" + + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + message_service "agentdeck-whatsapp-service/pkg/message/service" + "github.com/gin-gonic/gin" +) + +type MessageHandler interface { + React(ctx *gin.Context) + ChatPresence(ctx *gin.Context) + MarkRead(ctx *gin.Context) + MarkPlayed(ctx *gin.Context) + DownloadMedia(ctx *gin.Context) + GetMessageStatus(ctx *gin.Context) + DeleteMessageEveryone(ctx *gin.Context) + EditMessage(ctx *gin.Context) +} + +type messageHandler struct { + messageService message_service.MessageService +} + +// React a message +// @Summary React a message +// @Description React to a message with support for fromMe field and participant field for group messages +// @Tags Message +// @Accept json +// @Produce json +// @Param message body message_service.ReactStruct true "React to a message with fromMe and participant fields" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /message/react [post] +func (m *messageHandler) React(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *message_service.ReactStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if data.Reaction == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "message reaction is required"}) + return + } + + message, err := m.messageService.React(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) +} + +// ChatPresence set chat presence +// @Summary Set chat presence +// @Description Set chat presence +// @Tags Message +// @Accept json +// @Produce json +// @Param message body message_service.ChatPresenceStruct true "Set chat presence" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /message/presence [post] +func (m *messageHandler) ChatPresence(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *message_service.ChatPresenceStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if data.State == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "state is required"}) + return + } + + ts, err := m.messageService.ChatPresence(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + responseData := gin.H{ + "timestamp": ts, + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// MarkRead mark a message as read +// @Summary Mark a message as read +// @Description Mark a message as read +// @Tags Message +// @Accept json +// @Produce json +// @Param message body message_service.MarkReadStruct true "Mark a message as read" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /message/markread [post] +func (m *messageHandler) MarkRead(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *message_service.MarkReadStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if len(data.Id) < 1 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "id is required"}) + return + } + + ts, err := m.messageService.MarkRead(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + responseData := gin.H{ + "timestamp": ts, + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// MarkPlayed mark an audio message as played (blue mic icon) +// @Summary Mark an audio message as played +// @Description Mark an audio message as played +// @Tags Message +// @Accept json +// @Produce json +// @Param message body message_service.MarkPlayedStruct true "Mark an audio message as played" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /message/markplayed [post] +func (m *messageHandler) MarkPlayed(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *message_service.MarkPlayedStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if len(data.Id) < 1 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "id is required"}) + return + } + + ts, err := m.messageService.MarkPlayed(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + responseData := gin.H{ + "timestamp": ts, + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// DownloadMedia download a media message (image, video, audio, document) +// @Summary Download media +// @Description Download the media content of a message (image, video, audio or document) +// @Tags Message +// @Accept json +// @Produce json +// @Param message body message_service.DownloadMediaStruct true "Download media" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /message/downloadmedia [post] +func (m *messageHandler) DownloadMedia(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *message_service.DownloadMediaStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + dataUrl, ts, err := m.messageService.DownloadMedia(data, instance, ctx.Request) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + responseData := gin.H{ + "base64": dataUrl.String(), + "timestamp": ts, + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// GetMessageStatus get message status +// @Summary Get message status +// @Description Get message status +// @Tags Message +// @Accept json +// @Produce json +// @Param message body message_service.MessageStatusStruct true "Get message status" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /message/status [post] +func (m *messageHandler) GetMessageStatus(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *message_service.MessageStatusStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Id == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "id is required"}) + return + } + + message, ts, err := m.messageService.GetMessageStatus(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + responseData := gin.H{ + "result": message, + "timestamp": ts, + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// DeleteMessageEveryone delete a message for everyone +// @Summary Delete a message for everyone +// @Description Delete a message for everyone +// @Tags Message +// @Accept json +// @Produce json +// @Param message body message_service.MessageStruct true "Delete a message for everyone" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /message/delete [post] +func (m *messageHandler) DeleteMessageEveryone(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *message_service.MessageStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Chat == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"}) + return + } + + if data.MessageID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "messageId is required"}) + return + } + + msgId, ts, err := m.messageService.DeleteMessageEveryone(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + responseData := gin.H{ + "messageId": msgId, + "timestamp": ts, + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// EditMessage edit a message +// @Summary Edit a message +// @Description Edit a message +// @Tags Message +// @Accept json +// @Produce json +// @Param message body message_service.EditMessageStruct true "Edit a message" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /message/edit [post] +func (m *messageHandler) EditMessage(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *message_service.EditMessageStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Chat == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "chat is required"}) + return + } + + if data.Message == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "message is required"}) + return + } + + if data.MessageID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "messageId is required"}) + return + } + + msgId, ts, err := m.messageService.EditMessage(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + responseData := gin.H{ + "messageId": msgId, + "timestamp": ts, + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +func NewMessageHandler( + messageService message_service.MessageService, +) MessageHandler { + return &messageHandler{ + messageService: messageService, + } +} diff --git a/whatsapp-service/pkg/message/model/message_model.go b/whatsapp-service/pkg/message/model/message_model.go new file mode 100644 index 0000000000000000000000000000000000000000..2b2c1df6fdfc1317783b7e49c1ce894b6a343fb4 --- /dev/null +++ b/whatsapp-service/pkg/message/model/message_model.go @@ -0,0 +1,14 @@ +package message_model + +import ( + "encoding/json" +) + +type Message struct { + Id string `json:"id"` + MessageID string `json:"message_id"` + Timestamp string `json:"timestamp"` + Status string `json:"status"` + Source string `json:"source"` + Referral json.RawMessage `json:"referral,omitempty"` +} diff --git a/whatsapp-service/pkg/message/repository/message_repository.go b/whatsapp-service/pkg/message/repository/message_repository.go new file mode 100644 index 0000000000000000000000000000000000000000..996a03b3e08bb33776953c4b21edb974ba34fa9c --- /dev/null +++ b/whatsapp-service/pkg/message/repository/message_repository.go @@ -0,0 +1,104 @@ +package message_repository + +import ( + "context" + "encoding/json" + + message_model "agentdeck-whatsapp-service/pkg/message/model" + "agentdeck-whatsapp-service/pkg/supabase" + "github.com/google/uuid" +) + +type MessageRepository interface { + InsertMessage(message message_model.Message) error + GetMessageByID(messageID string) (*message_model.Message, error) + DeleteAllMessages() (int64, error) + GetLatestMessageID(source string) (string, string, error) +} + +type messageRepository struct { + supa *supabase.Client +} + +// messageRow is the snake_case PostgREST representation of a messages row. +type messageRow struct { + Id string `json:"id"` + MessageID string `json:"message_id"` + Timestamp string `json:"timestamp"` + Status string `json:"status"` + Source string `json:"source"` + Referral json.RawMessage `json:"referral,omitempty"` +} + +func (m *messageRepository) InsertMessage(message message_model.Message) error { + ctx := context.Background() + row := messageRow{ + Id: message.Id, + MessageID: message.MessageID, + Timestamp: message.Timestamp, + Status: message.Status, + Source: message.Source, + Referral: message.Referral, + } + if row.Id == "" { + row.Id = uuid.New().String() + } + // Upsert on message_id: merge-duplicates updates existing rows. When the + // incoming referral is empty we must not clobber a previously stored one. + payload := map[string]interface{}{ + "id": row.Id, + "message_id": row.MessageID, + "timestamp": row.Timestamp, + "status": row.Status, + "source": row.Source, + } + if len(row.Referral) > 0 { + payload["referral"] = row.Referral + } + return m.supa.Table("wp_messages").Upsert(ctx, payload, []string{"message_id"}, nil) +} + +func (m *messageRepository) GetMessageByID(messageID string) (*message_model.Message, error) { + q := supabase.NewQuery().Eq("message_id", messageID).Limit(1) + var rows []messageRow + ctx := context.Background() + if err := m.supa.Table("wp_messages").Select(ctx, q, &rows); err != nil { + return nil, err + } + if len(rows) == 0 { + return nil, nil + } + return &message_model.Message{ + Id: rows[0].Id, + MessageID: rows[0].MessageID, + Timestamp: rows[0].Timestamp, + Status: rows[0].Status, + Source: rows[0].Source, + Referral: rows[0].Referral, + }, nil +} + +func (m *messageRepository) DeleteAllMessages() (int64, error) { + ctx := context.Background() + if err := m.supa.Table("wp_messages").Delete(ctx, supabase.NewQuery()); err != nil { + return 0, err + } + return 0, nil +} + +func (m *messageRepository) GetLatestMessageID(source string) (string, string, error) { + q := supabase.NewQuery().Eq("source", source).Order("timestamp", true).Limit(1) + var rows []messageRow + ctx := context.Background() + if err := m.supa.Table("wp_messages").Select(ctx, q, &rows); err != nil { + return "", "", err + } + if len(rows) == 0 { + return "", "", nil + } + return rows[0].MessageID, rows[0].Timestamp, nil +} + +func NewMessageRepository(supa *supabase.Client) MessageRepository { + return &messageRepository{supa: supa} +} \ No newline at end of file diff --git a/whatsapp-service/pkg/message/service/message_service.go b/whatsapp-service/pkg/message/service/message_service.go new file mode 100644 index 0000000000000000000000000000000000000000..9977c14c9af87e4600c49aa18b14b05be92b20cf --- /dev/null +++ b/whatsapp-service/pkg/message/service/message_service.go @@ -0,0 +1,540 @@ +package message_service + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "strings" + "time" + + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + message_model "agentdeck-whatsapp-service/pkg/message/model" + message_repository "agentdeck-whatsapp-service/pkg/message/repository" + "agentdeck-whatsapp-service/pkg/utils" + whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service" + "github.com/vincent-petithory/dataurl" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/proto/waCommon" + "go.mau.fi/whatsmeow/proto/waE2E" + "go.mau.fi/whatsmeow/types" + "google.golang.org/protobuf/proto" +) + +type MessageService interface { + React(data *ReactStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + ChatPresence(data *ChatPresenceStruct, instance *instance_model.Instance) (string, error) + MarkRead(data *MarkReadStruct, instance *instance_model.Instance) (string, error) + MarkPlayed(data *MarkPlayedStruct, instance *instance_model.Instance) (string, error) + DownloadMedia(data *DownloadMediaStruct, instance *instance_model.Instance, request *http.Request) (*dataurl.DataURL, string, error) + GetMessageStatus(data *MessageStatusStruct, instance *instance_model.Instance) (*message_model.Message, string, error) + DeleteMessageEveryone(data *MessageStruct, instance *instance_model.Instance) (string, string, error) + EditMessage(data *EditMessageStruct, instance *instance_model.Instance) (string, string, error) +} + +type messageService struct { + clientPointer map[string]*whatsmeow.Client + messageRepository message_repository.MessageRepository + whatsmeowService whatsmeow_service.WhatsmeowService + loggerWrapper *logger_wrapper.LoggerManager +} + +type ReactStruct struct { + Number string `json:"number"` + Reaction string `json:"reaction"` + Id string `json:"id"` + FromMe bool `json:"fromMe"` + Participant string `json:"participant,omitempty"` +} + +type ChatPresenceStruct struct { + Number string `json:"number"` + State string `json:"state"` + IsAudio bool `json:"isAudio"` + // Delay, in milliseconds, keeps the "composing"/"recording" indicator alive + // for the given duration (re-sending it periodically) and then sends "paused". + // Only applies when State is "composing". 0 = single fire (legacy behaviour). + Delay int `json:"delay"` +} + +type MarkReadStruct struct { + Id []string `json:"id"` + Number string `json:"number"` +} + +type MarkPlayedStruct struct { + Id []string `json:"id"` + Number string `json:"number"` +} + +type DownloadMediaStruct struct { + Message *waE2E.Message `json:"message"` +} + +type MessageStatusStruct struct { + Id string `json:"id"` +} + +type MessageStruct struct { + Chat string `json:"chat"` + MessageID string `json:"messageId"` +} + +type EditMessageStruct struct { + Chat string `json:"chat"` + Message string `json:"message"` + MessageID string `json:"messageId"` +} + +type MessageSendStruct struct { + Info types.MessageInfo + Message *waE2E.Message + MessageContextInfo *waE2E.ContextInfo +} + +func (m *messageService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + client := m.clientPointer[instanceId] + m.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) + + if client == nil { + m.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] No client found, attempting to start new instance", instanceId) + err := m.whatsmeowService.StartInstance(instanceId) + if err != nil { + m.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to start instance: %v", instanceId, err) + return nil, errors.New("no active session found") + } + + m.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) + time.Sleep(2 * time.Second) + + client = m.clientPointer[instanceId] + m.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + + if client == nil || !client.IsConnected() { + m.loggerWrapper.GetLogger(instanceId).LogError("[%s] New client validation failed - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + return nil, errors.New("no active session found") + } + } else if !client.IsConnected() { + m.loggerWrapper.GetLogger(instanceId).LogError("[%s] Existing client is disconnected - Connected status: %v", + instanceId, + client.IsConnected()) + return nil, errors.New("client disconnected") + } + + m.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected()) + return client, nil +} + +func (m *messageService) React(data *ReactStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + client, err := m.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + msgId := "" + + recipient, ok := utils.ParseJID(data.Number) + if !ok { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return nil, errors.New("invalid phone number") + } + + // Strip the "+" that ParseJID/CreateJID adds. The recipient is used both as + // the SendMessage target (usync/device resolution) AND as the MessageKey + // RemoteJID that references the reacted message's chat. A malformed "+JID" + // breaks device resolution (usync) and prevents the reaction from matching + // the original message's chat. See utils.CanonicalJID. + recipient = utils.CanonicalJID(recipient) + + if data.Id == "" { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Missing Id in Payload", instance.Id) + return nil, errors.New("missing id in payload") + } else { + msgId = data.Id + } + + fromMe := data.FromMe + reaction := data.Reaction + if reaction == "remove" { + reaction = "" + } + + // Create MessageKey — msgId is the ID of the message being reacted to, + // NOT the ID of the reaction envelope itself. + messageKey := &waCommon.MessageKey{ + RemoteJID: proto.String(recipient.String()), + FromMe: proto.Bool(fromMe), + ID: proto.String(msgId), + } + + // Add participant if provided (for group messages) + if data.Participant != "" { + participantJID, ok := utils.ParseJID(data.Participant) + if ok { + messageKey.Participant = proto.String(utils.CanonicalJID(participantJID).String()) + } + } + + msg := &waE2E.Message{ + ReactionMessage: &waE2E.ReactionMessage{ + Key: messageKey, + Text: proto.String(reaction), + SenderTimestampMS: proto.Int64(time.Now().UnixMilli()), + }, + } + + // Do NOT pass ID: msgId in SendRequestExtra. Doing so would reuse the + // original message ID as the reaction envelope ID; WhatsApp silently + // deduplicates it and drops the reaction. Let whatsmeow generate a + // fresh, unique ID for the envelope. + response, err := client.SendMessage(context.Background(), recipient, msg) + if err != nil { + return nil, err + } + + isGroup := strings.Contains(data.Number, "@g.us") + messageType := "ReactionMessage" + + messageInfo := types.MessageInfo{ + MessageSource: types.MessageSource{ + Chat: recipient, + Sender: *client.Store.ID, + IsFromMe: true, + IsGroup: isGroup, + }, + ID: response.ID, + Timestamp: time.Now(), + ServerID: response.ServerID, + Type: messageType, + } + + messageSent := &MessageSendStruct{ + Info: messageInfo, + Message: msg, + } + + return messageSent, nil +} + +func (m *messageService) ChatPresence(data *ChatPresenceStruct, instance *instance_model.Instance) (string, error) { + client, err := m.ensureClientConnected(instance.Id) + if err != nil { + return "", err + } + + var ts time.Time + + recipient, ok := utils.ParseJID(data.Number) + if !ok { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return "", errors.New("invalid phone number") + } + + // chatstate (typing) is a RAW node sent without usync normalization, so it + // needs a canonical digits-only JID or WhatsApp silently drops it. See + // utils.CanonicalJID for the full rationale. + recipient = utils.CanonicalJID(recipient) + + media := "" + + if data.IsAudio { + media = "audio" + } + + // WhatsApp only forwards chatstate (typing / recording) events to the + // recipient while the sender is marked online. SendChatPresence merely + // sends the chatstate node — it does NOT mark us available. Background + // presence handling (events.AppStateSyncComplete) may have set us to + // Unavailable, in which case the server silently drops the typing + // indicator. Mark ourselves available first to guarantee delivery. + if presErr := client.SendPresence(context.Background(), types.PresenceAvailable); presErr != nil { + m.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] SendPresence(available) before chatstate failed (non-fatal): %v", instance.Id, presErr) + } + + state := types.ChatPresence(data.State) + mediaType := types.ChatPresenceMedia(media) + + err = client.SendChatPresence(context.Background(), recipient, state, mediaType) + if err != nil { + return "", err + } + + // A single "composing" indicator is ephemeral: WhatsApp expires it after a + // few seconds unless refreshed. When a Delay is provided (and we're typing), + // keep the indicator alive for the requested duration by re-sending it, then + // send "paused" so the indicator clears cleanly instead of timing out. + if data.Delay > 0 && state == types.ChatPresenceComposing { + const keepAliveInterval = 5 * time.Second + const maxDelay = 60 * time.Second + + remaining := time.Duration(data.Delay) * time.Millisecond + if remaining > maxDelay { + remaining = maxDelay + } + + for remaining > 0 { + sleep := keepAliveInterval + if remaining < sleep { + sleep = remaining + } + time.Sleep(sleep) + remaining -= sleep + + if remaining > 0 { + // Refresh the indicator so it doesn't expire mid-delay. + if refreshErr := client.SendChatPresence(context.Background(), recipient, state, mediaType); refreshErr != nil { + m.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Refresh chatstate failed (non-fatal): %v", instance.Id, refreshErr) + } + } + } + + if pausedErr := client.SendChatPresence(context.Background(), recipient, types.ChatPresencePaused, mediaType); pausedErr != nil { + m.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] SendChatPresence(paused) failed (non-fatal): %v", instance.Id, pausedErr) + } + } + + m.loggerWrapper.GetLogger(instance.Id).LogInfo("Presence (%s) sent to %s", data.State, data.Number) + + return ts.String(), nil +} + +func (m *messageService) MarkRead(data *MarkReadStruct, instance *instance_model.Instance) (string, error) { + client, err := m.ensureClientConnected(instance.Id) + if err != nil { + return "", err + } + + var ts time.Time + + jid, ok := utils.ParseJID(data.Number) + if !ok { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return "", errors.New("invalid phone number") + } + + // Read receipts are RAW nodes (no usync) — strip the "+" so the receipt + // reaches the recipient. Same root cause as the typing fix above. + jid = utils.CanonicalJID(jid) + + err = client.MarkRead(context.Background(), data.Id, time.Now(), jid, jid) + if err != nil { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error marking message as read: %v", instance.Id, err) + return "", errors.New("error marking message as read") + } + + return ts.String(), nil +} + +func (m *messageService) MarkPlayed(data *MarkPlayedStruct, instance *instance_model.Instance) (string, error) { + client, err := m.ensureClientConnected(instance.Id) + if err != nil { + return "", err + } + + var ts time.Time + + jid, ok := utils.ParseJID(data.Number) + if !ok { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return "", errors.New("invalid phone number") + } + + // Played receipts are RAW nodes (no usync) — strip the "+" so the receipt + // reaches the recipient. Same root cause as the MarkRead fix. + jid = utils.CanonicalJID(jid) + + err = client.MarkRead(context.Background(), data.Id, time.Now(), jid, jid, types.ReceiptTypePlayed) + if err != nil { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error marking message as played: %v", instance.Id, err) + return "", errors.New("error marking message as played") + } + + return ts.String(), nil +} + +func (m *messageService) DownloadMedia(data *DownloadMediaStruct, instance *instance_model.Instance, request *http.Request) (*dataurl.DataURL, string, error) { + client, err := m.ensureClientConnected(instance.Id) + if err != nil { + return nil, "", err + } + + var ts time.Time + + msg := data.Message + + mimetype := "" + var mediaData []byte + + img := msg.GetImageMessage() + audio := msg.GetAudioMessage() + document := msg.GetDocumentMessage() + video := msg.GetVideoMessage() + sticker := msg.GetStickerMessage() + + if img == nil && audio == nil && document == nil && video == nil && sticker == nil { + return nil, "", errors.New("invalid media type") + } + + userDirectory := fmt.Sprintf(`files/user_%s`, instance.Id) + _, err = os.Stat(userDirectory) + if os.IsNotExist(err) { + errDir := os.MkdirAll(userDirectory, 0751) + if errDir != nil { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Could not create user directory (%s)", instance.Id, userDirectory) + return nil, "", errDir + } + } + + if img != nil { + mediaData, err = client.Download(context.Background(), img) + if err != nil { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to download image", instance.Id) + msg := fmt.Sprintf("Failed to download image %v", err) + return nil, "", errors.New(msg) + } + mimetype = img.GetMimetype() + } + + if audio != nil { + mediaData, err = client.Download(context.Background(), audio) + if err != nil { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to download audio", instance.Id) + msg := fmt.Sprintf("Failed to download audio %v", err) + return nil, "", errors.New(msg) + } + mimetype = audio.GetMimetype() + } + + if document != nil { + mediaData, err = client.Download(context.Background(), document) + if err != nil { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to download document", instance.Id) + msg := fmt.Sprintf("Failed to download document %v", err) + return nil, "", errors.New(msg) + } + mimetype = document.GetMimetype() + } + + if video != nil { + mediaData, err = client.Download(context.Background(), video) + if err != nil { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to download video", instance.Id) + msg := fmt.Sprintf("Failed to download video %v", err) + return nil, "", errors.New(msg) + } + mimetype = video.GetMimetype() + } + + if sticker != nil { + mediaData, err = client.Download(context.Background(), sticker) + if err != nil { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to download sticker", instance.Id) + msg := fmt.Sprintf("Failed to download sticker %v", err) + return nil, "", errors.New(msg) + } + mimetype = sticker.GetMimetype() + } + + dataURL := dataurl.New(mediaData, mimetype) + + return dataURL, ts.String(), nil +} + +func (m *messageService) GetMessageStatus(data *MessageStatusStruct, instance *instance_model.Instance) (*message_model.Message, string, error) { + _, err := m.ensureClientConnected(instance.Id) + if err != nil { + return nil, "", err + } + + var ts time.Time + + result, err := m.messageRepository.GetMessageByID(data.Id) + if err != nil { + return nil, "", err + } + + return result, ts.String(), nil +} + +func (m *messageService) DeleteMessageEveryone(data *MessageStruct, instance *instance_model.Instance) (string, string, error) { + client, err := m.ensureClientConnected(instance.Id) + if err != nil { + return "", "", err + } + + var ts time.Time + + recipient, ok := utils.ParseJID(data.Chat) + if !ok { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return "", "", errors.New("invalid phone number") + } + + m.loggerWrapper.GetLogger(instance.Id).LogInfo("Revoking message %s from %s", data.MessageID, recipient) + + resp, err := client.SendMessage( + context.Background(), + recipient, + client.BuildRevoke(recipient, types.EmptyJID, data.MessageID)) + if err != nil { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error revoking message: %v", instance.Id, err) + return "", "", err + } + + response := resp.ID + + return response, ts.String(), nil +} + +func (m *messageService) EditMessage(data *EditMessageStruct, instance *instance_model.Instance) (string, string, error) { + client, err := m.ensureClientConnected(instance.Id) + if err != nil { + return "", "", err + } + + recipient, ok := utils.ParseJID(data.Chat) + if !ok { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return "", "", errors.New("invalid phone number") + } + + resp, err := client.SendMessage( + context.Background(), + recipient, + client.BuildEdit( + recipient, + data.MessageID, + &waE2E.Message{ + ExtendedTextMessage: &waE2E.ExtendedTextMessage{ + Text: &data.Message, + }, + })) + if err != nil { + m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error revoking message: %v", instance.Id, err) + return "", "", err + } + + return resp.ID, resp.Timestamp.String(), nil +} + +func NewMessageService( + clientPointer map[string]*whatsmeow.Client, + messageRepository message_repository.MessageRepository, + whatsmeowService whatsmeow_service.WhatsmeowService, + loggerWrapper *logger_wrapper.LoggerManager, +) MessageService { + return &messageService{ + clientPointer: clientPointer, + messageRepository: messageRepository, + whatsmeowService: whatsmeowService, + loggerWrapper: loggerWrapper, + } +} diff --git a/whatsapp-service/pkg/middleware/auth_middleware.go b/whatsapp-service/pkg/middleware/auth_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..c1ef3cae6fa1bf226ffa864c4595f180368dcfe7 --- /dev/null +++ b/whatsapp-service/pkg/middleware/auth_middleware.go @@ -0,0 +1,56 @@ +package auth_middleware + +import ( + "net/http" + + "agentdeck-whatsapp-service/pkg/config" + instance_service "agentdeck-whatsapp-service/pkg/instance/service" + "github.com/gin-gonic/gin" +) + +type Middleware interface { + Auth(ctx *gin.Context) + AuthAdmin(ctx *gin.Context) +} + +type middleware struct { + config *config.Config + instanceService instance_service.InstanceService +} + +func (m middleware) Auth(ctx *gin.Context) { + token := ctx.GetHeader("apikey") + if token == "" { + ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) + return + } + + instance, err := m.instanceService.GetInstanceByToken(token) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) + return + } + + ctx.Set("instance", instance) + + ctx.Next() +} + +func (m middleware) AuthAdmin(ctx *gin.Context) { + token := ctx.GetHeader("apikey") + if token == "" { + ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) + return + } + + if token != m.config.GlobalApiKey { + ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) + return + } + + ctx.Next() +} + +func NewMiddleware(config *config.Config, instanceService instance_service.InstanceService) *middleware { + return &middleware{config: config, instanceService: instanceService} +} diff --git a/whatsapp-service/pkg/middleware/jid_validation_middleware.go b/whatsapp-service/pkg/middleware/jid_validation_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..311b13bb21e4d7509a654c214bda46329db44ad5 --- /dev/null +++ b/whatsapp-service/pkg/middleware/jid_validation_middleware.go @@ -0,0 +1,533 @@ +package auth_middleware + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "agentdeck-whatsapp-service/pkg/utils" + "github.com/gin-gonic/gin" + "github.com/gomessguii/logger" +) + +// JIDValidationMiddleware validates JID parameters in request bodies +type JIDValidationMiddleware struct{} + +// NewJIDValidationMiddleware creates a new JID validation middleware +func NewJIDValidationMiddleware() *JIDValidationMiddleware { + return &JIDValidationMiddleware{} +} + +// ValidateJIDFields validates and normalizes JID fields in request body +func (m *JIDValidationMiddleware) ValidateJIDFields(fieldNames ...string) gin.HandlerFunc { + return func(c *gin.Context) { + // Only process JSON requests + contentType := c.ContentType() + if !strings.Contains(contentType, "application/json") { + // For multipart/form-data, validate form fields + if strings.Contains(contentType, "multipart/form-data") { + m.validateFormFields(c, fieldNames...) + return + } + c.Next() + return + } + + // Read the request body + body, err := io.ReadAll(c.Request.Body) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"}) + c.Abort() + return + } + + // Restore the request body for downstream handlers + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + + // Parse JSON + var requestData map[string]interface{} + if err := json.Unmarshal(body, &requestData); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON format"}) + c.Abort() + return + } + + // Validate and normalize JID fields + modified := false + for _, fieldName := range fieldNames { + if value, exists := requestData[fieldName]; exists { + if strValue, ok := value.(string); ok && strValue != "" { + // Validate and normalize the JID + normalizedJID, err := utils.CreateJID(strValue) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Invalid %s format: %s", fieldName, err.Error()), + }) + c.Abort() + return + } + + // Update the value if it was normalized + if normalizedJID != strValue { + requestData[fieldName] = normalizedJID + modified = true + logger.LogDebug("Normalized %s from %s to %s", fieldName, strValue, normalizedJID) + } + } else if strValue == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("%s is required and cannot be empty", fieldName), + }) + c.Abort() + return + } + } + } + + // If we modified the request, update the body + if modified { + newBody, err := json.Marshal(requestData) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process request"}) + c.Abort() + return + } + c.Request.Body = io.NopCloser(bytes.NewBuffer(newBody)) + } + + c.Next() + } +} + +// validateFormFields validates JID fields in multipart form data +func (m *JIDValidationMiddleware) validateFormFields(c *gin.Context, fieldNames ...string) { + for _, fieldName := range fieldNames { + value := c.PostForm(fieldName) + if value != "" { + // Validate the JID format + _, err := utils.CreateJID(value) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Invalid %s format: %s", fieldName, err.Error()), + }) + c.Abort() + return + } + } else if fieldName == "number" { // number is typically required + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("%s is required and cannot be empty", fieldName), + }) + c.Abort() + return + } + } + c.Next() +} + +// ValidateNumberField is a convenience method for the common "number" field +// It handles both single strings and arrays of strings +func (m *JIDValidationMiddleware) ValidateNumberField() gin.HandlerFunc { + return func(c *gin.Context) { + // Only process JSON requests + contentType := c.ContentType() + if !strings.Contains(contentType, "application/json") { + // For multipart/form-data, validate form fields + if strings.Contains(contentType, "multipart/form-data") { + m.validateFormFields(c, "number") + return + } + c.Next() + return + } + + // Read the request body + body, err := io.ReadAll(c.Request.Body) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"}) + c.Abort() + return + } + + // Restore the request body for downstream handlers + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + + // Parse JSON + var requestData map[string]interface{} + if err := json.Unmarshal(body, &requestData); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON format"}) + c.Abort() + return + } + + // Validate and normalize number field (can be string or array) + modified := false + if value, exists := requestData["number"]; exists { + // Handle array of strings + if arrayValue, ok := value.([]interface{}); ok { + if len(arrayValue) == 0 { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "number array cannot be empty", + }) + c.Abort() + return + } + + for i, item := range arrayValue { + if strValue, ok := item.(string); ok && strValue != "" { + normalizedJID, err := utils.CreateJID(strValue) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Invalid number[%d] format: %s", i, err.Error()), + }) + c.Abort() + return + } + + if normalizedJID != strValue { + arrayValue[i] = normalizedJID + modified = true + logger.LogDebug("Normalized number[%d] from %s to %s", i, strValue, normalizedJID) + } + } else if strValue == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("number[%d] cannot be empty", i), + }) + c.Abort() + return + } + } + } else if strValue, ok := value.(string); ok { + // Handle single string + if strValue == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "number is required and cannot be empty", + }) + c.Abort() + return + } + + normalizedJID, err := utils.CreateJID(strValue) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Invalid number format: %s", err.Error()), + }) + c.Abort() + return + } + + if normalizedJID != strValue { + requestData["number"] = normalizedJID + modified = true + logger.LogDebug("Normalized number from %s to %s", strValue, normalizedJID) + } + } else { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "number must be a string or array of strings", + }) + c.Abort() + return + } + } + + // If we modified the request, update the body + if modified { + newBody, err := json.Marshal(requestData) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process request"}) + c.Abort() + return + } + c.Request.Body = io.NopCloser(bytes.NewBuffer(newBody)) + } + + c.Next() + } +} + +// ValidateMultipleNumbers validates multiple number fields (for arrays or multiple contacts) +func (m *JIDValidationMiddleware) ValidateMultipleNumbers(fieldName string) gin.HandlerFunc { + return func(c *gin.Context) { + // Only process JSON requests + contentType := c.ContentType() + if !strings.Contains(contentType, "application/json") { + c.Next() + return + } + + // Read the request body + body, err := io.ReadAll(c.Request.Body) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"}) + c.Abort() + return + } + + // Restore the request body for downstream handlers + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + + // Parse JSON + var requestData map[string]interface{} + if err := json.Unmarshal(body, &requestData); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON format"}) + c.Abort() + return + } + + // Validate array of numbers + if value, exists := requestData[fieldName]; exists { + modified := false + + // Handle array of strings + if arrayValue, ok := value.([]interface{}); ok { + for i, item := range arrayValue { + if strValue, ok := item.(string); ok && strValue != "" { + normalizedJID, err := utils.CreateJID(strValue) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Invalid %s[%d] format: %s", fieldName, i, err.Error()), + }) + c.Abort() + return + } + + if normalizedJID != strValue { + arrayValue[i] = normalizedJID + modified = true + } + } + } + } + + // If we modified the request, update the body + if modified { + newBody, err := json.Marshal(requestData) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process request"}) + c.Abort() + return + } + c.Request.Body = io.NopCloser(bytes.NewBuffer(newBody)) + } + } + + c.Next() + } +} + +// ValidateNumberFieldWithFormatJid validates number field but respects FormatJid parameter +// When FormatJid is true (default), numbers are normalized to full JID format +// When FormatJid is false, numbers are kept as received (raw format) +func (m *JIDValidationMiddleware) ValidateNumberFieldWithFormatJid() gin.HandlerFunc { + return func(c *gin.Context) { + // Only process JSON requests + contentType := c.ContentType() + if !strings.Contains(contentType, "application/json") { + c.Next() + return + } + + // Read the request body + body, err := io.ReadAll(c.Request.Body) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"}) + c.Abort() + return + } + + // Restore the request body for downstream handlers + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + + // Parse JSON + var requestData map[string]interface{} + if err := json.Unmarshal(body, &requestData); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON format"}) + c.Abort() + return + } + + // Check FormatJid parameter (default is true) + formatJid := true + if formatJidValue, exists := requestData["formatJid"]; exists { + if formatJidBool, ok := formatJidValue.(bool); ok { + formatJid = formatJidBool + } + } + + // Validate and optionally normalize number field based on FormatJid + modified := false + if value, exists := requestData["number"]; exists { + // Handle array of strings + if arrayValue, ok := value.([]interface{}); ok { + if len(arrayValue) == 0 { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "number array cannot be empty", + }) + c.Abort() + return + } + + for i, item := range arrayValue { + if strValue, ok := item.(string); ok && strValue != "" { + // Only validate and normalize if FormatJid is true + if formatJid { + normalizedJID, err := utils.CreateJID(strValue) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Invalid number[%d] format: %s", i, err.Error()), + }) + c.Abort() + return + } + + if normalizedJID != strValue { + arrayValue[i] = normalizedJID + modified = true + logger.LogDebug("Normalized number[%d] from %s to %s", i, strValue, normalizedJID) + } + } + // When formatJid is false, we accept numbers as received without validation + } else if strValue == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("number[%d] cannot be empty", i), + }) + c.Abort() + return + } + } + } else if strValue, ok := value.(string); ok { + // Handle single string + if strValue == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "number is required and cannot be empty", + }) + c.Abort() + return + } + + // Only validate and normalize if FormatJid is true + if formatJid { + normalizedJID, err := utils.CreateJID(strValue) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Invalid number format: %s", err.Error()), + }) + c.Abort() + return + } + + if normalizedJID != strValue { + requestData["number"] = normalizedJID + modified = true + logger.LogDebug("Normalized number from %s to %s", strValue, normalizedJID) + } + } + // When formatJid is false, we accept numbers as received without validation + } else { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "number must be a string or array of strings", + }) + c.Abort() + return + } + } + + // If we modified the request, update the body + if modified { + newBody, err := json.Marshal(requestData) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process request"}) + c.Abort() + return + } + c.Request.Body = io.NopCloser(bytes.NewBuffer(newBody)) + } + + c.Next() + } +} + +// ValidateContactFields validates contact-specific fields that may contain phone numbers +func (m *JIDValidationMiddleware) ValidateContactFields() gin.HandlerFunc { + return func(c *gin.Context) { + // Only process JSON requests + contentType := c.ContentType() + if !strings.Contains(contentType, "application/json") { + c.Next() + return + } + + // Read the request body + body, err := io.ReadAll(c.Request.Body) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"}) + c.Abort() + return + } + + // Restore the request body for downstream handlers + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + + // Parse JSON + var requestData map[string]interface{} + if err := json.Unmarshal(body, &requestData); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON format"}) + c.Abort() + return + } + + modified := false + + // Validate main number field + if value, exists := requestData["number"]; exists { + if strValue, ok := value.(string); ok && strValue != "" { + normalizedJID, err := utils.CreateJID(strValue) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Invalid number format: %s", err.Error()), + }) + c.Abort() + return + } + + if normalizedJID != strValue { + requestData["number"] = normalizedJID + modified = true + } + } + } + + // Validate vcard phone field if present + if vcardValue, exists := requestData["vcard"]; exists { + if vcardMap, ok := vcardValue.(map[string]interface{}); ok { + if phoneValue, phoneExists := vcardMap["phone"]; phoneExists { + if phoneStr, ok := phoneValue.(string); ok && phoneStr != "" { + // For vcard phone, we just validate format but don't necessarily convert to JID + _, err := utils.CreateJID(phoneStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Invalid vcard phone format: %s", err.Error()), + }) + c.Abort() + return + } + } + } + } + } + + // If we modified the request, update the body + if modified { + newBody, err := json.Marshal(requestData) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process request"}) + c.Abort() + return + } + c.Request.Body = io.NopCloser(bytes.NewBuffer(newBody)) + } + + c.Next() + } +} diff --git a/whatsapp-service/pkg/newsletter/handler/newsletter_handler.go b/whatsapp-service/pkg/newsletter/handler/newsletter_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..df972eca556c255f751bdd027dc7ae0bcfc1ec1e --- /dev/null +++ b/whatsapp-service/pkg/newsletter/handler/newsletter_handler.go @@ -0,0 +1,262 @@ +package newsletter_handler + +import ( + "net/http" + + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + newsletter_service "agentdeck-whatsapp-service/pkg/newsletter/service" + "github.com/gin-gonic/gin" +) + +type NewsletterHandler interface { + CreateNewsletter(ctx *gin.Context) + ListNewsletter(ctx *gin.Context) + GetNewsletter(ctx *gin.Context) + GetNewsletterInvite(ctx *gin.Context) + SubscribeNewsletter(ctx *gin.Context) + GetNewsletterMessages(ctx *gin.Context) +} + +type newsletterHandler struct { + newsletterService newsletter_service.NewsletterService +} + +// Create newsletter +// @Summary Create newsletter +// @Description Create newsletter +// @Tags Newsletter +// @Accept json +// @Produce json +// @Param message body newsletter_service.CreateNewsletterStruct true "Newsletter data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /newsletter/create [post] +func (n *newsletterHandler) CreateNewsletter(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *newsletter_service.CreateNewsletterStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Name == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) + return + } + + newsletter, err := n.newsletterService.CreateNewsletter(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": newsletter}) +} + +// List newsletters +// @Summary List newsletters +// @Description List newsletters +// @Tags Newsletter +// @Accept json +// @Produce json +// @Success 200 {object} gin.H "success" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /newsletter/list [get] +func (n *newsletterHandler) ListNewsletter(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + newsletters, err := n.newsletterService.ListNewsletter(instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": newsletters}) +} + +// Get newsletter +// @Summary Get newsletter +// @Description Get newsletter +// @Tags Newsletter +// @Accept json +// @Produce json +// @Param message body newsletter_service.GetNewsletterStruct true "Newsletter data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /newsletter/info [post] +func (n *newsletterHandler) GetNewsletter(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *newsletter_service.GetNewsletterStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.JID.String() == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "jid is required"}) + return + } + + newsletter, err := n.newsletterService.GetNewsletter(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": newsletter}) +} + +// Get newsletter invite +// @Summary Get newsletter invite +// @Description Get newsletter invite +// @Tags Newsletter +// @Accept json +// @Produce json +// @Param message body newsletter_service.GetNewsletterInviteStruct true "Newsletter data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /newsletter/link [post] +func (n *newsletterHandler) GetNewsletterInvite(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *newsletter_service.GetNewsletterInviteStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Key == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "key is required"}) + return + } + + newsletter, err := n.newsletterService.GetNewsletterInvite(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": newsletter}) +} + +// Subscribe newsletter +// @Summary Subscribe newsletter +// @Description Subscribe newsletter +// @Tags Newsletter +// @Accept json +// @Produce json +// @Param message body newsletter_service.GetNewsletterStruct true "Newsletter data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /newsletter/subscribe [post] +func (n *newsletterHandler) SubscribeNewsletter(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *newsletter_service.GetNewsletterStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.JID.String() == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "jid is required"}) + return + } + + err = n.newsletterService.SubscribeNewsletter(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success"}) +} + +// Get newsletter messages +// @Summary Get newsletter messages +// @Description Get newsletter messages +// @Tags Newsletter +// @Accept json +// @Produce json +// @Param message body newsletter_service.GetNewsletterMessagesStruct true "Newsletter data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /newsletter/messages [post] +func (n *newsletterHandler) GetNewsletterMessages(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *newsletter_service.GetNewsletterMessagesStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.JID.String() == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "jid is required"}) + return + } + + messages, err := n.newsletterService.GetNewsletterMessages(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": messages}) +} + +func NewNewsletterHandler( + newsletterService newsletter_service.NewsletterService, +) NewsletterHandler { + return &newsletterHandler{ + newsletterService: newsletterService, + } +} diff --git a/whatsapp-service/pkg/newsletter/service/newsletter_service.go b/whatsapp-service/pkg/newsletter/service/newsletter_service.go new file mode 100644 index 0000000000000000000000000000000000000000..1223c4149668e1b4b0159330fc5b9899bf26a931 --- /dev/null +++ b/whatsapp-service/pkg/newsletter/service/newsletter_service.go @@ -0,0 +1,207 @@ +package newsletter_service + +import ( + "context" + "errors" + "time" + + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/types" +) + +type NewsletterService interface { + CreateNewsletter(data *CreateNewsletterStruct, instance *instance_model.Instance) (*types.NewsletterMetadata, error) + ListNewsletter(instance *instance_model.Instance) ([]*types.NewsletterMetadata, error) + GetNewsletter(data *GetNewsletterStruct, instance *instance_model.Instance) (*types.NewsletterMetadata, error) + GetNewsletterInvite(data *GetNewsletterInviteStruct, instance *instance_model.Instance) (*types.NewsletterMetadata, error) + SubscribeNewsletter(data *GetNewsletterStruct, instance *instance_model.Instance) error + GetNewsletterMessages(data *GetNewsletterMessagesStruct, instance *instance_model.Instance) ([]*types.NewsletterMessage, error) +} + +type newsletterService struct { + clientPointer map[string]*whatsmeow.Client + whatsmeowService whatsmeow_service.WhatsmeowService + loggerWrapper *logger_wrapper.LoggerManager +} + +type CreateNewsletterStruct struct { + Name string `json:"name"` + Description string `json:"description"` +} + +type GetNewsletterStruct struct { + JID types.JID `json:"jid"` +} + +type GetNewsletterInviteStruct struct { + Key string `json:"key"` +} + +type GetNewsletterMessagesStruct struct { + JID types.JID `json:"jid"` + Count int `json:"count"` + BeforeID int `json:"before_id"` +} + +func (n *newsletterService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + client := n.clientPointer[instanceId] + n.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) + + if client == nil { + n.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] No client found, attempting to start new instance", instanceId) + err := n.whatsmeowService.StartInstance(instanceId) + if err != nil { + n.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to start instance: %v", instanceId, err) + return nil, errors.New("no active session found") + } + + n.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) + time.Sleep(2 * time.Second) + + client = n.clientPointer[instanceId] + n.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + + if client == nil || !client.IsConnected() { + n.loggerWrapper.GetLogger(instanceId).LogError("[%s] New client validation failed - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + return nil, errors.New("no active session found") + } + } else if !client.IsConnected() { + n.loggerWrapper.GetLogger(instanceId).LogError("[%s] Existing client is disconnected - Connected status: %v", + instanceId, + client.IsConnected()) + return nil, errors.New("client disconnected") + } + + n.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected()) + return client, nil +} + +func (n *newsletterService) CreateNewsletter(data *CreateNewsletterStruct, instance *instance_model.Instance) (*types.NewsletterMetadata, error) { + client, err := n.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + newsletter, err := client.CreateNewsletter(context.Background(), whatsmeow.CreateNewsletterParams{ + Name: data.Name, + Description: data.Description, + }) + if err != nil { + n.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error create newsletter: %v", instance.Id, err) + return nil, err + } + + return newsletter, nil +} + +func (n *newsletterService) ListNewsletter(instance *instance_model.Instance) ([]*types.NewsletterMetadata, error) { + client, err := n.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + newsletters, err := client.GetSubscribedNewsletters(context.Background()) + if err != nil { + n.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error list newsletters: %v", instance.Id, err) + return nil, err + } + + // For each newsletter, fetch full info to get subscribers_count + fullNewsletters := make([]*types.NewsletterMetadata, 0, len(newsletters)) + for _, newsletter := range newsletters { + fullInfo, err := client.GetNewsletterInfo(context.Background(), newsletter.ID) + if err != nil { + // If we can't get full info, use the basic one + n.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] error getting full info for newsletter %s: %v", instance.Id, newsletter.ID.String(), err) + fullNewsletters = append(fullNewsletters, newsletter) + continue + } + fullNewsletters = append(fullNewsletters, fullInfo) + } + + return fullNewsletters, nil +} + +func (n *newsletterService) GetNewsletter(data *GetNewsletterStruct, instance *instance_model.Instance) (*types.NewsletterMetadata, error) { + client, err := n.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + newsletter, err := client.GetNewsletterInfo(context.Background(), data.JID) + if err != nil { + n.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error list newsletter: %v", instance.Id, err) + return nil, err + } + + return newsletter, nil +} + +func (n *newsletterService) GetNewsletterInvite(data *GetNewsletterInviteStruct, instance *instance_model.Instance) (*types.NewsletterMetadata, error) { + client, err := n.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + newsletter, err := client.GetNewsletterInfoWithInvite(context.Background(), data.Key) + if err != nil { + n.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error list newsletter: %v", instance.Id, err) + return nil, err + } + + return newsletter, nil +} + +func (n *newsletterService) SubscribeNewsletter(data *GetNewsletterStruct, instance *instance_model.Instance) error { + client, err := n.ensureClientConnected(instance.Id) + if err != nil { + return err + } + + _, err = client.NewsletterSubscribeLiveUpdates(context.TODO(), data.JID) + if err != nil { + n.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error list newsletter: %v", instance.Id, err) + return err + } + + return nil +} + +func (n *newsletterService) GetNewsletterMessages(data *GetNewsletterMessagesStruct, instance *instance_model.Instance) ([]*types.NewsletterMessage, error) { + client, err := n.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + messages, err := client.GetNewsletterMessages(context.Background(), data.JID, + &whatsmeow.GetNewsletterMessagesParams{ + Count: data.Count, Before: data.BeforeID, + }) + if err != nil { + n.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error list newsletter: %v", instance.Id, err) + return nil, err + } + + return messages, nil +} + +func NewNewsletterService( + clientPointer map[string]*whatsmeow.Client, + whatsmeowService whatsmeow_service.WhatsmeowService, + loggerWrapper *logger_wrapper.LoggerManager, +) NewsletterService { + return &newsletterService{ + clientPointer: clientPointer, + whatsmeowService: whatsmeowService, + loggerWrapper: loggerWrapper, + } +} diff --git a/whatsapp-service/pkg/passkey/ceremony/store.go b/whatsapp-service/pkg/passkey/ceremony/store.go new file mode 100644 index 0000000000000000000000000000000000000000..0165a0d5f1e80a725fda9320c7e0d66b6560b498 --- /dev/null +++ b/whatsapp-service/pkg/passkey/ceremony/store.go @@ -0,0 +1,251 @@ +// Package ceremony holds the in-memory state of WhatsApp passkey (WebAuthn) +// pairing ceremonies. It is written by the whatsmeow event goroutine and read +// by the public HTTP polling endpoint, so every access is mutex-guarded. +// +// The lifecycle mirrors the browser-extension contract (tools/passkey-helper): +// the extension polls GET /passkey-ceremony/{token} and drives the ceremony +// through the stages below. +package ceremony + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "sync" + "time" +) + +// Stage is the ceremony state the extension polls for. The values are the +// literal strings the extension's content.js switches on. +const ( + StageChallenge = "challenge" // publicKey available; extension runs navigator.credentials.get() + StageAwaitingConfirmation = "awaiting_confirmation" // response sent to WhatsApp, waiting for the confirmation code + StageConfirmation = "confirmation" // code available; user must verify + confirm + StageConfirmed = "confirmed" // confirmation sent, pairing completing + StageError = "error" // something failed +) + +// defaultTTL mirrors the whatsmeow passkey handoff validity window (~5 min). +const defaultTTL = 5 * time.Minute + +// State is the snapshot the poll endpoint serializes back to the extension. +// The extension reads json.data || json, so a flat object is fine; keys match +// what content.js expects. +// +// PublicKey is stored pre-serialized (json.RawMessage) because whatsmeow's +// types.WebAuthnPublicKey already carries the exact json tags the extension +// needs (challenge, rpId, allowCredentials[].{id,type,transports}, +// userVerification, base64url-unpadded via jsonbytes). The event goroutine +// json.Marshals the WebAuthnPublicKey once and hands the bytes here — no +// field-by-field remap, no base64 re-encoding mismatch. +type State struct { + Stage string `json:"stage"` + PublicKey json.RawMessage `json:"publicKey,omitempty"` + Code string `json:"code,omitempty"` + SkipHandoffUX bool `json:"skipHandoffUX"` + Error string `json:"error,omitempty"` +} + +type entry struct { + instanceID string + state State + expiresAt time.Time +} + +// Store keeps token -> ceremony entry. It is shared (pointer) across the +// value-receiver whatsmeowService copies and the HTTP handler, so its internal +// map is protected by a RWMutex. +type Store struct { + mu sync.RWMutex + byToken map[string]*entry // ephemeral ceremony token -> entry + byInst map[string]string // instanceID -> active token (for event-goroutine writes) +} + +// NewStore builds an empty ceremony store. +func NewStore() *Store { + return &Store{ + byToken: make(map[string]*entry), + byInst: make(map[string]string), + } +} + +func newToken() string { + b := make([]byte, 32) + // crypto/rand.Read never returns a short read on supported platforms. + _, _ = rand.Read(b) + return hex.EncodeToString(b) +} + +func (s *Store) prune(now time.Time) { + for tok, e := range s.byToken { + if now.After(e.expiresAt) { + delete(s.byToken, tok) + if s.byInst[e.instanceID] == tok { + delete(s.byInst, e.instanceID) + } + } + } +} + +// Start begins (or restarts) a ceremony for an instance with the challenge +// publicKey (pre-serialized JSON), mints a fresh ephemeral token, and returns +// it. Any previous ceremony for the same instance is replaced. +func (s *Store) Start(instanceID string, pk json.RawMessage) string { + s.mu.Lock() + defer s.mu.Unlock() + now := time.Now() + s.prune(now) + + // Drop any previous token for this instance. + if old, ok := s.byInst[instanceID]; ok { + delete(s.byToken, old) + } + + tok := newToken() + s.byToken[tok] = &entry{ + instanceID: instanceID, + state: State{Stage: StageChallenge, PublicKey: pk, SkipHandoffUX: false}, + expiresAt: now.Add(defaultTTL), + } + s.byInst[instanceID] = tok + return tok +} + +// setStateByInstance updates the state of the active ceremony for an instance. +// No-op if the instance has no active ceremony. +func (s *Store) setStateByInstance(instanceID string, mutate func(*State)) { + s.mu.Lock() + defer s.mu.Unlock() + tok, ok := s.byInst[instanceID] + if !ok { + return + } + e, ok := s.byToken[tok] + if !ok { + return + } + mutate(&e.state) + e.expiresAt = time.Now().Add(defaultTTL) +} + +// SetConfirmation moves the ceremony to the confirmation stage with a code. +// skipHandoffUX is intentionally forced to false by callers (DOC2 §4.2: never +// auto-confirm), so the extension always shows the manual "Confirmar" button. +func (s *Store) SetConfirmation(instanceID, code string, skipHandoffUX bool) { + s.setStateByInstance(instanceID, func(st *State) { + st.Stage = StageConfirmation + st.Code = code + st.SkipHandoffUX = skipHandoffUX + st.Error = "" + }) +} + +// SetAwaitingConfirmation is set right after the WebAuthn response is submitted. +func (s *Store) SetAwaitingConfirmation(instanceID string) { + s.setStateByInstance(instanceID, func(st *State) { + st.Stage = StageAwaitingConfirmation + st.PublicKey = nil + st.Error = "" + }) +} + +// SetConfirmed marks the confirmation as sent; pairing is finishing. +func (s *Store) SetConfirmed(instanceID string) { + s.setStateByInstance(instanceID, func(st *State) { + st.Stage = StageConfirmed + st.Error = "" + }) +} + +// SetError records a ceremony error. +func (s *Store) SetError(instanceID, msg string) { + s.setStateByInstance(instanceID, func(st *State) { + st.Stage = StageError + st.Error = msg + }) +} + +// Clear removes the ceremony for an instance (e.g. on PairSuccess). After this, +// the poll endpoint returns not-found, and the extension treats a cleared state +// after having started as "pairing concluded". +func (s *Store) Clear(instanceID string) { + s.mu.Lock() + defer s.mu.Unlock() + if tok, ok := s.byInst[instanceID]; ok { + delete(s.byToken, tok) + delete(s.byInst, instanceID) + } +} + +// Lookup resolves a ceremony token to its instance id and current state. +// ok is false if the token is unknown or expired. +func (s *Store) Lookup(token string) (instanceID string, state State, ok bool) { + s.mu.Lock() + defer s.mu.Unlock() + s.prune(time.Now()) + e, found := s.byToken[token] + if !found { + return "", State{}, false + } + return e.instanceID, e.state, true +} + +// InstanceForToken resolves just the instance id for a token (used by the +// response/confirm endpoints to reach the right whatsmeow client). +func (s *Store) InstanceForToken(token string) (string, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + e, ok := s.byToken[token] + if !ok { + return "", false + } + if time.Now().After(e.expiresAt) { + return "", false + } + return e.instanceID, true +} + +// StateByInstance returns the active ceremony token and state for an instance, +// or ok=false if there is no non-expired ceremony. Used by the authenticated +// instance/QR poll so the manager can render the passkey UI (the token lets it +// rebuild the #wapk openUrl). +func (s *Store) StateByInstance(instanceID string) (token string, state State, ok bool) { + s.mu.RLock() + defer s.mu.RUnlock() + tok, has := s.byInst[instanceID] + if !has { + return "", State{}, false + } + e, has := s.byToken[tok] + if !has { + return "", State{}, false + } + if time.Now().After(e.expiresAt) { + return "", State{}, false + } + return tok, e.state, true +} + +// HasActiveByInstance reports whether an instance has a non-expired passkey +// ceremony in progress that is NOT yet in a terminal (error) stage. The QR +// rotation uses this to avoid tearing down the socket/client while a passkey +// ceremony — which is driven by a human in a browser and can easily outlast the +// QR rotation window — is still in flight. Without this, teardownQR would delete +// the client mid-ceremony and the /response and /confirm steps would fail. +func (s *Store) HasActiveByInstance(instanceID string) bool { + s.mu.RLock() + defer s.mu.RUnlock() + tok, ok := s.byInst[instanceID] + if !ok { + return false + } + e, ok := s.byToken[tok] + if !ok { + return false + } + if time.Now().After(e.expiresAt) { + return false + } + // A ceremony in the error stage is terminal — QR teardown may proceed. + return e.state.Stage != StageError +} diff --git a/whatsapp-service/pkg/passkey/handler/passkey_handler.go b/whatsapp-service/pkg/passkey/handler/passkey_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..cee998c939099d09986c746df286ac6f2a21dc48 --- /dev/null +++ b/whatsapp-service/pkg/passkey/handler/passkey_handler.go @@ -0,0 +1,183 @@ +// Package handler exposes the PUBLIC passkey-ceremony HTTP endpoints that the +// AgentDeck Passkey Helper browser extension calls from the web.whatsapp.com +// origin. These routes are intentionally unauthenticated (no apikey): access is +// gated only by an opaque, short-lived ceremony token minted per pairing. +// +// Contract (tools/passkey-helper/content.js): +// +// GET /passkey-ceremony/:token -> { stage, publicKey?, code?, skipHandoffUX, error? } +// POST /passkey-ceremony/:token/response -> body = WebAuthn assertion +// POST /passkey-ceremony/:token/confirm -> finish pairing +package handler + +import ( + "net/http" + + whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service" + "github.com/gin-gonic/gin" + "go.mau.fi/whatsmeow/types" +) + +// PasskeyHandler wires the public ceremony endpoints to the whatsmeow service. +type PasskeyHandler struct { + whatsmeowService whatsmeow_service.WhatsmeowService +} + +// NewPasskeyHandler builds the handler. +func NewPasskeyHandler(svc whatsmeow_service.WhatsmeowService) *PasskeyHandler { + return &PasskeyHandler{whatsmeowService: svc} +} + +// GetCeremony returns the current ceremony state for a token. The extension +// polls this and drives its UI off the `stage` field. +// @Summary Get passkey ceremony state +// @Description Returns the current WebAuthn passkey-pairing ceremony state for a token. PUBLIC endpoint (no apikey) — access is gated by the opaque short-lived ceremony token. Polled by the AgentDeck Passkey Helper browser extension. +// @Tags Passkey +// @Produce json +// @Param token path string true "Ceremony token" +// @Success 200 {object} gin.H "Ceremony state ({stage, skipHandoffUX, publicKey?, code?, error?})" +// @Failure 400 {object} gin.H "token is required" +// @Failure 404 {object} gin.H "ceremony not found or expired" +// @Failure 503 {object} gin.H "passkey ceremony unavailable" +// @Router /passkey-ceremony/{token} [get] +func (h *PasskeyHandler) GetCeremony(c *gin.Context) { + token := c.Param("token") + if token == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "token is required"}) + return + } + + store := h.whatsmeowService.PasskeyCeremonyStore() + if store == nil { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "passkey ceremony unavailable"}) + return + } + + _, state, ok := store.Lookup(token) + if !ok { + // Unknown/expired token. The extension treats an empty/cleared state + // after having started as "pairing concluded"; before that it keeps + // waiting. Return 404 so a never-valid token is distinguishable. + c.JSON(http.StatusNotFound, gin.H{"error": "ceremony not found or expired"}) + return + } + + // gin.H keeps keys verbatim (publicKey, skipHandoffUX) — no struct-tag risk. + resp := gin.H{ + "stage": state.Stage, + "skipHandoffUX": state.SkipHandoffUX, + } + if len(state.PublicKey) > 0 { + resp["publicKey"] = state.PublicKey // json.RawMessage — emitted as-is + } + if state.Code != "" { + resp["code"] = state.Code + } + if state.Error != "" { + resp["error"] = state.Error + } + c.JSON(http.StatusOK, resp) +} + +// SubmitResponse receives the WebAuthn assertion produced by the extension and +// forwards it to WhatsApp via SendPasskeyResponse. +// @Summary Submit passkey WebAuthn response +// @Description Receives the WebAuthn assertion produced by the browser extension and forwards it to WhatsApp. PUBLIC endpoint (no apikey) — gated by the ceremony token. Body is the WebAuthnResponse shape (id, rawId, type, response{clientDataJSON, authenticatorData, signature, userHandle?}), base64url-unpadded. +// @Tags Passkey +// @Accept json +// @Produce json +// @Param token path string true "Ceremony token" +// @Param response body types.WebAuthnResponse true "WebAuthn assertion" +// @Success 200 {object} gin.H "ok" +// @Failure 400 {object} gin.H "token is required / invalid body" +// @Failure 404 {object} gin.H "ceremony not found or expired" +// @Failure 500 {object} gin.H "Internal server error" +// @Failure 503 {object} gin.H "passkey ceremony unavailable" +// @Router /passkey-ceremony/{token}/response [post] +func (h *PasskeyHandler) SubmitResponse(c *gin.Context) { + token := c.Param("token") + if token == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "token is required"}) + return + } + + store := h.whatsmeowService.PasskeyCeremonyStore() + if store == nil { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "passkey ceremony unavailable"}) + return + } + + instanceID, ok := store.InstanceForToken(token) + if !ok { + c.JSON(http.StatusNotFound, gin.H{"error": "ceremony not found or expired"}) + return + } + + // The extension posts exactly the WebAuthnResponse shape (id, rawId, type, + // response{clientDataJSON, authenticatorData, signature, userHandle?}), + // base64url-unpadded, which matches types.WebAuthnResponse's json tags. + var resp types.WebAuthnResponse + if err := c.ShouldBindJSON(&resp); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := h.whatsmeowService.SubmitPasskeyResponse(instanceID, &resp); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"ok": true}) +} + +// Confirm finishes the pairing after the user verified the confirmation code. +// @Summary Confirm passkey pairing +// @Description Finishes the passkey pairing after the user verified the confirmation code. PUBLIC endpoint (no apikey) — gated by the ceremony token. +// @Tags Passkey +// @Produce json +// @Param token path string true "Ceremony token" +// @Success 200 {object} gin.H "ok" +// @Failure 400 {object} gin.H "token is required" +// @Failure 404 {object} gin.H "ceremony not found or expired" +// @Failure 500 {object} gin.H "Internal server error" +// @Failure 503 {object} gin.H "passkey ceremony unavailable" +// @Router /passkey-ceremony/{token}/confirm [post] +func (h *PasskeyHandler) Confirm(c *gin.Context) { + token := c.Param("token") + if token == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "token is required"}) + return + } + + store := h.whatsmeowService.PasskeyCeremonyStore() + if store == nil { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "passkey ceremony unavailable"}) + return + } + + instanceID, ok := store.InstanceForToken(token) + if !ok { + c.JSON(http.StatusNotFound, gin.H{"error": "ceremony not found or expired"}) + return + } + + if err := h.whatsmeowService.ConfirmPasskey(instanceID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"ok": true}) +} + +// RegisterRoutes wires the 3 PUBLIC ceremony endpoints directly on the engine, +// with NO auth group (mirrors core.LicenseRoutes). Call from main.go right +// after the license routes. +func RegisterRoutes(eng *gin.Engine, svc whatsmeow_service.WhatsmeowService) { + h := NewPasskeyHandler(svc) + grp := eng.Group("/passkey-ceremony") + { + grp.GET("/:token", h.GetCeremony) + grp.POST("/:token/response", h.SubmitResponse) + grp.POST("/:token/confirm", h.Confirm) + } +} diff --git a/whatsapp-service/pkg/poll/handler/poll_handler.go b/whatsapp-service/pkg/poll/handler/poll_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..83d0c3495f07c816e3eaecc32bdb452bbd1c1bb9 --- /dev/null +++ b/whatsapp-service/pkg/poll/handler/poll_handler.go @@ -0,0 +1,103 @@ +package poll_handler + +import ( + "encoding/json" + "net/http" + + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + poll_model "agentdeck-whatsapp-service/pkg/poll/model" + poll_service "agentdeck-whatsapp-service/pkg/poll/service" + "github.com/gin-gonic/gin" +) + +// Keep poll_model referenced so the package import is not dropped +// (swag reads Go source, not pre-processed, and needs the alias to be in scope). +var _ = poll_model.PollResults{} + +type PollHandler struct { + pollService poll_service.PollService + loggerWrapper *logger_wrapper.LoggerManager +} + +// NewPollHandler cria handler usando PollService existente (evita dupla inicialização) +func NewPollHandler(pollService poll_service.PollService, loggerWrapper *logger_wrapper.LoggerManager) *PollHandler { + return &PollHandler{ + pollService: pollService, + loggerWrapper: loggerWrapper, + } +} + +// GetPollResults retorna os resultados de uma enquete +// @Summary Get poll results +// @Description Retorna todos os votos de uma enquete específica +// @Tags Polls +// @Accept json +// @Produce json +// @Param pollMessageId path string true "ID da mensagem da enquete" +// @Success 200 {object} poll_model.PollResults +// @Failure 400 {object} gin.H +// @Failure 404 {object} gin.H +// @Failure 500 {object} gin.H +// @Router /polls/{pollMessageId}/results [get] +func (h *PollHandler) GetPollResults(c *gin.Context) { + pollMessageID := c.Param("pollMessageId") + + // Pegar instance do contexto de autenticação + instanceInterface, exists := c.Get("instance") + if !exists { + h.loggerWrapper.GetLogger("poll-handler").LogWarn("[POLL] Instance not found in context") + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "Authentication required", + }) + return + } + + // Converter para struct Instance + type Instance struct { + Id string `json:"id"` + } + instanceBytes, _ := json.Marshal(instanceInterface) + var instance Instance + if err := json.Unmarshal(instanceBytes, &instance); err != nil { + h.loggerWrapper.GetLogger("poll-handler").LogError("[POLL] Failed to parse instance: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get instance information", + }) + return + } + + instanceID := instance.Id + + // Validações de segurança + if pollMessageID == "" { + h.loggerWrapper.GetLogger("poll-handler").LogWarn("[POLL] Missing pollMessageId") + c.JSON(http.StatusBadRequest, gin.H{ + "error": "pollMessageId is required", + }) + return + } + + h.loggerWrapper.GetLogger("poll-handler").LogInfo("[POLL] Fetching results for poll %s (instance: %s)", pollMessageID, instanceID) + + // Buscar resultados do banco + results, err := h.pollService.GetPollResults(c.Request.Context(), pollMessageID, instanceID) + if err != nil { + h.loggerWrapper.GetLogger("poll-handler").LogError("[POLL] Error fetching results: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to fetch poll results", + }) + return + } + + if results.TotalVotes == 0 { + h.loggerWrapper.GetLogger("poll-handler").LogInfo("[POLL] No votes found for poll %s", pollMessageID) + c.JSON(http.StatusNotFound, gin.H{ + "error": "No votes found for this poll", + "message": "This poll has no votes yet, or the pollMessageId is incorrect", + }) + return + } + + h.loggerWrapper.GetLogger("poll-handler").LogInfo("[POLL] Returning %d votes for poll %s", results.TotalVotes, pollMessageID) + c.JSON(http.StatusOK, results) +} diff --git a/whatsapp-service/pkg/poll/model/poll_vote.go b/whatsapp-service/pkg/poll/model/poll_vote.go new file mode 100644 index 0000000000000000000000000000000000000000..bb28c82df7975b3dee2761971a05f984075088b6 --- /dev/null +++ b/whatsapp-service/pkg/poll/model/poll_vote.go @@ -0,0 +1,38 @@ +package model + +import "time" + +// PollVote representa um voto em uma enquete do WhatsApp +type PollVote struct { + ID string `json:"id"` + CompanyID string `json:"companyId"` + InstanceID string `json:"instanceId"` + PollMessageID string `json:"pollMessageId"` + PollChatJid string `json:"pollChatJid"` + VoteMessageID string `json:"voteMessageId"` + VoterJid string `json:"voterJid"` + VoterPhone string `json:"voterPhone,omitempty"` + VoterName string `json:"voterName,omitempty"` + SelectedOptions []string `json:"selectedOptions"` // SHA-256 hashes + VotedAt time.Time `json:"votedAt"` + ReceivedAt time.Time `json:"receivedAt"` +} + +// PollResults representa os resultados agregados de uma enquete +type PollResults struct { + PollMessageID string `json:"pollMessageId"` + PollChatJid string `json:"pollChatJid"` + TotalVotes int `json:"totalVotes"` + Votes []PollVote `json:"votes"` + OptionCounts map[string]int `json:"optionCounts"` // hash -> count + Voters []VoterInfo `json:"voters"` +} + +// VoterInfo representa informações de um votante +type VoterInfo struct { + Jid string `json:"jid"` + Phone string `json:"phone,omitempty"` + Name string `json:"name,omitempty"` + SelectedOptions []string `json:"selectedOptions"` + VotedAt time.Time `json:"votedAt"` +} diff --git a/whatsapp-service/pkg/poll/service/poll_service.go b/whatsapp-service/pkg/poll/service/poll_service.go new file mode 100644 index 0000000000000000000000000000000000000000..e4d3c73091a1836be3038c3fbd64385c018b7b53 --- /dev/null +++ b/whatsapp-service/pkg/poll/service/poll_service.go @@ -0,0 +1,234 @@ +package poll_service + +import ( + "context" + "fmt" + "strings" + "time" + + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + "agentdeck-whatsapp-service/pkg/poll/model" + "agentdeck-whatsapp-service/pkg/supabase" + "github.com/google/uuid" + waProto "go.mau.fi/whatsmeow/proto/waE2E" + "go.mau.fi/whatsmeow/types" +) + +// PollService define a interface para gerenciamento de votos de enquetes +type PollService interface { + // SavePollVote salva um voto de enquete no banco de dados + SavePollVote(ctx context.Context, vote *model.PollVote) error + + // GetPollResults retorna os resultados de uma enquete + GetPollResults(ctx context.Context, pollMessageID string, instanceID string) (*model.PollResults, error) +} + +type pollService struct { + supa *supabase.Client + loggerWrapper *logger_wrapper.LoggerManager +} + +// NewPollService cria uma nova instância do serviço de polls. The poll_votes +// table is created by ddl/004_poll_votes.sql (Supabase); no auto-migration is +// performed here. +func NewPollService(supa *supabase.Client, loggerWrapper *logger_wrapper.LoggerManager) PollService { + return &pollService{ + supa: supa, + loggerWrapper: loggerWrapper, + } +} + +// pollVoteRow is the snake_case PostgREST representation of a poll_votes row. +type pollVoteRow struct { + ID string `json:"id"` + CompanyID string `json:"company_id"` + InstanceID string `json:"instance_id"` + PollMessageID string `json:"poll_message_id"` + PollChatJid string `json:"poll_chat_jid"` + VoteMessageID string `json:"vote_message_id"` + VoterJid string `json:"voter_jid"` + VoterPhone *string `json:"voter_phone"` + VoterName *string `json:"voter_name"` + SelectedOptions []string `json:"selected_options"` + VotedAt string `json:"voted_at"` + ReceivedAt string `json:"received_at"` +} + +// SavePollVote salva um voto de enquete no banco de dados (NÃO-INVASIVO) +func (s *pollService) SavePollVote(ctx context.Context, vote *model.PollVote) error { + s.loggerWrapper.GetLogger("poll-service").LogInfo("[POLL] Saving vote for poll %s from %s", vote.PollMessageID, vote.VoterJid) + + if vote.VoterPhone == "" && strings.Contains(vote.VoterJid, "@") { + vote.VoterPhone = strings.Split(vote.VoterJid, "@")[0] + } + if vote.ID == "" { + vote.ID = uuid.New().String() + } + if vote.VotedAt.IsZero() { + vote.VotedAt = time.Now() + } + if vote.ReceivedAt.IsZero() { + vote.ReceivedAt = time.Now() + } + + payload := map[string]interface{}{ + "id": vote.ID, + "company_id": vote.CompanyID, + "instance_id": vote.InstanceID, + "poll_message_id": vote.PollMessageID, + "poll_chat_jid": vote.PollChatJid, + "vote_message_id": vote.VoteMessageID, + "voter_jid": vote.VoterJid, + "voter_phone": nullIfEmpty(vote.VoterPhone), + "voter_name": nullIfEmpty(vote.VoterName), + "selected_options": vote.SelectedOptions, + "voted_at": vote.VotedAt.Format(time.RFC3339), + "received_at": vote.ReceivedAt.Format(time.RFC3339), + } + + // The table's unique constraint is (poll_message_id, voter_jid); it is used + // for idempotent upserting of a re-vote. + err := s.supa.Table("wp_poll_votes").Upsert(ctx, payload, []string{"poll_message_id", "voter_jid"}, nil) + if err != nil { + s.loggerWrapper.GetLogger("poll-service").LogError("[POLL] Failed to save vote: %v", err) + return fmt.Errorf("failed to save poll vote: %w", err) + } + + s.loggerWrapper.GetLogger("poll-service").LogInfo("[POLL] Vote saved successfully for poll %s", vote.PollMessageID) + return nil +} + +// GetPollResults retorna os resultados agregados de uma enquete +func (s *pollService) GetPollResults(ctx context.Context, pollMessageID string, instanceID string) (*model.PollResults, error) { + s.loggerWrapper.GetLogger("poll-service").LogInfo("[POLL] Fetching results for poll %s", pollMessageID) + + q := supabase.NewQuery(). + Eq("poll_message_id", pollMessageID). + Eq("instance_id", instanceID). + Order("voted_at", false) + + var rows []pollVoteRow + if err := s.supa.Table("wp_poll_votes").Select(ctx, q, &rows); err != nil { + s.loggerWrapper.GetLogger("poll-service").LogError("[POLL] Failed to query votes: %v", err) + return nil, fmt.Errorf("failed to query poll votes: %w", err) + } + + var votes []model.PollVote + optionCounts := make(map[string]int) + for _, r := range rows { + vote := model.PollVote{ + ID: r.ID, + CompanyID: r.CompanyID, + InstanceID: r.InstanceID, + PollMessageID: r.PollMessageID, + PollChatJid: r.PollChatJid, + VoteMessageID: r.VoteMessageID, + VoterJid: r.VoterJid, + SelectedOptions: r.SelectedOptions, + } + if r.VoterPhone != nil { + vote.VoterPhone = *r.VoterPhone + } + if r.VoterName != nil { + vote.VoterName = *r.VoterName + } + vote.VotedAt, _ = time.Parse(time.RFC3339, r.VotedAt) + vote.ReceivedAt, _ = time.Parse(time.RFC3339, r.ReceivedAt) + if vote.VotedAt.IsZero() { + if t, err := time.Parse("2006-01-02T15:04:05", r.VotedAt); err == nil { + vote.VotedAt = t + } + } + + for _, option := range vote.SelectedOptions { + optionCounts[option]++ + } + votes = append(votes, vote) + } + + if len(votes) == 0 { + s.loggerWrapper.GetLogger("poll-service").LogInfo("[POLL] No votes found for poll %s", pollMessageID) + return &model.PollResults{ + PollMessageID: pollMessageID, + PollChatJid: "", + TotalVotes: 0, + Votes: []model.PollVote{}, + OptionCounts: make(map[string]int), + Voters: []model.VoterInfo{}, + }, nil + } + + voters := make([]model.VoterInfo, len(votes)) + for i, vote := range votes { + voters[i] = model.VoterInfo{ + Jid: vote.VoterJid, + Phone: vote.VoterPhone, + Name: vote.VoterName, + SelectedOptions: vote.SelectedOptions, + VotedAt: vote.VotedAt, + } + } + + results := &model.PollResults{ + PollMessageID: pollMessageID, + PollChatJid: votes[0].PollChatJid, + TotalVotes: len(votes), + Votes: votes, + OptionCounts: optionCounts, + Voters: voters, + } + + s.loggerWrapper.GetLogger("poll-service").LogInfo("[POLL] Found %d votes for poll %s", len(votes), pollMessageID) + return results, nil +} + +func nullIfEmpty(s string) interface{} { + if s == "" { + return nil + } + return s +} + +// BuildPollVoteFromEvent constrói um model.PollVote a partir de eventos do WhatsApp (HELPER SEGURO) +// NOTA: Espera que voteInfo já tenha passado pelo JID swap (Sender = número real) +func BuildPollVoteFromEvent( + pollInfo *types.MessageInfo, + voteInfo *types.MessageInfo, + decryptedVote *waProto.PollVoteMessage, + companyID string, + instanceID string, +) *model.PollVote { + // Extrair opções selecionadas (hashes SHA-256) + selectedOptions := make([]string, len(decryptedVote.SelectedOptions)) + for i, option := range decryptedVote.SelectedOptions { + selectedOptions[i] = fmt.Sprintf("%x", option) // Converte bytes para hex + } + + // Extrair telefone do votante + // NOTA: O JID swap já foi feito antes de chegar aqui! + // Se havia LID+WhatsApp, o Sender JÁ É o número real (@s.whatsapp.net) e SenderAlt é o LID + voterPhone := voteInfo.Sender.User + voterJid := voteInfo.Sender.String() + + fmt.Printf("[POLL DEBUG] ==========================================\n") + fmt.Printf("[POLL DEBUG] Voter JID: %s\n", voterJid) + fmt.Printf("[POLL DEBUG] Sender.Server: %s\n", voteInfo.Sender.Server) + fmt.Printf("[POLL DEBUG] Sender.User: %s\n", voteInfo.Sender.User) + fmt.Printf("[POLL DEBUG] FINAL voterPhone: %s\n", voterPhone) + fmt.Printf("[POLL DEBUG] ==========================================\n") + + return &model.PollVote{ + ID: uuid.New().String(), + CompanyID: companyID, + InstanceID: instanceID, + PollMessageID: pollInfo.ID, + PollChatJid: pollInfo.Chat.String(), + VoteMessageID: voteInfo.ID, + VoterJid: voteInfo.Sender.String(), + VoterPhone: voterPhone, + VoterName: voteInfo.PushName, + SelectedOptions: selectedOptions, + VotedAt: voteInfo.Timestamp, + ReceivedAt: time.Now(), + } +} \ No newline at end of file diff --git a/whatsapp-service/pkg/routes/routes.go b/whatsapp-service/pkg/routes/routes.go new file mode 100644 index 0000000000000000000000000000000000000000..77d91c36496976445103508334f3532fa6bca5b7 --- /dev/null +++ b/whatsapp-service/pkg/routes/routes.go @@ -0,0 +1,269 @@ +package routes + +import ( + "net/http" + + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + + _ "agentdeck-whatsapp-service/docs" + call_handler "agentdeck-whatsapp-service/pkg/call/handler" + chat_handler "agentdeck-whatsapp-service/pkg/chat/handler" + community_handler "agentdeck-whatsapp-service/pkg/community/handler" + group_handler "agentdeck-whatsapp-service/pkg/group/handler" + instance_handler "agentdeck-whatsapp-service/pkg/instance/handler" + label_handler "agentdeck-whatsapp-service/pkg/label/handler" + message_handler "agentdeck-whatsapp-service/pkg/message/handler" + auth_middleware "agentdeck-whatsapp-service/pkg/middleware" + newsletter_handler "agentdeck-whatsapp-service/pkg/newsletter/handler" + poll_handler "agentdeck-whatsapp-service/pkg/poll/handler" + send_handler "agentdeck-whatsapp-service/pkg/sendMessage/handler" + server_handler "agentdeck-whatsapp-service/pkg/server/handler" + user_handler "agentdeck-whatsapp-service/pkg/user/handler" +) + +type Routes struct { + authMiddleware auth_middleware.Middleware + jidValidationMiddleware *auth_middleware.JIDValidationMiddleware + instanceHandler instance_handler.InstanceHandler + userHandler user_handler.UserHandler + sendHandler send_handler.SendHandler + messageHandler message_handler.MessageHandler + chatHandler chat_handler.ChatHandler + groupHandler group_handler.GroupHandler + callHandler call_handler.CallHandler + communityHandler community_handler.CommunityHandler + labelHandler label_handler.LabelHandler + newsletterHandler newsletter_handler.NewsletterHandler + pollHandler *poll_handler.PollHandler + serverHandler server_handler.ServerHandler +} + +func (r *Routes) AssignRoutes(eng *gin.Engine) { + // Configuração do CORS + eng.Use(func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") + 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") + c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(200) + return + } + + c.Next() + }) + + eng.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + eng.GET("/favicon.ico", func(c *gin.Context) { + c.Status(http.StatusNoContent) + }) + + eng.GET("/server/ok", r.serverHandler.ServerOk) + + routes := eng.Group("/instance") + { + routes.Use(r.authMiddleware.AuthAdmin) + { + routes.POST("/create", r.instanceHandler.Create) + routes.GET("/all", r.instanceHandler.All) + routes.GET("/info/:instanceId", r.instanceHandler.Info) + routes.DELETE("/delete/:instanceId", r.instanceHandler.Delete) + routes.POST("/proxy/:instanceId", r.instanceHandler.SetProxy) + routes.DELETE("/proxy/:instanceId", r.instanceHandler.DeleteProxy) + routes.POST("/forcereconnect/:instanceId", r.instanceHandler.ForceReconnect) + routes.GET("/logs/:instanceId", r.instanceHandler.GetLogs) + } + } + + routes = eng.Group("/instance") + { + routes.Use(r.authMiddleware.Auth) + { + routes.POST("/connect", r.instanceHandler.Connect) + routes.GET("/status", r.instanceHandler.Status) + routes.GET("/qr", r.instanceHandler.Qr) + routes.POST("/pair", r.jidValidationMiddleware.ValidateNumberField(), r.instanceHandler.Pair) + routes.POST("/disconnect", r.instanceHandler.Disconnect) + routes.POST("/reconnect", r.instanceHandler.Reconnect) + routes.DELETE("/logout", r.instanceHandler.Logout) + routes.GET("/:instanceId/advanced-settings", r.instanceHandler.GetAdvancedSettings) + routes.PUT("/:instanceId/advanced-settings", r.instanceHandler.UpdateAdvancedSettings) + } + } + + routes = eng.Group("/send") + { + routes.Use(r.authMiddleware.Auth) + { + routes.POST("/text", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.sendHandler.SendText) + routes.POST("/link", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.sendHandler.SendLink) + routes.POST("/media", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.sendHandler.SendMedia) + routes.POST("/poll", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.sendHandler.SendPoll) + routes.POST("/sticker", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.sendHandler.SendSticker) + routes.POST("/location", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.sendHandler.SendLocation) + routes.POST("/contact", r.jidValidationMiddleware.ValidateContactFields(), r.sendHandler.SendContact) // TODO: send multiple contacts + routes.POST("/button", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.sendHandler.SendButton) + routes.POST("/list", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.sendHandler.SendList) + routes.POST("/carousel", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.sendHandler.SendCarousel) + routes.POST("/status/text", r.sendHandler.SendStatusText) + routes.POST("/status/media", r.sendHandler.SendStatusMedia) + } + } + routes = eng.Group("/user") + { + routes.Use(r.authMiddleware.Auth) + { + routes.POST("/info", r.jidValidationMiddleware.ValidateNumberField(), r.userHandler.GetUser) + routes.POST("/check", r.jidValidationMiddleware.ValidateNumberFieldWithFormatJid(), r.userHandler.CheckUser) + routes.POST("/avatar", r.jidValidationMiddleware.ValidateNumberField(), r.userHandler.GetAvatar) + routes.GET("/contacts", r.userHandler.GetContacts) + routes.GET("/privacy", r.userHandler.GetPrivacy) + routes.POST("/privacy", r.userHandler.SetPrivacy) + routes.POST("/block", r.jidValidationMiddleware.ValidateNumberField(), r.userHandler.BlockContact) + routes.POST("/unblock", r.jidValidationMiddleware.ValidateNumberField(), r.userHandler.UnblockContact) + routes.GET("/blocklist", r.userHandler.GetBlockList) + routes.POST("/profilePicture", r.userHandler.SetProfilePicture) + routes.POST("/profileName", r.userHandler.SetProfileName) + routes.POST("/profileStatus", r.userHandler.SetProfileStatus) + } + } + routes = eng.Group("/message") + { + routes.Use(r.authMiddleware.Auth) + { + routes.POST("/react", r.jidValidationMiddleware.ValidateJIDFields("number"), r.messageHandler.React) + routes.POST("/presence", r.jidValidationMiddleware.ValidateNumberField(), r.messageHandler.ChatPresence) + routes.POST("/markread", r.jidValidationMiddleware.ValidateNumberField(), r.messageHandler.MarkRead) + routes.POST("/markplayed", r.jidValidationMiddleware.ValidateNumberField(), r.messageHandler.MarkPlayed) + routes.POST("/downloadmedia", r.messageHandler.DownloadMedia) + routes.POST("/status", r.messageHandler.GetMessageStatus) + routes.POST("/delete", r.jidValidationMiddleware.ValidateNumberField(), r.messageHandler.DeleteMessageEveryone) + routes.POST("/edit", r.jidValidationMiddleware.ValidateNumberField(), r.messageHandler.EditMessage) // TODO: edit MediaMessage too + } + } + routes = eng.Group("/chat") + { + routes.Use(r.authMiddleware.Auth) + { + routes.POST("/pin", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatPin) // TODO: not working + routes.POST("/unpin", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatUnpin) // TODO: not working + routes.POST("/archive", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatArchive) // TODO: not working + routes.POST("/unarchive", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatUnarchive) // TODO: not working + routes.POST("/mute", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatMute) // TODO: not working + routes.POST("/unmute", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatUnmute) // TODO: not working + routes.POST("/history-sync", r.chatHandler.HistorySyncRequest) + } + } + routes = eng.Group("/group") + { + routes.Use(r.authMiddleware.Auth) + { + routes.GET("/list", r.groupHandler.ListGroups) + routes.POST("/info", r.jidValidationMiddleware.ValidateNumberField(), r.groupHandler.GetGroupInfo) + routes.POST("/invitelink", r.jidValidationMiddleware.ValidateNumberField(), r.groupHandler.GetGroupInviteLink) + routes.POST("/photo", r.jidValidationMiddleware.ValidateNumberField(), r.groupHandler.SetGroupPhoto) + routes.POST("/name", r.jidValidationMiddleware.ValidateNumberField(), r.groupHandler.SetGroupName) + routes.POST("/description", r.jidValidationMiddleware.ValidateNumberField(), r.groupHandler.SetGroupDescription) + routes.POST("/create", r.jidValidationMiddleware.ValidateMultipleNumbers("participants"), r.groupHandler.CreateGroup) + routes.POST("/participant", r.jidValidationMiddleware.ValidateJIDFields("number", "participants"), r.groupHandler.UpdateParticipant) + routes.GET("/myall", r.groupHandler.GetMyGroups) // TODO: not working + routes.POST("/join", r.groupHandler.JoinGroupLink) + routes.POST("/leave", r.jidValidationMiddleware.ValidateNumberField(), r.groupHandler.LeaveGroup) + routes.POST("/settings", r.jidValidationMiddleware.ValidateNumberField(), r.groupHandler.UpdateGroupSettings) + } + } + routes = eng.Group("/call") + { + routes.Use(r.authMiddleware.Auth) + { + routes.POST("/reject", r.jidValidationMiddleware.ValidateNumberField(), r.callHandler.RejectCall) + } + } + routes = eng.Group("/community") + { + routes.Use(r.authMiddleware.Auth) + { + routes.POST("/create", r.communityHandler.CreateCommunity) + routes.POST("/add", r.jidValidationMiddleware.ValidateJIDFields("number", "communityId"), r.communityHandler.CommunityAdd) + routes.POST("/remove", r.jidValidationMiddleware.ValidateJIDFields("number", "communityId"), r.communityHandler.CommunityRemove) + } + } + routes = eng.Group("/label") + { + routes.Use(r.authMiddleware.Auth) + { + routes.POST("/chat", r.jidValidationMiddleware.ValidateNumberField(), r.labelHandler.ChatLabel) + routes.POST("/message", r.labelHandler.MessageLabel) + routes.POST("/edit", r.labelHandler.EditLabel) + routes.GET("/list", r.labelHandler.GetLabels) + } + } + routes = eng.Group("/unlabel") + { + routes.Use(r.authMiddleware.Auth) + { + routes.POST("/chat", r.jidValidationMiddleware.ValidateNumberField(), r.labelHandler.ChatUnlabel) + routes.POST("/message", r.labelHandler.MessageUnlabel) + } + } + routes = eng.Group("/newsletter") + { + routes.Use(r.authMiddleware.Auth) + { + routes.POST("/create", r.newsletterHandler.CreateNewsletter) + routes.GET("/list", r.newsletterHandler.ListNewsletter) + routes.POST("/info", r.jidValidationMiddleware.ValidateJIDFields("newsletterId"), r.newsletterHandler.GetNewsletter) + routes.POST("/link", r.jidValidationMiddleware.ValidateJIDFields("newsletterId"), r.newsletterHandler.GetNewsletterInvite) + routes.POST("/subscribe", r.jidValidationMiddleware.ValidateJIDFields("newsletterId"), r.newsletterHandler.SubscribeNewsletter) + routes.POST("/messages", r.jidValidationMiddleware.ValidateJIDFields("newsletterId"), r.newsletterHandler.GetNewsletterMessages) + } + } + + // NOVO: Rotas de Enquetes (Polls) + routes = eng.Group("/polls") + { + routes.Use(r.authMiddleware.Auth) + { + routes.GET("/:pollMessageId/results", r.pollHandler.GetPollResults) + } + } + +} + +func NewRouter( + authMiddleware auth_middleware.Middleware, + instanceHandler instance_handler.InstanceHandler, + userHandler user_handler.UserHandler, + sendHandler send_handler.SendHandler, + messageHandler message_handler.MessageHandler, + chatHandler chat_handler.ChatHandler, + groupHandler group_handler.GroupHandler, + callHandler call_handler.CallHandler, + communityHandler community_handler.CommunityHandler, + labelHandler label_handler.LabelHandler, + newsletterHandler newsletter_handler.NewsletterHandler, + pollHandler *poll_handler.PollHandler, + serverHandler server_handler.ServerHandler, +) *Routes { + return &Routes{ + authMiddleware: authMiddleware, + jidValidationMiddleware: auth_middleware.NewJIDValidationMiddleware(), + instanceHandler: instanceHandler, + userHandler: userHandler, + sendHandler: sendHandler, + messageHandler: messageHandler, + chatHandler: chatHandler, + groupHandler: groupHandler, + callHandler: callHandler, + communityHandler: communityHandler, + labelHandler: labelHandler, + newsletterHandler: newsletterHandler, + pollHandler: pollHandler, + serverHandler: serverHandler, + } +} diff --git a/whatsapp-service/pkg/sendMessage/handler/send_handler.go b/whatsapp-service/pkg/sendMessage/handler/send_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..8e3a19c385074b1629155135a164890b5ffafad5 --- /dev/null +++ b/whatsapp-service/pkg/sendMessage/handler/send_handler.go @@ -0,0 +1,839 @@ +package send_handler + +import ( + "encoding/base64" + "io" + "net/http" + "strconv" + "strings" + + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + send_service "agentdeck-whatsapp-service/pkg/sendMessage/service" + "github.com/gin-gonic/gin" +) + +type SendHandler interface { + SendText(ctx *gin.Context) + SendLink(ctx *gin.Context) + SendMedia(ctx *gin.Context) + SendPoll(ctx *gin.Context) + SendSticker(ctx *gin.Context) + SendLocation(ctx *gin.Context) + SendContact(ctx *gin.Context) + SendButton(ctx *gin.Context) + SendList(ctx *gin.Context) + SendCarousel(ctx *gin.Context) + SendStatusText(ctx *gin.Context) + SendStatusMedia(ctx *gin.Context) +} + +type sendHandler struct { + sendMessageService send_service.SendService +} + +// Send a text message +// @Summary Send a text message +// @Description Send a text message +// @Tags Send Message +// @Accept json +// @Produce json +// @Param message body send_service.TextStruct true "Message data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /send/text [post] +func (s *sendHandler) SendText(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *send_service.TextStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if data.Text == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "message body is required"}) + return + } + + message, err := s.sendMessageService.SendText(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) +} + +// Send a link message +// @Summary Send a link message +// @Description Send a link message +// @Tags Send Message +// @Accept json +// @Produce json +// @Param message body send_service.LinkStruct true "Message data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /send/link [post] +func (s *sendHandler) SendLink(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *send_service.LinkStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if data.Text == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "message body is required"}) + return + } + + message, err := s.sendMessageService.SendLink(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) +} + +// Send a media message +// @Summary Send a media message +// @Description Send a media message +// @Tags Send Message +// @Accept json +// @Produce json +// @Param message body send_service.MediaStruct true "Message data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /send/media [post] +func (s *sendHandler) SendMedia(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + contentType := ctx.ContentType() + + var data *send_service.MediaStruct + + if strings.HasPrefix(contentType, "multipart/form-data") { + // Handle form-data + number := ctx.PostForm("number") + if number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + mediaType := ctx.PostForm("type") + if mediaType == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "media type is required"}) + return + } + + caption := ctx.PostForm("caption") + filename := ctx.PostForm("filename") + id := ctx.PostForm("id") + delayStr := ctx.PostForm("delay") + delay := int32(0) + if delayStr != "" { + delay64, err := strconv.ParseInt(delayStr, 10, 32) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid delay"}) + return + } + delay = int32(delay64) + } + + mentionAll := ctx.PostForm("mentionAll") == "true" + + var mentionedJID []string + // Accept multiple values (mentionedJid=x&mentionedJid=y) or a single + // comma-separated string (mentionedJid=x,y). + for _, raw := range ctx.PostFormArray("mentionedJid") { + for _, v := range strings.Split(raw, ",") { + if trimmed := strings.TrimSpace(v); trimmed != "" { + mentionedJID = append(mentionedJID, trimmed) + } + } + } + + var quoted send_service.QuotedStruct + quoted.MessageID = ctx.PostForm("quoted.messageId") + quoted.Participant = ctx.PostForm("quoted.participant") + + // Get file + file, err := ctx.FormFile("file") + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "file is required"}) + return + } + + // Open file + fileData, err := file.Open() + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot open file"}) + return + } + defer fileData.Close() + fileBytes, err := io.ReadAll(fileData) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot read file"}) + return + } + + // Create MediaStruct + data = &send_service.MediaStruct{ + Number: number, + Type: mediaType, + Caption: caption, + Filename: filename, + Id: id, + Delay: delay, + MentionAll: mentionAll, + MentionedJID: mentionedJID, + Quoted: quoted, + } + + // Pass fileBytes to the send service + message, err := s.sendMessageService.SendMediaFile(data, fileBytes, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) + + } else { + + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if data.Url == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "URL is required"}) + return + } + + if data.Type == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "media type is required"}) + return + } + + var message *send_service.MessageSendStruct + + if !strings.HasPrefix(data.Url, "http://") && !strings.HasPrefix(data.Url, "https://") { + // Treat as base64-encoded media + fileBytes, err := base64.StdEncoding.DecodeString(data.Url) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid base64 encoding"}) + return + } + message, err = s.sendMessageService.SendMediaFile(data, fileBytes, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + } else { + message, err = s.sendMessageService.SendMediaUrl(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) + } +} + +// Send a poll message +// @Summary Send a poll message +// @Description Send a poll message +// @Tags Send Message +// @Accept json +// @Produce json +// @Param message body send_service.PollStruct true "Message data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /send/poll [post] +func (s *sendHandler) SendPoll(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *send_service.PollStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if data.Question == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "question is required"}) + return + } + + if len(data.Options) < 2 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "minimum 2 options are required"}) + return + } + + message, err := s.sendMessageService.SendPoll(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) +} + +// Send a sticker message +// @Summary Send a sticker message +// @Description Send a sticker message +// @Tags Send Message +// @Accept json +// @Produce json +// @Param message body send_service.StickerStruct true "Message data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /send/sticker [post] +func (s *sendHandler) SendSticker(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *send_service.StickerStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if data.Sticker == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "sticker is required"}) + return + } + + message, err := s.sendMessageService.SendSticker(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) +} + +// Send a location message +// @Summary Send a location message +// @Description Send a location message +// @Tags Send Message +// @Accept json +// @Produce json +// @Param message body send_service.LocationStruct true "Message data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /send/location [post] +func (s *sendHandler) SendLocation(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *send_service.LocationStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if data.Latitude == 0 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "latitude is required"}) + return + } + + if data.Longitude == 0 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "longitude is required"}) + return + } + + if data.Address == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "address is required"}) + return + } + + if data.Name == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) + return + } + + message, err := s.sendMessageService.SendLocation(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) +} + +// Send a contact message +// @Summary Send a contact message +// @Description Send a contact message +// @Tags Send Message +// @Accept json +// @Produce json +// @Param message body send_service.ContactStruct true "Message data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /send/contact [post] +func (s *sendHandler) SendContact(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *send_service.ContactStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if data.Vcard.Phone == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "contact phone number is required"}) + return + } + + if data.Vcard.FullName == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "contact full name is required"}) + return + } + + message, err := s.sendMessageService.SendContact(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) +} + +// Send a button message +// @Summary Send a button message +// @Description Send an interactive message with buttons. Each button has a `type`: `reply`, `copy`, `url`, `call` or `pix`. +// @Description +// @Description Combination rules enforced by the server: +// @Description - Up to 3 `reply` buttons per message. +// @Description - `reply` buttons cannot be mixed with any other type. +// @Description - `pix` button must be sent ALONE (no other button in the same message). +// @Description +// @Description WhatsApp client rendering quirks (NOT enforced by the server, but verified in the field): +// @Description - WhatsApp Web: only `reply`-only messages (up to 3) OR CTAs grouped together (`copy` + `url` + `call`) render correctly. +// @Description - Do NOT mix `reply` with CTA buttons (`copy`/`url`/`call`) — the message will not appear on WhatsApp Web. +// @Description +// @Description Required body fields: `number`, `title`, `description`, `footer`, `buttons`. +// @Tags Send Message +// @Accept json +// @Produce json +// @Param message body send_service.ButtonStruct true "Message data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /send/button [post] +func (s *sendHandler) SendButton(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *send_service.ButtonStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if data.Title == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "title is required"}) + return + } + + if data.Description == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "description is required"}) + return + } + + if data.Footer == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "footer is required"}) + return + } + + message, err := s.sendMessageService.SendButton(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) +} + +// Send a list message +// @Summary Send a list message +// @Description Send an interactive list message (single-select) rendered as a tappable menu. +// @Description +// @Description Required body fields: `number`, `title`, `description`, `footerText`, `buttonText`, `sections`. +// @Description Each section must contain one or more `rows`. When `rowId` is omitted, the server generates a fallback ID. +// @Description When `buttonText` is empty, the server falls back to "Ver Menu". +// @Description +// @Description Uses legacy `ListMessage` format (no ViewOnceMessage wrapper) so it renders on iOS, Android and WhatsApp Web. +// @Tags Send Message +// @Accept json +// @Produce json +// @Param message body send_service.ListStruct true "Message data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /send/list [post] +func (s *sendHandler) SendList(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *send_service.ListStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if data.Title == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "title is required"}) + return + } + + if data.Description == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "description is required"}) + return + } + + if data.FooterText == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "footer is required"}) + return + } + + if data.ButtonText == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "button text is required"}) + return + } + + message, err := s.sendMessageService.SendList(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) +} + +// Send a carousel message +// @Summary Send a carousel message +// @Description Send an interactive carousel (multiple swipeable cards). Each card carries its own image or video, body and optional buttons. +// @Description +// @Description Card button `type` accepted values (case-insensitive, uppercased internally): `REPLY` (default), `URL`, `CALL`, `COPY`. +// @Description The `PIX` button type is NOT supported in carousel cards — use `/send/button` for PIX. +// @Description +// @Description IMPORTANT — `CarouselButtonStruct` is different from the flat button used in `/send/button`: +// @Description - URL button: put the link in the `id` field (NOT in a `url` field). +// @Description - CALL button: put the phone number in the `id` field (NOT in a `phoneNumber` field). +// @Description - COPY button: put the code to be copied in `copyCode`. +// @Description - REPLY button: put the payload/callback ID in `id`. +// @Description +// @Description Per-card combination rules (NOT enforced by the server, but verified in the field): +// @Description - Same WhatsApp Web quirk as `/send/button`: avoid mixing REPLY with CTA buttons (URL/CALL/COPY) in the same card — mixed sets do not render on Web. +// @Description - Stick to either "only REPLY" or "only CTAs grouped together" per card. +// @Description +// @Description Required body fields: `number`, `cards` (at least one). Each card requires `header` + `body`. +// @Tags Send Message +// @Accept json +// @Produce json +// @Param message body send_service.CarouselStruct true "Message data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /send/carousel [post] +func (s *sendHandler) SendCarousel(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *send_service.CarouselStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if len(data.Cards) == 0 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "at least one card is required"}) + return + } + + message, err := s.sendMessageService.SendCarousel(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) +} + +// Send a text status message +// @Summary Send a WhatsApp text status +// @Description Send a WhatsApp text status to status@broadcast +// @Tags Send Message +// @Accept json +// @Produce json +// @Param message body send_service.StatusTextStruct true "Status text data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /send/status/text [post] +func (s *sendHandler) SendStatusText(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + data := new(send_service.StatusTextStruct) + err := ctx.ShouldBindBodyWithJSON(data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Text == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "text is required"}) + return + } + + message, err := s.sendMessageService.SendStatusText(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) +} + +// Send a media status message (image or video) +// @Summary Send a WhatsApp media status (image/video) +// @Description Send an image or video status to status@broadcast. Supports JSON (URL) or multipart/form-data (file upload) +// @Tags Send Message +// @Accept json, multipart/form-data +// @Produce json +// @Param type formData string true "Media type: image or video" +// @Param file formData file false "Media file (for multipart upload)" +// @Param url formData string false "Media URL (for JSON upload)" +// @Param caption formData string false "Caption for the media" +// @Param id formData string false "Custom message ID" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /send/status/media [post] +func (s *sendHandler) SendStatusMedia(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + contentType := ctx.ContentType() + + data := new(send_service.StatusMediaStruct) + + if strings.HasPrefix(contentType, "multipart/form-data") { + mediaType := ctx.PostForm("type") + if mediaType == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "media type is required"}) + return + } + + if mediaType != "image" && mediaType != "video" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "type must be 'image' or 'video'"}) + return + } + + caption := ctx.PostForm("caption") + id := ctx.PostForm("id") + + file, err := ctx.FormFile("file") + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "file is required"}) + return + } + + fileData, err := file.Open() + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot open file"}) + return + } + defer fileData.Close() + fileBytes, err := io.ReadAll(fileData) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot read file"}) + return + } + + data = &send_service.StatusMediaStruct{ + Type: mediaType, + Caption: caption, + Id: id, + } + + message, err := s.sendMessageService.SendStatusMediaFile(data, fileBytes, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) + return + } + + err := ctx.ShouldBindBodyWithJSON(data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Url == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "url is required"}) + return + } + + if data.Type != "image" && data.Type != "video" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "type must be 'image' or 'video'"}) + return + } + + message, err := s.sendMessageService.SendStatusMediaUrl(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": message}) +} + +func NewSendHandler( + sendMessageService send_service.SendService, +) SendHandler { + return &sendHandler{ + sendMessageService: sendMessageService, + } +} diff --git a/whatsapp-service/pkg/sendMessage/service/send_service.go b/whatsapp-service/pkg/sendMessage/service/send_service.go new file mode 100644 index 0000000000000000000000000000000000000000..b7780d4a139c459661bbd905f022a44758ceb108 --- /dev/null +++ b/whatsapp-service/pkg/sendMessage/service/send_service.go @@ -0,0 +1,3380 @@ +package send_service + +import ( + "bytes" + "context" + crypto_rand "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "image" + "image/jpeg" + "image/png" + "io" + "mime/multipart" + "net/http" + "os/exec" + "regexp" + "strconv" + "strings" + "time" + + "github.com/chai2010/webp" + config "agentdeck-whatsapp-service/pkg/config" + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + "agentdeck-whatsapp-service/pkg/utils" + whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service" + "github.com/gabriel-vasile/mimetype" + "go.mau.fi/whatsmeow" + waBinary "go.mau.fi/whatsmeow/binary" + "go.mau.fi/whatsmeow/proto/waE2E" + "go.mau.fi/whatsmeow/types" + "golang.org/x/net/html" + "google.golang.org/protobuf/proto" +) + +type SendService interface { + SendText(data *TextStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendLink(data *LinkStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendMediaUrl(data *MediaStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendMediaFile(data *MediaStruct, fileData []byte, instance *instance_model.Instance) (*MessageSendStruct, error) + SendPoll(data *PollStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendSticker(data *StickerStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendLocation(data *LocationStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendContact(data *ContactStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendButton(data *ButtonStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendList(data *ListStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendCarousel(data *CarouselStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendStatusText(data *StatusTextStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendStatusMediaUrl(data *StatusMediaStruct, instance *instance_model.Instance) (*MessageSendStruct, error) + SendStatusMediaFile(data *StatusMediaStruct, fileData []byte, instance *instance_model.Instance) (*MessageSendStruct, error) +} + +type sendService struct { + clientPointer map[string]*whatsmeow.Client + whatsmeowService whatsmeow_service.WhatsmeowService + config *config.Config + loggerWrapper *logger_wrapper.LoggerManager +} + +type SendDataStruct struct { + Id string + Number string + Delay int32 + MentionAll bool + MentionedJID []string + FormatJid *bool + Quoted QuotedStruct + MediaHandle string + AdditionalNodes *[]waBinary.Node + ForwardingScore *uint32 +} + +type QuotedStruct struct { + MessageID string `json:"messageId"` + Participant string `json:"participant"` +} + +type TextStruct struct { + Number string `json:"number"` + Text string `json:"text"` + Id string `json:"id"` + Delay int32 `json:"delay"` + MentionedJID []string `json:"mentionedJid"` + MentionAll bool `json:"mentionAll"` + FormatJid *bool `json:"formatJid,omitempty"` + Quoted QuotedStruct `json:"quoted"` + ForwardingScore *uint32 `json:"forwardingScore,omitempty"` +} + +type LinkStruct struct { + Number string `json:"number"` + Text string `json:"text"` + Title string `json:"title"` + Url string `json:"url"` + Description string `json:"description"` + ImgUrl string `json:"imgUrl"` + Id string `json:"id"` + Delay int32 `json:"delay"` + MentionedJID []string `json:"mentionedJid"` + MentionAll bool `json:"mentionAll"` + FormatJid *bool `json:"formatJid,omitempty"` + Quoted QuotedStruct `json:"quoted"` +} + +type MediaStruct struct { + Number string `json:"number"` + Url string `json:"url"` + Type string `json:"type"` + Caption string `json:"caption"` + Filename string `json:"filename"` + Id string `json:"id"` + Delay int32 `json:"delay"` + MentionedJID []string `json:"mentionedJid"` + MentionAll bool `json:"mentionAll"` + FormatJid *bool `json:"formatJid,omitempty"` + Quoted QuotedStruct `json:"quoted"` + ForwardingScore *uint32 `json:"forwardingScore,omitempty"` +} + +type PollStruct struct { + Id string `json:"id"` + Number string `json:"number"` + Question string `json:"question"` + MaxAnswer int `json:"maxAnswer"` + Options []string `json:"options"` + Delay int32 `json:"delay"` + MentionedJID []string `json:"mentionedJid"` + MentionAll bool `json:"mentionAll"` + FormatJid *bool `json:"formatJid,omitempty"` + Quoted QuotedStruct `json:"quoted"` +} + +type StickerStruct struct { + Number string `json:"number"` + Sticker string `json:"sticker"` + Id string `json:"id"` + Delay int32 `json:"delay"` + MentionedJID []string `json:"mentionedJid"` + MentionAll bool `json:"mentionAll"` + FormatJid *bool `json:"formatJid,omitempty"` + Quoted QuotedStruct `json:"quoted"` +} + +type LocationStruct struct { + Number string `json:"number"` + Id string `json:"id"` + Name string `json:"name"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Address string `json:"address"` + Delay int32 `json:"delay"` + MentionedJID []string `json:"mentionedJid"` + MentionAll bool `json:"mentionAll"` + FormatJid *bool `json:"formatJid,omitempty"` + Quoted QuotedStruct `json:"quoted"` +} + +type ContactStruct struct { + Number string `json:"number"` + Id string `json:"id"` + Vcard utils.VCardStruct `json:"vcard"` + Delay int32 `json:"delay"` + MentionedJID []string `json:"mentionedJid"` + MentionAll bool `json:"mentionAll"` + FormatJid *bool `json:"formatJid,omitempty"` + Quoted QuotedStruct `json:"quoted"` +} + +// Button represents a single interactive button for /send/button. +// The `type` field drives which of the other fields are used: +// - reply: uses `displayText` + `id` +// - copy: uses `displayText` + `copyCode` +// - url: uses `displayText` + `url` +// - call: uses `displayText` + `phoneNumber` +// - pix: uses `currency` + `name` + `keyType` + `key` (must be sent alone) +type Button struct { + // Button kind. One of: reply, copy, url, call, pix. + Type string `json:"type" enums:"reply,copy,url,call,pix" example:"reply"` + // Label rendered inside the button (reply / copy / url / call). Ignored for pix. + DisplayText string `json:"displayText" example:"Quero saber mais"` + // Callback payload for `reply` or code-to-copy internal id for `copy`. + Id string `json:"id" example:"btn_info"` + // Code placed in the clipboard when type=copy. + CopyCode string `json:"copyCode,omitempty" example:"PROMO2026"` + // Target URL when type=url. + URL string `json:"url,omitempty" example:"https://agentdeck.ai"` + // Destination phone number (E.164) when type=call. + PhoneNumber string `json:"phoneNumber,omitempty" example:"+5582988898565"` + // ISO currency code for type=pix (e.g. BRL). + Currency string `json:"currency,omitempty" example:"BRL"` + // Merchant display name shown on the Pix sheet. + Name string `json:"name,omitempty" example:"Minha Loja"` + // Pix key type. One of: phone, email, cpf, cnpj, random. + KeyType string `json:"keyType,omitempty" enums:"phone,email,cpf,cnpj,random" example:"cpf"` + // Pix key value matching the keyType. + Key string `json:"key,omitempty" example:"12345678900"` +} + +// ButtonStruct is the body for POST /send/button. +// +// Server-side validation: +// - up to 3 `reply` buttons per message; +// - `reply` cannot be mixed with any other type; +// - `pix` must be the only button in the message. +// +// WhatsApp Web rendering quirk (NOT enforced by the server): +// - mixing `reply` with CTA buttons (copy/url/call) makes the message invisible on WhatsApp Web; +// - safe combinations: only-reply (up to 3) OR grouped CTAs (copy + url + call). +type ButtonStruct struct { + // Destination phone number. + Number string `json:"number" example:"5582988898565"` + // Header title (required). + Title string `json:"title" example:"Oferta especial"` + // Body description text (required). + Description string `json:"description" example:"Confira as condicoes abaixo"` + // Footer text (required). + Footer string `json:"footer" example:"AgentDeck Whatsapp Service"` + // Buttons array. See combination rules on the parent type description. + Buttons []Button `json:"buttons"` + // Typing delay (milliseconds) applied before sending the message. + Delay int32 `json:"delay,omitempty" example:"1200"` + // JIDs to mention inside the body text. + MentionedJID []string `json:"mentionedJid,omitempty"` + // Mention every participant (groups only). + MentionAll bool `json:"mentionAll,omitempty"` + // If false, skips automatic formatting/validation of `number` into a JID. + FormatJid *bool `json:"formatJid,omitempty"` + // Quoted (reply-to) context. + Quoted QuotedStruct `json:"quoted,omitempty"` + // Optional image URL used as header for reply-only buttons. + ImageUrl string `json:"imageUrl,omitempty"` + // Optional video URL used as header for reply-only buttons. + VideoUrl string `json:"videoUrl,omitempty"` +} + +// Row is a selectable item inside a list Section. +type Row struct { + // Row main label. + Title string `json:"title" example:"Plano Basico"` + // Optional secondary line below the title. + Description string `json:"description,omitempty" example:"R$ 29,90/mes"` + // Callback payload returned when the user taps the row. Auto-generated if empty. + RowId string `json:"rowId,omitempty" example:"plan_basic"` +} + +// Section groups related Rows under an optional title. +type Section struct { + // Section heading (optional; rendered as a group separator). + Title string `json:"title,omitempty" example:"Planos"` + // Rows inside this section. + Rows []Row `json:"rows"` +} + +// ListStruct is the body for POST /send/list. +// +// Renders as a single-select menu (legacy ListMessage format — compatible with iOS, Android and WhatsApp Web). +type ListStruct struct { + // Destination phone number. + Number string `json:"number" example:"5582988898565"` + // Header title (required). + Title string `json:"title" example:"Nossos planos"` + // Body description text (required). + Description string `json:"description" example:"Escolha o plano ideal para voce"` + // Label of the button that opens the list. Defaults to "Ver Menu" when empty. + ButtonText string `json:"buttonText" example:"Abrir cardapio"` + // Footer text (required). + FooterText string `json:"footerText" example:"AgentDeck Whatsapp Service"` + // Sections with rows. At least one section with one row is required. + Sections []Section `json:"sections"` + // Typing delay (milliseconds) applied before sending the message. + Delay int32 `json:"delay,omitempty" example:"1200"` + // JIDs to mention inside the body text. + MentionedJID []string `json:"mentionedJid,omitempty"` + // Mention every participant (groups only). + MentionAll bool `json:"mentionAll,omitempty"` + // If false, skips automatic formatting/validation of `number` into a JID. + FormatJid *bool `json:"formatJid,omitempty"` + // Quoted (reply-to) context. + Quoted QuotedStruct `json:"quoted,omitempty"` +} + +// CarouselButtonStruct is a button attached to a single carousel card. +// +// IMPORTANT — this struct is different from `Button` (used in /send/button): +// it has NO dedicated `url` or `phoneNumber` fields. For URL and CALL buttons +// you must put the link / phone number in the `id` field. +// +// - REPLY (default): uses `displayText` + `id` as callback payload. +// - URL: uses `displayText` + `id` (put the URL here). +// - CALL: uses `displayText` + `id` (put the phone number here). +// - COPY: uses `displayText` + `copyCode`. +// +// PIX buttons are NOT supported inside carousel cards — use /send/button instead. +// +// WhatsApp Web rendering quirk (NOT enforced by the server): +// avoid mixing REPLY with CTA buttons (URL/CALL/COPY) in the same card — +// mixed sets do not render on WhatsApp Web. Prefer only-REPLY or only-CTAs per card. +type CarouselButtonStruct struct { + // Button kind (case-insensitive). One of: REPLY (default), URL, CALL, COPY. + Type string `json:"type" enums:"REPLY,URL,CALL,COPY,reply,url,call,copy" example:"REPLY"` + // Label rendered inside the button. + DisplayText string `json:"displayText" example:"Quero saber mais"` + // Context-dependent: REPLY payload, URL target (type=URL) or phone number (type=CALL). + Id string `json:"id" example:"card1_info"` + // Code placed in the clipboard when type=COPY. + CopyCode string `json:"copyCode,omitempty" example:"PROMO2026"` +} + +// CarouselCardHeaderStruct is the top area of a carousel card. +// Either `imageUrl` OR `videoUrl` may be provided (image takes precedence when both are set). +type CarouselCardHeaderStruct struct { + // Optional visible title above the media. + Title string `json:"title,omitempty" example:"Oferta do dia"` + // Optional subtitle rendered below the title. + Subtitle string `json:"subtitle,omitempty" example:"Somente hoje"` + // Public URL to an image. Downloaded, uploaded to WhatsApp servers and used as card media. + ImageUrl string `json:"imageUrl,omitempty" example:"https://picsum.photos/seed/card1/600/400"` + // Public URL to a video. Used only when `imageUrl` is empty. + VideoUrl string `json:"videoUrl,omitempty"` +} + +// CarouselCardBodyStruct is the text area of a carousel card. +type CarouselCardBodyStruct struct { + // Main text of the card. + Text string `json:"text" example:"Card 1 - Oferta especial"` +} + +// CarouselCardStruct is a single card inside a carousel message. +// Each card requires at least `header` + `body`. +type CarouselCardStruct struct { + // Card header (media + title/subtitle). + Header CarouselCardHeaderStruct `json:"header"` + // Card body text (required). + Body CarouselCardBodyStruct `json:"body"` + // Optional footer rendered under the body. + Footer string `json:"footer,omitempty" example:"Por tempo limitado"` + // Buttons shown on the card. See CarouselButtonStruct for combination rules. + Buttons []CarouselButtonStruct `json:"buttons,omitempty"` +} + +// CarouselStruct is the body for POST /send/carousel. +// +// Sends an interactive carousel of swipeable cards. At least one card is required. +// Each card must have `header` + `body`; button rules are described on CarouselButtonStruct. +type CarouselStruct struct { + // Destination phone number. + Number string `json:"number" example:"5582988898565"` + // Optional message body shown above the cards. + Body string `json:"body,omitempty" example:"Confira nossas novidades!"` + // Optional message footer shown below the cards. + Footer string `json:"footer,omitempty" example:"AgentDeck Whatsapp Service"` + // Typing delay (milliseconds) applied before sending the message. + Delay int32 `json:"delay,omitempty" example:"1200"` + // If false, skips automatic formatting/validation of `number` into a JID. + FormatJid *bool `json:"formatJid,omitempty"` + // Quoted (reply-to) context. + Quoted QuotedStruct `json:"quoted,omitempty"` + // Cards displayed in order. At least one card is required. + Cards []CarouselCardStruct `json:"cards"` +} + +type StatusTextStruct struct { + Text string `json:"text"` + Id string `json:"id"` +} + +type StatusMediaStruct struct { + Type string `json:"type"` + Url string `json:"url"` + Caption string `json:"caption"` + Id string `json:"id"` +} + +type MessageSendStruct struct { + Info types.MessageInfo + Message *waE2E.Message + MessageContextInfo *waE2E.ContextInfo +} + +func (s *sendService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + client := s.clientPointer[instanceId] + s.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) + + if client == nil { + s.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] No client found, attempting to start new instance", instanceId) + err := s.whatsmeowService.StartInstance(instanceId) + if err != nil { + s.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to start instance: %v", instanceId, err) + return nil, errors.New("no active session found") + } + + s.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) + time.Sleep(2 * time.Second) + + client = s.clientPointer[instanceId] + s.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + + if client == nil || !client.IsConnected() { + s.loggerWrapper.GetLogger(instanceId).LogError("[%s] New client validation failed - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + return nil, errors.New("no active session found") + } + } else if !client.IsConnected() { + s.loggerWrapper.GetLogger(instanceId).LogError("[%s] Existing client is disconnected - Connected status: %v", + instanceId, + client.IsConnected()) + return nil, errors.New("client disconnected") + } + + s.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected()) + return client, nil +} + +// ensureClientConnectedWithRetry attempts to ensure client connection with automatic reconnection and retry +func (s *sendService) ensureClientConnectedWithRetry(instanceId string, maxRetries int) (*whatsmeow.Client, error) { + for attempt := 1; attempt <= maxRetries; attempt++ { + s.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Connection attempt %d/%d", instanceId, attempt, maxRetries) + + client, err := s.ensureClientConnected(instanceId) + if err == nil { + return client, nil + } + + // Check if it's a disconnection error that we can retry + if err.Error() == "client disconnected" || err.Error() == "no active session found" { + s.loggerWrapper.GetLogger(instanceId).LogWarn("[%s] Client disconnected on attempt %d/%d, attempting reconnection...", instanceId, attempt, maxRetries) + + // Attempt to reconnect the client + reconnectErr := s.whatsmeowService.ReconnectClient(instanceId) + if reconnectErr != nil { + s.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to reconnect client on attempt %d: %v", instanceId, attempt, reconnectErr) + } else { + s.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Reconnection initiated on attempt %d, waiting 3 seconds...", instanceId, attempt) + time.Sleep(3 * time.Second) + } + + // If this is not the last attempt, continue to retry + if attempt < maxRetries { + waitTime := time.Duration(attempt*2) * time.Second // Progressive backoff + s.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Waiting %v before retry attempt %d", instanceId, waitTime, attempt+1) + time.Sleep(waitTime) + continue + } + } + + // If it's the last attempt or a non-retryable error, return the error + s.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to ensure client connection after %d attempts: %v", instanceId, attempt, err) + return nil, err + } + + return nil, fmt.Errorf("failed to connect client after %d attempts", maxRetries) +} + +func validateMessageFields(phone string, formatJid *bool, messageID *string, participant *string) (types.JID, error) { + // Apply formatting if formatJid is true (default) + shouldFormat := true // Default value + if formatJid != nil { + shouldFormat = *formatJid + } + + var finalPhone string + if shouldFormat { + // Extract raw number if it's already a JID, then apply CreateJID formatting + rawNumber := phone + if strings.Contains(phone, "@s.whatsapp.net") { + rawNumber = strings.Split(phone, "@")[0] + } + + normalizedJID, err := utils.CreateJID(rawNumber) + if err != nil { + // If CreateJID fails, try with ParseJID as fallback + recipient, ok := utils.ParseJID(phone) + if !ok { + return types.NewJID("", types.DefaultUserServer), fmt.Errorf("could not parse phone: %s", phone) + } + finalPhone = recipient.String() + } else { + finalPhone = normalizedJID + } + } else { + // Use phone as received without formatting + finalPhone = phone + } + + recipient, ok := utils.ParseJID(finalPhone) + if !ok { + return types.NewJID("", types.DefaultUserServer), errors.New("could not parse formatted phone") + } + + if messageID != nil { + if participant == nil { + return types.NewJID("", types.DefaultUserServer), errors.New("missing Participant in ContextInfo") + } + } + + if participant != nil { + if messageID == nil { + return types.NewJID("", types.DefaultUserServer), errors.New("missing StanzaId in ContextInfo") + } + } + + return recipient, nil +} + +// validateAndCheckUserExists validates message fields and checks if the user exists on WhatsApp +// Now uses the new approach: CheckUser with formatJid=false by default, and uses remoteJID for messaging +func (s *sendService) validateAndCheckUserExists(phone string, formatJid *bool, messageID *string, participant *string, instance *instance_model.Instance) (types.JID, error) { + // Skip WhatsApp check if disabled in config + if !s.config.CheckUserExists { + s.loggerWrapper.GetLogger(instance.Id).LogDebug("[%s] User existence check disabled by configuration", instance.Id) + // Use original validation logic when check is disabled + return validateMessageFields(phone, formatJid, messageID, participant) + } + + // Skip WhatsApp check for group messages, broadcast, newsletter, and LID + if strings.Contains(phone, "@g.us") || strings.Contains(phone, "@broadcast") || strings.Contains(phone, "@newsletter") || strings.Contains(phone, "@lid") { + return validateMessageFields(phone, formatJid, messageID, participant) + } + + // Get the client to check if user exists on WhatsApp + client, err := s.ensureClientConnected(instance.Id) + if err != nil { + return types.NewJID("", types.DefaultUserServer), fmt.Errorf("failed to connect client: %v", err) + } + + // Use CheckUser approach: formatJid=false by default + formatJidForCheck := false + + // First attempt with formatJid=false + remoteJID, found, err := s.checkSingleUserExists(client, phone, formatJidForCheck, instance.Id) + if err != nil { + s.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Failed to check user existence: %v", instance.Id, err) + // Continue with sending even if check fails (network issues, etc.) + return validateMessageFields(phone, formatJid, messageID, participant) + } + + // If not found with formatJid=false, try with formatJid=true as fallback + if !found { + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] User not found with formatJid=false, trying with formatJid=true", instance.Id) + remoteJIDRetry, foundRetry, errRetry := s.checkSingleUserExists(client, phone, true, instance.Id) + if errRetry == nil && foundRetry { + remoteJID = remoteJIDRetry + found = foundRetry + } + } + + if !found { + return types.NewJID("", types.DefaultUserServer), fmt.Errorf("number %s is not registered on WhatsApp", phone) + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Number %s verified as valid WhatsApp user, using remoteJID: %s", instance.Id, phone, remoteJID) + + // Validate the remoteJID with formatJid=false for message sending + formatJidFalse := false + return validateMessageFields(remoteJID, &formatJidFalse, messageID, participant) +} + +// checkSingleUserExists checks if a single user exists on WhatsApp with the specified formatJid setting +// Returns: remoteJID, found, error +func (s *sendService) checkSingleUserExists(client *whatsmeow.Client, phone string, formatJid bool, instanceId string) (string, bool, error) { + phoneNumbers, err := utils.PrepareNumbersForWhatsAppCheck([]string{phone}, &formatJid) + if err != nil { + return "", false, fmt.Errorf("failed to prepare number for WhatsApp check: %v", err) + } + + // Check if the number exists on WhatsApp + resp, err := client.IsOnWhatsApp(context.Background(), phoneNumbers) + if err != nil { + return "", false, fmt.Errorf("failed to check if number %s exists on WhatsApp: %v", phoneNumbers[0], err) + } + + // Verify if the number was found + if len(resp) == 0 { + return "", false, fmt.Errorf("number %s not found in WhatsApp response", phoneNumbers[0]) + } + + // Check if the first result indicates the number is on WhatsApp + if !resp[0].IsIn { + return "", false, nil // Not an error, just not found + } + + // Return the remoteJID from WhatsApp's response + remoteJID := fmt.Sprintf("%v", resp[0].JID) + return remoteJID, true, nil +} + +func findURL(text string) string { + urlRegex := `http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+` + re := regexp.MustCompile(urlRegex) + urls := re.FindAllString(text, -1) + if len(urls) > 0 { + return urls[0] + } + return "" +} + +func (s *sendService) SendText(data *TextStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + return s.sendTextWithRetry(data, instance, 3) // 3 tentativas máximas +} + +func (s *sendService) sendTextWithRetry(data *TextStruct, instance *instance_model.Instance, maxRetries int) (*MessageSendStruct, error) { + for attempt := 1; attempt <= maxRetries; attempt++ { + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] SendText attempt %d/%d", instance.Id, attempt, maxRetries) + + _, err := s.ensureClientConnectedWithRetry(instance.Id, 2) + if err != nil { + if attempt == maxRetries { + return nil, err + } + continue + } + + msg := &waE2E.Message{ + ExtendedTextMessage: &waE2E.ExtendedTextMessage{ + Text: &data.Text, + }, + } + + message, err := s.SendMessage(instance, msg, "ExtendedTextMessage", &SendDataStruct{ + Id: data.Id, + Number: data.Number, + Quoted: data.Quoted, + Delay: data.Delay, + MentionAll: data.MentionAll, + MentionedJID: data.MentionedJID, + FormatJid: data.FormatJid, + ForwardingScore: data.ForwardingScore, + }) + + if err != nil { + // Check if it's a client disconnection error + if strings.Contains(err.Error(), "client disconnected") || strings.Contains(err.Error(), "no active session") { + s.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] SendText failed due to disconnection on attempt %d/%d: %v", instance.Id, attempt, maxRetries, err) + if attempt < maxRetries { + waitTime := time.Duration(attempt) * time.Second + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Waiting %v before retry", instance.Id, waitTime) + time.Sleep(waitTime) + continue + } + } + return nil, err + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] SendText successful on attempt %d", instance.Id, attempt) + return message, nil + } + + return nil, fmt.Errorf("failed to send text after %d attempts", maxRetries) +} + +func fetchLinkMetadata(url string) (string, string, string, error) { + resp, err := http.Get(url) + if err != nil { + return "", "", "", err + } + defer resp.Body.Close() + + doc, err := html.Parse(resp.Body) + if err != nil { + return "", "", "", err + } + + var title, description, imgURL string + + var f func(*html.Node) + f = func(n *html.Node) { + if n.Type == html.ElementNode { + if n.Data == "title" && n.FirstChild != nil { + title = n.FirstChild.Data + } + if n.Data == "meta" { + var property, content string + for _, attr := range n.Attr { + if attr.Key == "property" || attr.Key == "name" { + property = attr.Val + } + if attr.Key == "content" { + content = attr.Val + } + } + + if (property == "description" || property == "og:description") && content != "" { + description = content + } + + if property == "og:image" && content != "" { + imgURL = content + } + } + } + + for c := n.FirstChild; c != nil; c = c.NextSibling { + f(c) + } + } + + f(doc) + + return title, description, imgURL, nil +} + +func (s *sendService) SendLink(data *LinkStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + return s.sendLinkWithRetry(data, instance, 3) +} + +func (s *sendService) sendLinkWithRetry(data *LinkStruct, instance *instance_model.Instance, maxRetries int) (*MessageSendStruct, error) { + for attempt := 1; attempt <= maxRetries; attempt++ { + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] SendLink attempt %d/%d", instance.Id, attempt, maxRetries) + + _, err := s.ensureClientConnectedWithRetry(instance.Id, 2) + if err != nil { + if attempt == maxRetries { + return nil, err + } + continue + } + + matchedText := findURL(data.Text) + + if matchedText != "" { + title, description, imgUrl, err := fetchLinkMetadata(matchedText) + if err != nil { + if attempt == maxRetries { + return nil, err + } + continue + } + + data.Title = title + data.Description = description + data.ImgUrl = imgUrl + } + + var fileData []byte + if data.ImgUrl != "" { + resp, err := http.Get(data.ImgUrl) + if err != nil { + if attempt == maxRetries { + return nil, err + } + continue + } + defer resp.Body.Close() + fileData, _ = io.ReadAll(resp.Body) + } + + previewType := waE2E.ExtendedTextMessage_VIDEO + msg := &waE2E.Message{ + ExtendedTextMessage: &waE2E.ExtendedTextMessage{ + Text: &data.Text, + Title: &data.Title, + MatchedText: &matchedText, + JPEGThumbnail: fileData, + Description: &data.Description, + PreviewType: &previewType, + }, + } + + message, err := s.SendMessage(instance, msg, "ExtendedTextMessage", &SendDataStruct{ + Id: data.Id, + Number: data.Number, + Quoted: data.Quoted, + Delay: data.Delay, + MentionAll: data.MentionAll, + MentionedJID: data.MentionedJID, + FormatJid: data.FormatJid, + }) + + if err != nil { + // Check if it's a client disconnection error + if strings.Contains(err.Error(), "client disconnected") || strings.Contains(err.Error(), "no active session") { + s.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] SendLink failed due to disconnection on attempt %d/%d: %v", instance.Id, attempt, maxRetries, err) + if attempt < maxRetries { + waitTime := time.Duration(attempt) * time.Second + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Waiting %v before retry", instance.Id, waitTime) + time.Sleep(waitTime) + continue + } + } + return nil, err + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] SendLink successful on attempt %d", instance.Id, attempt) + return message, nil + } + + return nil, fmt.Errorf("failed to send link after %d attempts", maxRetries) +} + +type ConvertAudio struct { + Url string `json:"url,omitempty"` + Base64 string `json:"base64,omitempty"` +} + +type ApiResponse struct { + Duration int `json:"duration"` + Audio string `json:"audio"` +} + +func convertAudioWithApi(apiUrl string, apiKey string, convertData ConvertAudio) ([]byte, int, error) { + var requestBody bytes.Buffer + writer := multipart.NewWriter(&requestBody) + + // Adiciona o campo "url" ao form-data se a URL for fornecida + if convertData.Url != "" { + err := writer.WriteField("url", convertData.Url) + if err != nil { + return nil, 0, fmt.Errorf("erro ao adicionar a URL no form-data: %v", err) + } + } + + // Adiciona o campo "base64" ao form-data se a string base64 for fornecida + if convertData.Base64 != "" { + err := writer.WriteField("base64", convertData.Base64) + if err != nil { + return nil, 0, fmt.Errorf("erro ao adicionar o base64 no form-data: %v", err) + } + } + + // Fecha o writer multipart + err := writer.Close() + if err != nil { + return nil, 0, fmt.Errorf("erro ao finalizar o form-data: %v", err) + } + + req, err := http.NewRequest("POST", apiUrl, &requestBody) + if err != nil { + return nil, 0, fmt.Errorf("erro ao criar a requisição: %v", err) + } + + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("apikey", apiKey) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, 0, fmt.Errorf("erro ao enviar a requisição: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, 0, fmt.Errorf("erro ao ler a resposta: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, 0, fmt.Errorf("requisição falhou com status: %d, resposta: %s", resp.StatusCode, string(body)) + } + + var apiResponse ApiResponse + err = json.Unmarshal(body, &apiResponse) + if err != nil { + return nil, 0, fmt.Errorf("erro ao deserializar a resposta: %v", err) + } + + base64ToBytes, err := base64.StdEncoding.DecodeString(apiResponse.Audio) + if err != nil { + return nil, 0, fmt.Errorf("erro ao decodificar o áudio: %v", err) + } + + return base64ToBytes, apiResponse.Duration, nil +} + +func convertAudioToOpusWithDuration(inputData []byte) ([]byte, int, error) { + cmd := exec.Command("ffmpeg", "-i", "pipe:0", + "-f", + "ogg", + "-vn", + "-c:a", + "libopus", + "-avoid_negative_ts", + "make_zero", + "-b:a", + "128k", + "-ar", + "48000", + "-ac", + "1", + "-write_xing", + "0", + "-compression_level", + "10", + "-application", + "voip", + "-fflags", + "+bitexact", + "-flags", + "+bitexact", + "-id3v2_version", + "0", + "-map_metadata", + "-1", + "-map_chapters", + "-1", + "-write_bext", + "0", + "pipe:1", + ) + + var outBuffer bytes.Buffer + var errBuffer bytes.Buffer + + cmd.Stdin = bytes.NewReader(inputData) + cmd.Stdout = &outBuffer + cmd.Stderr = &errBuffer + + err := cmd.Run() + if err != nil { + return nil, 0, fmt.Errorf("error during conversion: %v, details: %s", err, errBuffer.String()) + } + + convertedData := outBuffer.Bytes() + + outputText := errBuffer.String() + + splitTime := strings.Split(outputText, "time=") + + if len(splitTime) < 2 { + return nil, 0, errors.New("duração não encontrada") + } + + // Use the last occurrence of time= in case there are multiple + timeString := splitTime[len(splitTime)-1] + + re := regexp.MustCompile(`(\d+):(\d+):(\d+\.\d+)`) + matches := re.FindStringSubmatch(timeString) + if len(matches) != 4 { + return nil, 0, errors.New("formato de duração não encontrado") + } + + hours, _ := strconv.ParseFloat(matches[1], 64) + minutes, _ := strconv.ParseFloat(matches[2], 64) + seconds, _ := strconv.ParseFloat(matches[3], 64) + duration := int(hours*3600 + minutes*60 + seconds) + + return convertedData, duration, nil +} + +func (s *sendService) SendMediaFile(data *MediaStruct, fileData []byte, instance *instance_model.Instance) (*MessageSendStruct, error) { + return s.sendMediaFileWithRetry(data, fileData, instance, 3) +} + +func (s *sendService) sendMediaFileWithRetry(data *MediaStruct, fileData []byte, instance *instance_model.Instance, maxRetries int) (*MessageSendStruct, error) { + for attempt := 1; attempt <= maxRetries; attempt++ { + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] SendMediaFile attempt %d/%d", instance.Id, attempt, maxRetries) + + client, err := s.ensureClientConnectedWithRetry(instance.Id, 2) + if err != nil { + if attempt == maxRetries { + return nil, err + } + continue + } + + mime, _ := mimetype.DetectReader(bytes.NewReader(fileData)) + mimeType := mime.String() + + var uploadType whatsmeow.MediaType + var duration int + + switch data.Type { + case "image": + if mimeType != "image/jpeg" && mimeType != "image/png" && mimeType != "image/webp" { + errMsg := fmt.Sprintf("Invalid file format: '%s'. Only 'image/jpeg', 'image/png' and 'image/webp' are accepted", mimeType) + return nil, errors.New(errMsg) + } + if mimeType == "image/webp" { + mimeType = "image/jpeg" + } + uploadType = whatsmeow.MediaImage + case "video": + if mimeType != "video/mp4" { + errMsg := fmt.Sprintf("Invalid file format: '%s'. Only 'video/mp4' is accepted", mimeType) + return nil, errors.New(errMsg) + } + uploadType = whatsmeow.MediaVideo + case "audio": + converterApiUrl := s.config.ApiAudioConverter + converterApiKey := s.config.ApiAudioConverterKey + var convertedData []byte + var err error + if converterApiUrl == "" { + + convertedData, duration, err = convertAudioToOpusWithDuration(fileData) + if err != nil { + return nil, err + } + } else { + convertedData, duration, err = convertAudioWithApi(converterApiUrl, converterApiKey, ConvertAudio{Base64: base64.StdEncoding.EncodeToString(fileData)}) + if err != nil { + return nil, err + } + } + + fileData = convertedData + mimeType = "audio/ogg; codecs=opus" + uploadType = whatsmeow.MediaAudio + case "document": + uploadType = whatsmeow.MediaDocument + default: + return nil, errors.New("invalid media type") + } + + // Detectar se é newsletter para usar upload sem criptografia + isNewsletter := strings.Contains(data.Number, "@newsletter") + + // Validar se é documento em newsletter (não suportado) + if isNewsletter && data.Type == "document" { + return nil, errors.New("documentos não são suportados em canais do WhatsApp. Use imagem, vídeo, áudio ou enquete") + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] SendMediaFile - Upload iniciado (Newsletter: %v)...", instance.Id, isNewsletter) + + var uploaded whatsmeow.UploadResponse + if isNewsletter { + // Newsletter: upload SEM criptografia + uploaded, err = client.UploadNewsletter(context.Background(), fileData, uploadType) + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Newsletter upload - Handle: %s", instance.Id, uploaded.Handle) + } else { + // Normal: upload COM criptografia + uploaded, err = client.Upload(context.Background(), fileData, uploadType) + } + + if err != nil { + return nil, err + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Media uploaded with size %d", instance.Id, uploaded.FileLength) + + var media *waE2E.Message + var mediaType string + + switch data.Type { + case "image": + // Generate a JPEG preview thumbnail for better client UX (iOS in + // particular). On failure jpegThumb is nil and the message is sent + // without a preview rather than failing the request. + jpegThumb := makeJPEGThumbnail(fileData, 72) + if isNewsletter { + // Newsletter: SEM MediaKey e FileEncSHA256 + media = &waE2E.Message{ImageMessage: &waE2E.ImageMessage{ + Caption: proto.String(data.Caption), + URL: &uploaded.URL, + DirectPath: &uploaded.DirectPath, + Mimetype: proto.String(mimeType), + FileSHA256: uploaded.FileSHA256, + FileLength: &uploaded.FileLength, + JPEGThumbnail: jpegThumb, + }} + } else { + // Normal: COM MediaKey e FileEncSHA256 + media = &waE2E.Message{ImageMessage: &waE2E.ImageMessage{ + Caption: proto.String(data.Caption), + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimeType), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + JPEGThumbnail: jpegThumb, + }} + } + mediaType = "ImageMessage" + case "video": + if isNewsletter { + media = &waE2E.Message{VideoMessage: &waE2E.VideoMessage{ + Caption: proto.String(data.Caption), + URL: &uploaded.URL, + DirectPath: &uploaded.DirectPath, + Mimetype: proto.String(mimeType), + FileSHA256: uploaded.FileSHA256, + FileLength: &uploaded.FileLength, + }} + } else { + media = &waE2E.Message{VideoMessage: &waE2E.VideoMessage{ + Caption: proto.String(data.Caption), + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimeType), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + }} + } + mediaType = "VideoMessage" + case "ptv": + if isNewsletter { + media = &waE2E.Message{PtvMessage: &waE2E.VideoMessage{ + URL: &uploaded.URL, + DirectPath: &uploaded.DirectPath, + Mimetype: proto.String(mimeType), + FileSHA256: uploaded.FileSHA256, + FileLength: &uploaded.FileLength, + }} + } else { + media = &waE2E.Message{PtvMessage: &waE2E.VideoMessage{ + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimeType), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + }} + } + mediaType = "PtvMessage" + case "audio": + if isNewsletter { + media = &waE2E.Message{AudioMessage: &waE2E.AudioMessage{ + URL: &uploaded.URL, + PTT: proto.Bool(true), + DirectPath: &uploaded.DirectPath, + Mimetype: proto.String(mimeType), + FileSHA256: uploaded.FileSHA256, + FileLength: &uploaded.FileLength, + Seconds: proto.Uint32(uint32(duration)), + }} + } else { + media = &waE2E.Message{AudioMessage: &waE2E.AudioMessage{ + URL: proto.String(uploaded.URL), + PTT: proto.Bool(true), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimeType), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uploaded.FileLength), + Seconds: proto.Uint32(uint32(duration)), + }} + } + mediaType = "AudioMessage" + case "document": + // For PDF documents, rasterize page 1 into a JPEG preview thumbnail. + // A missing pdftoppm or a failure yields nil and the document is + // sent without a preview instead of failing the request. + var jpegThumb []byte + if mimeType == "application/pdf" { + jpegThumb = makePDFThumbnail(fileData, 200) + } + if isNewsletter { + media = &waE2E.Message{DocumentMessage: &waE2E.DocumentMessage{ + FileName: &data.Filename, + Caption: proto.String(data.Caption), + URL: &uploaded.URL, + DirectPath: &uploaded.DirectPath, + Mimetype: proto.String(mimeType), + FileSHA256: uploaded.FileSHA256, + FileLength: &uploaded.FileLength, + JPEGThumbnail: jpegThumb, + }} + } else { + media = &waE2E.Message{DocumentMessage: &waE2E.DocumentMessage{ + FileName: &data.Filename, + Caption: proto.String(data.Caption), + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimeType), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + JPEGThumbnail: jpegThumb, + }} + } + + if media.GetDocumentMessage().GetCaption() != "" { + media.DocumentWithCaptionMessage = &waE2E.FutureProofMessage{ + Message: &waE2E.Message{ + DocumentMessage: media.DocumentMessage, + }, + } + media.DocumentMessage = nil + } + + mediaType = "DocumentMessage" + default: + return nil, errors.New("invalid media type") + } + + message, err := s.SendMessage(instance, media, mediaType, &SendDataStruct{ + Id: data.Id, + Number: data.Number, + Quoted: data.Quoted, + Delay: data.Delay, + MentionAll: data.MentionAll, + MentionedJID: data.MentionedJID, + FormatJid: data.FormatJid, + MediaHandle: uploaded.Handle, + ForwardingScore: data.ForwardingScore, + }) + + if err != nil { + // Check if it's a client disconnection error + if strings.Contains(err.Error(), "client disconnected") || strings.Contains(err.Error(), "no active session") { + s.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] SendMediaFile failed due to disconnection on attempt %d/%d: %v", instance.Id, attempt, maxRetries, err) + if attempt < maxRetries { + waitTime := time.Duration(attempt) * time.Second + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Waiting %v before retry", instance.Id, waitTime) + time.Sleep(waitTime) + continue + } + } + return nil, err + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] SendMediaFile successful on attempt %d", instance.Id, attempt) + return message, nil + } + + return nil, fmt.Errorf("failed to send media file after %d attempts", maxRetries) +} + +func (s *sendService) SendMediaUrl(data *MediaStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + return s.sendMediaUrlWithRetry(data, instance, 3) +} + +func (s *sendService) sendMediaUrlWithRetry(data *MediaStruct, instance *instance_model.Instance, maxRetries int) (*MessageSendStruct, error) { + for attempt := 1; attempt <= maxRetries; attempt++ { + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] SendMediaUrl attempt %d/%d for URL: %s", instance.Id, attempt, maxRetries, data.Url) + startTime := time.Now() + + client, err := s.ensureClientConnectedWithRetry(instance.Id, 2) + if err != nil { + if attempt == maxRetries { + return nil, err + } + continue + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Iniciando download da URL: %s", instance.Id, data.Url) + + resp, err := http.Get(data.Url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Download concluído em %v. Lendo dados...", instance.Id, time.Since(startTime)) + + downloadStart := time.Now() + fileData, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Leitura dos dados concluída em %v. Tamanho: %d bytes", instance.Id, time.Since(downloadStart), len(fileData)) + + mime, _ := mimetype.DetectReader(bytes.NewReader(fileData)) + mimeType := mime.String() + if strings.HasSuffix(strings.ToLower(data.Url), ".mp4") { + mimeType = "video/mp4" + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Tipo MIME detectado: %s", instance.Id, mimeType) + + var uploadType whatsmeow.MediaType + var duration int + + processingStart := time.Now() + switch data.Type { + case "image": + if mimeType != "image/jpeg" && mimeType != "image/png" && mimeType != "image/webp" { + errMsg := fmt.Sprintf("Invalid file format: '%s'. Only 'image/jpeg', 'image/png' and 'image/webp' are accepted", mimeType) + return nil, errors.New(errMsg) + } + if mimeType == "image/webp" { + mimeType = "image/jpeg" + } + uploadType = whatsmeow.MediaImage + + case "video", "ptv": + if mimeType != "video/mp4" { + errMsg := fmt.Sprintf("Invalid file format: '%s'. Only 'video/mp4' are accepted", mimeType) + return nil, errors.New(errMsg) + } + uploadType = whatsmeow.MediaVideo + case "audio": + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Iniciando conversão de áudio...", instance.Id) + converterApiUrl := s.config.ApiAudioConverter + converterApiKey := s.config.ApiAudioConverterKey + var convertedData []byte + var err error + if converterApiUrl == "" { + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Usando conversão local...", instance.Id) + convertedData, duration, err = convertAudioToOpusWithDuration(fileData) + } else { + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Usando API de conversão...", instance.Id) + convertedData, duration, err = convertAudioWithApi(converterApiUrl, converterApiKey, ConvertAudio{Base64: base64.StdEncoding.EncodeToString(fileData)}) + } + if err != nil { + return nil, err + } + fileData = convertedData + mimeType = "audio/ogg; codecs=opus" + uploadType = whatsmeow.MediaAudio + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Conversão de áudio concluída em %v", instance.Id, time.Since(processingStart)) + case "document": + uploadType = whatsmeow.MediaDocument + default: + return nil, errors.New("invalid media type") + } + + // Detectar se é newsletter para usar upload sem criptografia + isNewsletter := strings.Contains(data.Number, "@newsletter") + + // Validar se é documento em newsletter (não suportado) + if isNewsletter && data.Type == "document" { + return nil, errors.New("documentos não são suportados em canais do WhatsApp. Use imagem, vídeo, áudio ou enquete") + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Iniciando upload para WhatsApp (Newsletter: %v)...", instance.Id, isNewsletter) + uploadStart := time.Now() + + var uploaded whatsmeow.UploadResponse + if isNewsletter { + // Newsletter: upload sem criptografia + uploaded, err = client.UploadNewsletter(context.Background(), fileData, uploadType) + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Newsletter upload - Handle: %s", instance.Id, uploaded.Handle) + } else { + // Upload normal com criptografia + uploaded, err = client.Upload(context.Background(), fileData, uploadType) + } + + if err != nil { + return nil, err + } + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Upload concluído em %v. Tamanho: %d", instance.Id, time.Since(uploadStart), uploaded.FileLength) + + var media *waE2E.Message + var mediaType string + + switch data.Type { + case "image": + // Generate a JPEG preview thumbnail for better client UX (iOS in + // particular). On failure jpegThumb is nil and the message is sent + // without a preview rather than failing the request. + jpegThumb := makeJPEGThumbnail(fileData, 72) + if isNewsletter { + // Newsletter: sem criptografia (sem MediaKey e FileEncSHA256) + media = &waE2E.Message{ImageMessage: &waE2E.ImageMessage{ + Caption: proto.String(data.Caption), + URL: &uploaded.URL, + DirectPath: &uploaded.DirectPath, + Mimetype: proto.String(mimeType), + FileSHA256: uploaded.FileSHA256, + FileLength: &uploaded.FileLength, + JPEGThumbnail: jpegThumb, + }} + } else { + // Normal: com criptografia + media = &waE2E.Message{ImageMessage: &waE2E.ImageMessage{ + Caption: proto.String(data.Caption), + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimeType), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + JPEGThumbnail: jpegThumb, + }} + } + mediaType = "ImageMessage" + case "video": + if isNewsletter { + media = &waE2E.Message{VideoMessage: &waE2E.VideoMessage{ + Caption: proto.String(data.Caption), + URL: &uploaded.URL, + DirectPath: &uploaded.DirectPath, + Mimetype: proto.String(mimeType), + FileSHA256: uploaded.FileSHA256, + FileLength: &uploaded.FileLength, + }} + } else { + media = &waE2E.Message{VideoMessage: &waE2E.VideoMessage{ + Caption: proto.String(data.Caption), + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimeType), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + }} + } + mediaType = "VideoMessage" + case "ptv": + if isNewsletter { + media = &waE2E.Message{PtvMessage: &waE2E.VideoMessage{ + URL: &uploaded.URL, + DirectPath: &uploaded.DirectPath, + Mimetype: proto.String(mimeType), + FileSHA256: uploaded.FileSHA256, + FileLength: &uploaded.FileLength, + }} + } else { + media = &waE2E.Message{PtvMessage: &waE2E.VideoMessage{ + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimeType), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + }} + } + mediaType = "PtvMessage" + case "audio": + if isNewsletter { + media = &waE2E.Message{AudioMessage: &waE2E.AudioMessage{ + URL: &uploaded.URL, + PTT: proto.Bool(true), + DirectPath: &uploaded.DirectPath, + Mimetype: proto.String(mimeType), + FileSHA256: uploaded.FileSHA256, + FileLength: &uploaded.FileLength, + StreamingSidecar: []byte(*proto.String("QpmXDsU7YLagdg==")), + Waveform: []byte(*proto.String("OjAnExISDgsKCAkJBwgkHAQEBBEFAwMNAxAcKCgkFzM0QUE4Jh4eKAoKChcLCwkeFgkJCQo3JiQmIiIRPz8/Ow==")), + Seconds: proto.Uint32(uint32(duration)), + }} + } else { + media = &waE2E.Message{AudioMessage: &waE2E.AudioMessage{ + URL: proto.String(uploaded.URL), + PTT: proto.Bool(true), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimeType), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uploaded.FileLength), + StreamingSidecar: []byte(*proto.String("QpmXDsU7YLagdg==")), + Waveform: []byte(*proto.String("OjAnExISDgsKCAkJBwgkHAQEBBEFAwMNAxAcKCgkFzM0QUE4Jh4eKAoKChcLCwkeFgkJCQo3JiQmIiIRPz8/Ow==")), + Seconds: proto.Uint32(uint32(duration)), + }} + } + mediaType = "AudioMessage" + case "document": + // For PDF documents, rasterize page 1 into a JPEG preview thumbnail. + // A missing pdftoppm or a failure yields nil and the document is + // sent without a preview instead of failing the request. + var jpegThumb []byte + if mimeType == "application/pdf" { + jpegThumb = makePDFThumbnail(fileData, 200) + } + if isNewsletter { + media = &waE2E.Message{DocumentMessage: &waE2E.DocumentMessage{ + URL: &uploaded.URL, + FileName: &data.Filename, + Caption: proto.String(data.Caption), + DirectPath: &uploaded.DirectPath, + Mimetype: proto.String(mimeType), + FileSHA256: uploaded.FileSHA256, + FileLength: &uploaded.FileLength, + JPEGThumbnail: jpegThumb, + }} + } else { + media = &waE2E.Message{DocumentMessage: &waE2E.DocumentMessage{ + URL: proto.String(uploaded.URL), + FileName: &data.Filename, + Caption: proto.String(data.Caption), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimeType), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + JPEGThumbnail: jpegThumb, + }} + } + + if media.GetDocumentMessage().GetCaption() != "" { + media.DocumentWithCaptionMessage = &waE2E.FutureProofMessage{ + Message: &waE2E.Message{ + DocumentMessage: media.DocumentMessage, + }, + } + media.DocumentMessage = nil + } + + mediaType = "DocumentMessage" + default: + return nil, errors.New("invalid media type") + } + + messageStart := time.Now() + message, err := s.SendMessage(instance, media, mediaType, &SendDataStruct{ + Id: data.Id, + Number: data.Number, + Quoted: data.Quoted, + Delay: data.Delay, + MentionAll: data.MentionAll, + MentionedJID: data.MentionedJID, + FormatJid: data.FormatJid, + MediaHandle: uploaded.Handle, + ForwardingScore: data.ForwardingScore, + }) + + if err != nil { + // Check if it's a client disconnection error + if strings.Contains(err.Error(), "client disconnected") || strings.Contains(err.Error(), "no active session") { + s.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] SendMediaUrl failed due to disconnection on attempt %d/%d: %v", instance.Id, attempt, maxRetries, err) + if attempt < maxRetries { + waitTime := time.Duration(attempt) * time.Second + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Waiting %v before retry", instance.Id, waitTime) + time.Sleep(waitTime) + continue + } + } + return nil, err + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Mensagem enviada em %v", instance.Id, time.Since(messageStart)) + + totalTime := time.Since(startTime) + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] SendMediaUrl successful on attempt %d, processo completo em %v", instance.Id, attempt, totalTime) + + return message, nil + } + + return nil, fmt.Errorf("failed to send media url after %d attempts", maxRetries) +} + +func (s *sendService) SendPoll(data *PollStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + return s.sendPollWithRetry(data, instance, 3) +} + +func (s *sendService) sendPollWithRetry(data *PollStruct, instance *instance_model.Instance, maxRetries int) (*MessageSendStruct, error) { + for attempt := 1; attempt <= maxRetries; attempt++ { + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] SendPoll attempt %d/%d", instance.Id, attempt, maxRetries) + + client, err := s.ensureClientConnectedWithRetry(instance.Id, 2) + if err != nil { + if attempt == maxRetries { + return nil, err + } + continue + } + + msg := client.BuildPollCreation(data.Question, data.Options, data.MaxAnswer) + + message, err := s.SendMessage(instance, msg, "PollCreationMessage", &SendDataStruct{ + Id: data.Id, + Number: data.Number, + Quoted: data.Quoted, + Delay: data.Delay, + MentionAll: data.MentionAll, + MentionedJID: data.MentionedJID, + FormatJid: data.FormatJid, + }) + + if err != nil { + // Check if it's a client disconnection error + if strings.Contains(err.Error(), "client disconnected") || strings.Contains(err.Error(), "no active session") { + s.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] SendPoll failed due to disconnection on attempt %d/%d: %v", instance.Id, attempt, maxRetries, err) + if attempt < maxRetries { + waitTime := time.Duration(attempt) * time.Second + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Waiting %v before retry", instance.Id, waitTime) + time.Sleep(waitTime) + continue + } + } + return nil, err + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] SendPoll successful on attempt %d", instance.Id, attempt) + return message, nil + } + + return nil, fmt.Errorf("failed to send poll after %d attempts", maxRetries) +} + +func convertToWebP(imageData string) ([]byte, error) { + var img image.Image + var err error + + resp, err := http.Get(imageData) + if err != nil { + return nil, fmt.Errorf("failed to fetch image from URL: %v", err) + } + defer resp.Body.Close() + + img, _, err = image.Decode(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to decode image: %v", err) + } + + var webpBuffer bytes.Buffer + err = webp.Encode(&webpBuffer, img, &webp.Options{Lossless: false, Quality: 80}) + if err != nil { + return nil, fmt.Errorf("failed to encode image to WebP: %v", err) + } + + return webpBuffer.Bytes(), nil +} + +func (s *sendService) SendSticker(data *StickerStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + client, err := s.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + var uploaded whatsmeow.UploadResponse + var filedata []byte + + if strings.HasPrefix(data.Sticker, "http") { + webpData, err := convertToWebP(data.Sticker) + if err != nil { + return nil, fmt.Errorf("failed to convert image to WebP: %v", err) + } + + filedata = webpData + + uploaded, err = client.Upload(context.Background(), filedata, whatsmeow.MediaImage) + if err != nil { + return nil, fmt.Errorf("failed to upload sticker: %v", err) + } + } else { + return nil, fmt.Errorf("invalid sticker URL") + } + + msg := &waE2E.Message{StickerMessage: &waE2E.StickerMessage{ + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(http.DetectContentType(filedata)), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(filedata))), + }} + + message, err := s.SendMessage(instance, msg, "StickerMessage", &SendDataStruct{ + Id: data.Id, + Number: data.Number, + Quoted: data.Quoted, + Delay: data.Delay, + MentionAll: data.MentionAll, + MentionedJID: data.MentionedJID, + FormatJid: data.FormatJid, + }) + if err != nil { + return nil, err + } + + return message, nil +} + +func (s *sendService) SendLocation(data *LocationStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + _, err := s.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + msg := &waE2E.Message{LocationMessage: &waE2E.LocationMessage{ + DegreesLatitude: &data.Latitude, + DegreesLongitude: &data.Longitude, + Name: &data.Name, + Address: &data.Address, + }} + + message, err := s.SendMessage(instance, msg, "LocationMessage", &SendDataStruct{ + Id: data.Id, + Number: data.Number, + Quoted: data.Quoted, + Delay: data.Delay, + MentionAll: data.MentionAll, + MentionedJID: data.MentionedJID, + FormatJid: data.FormatJid, + }) + if err != nil { + return nil, err + } + + return message, nil +} + +func (s *sendService) SendContact(data *ContactStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + _, err := s.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + VCstring := utils.GenerateVC(utils.VCardStruct{ + FullName: data.Vcard.FullName, + Phone: data.Vcard.Phone, + Organization: data.Vcard.Organization, + }) + + fmt.Println(VCstring) + + msg := &waE2E.Message{ContactMessage: &waE2E.ContactMessage{ + DisplayName: &data.Vcard.FullName, + Vcard: &VCstring, + }} + + messaged, err := s.SendMessage(instance, msg, "ContactMessage", &SendDataStruct{ + Id: data.Id, + Number: data.Number, + Quoted: data.Quoted, + Delay: data.Delay, + MentionAll: data.MentionAll, + MentionedJID: data.MentionedJID, + FormatJid: data.FormatJid, + }) + if err != nil { + return nil, err + } + + return messaged, nil +} + +func mapKeyType(keyType string) string { + switch keyType { + case "phone": + return "PHONE" + case "email": + return "EMAIL" + case "cpf": + return "CPF" + case "cnpj": + return "CNPJ" + case "random": + return "EVP" + default: + return keyType + } +} + +func (s *sendService) SendButton(data *ButtonStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + client, err := s.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + hasReply := false + hasPix := false + hasOtherTypes := false + replyCount := 0 + + for _, v := range data.Buttons { + switch v.Type { + case "reply": + hasReply = true + replyCount++ + case "pix": + hasPix = true + default: + hasOtherTypes = true + } + } + + if hasReply { + if replyCount > 3 { + return nil, errors.New("máximo de 3 botões do tipo 'reply' permitidos") + } + if hasOtherTypes { + return nil, errors.New("botões do tipo 'reply' não podem ser misturados com outros tipos") + } + } + + if hasPix { + if len(data.Buttons) > 1 { + return nil, errors.New("botão do tipo 'pix' não pode ser combinado com outros botões") + } + } + + buttons := []*waE2E.InteractiveMessage_NativeFlowMessage_NativeFlowButton{} + + for _, v := range data.Buttons { + var paramsJSON *string + var name *string + + switch v.Type { + case "reply": + name = proto.String("quick_reply") + jsonBytes, _ := json.Marshal(map[string]string{"display_text": v.DisplayText, "id": v.Id}) + paramsJSON = proto.String(string(jsonBytes)) + case "copy": + name = proto.String("cta_copy") + copyCode := v.CopyCode + if copyCode == "" { + copyCode = v.Id + } + copyId := v.Id + if copyId == "" { + copyId = "copy_" + strconv.FormatInt(time.Now().UnixNano(), 10) + } + jsonBytes, _ := json.Marshal(map[string]string{"display_text": v.DisplayText, "id": copyId, "copy_code": copyCode}) + paramsJSON = proto.String(string(jsonBytes)) + case "url": + name = proto.String("cta_url") + jsonBytes, _ := json.Marshal(map[string]string{"display_text": v.DisplayText, "url": v.URL, "merchant_url": v.URL}) + paramsJSON = proto.String(string(jsonBytes)) + case "call": + name = proto.String("cta_call") + jsonBytes, _ := json.Marshal(map[string]string{"display_text": v.DisplayText, "phone_number": v.PhoneNumber}) + paramsJSON = proto.String(string(jsonBytes)) + case "pix": + randomId := utils.GenerateRandomString(11) + name = proto.String("payment_info") + paymentPayload := map[string]interface{}{ + "currency": v.Currency, + "total_amount": map[string]interface{}{"value": 0, "offset": 100}, + "reference_id": randomId, + "type": "physical-goods", + "order": map[string]interface{}{ + "status": "pending", + "subtotal": map[string]interface{}{"value": 0, "offset": 100}, + "order_type": "ORDER", + "items": []map[string]interface{}{ + { + "name": "", + "amount": map[string]interface{}{"value": 0, "offset": 100}, + "quantity": 0, + "sale_amount": map[string]interface{}{"value": 0, "offset": 100}, + }, + }, + }, + "payment_settings": []map[string]interface{}{ + { + "type": "pix_static_code", + "pix_static_code": map[string]string{ + "merchant_name": v.Name, + "key": v.Key, + "key_type": mapKeyType(v.KeyType), + }, + }, + }, + "share_payment_status": false, + } + jsonBytes, _ := json.Marshal(paymentPayload) + paramsJSON = proto.String(string(jsonBytes)) + } + + buttons = append(buttons, &waE2E.InteractiveMessage_NativeFlowMessage_NativeFlowButton{ + Name: name, + ButtonParamsJSON: paramsJSON, + }) + } + + templateId := strconv.FormatInt(time.Now().UnixNano()/1000000, 10) + messageParamsJSON := `{"from":"api","templateId":` + templateId + `}` + + // MessageSecret (32 random bytes) — required for iOS to render interactive messages. + btnMsgSecret := make([]byte, 32) + _, _ = crypto_rand.Read(btnMsgSecret) + + var msg *waE2E.Message + var msgType string + + if hasReply && !hasOtherTypes && !hasPix { + // Reply-only: native ButtonsMessage wrapped in DocumentWithCaptionMessage (Baileys PR #36). + var replyButtons []*waE2E.ButtonsMessage_Button + for _, v := range data.Buttons { + replyButtons = append(replyButtons, &waE2E.ButtonsMessage_Button{ + ButtonID: proto.String(v.Id), + ButtonText: &waE2E.ButtonsMessage_Button_ButtonText{ + DisplayText: proto.String(v.DisplayText), + }, + Type: waE2E.ButtonsMessage_Button_RESPONSE.Enum(), + }) + } + + buttonsMsg := &waE2E.ButtonsMessage{ + ContentText: proto.String(data.Description), + FooterText: proto.String(data.Footer), + HeaderType: waE2E.ButtonsMessage_EMPTY.Enum(), + Buttons: replyButtons, + } + + // Optional media header (image or video URL). + if data.ImageUrl != "" { + if resp, err := http.Get(data.ImageUrl); err == nil { + fileData, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + if readErr == nil { + if uploaded, upErr := client.Upload(context.Background(), fileData, whatsmeow.MediaImage); upErr == nil { + buttonsMsg.HeaderType = waE2E.ButtonsMessage_IMAGE.Enum() + buttonsMsg.Header = &waE2E.ButtonsMessage_ImageMessage{ + ImageMessage: &waE2E.ImageMessage{ + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String("image/jpeg"), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + }, + } + } + } + } + } else if data.VideoUrl != "" { + if resp, err := http.Get(data.VideoUrl); err == nil { + fileData, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + if readErr == nil { + if uploaded, upErr := client.Upload(context.Background(), fileData, whatsmeow.MediaVideo); upErr == nil { + buttonsMsg.HeaderType = waE2E.ButtonsMessage_VIDEO.Enum() + buttonsMsg.Header = &waE2E.ButtonsMessage_VideoMessage{ + VideoMessage: &waE2E.VideoMessage{ + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String("video/mp4"), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + }, + } + } + } + } + } + + msg = &waE2E.Message{ + DocumentWithCaptionMessage: &waE2E.FutureProofMessage{ + Message: &waE2E.Message{ + ButtonsMessage: buttonsMsg, + }, + }, + MessageContextInfo: &waE2E.MessageContextInfo{ + MessageSecret: btnMsgSecret, + }, + } + msgType = "ButtonsMessage" + } else if hasPix { + // Pix: NativeFlowMessage wrapped in DocumentWithCaptionMessage. + paymentMsgParams := `{"native_flow_name":"order_details","version":1}` + + var interactiveBody *waE2E.InteractiveMessage_Body + if data.Title != "" { + bodyText := data.Title + interactiveBody = &waE2E.InteractiveMessage_Body{Text: &bodyText} + } + + msg = &waE2E.Message{ + DocumentWithCaptionMessage: &waE2E.FutureProofMessage{ + Message: &waE2E.Message{ + InteractiveMessage: &waE2E.InteractiveMessage{ + Body: interactiveBody, + InteractiveMessage: &waE2E.InteractiveMessage_NativeFlowMessage_{ + NativeFlowMessage: &waE2E.InteractiveMessage_NativeFlowMessage{ + Buttons: buttons, + MessageParamsJSON: &paymentMsgParams, + MessageVersion: proto.Int32(1), + }, + }, + }, + }, + }, + MessageContextInfo: &waE2E.MessageContextInfo{ + MessageSecret: btnMsgSecret, + }, + } + msgType = "InteractiveMessage" + } else { + // Mixed CTA buttons (url/copy/call): NativeFlowMessage wrapped in DocumentWithCaptionMessage. + body := func() string { + t := "*" + data.Title + "*" + if data.Description != "" { + t += "\n\n" + data.Description + "\n" + } + return t + }() + + msg = &waE2E.Message{ + DocumentWithCaptionMessage: &waE2E.FutureProofMessage{ + Message: &waE2E.Message{ + InteractiveMessage: &waE2E.InteractiveMessage{ + Body: &waE2E.InteractiveMessage_Body{ + Text: &body, + }, + Footer: &waE2E.InteractiveMessage_Footer{ + Text: &data.Footer, + }, + InteractiveMessage: &waE2E.InteractiveMessage_NativeFlowMessage_{ + NativeFlowMessage: &waE2E.InteractiveMessage_NativeFlowMessage{ + Buttons: buttons, + MessageParamsJSON: &messageParamsJSON, + MessageVersion: proto.Int32(1), + }, + }, + }, + }, + }, + MessageContextInfo: &waE2E.MessageContextInfo{ + MessageSecret: btnMsgSecret, + }, + } + msgType = "InteractiveMessage" + } + + // Build biz/bot nodes injected directly in the XMPP stanza — required for mobile rendering. + // Reply-only buttons get ; CTA/Pix get . + // The node is required for 1:1 chats (skipped on groups). + var bizInteractiveContent waBinary.Node + if hasReply && !hasOtherTypes && !hasPix { + bizInteractiveContent = waBinary.Node{ + Tag: "interactive", + Attrs: waBinary.Attrs{ + "type": "native_flow", + "v": "1", + }, + Content: []waBinary.Node{{ + Tag: "native_flow", + Attrs: waBinary.Attrs{ + "name": "quick_reply", + }, + }}, + } + } else if hasPix { + bizInteractiveContent = waBinary.Node{ + Tag: "interactive", + Attrs: waBinary.Attrs{ + "type": "native_flow", + "v": "1", + }, + Content: []waBinary.Node{{ + Tag: "native_flow", + Attrs: waBinary.Attrs{ + "name": "payment_info", + }, + }}, + } + } else { + // Mixed CTA buttons (url/copy/call) — name="mixed" is the WhatsApp convention. + bizInteractiveContent = waBinary.Node{ + Tag: "interactive", + Attrs: waBinary.Attrs{ + "type": "native_flow", + "v": "1", + }, + Content: []waBinary.Node{{ + Tag: "native_flow", + Attrs: waBinary.Attrs{ + "name": "mixed", + }, + }}, + } + } + + bizNodes := []waBinary.Node{ + { + Tag: "biz", + Content: []waBinary.Node{bizInteractiveContent}, + }, + } + if !strings.Contains(data.Number, "@g.us") { + bizNodes = append(bizNodes, waBinary.Node{ + Tag: "bot", + Attrs: waBinary.Attrs{"biz_bot": "1"}, + }) + } + + // Route through centralized SendMessage for ContextInfo, webhooks, quotes, mentions. + message, err := s.SendMessage(instance, msg, msgType, &SendDataStruct{ + Number: data.Number, + Delay: data.Delay, + MentionAll: data.MentionAll, + MentionedJID: data.MentionedJID, + FormatJid: data.FormatJid, + Quoted: data.Quoted, + AdditionalNodes: &bizNodes, + }) + if err != nil { + s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error sending button message: %v", instance.Id, err) + return nil, err + } + + return message, nil +} + +func stringPointer(s string) *string { + return &s +} + +// makeJPEGThumbnail decodes raw image bytes and produces a small JPEG +// thumbnail suitable for the JPEGThumbnail field of WhatsApp media messages. +// The thumbnail keeps the original aspect ratio and is capped at maxWidth +// pixels wide. It returns nil if the image cannot be decoded so callers can +// fall back to sending the message without a preview thumbnail. +func makeJPEGThumbnail(fileData []byte, maxWidth int) []byte { + if maxWidth < 1 { + maxWidth = 72 + } + + img, _, err := image.Decode(bytes.NewReader(fileData)) + if err != nil { + return nil + } + + bounds := img.Bounds() + srcWidth := bounds.Dx() + srcHeight := bounds.Dy() + if srcWidth < 1 || srcHeight < 1 { + return nil + } + + thumbWidth := maxWidth + if srcWidth < thumbWidth { + thumbWidth = srcWidth + } + thumbHeight := int(float64(srcHeight) * float64(thumbWidth) / float64(srcWidth)) + if thumbHeight < 1 { + thumbHeight = 1 + } + + thumbImg := image.NewRGBA(image.Rect(0, 0, thumbWidth, thumbHeight)) + for y := 0; y < thumbHeight; y++ { + for x := 0; x < thumbWidth; x++ { + srcX := x * srcWidth / thumbWidth + srcY := y * srcHeight / thumbHeight + thumbImg.Set(x, y, img.At(srcX+bounds.Min.X, srcY+bounds.Min.Y)) + } + } + + var thumbBuf bytes.Buffer + if err := jpeg.Encode(&thumbBuf, thumbImg, &jpeg.Options{Quality: 50}); err != nil { + return nil + } + return thumbBuf.Bytes() +} + +// makePDFThumbnail rasterizes the first page of a PDF into a JPEG thumbnail +// using the external "pdftoppm" tool (poppler-utils). It returns nil when +// pdftoppm is not installed or rasterization fails, so callers can gracefully +// send the document without a preview instead of failing the request. +func makePDFThumbnail(fileData []byte, maxWidth int) []byte { + if _, err := exec.LookPath("pdftoppm"); err != nil { + return nil + } + + scaleWidth := maxWidth + if scaleWidth < 1 { + scaleWidth = 72 + } + + // Render only the first page to a PNG on stdout, scaled to scaleWidth. + // "-scale-to-y -1" keeps the original aspect ratio. + cmd := exec.Command("pdftoppm", + "-png", + "-f", "1", + "-l", "1", + "-singlefile", + "-scale-to-x", strconv.Itoa(scaleWidth), + "-scale-to-y", "-1", + ) + cmd.Stdin = bytes.NewReader(fileData) + + var out bytes.Buffer + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + return nil + } + if out.Len() == 0 { + return nil + } + + // Re-encode the rendered PNG as a JPEG thumbnail for consistency with images. + return makeJPEGThumbnail(out.Bytes(), maxWidth) +} + +func sectionsToString(data *ListStruct) (string, error) { + type row struct { + Header string `json:"header"` + Title string `json:"title"` + Description string `json:"description"` + ID string `json:"id"` + } + + type listSection struct { + Title string `json:"title"` + HighlightLabel string `json:"highlight_label"` + Rows []row `json:"rows"` + } + + type list struct { + Title string `json:"title"` + Sections []listSection `json:"sections"` + } + + sections := []listSection{} + + for _, s := range data.Sections { + sectionTitle := s.Title + if sectionTitle == "" { + sectionTitle = " " + } + rows := []row{} + + for _, r := range s.Rows { + rowTitle := r.Title + if rowTitle == "" { + rowTitle = " " + } + rowDesc := r.Description + if rowDesc == "" { + rowDesc = " " + } + rowId := r.RowId + if rowId == "" { + rowId = fmt.Sprintf("row_%d", len(rows)) + } + rows = append(rows, row{ + Header: rowTitle, + Title: rowTitle, + Description: rowDesc, + ID: rowId, + }) + } + + section := listSection{ + Title: sectionTitle, + HighlightLabel: "", + Rows: rows, + } + + sections = append(sections, section) + } + + buttonText := data.ButtonText + if buttonText == "" { + buttonText = "Ver Menu" + } + + listData := list{ + Title: buttonText, + Sections: sections, + } + + jsonData, err := json.Marshal(listData) + if err != nil { + return "", err + } + + return string(jsonData), nil +} + +func (s *sendService) SendList(data *ListStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + // Legacy ListMessage format - works on iOS, Android and Web + // Matching PAPI Node.js default (non-modern) path exactly + + buttonText := data.ButtonText + if buttonText == "" { + buttonText = "Ver Menu" + } + + // Build sections in legacy ListMessage format + var sections []*waE2E.ListMessage_Section + for _, sec := range data.Sections { + sectionTitle := sec.Title + if sectionTitle == "" { + sectionTitle = " " + } + var rows []*waE2E.ListMessage_Row + for i, r := range sec.Rows { + rowTitle := r.Title + if rowTitle == "" { + rowTitle = " " + } + rowId := r.RowId + if rowId == "" { + rowId = fmt.Sprintf("row_%d_%d", i, len(rows)) + } + rows = append(rows, &waE2E.ListMessage_Row{ + Title: proto.String(rowTitle), + Description: proto.String(r.Description), + RowID: proto.String(rowId), + }) + } + sections = append(sections, &waE2E.ListMessage_Section{ + Title: proto.String(sectionTitle), + Rows: rows, + }) + } + + listType := waE2E.ListMessage_SINGLE_SELECT + listMessage := &waE2E.ListMessage{ + Title: proto.String(data.Title), + Description: proto.String(data.Description), + ButtonText: proto.String(buttonText), + FooterText: proto.String(data.FooterText), + ListType: &listType, + Sections: sections, + } + + // Wrap ListMessage in DocumentWithCaptionMessage (Baileys PR #36) so modern WhatsApp renders it. + // MessageSecret (32 random bytes) is required for iOS rendering. + listMsgSecret := make([]byte, 32) + _, _ = crypto_rand.Read(listMsgSecret) + + msg := &waE2E.Message{ + DocumentWithCaptionMessage: &waE2E.FutureProofMessage{ + Message: &waE2E.Message{ + ListMessage: listMessage, + }, + }, + MessageContextInfo: &waE2E.MessageContextInfo{ + MessageSecret: listMsgSecret, + }, + } + + // Build biz node — required for mobile rendering of modern lists. + listBizNodes := []waBinary.Node{ + { + Tag: "biz", + Content: []waBinary.Node{{ + Tag: "list", + Attrs: waBinary.Attrs{ + "v": "2", + "type": "single_select", + }, + }}, + }, + } + if !strings.Contains(data.Number, "@g.us") { + listBizNodes = append(listBizNodes, waBinary.Node{ + Tag: "bot", + Attrs: waBinary.Attrs{"biz_bot": "1"}, + }) + } + + message, err := s.SendMessage(instance, msg, "ListMessage", &SendDataStruct{ + Number: data.Number, + Delay: data.Delay, + MentionAll: data.MentionAll, + MentionedJID: data.MentionedJID, + FormatJid: data.FormatJid, + Quoted: data.Quoted, + AdditionalNodes: &listBizNodes, + }) + + if err != nil { + s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error sending list: %v", instance.Id, err) + return nil, err + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] List sent to %s", instance.Id, data.Number) + return message, nil +} + +func (s *sendService) SendMessage(instance *instance_model.Instance, msg *waE2E.Message, messageType string, data *SendDataStruct) (*MessageSendStruct, error) { + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] SendMessage called for number: %s, type: %s", instance.Id, data.Number, messageType) + + recipient, err := s.validateAndCheckUserExists(data.Number, data.FormatJid, &data.Quoted.MessageID, &data.Quoted.MessageID, instance) + if err != nil { + s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields or user check: %v", instance.Id, err) + return nil, err + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Recipient validated: %s (Server: %s)", instance.Id, recipient.String(), recipient.Server) + + var message string + if data.Id == "" { + message = s.clientPointer[instance.Id].GenerateMessageID() + } else { + message = data.Id + } + + if data.Delay > 0 { + media := "" + if messageType == "AudioMessage" { + media = "audio" + } + + err := s.clientPointer[instance.Id].SendChatPresence(context.Background(), recipient, types.ChatPresence("composing"), types.ChatPresenceMedia(media)) + if err != nil { + return nil, err + } + + time.Sleep(time.Duration(data.Delay) * time.Millisecond) + + err = s.clientPointer[instance.Id].SendChatPresence(context.Background(), recipient, types.ChatPresence("paused"), types.ChatPresenceMedia(media)) + if err != nil { + return nil, err + } + } + + isMedia := false + + if data.Quoted.MessageID != "" { + switch messageType { + case "ExtendedTextMessage": + msg.ExtendedTextMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + case "ImageMessage": + msg.ImageMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + isMedia = true + case "VideoMessage": + msg.VideoMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + isMedia = true + case "PtvMessage": + msg.PtvMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + isMedia = true + case "AudioMessage": + msg.AudioMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + isMedia = true + case "DocumentMessage": + if msg.DocumentMessage != nil { + msg.DocumentMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + } else if msg.DocumentWithCaptionMessage != nil { + msg.DocumentWithCaptionMessage.Message.DocumentMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + } + isMedia = true + case "PollCreationMessage": + msg.PollCreationMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + case "StickerMessage": + msg.StickerMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + isMedia = true + case "LocationMessage": + msg.LocationMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + case "ContactMessage": + msg.ContactMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + case "InteractiveMessage": + if msg.InteractiveMessage != nil { + msg.InteractiveMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + } else if msg.DocumentWithCaptionMessage != nil && + msg.DocumentWithCaptionMessage.Message != nil && + msg.DocumentWithCaptionMessage.Message.InteractiveMessage != nil { + msg.DocumentWithCaptionMessage.Message.InteractiveMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + } + case "ListMessage": + if msg.ListMessage != nil { + msg.ListMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + } else if msg.DocumentWithCaptionMessage != nil && + msg.DocumentWithCaptionMessage.Message != nil && + msg.DocumentWithCaptionMessage.Message.ListMessage != nil { + msg.DocumentWithCaptionMessage.Message.ListMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + } + case "ButtonsMessage": + if msg.ButtonsMessage != nil { + msg.ButtonsMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + } else if msg.DocumentWithCaptionMessage != nil && + msg.DocumentWithCaptionMessage.Message != nil && + msg.DocumentWithCaptionMessage.Message.ButtonsMessage != nil { + msg.DocumentWithCaptionMessage.Message.ButtonsMessage.ContextInfo = &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + } + } + default: + return nil, fmt.Errorf("invalid messageType: %s", messageType) + } + } else { + switch messageType { + case "ExtendedTextMessage": + msg.ExtendedTextMessage.ContextInfo = &waE2E.ContextInfo{} + case "ImageMessage": + msg.ImageMessage.ContextInfo = &waE2E.ContextInfo{} + isMedia = true + case "VideoMessage": + msg.VideoMessage.ContextInfo = &waE2E.ContextInfo{} + isMedia = true + case "PtvMessage": + msg.PtvMessage.ContextInfo = &waE2E.ContextInfo{} + isMedia = true + case "AudioMessage": + msg.AudioMessage.ContextInfo = &waE2E.ContextInfo{} + isMedia = true + case "DocumentMessage": + if msg.DocumentMessage != nil { + msg.DocumentMessage.ContextInfo = &waE2E.ContextInfo{} + } else if msg.DocumentWithCaptionMessage != nil { + msg.DocumentWithCaptionMessage.Message.DocumentMessage.ContextInfo = &waE2E.ContextInfo{} + } + isMedia = true + case "PollCreationMessage": + msg.PollCreationMessage.ContextInfo = &waE2E.ContextInfo{} + case "StickerMessage": + msg.StickerMessage.ContextInfo = &waE2E.ContextInfo{} + case "LocationMessage": + msg.LocationMessage.ContextInfo = &waE2E.ContextInfo{} + case "ContactMessage": + msg.ContactMessage.ContextInfo = &waE2E.ContextInfo{} + case "InteractiveMessage": + // ContextInfo already set in SendCarousel/SendButton/SendList + case "ListMessage": + // ContextInfo already set in SendList + case "ButtonsMessage": + // Reply-only buttons: ContextInfo already set in SendButton + default: + return nil, fmt.Errorf("invalid messageType: %s", messageType) + } + } + + // Apply ForwardingScore to whichever ContextInfo was set above. + // WhatsApp renders "Encaminhada" when ContextInfo.ForwardingScore > 0. + if data.ForwardingScore != nil && *data.ForwardingScore > 0 { + switch messageType { + case "ExtendedTextMessage": + if msg.ExtendedTextMessage != nil && msg.ExtendedTextMessage.ContextInfo != nil { + msg.ExtendedTextMessage.ContextInfo.ForwardingScore = data.ForwardingScore + msg.ExtendedTextMessage.ContextInfo.IsForwarded = proto.Bool(true) + } + case "ImageMessage": + if msg.ImageMessage != nil && msg.ImageMessage.ContextInfo != nil { + msg.ImageMessage.ContextInfo.ForwardingScore = data.ForwardingScore + msg.ImageMessage.ContextInfo.IsForwarded = proto.Bool(true) + } + case "VideoMessage": + if msg.VideoMessage != nil && msg.VideoMessage.ContextInfo != nil { + msg.VideoMessage.ContextInfo.ForwardingScore = data.ForwardingScore + msg.VideoMessage.ContextInfo.IsForwarded = proto.Bool(true) + } + case "PtvMessage": + if msg.PtvMessage != nil && msg.PtvMessage.ContextInfo != nil { + msg.PtvMessage.ContextInfo.ForwardingScore = data.ForwardingScore + msg.PtvMessage.ContextInfo.IsForwarded = proto.Bool(true) + } + case "AudioMessage": + if msg.AudioMessage != nil && msg.AudioMessage.ContextInfo != nil { + msg.AudioMessage.ContextInfo.ForwardingScore = data.ForwardingScore + msg.AudioMessage.ContextInfo.IsForwarded = proto.Bool(true) + } + case "DocumentMessage": + if msg.DocumentMessage != nil && msg.DocumentMessage.ContextInfo != nil { + msg.DocumentMessage.ContextInfo.ForwardingScore = data.ForwardingScore + msg.DocumentMessage.ContextInfo.IsForwarded = proto.Bool(true) + } else if msg.DocumentWithCaptionMessage != nil && msg.DocumentWithCaptionMessage.Message != nil && msg.DocumentWithCaptionMessage.Message.DocumentMessage != nil && msg.DocumentWithCaptionMessage.Message.DocumentMessage.ContextInfo != nil { + msg.DocumentWithCaptionMessage.Message.DocumentMessage.ContextInfo.ForwardingScore = data.ForwardingScore + msg.DocumentWithCaptionMessage.Message.DocumentMessage.ContextInfo.IsForwarded = proto.Bool(true) + } + case "PollCreationMessage": + if msg.PollCreationMessage != nil && msg.PollCreationMessage.ContextInfo != nil { + msg.PollCreationMessage.ContextInfo.ForwardingScore = data.ForwardingScore + msg.PollCreationMessage.ContextInfo.IsForwarded = proto.Bool(true) + } + case "StickerMessage": + if msg.StickerMessage != nil && msg.StickerMessage.ContextInfo != nil { + msg.StickerMessage.ContextInfo.ForwardingScore = data.ForwardingScore + msg.StickerMessage.ContextInfo.IsForwarded = proto.Bool(true) + } + case "LocationMessage": + if msg.LocationMessage != nil && msg.LocationMessage.ContextInfo != nil { + msg.LocationMessage.ContextInfo.ForwardingScore = data.ForwardingScore + msg.LocationMessage.ContextInfo.IsForwarded = proto.Bool(true) + } + case "ContactMessage": + if msg.ContactMessage != nil && msg.ContactMessage.ContextInfo != nil { + msg.ContactMessage.ContextInfo.ForwardingScore = data.ForwardingScore + msg.ContactMessage.ContextInfo.IsForwarded = proto.Bool(true) + } + case "InteractiveMessage": + if msg.InteractiveMessage != nil && msg.InteractiveMessage.ContextInfo != nil { + msg.InteractiveMessage.ContextInfo.ForwardingScore = data.ForwardingScore + msg.InteractiveMessage.ContextInfo.IsForwarded = proto.Bool(true) + } + case "ListMessage": + if msg.ListMessage != nil && msg.ListMessage.ContextInfo != nil { + msg.ListMessage.ContextInfo.ForwardingScore = data.ForwardingScore + msg.ListMessage.ContextInfo.IsForwarded = proto.Bool(true) + } + } + } + + isGroup := strings.Contains(data.Number, "@g.us") + isNewsletter := strings.Contains(data.Number, "@newsletter") + + // Only try to get participants for actual groups, not newsletters + if isGroup && !isNewsletter { + if data.MentionAll { + groupInfo, err := s.clientPointer[instance.Id].GetGroupInfo(context.Background(), recipient) + if err != nil { + return nil, err + } + + var mentionedJIDs []string + for _, participant := range groupInfo.Participants { + mentionedJIDs = append(mentionedJIDs, participant.JID.String()) + } + + switch messageType { + case "ExtendedTextMessage": + if msg.ExtendedTextMessage.ContextInfo == nil { + msg.ExtendedTextMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.ExtendedTextMessage.ContextInfo.MentionedJID = mentionedJIDs + case "ImageMessage": + if msg.ImageMessage.ContextInfo == nil { + msg.ImageMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.ImageMessage.ContextInfo.MentionedJID = mentionedJIDs + case "VideoMessage": + if msg.VideoMessage.ContextInfo == nil { + msg.VideoMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.VideoMessage.ContextInfo.MentionedJID = mentionedJIDs + case "PtvMessage": + if msg.PtvMessage.ContextInfo == nil { + msg.PtvMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.PtvMessage.ContextInfo.MentionedJID = mentionedJIDs + case "AudioMessage": + if msg.AudioMessage.ContextInfo == nil { + msg.AudioMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.AudioMessage.ContextInfo.MentionedJID = mentionedJIDs + case "DocumentMessage": + if msg.DocumentMessage.ContextInfo == nil { + msg.DocumentMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.DocumentMessage.ContextInfo.MentionedJID = mentionedJIDs + case "PollCreationMessage": + if msg.PollCreationMessage.ContextInfo == nil { + msg.PollCreationMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.PollCreationMessage.ContextInfo.MentionedJID = mentionedJIDs + case "StickerMessage": + if msg.StickerMessage.ContextInfo == nil { + msg.StickerMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.StickerMessage.ContextInfo.MentionedJID = mentionedJIDs + case "LocationMessage": + if msg.LocationMessage.ContextInfo == nil { + msg.LocationMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.LocationMessage.ContextInfo.MentionedJID = mentionedJIDs + case "ContactMessage": + if msg.ContactMessage.ContextInfo == nil { + msg.ContactMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.ContactMessage.ContextInfo.MentionedJID = mentionedJIDs + } + + } + + if len(data.MentionedJID) > 0 { + switch messageType { + case "ExtendedTextMessage": + if msg.ExtendedTextMessage.ContextInfo == nil { + msg.ExtendedTextMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.ExtendedTextMessage.ContextInfo.MentionedJID = data.MentionedJID + case "ImageMessage": + if msg.ImageMessage.ContextInfo == nil { + msg.ImageMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.ImageMessage.ContextInfo.MentionedJID = data.MentionedJID + case "VideoMessage": + if msg.VideoMessage.ContextInfo == nil { + msg.VideoMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.VideoMessage.ContextInfo.MentionedJID = data.MentionedJID + case "PtvMessage": + if msg.PtvMessage.ContextInfo == nil { + msg.PtvMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.PtvMessage.ContextInfo.MentionedJID = data.MentionedJID + case "AudioMessage": + if msg.AudioMessage.ContextInfo == nil { + msg.AudioMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.AudioMessage.ContextInfo.MentionedJID = data.MentionedJID + case "DocumentMessage": + if msg.DocumentMessage.ContextInfo == nil { + msg.DocumentMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.DocumentMessage.ContextInfo.MentionedJID = data.MentionedJID + case "PollCreationMessage": + if msg.PollCreationMessage.ContextInfo == nil { + msg.PollCreationMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.PollCreationMessage.ContextInfo.MentionedJID = data.MentionedJID + case "StickerMessage": + if msg.StickerMessage.ContextInfo == nil { + msg.StickerMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.StickerMessage.ContextInfo.MentionedJID = data.MentionedJID + case "LocationMessage": + if msg.LocationMessage.ContextInfo == nil { + msg.LocationMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.LocationMessage.ContextInfo.MentionedJID = data.MentionedJID + case "ContactMessage": + if msg.ContactMessage.ContextInfo == nil { + msg.ContactMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.ContactMessage.ContextInfo.MentionedJID = data.MentionedJID + } + } + } + + recipient.User = strings.ReplaceAll(recipient.User, "+", "") + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Sending message to %s with ID %s", instance.Id, recipient.String(), message) + + // Preparar extra parameters para o envio + sendExtra := whatsmeow.SendRequestExtra{ID: message} + + // Para newsletters/canais, adicionar o MediaHandle se houver mídia + if recipient.Server == "newsletter" && data.MediaHandle != "" { + sendExtra.MediaHandle = data.MediaHandle + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Newsletter detected, using MediaHandle: %s", instance.Id, data.MediaHandle) + } + + // Injetar nodes biz/bot customizados (PIX, botões interativos, etc.) no stanza XMPP. + if data.AdditionalNodes != nil { + sendExtra.AdditionalNodes = data.AdditionalNodes + } + + response, err := s.clientPointer[instance.Id].SendMessage(context.Background(), recipient, msg, sendExtra) + if err != nil { + s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error sending message: %v", instance.Id, err) + return nil, err + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Message sent successfully! ServerID: %d", instance.Id, response.ServerID) + + messageInfo := types.MessageInfo{ + MessageSource: types.MessageSource{ + Chat: recipient, + Sender: *s.clientPointer[instance.Id].Store.ID, + IsFromMe: true, + IsGroup: isGroup, + }, + ID: message, + Timestamp: time.Now(), + ServerID: response.ServerID, + Type: messageType, + } + + messageSent := &MessageSendStruct{ + Info: messageInfo, + Message: msg, + MessageContextInfo: &waE2E.ContextInfo{ + StanzaID: proto.String(data.Quoted.MessageID), + Participant: proto.String(data.Quoted.Participant), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + }, + } + + postMap := make(map[string]interface{}) + postMap["event"] = "SendMessage" + + // Convertendo o MessageSendStruct para map antes de atribuir + messageData := make(map[string]interface{}) + messageData["Info"] = messageSent.Info + + // Convertendo a mensagem para map usando json marshal/unmarshal + msgBytes, err := json.Marshal(messageSent.Message) + if err != nil { + return nil, fmt.Errorf("failed to marshal message: %v", err) + } + + var msgMap map[string]interface{} + if err := json.Unmarshal(msgBytes, &msgMap); err != nil { + return nil, fmt.Errorf("failed to unmarshal message: %v", err) + } + + messageData["Message"] = msgMap + messageData["MessageContextInfo"] = messageSent.MessageContextInfo + + postMap["data"] = messageData + + if isMedia && s.config.WebhookFiles { + var data []byte + var err error + + img := msg.GetImageMessage() + audio := msg.GetAudioMessage() + document := msg.GetDocumentMessage() + video := msg.GetVideoMessage() + sticker := msg.GetStickerMessage() + + if img != nil { + data, err = s.clientPointer[instance.Id].Download(context.Background(), img) + } else if audio != nil { + data, err = s.clientPointer[instance.Id].Download(context.Background(), audio) + } else if document != nil { + data, err = s.clientPointer[instance.Id].Download(context.Background(), document) + } else if video != nil { + data, err = s.clientPointer[instance.Id].Download(context.Background(), video) + } else if sticker != nil { + data, err = s.clientPointer[instance.Id].Download(context.Background(), sticker) + + webpReader := bytes.NewReader(data) + img, err := webp.Decode(webpReader) + if err == nil { + var pngBuffer bytes.Buffer + err = png.Encode(&pngBuffer, img) + if err == nil { + data = pngBuffer.Bytes() + } + } + } + + if err == nil { + // Acessando o Message do map já convertido + messageMap := msgMap + if messageMap == nil { + messageMap = make(map[string]interface{}) + } + + encodeData := base64.StdEncoding.EncodeToString(data) + messageMap["base64"] = encodeData + + messageData["Message"] = messageMap + } + } + + postMap["instanceToken"] = instance.Token + postMap["instanceId"] = instance.Id + postMap["instanceName"] = instance.Name + + var queueName string + + if _, ok := postMap["event"]; ok { + queueName = strings.ToLower(fmt.Sprintf("%s.%s", instance.Id, postMap["event"])) + } + + values, err := json.Marshal(postMap) + if err != nil { + s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to marshal JSON for queue", instance.Id) + return nil, err + } + + go s.whatsmeowService.CallWebhook(instance, queueName, values) + + if s.config.AmqpGlobalEnabled || s.config.NatsGlobalEnabled { + go s.whatsmeowService.SendToGlobalQueues(postMap["event"].(string), values, instance.Id) + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Message sent to %s", instance.Id, data.Number) + return messageSent, nil +} + +func (s *sendService) SendCarousel(data *CarouselStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + client, err := s.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + formatJid := true + if data.FormatJid != nil { + formatJid = *data.FormatJid + } + + var recipient types.JID + var ok bool + recipient, ok = utils.ParseJID(data.Number) + if !ok && formatJid { + s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) + return nil, errors.New("invalid phone number") + } else if !ok && !formatJid { + recipient = types.JID{ + User: data.Number, + Server: types.DefaultUserServer, + } + } + + // Build carousel cards + cards := make([]*waE2E.InteractiveMessage, len(data.Cards)) + messageVersion := int32(1) + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Building carousel for %s with %d cards", instance.Id, recipient.String(), len(data.Cards)) + + for i, card := range data.Cards { + // Each card MUST have both header and body for carousel to work + interactiveCard := &waE2E.InteractiveMessage{ + Body: &waE2E.InteractiveMessage_Body{ + Text: proto.String(card.Body.Text), + }, + Header: &waE2E.InteractiveMessage_Header{ + Title: proto.String(card.Header.Title), + Subtitle: proto.String(card.Header.Subtitle), + HasMediaAttachment: proto.Bool(false), + }, + } + + // Add media to header if URL provided + if card.Header.ImageUrl != "" || card.Header.VideoUrl != "" { + header := interactiveCard.Header + + if card.Header.ImageUrl != "" { + // Download image + resp, err := http.Get(card.Header.ImageUrl) + if err == nil { + defer resp.Body.Close() + fileData, err := io.ReadAll(resp.Body) + if err == nil { + uploaded, err := client.Upload(context.Background(), fileData, whatsmeow.MediaImage) + if err == nil { + // Generate JPEG thumbnail for iOS compatibility + jpegThumb := makeJPEGThumbnail(fileData, 72) + + header.HasMediaAttachment = proto.Bool(true) + header.Media = &waE2E.InteractiveMessage_Header_ImageMessage{ + ImageMessage: &waE2E.ImageMessage{ + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String("image/jpeg"), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + JPEGThumbnail: jpegThumb, + }, + } + } + } + } + } else if card.Header.VideoUrl != "" { + // Download and upload video + resp, err := http.Get(card.Header.VideoUrl) + if err == nil { + defer resp.Body.Close() + fileData, err := io.ReadAll(resp.Body) + if err == nil { + uploaded, err := client.Upload(context.Background(), fileData, whatsmeow.MediaVideo) + if err == nil { + header.HasMediaAttachment = proto.Bool(true) + header.Media = &waE2E.InteractiveMessage_Header_VideoMessage{ + VideoMessage: &waE2E.VideoMessage{ + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String("video/mp4"), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + }, + } + } + } + } + } + } + + // Add footer if exists + if card.Footer != "" { + interactiveCard.Footer = &waE2E.InteractiveMessage_Footer{ + Text: proto.String(card.Footer), + } + } + + // Add buttons if exist + if len(card.Buttons) > 0 { + buttons := make([]*waE2E.InteractiveMessage_NativeFlowMessage_NativeFlowButton, len(card.Buttons)) + for j, btn := range card.Buttons { + buttonType := strings.ToUpper(btn.Type) + if buttonType == "" { + buttonType = "REPLY" // Default type + } + + var buttonName string + var buttonParams string + + switch buttonType { + case "URL": + // URL button - opens a link + buttonName = "cta_url" + buttonParams = fmt.Sprintf(`{"display_text":"%s","url":"%s"}`, btn.DisplayText, btn.Id) + case "CALL": + // Call button - initiates a phone call + buttonName = "cta_call" + buttonParams = fmt.Sprintf(`{"display_text":"%s","phone_number":"%s"}`, btn.DisplayText, btn.Id) + case "COPY": + // Copy button - copies text to clipboard + buttonName = "cta_copy" + buttonParams = fmt.Sprintf(`{"display_text":"%s","copy_code":"%s"}`, btn.DisplayText, btn.CopyCode) + case "REPLY": + fallthrough + default: + // Quick reply button (default) + buttonName = "quick_reply" + buttonParams = fmt.Sprintf(`{"display_text":"%s","id":"%s"}`, btn.DisplayText, btn.Id) + } + + buttons[j] = &waE2E.InteractiveMessage_NativeFlowMessage_NativeFlowButton{ + Name: proto.String(buttonName), + ButtonParamsJSON: proto.String(buttonParams), + } + } + + // Cards in carousel: do NOT set MessageParamsJSON or MessageVersion + // (matching PAPI Node.js behavior for iOS compatibility) + interactiveCard.InteractiveMessage = &waE2E.InteractiveMessage_NativeFlowMessage_{ + NativeFlowMessage: &waE2E.InteractiveMessage_NativeFlowMessage{ + Buttons: buttons, + }, + } + } + + cards[i] = interactiveCard + } + + // Build carousel message (do NOT set CarouselCardType - matching PAPI Node.js for iOS) + interactiveMsg := &waE2E.InteractiveMessage{ + InteractiveMessage: &waE2E.InteractiveMessage_CarouselMessage_{ + CarouselMessage: &waE2E.InteractiveMessage_CarouselMessage{ + Cards: cards, + MessageVersion: &messageVersion, + }, + }, + } + + // Add body if provided (main message above carousel) + if data.Body != "" { + interactiveMsg.Body = &waE2E.InteractiveMessage_Body{ + Text: proto.String(data.Body), + } + } + + // Add footer if provided (text below carousel) + if data.Footer != "" { + interactiveMsg.Footer = &waE2E.InteractiveMessage_Footer{ + Text: proto.String(data.Footer), + } + } + + // ContextInfo is REQUIRED for iOS compatibility + // Even if empty, iOS requires this field to display carousel + contextInfo := &waE2E.ContextInfo{} + + // Add quoted message if exists + if data.Quoted.MessageID != "" { + contextInfo.StanzaID = proto.String(data.Quoted.MessageID) + if data.Quoted.Participant != "" { + participantJID, ok := utils.ParseJID(data.Quoted.Participant) + if ok { + contextInfo.Participant = proto.String(participantJID.String()) + } + } + } + + // Always set ContextInfo (required for iOS) + interactiveMsg.ContextInfo = contextInfo + + // Build final message with MessageContextInfo for proper notification delivery + msg := &waE2E.Message{ + InteractiveMessage: interactiveMsg, + MessageContextInfo: &waE2E.MessageContextInfo{ + DeviceListMetadata: &waE2E.DeviceListMetadata{}, + }, + } + + message, err := s.SendMessage(instance, msg, "InteractiveMessage", &SendDataStruct{ + Number: data.Number, + Delay: data.Delay, + }) + + if err != nil { + s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error sending carousel: %v", instance.Id, err) + return nil, err + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Carousel sent to %s with %d cards", instance.Id, data.Number, len(data.Cards)) + return message, nil +} + +func (s *sendService) SendStatusText(data *StatusTextStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + client, err := s.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + if data.Text == "" { + return nil, errors.New("text is required") + } + + msg := &waE2E.Message{ + ExtendedTextMessage: &waE2E.ExtendedTextMessage{ + Text: &data.Text, + }, + } + + messageID := data.Id + if messageID == "" { + messageID = client.GenerateMessageID() + } + + recipient := types.NewJID("status", "broadcast") + + response, err := client.SendMessage(context.Background(), recipient, msg, whatsmeow.SendRequestExtra{ID: messageID}) + if err != nil { + return nil, err + } + + messageInfo := types.MessageInfo{ + MessageSource: types.MessageSource{ + Chat: recipient, + Sender: *client.Store.ID, + IsFromMe: true, + IsGroup: false, + }, + ID: messageID, + Timestamp: time.Now(), + ServerID: response.ServerID, + Type: "StatusTextMessage", + } + + messageSent := &MessageSendStruct{ + Info: messageInfo, + Message: msg, + MessageContextInfo: &waE2E.ContextInfo{ + StanzaID: proto.String(""), + Participant: proto.String(""), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + }, + } + + s.sendStatusWebhook(messageSent, instance, "text") + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Status text sent successfully", instance.Id) + return messageSent, nil +} + +func (s *sendService) SendStatusMediaUrl(data *StatusMediaStruct, instance *instance_model.Instance) (*MessageSendStruct, error) { + client, err := s.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + if data.Url == "" { + return nil, errors.New("url is required") + } + if data.Type != "image" && data.Type != "video" { + return nil, errors.New("type must be 'image' or 'video'") + } + + req, err := http.NewRequest("GET", data.Url, nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", "agentdeck-whatsapp/1.0") + + httpClient := &http.Client{} + resp, err := httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to download file from URL: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return nil, fmt.Errorf("failed to download file: HTTP status %d", resp.StatusCode) + } + + fileData, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return s.sendStatusMedia(client, data, fileData, instance) +} + +func (s *sendService) SendStatusMediaFile(data *StatusMediaStruct, fileData []byte, instance *instance_model.Instance) (*MessageSendStruct, error) { + client, err := s.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + if data.Type != "image" && data.Type != "video" { + return nil, errors.New("type must be 'image' or 'video'") + } + + return s.sendStatusMedia(client, data, fileData, instance) +} + +func (s *sendService) sendStatusMedia(client *whatsmeow.Client, data *StatusMediaStruct, fileData []byte, instance *instance_model.Instance) (*MessageSendStruct, error) { + mime, _ := mimetype.DetectReader(bytes.NewReader(fileData)) + mimeType := mime.String() + + var uploadType whatsmeow.MediaType + switch data.Type { + case "image": + if mimeType != "image/jpeg" && mimeType != "image/png" && mimeType != "image/webp" { + return nil, fmt.Errorf("invalid file format: '%s'. Only 'image/jpeg', 'image/png' and 'image/webp' are accepted", mimeType) + } + if mimeType == "image/webp" { + mimeType = "image/jpeg" + } + uploadType = whatsmeow.MediaImage + case "video": + if mimeType != "video/mp4" { + return nil, fmt.Errorf("invalid file format: '%s'. Only 'video/mp4' is accepted", mimeType) + } + uploadType = whatsmeow.MediaVideo + default: + return nil, errors.New("invalid media type") + } + + uploaded, err := client.Upload(context.Background(), fileData, uploadType) + if err != nil { + return nil, err + } + + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Status media uploaded, size: %d", instance.Id, uploaded.FileLength) + + var media *waE2E.Message + var mediaType string + + switch data.Type { + case "image": + // Generate a JPEG preview thumbnail so status/story images render an + // inline preview instead of the gray camera placeholder. On failure + // jpegThumb is nil and the status is posted without a preview. + jpegThumb := makeJPEGThumbnail(fileData, 72) + media = &waE2E.Message{ImageMessage: &waE2E.ImageMessage{ + Caption: proto.String(data.Caption), + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimeType), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + JPEGThumbnail: jpegThumb, + }} + mediaType = "ImageMessage" + case "video": + media = &waE2E.Message{VideoMessage: &waE2E.VideoMessage{ + Caption: proto.String(data.Caption), + URL: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(mimeType), + FileEncSHA256: uploaded.FileEncSHA256, + FileSHA256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(fileData))), + }} + mediaType = "VideoMessage" + } + + messageID := data.Id + if messageID == "" { + messageID = client.GenerateMessageID() + } + + recipient := types.NewJID("status", "broadcast") + + response, err := client.SendMessage(context.Background(), recipient, media, whatsmeow.SendRequestExtra{ID: messageID}) + if err != nil { + return nil, err + } + + messageInfo := types.MessageInfo{ + MessageSource: types.MessageSource{ + Chat: recipient, + Sender: *client.Store.ID, + IsFromMe: true, + IsGroup: false, + }, + ID: messageID, + Timestamp: time.Now(), + ServerID: response.ServerID, + Type: mediaType, + } + + messageSent := &MessageSendStruct{ + Info: messageInfo, + Message: media, + MessageContextInfo: &waE2E.ContextInfo{ + StanzaID: proto.String(""), + Participant: proto.String(""), + QuotedMessage: &waE2E.Message{Conversation: proto.String("")}, + }, + } + + s.sendStatusWebhook(messageSent, instance, "media") + return messageSent, nil +} + +func (s *sendService) sendStatusWebhook(messageSent *MessageSendStruct, instance *instance_model.Instance, messageType string) { + postMap := make(map[string]interface{}) + postMap["event"] = "SendStatus" + messageData := make(map[string]interface{}) + messageData["Info"] = messageSent.Info + msgBytes, err := json.Marshal(messageSent.Message) + if err != nil { + s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to marshal status message: %v", instance.Id, err) + return + } + var msgMap map[string]interface{} + if err := json.Unmarshal(msgBytes, &msgMap); err != nil { + s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to unmarshal status message: %v", instance.Id, err) + return + } + messageData["Message"] = msgMap + messageData["MessageContextInfo"] = messageSent.MessageContextInfo + postMap["data"] = messageData + postMap["instanceToken"] = instance.Token + postMap["instanceId"] = instance.Id + postMap["instanceName"] = instance.Name + + values, err := json.Marshal(postMap) + if err != nil { + s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to marshal webhook payload: %v", instance.Id, err) + return + } + go s.whatsmeowService.CallWebhook(instance, "sendstatus", values) + if s.config.AmqpGlobalEnabled || s.config.NatsGlobalEnabled { + go s.whatsmeowService.SendToGlobalQueues("SendStatus", values, instance.Id) + } + s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Status %s sent successfully", instance.Id, messageType) +} + +func NewSendService( + clientPointer map[string]*whatsmeow.Client, + whatsmeowService whatsmeow_service.WhatsmeowService, + config *config.Config, + loggerWrapper *logger_wrapper.LoggerManager, +) SendService { + return &sendService{ + clientPointer: clientPointer, + whatsmeowService: whatsmeowService, + config: config, + loggerWrapper: loggerWrapper, + } +} diff --git a/whatsapp-service/pkg/server/handler/server_handler.go b/whatsapp-service/pkg/server/handler/server_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..d20bd731a6ca2c1f239385f3da59906e14bb0b9e --- /dev/null +++ b/whatsapp-service/pkg/server/handler/server_handler.go @@ -0,0 +1,21 @@ +package server_handler + +import "github.com/gin-gonic/gin" + +type ServerHandler interface { + ServerOk(ctx *gin.Context) +} + +type serverHandler struct { +} + +// ServerOk implements ServerHandler. +func (s *serverHandler) ServerOk(ctx *gin.Context) { + ctx.JSON(200, gin.H{ + "status": "ok", + }) +} + +func NewServerHandler() ServerHandler { + return &serverHandler{} +} diff --git a/whatsapp-service/pkg/storage/interfaces/media_storage.go b/whatsapp-service/pkg/storage/interfaces/media_storage.go new file mode 100644 index 0000000000000000000000000000000000000000..07b6a668e8e9665692087def3bf23e8a1bafb1d2 --- /dev/null +++ b/whatsapp-service/pkg/storage/interfaces/media_storage.go @@ -0,0 +1,15 @@ +package storage_interfaces + +import "context" + +// MediaStorage defines the contract for storing and retrieving media files +type MediaStorage interface { + // Store saves the media data and returns a public URL to access it + Store(ctx context.Context, data []byte, fileName string, contentType string) (string, error) + + // Delete removes the stored media + Delete(ctx context.Context, fileName string) error + + // GetURL returns the public URL for accessing the media + GetURL(ctx context.Context, fileName string) (string, error) +} diff --git a/whatsapp-service/pkg/storage/minio/media_storage.go b/whatsapp-service/pkg/storage/minio/media_storage.go new file mode 100644 index 0000000000000000000000000000000000000000..978d025e6137c04d3c043678e740654475ec22e8 --- /dev/null +++ b/whatsapp-service/pkg/storage/minio/media_storage.go @@ -0,0 +1,155 @@ +package minio_storage + +import ( + "bytes" + "context" + "fmt" + "net/url" + "strings" + "time" + + storage_interfaces "agentdeck-whatsapp-service/pkg/storage/interfaces" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +type MinioMediaStorage struct { + client *minio.Client + bucketName string + baseURL string +} + +func setBucketPolicy(client *minio.Client, bucketName string) error { + policy := `{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": ["s3:GetObject"], + "Resource": ["arn:aws:s3:::` + bucketName + `/*"] + } + ] + }` + + return client.SetBucketPolicy(context.Background(), bucketName, policy) +} + +// generateFilePath creates a simple media folder structure +// Format: agentdeck-whatsapp-medias/{filename} +func generateFilePath(fileName string) string { + return fmt.Sprintf("agentdeck-whatsapp-medias/%s", fileName) +} + +// resolveFilePath determines if the input is a full path or just a filename +// If it's just a filename, it assumes it's in the agentdeck-whatsapp-medias folder +// If it's a full path, it returns it as-is +func (m *MinioMediaStorage) resolveFilePath(ctx context.Context, fileNameOrPath string) (string, error) { + // If the input already contains path separators, assume it's a full path + if strings.Contains(fileNameOrPath, "/") { + return fileNameOrPath, nil + } + + // If it's just a filename, assume it's in the agentdeck-whatsapp-medias folder + return fmt.Sprintf("agentdeck-whatsapp-medias/%s", fileNameOrPath), nil +} + +func NewMinioMediaStorage( + endpoint, + accessKeyID, + secretAccessKey, + bucketName, + region string, + useSSL bool, +) (storage_interfaces.MediaStorage, error) { + client, err := minio.New(endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""), + Secure: useSSL, + Region: region, + }) + if err != nil { + return nil, fmt.Errorf("failed to create MinIO client: %w", err) + } + + // Try to set bucket policy to allow public access (optional for some providers) + err = setBucketPolicy(client, bucketName) + if err != nil { + // Some providers (like Backblaze B2) don't support SetBucketPolicy + // Log warning but continue - files can still be accessed via presigned URLs + fmt.Printf("Warning: Failed to set bucket policy (provider may not support it): %v\n", err) + } + + baseURL := fmt.Sprintf("https://%s/%s", endpoint, bucketName) + if !useSSL { + baseURL = fmt.Sprintf("http://%s/%s", endpoint, bucketName) + } + + return &MinioMediaStorage{ + client: client, + bucketName: bucketName, + baseURL: baseURL, + }, nil +} + +func (m *MinioMediaStorage) Store(ctx context.Context, data []byte, fileName string, contentType string) (string, error) { + // Generate organized file path + filePath := generateFilePath(fileName) + reader := bytes.NewReader(data) + + _, err := m.client.PutObject(ctx, m.bucketName, filePath, reader, int64(len(data)), minio.PutObjectOptions{ + ContentType: contentType, + }) + if err != nil { + return "", fmt.Errorf("failed to store object: %w", err) + } + + // Gerando URL assinada com validade de 7 dias + reqParams := make(url.Values) + presignedURL, err := m.client.PresignedGetObject(ctx, m.bucketName, filePath, time.Hour*24*7, reqParams) + if err != nil { + return "", fmt.Errorf("failed to generate presigned URL: %w", err) + } + + fmt.Println(presignedURL.String()) + + return presignedURL.String(), nil +} + +func (m *MinioMediaStorage) Delete(ctx context.Context, fileName string) error { + // Resolve the full path for the file + filePath, err := m.resolveFilePath(ctx, fileName) + if err != nil { + return fmt.Errorf("failed to resolve file path: %w", err) + } + + err = m.client.RemoveObject(ctx, m.bucketName, filePath, minio.RemoveObjectOptions{}) + if err != nil { + return fmt.Errorf("failed to delete object: %w", err) + } + return nil +} + +func (m *MinioMediaStorage) GetURL(ctx context.Context, fileName string) (string, error) { + // Resolve the full path for the file + filePath, err := m.resolveFilePath(ctx, fileName) + if err != nil { + return "", fmt.Errorf("failed to resolve file path: %w", err) + } + + // Check if object exists + _, err = m.client.StatObject(ctx, m.bucketName, filePath, minio.StatObjectOptions{}) + if err != nil { + return "", fmt.Errorf("failed to get object stats: %w", err) + } + + // Gerando URL assinada com validade de 7 dias + reqParams := make(url.Values) + presignedURL, err := m.client.PresignedGetObject(ctx, m.bucketName, filePath, time.Hour*24*7, reqParams) + if err != nil { + return "", fmt.Errorf("failed to generate presigned URL: %w", err) + } + + fmt.Println(presignedURL.String()) + + return presignedURL.String(), nil +} diff --git a/whatsapp-service/pkg/supabase/client.go b/whatsapp-service/pkg/supabase/client.go new file mode 100644 index 0000000000000000000000000000000000000000..cca1aa4f622fe1940522cea473b4e215b0177e2d --- /dev/null +++ b/whatsapp-service/pkg/supabase/client.go @@ -0,0 +1,230 @@ +// Package supabase is a client for the Supabase PostgREST API. +// It talks to the /rest/v1 endpoint using the service-role key, which bypasses +// row level security so the backend can read/write the app tables. +package supabase + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +// Client wraps the PostgREST endpoint exposed by Supabase. +type Client struct { + baseURL string // e.g. https://.supabase.co/rest/v1 + serviceKey string + http *http.Client +} + +// New builds a PostgREST client from the Supabase project URL and service role key. +func New(supabaseURL, serviceKey string) *Client { + base := strings.TrimSuffix(supabaseURL, "/") + if !strings.HasSuffix(base, "/rest/v1") { + base += "/rest/v1" + } + return &Client{ + baseURL: base, + serviceKey: serviceKey, + http: &http.Client{Timeout: 30 * time.Second}, + } +} + +func (c *Client) do(ctx context.Context, method, path string, body interface{}, prefer string, out interface{}) error { + var reader io.Reader + if body != nil { + b, err := json.Marshal(body) + if err != nil { + return err + } + reader = bytes.NewReader(b) + } + + req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, reader) + if err != nil { + return err + } + req.Header.Set("apikey", c.serviceKey) + req.Header.Set("Authorization", "Bearer "+c.serviceKey) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + if prefer != "" { + req.Header.Set("Prefer", prefer) + } + + resp, err := c.http.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return &APIError{Status: resp.StatusCode, Body: string(data), Table: tableFromPath(path)} + } + + if out != nil && len(data) > 0 { + if err := json.Unmarshal(data, out); err != nil { + return fmt.Errorf("supabase: failed to decode response for %s: %v", path, err) + } + } + return nil +} + +// Query builds PostgREST query parameters (e.g. ?id=eq.xxx&select=...). +type Query struct { + values url.Values +} + +// NewQuery creates an empty query builder. +func NewQuery() *Query { + return &Query{values: url.Values{}} +} + +// Eq adds an equality filter: column=eq.value. +func (q *Query) Eq(column, value string) *Query { + q.values.Set(column, "eq."+value) + return q +} + +// Neq adds a not-equal filter: column=neq.value. +func (q *Query) Neq(column, value string) *Query { + q.values.Set(column, "neq."+value) + return q +} + +// Gte adds a greater-than-or-equal filter. +func (q *Query) Gte(column, value string) *Query { + q.values.Set(column, "gte."+value) + return q +} + +// Select restricts returned columns. +func (q *Query) Select(cols ...string) *Query { + q.values.Set("select", strings.Join(cols, ",")) + return q +} + +// Order changes the result ordering. +func (q *Query) Order(column string, desc bool) *Query { + dir := "asc" + if desc { + dir = "desc" + } + q.values.Set("order", column+"."+dir) + return q +} + +// Limit caps the number of returned rows. +func (q *Query) Limit(n int) *Query { + q.values.Set("limit", fmt.Sprintf("%d", n)) + return q +} + +func (q *Query) Encode() string { return q.values.Encode() } + +// Table offers CRUD operations against a single table. +type Table struct { + c *Client + name string +} + +// Table returns a Table handle for the given table name. +func (c *Client) Table(name string) *Table { + return &Table{c: c, name: name} +} + +// SelectOne fetches a single row. It returns (false, nil) if no row matches. +func (t *Table) SelectOne(ctx context.Context, query *Query, out interface{}) (bool, error) { + var rows []json.RawMessage + effective := NewQuery() + for k, vals := range query.values { + for _, v := range vals { + effective.values.Add(k, v) + } + } + effective.values.Set("limit", "1") + if err := t.c.do(ctx, http.MethodGet, "/"+t.name+"?"+effective.Encode(), nil, "", &rows); err != nil { + return false, err + } + if len(rows) == 0 { + return false, nil + } + if err := json.Unmarshal(rows[0], out); err != nil { + return false, err + } + return true, nil +} + +// Select reads all matching rows into out (a slice pointer). +func (t *Table) Select(ctx context.Context, query *Query, out interface{}) error { + return t.c.do(ctx, http.MethodGet, "/"+t.name+"?"+query.Encode(), nil, "", out) +} + +// Insert inserts one or more rows (out receives inserted rows). +func (t *Table) Insert(ctx context.Context, body interface{}, prefer string, out interface{}) error { + return t.c.do(ctx, http.MethodPost, "/"+t.name, body, prefer, out) +} + +// Upsert inserts rows, updating conflicting columns. conflictCols are the unique keyed +// columns used by PostgREST's on_conflict resolution. PostgREST expects +// on_conflict as a query parameter and resolution in the Prefer header: +// +// POST /table?on_conflict=col1,col2 +// Prefer: resolution=merge-duplicates +func (t *Table) Upsert(ctx context.Context, body interface{}, conflictCols []string, out interface{}) error { + path := "/" + t.name + prefer := "resolution=ignore-duplicates" + if len(conflictCols) > 0 { + prefer = "resolution=merge-duplicates" + path += "?on_conflict=" + url.QueryEscape(strings.Join(conflictCols, ",")) + } + return t.c.do(ctx, http.MethodPost, path, body, prefer, out) +} + +// Update patches rows matching the query. +func (t *Table) Update(ctx context.Context, query *Query, body interface{}) error { + return t.c.do(ctx, http.MethodPatch, "/"+t.name+"?"+query.Encode(), body, "return=representation", nil) +} + +// Delete removes rows matching the query. +func (t *Table) Delete(ctx context.Context, query *Query) error { + return t.c.do(ctx, http.MethodDelete, "/"+t.name+"?"+query.Encode(), nil, "", nil) +} + +// RPC calls a stored function. +func (c *Client) RPC(ctx context.Context, fn string, body, out interface{}) error { + return c.do(ctx, http.MethodPost, "/rpc/"+fn, body, "", out) +} + +// APIError represents a non-2xx PostgREST response. +type APIError struct { + Status int + Body string + Table string +} + +func (e *APIError) Error() string { + msg := e.Body + if msg == "" { + msg = http.StatusText(e.Status) + } + return fmt.Sprintf("supabase: %s: HTTP %d: %s", e.Table, e.Status, strings.TrimSpace(msg)) +} + +func tableFromPath(path string) string { + seg := strings.Split(strings.Trim(path, "/"), "/") + if len(seg) > 0 { + return seg[0] + } + return path +} \ No newline at end of file diff --git a/whatsapp-service/pkg/telemetry/telemetry.go b/whatsapp-service/pkg/telemetry/telemetry.go new file mode 100644 index 0000000000000000000000000000000000000000..63d64dd94b29791b7a62f5d97a8ff91633d1d8b7 --- /dev/null +++ b/whatsapp-service/pkg/telemetry/telemetry.go @@ -0,0 +1,62 @@ +package telemetry + +import ( + "bytes" + "encoding/json" + "log" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +type TelemetryData struct { + Route string `json:"route"` + APIVersion string `json:"apiVersion"` + Timestamp time.Time `json:"timestamp"` +} + +type telemetryService struct{} + +func (t *telemetryService) TelemetryMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + route := c.FullPath() + go SendTelemetry(route) + c.Next() + } +} + +type TelemetryService interface { + TelemetryMiddleware() gin.HandlerFunc +} + +func SendTelemetry(route string) { + if route == "/" { + return + } + + telemetry := TelemetryData{ + Route: route, + APIVersion: "agentdeck", + Timestamp: time.Now(), + } + + url := "https://telemetry.agentdeck.ai/telemetry" + + data, err := json.Marshal(telemetry) + if err != nil { + log.Println("Erro ao serializar telemetria:", err) + return + } + + resp, err := http.Post(url, "application/json", bytes.NewBuffer(data)) + if err != nil { + log.Println("Erro ao enviar telemetria:", err) + return + } + defer resp.Body.Close() +} + +func NewTelemetryService() TelemetryService { + return &telemetryService{} +} diff --git a/whatsapp-service/pkg/user/handler/user_handler.go b/whatsapp-service/pkg/user/handler/user_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..2a7e5ca771d26716461169b329fe9bfde0e7229a --- /dev/null +++ b/whatsapp-service/pkg/user/handler/user_handler.go @@ -0,0 +1,551 @@ +package user_handler + +import ( + "net/http" + + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + user_service "agentdeck-whatsapp-service/pkg/user/service" + "github.com/gin-gonic/gin" +) + +type UserHandler interface { + GetUser(ctx *gin.Context) + CheckUser(ctx *gin.Context) + GetAvatar(ctx *gin.Context) + GetContacts(ctx *gin.Context) + GetPrivacy(ctx *gin.Context) + SetPrivacy(ctx *gin.Context) + BlockContact(ctx *gin.Context) + UnblockContact(ctx *gin.Context) + GetBlockList(ctx *gin.Context) + SetProfilePicture(ctx *gin.Context) + SetProfileName(ctx *gin.Context) + SetProfileStatus(ctx *gin.Context) +} + +type userHandler struct { + userService user_service.UserService +} + +// Get a user +// @Summary Get a user +// @Description Get a user +// @Tags User +// @Accept json +// @Produce json +// @Param message body user_service.CheckUserStruct true "User data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /user/info [post] +func (u *userHandler) GetUser(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *user_service.CheckUserStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if len(data.Number) < 1 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + uc, err := u.userService.GetUser(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": uc}) +} + +// Check a user +// @Summary Check a user +// @Description Check a user +// @Tags User +// @Accept json +// @Produce json +// @Param message body user_service.CheckUserStruct true "User data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /user/check [post] +func (u *userHandler) CheckUser(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *user_service.CheckUserStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if len(data.Number) < 1 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + uc, err := u.userService.CheckUser(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": uc}) +} + +// Get a user's avatar +// @Summary Get a user's avatar +// @Description Get a user's avatar +// @Tags User +// @Accept json +// @Produce json +// @Param message body user_service.GetAvatarStruct true "Avatar data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /user/avatar [post] +func (u *userHandler) GetAvatar(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *user_service.GetAvatarStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if len(data.Number) < 1 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + pic, err := u.userService.GetAvatar(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": pic}) +} + +// Get a user's contacts +// @Summary Get a user's contacts +// @Description Get a user's contacts +// @Tags User +// @Accept json +// @Produce json +// @Success 200 {object} gin.H "success" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /user/contacts [get] +func (u *userHandler) GetContacts(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + contacts, err := u.userService.GetContacts(instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": contacts}) +} + +// Get a user's privacy settings +// @Summary Get a user's privacy settings +// @Description Get a user's privacy settings +// @Tags User +// @Accept json +// @Produce json +// @Success 200 {object} gin.H "success" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /user/privacy [get] +func (u *userHandler) GetPrivacy(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + privacy, err := u.userService.GetPrivacy(instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": privacy}) +} + +// Set a user's privacy settings +// @Summary Set a user's privacy settings +// @Description Set a user's privacy settings +// @Tags User +// @Accept json +// @Produce json +// @Param message body user_service.PrivacyStruct true "Privacy data" +// @Success 200 {object} gin.H "success" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /user/privacy [post] +func (u *userHandler) SetPrivacy(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *user_service.PrivacyStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.CallAdd == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "call add is required"}) + return + } + + if data.GroupAdd == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "group add is required"}) + return + } + + if data.LastSeen == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "last seen is required"}) + return + } + + if data.Online == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "online is required"}) + return + } + + if data.Profile == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "profile is required"}) + return + } + + if data.ReadReceipts == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "read receipts is required"}) + return + } + + if data.Status == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "status is required"}) + return + } + + privacy, err := u.userService.SetPrivacy(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": privacy}) +} + +// Block a contact +// @Summary Block a contact +// @Description Block a contact +// @Tags User +// @Accept json +// @Produce json +// @Param message body user_service.BlockStruct true "Block data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /user/block [post] +func (u *userHandler) BlockContact(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *user_service.BlockStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if len(data.Number) < 1 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + resp, err := u.userService.BlockContact(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp}) +} + +// Unblock a contact +// @Summary Unblock a contact +// @Description Unblock a contact +// @Tags User +// @Accept json +// @Produce json +// @Param message body user_service.BlockStruct true "Block data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /user/unblock [post] +func (u *userHandler) UnblockContact(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *user_service.BlockStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if len(data.Number) < 1 { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + if data.Number == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "phone number is required"}) + return + } + + resp, err := u.userService.UnlockContact(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp}) +} + +// Get a user's block list +// @Summary Get a user's block list +// @Description Get a user's block list +// @Tags User +// @Accept json +// @Produce json +// @Success 200 {object} gin.H "success" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /user/blocklist [get] +func (u *userHandler) GetBlockList(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + resp, err := u.userService.GetBlockList(instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": resp}) +} + +// Set a user's profile picture +// @Summary Set a user's profile picture +// @Description Set a user's profile picture +// @Tags User +// @Accept json +// @Produce json +// @Param message body user_service.SetProfilePictureStruct true "Profile picture data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /user/profilePicture [post] +func (u *userHandler) SetProfilePicture(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *user_service.SetProfilePictureStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Image == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "image is required"}) + return + } + + resp, err := u.userService.SetProfilePicture(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if !resp { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to set profile picture"}) + return + } + + responseData := gin.H{"image": data.Image} + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// Set a user's profile name +// @Summary Set a user's profile name +// @Description Set a user's profile name +// @Tags User +// @Accept json +// @Produce json +// @Param message body user_service.SetProfilePictureStruct true "Profile name data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /user/profileName [post] +func (u *userHandler) SetProfileName(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *user_service.SetProfileNameStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Name == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) + return + } + + resp, err := u.userService.SetProfileName(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if !resp { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to set profile picture"}) + return + } + + responseData := gin.H{"name": data.Name} + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +// Set a user's profile status +// @Summary Set a user's profile status +// @Description Set a user's profile status +// @Tags User +// @Accept json +// @Produce json +// @Param message body user_service.SetProfilePictureStruct true "Profile status data" +// @Success 200 {object} gin.H "success" +// @Failure 400 {object} gin.H "Error on validation" +// @Failure 500 {object} gin.H "Internal server error" +// @Router /user/profileStatus [post] +func (u *userHandler) SetProfileStatus(ctx *gin.Context) { + getInstance := ctx.MustGet("instance") + + instance, ok := getInstance.(*instance_model.Instance) + if !ok { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"}) + return + } + + var data *user_service.SetProfileStatusStruct + err := ctx.ShouldBindBodyWithJSON(&data) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if data.Status == "" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) + return + } + + resp, err := u.userService.SetProfileStatus(data, instance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if !resp { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to set profile picture"}) + return + } + + responseData := gin.H{"status": data.Status} + + ctx.JSON(http.StatusOK, gin.H{"message": "success", "data": responseData}) +} + +func NewUserHandler( + userService user_service.UserService, +) UserHandler { + return &userHandler{ + userService: userService, + } +} diff --git a/whatsapp-service/pkg/user/service/user_service.go b/whatsapp-service/pkg/user/service/user_service.go new file mode 100644 index 0000000000000000000000000000000000000000..f2269ad6005b1b7a1cb0b95efae76e398cdccc62 --- /dev/null +++ b/whatsapp-service/pkg/user/service/user_service.go @@ -0,0 +1,553 @@ +package user_service + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "time" + + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + "agentdeck-whatsapp-service/pkg/utils" + whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/types" + "go.mau.fi/whatsmeow/types/events" +) + +type UserService interface { + GetUser(data *CheckUserStruct, instance *instance_model.Instance) (*UserCollection, error) + CheckUser(data *CheckUserStruct, instance *instance_model.Instance) (*CheckUserCollection, error) + GetAvatar(data *GetAvatarStruct, instance *instance_model.Instance) (*types.ProfilePictureInfo, error) + GetContacts(instance *instance_model.Instance) ([]ContactInfo, error) + GetPrivacy(instance *instance_model.Instance) (types.PrivacySettings, error) + SetPrivacy(data *PrivacyStruct, instance *instance_model.Instance) (*types.PrivacySettings, error) + BlockContact(data *BlockStruct, instance *instance_model.Instance) (*types.Blocklist, error) + UnlockContact(data *BlockStruct, instance *instance_model.Instance) (*types.Blocklist, error) + GetBlockList(instance *instance_model.Instance) (*types.Blocklist, error) + SetProfilePicture(data *SetProfilePictureStruct, instance *instance_model.Instance) (bool, error) + SetProfileName(data *SetProfileNameStruct, instance *instance_model.Instance) (bool, error) + SetProfileStatus(data *SetProfileStatusStruct, instance *instance_model.Instance) (bool, error) +} + +type userService struct { + clientPointer map[string]*whatsmeow.Client + whatsmeowService whatsmeow_service.WhatsmeowService + loggerWrapper *logger_wrapper.LoggerManager +} + +type ContactInfo struct { + Jid string `json:"Jid"` + Found bool `json:"Found"` + FirstName string `json:"FirstName"` + FullName string `json:"FullName"` + PushName string `json:"PushName"` + BusinessName string `json:"BusinessName"` +} + +type UserInfo struct { + VerifiedName *types.VerifiedName + Status string + PictureID string + Devices []types.JID + LID *string // The local ID (if available) +} + +type UserCollection struct { + Users map[types.JID]UserInfo +} + +type User struct { + Query string + IsInWhatsapp bool + JID string + RemoteJID string + LID *string + VerifiedName string +} + +type CheckUserCollection struct { + Users []User +} + +type CheckUserStruct struct { + Number []string `json:"number"` + FormatJid *bool `json:"formatJid,omitempty"` +} + +type GetAvatarStruct struct { + Number string `json:"number"` + Preview bool `json:"preview"` +} + +type BlockStruct struct { + Number string `json:"number"` +} + +type SetProfilePictureStruct struct { + Image string `json:"image"` +} + +type SetProfileNameStruct struct { + Name string `json:"name"` +} + +type SetProfileStatusStruct struct { + Status string `json:"status"` +} + +type PrivacyStruct struct { + GroupAdd types.PrivacySetting `json:"groupAdd"` + LastSeen types.PrivacySetting `json:"lastSeen"` + Status types.PrivacySetting `json:"status"` + Profile types.PrivacySetting `json:"profile"` + ReadReceipts types.PrivacySetting `json:"readReceipts"` + CallAdd types.PrivacySetting `json:"callAdd"` + Online types.PrivacySetting `json:"online"` +} + +func (u *userService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + client := u.clientPointer[instanceId] + u.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) + + if client == nil { + u.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] No client found, attempting to start new instance", instanceId) + err := u.whatsmeowService.StartInstance(instanceId) + if err != nil { + u.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to start instance: %v", instanceId, err) + return nil, errors.New("no active session found") + } + + u.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) + time.Sleep(2 * time.Second) + + client = u.clientPointer[instanceId] + u.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + + if client == nil || !client.IsConnected() { + u.loggerWrapper.GetLogger(instanceId).LogError("[%s] New client validation failed - Exists: %v, Connected: %v", + instanceId, + client != nil, + client != nil && client.IsConnected()) + return nil, errors.New("no active session found") + } + } else if !client.IsConnected() { + u.loggerWrapper.GetLogger(instanceId).LogError("[%s] Existing client is disconnected - Connected status: %v", + instanceId, + client.IsConnected()) + return nil, errors.New("client disconnected") + } + + u.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected()) + return client, nil +} + +func (u *userService) GetUser(data *CheckUserStruct, instance *instance_model.Instance) (*UserCollection, error) { + client, err := u.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + var jids []types.JID + for _, arg := range data.Number { + jid, ok := utils.ParseJID(arg) + if !ok { + return nil, errors.New("invalid phone number") + } + jids = append(jids, jid) + } + resp, err := client.GetUserInfo(context.Background(), jids) + if err != nil { + return nil, err + } + + uc := new(UserCollection) + uc.Users = make(map[types.JID]UserInfo) + + for jid, whatsmeowInfo := range resp { + // Consultar LID Store para obter LID associado ao JID + var lidStr *string + if client.Store.LIDs != nil { + if lid, err := client.Store.LIDs.GetLIDForPN(context.TODO(), jid); err == nil && !lid.IsEmpty() { + lidString := fmt.Sprintf("%v", lid) + lidStr = &lidString + } + } + + // Converter para nossa estrutura UserInfo que inclui LID + info := UserInfo{ + VerifiedName: whatsmeowInfo.VerifiedName, + Status: whatsmeowInfo.Status, + PictureID: whatsmeowInfo.PictureID, + Devices: whatsmeowInfo.Devices, + LID: lidStr, + } + uc.Users[jid] = info + } + + return uc, nil +} + +func (u *userService) CheckUser(data *CheckUserStruct, instance *instance_model.Instance) (*CheckUserCollection, error) { + client, err := u.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + // Set formatJid to false by default for CheckUser + formatJid := false + if data.FormatJid != nil { + formatJid = *data.FormatJid + } + + // First attempt with the requested formatJid setting + uc, shouldRetry := u.performCheckUser(client, data.Number, formatJid, instance.Id) + if !shouldRetry { + return uc, nil + } + + // If formatJid was true and we got false results, retry with formatJid=false + if formatJid { + u.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Some users not found with formatJid=true, retrying with formatJid=false", instance.Id) + ucRetry, _ := u.performCheckUser(client, data.Number, false, instance.Id) + + // Merge results: use retry results for users that weren't found in first attempt + return u.mergeCheckUserResults(uc, ucRetry), nil + } + + return uc, nil +} + +// performCheckUser executes the actual user check with specified formatJid +func (u *userService) performCheckUser(client *whatsmeow.Client, numbers []string, formatJid bool, instanceId string) (*CheckUserCollection, bool) { + // Use centralized function to prepare numbers for WhatsApp check + phoneNumbers, err := utils.PrepareNumbersForWhatsAppCheck(numbers, &formatJid) + if err != nil { + u.loggerWrapper.GetLogger(instanceId).LogWarn("[%s] Failed to prepare numbers for WhatsApp check: %v", instanceId, err) + return nil, false + } + + resp, err := client.IsOnWhatsApp(context.Background(), phoneNumbers) + if err != nil { + u.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to check users on WhatsApp: %v", instanceId, err) + return nil, false + } + + uc := new(CheckUserCollection) + shouldRetry := false + + for _, item := range resp { + // Consultar LID Store para obter LID associado ao JID + var lidStr *string + if client.Store.LIDs != nil { + if lid, err := client.Store.LIDs.GetLIDForPN(context.TODO(), item.JID); err == nil && !lid.IsEmpty() { + lidString := fmt.Sprintf("%v", lid) + lidStr = &lidString + } + } + + // Determine the RemoteJID to use for messaging + remoteJID := item.Query // Default to original query + if item.IsIn { + // When user exists on WhatsApp, use the JID returned by WhatsApp + remoteJID = fmt.Sprintf("%v", item.JID) + } else if formatJid { + // If user not found and we used formatJid=true, we should retry with formatJid=false + shouldRetry = true + } + + if item.VerifiedName != nil { + var msg = User{ + Query: item.Query, + IsInWhatsapp: item.IsIn, + JID: fmt.Sprintf("%v", item.JID), + RemoteJID: remoteJID, + LID: lidStr, + VerifiedName: item.VerifiedName.Details.GetVerifiedName(), + } + uc.Users = append(uc.Users, msg) + } else { + var msg = User{ + Query: item.Query, + IsInWhatsapp: item.IsIn, + JID: fmt.Sprintf("%v", item.JID), + RemoteJID: remoteJID, + LID: lidStr, + VerifiedName: "", + } + uc.Users = append(uc.Users, msg) + } + } + + return uc, shouldRetry +} + +// mergeCheckUserResults merges results from two CheckUser attempts +// Priority: if a user is found in retry (formatJid=false), use that result +func (u *userService) mergeCheckUserResults(original, retry *CheckUserCollection) *CheckUserCollection { + if retry == nil { + return original + } + + // Create a map of retry results by original query for quick lookup + retryMap := make(map[string]User) + for _, user := range retry.Users { + retryMap[user.Query] = user + } + + // Merge results + merged := &CheckUserCollection{} + for _, originalUser := range original.Users { + if retryUser, exists := retryMap[originalUser.Query]; exists && retryUser.IsInWhatsapp && !originalUser.IsInWhatsapp { + // Use retry result if it found the user and original didn't + merged.Users = append(merged.Users, retryUser) + } else { + // Use original result + merged.Users = append(merged.Users, originalUser) + } + } + + return merged +} + +func (u *userService) GetAvatar(data *GetAvatarStruct, instance *instance_model.Instance) (*types.ProfilePictureInfo, error) { + client, err := u.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + // 🔒 FIX: Verificar se o cliente está conectado antes de fazer a requisição + if !client.IsConnected() { + return nil, errors.New("client is not connected to WhatsApp") + } + + // 🔒 FIX: Verificar se o cliente está autenticado + if !client.IsLoggedIn() { + return nil, errors.New("client is not logged in to WhatsApp") + } + + jid, ok := utils.ParseJID(data.Number) + if !ok { + return nil, errors.New("invalid phone number") + } + + u.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Requesting avatar for JID: %s, Preview: %v", instance.Id, jid, data.Preview) + + var pic *types.ProfilePictureInfo + + // 🔒 FIX: Adicionar timeout ao contexto para evitar que a requisição trave indefinidamente + // Usar timeout maior que o padrão do sendIQ (75s) para dar tempo suficiente + ctx, cancel := context.WithTimeout(context.Background(), 80*time.Second) + defer cancel() + + u.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Starting GetProfilePictureInfo request...", instance.Id) + pic, err = client.GetProfilePictureInfo(ctx, jid, &whatsmeow.GetProfilePictureParams{ + Preview: data.Preview, + }) + if err != nil { + u.loggerWrapper.GetLogger(instance.Id).LogError("[%s] GetProfilePictureInfo failed: %v", instance.Id, err) + return nil, err + } + + if pic == nil { + return nil, errors.New("no profile picture found") + } + + u.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Got avatar %s", instance.Id, pic.URL) + + return pic, nil +} + +func (u *userService) GetContacts(instance *instance_model.Instance) ([]ContactInfo, error) { + client, err := u.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + contacts, err := client.Store.Contacts.GetAllContacts(context.Background()) + if err != nil { + return nil, err + } + + var contactsArray []ContactInfo + + for jid, contact := range contacts { + contactsArray = append(contactsArray, ContactInfo{ + Jid: jid.String(), + Found: contact.Found, + FirstName: contact.FirstName, + FullName: contact.FullName, + PushName: contact.PushName, + BusinessName: contact.BusinessName, + }) + } + + return contactsArray, nil + +} + +func (u *userService) GetPrivacy(instance *instance_model.Instance) (types.PrivacySettings, error) { + client, err := u.ensureClientConnected(instance.Id) + if err != nil { + return types.PrivacySettings{}, err + } + + privacy := client.GetPrivacySettings(context.Background()) + + return privacy, nil +} + +func (u *userService) SetPrivacy(data *PrivacyStruct, instance *instance_model.Instance) (*types.PrivacySettings, error) { + client, err := u.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + privacySettings := []struct { + name types.PrivacySettingType + value types.PrivacySetting + }{ + {types.PrivacySettingTypeGroupAdd, data.GroupAdd}, + {types.PrivacySettingTypeLastSeen, data.LastSeen}, + {types.PrivacySettingTypeStatus, data.Status}, + {types.PrivacySettingTypeProfile, data.Profile}, + {types.PrivacySettingTypeReadReceipts, data.ReadReceipts}, + {types.PrivacySettingTypeCallAdd, data.CallAdd}, + {types.PrivacySettingTypeOnline, data.Online}, + } + + for _, setting := range privacySettings { + _, err := client.SetPrivacySetting(context.Background(), setting.name, setting.value) + if err != nil { + return nil, err + } + } + + privacy := client.GetPrivacySettings(context.Background()) + + return &privacy, nil +} + +func (u *userService) BlockContact(data *BlockStruct, instance *instance_model.Instance) (*types.Blocklist, error) { + client, err := u.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + jid, ok := utils.ParseJID(data.Number) + if !ok { + return nil, errors.New("invalid phone number") + } + + resp, err := client.UpdateBlocklist(context.Background(), jid, events.BlocklistChangeActionBlock) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (u *userService) UnlockContact(data *BlockStruct, instance *instance_model.Instance) (*types.Blocklist, error) { + client, err := u.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + jid, ok := utils.ParseJID(data.Number) + if !ok { + return nil, errors.New("invalid phone number") + } + + resp, err := client.UpdateBlocklist(context.Background(), jid, events.BlocklistChangeActionUnblock) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (u *userService) GetBlockList(instance *instance_model.Instance) (*types.Blocklist, error) { + client, err := u.ensureClientConnected(instance.Id) + if err != nil { + return nil, err + } + + resp, err := client.GetBlocklist(context.Background()) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (u *userService) SetProfilePicture(data *SetProfilePictureStruct, instance *instance_model.Instance) (bool, error) { + client, err := u.ensureClientConnected(instance.Id) + if err != nil { + return false, err + } + + var filedata []byte + + resp, err := http.Get(data.Image) + if err != nil { + return false, fmt.Errorf("failed to fetch image from URL: %v", err) + } + defer resp.Body.Close() + + filedata, err = io.ReadAll(resp.Body) + if err != nil { + return false, fmt.Errorf("failed to read image data: %v", err) + } + + _, err = client.SetGroupPhoto(context.Background(), types.EmptyJID, filedata) + if err != nil { + return false, err + } + + return true, nil +} + +func (u *userService) SetProfileName(data *SetProfileNameStruct, instance *instance_model.Instance) (bool, error) { + client, err := u.ensureClientConnected(instance.Id) + if err != nil { + return false, err + } + + err = client.SetGroupName(context.Background(), types.EmptyJID, data.Name) + if err != nil { + return false, err + } + + return true, nil +} + +func (u *userService) SetProfileStatus(data *SetProfileStatusStruct, instance *instance_model.Instance) (bool, error) { + client, err := u.ensureClientConnected(instance.Id) + if err != nil { + return false, err + } + + err = client.SetStatusMessage(context.Background(), data.Status) + if err != nil { + return false, err + } + + return true, nil +} + +func NewUserService( + clientPointer map[string]*whatsmeow.Client, + whatsmeowService whatsmeow_service.WhatsmeowService, + loggerWrapper *logger_wrapper.LoggerManager, +) UserService { + return &userService{ + clientPointer: clientPointer, + whatsmeowService: whatsmeowService, + loggerWrapper: loggerWrapper, + } +} diff --git a/whatsapp-service/pkg/utils/utils.go b/whatsapp-service/pkg/utils/utils.go new file mode 100644 index 0000000000000000000000000000000000000000..0e786c008c71e9ac1a387e2a7b6e5d8b56b9be51 --- /dev/null +++ b/whatsapp-service/pkg/utils/utils.go @@ -0,0 +1,653 @@ +package utils + +import ( + "encoding/json" + "fmt" + "net" + "net/http" + "net/url" + "runtime" + "strconv" + "strings" + "time" + + "github.com/gomessguii/logger" + "go.mau.fi/whatsmeow/proto/waCompanionReg" + "go.mau.fi/whatsmeow/proto/waE2E" + whatsmeow_types "go.mau.fi/whatsmeow/types" + "golang.org/x/exp/rand" + "golang.org/x/net/proxy" +) + +type Values struct { + m map[string]string +} + +type VCardStruct struct { + FullName string `json:"fullName"` + Organization string `json:"organization"` + Phone string `json:"phone"` +} + +func GenerateRandomString(length int) string { + characters := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + b := make([]byte, length) + for i := range b { + b[i] = characters[rand.Intn(len(characters))] + } + return string(b) +} + +func Find(slice []string, val string) bool { + for _, item := range slice { + if item == val { + return true + } + } + return false +} + +// CreateJID creates a properly formatted WhatsApp JID from a number string +// This function matches the TypeScript createJid functionality with enhanced validation +func CreateJID(number string) (string, error) { + if number == "" { + return "", fmt.Errorf("number cannot be empty") + } + + // Remove timestamp suffix if present + // number = strings.Split(number, ":")[0] + + // Check if already a valid JID format + if strings.Contains(number, "@g.us") || + strings.Contains(number, "@s.whatsapp.net") || + strings.Contains(number, "@lid") || + strings.Contains(number, "@broadcast") || + strings.Contains(number, "@newsletter") { + return number, nil + } + + // Clean the number + number = strings.ReplaceAll(number, " ", "") + number = strings.ReplaceAll(number, "+", "") + number = strings.ReplaceAll(number, "(", "") + number = strings.ReplaceAll(number, ")", "") + number = strings.Split(number, ":")[0] + + // Check if it's a group by hyphen and length + if strings.Contains(number, "-") && len(number) >= 24 { + // Remove non-digit and non-hyphen characters + groupID := strings.Map(func(r rune) rune { + if (r >= '0' && r <= '9') || r == '-' { + return r + } + return -1 + }, number) + return groupID + "@g.us", nil + } + + // Check if it's a group by length (18+ digits) + if len(number) >= 18 { + // Remove non-digit and non-hyphen characters + groupID := strings.Map(func(r rune) rune { + if (r >= '0' && r <= '9') || r == '-' { + return r + } + return -1 + }, number) + return groupID + "@g.us", nil + } + + // Remove all non-numeric characters for phone numbers + number = strings.Map(func(r rune) rune { + if r >= '0' && r <= '9' { + return r + } + return -1 + }, number) + + if number == "" { + return "", fmt.Errorf("invalid number format") + } + + // Format MX (52) or AR (54) numbers + number = formatMXOrARNumber(number) + + // Format BR (55) numbers + number = formatBRNumber(number) + + // Add + prefix for international format + if !strings.HasPrefix(number, "+") { + number = "+" + number + } + + return number + "@s.whatsapp.net", nil +} + +// formatMXOrARNumber formats Mexican (52) or Argentine (54) numbers +func formatMXOrARNumber(jid string) string { + if len(jid) < 2 { + return jid + } + + countryCode := jid[:2] + + // Check if it's MX (52) or AR (54) + if countryCode == "52" && len(jid) == 13 { + // Mexico: remove 2 digits (positions 2-3) + // 5215551234567 -> 52 + 551234567 = 52551234567 + return countryCode + jid[4:] + } else if countryCode == "54" && len(jid) == 13 { + // Argentina: remove 1 digit (position 2) + // 5411123456789 -> 54 + 11123456789 = 5411123456789 + return countryCode + jid[3:] + } + + return jid +} + +// formatBRNumber formats Brazilian (55) numbers according to the mobile number rules +func formatBRNumber(jid string) string { + // Only process if it's exactly 13 digits and starts with "55" + if len(jid) != 13 || !strings.HasPrefix(jid, "55") { + return jid + } + + // Extract DDD (area code) - should be between 11-99 for Brazil + ddd := jid[2:4] + + // Convert DDD to integer for validation + dddNum, err := strconv.Atoi(ddd) + if err != nil { + return jid + } + + // Brazilian DDD codes are between 11-99, if it's outside this range, it's not Brazil + if dddNum < 11 || dddNum > 99 { + return jid + } + + // Extract the first digit after DDD + if len(jid) < 6 { + return jid + } + + firstDigit := jid[4:5] + firstDigitNum, err := strconv.Atoi(firstDigit) + if err != nil { + return jid + } + + // Check if it's a mobile number (9 prefix) and DDD >= 31 + if firstDigitNum >= 7 && dddNum >= 31 { + // Remove the 9 prefix for mobile numbers with DDD >= 31 + return jid[:4] + jid[5:] + } + + // Keep the number as is (landline or special case) + return jid +} + +// ParseJID parses a number string into a WhatsApp JID with validation +func ParseJID(arg string) (whatsmeow_types.JID, bool) { + if arg == "" { + return whatsmeow_types.NewJID("", whatsmeow_types.DefaultUserServer), false + } + + // Use CreateJID for consistent formatting + jidString, err := CreateJID(arg) + if err != nil { + logger.LogWarn("Failed to create JID: %s", err.Error()) + return whatsmeow_types.NewJID("", whatsmeow_types.DefaultUserServer), false + } + + // Parse the formatted JID + recipient, err := whatsmeow_types.ParseJID(jidString) + if err != nil { + logger.LogWarn("Invalid JID: %s", err.Error()) + return recipient, false + } + + if recipient.User == "" && !strings.Contains(jidString, "@broadcast") { + logger.LogError("Invalid JID. No user specified: %s", jidString) + return recipient, false + } + + return recipient, true +} + +// CanonicalJID returns a JID safe for RAW protocol nodes (chatstate / typing, +// read receipts, presence subscribe, reactions, etc.). +// +// CreateJID intentionally prefixes phone numbers with "+" (e.g. +// "+554187083284@s.whatsapp.net") to match the IsOnWhatsApp/display convention. +// Message sending tolerates this because whatsmeow normalizes the JID during +// usync/device resolution. RAW nodes are sent WITHOUT usync, so a malformed +// "+JID" reaches the server and the node is silently dropped (e.g. the "typing" +// indicator never reaches the recipient). WhatsApp user JIDs are digits-only, so +// strip the leading "+" to get the canonical form. +func CanonicalJID(jid whatsmeow_types.JID) whatsmeow_types.JID { + jid.User = strings.TrimPrefix(jid.User, "+") + return jid +} + +func CreateHTTPProxy(httpHost, httpPort, user, password string) (func(*http.Request) (*url.URL, error), error) { + address := fmt.Sprintf("http://%s:%s@%s:%s", user, password, httpHost, httpPort) + + parsed, err := url.Parse(address) + if err != nil { + return nil, err + } + + return http.ProxyURL(parsed), nil +} + +func CreateSocks5Proxy(socks5Host, socks5Port, user, password string) (proxy.Dialer, error) { + auth := &proxy.Auth{ + User: user, + Password: password, + } + + dialer, err := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%s", socks5Host, socks5Port), auth, proxy.Direct) + if err != nil { + return nil, err + } + + return dialer, nil + + // return func(req *http.Request) (*url.URL, error) { + // host := req.URL.Host + // if !strings.Contains(host, ":") { + // host = fmt.Sprintf("%s:443", host) // Adiciona porta padrão 443 se não especificada + // } + // conn, err := dialer.Dial("tcp", host) + // if err != nil { + // return nil, err + // } + // defer conn.Close() + + // return nil, nil + // }, nil +} + +// NormalizeProxyProtocol returns the proxy protocol normalized to one of +// http/https/socks5. If not supplied, it is inferred from the port — ports +// 1080, 2080, and 42000-43000 map to socks5; everything else defaults to http. +func NormalizeProxyProtocol(protocol, port string) string { + normalized := strings.ToLower(strings.TrimSpace(protocol)) + + switch normalized { + case "socks": + return "socks5" + case "http", "https", "socks5": + return normalized + } + + switch strings.TrimSpace(port) { + case "1080", "2080": + return "socks5" + } + + portNum, err := strconv.Atoi(strings.TrimSpace(port)) + if err == nil && portNum >= 42000 && portNum <= 43000 { + return "socks5" + } + + return "http" +} + +// BuildProxyAddress builds a proxy URL string suitable for whatsmeow's +// client.SetProxyAddress — it supports http, https, and socks5 with optional +// basic auth credentials. +func BuildProxyAddress(protocol, host, port, user, password string) (string, error) { + if strings.TrimSpace(host) == "" { + return "", fmt.Errorf("proxy host is required") + } + + if strings.TrimSpace(port) == "" { + return "", fmt.Errorf("proxy port is required") + } + + normalizedProtocol := NormalizeProxyProtocol(protocol, port) + + if normalizedProtocol != "http" && normalizedProtocol != "https" && normalizedProtocol != "socks5" { + return "", fmt.Errorf("unsupported proxy protocol %q", protocol) + } + + proxyURL := &url.URL{ + Scheme: normalizedProtocol, + Host: net.JoinHostPort(strings.TrimSpace(host), strings.TrimSpace(port)), + } + + if user != "" { + if password != "" { + proxyURL.User = url.UserPassword(user, password) + } else { + proxyURL.User = url.User(user) + } + } + + return proxyURL.String(), nil +} + +func UpdateUserInfo(values interface{}, field string, value string) interface{} { + v, ok := values.(Values) + if !ok { + logger.LogError("Failed to cast values to Values type") + return values + } + + logger.LogDebug("User info updated field: %s value: %s", field, value) + v.m[field] = value + return v +} + +func TimestampToUnixInt(timestamp string) (int64, error) { + layout := "2006-01-02 15:04:05" + + t, err := time.Parse(layout, timestamp) + if err != nil { + return 0, err + } + + unixTimestamp := t.Unix() + + return unixTimestamp, nil +} + +func GenerateVC(data VCardStruct) string { + result := ` +BEGIN:VCARD +VERSION:3.0 +FN:` + data.FullName + ` +ORG:` + data.Organization + `; +TEL;type=CELL;type=VOICE;waid=` + data.Phone + `:` + data.Phone + ` +END:VCARD` + + return result +} + +func GetObject(message []byte, keyFind string) string { + var messageMap map[string]interface{} + err := json.Unmarshal(message, &messageMap) + if err != nil { + logger.LogError("failed to unmarshal message: %s", err) + return "" + } + for key, value := range messageMap { + if key == keyFind { + if captionStr, ok := value.(string); ok { + return captionStr + } + } + + if nestedMap, ok := value.(map[string]interface{}); ok { + nestedMapBytes, err := json.Marshal(nestedMap) + if err != nil { + logger.LogError("failed to marshal nestedMap: %s", err) + continue + } + if caption := GetObject(nestedMapBytes, keyFind); caption != "" { + return caption + } + } + } + return "" +} + +func WhatsAppGetUserOS() string { + switch runtime.GOOS { + case "windows": + return "Windows" + case "darwin": + return "macOS" + default: + return "Linux" + } +} + +func WhatsAppGetUserAgent(agentType string) waCompanionReg.DeviceProps_PlatformType { + switch strings.ToLower(agentType) { + case "desktop": + return waCompanionReg.DeviceProps_DESKTOP + case "mac": + return waCompanionReg.DeviceProps_CATALINA + case "android": + return waCompanionReg.DeviceProps_ANDROID_AMBIGUOUS + case "android-phone": + return waCompanionReg.DeviceProps_ANDROID_PHONE + case "andorid-tablet": + return waCompanionReg.DeviceProps_ANDROID_TABLET + case "ios-phone": + return waCompanionReg.DeviceProps_IOS_PHONE + case "ios-catalyst": + return waCompanionReg.DeviceProps_IOS_CATALYST + case "ipad": + return waCompanionReg.DeviceProps_IPAD + case "wearos": + return waCompanionReg.DeviceProps_WEAR_OS + case "ie": + return waCompanionReg.DeviceProps_IE + case "edge": + return waCompanionReg.DeviceProps_EDGE + case "chrome": + return waCompanionReg.DeviceProps_CHROME + case "safari": + return waCompanionReg.DeviceProps_SAFARI + case "firefox": + return waCompanionReg.DeviceProps_FIREFOX + case "opera": + return waCompanionReg.DeviceProps_OPERA + case "uwp": + return waCompanionReg.DeviceProps_UWP + case "aloha": + return waCompanionReg.DeviceProps_ALOHA + case "tv-tcl": + return waCompanionReg.DeviceProps_TCL_TV + default: + return waCompanionReg.DeviceProps_UNKNOWN + } +} + +func GetMessageType(waMsg *waE2E.Message) string { + switch { + case waMsg == nil: + return "ignore" + case waMsg.Conversation != nil, waMsg.ExtendedTextMessage != nil: + return "text" + case waMsg.ImageMessage != nil: + return fmt.Sprintf("image %s", waMsg.GetImageMessage().GetMimetype()) + case waMsg.StickerMessage != nil: + return fmt.Sprintf("sticker %s", waMsg.GetStickerMessage().GetMimetype()) + case waMsg.VideoMessage != nil: + return fmt.Sprintf("video %s", waMsg.GetVideoMessage().GetMimetype()) + case waMsg.PtvMessage != nil: + return fmt.Sprintf("round video %s", waMsg.GetPtvMessage().GetMimetype()) + case waMsg.AudioMessage != nil: + return fmt.Sprintf("audio %s", waMsg.GetAudioMessage().GetMimetype()) + case waMsg.DocumentMessage != nil: + return fmt.Sprintf("document %s", waMsg.GetDocumentMessage().GetMimetype()) + case waMsg.ContactMessage != nil: + return "contact" + case waMsg.ContactsArrayMessage != nil: + return "contact array" + case waMsg.LocationMessage != nil: + return "location" + case waMsg.LiveLocationMessage != nil: + return "live location start" + case waMsg.GroupInviteMessage != nil: + return "group invite" + case waMsg.GroupMentionedMessage != nil: + return "group mention" + case waMsg.ScheduledCallCreationMessage != nil: + return "scheduled call create" + case waMsg.ScheduledCallEditMessage != nil: + return "scheduled call edit" + case waMsg.ReactionMessage != nil: + if waMsg.ReactionMessage.GetText() == "" { + return "reaction remove" + } + return "reaction" + case waMsg.EncReactionMessage != nil: + return "encrypted reaction" + case waMsg.PollCreationMessage != nil || waMsg.PollCreationMessageV2 != nil || waMsg.PollCreationMessageV3 != nil: + return "poll create" + case waMsg.PollUpdateMessage != nil: + return "poll update" + case waMsg.ProtocolMessage != nil: + switch waMsg.GetProtocolMessage().GetType() { + case waE2E.ProtocolMessage_REVOKE: + if waMsg.GetProtocolMessage().GetKey() == nil { + return "ignore" + } + return "revoke" + case waE2E.ProtocolMessage_MESSAGE_EDIT: + return "edit" + case waE2E.ProtocolMessage_EPHEMERAL_SETTING: + return "disappearing timer change" + case waE2E.ProtocolMessage_APP_STATE_SYNC_KEY_SHARE, + waE2E.ProtocolMessage_HISTORY_SYNC_NOTIFICATION, + waE2E.ProtocolMessage_INITIAL_SECURITY_NOTIFICATION_SETTING_SYNC: + return "ignore" + default: + return fmt.Sprintf("unknown_protocol_%d", waMsg.GetProtocolMessage().GetType()) + } + case waMsg.ButtonsMessage != nil: + return "buttons" + case waMsg.ButtonsResponseMessage != nil: + return "buttons response" + case waMsg.TemplateMessage != nil: + return "template" + case waMsg.HighlyStructuredMessage != nil: + return "highly structured template" + case waMsg.TemplateButtonReplyMessage != nil: + return "template button reply" + case waMsg.InteractiveMessage != nil: + return "interactive" + case waMsg.GetInteractiveResponseMessage() != nil: + return "interactive response" + case waMsg.ListMessage != nil: + return "list" + case waMsg.ProductMessage != nil: + return "product" + case waMsg.ListResponseMessage != nil: + return "list response" + case waMsg.OrderMessage != nil: + return "order" + case waMsg.InvoiceMessage != nil: + return "invoice" + case waMsg.BotInvokeMessage != nil: + return "bot invoke" + case waMsg.EventMessage != nil: + return "event" + case waMsg.EventCoverImage != nil: + return "event cover image" + case waMsg.EncEventResponseMessage != nil: + return "ignore" // these are ignored for now as they're not meant to be shown as new messages + //return "encrypted event response" + case waMsg.CommentMessage != nil: + return "comment" + case waMsg.EncCommentMessage != nil: + return "encrypted comment" + case waMsg.NewsletterAdminInviteMessage != nil: + return "newsletter admin invite" + case waMsg.SecretEncryptedMessage != nil: + return "secret encrypted" + case waMsg.PollResultSnapshotMessage != nil: + return "poll result snapshot" + case waMsg.MessageHistoryBundle != nil: + return "message history bundle" + case waMsg.RequestPhoneNumberMessage != nil: + return "request phone number" + case waMsg.KeepInChatMessage != nil: + return "keep in chat" + case waMsg.StatusMentionMessage != nil: + return "status mention" + case waMsg.StickerPackMessage != nil: + return "sticker pack" + case waMsg.AlbumMessage != nil: + return "album" // or maybe these should be ignored? + case waMsg.SendPaymentMessage != nil, waMsg.RequestPaymentMessage != nil, + waMsg.DeclinePaymentRequestMessage != nil, waMsg.CancelPaymentRequestMessage != nil, + waMsg.PaymentInviteMessage != nil: + return "payment" + case waMsg.Call != nil: + return "call" + case waMsg.Chat != nil: + return "chat" + case waMsg.PlaceholderMessage != nil: + return "placeholder" + case waMsg.SenderKeyDistributionMessage != nil, waMsg.StickerSyncRmrMessage != nil: + return "ignore" + default: + return "unknown" + } +} + +func GetStringValue(s *string) string { + if s == nil { + return "" + } + return *s +} + +// PrepareNumbersForWhatsAppCheck prepares phone numbers for IsOnWhatsApp call +// based on formatJid flag. This centralizes the logic used by both CheckUser and SendText. +func PrepareNumbersForWhatsAppCheck(numbers []string, formatJid *bool) ([]string, error) { + // Default formatJid to true if not specified + shouldFormat := true + if formatJid != nil { + shouldFormat = *formatJid + } + + var phoneNumbers []string + + if shouldFormat { + // Normalize numbers using CreateJID for consistent formatting + for _, number := range numbers { + // First, extract the raw number if it's already a JID + rawNumber := number + if strings.Contains(number, "@s.whatsapp.net") { + rawNumber = strings.Split(number, "@")[0] + } + + // Use CreateJID to normalize the raw number format + normalizedJID, err := CreateJID(rawNumber) + if err != nil { + // Continue with original number if normalization fails + phoneNumbers = append(phoneNumbers, number) + continue + } + + // Extract the phone number part from the JID for IsOnWhatsApp call + // e.g., "+5511999999999@s.whatsapp.net" -> "+5511999999999" (keep + for IsOnWhatsApp) + if strings.Contains(normalizedJID, "@s.whatsapp.net") { + phoneNumber := strings.Split(normalizedJID, "@")[0] + phoneNumbers = append(phoneNumbers, phoneNumber) + } else if strings.Contains(normalizedJID, "@g.us") || strings.Contains(normalizedJID, "@broadcast") || strings.Contains(normalizedJID, "@lid") { + // For groups, broadcasts, and LIDs, use the full JID + phoneNumbers = append(phoneNumbers, normalizedJID) + } else { + phoneNumbers = append(phoneNumbers, normalizedJID) + } + } + } else { + // Use numbers exactly as received (raw format) + phoneNumbers = append(phoneNumbers, numbers...) + } + + return phoneNumbers, nil +} + +// PrepareNumberForWhatsAppCheck prepares a single phone number for IsOnWhatsApp call +// This is used by SendText which works with single numbers +func PrepareNumberForWhatsAppCheck(phone string, formatJid bool) (string, error) { + formatJidPtr := &formatJid + numbers, err := PrepareNumbersForWhatsAppCheck([]string{phone}, formatJidPtr) + if err != nil { + return "", err + } + if len(numbers) == 0 { + return "", fmt.Errorf("no valid number processed") + } + return numbers[0], nil +} diff --git a/whatsapp-service/pkg/whatsmeow/service/referral.go b/whatsapp-service/pkg/whatsmeow/service/referral.go new file mode 100644 index 0000000000000000000000000000000000000000..987d5d34394ad609e4a26f9b45db627203113d09 --- /dev/null +++ b/whatsapp-service/pkg/whatsmeow/service/referral.go @@ -0,0 +1,50 @@ +package whatsmeow_service + +import ( + "encoding/json" + + "go.mau.fi/whatsmeow/proto/waE2E" + "google.golang.org/protobuf/encoding/protojson" +) + +func extractReferralFromMessage(message *waE2E.Message) json.RawMessage { + contextInfo := getContextInfoFromMessage(message) + if contextInfo == nil || contextInfo.GetExternalAdReply() == nil { + return nil + } + + referral, err := protojson.Marshal(contextInfo.GetExternalAdReply()) + if err != nil || len(referral) == 0 { + return nil + } + + return json.RawMessage(referral) +} + +func getContextInfoFromMessage(message *waE2E.Message) *waE2E.ContextInfo { + if message == nil { + return nil + } + + if extendedText := message.GetExtendedTextMessage(); extendedText != nil { + return extendedText.GetContextInfo() + } + + if image := message.GetImageMessage(); image != nil { + return image.GetContextInfo() + } + + if audio := message.GetAudioMessage(); audio != nil { + return audio.GetContextInfo() + } + + if document := message.GetDocumentMessage(); document != nil { + return document.GetContextInfo() + } + + if video := message.GetVideoMessage(); video != nil { + return video.GetContextInfo() + } + + return nil +} diff --git a/whatsapp-service/pkg/whatsmeow/service/whatsmeow.go b/whatsapp-service/pkg/whatsmeow/service/whatsmeow.go new file mode 100644 index 0000000000000000000000000000000000000000..669406de938b88c20cc134ae51b75314633d0f41 --- /dev/null +++ b/whatsapp-service/pkg/whatsmeow/service/whatsmeow.go @@ -0,0 +1,2912 @@ +package whatsmeow_service + +import ( + "bytes" + "context" + "database/sql" + "encoding/base64" + "encoding/json" + "fmt" + "image/png" + "io" + "math/rand" + "net/http" + "os" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "golang.org/x/image/webp" + "google.golang.org/protobuf/proto" + + _ "github.com/lib/pq" + "github.com/redis/go-redis/v9" + "github.com/skip2/go-qrcode" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/appstate" + "go.mau.fi/whatsmeow/proto/waCompanionReg" + "go.mau.fi/whatsmeow/proto/waE2E" + "go.mau.fi/whatsmeow/store" + "go.mau.fi/whatsmeow/store/sqlstore" + "go.mau.fi/whatsmeow/types" + "go.mau.fi/whatsmeow/types/events" + waLog "go.mau.fi/whatsmeow/util/log" + + "agentdeck-whatsapp-service/pkg/cache" + "agentdeck-whatsapp-service/pkg/config" + producer_interfaces "agentdeck-whatsapp-service/pkg/events/interfaces" + instance_model "agentdeck-whatsapp-service/pkg/instance/model" + instance_repository "agentdeck-whatsapp-service/pkg/instance/repository" + "agentdeck-whatsapp-service/pkg/internal/event_types" + label_model "agentdeck-whatsapp-service/pkg/label/model" + label_repository "agentdeck-whatsapp-service/pkg/label/repository" + logger_wrapper "agentdeck-whatsapp-service/pkg/logger" + message_model "agentdeck-whatsapp-service/pkg/message/model" + message_repository "agentdeck-whatsapp-service/pkg/message/repository" + "agentdeck-whatsapp-service/pkg/passkey/ceremony" + poll_service "agentdeck-whatsapp-service/pkg/poll/service" + "agentdeck-whatsapp-service/pkg/supabase" + storage_interfaces "agentdeck-whatsapp-service/pkg/storage/interfaces" + "agentdeck-whatsapp-service/pkg/utils" +) + +type WhatsmeowService interface { + StartClient(clientData *ClientData) + ConnectOnStartup(clientName string) + StartInstance(instanceId string) error + ReconnectClient(instanceId string) error + ClearInstanceCache(instanceId string, token string) error + CallWebhook(instance *instance_model.Instance, queueName string, jsonData []byte) + SendToGlobalQueues(event string, jsonData []byte, userId string) + ForceUpdateJid(instanceId string, number string) error + UpdateInstanceSettings(instanceId string) error + UpdateInstanceAdvancedSettings(instanceId string) error + GetPollService() poll_service.PollService // NOVO: Acesso ao serviço de polls + + // Passkey (WebAuthn) pairing bridge — read by the public ceremony endpoint, + // written by the whatsmeow event goroutine. + PasskeyCeremonyStore() *ceremony.Store + SubmitPasskeyResponse(instanceId string, resp *types.WebAuthnResponse) error + ConfirmPasskey(instanceId string) error +} + +type clientVersion struct { + Major int + Minor int + Patch int +} + +type whatsmeowService struct { + instanceRepository instance_repository.InstanceRepository + authDB *sql.DB + supa *supabase.Client + messageRepository message_repository.MessageRepository + labelRepository label_repository.LabelRepository + pollService poll_service.PollService // NOVO: Serviço de enquetes + config *config.Config + killChannel map[string](chan bool) + userInfoCache *cache.Cache + clientPointer map[string]*whatsmeow.Client + myClientPointer map[string]*MyClient + rabbitmqProducer producer_interfaces.Producer + webhookProducer producer_interfaces.Producer + websocketProducer producer_interfaces.Producer + sqliteDB *sql.DB + exPath string + mediaStorage storage_interfaces.MediaStorage + processedMessages *cache.Cache + natsProducer producer_interfaces.Producer + loggerWrapper *logger_wrapper.LoggerManager + passkeyCeremony *ceremony.Store +} + +type MyClient struct { + service WhatsmeowService + WAClient *whatsmeow.Client + eventHandlerID uint32 + userID string + Instance *instance_model.Instance + token string + subscriptions []string + webhookUrl string + rabbitmqEnable string + natsEnable string + websocketEnable string + instanceRepository instance_repository.InstanceRepository + messageRepository message_repository.MessageRepository + labelRepository label_repository.LabelRepository + pollService poll_service.PollService // NOVO: Serviço de enquetes + clientPointer map[string]*whatsmeow.Client + myClientPointer map[string]*MyClient + killChannel map[string](chan bool) + userInfoCache *cache.Cache + config *config.Config + historySyncID int32 + rabbitmqProducer producer_interfaces.Producer + webhookProducer producer_interfaces.Producer + websocketProducer producer_interfaces.Producer + mediaStorage storage_interfaces.MediaStorage + processedMessages *cache.Cache + natsProducer producer_interfaces.Producer + loggerWrapper *logger_wrapper.LoggerManager + qrcodeCount int + passkeyCeremony *ceremony.Store +} + +func (mycli *MyClient) persistMessageAsync(message message_model.Message) { + if mycli == nil || mycli.messageRepository == nil { + return + } + + go func() { + if err := mycli.messageRepository.InsertMessage(message); err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to persist message %s: %v", mycli.userID, message.MessageID, err) + } + }() +} + +type ClientData struct { + Instance *instance_model.Instance + Subscriptions []string + Phone string + IsProxy bool +} + +type Values struct { + m map[string]string +} + +func (v Values) Get(key string) string { + return v.m[key] +} + +// MarshalJSON encodes the internal map so Values can be stored in Redis. +func (v Values) MarshalJSON() ([]byte, error) { + return json.Marshal(v.m) +} + +// UnmarshalJSON decodes the internal map from Redis. +func (v *Values) UnmarshalJSON(data []byte) error { + if v.m == nil { + v.m = make(map[string]string) + } + return json.Unmarshal(data, &v.m) +} + +// NewValues builds a Values from a flat map. +func NewValues(m map[string]string) Values { + return Values{m: m} +} + +type UserCollection struct { + Users map[types.JID]types.UserInfo +} + +type ProxyConfig struct { + Protocol string `json:"protocol,omitempty"` + Host string `json:"host"` + Password string `json:"password"` + Port string `json:"port"` + Username string `json:"username"` +} + +func (w whatsmeowService) ReconnectClient(instanceId string) error { + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Starting reconnection process - simulating restart", instanceId) + + // Passo 1: Limpar conexão existente se houver + if client, exists := w.clientPointer[instanceId]; exists { + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Disconnecting existing client", instanceId) + + // Desconectar o cliente WebSocket + if client.IsConnected() { + client.Disconnect() + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] WebSocket disconnected", instanceId) + } + + // Remover event handler se existir + if mycli, ok := w.myClientPointer[instanceId]; ok { + if mycli.eventHandlerID != 0 { + client.RemoveEventHandler(mycli.eventHandlerID) + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Event handler removed", instanceId) + } + } + } + + // Passo 2: Limpar todos os recursos da instância + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Cleaning up resources", instanceId) + + // Enviar sinal de kill se o canal existir + if killChan, exists := w.killChannel[instanceId]; exists { + select { + case killChan <- true: + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Kill signal sent", instanceId) + default: + // Canal pode estar bloqueado, continua + } + } + + // Remover das estruturas + delete(w.clientPointer, instanceId) + delete(w.myClientPointer, instanceId) + delete(w.killChannel, instanceId) + + // Limpar cache de userInfo para esta instância + if instance, err := w.instanceRepository.GetInstanceByID(instanceId); err == nil { + w.userInfoCache.Delete(instance.Token) + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] UserInfo cache cleared for token: %s", instanceId, instance.Token) + } + + // Passo 3: Atualizar status no banco + instance, err := w.instanceRepository.GetInstanceByID(instanceId) + if err != nil { + return fmt.Errorf("failed to get instance: %v", err) + } + + instance.Connected = false + instance.DisconnectReason = "Reconnecting" + err = w.instanceRepository.UpdateConnected(instanceId, false, "Reconnecting") + if err != nil { + w.loggerWrapper.GetLogger(instanceId).LogWarn("[%s] Failed to update disconnect status: %v", instanceId, err) + } + + // Passo 4: Aguardar um pouco para garantir limpeza completa + time.Sleep(2 * time.Second) + + // Passo 5: Iniciar nova instância como se fosse a primeira vez + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Starting fresh instance", instanceId) + return w.StartInstance(instanceId) +} + +func (w whatsmeowService) ForceUpdateJid(instanceId string, number string) error { + instance, err := w.instanceRepository.GetInstanceByID(instanceId) + if err != nil { + w.loggerWrapper.GetLogger(instanceId).LogError("[%s] Error getting instance: %v", instanceId, err) + return err + } + + if instance.Jid == "" && number != "" { + sqlDeviceSearch := fmt.Sprintf("SELECT jid FROM whatsmeow_device WHERE jid LIKE '%%%s%%'", number) + rows, err := w.authDB.Query(sqlDeviceSearch) + if err != nil { + w.loggerWrapper.GetLogger(instanceId).LogError("[%s] Error getting device: %v", instanceId, err) + return err + } + + defer rows.Close() + + var latestJid string + var latestSession int + + for rows.Next() { + type deviceStruct struct { + Jid string `json:"jid"` + } + var device deviceStruct + err := rows.Scan(&device.Jid) + if err != nil { + w.loggerWrapper.GetLogger(instanceId).LogError("[%s] Error getting device: %v", instanceId, err) + return err + } + + // Extrair o número da sessão do JID + parts := strings.Split(device.Jid, ":") + if len(parts) == 2 { + sessionPart := strings.Split(parts[1], "@")[0] + session, err := strconv.Atoi(sessionPart) + if err != nil { + w.loggerWrapper.GetLogger(instanceId).LogError("[%s] Error parsing session number: %v", instanceId, err) + return err + } + + // Atualizar se for a sessão mais recente + if session > latestSession { + latestSession = session + latestJid = device.Jid + } + } + } + + // Atualizar a instância com o JID mais recente + if latestJid != "" { + instance.Jid = latestJid + err = w.instanceRepository.UpdateJid(instanceId, latestJid) + if err != nil { + w.loggerWrapper.GetLogger(instanceId).LogError("[%s] Error updating instance: %v", instanceId, err) + } + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Updated instance with latest JID: %s (session: %d)", instanceId, latestJid, latestSession) + } + } + + return nil +} + +func (w whatsmeowService) StartClient(cd *ClientData) { + + w.loggerWrapper.GetLogger(cd.Instance.Id).LogInfo("Starting websocket connection to Whatsapp for user '%s'", cd.Instance.Id) + + var deviceStore *store.Device + var err error + + if w.clientPointer[cd.Instance.Id] != nil { + if w.clientPointer[cd.Instance.Id].IsConnected() { + return + } + } + + var container *sqlstore.Container + + // whatsmeow's device/session store requires a native Postgres connection. + // It cannot use the HTTP PostgREST API; the DSN comes from SUPABASE_DB_URL. + if w.config.SupabaseDBURL == "" { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] SupabaseDBURL is required for the whatsmeow session store", cd.Instance.Id) + return + } + + dbLog := waLog.Stdout("Database", "WARN", true) + if w.config.WaDebug != "" { + dbLog = waLog.Stdout("Database", w.config.WaDebug, true) + } + container, err = sqlstore.New(context.Background(), "postgres", w.config.SupabaseDBURL, dbLog) + + if err != nil { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Failed to create container: %v", cd.Instance.Id, err) + return + } + + if cd.Instance.Jid != "" { + jid, _ := utils.ParseJID(cd.Instance.Jid) + w.loggerWrapper.GetLogger(cd.Instance.Id).LogInfo("[%s] Jid found. Getting device store for jid: %s", cd.Instance.Id, jid) + deviceStore, err = container.GetDevice(context.Background(), jid) + if err != nil { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Erro ao obter device store: %v", cd.Instance.Id, err) + return + } + } else { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogWarn("[%s] No jid found. Creating new device", cd.Instance.Id) + deviceStore = container.NewDevice() + } + + if deviceStore == nil { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogWarn("[%s] No store found. Creating new one", cd.Instance.Id) + deviceStore = container.NewDevice() + + cd.Instance.Connected = false + err := w.instanceRepository.UpdateConnected(cd.Instance.Id, cd.Instance.Connected, cd.Instance.DisconnectReason) + if err != nil { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Error updating instance: %s", cd.Instance.Id, err) + } + } + + var version clientVersion + + platformID, ok := waCompanionReg.DeviceProps_PlatformType_value[strings.ToUpper("chrome")] + if ok { + store.DeviceProps.PlatformType = waCompanionReg.DeviceProps_PlatformType(platformID).Enum() + } + if cd.Instance.OsName == "" { + cd.Instance.OsName = utils.WhatsAppGetUserOS() + } + + store.DeviceProps.Os = &cd.Instance.OsName + store.DeviceProps.RequireFullSync = proto.Bool(true) + + if w.config.WhatsappVersionMajor != 0 && w.config.WhatsappVersionMinor != 0 && w.config.WhatsappVersionPatch != 0 { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogInfo("[%s] Setting whatsapp version to %d.%d.%d", cd.Instance.Id, w.config.WhatsappVersionMajor, w.config.WhatsappVersionMinor, w.config.WhatsappVersionPatch) + version.Major = w.config.WhatsappVersionMajor + if err == nil { + store.DeviceProps.Version.Primary = proto.Uint32(uint32(version.Major)) + } + version.Minor = w.config.WhatsappVersionMinor + if err == nil { + store.DeviceProps.Version.Secondary = proto.Uint32(uint32(version.Minor)) + } + version.Patch = w.config.WhatsappVersionPatch + if err == nil { + store.DeviceProps.Version.Tertiary = proto.Uint32(uint32(version.Patch)) + } + } else { + // Try to fetch version from WhatsApp Web + webVersion, err := fetchWhatsAppWebVersion() + if err != nil { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Failed to fetch WhatsApp Web version: %v", cd.Instance.Id, err) + } else { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogInfo("[%s] Setting whatsapp version from web to %d.%d.%d", cd.Instance.Id, webVersion.Major, webVersion.Minor, webVersion.Patch) + version = *webVersion + store.DeviceProps.Version.Primary = proto.Uint32(uint32(version.Major)) + store.DeviceProps.Version.Secondary = proto.Uint32(uint32(version.Minor)) + store.DeviceProps.Version.Tertiary = proto.Uint32(uint32(version.Patch)) + } + } + + // 🔒 FIX: Sempre criar logger, mesmo que WaDebug esteja vazio + // Usar "INFO" como nível mínimo para garantir que logs importantes apareçam + minLevel := w.config.WaDebug + if minLevel == "" { + minLevel = "INFO" // Nível mínimo para garantir que logs INFO apareçam + } + clientLog := waLog.Stdout("Client", minLevel, true) + client := whatsmeow.NewClient(deviceStore, clientLog) + + w.clientPointer[cd.Instance.Id] = client + + if cd.IsProxy { + var proxyConfig ProxyConfig + err := json.Unmarshal([]byte(cd.Instance.Proxy), &proxyConfig) + if err != nil { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] error unmarshalling proxy config", cd.Instance.Id) + return + } + + proxyProtocol := proxyConfig.Protocol + proxyHost := proxyConfig.Host + proxyPort := proxyConfig.Port + proxyUsername := proxyConfig.Username + proxyPassword := proxyConfig.Password + + if proxyConfig.Host == "" { + proxyHost = w.config.ProxyHost + } + + if proxyConfig.Port == "" { + proxyPort = w.config.ProxyPort + } + + if proxyConfig.Protocol == "" { + proxyProtocol = w.config.ProxyProtocol + } + + if proxyConfig.Username == "" { + proxyUsername = w.config.ProxyUsername + } + + if proxyConfig.Password == "" { + proxyPassword = w.config.ProxyPassword + } + + proxyAddress, err := utils.BuildProxyAddress(proxyProtocol, proxyHost, proxyPort, proxyUsername, proxyPassword) + if err != nil { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogWarn("[%s] Proxy error, continuing without proxy: %v", cd.Instance.Id, err) + } else { + err = client.SetProxyAddress(proxyAddress) + if err != nil { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogWarn("[%s] Proxy error, continuing without proxy: %v", cd.Instance.Id, err) + } else { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogInfo("[%s] Proxy enabled (%s)", cd.Instance.Id, utils.NormalizeProxyProtocol(proxyProtocol, proxyPort)) + } + } + } + + client.EnableAutoReconnect = false + client.AutoTrustIdentity = true + + mycli := &MyClient{ + service: &w, + Instance: cd.Instance, + WAClient: client, + eventHandlerID: 1, + userID: cd.Instance.Id, + token: cd.Instance.Token, + subscriptions: cd.Subscriptions, + webhookUrl: cd.Instance.Webhook, + rabbitmqEnable: cd.Instance.RabbitmqEnable, + natsEnable: cd.Instance.NatsEnable, + websocketEnable: cd.Instance.WebSocketEnable, + instanceRepository: w.instanceRepository, + messageRepository: w.messageRepository, + labelRepository: w.labelRepository, + pollService: w.pollService, // NOVO: Serviço de enquetes + userInfoCache: w.userInfoCache, + clientPointer: w.clientPointer, + myClientPointer: w.myClientPointer, + killChannel: w.killChannel, + config: w.config, + historySyncID: 0, + rabbitmqProducer: w.rabbitmqProducer, + webhookProducer: w.webhookProducer, + websocketProducer: w.websocketProducer, + mediaStorage: w.mediaStorage, + processedMessages: w.processedMessages, + natsProducer: w.natsProducer, + loggerWrapper: w.loggerWrapper, + qrcodeCount: 0, + passkeyCeremony: w.passkeyCeremony, + } + + mycli.eventHandlerID = mycli.WAClient.AddEventHandler(mycli.myEventHandler) + + // Armazena o MyClient no map para permitir atualizações posteriores + w.myClientPointer[cd.Instance.Id] = mycli + + if client.Store.ID != nil { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogInfo("[%s] Already logged in with JID: %s", cd.Instance.Id, client.Store.ID.String()) + err = client.Connect() + if err != nil { + if strings.Contains(err.Error(), "EOF") { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Erro de conexão WebSocket (EOF). Tentando reconectar em 5 segundos...", cd.Instance.Id) + time.Sleep(5 * time.Second) + err = client.Connect() + if err != nil { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Falha na segunda tentativa de conexão: %v", cd.Instance.Id, err) + return + } + } else if strings.Contains(err.Error(), "username/password authentication failed") { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogWarn("[%s] Proxy authentication failed, attempting to connect without proxy", cd.Instance.Id) + + // Desabilita o proxy + client.SetProxy(nil) + + // Tenta conectar sem proxy + err = client.Connect() + if err != nil { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Failed to connect even without proxy: %v", cd.Instance.Id, err) + return + } + w.loggerWrapper.GetLogger(cd.Instance.Id).LogInfo("[%s] Successfully connected without proxy", cd.Instance.Id) + } else { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Failed to connect: %v", cd.Instance.Id, err) + return + } + } + } else { + // New-device pairing. We intentionally do NOT use client.GetQRChannel: + // in the installed whatsmeow its qrChannel handler auto-confirms a + // passkey ceremony (SkipHandoffUX) and Disconnects the socket when the + // QR codes run out, both of which break passkey pairing (DOC2 §4.3/§4.4). + // Instead we Connect() directly and consume *events.QR in myEventHandler + // (see handleQRCodes), which pair.go dispatches to every handler anyway. + err = client.Connect() + if err != nil { + if strings.Contains(err.Error(), "EOF") { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Erro de conexão WebSocket (EOF). Tentando reconectar em 5 segundos...", cd.Instance.Id) + time.Sleep(5 * time.Second) + err = client.Connect() + if err != nil { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Falha na segunda tentativa de conexão: %v", cd.Instance.Id, err) + return + } + } else if strings.Contains(err.Error(), "username/password authentication failed") { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogWarn("[%s] Proxy authentication failed during QR connection, attempting without proxy", cd.Instance.Id) + + // Desabilita o proxy + client.SetProxy(nil) + + // Tenta conectar sem proxy + err = client.Connect() + if err != nil { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Failed to connect even without proxy: %v", cd.Instance.Id, err) + return + } + w.loggerWrapper.GetLogger(cd.Instance.Id).LogInfo("[%s] Successfully connected without proxy", cd.Instance.Id) + } else { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Failed to connect: %v", cd.Instance.Id, err) + return + } + } + + } + + // Removed auto-reconnect logic to prevent infinite loops + + for { + select { + case <-w.killChannel[cd.Instance.Id]: + w.loggerWrapper.GetLogger(cd.Instance.Id).LogInfo("Received kill signal for user '%s'", cd.Instance.Id) + client.Disconnect() + + delete(w.clientPointer, cd.Instance.Id) + delete(w.myClientPointer, cd.Instance.Id) + + // Limpar cache de userInfo para esta instância + w.userInfoCache.Delete(cd.Instance.Token) + w.loggerWrapper.GetLogger(cd.Instance.Id).LogInfo("[%s] UserInfo cache cleared for token: %s", cd.Instance.Id, cd.Instance.Token) + + cd.Instance.Connected = false + + err := w.instanceRepository.UpdateConnected(cd.Instance.Id, cd.Instance.Connected, cd.Instance.DisconnectReason) + if err != nil { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Error updating instance: %s", cd.Instance.Id, err) + } + + postMap := make(map[string]interface{}) + + postMap["event"] = "LoggedOut" + + dataMap := make(map[string]interface{}) + + dataMap["reason"] = "Logged out" + + postMap["data"] = dataMap + + postMap["instanceToken"] = mycli.token + postMap["instanceId"] = mycli.userID + postMap["instanceName"] = cd.Instance.Name + + var queueName string + + if _, ok := postMap["event"]; ok { + queueName = strings.ToLower(fmt.Sprintf("%s.%s", cd.Instance.Id, postMap["event"])) + } + + values, err := json.Marshal(postMap) + if err != nil { + w.loggerWrapper.GetLogger(cd.Instance.Id).LogError("[%s] Failed to marshal JSON for queue", cd.Instance.Id) + return + } + + go w.CallWebhook(cd.Instance, queueName, values) + + if mycli.config.AmqpGlobalEnabled || mycli.config.NatsGlobalEnabled { + go mycli.service.SendToGlobalQueues(postMap["event"].(string), values, mycli.userID) + } + + // restart client + w.loggerWrapper.GetLogger(cd.Instance.Id).LogInfo("[%s] Restarting client", cd.Instance.Id) + w.StartClient(cd) + return + default: + time.Sleep(1000 * time.Millisecond) + } + } +} + +func schedulePresenceUpdates(mycli *MyClient) { + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + // Verificar se a instância ainda existe + _, err := mycli.instanceRepository.GetInstanceByID(mycli.userID) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Instance no longer exists, stopping presence updates", mycli.userID) + return // Encerra a goroutine se a instância não existir mais + } + + processPresenceUpdates(mycli) + + ticker.Stop() + randomInterval := time.Duration(1+rand.Intn(3)) * time.Hour + ticker = time.NewTicker(randomInterval) + + case <-mycli.killChannel[mycli.userID]: + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Received kill signal, stopping presence updates", mycli.userID) + return // Encerra a goroutine quando receber sinal de kill + } + } +} + +func processPresenceUpdates(mycli *MyClient) { + now := time.Now() + location, _ := time.LoadLocation("America/Sao_Paulo") + nowSp := now.In(location) + + if nowSp.Hour() >= 1 && nowSp.Hour() < 24 { + err := mycli.WAClient.SendPresence(context.Background(), types.PresenceUnavailable) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to set presence as unavailable %v", mycli.userID, err) + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Marked self as unavailable", mycli.userID) + } + + time.Sleep(time.Duration(1+rand.Intn(5)) * time.Second) + + err = mycli.WAClient.SendPresence(context.Background(), types.PresenceAvailable) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to set presence as available %v", mycli.userID, err) + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Marked self as available", mycli.userID) + } + } +} + +// handleQRCodes forwards a batch of QR codes (events.QR.Codes) to the manager, +// rotating them with whatsmeow's native timing (first code ~60s, the rest ~20s) +// WITHOUT using GetQRChannel. GetQRChannel is deliberately avoided for new-device +// pairing because, in the installed whatsmeow, its qrChannel handler both +// auto-confirms PairPasskeyConfirmation when SkipHandoffUX is set (racing our +// own confirm flow) and Disconnects the socket when codes run out — either of +// which breaks an in-flight passkey ceremony (DOC2 §4.3/§4.4). Consuming +// events.QR here keeps the socket alive for as long as pairing (QR or passkey) +// needs, since events.QR is dispatched to every handler by pair.go regardless. +// +// This preserves the original GetQRChannel-loop behavior byte-for-byte for the +// per-code work (max-count enforcement, PNG encode, DB persist, webhook/queue +// fan-out) and the timeout teardown; only the trigger (batch vs. per-code) and +// the rotation/self-timer are new. Runs in its own goroutine so it never blocks +// the whatsmeow event dispatch. +func (mycli *MyClient) handleQRCodes(codes []string) { + go func() { + instanceID := mycli.userID + for i, code := range codes { + // A successful pair (Store.ID set) or an in-flight passkey ceremony + // supersedes QR — stop rotating WITHOUT tearing down. Store.ID stays + // nil throughout a passkey ceremony (it is only set at PairSuccess), + // so we must also consult the ceremony store, otherwise a ceremony + // that outlasts QR rotation would have its socket/client torn down. + if mycli.WAClient == nil || mycli.WAClient.Store.ID != nil { + return + } + if mycli.passkeyCeremony != nil && mycli.passkeyCeremony.HasActiveByInstance(instanceID) { + mycli.loggerWrapper.GetLogger(instanceID).LogInfo("[%s] Passkey ceremony in progress — pausing QR rotation, keeping socket alive", instanceID) + return + } + + mycli.qrcodeCount++ + + if mycli.config.QrcodeMaxCount > 0 { + mycli.loggerWrapper.GetLogger(instanceID).LogInfo("[%s] QR code generated #%d (max: %d)", instanceID, mycli.qrcodeCount, mycli.config.QrcodeMaxCount) + } else { + mycli.loggerWrapper.GetLogger(instanceID).LogInfo("[%s] QR code generated #%d (limit disabled)", instanceID, mycli.qrcodeCount) + } + + // Max-count reached: force logout + teardown + QRTimeout (0 = disabled). + // But never tear down while a passkey ceremony is in flight. + if mycli.config.QrcodeMaxCount > 0 && mycli.qrcodeCount >= mycli.config.QrcodeMaxCount { + if mycli.passkeyCeremony != nil && mycli.passkeyCeremony.HasActiveByInstance(instanceID) { + mycli.loggerWrapper.GetLogger(instanceID).LogInfo("[%s] QR max-count reached but passkey ceremony active — not tearing down", instanceID) + return + } + mycli.loggerWrapper.GetLogger(instanceID).LogWarn("[%s] Maximum QR code count reached (%d), forcing logout and QRTimeout", instanceID, mycli.config.QrcodeMaxCount) + + if mycli.WAClient.IsConnected() { + if err := mycli.WAClient.Logout(context.Background()); err != nil { + mycli.loggerWrapper.GetLogger(instanceID).LogWarn("[%s] Error during forced logout: %v", instanceID, err) + } + } + mycli.teardownQR(fmt.Sprintf("Maximum QR code count (%d) reached", mycli.config.QrcodeMaxCount), true) + return + } + + if mycli.config.LogType != "json" { + fmt.Println("QR code:\n", code) + } + + image, _ := qrcode.Encode(code, qrcode.Medium, 256) + base64qrcode := "data:image/png;base64," + base64.StdEncoding.EncodeToString(image) + base64WithCode := base64qrcode + "|" + code + + if err := mycli.instanceRepository.UpdateQrcode(instanceID, base64WithCode); err != nil { + mycli.loggerWrapper.GetLogger(instanceID).LogError("[%s] Error updating instance: %s", instanceID, err) + } + + postMap := map[string]interface{}{ + "event": "QRCode", + "data": map[string]interface{}{ + "qrcode": base64qrcode, + "code": code, + "count": mycli.qrcodeCount, + "maxCount": mycli.config.QrcodeMaxCount, + }, + "instanceToken": mycli.token, + "instanceId": instanceID, + "instanceName": mycli.Instance.Name, + } + queueName := strings.ToLower(fmt.Sprintf("%s.%s", instanceID, "QRCode")) + if values, err := json.Marshal(postMap); err == nil { + go mycli.service.CallWebhook(mycli.Instance, queueName, values) + if mycli.config.AmqpGlobalEnabled || mycli.config.NatsGlobalEnabled { + go mycli.service.SendToGlobalQueues("QRCode", values, instanceID) + } + } else { + mycli.loggerWrapper.GetLogger(instanceID).LogError("[%s] Failed to marshal JSON for queue", instanceID) + } + + // Rotation timing: first code lives ~60s, subsequent ~20s (whatsmeow native). + timeout := 20 * time.Second + if i == 0 { + timeout = 60 * time.Second + } + time.Sleep(timeout) + } + + // Ran out of codes without a PairSuccess. Treat as QR timeout (mirrors + // GetQRChannel's "timeout") — UNLESS a passkey ceremony is in flight, in + // which case the socket must stay alive for the ceremony to complete. + if mycli.WAClient != nil && mycli.WAClient.Store.ID == nil { + if mycli.passkeyCeremony != nil && mycli.passkeyCeremony.HasActiveByInstance(instanceID) { + mycli.loggerWrapper.GetLogger(instanceID).LogInfo("[%s] QR codes exhausted but passkey ceremony active — keeping socket alive", instanceID) + return + } + mycli.teardownQR("", false) + } + }() +} + +// teardownQR clears the QR state and emits a QRTimeout event, then signals the +// kill channel so StartClient's select loop performs the actual disconnect and +// map cleanup. IMPORTANT: this method must NOT delete from the shared +// clientPointer/myClientPointer/killChannel maps itself — those are unsynchronized +// service-wide maps and this runs in the handleQRCodes goroutine; doing the +// delete()s here (concurrent with other instances' goroutines and the whatsmeow +// dispatch) risks a `fatal error: concurrent map writes`. The kill-channel send +// is blocking (like the original GetQRChannel timeout branch) so the signal is +// never dropped and the socket can't be orphaned. Cleanup happens in the +// StartClient goroutine, the single writer of those maps for this instance. +// If reason is non-empty it is included in the QRTimeout payload (max-count path). +func (mycli *MyClient) teardownQR(reason string, forceLogout bool) { + instanceID := mycli.userID + + if err := mycli.instanceRepository.UpdateQrcode(instanceID, ""); err != nil { + mycli.loggerWrapper.GetLogger(instanceID).LogError("[%s] Error updating instance: %s", instanceID, err) + } + + if reason != "" { + if err := mycli.instanceRepository.UpdateConnected(instanceID, false, reason); err != nil { + mycli.loggerWrapper.GetLogger(instanceID).LogError("[%s] Error updating instance status: %v", instanceID, err) + } + } + + data := map[string]interface{}{} + if reason != "" { + data["reason"] = reason + data["qrcount"] = mycli.qrcodeCount + data["maxCount"] = mycli.config.QrcodeMaxCount + data["forceLogout"] = forceLogout + } + postMap := map[string]interface{}{ + "event": "QRTimeout", + "data": data, + "instanceToken": mycli.token, + "instanceId": instanceID, + "instanceName": mycli.Instance.Name, + } + queueName := strings.ToLower(fmt.Sprintf("%s.%s", instanceID, "QRTimeout")) + if values, err := json.Marshal(postMap); err == nil { + go mycli.service.CallWebhook(mycli.Instance, queueName, values) + if mycli.config.AmqpGlobalEnabled || mycli.config.NatsGlobalEnabled { + go mycli.service.SendToGlobalQueues("QRTimeout", values, instanceID) + } + } + + // Signal StartClient's select loop to disconnect and clean up the shared + // maps (it is the single writer for this instance). Blocking send mirrors + // the original timeout branch so the signal is never dropped. + mycli.loggerWrapper.GetLogger(instanceID).LogWarn("[%s] QR timeout — signaling kill channel", instanceID) + if killChan, exists := mycli.killChannel[instanceID]; exists { + killChan <- true + } +} + +func (mycli *MyClient) myEventHandler(rawEvt interface{}) { + userID := mycli.userID + postMap := make(map[string]interface{}) + postMap["data"] = rawEvt + doWebhook := false + + switch evt := rawEvt.(type) { + case *events.QR: + // New-device pairing emits QR codes here (we connect without GetQRChannel + // so the socket survives a passkey ceremony). Forward + rotate them. + mycli.handleQRCodes(evt.Codes) + return + case *events.AppStateSyncComplete: + if len(mycli.WAClient.Store.PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock { + err := mycli.WAClient.SendPresence(context.Background(), types.PresenceUnavailable) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Failed to send unavailable presence %v", mycli.userID, err) + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Marked self as unavailable", mycli.userID) + } + } + case *events.Connected, *events.PushNameSetting: + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] events.Connected to Whatsapp for user '%s'", mycli.userID, mycli.WAClient.Store.PushName) + if len(mycli.WAClient.Store.PushName) > 0 { + doWebhook = true + postMap["event"] = "Connected" + + if postMap["data"] != nil { + jsonBytes, err := json.Marshal(postMap["data"]) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to marshal postMap['data']: %v", mycli.userID, err) + return + } + + var dataMap map[string]interface{} + err = json.Unmarshal(jsonBytes, &dataMap) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to unmarshal postMap['data'] to map[string]interface{}: %v", mycli.userID, err) + return + } + + postMap["data"] = dataMap + } else { + postMap["data"] = make(map[string]interface{}) + } + + dataMap := postMap["data"].(map[string]interface{}) + + dataMap["status"] = "open" + dataMap["jid"] = mycli.WAClient.Store.ID.String() + dataMap["pushName"] = mycli.WAClient.Store.PushName + + // jid, ok := utils.ParseJID(mycli.WAClient.Store.ID.ToNonAD().User) + // if ok { + // profilePicUrl, err := mycli.clientPointer[mycli.userID].GetProfilePictureInfo(jid, &whatsmeow.GetProfilePictureParams{ + // Preview: false, + // }) + // if err != nil { + // w.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to get profile picture info: %v", mycli.userID, err) + // } else { + // dataMap["profilePicUrl"] = profilePicUrl.URL + // } + // } + + postMap["data"] = dataMap + + // Respect the alwaysOnline instance flag. Previously the device was marked + // online unconditionally on every connect (and the periodic presence job was + // started), which kept the linked device permanently "available". WhatsApp then + // delivers messages to that active session and suppresses push notifications on + // the user's phone. When alwaysOnline is false we now send Unavailable instead. + var err error + if mycli.Instance.AlwaysOnline { + go schedulePresenceUpdates(mycli) + + err = mycli.WAClient.SendPresence(context.Background(), types.PresenceAvailable) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Failed to send available presence %v", mycli.userID, err) + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Marked self as available", mycli.userID) + } + } else { + err = mycli.WAClient.SendPresence(context.Background(), types.PresenceUnavailable) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Failed to send unavailable presence %v", mycli.userID, err) + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Marked self as unavailable (alwaysOnline=false)", mycli.userID) + } + } + + mycli.Instance.Connected = true + mycli.Instance.DisconnectReason = "" + err = mycli.instanceRepository.UpdateConnected(mycli.Instance.Id, mycli.Instance.Connected, mycli.Instance.DisconnectReason) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Error updating instance: %s", mycli.Instance.Id, err) + } + + err = mycli.instanceRepository.UpdateQrcode(mycli.Instance.Id, "") + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Error updating instance: %s", mycli.Instance.Id, err) + } + } + case *events.PairSuccess: + doWebhook = true + postMap["event"] = "PairSuccess" + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("QR Pair Success for user '%s' with JID '%s' - '%s'", mycli.userID, evt.ID.String(), mycli.WAClient.Store.ID.String()) + + instance, err := mycli.instanceRepository.GetInstanceByID(mycli.userID) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Error getting instance: %s", mycli.userID, err) + } + + instance.Qrcode = "" + instance.Connected = true + instance.DisconnectReason = "" + instance.Jid = mycli.WAClient.Store.ID.String() + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Updating JID: %s in Instance: %s", mycli.userID, mycli.WAClient.Store.ID.String(), instance.Jid) + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Attempting to update instance in DB: %+v", mycli.userID, instance) + err = mycli.instanceRepository.Update(instance) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Error updating instance: %s", mycli.userID, err) + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Instance successfully updated", mycli.userID) + } + + var userInfo Values + found, _ := mycli.userInfoCache.Get(mycli.token, &userInfo) + + if !found { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] No user info cached on pairing?", mycli.userID) + } else { + txtid := userInfo.Get("Id") + token := userInfo.Get("Token") + + updatedUserInfo := utils.UpdateUserInfo(userInfo, "Jid", evt.ID.String()) + + _ = mycli.userInfoCache.Set(token, updatedUserInfo, 0) + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] User information set for user '%s'", mycli.userID, txtid) + } + + if postMap["data"] != nil { + jsonBytes, err := json.Marshal(postMap["data"]) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to marshal postMap['data']: %v", mycli.userID, err) + return + } + + var dataMap map[string]interface{} + err = json.Unmarshal(jsonBytes, &dataMap) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to unmarshal postMap['data'] to map[string]interface{}: %v", mycli.userID, err) + return + } + + postMap["data"] = dataMap + } else { + postMap["data"] = make(map[string]interface{}) + } + + dataMap := postMap["data"].(map[string]interface{}) + + dataMap["status"] = "open" + dataMap["jid"] = mycli.WAClient.Store.ID.String() + + if mycli.WAClient.Store.PushName != "" { + dataMap["pushName"] = mycli.WAClient.Store.PushName + } + + postMap["data"] = dataMap + + // Pairing succeeded — tear down any pending passkey ceremony for this instance. + mycli.passkeyCeremony.Clear(mycli.userID) + case *events.PairPasskeyRequest: + // The server demands a WebAuthn passkey to finish linking. We CANNOT + // produce the assertion here (it needs the user's authenticator on the + // web.whatsapp.com origin) — we only forward the challenge. The browser + // extension (tools/passkey-helper) runs navigator.credentials.get() and + // POSTs the assertion back to /passkey-ceremony/{token}/response, which + // is where SendPasskeyResponse is actually called. + doWebhook = true + postMap["event"] = "PasskeyRequest" + + pkJSON, err := json.Marshal(evt.PublicKey) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to marshal passkey publicKey: %v", mycli.userID, err) + mycli.passkeyCeremony.SetError(mycli.userID, "failed to encode passkey challenge") + return + } + + token := mycli.passkeyCeremony.Start(mycli.userID, pkJSON) + + // Build the #wapk payload the extension consumes: base64url({t,b}). + // `b` must be the PUBLICLY reachable API base the browser can hit + // (a tunnel / LAN IP in dev) — set PASSKEY_PUBLIC_URL to that base. + publicBase := os.Getenv("PASSKEY_PUBLIC_URL") + if publicBase == "" { + publicBase = "" + } + payload := fmt.Sprintf(`{"t":%q,"b":%q}`, token, publicBase) + wapk := base64.RawURLEncoding.EncodeToString([]byte(payload)) + openURL := "https://web.whatsapp.com/#wapk=" + wapk + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo( + "[%s] Passkey required. Open this URL in a browser with the AgentDeck Passkey Helper extension:\n%s\n(ceremony token=%s, base=%s)", + mycli.userID, openURL, token, publicBase, + ) + + // Surface the ceremony info to webhooks/queues so the manager UI can + // render the "Abrir WhatsApp Web" button. + postMap["data"] = map[string]interface{}{ + "ceremonyToken": token, + "openUrl": openURL, + "stage": "challenge", + } + case *events.PairPasskeyConfirmation: + // The server returned a confirmation code. Per DOC2 §4.2 we NEVER + // auto-confirm on SkipHandoffUX — we always force skipHandoffUX=false so + // the extension shows the manual "Confirmar" button, and the actual + // SendPasskeyConfirmation happens from /passkey-ceremony/{token}/confirm. + doWebhook = true + postMap["event"] = "PasskeyConfirmation" + mycli.passkeyCeremony.SetConfirmation(mycli.userID, evt.Code, false) + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo( + "[%s] Passkey confirmation code=%s (skipHandoffUX from server=%v, forced to manual)", + mycli.userID, evt.Code, evt.SkipHandoffUX, + ) + postMap["data"] = map[string]interface{}{ + "code": evt.Code, + "stage": "confirmation", + } + case *events.PairPasskeyError: + doWebhook = true + postMap["event"] = "PasskeyError" + msg := "unknown passkey error" + if evt.Error != nil { + msg = evt.Error.Error() + } + mycli.passkeyCeremony.SetError(mycli.userID, msg) + mycli.loggerWrapper.GetLogger(mycli.userID).LogError( + "[%s] Passkey pairing error (continuation=%v): %s", mycli.userID, evt.Continuation, msg, + ) + postMap["data"] = map[string]interface{}{ + "error": msg, + "stage": "error", + } + case *events.StreamReplaced: + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Received StreamReplaced event", mycli.userID) + return + case *events.TemporaryBan: + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] User received temporary ban for %s", mycli.userID, evt.Code.String()) + doWebhook = true + postMap["event"] = "TemporaryBan" + + if postMap["data"] != nil { + jsonBytes, err := json.Marshal(postMap["data"]) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to marshal postMap['data']: %v", mycli.userID, err) + return + } + + var dataMap map[string]interface{} + err = json.Unmarshal(jsonBytes, &dataMap) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to unmarshal postMap['data'] to map[string]interface{}: %v", mycli.userID, err) + return + } + + postMap["data"] = dataMap + } else { + postMap["data"] = make(map[string]interface{}) + } + + dataMap := postMap["data"].(map[string]interface{}) + + dataMap["reason"] = evt.Code.String() + dataMap["expire"] = evt.Expire + + postMap["data"] = dataMap + case *events.Message: + doWebhook = true + postMap["event"] = "Message" + // Message received + + // Log message arrival with detailed info + messageSize := "unknown" + if evt.Message.GetDocumentMessage() != nil && evt.Message.GetDocumentMessage().FileLength != nil { + messageSize = fmt.Sprintf("%d bytes", *evt.Message.GetDocumentMessage().FileLength) + } else if evt.Message.GetVideoMessage() != nil && evt.Message.GetVideoMessage().FileLength != nil { + messageSize = fmt.Sprintf("%d bytes", *evt.Message.GetVideoMessage().FileLength) + } else if evt.Message.GetImageMessage() != nil && evt.Message.GetImageMessage().FileLength != nil { + messageSize = fmt.Sprintf("%d bytes", *evt.Message.GetImageMessage().FileLength) + } else if evt.Message.GetAudioMessage() != nil && evt.Message.GetAudioMessage().FileLength != nil { + messageSize = fmt.Sprintf("%d bytes", *evt.Message.GetAudioMessage().FileLength) + } + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] ===== MESSAGE RECEIVED ===== ID: %s, From: %s, Type: %s, Size: %s", mycli.userID, evt.Info.ID, evt.Info.Chat.String(), evt.Info.Type, messageSize) + + // se readMessages for true ele marca como lida + if mycli.Instance.ReadMessages { + messageIDs := []string{evt.Info.ID} + err := mycli.WAClient.MarkRead(context.Background(), messageIDs, time.Now(), evt.Info.Sender, evt.Info.Sender) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to auto-mark message as read: %v", mycli.userID, err) + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Auto-marked message as read from %s", mycli.userID, evt.Info.Chat.String()) + } + } + + // se ignoreStatus for true e o chat for broadcast ou o id for broadcast retorna + if mycli.Instance.IgnoreStatus && (strings.Contains(evt.Info.Chat.String(), "@broadcast") || strings.Contains(evt.Info.ID, "@broadcast")) { + return + } + + // se ignoreGroup for true e o chat for grupo retorna + if mycli.Instance.IgnoreGroups && strings.Contains(evt.Info.Chat.String(), "@g.us") { + return + } + + // Verifica advanced settings para ignorar grupos + if (mycli.config.EventIgnoreGroup || mycli.Instance.IgnoreGroups) && strings.Contains(evt.Info.Chat.String(), "@g.us") { + return + } + + // Verifica advanced settings para ignorar status/broadcast + if (mycli.config.EventIgnoreStatus || mycli.Instance.IgnoreStatus) && (strings.Contains(evt.Info.Chat.String(), "@broadcast") || strings.Contains(evt.Info.ID, "@broadcast")) { + return + } + + // Trata o caso especial onde Sender é @lid e SenderAlt é @s.whatsapp.net + // Neste caso, devemos inverter: Sender e Chat devem ser @s.whatsapp.net, SenderAlt deve ser @lid + senderStr := evt.Info.Sender.String() + senderAltStr := evt.Info.SenderAlt.String() + chatStr := evt.Info.Chat.String() + + if strings.Contains(senderStr, "@lid") && strings.Contains(senderAltStr, "@s.whatsapp.net") { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Detected LID/WhatsApp JID swap case - Sender: %s, SenderAlt: %s", mycli.userID, senderStr, senderAltStr) + + // Limpa os IDs antes de fazer a troca + cleanSenderAlt := cleanSenderID(senderAltStr) + cleanSender := cleanSenderID(senderStr) + + // Inverte: Sender e Chat recebem o @s.whatsapp.net, SenderAlt recebe o @lid + if cleanedWhatsAppJID, err := types.ParseJID(cleanSenderAlt); err == nil { + evt.Info.Sender = cleanedWhatsAppJID + // Se Chat também é @lid, atualiza para @s.whatsapp.net + if strings.Contains(chatStr, "@lid") { + evt.Info.Chat = cleanedWhatsAppJID + } + } + + if cleanedLID, err := types.ParseJID(cleanSender); err == nil { + evt.Info.SenderAlt = cleanedLID + } + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] JID swap completed - New Sender: %s, New SenderAlt: %s, New Chat: %s", + mycli.userID, evt.Info.Sender.String(), evt.Info.SenderAlt.String(), evt.Info.Chat.String()) + } else { + // Comportamento normal: apenas limpa os IDs + cleanSender := cleanSenderID(senderStr) + if cleanedJID, err := types.ParseJID(cleanSender); err == nil { + evt.Info.Sender = cleanedJID + } + + cleanSenderAlt := cleanSenderID(senderAltStr) + if cleanedLID, err := types.ParseJID(cleanSenderAlt); err == nil { + evt.Info.SenderAlt = cleanedLID + } + } + + // Auto-marca mensagens como lidas se configurado + if mycli.Instance.ReadMessages && !evt.Info.IsFromMe { + go func() { + time.Sleep(1 * time.Second) // Pequeno delay para parecer mais natural + err := mycli.WAClient.MarkRead(context.Background(), []types.MessageID{evt.Info.ID}, evt.Info.Timestamp, evt.Info.Chat, evt.Info.Sender) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to auto-mark message as read: %v", mycli.userID, err) + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Auto-marked message as read from %s", mycli.userID, evt.Info.Chat.String()) + } + }() + } + + parsedMessageType := utils.GetMessageType(evt.Message) + if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Message ignored because it's a unknown protocol message", mycli.userID) + return + } + + if postMap["data"] != nil { + jsonBytes, err := json.Marshal(postMap["data"]) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to marshal postMap['data']: %v", mycli.userID, err) + return + } + + var dataMap map[string]interface{} + err = json.Unmarshal(jsonBytes, &dataMap) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to unmarshal postMap['data'] to map[string]interface{}: %v", mycli.userID, err) + return + } + + postMap["data"] = dataMap + } else { + postMap["data"] = make(map[string]interface{}) + } + + dataMap, ok := postMap["data"].(map[string]interface{}) + if !ok { + dataMap = make(map[string]interface{}) + } + + referral := extractReferralFromMessage(evt.Message) + + if evt.Message.GetPollUpdateMessage() != nil { + fmt.Printf("[POLL DEBUG] 🎯 PollUpdateMessage detected!\n") + fmt.Printf("[POLL DEBUG] � BEFORE accessing evt.Info - Sender: %s, Server: %s\n", evt.Info.Sender.String(), evt.Info.Sender.Server) + fmt.Printf("[POLL DEBUG] 📍 BEFORE accessing evt.Info - SenderAlt: %s\n", evt.Info.SenderAlt.String()) + fmt.Printf("[POLL DEBUG] �� mycli.WAClient is nil: %v\n", mycli.WAClient == nil) + if mycli.WAClient != nil { + fmt.Printf("[POLL DEBUG] ✅ mycli.WAClient is initialized: %s\n", mycli.WAClient.Store.ID) + } + + decrypted, err := mycli.clientPointer[mycli.userID].DecryptPollVote(context.Background(), evt) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to decrypt vote: %v", mycli.userID, err) + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Selected options in decrypted vote:", mycli.userID) + for _, option := range decrypted.SelectedOptions { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("- %X", option) + + } + + // NOVO: Salvar voto no banco de dados de forma NÃO-INVASIVA + if mycli.pollService != nil { + go func() { + defer func() { + if r := recover(); r != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Panic ao salvar voto: %v", mycli.userID, r) + } + }() + + pollKey := evt.Message.GetPollUpdateMessage().GetPollCreationMessageKey() + if pollKey == nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] PollCreationMessageKey not found", mycli.userID) + return + } + + pollInfo := &types.MessageInfo{ + ID: pollKey.GetID(), + MessageSource: types.MessageSource{ + Chat: evt.Info.Chat, // Usar o chat do evento atual + }, + } + + // Construir modelo de voto usando helper seguro + // evt.Info já passou pelo JID swap, então Sender = número real + pollVote := poll_service.BuildPollVoteFromEvent( + pollInfo, + &evt.Info, + decrypted, + "", // CompanyID não disponível no MyClient, será vazio + mycli.Instance.Id, + ) + + // Salvar no banco com timeout de segurança + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := mycli.pollService.SavePollVote(ctx, pollVote); err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to save poll vote to database: %v", mycli.userID, err) + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Poll vote saved to database successfully", mycli.userID) + } + }() + } + } + } + + var quotedMessage *waE2E.Message + var stanzaID string + + if evt.Message.GetExtendedTextMessage() != nil { + quotedMessage = evt.Message.GetExtendedTextMessage().GetContextInfo().GetQuotedMessage() + stanzaID = evt.Message.GetExtendedTextMessage().GetContextInfo().GetStanzaID() + } else if evt.Message.GetImageMessage() != nil { + quotedMessage = evt.Message.GetImageMessage().GetContextInfo().GetQuotedMessage() + stanzaID = evt.Message.GetImageMessage().GetContextInfo().GetStanzaID() + } else if evt.Message.GetAudioMessage() != nil { + quotedMessage = evt.Message.GetAudioMessage().GetContextInfo().GetQuotedMessage() + stanzaID = evt.Message.GetAudioMessage().GetContextInfo().GetStanzaID() + } else if evt.Message.GetDocumentMessage() != nil { + quotedMessage = evt.Message.GetDocumentMessage().GetContextInfo().GetQuotedMessage() + stanzaID = evt.Message.GetDocumentMessage().GetContextInfo().GetStanzaID() + } else if evt.Message.GetVideoMessage() != nil { + quotedMessage = evt.Message.GetVideoMessage().GetContextInfo().GetQuotedMessage() + stanzaID = evt.Message.GetVideoMessage().GetContextInfo().GetStanzaID() + } + + if stanzaID != "" && quotedMessage != nil { + quotedMap := make(map[string]interface{}) + + quotedMap["stanzaID"] = stanzaID + quotedMap["quotedMessage"] = quotedMessage + + dataMap["quoted"] = quotedMap + dataMap["isQuoted"] = true + } + + if len(referral) > 0 { + dataMap["referral"] = referral + } + + if mycli.config.WebhookFiles { + isMedia := false + + img := evt.Message.GetImageMessage() + audio := evt.Message.GetAudioMessage() + document := evt.Message.GetDocumentMessage() + video := evt.Message.GetVideoMessage() + sticker := evt.Message.GetStickerMessage() + + // Check for associated child messages (like media in replies) + var associatedImg *waE2E.ImageMessage + var associatedAudio *waE2E.AudioMessage + var associatedDocument *waE2E.DocumentMessage + var associatedVideo *waE2E.VideoMessage + var associatedSticker *waE2E.StickerMessage + + if evt.Message.GetAssociatedChildMessage() != nil { + childMsg := evt.Message.GetAssociatedChildMessage().GetMessage() + if childMsg != nil { + associatedImg = childMsg.GetImageMessage() + associatedAudio = childMsg.GetAudioMessage() + associatedDocument = childMsg.GetDocumentMessage() + associatedVideo = childMsg.GetVideoMessage() + associatedSticker = childMsg.GetStickerMessage() + } + } + + if img != nil || audio != nil || document != nil || video != nil || sticker != nil || + associatedImg != nil || associatedAudio != nil || associatedDocument != nil || + associatedVideo != nil || associatedSticker != nil { + isMedia = true + } + + if isMedia { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Processing media message - ID: %s", mycli.userID, evt.Info.ID) + + var data []byte + var err error + var extension string + var mimeType string + var mediaSize int64 + + // Create context with timeout for large files + downloadCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + downloadStart := time.Now() + + // Handle regular media messages + if img != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Downloading image - ID: %s", mycli.userID, evt.Info.ID) + data, err = mycli.WAClient.Download(downloadCtx, img) + extension = ".jpg" + mimeType = "image/jpeg" + if img.FileLength != nil { + mediaSize = int64(*img.FileLength) + } + } else if audio != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Downloading audio - ID: %s", mycli.userID, evt.Info.ID) + data, err = mycli.WAClient.Download(downloadCtx, audio) + extension = ".ogg" + mimeType = "audio/ogg" + if audio.FileLength != nil { + mediaSize = int64(*audio.FileLength) + } + } else if document != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Downloading document - ID: %s, FileName: %s, Size: %d bytes", mycli.userID, evt.Info.ID, document.GetFileName(), document.GetFileLength()) + data, err = mycli.WAClient.Download(downloadCtx, document) + extension = getExtensionFromMimeType(document.GetMimetype()) + mimeType = document.GetMimetype() + if document.FileLength != nil { + mediaSize = int64(*document.FileLength) + } + } else if video != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Downloading video - ID: %s, Size: %d bytes", mycli.userID, evt.Info.ID, video.GetFileLength()) + data, err = mycli.WAClient.Download(downloadCtx, video) + extension = ".mp4" + mimeType = "video/mp4" + if video.FileLength != nil { + mediaSize = int64(*video.FileLength) + } + } else if sticker != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Downloading sticker - ID: %s", mycli.userID, evt.Info.ID) + data, err = mycli.WAClient.Download(downloadCtx, sticker) + extension = ".png" + mimeType = "image/png" + if sticker.FileLength != nil { + mediaSize = int64(*sticker.FileLength) + } + + if err == nil { + webpReader := bytes.NewReader(data) + img, decErr := webp.Decode(webpReader) + if decErr != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Failed to decode webp sticker, keeping raw webp: %v", mycli.userID, decErr) + extension = ".webp" + mimeType = "image/webp" + } else { + var pngBuffer bytes.Buffer + if encErr := png.Encode(&pngBuffer, img); encErr != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Failed to encode png from sticker, keeping raw webp: %v", mycli.userID, encErr) + extension = ".webp" + mimeType = "image/webp" + } else { + data = pngBuffer.Bytes() + } + } + } + // Handle associated child media messages + } else if associatedImg != nil { + data, err = mycli.WAClient.Download(context.Background(), associatedImg) + extension = ".jpg" + mimeType = "image/jpeg" + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Processing associated child image message", mycli.userID) + } else if associatedAudio != nil { + data, err = mycli.WAClient.Download(context.Background(), associatedAudio) + extension = ".ogg" + mimeType = "audio/ogg" + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Processing associated child audio message", mycli.userID) + } else if associatedDocument != nil { + data, err = mycli.WAClient.Download(context.Background(), associatedDocument) + extension = getExtensionFromMimeType(associatedDocument.GetMimetype()) + mimeType = associatedDocument.GetMimetype() + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Processing associated child document message", mycli.userID) + } else if associatedVideo != nil { + data, err = mycli.WAClient.Download(context.Background(), associatedVideo) + extension = ".mp4" + mimeType = "video/mp4" + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Processing associated child video message", mycli.userID) + } else if associatedSticker != nil { + data, err = mycli.WAClient.Download(context.Background(), associatedSticker) + extension = ".png" + mimeType = "image/png" + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Processing associated child sticker message", mycli.userID) + + if err == nil { + webpReader := bytes.NewReader(data) + img, decErr := webp.Decode(webpReader) + if decErr != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Failed to decode webp sticker, keeping raw webp: %v", mycli.userID, decErr) + extension = ".webp" + mimeType = "image/webp" + } else { + var pngBuffer bytes.Buffer + if encErr := png.Encode(&pngBuffer, img); encErr != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Failed to encode png from associated sticker, keeping raw webp: %v", mycli.userID, encErr) + extension = ".webp" + mimeType = "image/webp" + } else { + data = pngBuffer.Bytes() + } + } + } + } + + downloadDuration := time.Since(downloadStart) + + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to download media - ID: %s, Size: %d bytes, Duration: %v, Error: %v", mycli.userID, evt.Info.ID, mediaSize, downloadDuration, err) + + // Check if it's a timeout error + if downloadCtx.Err() == context.DeadlineExceeded { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Download timeout exceeded (5 minutes) for large file - ID: %s, Size: %d bytes", mycli.userID, evt.Info.ID, mediaSize) + } + + // Don't return here - continue processing the message without media + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Continuing message processing without media download - ID: %s", mycli.userID, evt.Info.ID) + } else { + actualSize := len(data) + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Media download successful - ID: %s, Expected: %d bytes, Actual: %d bytes, Duration: %v", mycli.userID, evt.Info.ID, mediaSize, actualSize, downloadDuration) + + // Check for size mismatch + if mediaSize > 0 && int64(actualSize) != mediaSize { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Size mismatch detected - ID: %s, Expected: %d, Got: %d", mycli.userID, evt.Info.ID, mediaSize, actualSize) + } + + // Log large file processing + if actualSize > 13*1024*1024 { // 13MB + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Processing large file (>13MB) - ID: %s, Size: %d bytes", mycli.userID, evt.Info.ID, actualSize) + } + } + + messageMap, ok := dataMap["Message"].(map[string]interface{}) + if !ok { + messageMap = make(map[string]interface{}) + } + + // Only process storage if download was successful + if err == nil && len(data) > 0 { + if mycli.config.MinioEnabled { + fileName := evt.Info.ID + extension + storageStart := time.Now() + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Uploading to S3/Minio - ID: %s, FileName: %s, Size: %d bytes", mycli.userID, evt.Info.ID, fileName, len(data)) + + mediaURL, err := mycli.mediaStorage.Store(context.Background(), data, fileName, mimeType) + storageDuration := time.Since(storageStart) + + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to store media in S3/Minio - ID: %s, Size: %d bytes, Duration: %v, Error: %v", mycli.userID, evt.Info.ID, len(data), storageDuration, err) + + // Continue processing without storage URL + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Continuing message processing without S3 URL - ID: %s", mycli.userID, evt.Info.ID) + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] S3/Minio upload successful - ID: %s, Size: %d bytes, Duration: %v, URL: %s", mycli.userID, evt.Info.ID, len(data), storageDuration, mediaURL) + messageMap["mediaUrl"] = mediaURL + messageMap["mimetype"] = mimeType + } + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Encoding to base64 - ID: %s, Size: %d bytes", mycli.userID, evt.Info.ID, len(data)) + encodeStart := time.Now() + + encodeData := base64.StdEncoding.EncodeToString(data) + encodeDuration := time.Since(encodeStart) + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Base64 encoding completed - ID: %s, Original: %d bytes, Encoded: %d chars, Duration: %v", mycli.userID, evt.Info.ID, len(data), len(encodeData), encodeDuration) + messageMap["base64"] = encodeData + } + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Skipping media storage due to download failure - ID: %s", mycli.userID, evt.Info.ID) + } + + dataMap["Message"] = messageMap + } + } + + isGroup := strings.HasSuffix(evt.Info.Chat.String(), "@g.us") + if isGroup { + groupData, err := mycli.WAClient.GetGroupInfo(context.Background(), evt.Info.Chat) + if err == nil { + dataMap["groupData"] = groupData + } + } + + delete(dataMap, "RawMessage") + + if message, ok := dataMap["Message"].(map[string]interface{}); ok { + if imageMessage, ok := message["imageMessage"].(map[string]interface{}); ok { + delete(imageMessage, "JPEGThumbnail") + message["imageMessage"] = imageMessage + dataMap["Message"] = message + } + + if videoMessage, ok := message["videoMessage"].(map[string]interface{}); ok { + delete(videoMessage, "JPEGThumbnail") + message["videoMessage"] = videoMessage + dataMap["Message"] = message + } + + if documentMessage, ok := message["documentMessage"].(map[string]interface{}); ok { + delete(documentMessage, "JPEGThumbnail") + message["documentMessage"] = documentMessage + dataMap["Message"] = message + } + } + + postMap["data"] = dataMap + + if mycli.config.DatabaseSaveMessages { + message := message_model.Message{ + MessageID: evt.Info.ID, + Timestamp: evt.Info.Timestamp.Format("2006-01-02 15:04:05"), + Status: "Received", + Source: evt.Info.Chat.ToNonAD().User, + Referral: referral, + } + + mycli.persistMessageAsync(message) + } + + // ===== BUTTON CLICK EVENT DETECTION ===== + // Detecta cliques em botões e emite evento separado "ButtonClick" + // Suporta 3 formatos: ButtonsResponseMessage, InteractiveResponseMessage (NativeFlow), TemplateButtonReplyMessage + var buttonClickData map[string]interface{} + + if resp := evt.Message.GetButtonsResponseMessage(); resp != nil { + // Legacy buttons response + buttonClickData = map[string]interface{}{ + "buttonId": resp.GetSelectedButtonID(), + "buttonText": resp.GetSelectedDisplayText(), + "type": "buttons_response", + } + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Button click detected (legacy): buttonId=%s, buttonText=%s", mycli.userID, resp.GetSelectedButtonID(), resp.GetSelectedDisplayText()) + } else if resp := evt.Message.GetInteractiveResponseMessage(); resp != nil { + // NativeFlow interactive response (quick_reply, cta_url, cta_call, cta_copy) + if nf := resp.GetNativeFlowResponseMessage(); nf != nil { + buttonId := "" + buttonText := "" + // Parse paramsJSON to extract id and display_text + if nf.GetParamsJSON() != "" { + var params map[string]interface{} + if err := json.Unmarshal([]byte(nf.GetParamsJSON()), ¶ms); err == nil { + if id, ok := params["id"].(string); ok { + buttonId = id + } + if dt, ok := params["display_text"].(string); ok { + buttonText = dt + } + } + } + buttonClickData = map[string]interface{}{ + "buttonId": buttonId, + "buttonText": buttonText, + "type": "native_flow_response", + "name": nf.GetName(), + "paramsJSON": nf.GetParamsJSON(), + } + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Button click detected (native_flow): name=%s, buttonId=%s, buttonText=%s", mycli.userID, nf.GetName(), buttonId, buttonText) + } + } else if resp := evt.Message.GetTemplateButtonReplyMessage(); resp != nil { + // Template button reply + buttonClickData = map[string]interface{}{ + "buttonId": resp.GetSelectedID(), + "buttonText": resp.GetSelectedDisplayText(), + "type": "template_button_reply", + } + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Button click detected (template): buttonId=%s, buttonText=%s", mycli.userID, resp.GetSelectedID(), resp.GetSelectedDisplayText()) + } else if resp := evt.Message.GetListResponseMessage(); resp != nil { + // List response (single select) + buttonClickData = map[string]interface{}{ + "buttonId": resp.GetSingleSelectReply().GetSelectedRowID(), + "buttonText": resp.GetTitle(), + "type": "list_response", + "description": resp.GetDescription(), + } + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] List selection detected: rowId=%s, title=%s", mycli.userID, resp.GetSingleSelectReply().GetSelectedRowID(), resp.GetTitle()) + } + + // Se detectou clique em botão, emite evento separado "ButtonClick" + if buttonClickData != nil { + buttonClickMap := map[string]interface{}{ + "event": "ButtonClick", + "data": map[string]interface{}{ + "buttonId": buttonClickData["buttonId"], + "buttonText": buttonClickData["buttonText"], + "type": buttonClickData["type"], + "phone": dataMap["Sender"], + "jid": dataMap["Sender"], + "pushName": dataMap["PushName"], + "messageId": dataMap["ID"], + "chat": dataMap["Chat"], + "fromMe": dataMap["FromMe"], + "timestamp": evt.Info.Timestamp.Unix(), + "extraData": buttonClickData, + }, + "instanceToken": mycli.token, + "instanceId": mycli.userID, + "instanceName": mycli.Instance.Name, + } + + buttonClickJSON, err := json.Marshal(buttonClickMap) + if err == nil { + buttonClickQueue := strings.ToLower(fmt.Sprintf("%s.buttonclick", userID)) + go mycli.service.CallWebhook(mycli.Instance, buttonClickQueue, buttonClickJSON) + if mycli.config.AmqpGlobalEnabled || mycli.config.NatsGlobalEnabled { + go mycli.service.SendToGlobalQueues("ButtonClick", buttonClickJSON, mycli.userID) + } + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] ===== BUTTON CLICK EVENT DISPATCHED ===== Type: %s, ButtonId: %s", mycli.userID, buttonClickData["type"], buttonClickData["buttonId"]) + } + } + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] ===== MESSAGE PROCESSING COMPLETED ===== ID: %s, From: %s, Type: %s, Webhook: %v", mycli.userID, evt.Info.ID, evt.Info.Chat.String(), evt.Info.Type, doWebhook) + case *events.Receipt: + doWebhook = true + postMap["event"] = "Receipt" + + // se ignoreGroup for true e o chat for grupo retorna + if mycli.Instance.IgnoreGroups && strings.Contains(evt.Chat.String(), "@g.us") { + return + } + + if mycli.config.EventIgnoreGroup && strings.Contains(evt.Chat.String(), "@g.us") { + return + } + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Receipt received with ID: %s from %s with type %s", mycli.userID, evt.MessageIDs[0], evt.SourceString(), evt.Type) + + if evt.Type == types.ReceiptTypeRead || evt.Type == types.ReceiptTypeReadSelf { + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Message was read by %s", mycli.userID, evt.SourceString()) + if evt.Type == types.ReceiptTypeRead { + postMap["state"] = "Read" + for _, v := range evt.MessageIDs { + messageKey := fmt.Sprintf("%s_%s_%s", mycli.userID, v, "Read") + if found, _ := mycli.processedMessages.Get(messageKey, nil); found { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Message duplicated ignored: %s", mycli.userID, v) + continue + } + + _ = mycli.processedMessages.Set(messageKey, true, 30*time.Minute) + + var message message_model.Message + + message.MessageID = v + message.Timestamp = evt.Timestamp.Format("2006-01-02 15:04:05") + message.Status = "Read" + message.Source = evt.Chat.ToNonAD().User + + if mycli.config.DatabaseSaveMessages { + mycli.persistMessageAsync(message) + } + } + } else { + postMap["state"] = "ReadSelf" + } + } else if evt.Type == types.ReceiptTypeDelivered { + postMap["state"] = "Delivered" + + var message message_model.Message + + message.MessageID = evt.MessageIDs[0] + message.Timestamp = evt.Timestamp.Format("2006-01-02 15:04:05") + message.Status = "Delivered" + message.Source = evt.Chat.ToNonAD().User + + messageKey := fmt.Sprintf("%s_%s_%s", mycli.userID, evt.MessageIDs[0], "Delivered") + if found, _ := mycli.processedMessages.Get(messageKey, nil); found { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Message duplicated ignored: %s", mycli.userID, evt.MessageIDs[0]) + return + } + + _ = mycli.processedMessages.Set(messageKey, true, 30*time.Minute) + + if mycli.config.DatabaseSaveMessages { + mycli.persistMessageAsync(message) + } + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Message delivered to %s", mycli.userID, evt.SourceString()) + } else { + return + } + case *events.Presence: + doWebhook = true + postMap["event"] = "Presence" + + if evt.Unavailable { + postMap["state"] = "offline" + if evt.LastSeen.IsZero() { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] User is now offline", mycli.userID) + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] User is now offline since %s", mycli.userID, evt.LastSeen.Format("2006-01-02 15:04:05")) + } + } else { + postMap["state"] = "online" + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] User is now online", mycli.userID) + } + case *events.Archive: + doWebhook = true + postMap["event"] = "Archive" + + dataMap := postMap["data"].(map[string]interface{}) + dataMap["JID"] = evt.JID + dataMap["Timestamp"] = evt.Timestamp + dataMap["Action"] = evt.Action + dataMap["FromFullSync"] = evt.FromFullSync + postMap["data"] = dataMap + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Chat archived", mycli.userID) + case *events.HistorySync: + doWebhook = true + postMap["event"] = "HistorySync" + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] History sync event received %+v", mycli.userID, evt.Data.SyncType) + case *events.AppState: + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] App state event received %+v", mycli.userID, evt) + case *events.LoggedOut: + doWebhook = true + postMap["event"] = "LoggedOut" + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Logged out for reason %s", mycli.userID, evt.Reason.String()) + + // Limpar cache de userInfo para esta instância + mycli.userInfoCache.Delete(mycli.Instance.Token) + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] UserInfo cache cleared for token: %s", mycli.userID, mycli.Instance.Token) + + mycli.Instance.DisconnectReason = evt.Reason.String() + mycli.Instance.Connected = false + err := mycli.instanceRepository.UpdateConnected(mycli.Instance.Id, mycli.Instance.Connected, mycli.Instance.DisconnectReason) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Error updating instance: %s", mycli.Instance.Id, err) + } + + if postMap["data"] != nil { + jsonBytes, err := json.Marshal(postMap["data"]) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to marshal postMap['data']: %v", mycli.userID, err) + return + } + + var dataMap map[string]interface{} + err = json.Unmarshal(jsonBytes, &dataMap) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to unmarshal postMap['data'] to map[string]interface{}: %v", mycli.userID, err) + return + } + + postMap["data"] = dataMap + } else { + postMap["data"] = make(map[string]interface{}) + } + + dataMap := postMap["data"].(map[string]interface{}) + + dataMap["reason"] = evt.Reason.String() + + // Enviar evento LoggedOut para webhook/RabbitMQ ANTES de matar o canal + postMap["instanceToken"] = mycli.Instance.Token + postMap["instanceId"] = mycli.userID + postMap["instanceName"] = mycli.Instance.Name + + values, err := json.Marshal(postMap) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to marshal JSON for LoggedOut event", mycli.userID) + } else { + var queueName string + if _, ok := postMap["event"]; ok { + queueName = strings.ToLower(fmt.Sprintf("%s.%s", mycli.userID, postMap["event"])) + } + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] ===== DISPATCHING LOGGEDOUT EVENT ===== Queue: %s", mycli.userID, queueName) + + // Enviar para webhook/RabbitMQ + go mycli.service.CallWebhook(mycli.Instance, queueName, values) + + if mycli.config.AmqpGlobalEnabled || mycli.config.NatsGlobalEnabled { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Sending LoggedOut to global queues - AMQP: %v, NATS: %v", mycli.userID, mycli.config.AmqpGlobalEnabled, mycli.config.NatsGlobalEnabled) + go mycli.service.SendToGlobalQueues(postMap["event"].(string), values, mycli.userID) + } + } + + // Agora mata o canal DEPOIS de enviar o evento + mycli.killChannel[mycli.userID] <- true + case *events.ChatPresence: + doWebhook = true + postMap["event"] = "ChatPresence" + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Chat presence received %+v", mycli.userID, evt) + case *events.CallOffer: + doWebhook = true + postMap["event"] = "CallOffer" + + // Verifica se deve rejeitar chamadas automaticamente + if mycli.Instance.RejectCall { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Auto-rejecting call from %s", mycli.userID, evt.CallCreator.String()) + + // Rejeita a chamada + mycli.WAClient.RejectCall(context.Background(), evt.CallCreator, evt.CallID) + + // Envia mensagem de rejeição se configurada + if mycli.Instance.MsgRejectCall != "" { + msg := &waE2E.Message{ + ExtendedTextMessage: &waE2E.ExtendedTextMessage{ + Text: &mycli.Instance.MsgRejectCall, + }, + } + + _, err := mycli.WAClient.SendMessage(context.Background(), evt.CallCreator, msg) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to send reject call message: %v", mycli.userID, err) + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Sent reject call message to %s", mycli.userID, evt.CallCreator.String()) + } + } + return + } + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Got call offer %+v", mycli.userID, evt) + case *events.CallAccept: + doWebhook = true + postMap["event"] = "CallAccept" + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Got call accept %+v", mycli.userID, evt) + case *events.CallTerminate: + doWebhook = true + postMap["event"] = "CallTerminate" + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Got call terminate %+v", mycli.userID, evt) + case *events.CallOfferNotice: + doWebhook = true + postMap["event"] = "CallOfferNotice" + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Got call offer notice %+v", mycli.userID, evt) + case *events.CallRelayLatency: + doWebhook = true + postMap["event"] = "CallRelayLatency" + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Got call relay latency %+v", mycli.userID, evt) + case *events.OfflineSyncCompleted: + doWebhook = true + postMap["event"] = "OfflineSyncCompleted" + case *events.ConnectFailure: + doWebhook = true + postMap["event"] = "ConnectFailure" + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Connection failed with reason %s", mycli.userID, evt.Reason.String()) + + // Limpar cache de userInfo para esta instância + mycli.userInfoCache.Delete(mycli.Instance.Token) + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] UserInfo cache cleared for token: %s", mycli.userID, mycli.Instance.Token) + + mycli.Instance.DisconnectReason = evt.Reason.String() + mycli.Instance.Connected = false + err := mycli.instanceRepository.UpdateConnected(mycli.Instance.Id, mycli.Instance.Connected, mycli.Instance.DisconnectReason) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Error updating instance: %s", mycli.Instance.Id, err) + } + case *events.Disconnected: + doWebhook = true + postMap["event"] = "Disconnected" + + // Limpar cache de userInfo para esta instância (mas não para reconexão automática) + mycli.userInfoCache.Delete(mycli.Instance.Token) + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] UserInfo cache cleared for token: %s", mycli.userID, mycli.Instance.Token) + + mycli.Instance.DisconnectReason = "Disconnected emitted because the websocket is closed by the server." + mycli.Instance.Connected = false + err := mycli.instanceRepository.UpdateConnected(mycli.Instance.Id, mycli.Instance.Connected, mycli.Instance.DisconnectReason) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Error updating instance: %s", mycli.Instance.Id, err) + } + + // Trigger instance restart via websocket-capable service (non-blocking) + go func(instanceID string) { + mycli.loggerWrapper.GetLogger(instanceID).LogInfo("[%s] Disconnected detected, restarting instance", instanceID) + if err := mycli.service.ReconnectClient(instanceID); err != nil { + mycli.loggerWrapper.GetLogger(instanceID).LogError("[%s] Failed to restart instance: %v", instanceID, err) + } + }(mycli.userID) + case *events.LabelEdit: + doWebhook = true + postMap["event"] = "LabelEdit" + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Got label edit %+v", mycli.userID, evt.Action) + + label := label_model.Label{ + InstanceID: mycli.userID, + LabelID: evt.LabelID, + LabelName: utils.GetStringValue(evt.Action.Name), + LabelColor: fmt.Sprintf("%d", evt.Action.Color), + PredefinedId: fmt.Sprintf("%d", evt.Action.PredefinedID), + } + + err := mycli.labelRepository.UpsertLabel(label) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to upsert label: %v", mycli.userID, err) + } + case *events.LabelAssociationChat: + doWebhook = true + postMap["event"] = "LabelAssociationChat" + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Label association chat received %+v", mycli.userID, evt) + case *events.LabelAssociationMessage: + doWebhook = true + postMap["event"] = "LabelAssociationMessage" + + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Label association message received %+v", mycli.userID, evt) + case *events.Contact: + doWebhook = true + postMap["event"] = "Contact" + case *events.PushName: + doWebhook = true + postMap["event"] = "PushName" + case *events.Picture: + doWebhook = true + postMap["event"] = "Picture" + case *events.UserAbout: + doWebhook = true + postMap["event"] = "UserAbout" + case *events.IdentityChange: + doWebhook = false + case *events.GroupInfo: + doWebhook = true + postMap["event"] = "GroupInfo" + case *events.JoinedGroup: + doWebhook = true + postMap["event"] = "JoinedGroup" + case *events.NewsletterJoin: + doWebhook = true + postMap["event"] = "NewsletterJoin" + case *events.NewsletterLeave: + doWebhook = true + postMap["event"] = "NewsletterLeave" + case *events.UndecryptableMessage: + jsonEvt, err := json.Marshal(evt) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Undecryptable message received: %s", mycli.userID, evt.Info.ID) + } + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Undecryptable message received all: %+v", mycli.userID, string(jsonEvt)) + + if evt.UnavailableType == "view_once" { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Undecryptable message received view_once: %s", mycli.userID, evt.Info.ID) + + doWebhook = true + postMap["event"] = "Message" + + postMap["data"] = evt + } else if strings.HasPrefix(evt.Info.ID, "66") || strings.HasPrefix(evt.Info.ID, "67") { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] ID 66 or 67 found, reconnecting client", mycli.userID) + mycli.WAClient.Disconnect() + err := mycli.WAClient.Connect() + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Error reconnecting client: %s", mycli.userID, err) + } + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] ID is not 66 or 67 or view_once, skipping", mycli.userID) + } + default: + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] Unhandled event %s: %+v", mycli.userID, fmt.Sprintf("%T", evt), evt) + return + } + + if doWebhook { + postMap["instanceToken"] = mycli.token + postMap["instanceId"] = mycli.userID + postMap["instanceName"] = mycli.Instance.Name + + values, err := json.Marshal(postMap) + if err != nil { + mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to marshal JSON for queue", mycli.userID) + return + } + + var queueName string + if _, ok := postMap["event"]; ok { + queueName = strings.ToLower(fmt.Sprintf("%s.%s", userID, postMap["event"])) + } + + // Log webhook dispatch + eventType := "unknown" + if event, ok := postMap["event"].(string); ok { + eventType = event + } + + dataSize := len(values) + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] ===== DISPATCHING WEBHOOK ===== Event: %s, Queue: %s, DataSize: %d bytes", mycli.userID, eventType, queueName, dataSize) + + go mycli.service.CallWebhook(mycli.Instance, queueName, values) + + if mycli.config.AmqpGlobalEnabled || mycli.config.NatsGlobalEnabled { + mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Sending to global queues - Event: %s, AMQP: %v, NATS: %v", mycli.userID, eventType, mycli.config.AmqpGlobalEnabled, mycli.config.NatsGlobalEnabled) + go mycli.service.SendToGlobalQueues(postMap["event"].(string), values, mycli.userID) + } + } else { + mycli.loggerWrapper.GetLogger(mycli.userID).LogWarn("[%s] ===== WEBHOOK SKIPPED ===== doWebhook=false", mycli.userID) + } +} + +func (w *whatsmeowService) CallWebhook(instance *instance_model.Instance, queueName string, jsonData []byte) { + var data map[string]interface{} + if err := json.Unmarshal(jsonData, &data); err != nil { + return + } + + eventType, ok := data["event"].(string) + if !ok { + return + } + + eventArray := strings.Split(instance.Events, ",") + + var subscriptions []string + + if len(eventArray) < 1 { + subscriptions = append(subscriptions, event_types.MESSAGE) + subscriptions = append(subscriptions, event_types.SEND_MESSAGE) + } else { + for _, arg := range eventArray { + if !event_types.IsEventType(arg) { + w.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Message type discarded: %s", instance.Id, arg) + continue + } + if !utils.Find(subscriptions, arg) { + subscriptions = append(subscriptions, arg) + } + + } + } + + if contains(subscriptions, "ALL") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + return + } + + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] subscriptions %s eventType %s", instance.Id, subscriptions, eventType) + + switch eventType { + case "Message": + if contains(subscriptions, "MESSAGE") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } else { + // Forward to GROUP/NEWSLETTER subscribers even without MESSAGE subscription + if dataMap, ok := data["data"].(map[string]interface{}); ok { + if infoMap, ok := dataMap["Info"].(map[string]interface{}); ok { + if chat, ok := infoMap["Chat"].(string); ok { + if strings.HasSuffix(chat, "@g.us") && contains(subscriptions, "GROUP") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s (Group)", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } else if strings.HasSuffix(chat, "@newsletter") && contains(subscriptions, "NEWSLETTER") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s (Newsletter)", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + } + } + } + } + case "SendMessage": + if contains(subscriptions, "SEND_MESSAGE") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } else { + if dataMap, ok := data["data"].(map[string]interface{}); ok { + if infoMap, ok := dataMap["Info"].(map[string]interface{}); ok { + if chat, ok := infoMap["Chat"].(string); ok { + if strings.HasSuffix(chat, "@g.us") && contains(subscriptions, "GROUP") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s (Group)", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } else if strings.HasSuffix(chat, "@newsletter") && contains(subscriptions, "NEWSLETTER") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s (Newsletter)", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + } + } + } + } + case "Receipt": + if contains(subscriptions, "READ_RECEIPT") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } else { + if dataMap, ok := data["data"].(map[string]interface{}); ok { + if chat, ok := dataMap["Chat"].(string); ok { + if strings.HasSuffix(chat, "@g.us") && contains(subscriptions, "GROUP") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s (Group)", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } else if strings.HasSuffix(chat, "@newsletter") && contains(subscriptions, "NEWSLETTER") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s (Newsletter)", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + } + } + } + case "Presence": + if contains(subscriptions, "PRESENCE") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + case "HistorySync": + if contains(subscriptions, "HISTORY_SYNC") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + case "ChatPresence", "Archive": + if contains(subscriptions, "CHAT_PRESENCE") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + case "CallOffer", "CallAccept", "CallTerminate", "CallOfferNotice", "CallRelayLatency": + if contains(subscriptions, "CALL") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + case "Connected", "PairSuccess", "TemporaryBan", "LoggedOut", "ConnectFailure", "Disconnected": + if contains(subscriptions, "CONNECTION") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + case "LabelEdit", "LabelAssociationChat", "LabelAssociationMessage": + if contains(subscriptions, "LABEL") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + case "Contact", "PushName": + if contains(subscriptions, "CONTACT") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + case "Picture": + if contains(subscriptions, "PICTURE") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + case "UserAbout": + if contains(subscriptions, "USER_ABOUT") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + case "GroupInfo", "JoinedGroup": + if contains(subscriptions, "GROUP") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + case "NewsletterJoin", "NewsletterLeave": + if contains(subscriptions, "NEWSLETTER") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + case "QRCode", "QRTimeout", "QRSuccess": + if contains(subscriptions, "QRCODE") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + case "ButtonClick": + if contains(subscriptions, "BUTTON_CLICK") || contains(subscriptions, "MESSAGE") { + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType) + w.sendToQueueOrWebhook(instance, queueName, jsonData) + } + + default: + return + } +} + +func contains(subscriptions []string, event string) bool { + for _, sub := range subscriptions { + if strings.EqualFold(sub, event) { + return true + } + } + return false +} + +func (w *whatsmeowService) sendToQueueOrWebhook(instance *instance_model.Instance, queueName string, jsonData []byte) { + if instance.RabbitmqEnable == "enabled" || instance.RabbitmqEnable == "true" { + err := w.rabbitmqProducer.Produce(queueName, jsonData, instance.RabbitmqEnable, instance.Id) + if err != nil { + w.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to send message to rabbitmq: %s", instance.Id, err) + return + } + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Message sent to rabbitmq successfully", instance.Id) + } + + if instance.NatsEnable == "enabled" || instance.NatsEnable == "true" { + err := w.natsProducer.Produce(queueName, jsonData, instance.NatsEnable, instance.Id) + if err != nil { + w.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to send message to nats: %s", instance.Id, err) + return + } + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Message sent to nats successfully", instance.Id) + } + + if instance.WebSocketEnable == "enabled" || instance.WebSocketEnable == "true" { + err := w.websocketProducer.Produce(queueName, jsonData, instance.Id, instance.Token) + if err != nil { + w.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to send message to websocket: %s", instance.Id, err) + return + } + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Message sent to websocket successfully", instance.Id) + } + + if instance.Webhook != "" && instance.Webhook != "disabled" { + err := w.webhookProducer.Produce(queueName, jsonData, instance.Webhook, instance.Id) + if err != nil { + w.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Failed to send message to webhook: %s", instance.Id, err) + return + } + w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Message sent to webhook successfully", instance.Id) + } +} + +func (w whatsmeowService) StartInstance(instanceId string) error { + instance, err := w.instanceRepository.GetInstanceByID(instanceId) + if err != nil { + return err + } + + if instance.Proxy == "" && w.config.ProxyHost != "" && w.config.ProxyPort != "" && w.config.ProxyUsername != "" && w.config.ProxyPassword != "" { + proxyConfig := ProxyConfig{ + Protocol: utils.NormalizeProxyProtocol(w.config.ProxyProtocol, w.config.ProxyPort), + Host: w.config.ProxyHost, + Port: w.config.ProxyPort, + Username: w.config.ProxyUsername, + Password: w.config.ProxyPassword, + } + + proxyJSON, err := json.Marshal(proxyConfig) + if err != nil { + w.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to marshal proxy config: %v", instanceId, err) + return err + } + + instance.Proxy = string(proxyJSON) + + err = w.instanceRepository.UpdateProxy(instance.Id, instance.Proxy) + if err != nil { + w.loggerWrapper.GetLogger(instanceId).LogError("[%s] Failed to update instance: %s", instanceId, err) + return err + } + } + + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Starting client", instance.Id) + + v := Values{map[string]string{ + "Id": instance.Id, + "Jid": instance.Jid, + "Token": instance.Token, + "Events": instance.Events, + "osName": instance.OsName, + "Proxy": instance.Proxy, + }} + + w.userInfoCache.Set(instance.Token, v, 0) + + eventArray := strings.Split(instance.Events, ",") + + var subscribedEvents []string + + if len(eventArray) < 1 { + subscribedEvents = append(subscribedEvents, event_types.MESSAGE) + } else { + for _, arg := range eventArray { + if !event_types.IsEventType(arg) { + w.loggerWrapper.GetLogger(instanceId).LogWarn("[%s] Message type discarded: %s", instanceId, arg) + continue + } + if !utils.Find(subscribedEvents, arg) { + subscribedEvents = append(subscribedEvents, arg) + } + + } + } + + w.killChannel[instance.Id] = make(chan bool) + + clientData := &ClientData{ + Instance: instance, + Subscriptions: subscribedEvents, + Phone: "", + IsProxy: false, + } + + if instance.Proxy != "" { + var proxyConfig ProxyConfig + err := json.Unmarshal([]byte(instance.Proxy), &proxyConfig) + if err != nil { + w.loggerWrapper.GetLogger(instanceId).LogError("[%s] error unmarshalling proxy config", instanceId) + return err + } + + if proxyConfig.Host != "" { + clientData.IsProxy = true + } + } + + go w.StartClient(clientData) + + return nil +} + +func (w whatsmeowService) ConnectOnStartup(clientName string) { + w.loggerWrapper.GetLogger(clientName).LogInfo("Connecting all instances on startup") + var instances []*instance_model.Instance + var err error + + if clientName != "" { + instances, err = w.instanceRepository.GetAllConnectedInstancesByClientName(clientName) + if err != nil { + w.loggerWrapper.GetLogger(clientName).LogError("[%s] Error getting all connected instances: %s", clientName, err) + return + } + } else { + instances, err = w.instanceRepository.GetAllConnectedInstances() + if err != nil { + w.loggerWrapper.GetLogger(clientName).LogError("[%s] Error getting all connected instances: %s", clientName, err) + return + } + } + + w.loggerWrapper.GetLogger(clientName).LogInfo("[%s] Found %d connected instances", clientName, len(instances)) + + for _, instance := range instances { + w.loggerWrapper.GetLogger(clientName).LogInfo("[%s] Starting client for user '%s'", clientName, instance.Id) + + err := w.StartInstance(instance.Id) + if err != nil { + w.loggerWrapper.GetLogger(clientName).LogError("[%s] Error starting client: %s", clientName, err) + } + } +} + +func getExtensionFromMimeType(mimeType string) string { + switch mimeType { + case "image/jpeg": + return ".jpg" + case "image/png": + return ".png" + case "image/webp": + return ".webp" + case "video/mp4": + return ".mp4" + case "audio/ogg": + return ".ogg" + case "audio/mpeg": + return ".mp3" + case "application/pdf": + return ".pdf" + case "application/vnd.openxmlformats-officedocument.wordprocessingml.document": + return ".docx" + case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": + return ".xlsx" + case "application/vnd.openxmlformats-officedocument.presentationml.presentation": + return ".pptx" + default: + // Se não encontrar um tipo conhecido, extrai a extensão do mimetype + parts := strings.Split(mimeType, "/") + if len(parts) > 1 { + return "." + parts[1] + } + return ".bin" + } +} + +func (w *whatsmeowService) SendToGlobalQueues(eventType string, payload []byte, userId string) { + w.loggerWrapper.GetLogger(userId).LogInfo("[%s] Starting sendToGlobalQueues for event: %s", userId, eventType) + + // AMQP: AMQP_SPECIFIC_EVENTS tem prioridade sobre AMQP_GLOBAL_EVENTS + if w.config.AmqpGlobalEnabled { + var shouldSendToAmqp bool + var amqpQueueName string + + // Se AMQP_SPECIFIC_EVENTS estiver configurada, ela tem prioridade + if len(w.config.AmqpSpecificEvents) > 0 { + w.loggerWrapper.GetLogger(userId).LogInfo("[%s] Using AMQP_SPECIFIC_EVENTS (priority over AMQP_GLOBAL_EVENTS)", userId) + // Verifica se o evento específico está na lista + if utils.Find(w.config.AmqpSpecificEvents, eventType) { + shouldSendToAmqp = true + amqpQueueName = strings.ToLower(eventType) + w.loggerWrapper.GetLogger(userId).LogInfo("[%s] Event %s found in AMQP_SPECIFIC_EVENTS", userId, eventType) + } + } else { + // Fallback para AMQP_GLOBAL_EVENTS (modo antigo com grupos de eventos) + w.loggerWrapper.GetLogger(userId).LogInfo("[%s] Using AMQP_GLOBAL_EVENTS (fallback mode)", userId) + + // Mapeia o evento do Whatsmeow para o tipo de evento global + var globalEventType string + switch eventType { + case "Message": + globalEventType = "MESSAGE" + case "SendMessage": + globalEventType = "SEND_MESSAGE" + case "Receipt": + globalEventType = "READ_RECEIPT" + case "Presence": + globalEventType = "PRESENCE" + case "HistorySync": + globalEventType = "HISTORY_SYNC" + case "ChatPresence", "Archive": + globalEventType = "CHAT_PRESENCE" + case "CallOffer", "CallAccept", "CallTerminate", "CallOfferNotice", "CallRelayLatency": + globalEventType = "CALL" + case "Connected", "PairSuccess", "TemporaryBan", "LoggedOut", "ConnectFailure", "Disconnected": + globalEventType = "CONNECTION" + case "LabelEdit", "LabelAssociationChat", "LabelAssociationMessage": + globalEventType = "LABEL" + case "Contact", "PushName": + globalEventType = "CONTACT" + case "Picture": + globalEventType = "PICTURE" + case "UserAbout": + globalEventType = "USER_ABOUT" + case "GroupInfo", "JoinedGroup": + globalEventType = "GROUP" + case "NewsletterJoin", "NewsletterLeave": + globalEventType = "NEWSLETTER" + case "QRCode", "QRTimeout", "QRSuccess": + globalEventType = "QRCODE" + default: + w.loggerWrapper.GetLogger(userId).LogInfo("[%s] Event %s not mapped to global event type", userId, eventType) + return + } + + // Verifica se o grupo de eventos está na lista + if utils.Find(w.config.AmqpGlobalEvents, globalEventType) { + shouldSendToAmqp = true + amqpQueueName = strings.ToLower(eventType) + w.loggerWrapper.GetLogger(userId).LogInfo("[%s] Event group %s found in AMQP_GLOBAL_EVENTS", userId, globalEventType) + } + } + + // Envia para RabbitMQ se necessário + if shouldSendToAmqp { + w.loggerWrapper.GetLogger(userId).LogInfo("[%s] Sending to AMQP queue: %s", userId, amqpQueueName) + err := w.rabbitmqProducer.Produce(amqpQueueName, payload, "global", userId) + if err != nil { + w.loggerWrapper.GetLogger(userId).LogError("[%s] Failed to send message to RabbitMQ global queue %s: %v", userId, amqpQueueName, err) + } else { + w.loggerWrapper.GetLogger(userId).LogInfo("[%s] Successfully sent message to RabbitMQ global queue %s", userId, amqpQueueName) + } + } else { + w.loggerWrapper.GetLogger(userId).LogInfo("[%s] Event %s not configured for AMQP", userId, eventType) + } + } + + // NATS: Mantém o comportamento original por enquanto (só NATS_GLOBAL_EVENTS) + if w.config.NatsGlobalEnabled { + // Mapeia o evento para grupo (necessário para NATS por enquanto) + var globalEventType string + switch eventType { + case "Message": + globalEventType = "MESSAGE" + case "SendMessage": + globalEventType = "SEND_MESSAGE" + case "Receipt": + globalEventType = "READ_RECEIPT" + case "Presence": + globalEventType = "PRESENCE" + case "HistorySync": + globalEventType = "HISTORY_SYNC" + case "ChatPresence", "Archive": + globalEventType = "CHAT_PRESENCE" + case "CallOffer", "CallAccept", "CallTerminate", "CallOfferNotice", "CallRelayLatency": + globalEventType = "CALL" + case "Connected", "PairSuccess", "TemporaryBan", "LoggedOut", "ConnectFailure", "Disconnected": + globalEventType = "CONNECTION" + case "LabelEdit", "LabelAssociationChat", "LabelAssociationMessage": + globalEventType = "LABEL" + case "Contact", "PushName": + globalEventType = "CONTACT" + case "GroupInfo", "JoinedGroup": + globalEventType = "GROUP" + case "NewsletterJoin", "NewsletterLeave": + globalEventType = "NEWSLETTER" + case "QRCode", "QRTimeout", "QRSuccess": + globalEventType = "QRCODE" + default: + globalEventType = "" + } + + // Verifica se o evento está na lista de eventos globais NATS + if globalEventType != "" && utils.Find(w.config.NatsGlobalEvents, globalEventType) { + queueName := strings.ToLower(eventType) + w.loggerWrapper.GetLogger(userId).LogInfo("[%s] Sending to NATS subject: %s", userId, queueName) + + err := w.natsProducer.Produce(queueName, payload, "global", userId) + if err != nil { + w.loggerWrapper.GetLogger(userId).LogError("[%s] Failed to send message to NATS global subject %s: %v", userId, queueName, err) + } else { + w.loggerWrapper.GetLogger(userId).LogInfo("[%s] Successfully sent message to NATS global subject %s", userId, queueName) + } + } + } +} + +var ( + cachedWebVersion *clientVersion + cachedWebVersionAt time.Time + cachedWebVersionMu sync.Mutex + webVersionCacheTTL = 1 * time.Hour +) + +func fetchWhatsAppWebVersion() (*clientVersion, error) { + cachedWebVersionMu.Lock() + defer cachedWebVersionMu.Unlock() + + if cachedWebVersion != nil && time.Since(cachedWebVersionAt) < webVersionCacheTTL { + return cachedWebVersion, nil + } + + resp, err := http.Get("https://web.whatsapp.com/sw.js") + if err != nil { + return nil, fmt.Errorf("failed to fetch WhatsApp Web version: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %v", err) + } + + content := string(body) + + // Múltiplas estratégias para encontrar client_revision + patterns := []string{ + `"client_revision":\s*(\d+)`, // Formato direto + `\\"client_revision\\":\s*(\d+)`, // Formato escaped + `client_revision\\?\\"?:[\s]*(\d+)`, // Formato mais flexível + `["']client_revision["'][\s]*:[\s]*(\d+)`, // Com aspas variadas + } + + for _, pattern := range patterns { + re := regexp.MustCompile(pattern) + matches := re.FindStringSubmatch(content) + + if len(matches) >= 2 { + clientRevision, err := strconv.Atoi(matches[1]) + if err != nil { + continue // Tenta próximo padrão + } + + // Log qual padrão funcionou + if clientRevision > 0 { + cachedWebVersion = &clientVersion{ + Major: 2, + Minor: 3000, + Patch: clientRevision, + } + cachedWebVersionAt = time.Now() + return cachedWebVersion, nil + } + } + } + + // Se chegou aqui, nenhum padrão funcionou - log do conteúdo para debug + // Mostra apenas uma parte para não logar muito + previewLength := 500 + if len(content) > previewLength { + content = content[:previewLength] + "..." + } + + return nil, fmt.Errorf("could not find client revision in the fetched content. Content preview: %s", content) +} + +func (w whatsmeowService) UpdateInstanceSettings(instanceId string) error { + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Updating instance settings in runtime", instanceId) + + // Busca a instância atualizada do banco + instance, err := w.instanceRepository.GetInstanceByID(instanceId) + if err != nil { + w.loggerWrapper.GetLogger(instanceId).LogError("[%s] Error getting instance from DB: %v", instanceId, err) + return err + } + + // Verifica se o MyClient existe + myClient, exists := w.myClientPointer[instanceId] + if !exists { + w.loggerWrapper.GetLogger(instanceId).LogWarn("[%s] MyClient not found in runtime, instance may not be connected", instanceId) + return fmt.Errorf("instance %s not found in runtime", instanceId) + } + + // Atualiza as configurações no MyClient em execução + myClient.Instance = instance + myClient.webhookUrl = instance.Webhook + myClient.rabbitmqEnable = instance.RabbitmqEnable + myClient.natsEnable = instance.NatsEnable + myClient.websocketEnable = instance.WebSocketEnable + + // Atualiza as subscriptions se os eventos mudaram + eventArray := strings.Split(instance.Events, ",") + var subscribedEvents []string + + if len(eventArray) < 1 { + subscribedEvents = append(subscribedEvents, event_types.MESSAGE) + } else { + for _, arg := range eventArray { + if !event_types.IsEventType(arg) { + w.loggerWrapper.GetLogger(instanceId).LogWarn("[%s] Message type discarded: %s", instanceId, arg) + continue + } + if !utils.Find(subscribedEvents, arg) { + subscribedEvents = append(subscribedEvents, arg) + } + } + } + + myClient.subscriptions = subscribedEvents + + // Atualiza o cache do userInfo com as novas configurações + v := Values{map[string]string{ + "Id": instance.Id, + "Jid": instance.Jid, + "Token": instance.Token, + "Events": instance.Events, + "osName": instance.OsName, + "Proxy": instance.Proxy, + }} + w.userInfoCache.Set(instance.Token, v, 0) + + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance settings and cache updated in runtime successfully", instanceId) + return nil +} + +func (w whatsmeowService) UpdateInstanceAdvancedSettings(instanceId string) error { + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Updating advanced settings in runtime", instanceId) + + // Busca a instância atualizada do banco + instance, err := w.instanceRepository.GetInstanceByID(instanceId) + if err != nil { + w.loggerWrapper.GetLogger(instanceId).LogError("[%s] Error getting instance from DB: %v", instanceId, err) + return err + } + + // Verifica se o MyClient existe + myClient, exists := w.myClientPointer[instanceId] + if !exists { + w.loggerWrapper.GetLogger(instanceId).LogWarn("[%s] MyClient not found in runtime, instance may not be connected", instanceId) + return fmt.Errorf("instance %s not found in runtime", instanceId) + } + + // Atualiza a instância no MyClient com as advanced settings atualizadas + myClient.Instance = instance + + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Advanced settings updated in runtime successfully", instanceId) + return nil +} + +func (w whatsmeowService) ClearInstanceCache(instanceId string, token string) error { + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Clearing instance cache - Token: %s", instanceId, token) + + // Limpar userInfoCache + w.userInfoCache.Delete(token) + + // Limpar myClientPointer se existir + if _, exists := w.myClientPointer[instanceId]; exists { + delete(w.myClientPointer, instanceId) + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] MyClient pointer cleared", instanceId) + } + + // Limpar clientPointer se existir + if _, exists := w.clientPointer[instanceId]; exists { + delete(w.clientPointer, instanceId) + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client pointer cleared", instanceId) + } + + // Limpar killChannel se existir + if killChan, exists := w.killChannel[instanceId]; exists { + select { + case killChan <- true: + // Canal recebeu o sinal + default: + // Canal pode estar bloqueado, apenas fecha + } + close(killChan) + delete(w.killChannel, instanceId) + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Kill channel cleared", instanceId) + } + + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance cache completely cleared", instanceId) + return nil +} + +func NewWhatsmeowService( + instanceRepository instance_repository.InstanceRepository, + authDB *sql.DB, + supa *supabase.Client, + messageRepository message_repository.MessageRepository, + labelRepository label_repository.LabelRepository, + config *config.Config, + killChannel map[string](chan bool), + clientPointer map[string]*whatsmeow.Client, + rabbitmqProducer producer_interfaces.Producer, + webhookProducer producer_interfaces.Producer, + websocketProducer producer_interfaces.Producer, + redisClient *redis.Client, + mediaStorage storage_interfaces.MediaStorage, + natsProducer producer_interfaces.Producer, + loggerWrapper *logger_wrapper.LoggerManager, +) WhatsmeowService { + // Inicializar PollService de forma segura + pollSvc := poll_service.NewPollService(supa, loggerWrapper) + + return &whatsmeowService{ + instanceRepository: instanceRepository, + authDB: authDB, + supa: supa, + messageRepository: messageRepository, + labelRepository: labelRepository, + pollService: pollSvc, // NOVO: Serviço de enquetes + config: config, + killChannel: killChannel, + userInfoCache: cache.New(redisClient), + clientPointer: clientPointer, + myClientPointer: make(map[string]*MyClient), + rabbitmqProducer: rabbitmqProducer, + webhookProducer: webhookProducer, + websocketProducer: websocketProducer, + mediaStorage: mediaStorage, + processedMessages: cache.New(redisClient), + natsProducer: natsProducer, + loggerWrapper: loggerWrapper, + passkeyCeremony: ceremony.NewStore(), + } +} + +// GetPollService retorna o serviço de polls (evita dupla inicialização) +func (w *whatsmeowService) GetPollService() poll_service.PollService { + return w.pollService +} + +// PasskeyCeremonyStore exposes the shared ceremony store so the public HTTP +// polling endpoint can read the current stage for a given ceremony token. +func (w *whatsmeowService) PasskeyCeremonyStore() *ceremony.Store { + return w.passkeyCeremony +} + +// SubmitPasskeyResponse forwards the browser's WebAuthn assertion to WhatsApp +// for the given instance. Called by POST /passkey-ceremony/{token}/response. +func (w *whatsmeowService) SubmitPasskeyResponse(instanceId string, resp *types.WebAuthnResponse) error { + client, ok := w.clientPointer[instanceId] + if !ok || client == nil { + return fmt.Errorf("no active client for instance %s", instanceId) + } + ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) + defer cancel() + if err := client.SendPasskeyResponse(ctx, resp); err != nil { + w.passkeyCeremony.SetError(instanceId, err.Error()) + return err + } + // Server will asynchronously emit PairPasskeyConfirmation (or Error) into + // the event handler; move to the waiting stage in the meantime. + w.passkeyCeremony.SetAwaitingConfirmation(instanceId) + return nil +} + +// ConfirmPasskey finishes the pairing after the user verified the code. +// Called by POST /passkey-ceremony/{token}/confirm. +func (w *whatsmeowService) ConfirmPasskey(instanceId string) error { + client, ok := w.clientPointer[instanceId] + if !ok || client == nil { + return fmt.Errorf("no active client for instance %s", instanceId) + } + ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) + defer cancel() + if err := client.SendPasskeyConfirmation(ctx); err != nil { + w.passkeyCeremony.SetError(instanceId, err.Error()) + return err + } + w.passkeyCeremony.SetConfirmed(instanceId) + return nil +} + +// cleanSenderID remove a parte ":numero" do sender ID para exibir apenas o remoteJid correto +// Exemplo: "557499879409:3@s.whatsapp.net" -> "557499879409@s.whatsapp.net" +func cleanSenderID(senderID string) string { + // Procura pelo padrão ":numero" antes do @ + if colonIndex := strings.Index(senderID, ":"); colonIndex != -1 { + if atIndex := strings.Index(senderID, "@"); atIndex != -1 && colonIndex < atIndex { + // Remove a parte ":numero" mantendo apenas o número principal e o domínio + return senderID[:colonIndex] + senderID[atIndex:] + } + } + return senderID +}