diff --git a/.env.sample b/.env.sample index 95baa0a9..513e302b 100644 --- a/.env.sample +++ b/.env.sample @@ -2,6 +2,7 @@ # Server Configuration WUZAPI_PORT=8080 WUZAPI_ADDRESS=0.0.0.0 +GO_ENV=dev # Token for WuzAPI Admin WUZAPI_ADMIN_TOKEN=1234ABCD @@ -21,6 +22,9 @@ WEBHOOK_FORMAT=json # WuzAPI Session Configuration SESSION_DEVICE_NAME=WuzAPI +# Adminer Configuration +ADMINER_PORT=8081 + # Database configuration DB_USER=wuzapi DB_PASSWORD=wuzapi @@ -31,5 +35,9 @@ DB_SSLMODE=false TZ=America/Sao_Paulo # RabbitMQ configuration Optional -RABBITMQ_URL=amqp://wuzapi:wuzapi@localhost:5672/%2F +RABBITMQ_URL=amqp://wuzapi:wuzapi@localhost:5672/ RABBITMQ_QUEUE=whatsapp_events + +# MinIO Configuration +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=minioadmin \ No newline at end of file diff --git a/.gitignore b/.gitignore index 19e6158c..fb5434a3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ files/ wuzapi .env .tool-versions +tmp # Added by Claude Task Master # Logs diff --git a/docker-compose.yml b/docker-compose.yml index 66731838..88fe21ba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,9 +18,8 @@ services: - TZ=${TZ:-America/Sao_Paulo} - WEBHOOK_FORMAT=${WEBHOOK_FORMAT:-json} - SESSION_DEVICE_NAME=${SESSION_DEVICE_NAME:-WuzAPI} - # RabbitMQ configuration Optional - - RABBITMQ_URL=amqp://wuzapi:wuzapi@rabbitmq:5672/ - - RABBITMQ_QUEUE=whatsapp_events + - RABBITMQ_URL=${RABBITMQ_URL} + - RABBITMQ_QUEUE=${RABBITMQ_QUEUE:-whatsapp_events} depends_on: db: condition: service_healthy @@ -36,8 +35,8 @@ services: POSTGRES_USER: ${DB_USER:-wuzapi} POSTGRES_PASSWORD: ${DB_PASSWORD:-wuzapi} POSTGRES_DB: ${DB_NAME:-wuzapi} - # ports: - # - "${DB_PORT:-5432}:5432" # Uncomment to access the database directly from your host machine. + ports: + - "${DB_PORT:-5432}:5432" volumes: - db_data:/var/lib/postgresql/data networks: @@ -70,6 +69,63 @@ services: retries: 5 restart: always + minio: + image: minio/minio:RELEASE.2025-02-28T09-55-16Z + container_name: minio_s3 + restart: unless-stopped + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin} + ports: + - "9000:9000" # Port API S3 + - "9001:9001" # Web Console + volumes: + - minio_data:/data + command: server /data --console-address ":9001" + networks: + - wuzapi-network + + adminer: + image: adminer + restart: always + ports: + - "${ADMINER_PORT:-8081}:8080" + networks: + - wuzapi-network + + webhook-tester: + image: ghcr.io/tarampampam/webhook-tester:latest + container_name: webhook-tester + command: serve + ports: + - "8082:8080" + networks: + - wuzapi-network + restart: unless-stopped + + proxy-http: + image: kalaksi/tinyproxy:latest + restart: unless-stopped + environment: + - ALLOW=0.0.0.0/0 + networks: + - wuzapi-network + ports: + - "8888:8888" + + # --- SOCKS5 Proxy (With Auth) --- + proxy-socks: + image: serjs/go-socks5-proxy + restart: unless-stopped + environment: + - PROXY_USER=wuzapi + - PROXY_PASSWORD=wuzapi + - PROXY_PORT=1080 + networks: + - wuzapi-network + ports: + - "1080:1080" + networks: wuzapi-network: driver: bridge @@ -77,3 +133,4 @@ networks: volumes: db_data: rabbitmq_data: + minio_data: diff --git a/handlers.go b/handlers.go index 0711ce83..b5431b19 100644 --- a/handlers.go +++ b/handlers.go @@ -854,7 +854,7 @@ func (s *server) SendDocument() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) + recipient, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -992,7 +992,7 @@ func (s *server) SendAudio() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) + recipient, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -1142,7 +1142,7 @@ func (s *server) SendImage() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) + recipient, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -1333,7 +1333,7 @@ func (s *server) SendSticker() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) + recipient, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -1466,7 +1466,7 @@ func (s *server) SendVideo() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) + recipient, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -1623,7 +1623,7 @@ func (s *server) SendContact() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) + recipient, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -1728,7 +1728,7 @@ func (s *server) SendLocation() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) + recipient, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -2140,7 +2140,7 @@ func (s *server) SendMessage() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) + recipient, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -2277,7 +2277,7 @@ func (s *server) SendPoll() http.HandlerFunc { msgid = req.Id } - recipient, err := validateMessageFields(req.Group, nil, nil) + recipient, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), req.Group, nil, nil) if err != nil { s.Respond(w, r, http.StatusBadRequest, err) return @@ -2342,8 +2342,8 @@ func (s *server) DeleteMessage() http.HandlerFunc { msgid = t.Id - recipient, ok := parseJID(t.Phone) - if !ok { + recipient, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), t.Phone, nil, nil) + if err != nil { s.Respond(w, r, http.StatusBadRequest, errors.New("could not parse Phone")) return } @@ -2407,7 +2407,7 @@ func (s *server) SendEditMessage() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) + recipient, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), t.Phone, t.ContextInfo.StanzaID, t.ContextInfo.Participant) if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -2952,8 +2952,9 @@ func (s *server) GetAvatar() http.HandlerFunc { return } - jid, ok := parseJID(t.Phone) - if !ok { + jid, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), t.Phone, nil, nil) + if err != nil { + log.Error().Err(err).Msg("Error parsing Phone") s.Respond(w, r, http.StatusBadRequest, errors.New("could not parse Phone")) return } @@ -3055,9 +3056,10 @@ func (s *server) ChatPresence() http.HandlerFunc { return } - jid, ok := parseJID(t.Phone) - if !ok { - s.Respond(w, r, http.StatusBadRequest, errors.New("could not parse Phone")) + jid, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), t.Phone, nil, nil) + if err != nil { + log.Error().Err(err).Msg("Error parsing Phone") + s.Respond(w, r, http.StatusBadRequest, errors.New("could not parse Group JID")) return } @@ -3434,8 +3436,9 @@ func (s *server) React() http.HandlerFunc { return } - recipient, ok := parseJID(t.Phone) - if !ok { + recipient, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), t.Phone, nil, nil) + if err != nil { + log.Error().Err(err).Msg("Error parsing Phone") s.Respond(w, r, http.StatusBadRequest, errors.New("could not parse Group JID")) return } @@ -3530,11 +3533,11 @@ func (s *server) MarkRead() http.HandlerFunc { } var jidChat types.JID - if len(t.ChatPhone) > 0 { - var ok bool - jidChat, ok = parseJID(t.ChatPhone) - if !ok { + var err error + jidChat, err = validateMessageFields(clientManager.GetWhatsmeowClient(txtid), t.ChatPhone, nil, nil) + if err != nil { + log.Error().Err(err).Msg("Error parsing ChatPhone") s.Respond(w, r, http.StatusBadRequest, errors.New("could not parse ChatPhone")) return } @@ -3546,11 +3549,11 @@ func (s *server) MarkRead() http.HandlerFunc { } var jidSender types.JID - if len(t.SenderPhone) > 0 { - var ok bool - jidSender, ok = parseJID(t.SenderPhone) - if !ok { + var err error + jidSender, err = validateMessageFields(clientManager.GetWhatsmeowClient(txtid), t.SenderPhone, nil, nil) + if err != nil { + log.Error().Err(err).Msg("Error parsing SenderPhone") s.Respond(w, r, http.StatusBadRequest, errors.New("could not parse SenderPhone")) return } @@ -3823,13 +3826,13 @@ func (s *server) CreateGroup() http.HandlerFunc { // Parse participant phone numbers participantJIDs := make([]types.JID, len(t.Participants)) - var ok bool for i, phone := range t.Participants { - participantJIDs[i], ok = parseJID(phone) - if !ok { + jid, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), phone, nil, nil) + if err != nil { s.Respond(w, r, http.StatusBadRequest, errors.New("could not parse Participant Phone")) return } + participantJIDs[i] = jid } req := whatsmeow.ReqCreateGroup{ @@ -4076,11 +4079,13 @@ func (s *server) UpdateGroupParticipants() http.HandlerFunc { // parse phone numbers phoneParsed := make([]types.JID, len(t.Phone)) for i, phone := range t.Phone { - phoneParsed[i], ok = parseJID(phone) - if !ok { + jid, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), phone, nil, nil) + if err != nil { + log.Error().Err(err).Msg("Error parsing Phone") s.Respond(w, r, http.StatusBadRequest, errors.New("could not parse Phone")) return } + phoneParsed[i] = jid } if t.Action == "" { @@ -5269,14 +5274,30 @@ func (s *server) Respond(w http.ResponseWriter, r *http.Request, status int, dat } } -// Validate message fields -func validateMessageFields(phone string, stanzaid *string, participant *string) (types.JID, error) { +// validateMessageFields validates and corrects the JID using the WhatsApp API +func validateMessageFields(client *whatsmeow.Client, phone string, stanzaid *string, participant *string) (types.JID, error) { recipient, ok := parseJID(phone) if !ok { return types.NewJID("", types.DefaultUserServer), errors.New("could not parse Phone") } + if client != nil && recipient.Server == types.DefaultUserServer { + + checkPhone := "+" + recipient.User + results, err := client.IsOnWhatsApp(context.Background(), []string{checkPhone}) + + if err != nil { + return types.NewJID("", types.DefaultUserServer), fmt.Errorf("failed to check if number exists: %v", err) + } + + if len(results) == 0 || !results[0].IsIn { + return types.NewJID("", types.DefaultUserServer), errors.New("no account exists") + } + + recipient = results[0].JID + } + if stanzaid != nil { if participant == nil { return types.NewJID("", types.DefaultUserServer), errors.New("missing Participant in ContextInfo") @@ -5289,6 +5310,11 @@ func validateMessageFields(phone string, stanzaid *string, participant *string) } } + log.Info(). + Str("original_input", phone). + Str("resolved_jid", recipient.String()). + Msg("JID Validated") + return recipient, nil } @@ -6214,9 +6240,10 @@ func (s *server) GetUserLID() http.HandlerFunc { } // Parse the JID (phone number) - jid, ok := parseJID(jidParam) - if !ok { - s.Respond(w, r, http.StatusBadRequest, errors.New("invalid jid format")) + jid, err := validateMessageFields(clientManager.GetWhatsmeowClient(txtid), jidParam, nil, nil) + if err != nil { + log.Error().Err(err).Msg("Error parsing JID") + s.Respond(w, r, http.StatusBadRequest, errors.New("could not parse JID")) return } @@ -6478,3 +6505,61 @@ func (s *server) DownloadSticker() http.HandlerFunc { return } } + +// GetFormattedPhone uses the WhatsApp network to validate and correct a phone number +func (s *server) GetFormattedPhone() http.HandlerFunc { + + type phoneStruct struct { + Phone string `json:"phone"` + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(Values).Get("Id") + + // Necesitamos el cliente conectado para preguntar a la API de WhatsApp + client := clientManager.GetWhatsmeowClient(txtid) + if client == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("no session found")) + return + } + + decoder := json.NewDecoder(r.Body) + var t phoneStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("could not decode Payload")) + return + } + + if t.Phone == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("missing Phone in Payload")) + return + } + + // Usamos la función mágica que ya creamos/arreglamos + // Pasamos nil en stanza y participant porque no es un mensaje real, solo queremos validar el JID + jid, err := validateMessageFields(client, t.Phone, nil, nil) + + if err != nil { + // Si falla (ej. el número no existe en WhatsApp), devolvemos error + s.Respond(w, r, http.StatusBadRequest, fmt.Errorf("validation failed: %v", err)) + return + } + + // Preparamos la respuesta con los datos limpios + response := map[string]interface{}{ + "input": t.Phone, // Lo que enviaste (ej: 5233...) + "jid": jid.String(), // El JID completo (ej: 52133...@s.whatsapp.net) + "number": jid.User, // Solo el número corregido (ej: 52133...) + "exists": true, // Confirmación de que existe + } + + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + } +} diff --git a/routes.go b/routes.go index 19c96b26..8cea9546 100644 --- a/routes.go +++ b/routes.go @@ -15,11 +15,14 @@ type Middleware = alice.Constructor func (s *server) routes() { - ex, err := os.Executable() - if err != nil { - panic(err) + staticPath := "./static/" + + if os.Getenv("GO_ENV") != "dev" { + ex, err := os.Executable() + if err == nil { + staticPath = filepath.Join(filepath.Dir(ex), "static") + } } - exPath := filepath.Dir(ex) var routerLog zerolog.Logger logOutput := os.Stdout @@ -159,5 +162,7 @@ func (s *server) routes() { s.router.Handle("/newsletter/list", c.Then(s.ListNewsletter())).Methods("GET") - s.router.PathPrefix("/").Handler(http.FileServer(http.Dir(exPath + "/static/"))) + s.router.Handle("/misc/phone", c.Then(s.GetFormattedPhone())).Methods("POST") + + s.router.PathPrefix("/").Handler(http.FileServer(http.Dir(staticPath))) } diff --git a/static/api/spec.yml b/static/api/spec.yml index 8ab70952..22a5d1c7 100644 --- a/static/api/spec.yml +++ b/static/api/spec.yml @@ -881,6 +881,39 @@ paths: description: Invalid or missing token 404: description: User not found + /misc/phone: + post: + tags: + - Misc + summary: Validate and format phone number + description: Sends a phone number to WhatsApp API to verify existence and get the correct international format (JID). ideal for correcting Mexico numbers (+521). + security: + - ApiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + phone: + type: string + example: "523312345678" + responses: + 200: + description: Phone Validated + content: + application/json: + schema: + example: { + "input": "523312345678", + "jid": "5213312345678@s.whatsapp.net", + "number": "5213312345678", + "exists": true + } + 400: + description: Invalid Number or Not on WhatsApp + /chat/delete: post: tags: diff --git a/static/dashboard/index.html b/static/dashboard/index.html index 621fe66c..5b8e4b51 100644 --- a/static/dashboard/index.html +++ b/static/dashboard/index.html @@ -175,6 +175,35 @@

Instances Management

+
Session
+
+ +
+
+ Session +
Connect
+
Start WhatsApp session (Scan QR).
+
+
+ +
+
+ Session +
Disconnect
+
Disconnect from WS (Pause).
+
+
+ +
+
+ Session +
Logout
+
Close session and remove credentials.
+
+
+ +
+
Chat
@@ -825,6 +854,59 @@

HMAC Configuration (Optional)

+ + + + + + + +