Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions docs/wiki/guias-api/api-messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ delay: 0
| Tipo | Formatos Aceitos | Observações |
|------|------------------|-------------|
| `image` | JPG, PNG, WebP | WebP convertido para JPEG |
| `video` | MP4 | Apenas MP4 |
| `video` | MP4, GIF | MP4. **GIF** URLs são convertidos automaticamente para vídeo com `GifPlayback` |
| `audio` | Qualquer | Convertido para Opus (PTT) automaticamente |
| `document` | Qualquer | Qualquer tipo de arquivo |

Expand Down Expand Up @@ -334,7 +334,8 @@ Envia um sticker (figurinha) via URL.
```json
{
"number": "5511999999999",
"sticker": "https://example.com/sticker.webp"
"sticker": "https://example.com/video.mp4",
"transparentColor": "#000000"
}
```

Expand All @@ -343,9 +344,13 @@ Envia um sticker (figurinha) via URL.
| Campo | Tipo | Obrigatório | Descrição |
|-------|------|-------------|-----------|
| `number` | string | ✅ Sim | Número do destinatário |
| `sticker` | string | ✅ Sim | URL da imagem (convertida para WebP automaticamente) |
| `sticker` | string | ✅ Sim | URL da imagem ou vídeo (convertida para WebP automaticamente) |
| `transparentColor` | string | ❌ Não | Cor em Hex (ex: #000000) para tornar transparente (útil para vídeos) |

**Nota**: O sistema converte automaticamente a imagem para o formato WebP (formato de sticker do WhatsApp).
**Nota**:
- O sistema converte automaticamente imagens e **vídeos (MP4)** para o formato WebP (formato de sticker do WhatsApp).
- Vídeos são convertidos para **stickers animados**.
- O parâmetro `transparentColor` permite remover o fundo de uma cor específica durante a conversão.

**Resposta de Sucesso (200)**:
```json
Expand All @@ -362,13 +367,24 @@ Envia um sticker (figurinha) via URL.

**Exemplo cURL**:
```bash
# Sticker estático (Imagem)
curl -X POST http://localhost:4000/send/sticker \
-H "Content-Type: application/json" \
-H "apikey: SUA-CHAVE-API" \
-d '{
"number": "5511999999999",
"sticker": "https://exemplo.com/figurinha.png"
}'

# Sticker animado (Vídeo com fundo removido)
curl -X POST http://localhost:4000/send/sticker \
-H "Content-Type: application/json" \
-H "apikey: SUA-CHAVE-API" \
-d '{
"number": "5511999999999",
"sticker": "https://exemplo.com/video.mp4",
"transparentColor": "#000000"
}'
```

---
Expand Down
175 changes: 142 additions & 33 deletions pkg/sendMessage/service/send_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import (
"errors"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
"image/png"
"io"
"mime/multipart"
"net/http"
"os"
"os/exec"
"regexp"
"strconv"
Expand Down Expand Up @@ -122,14 +125,15 @@ type PollStruct struct {
}

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"`
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"`
TransparentColor string `json:"transparentColor,omitempty"`
Quoted QuotedStruct `json:"quoted"`
}

type LocationStruct struct {
Expand Down Expand Up @@ -933,14 +937,17 @@ func (s *sendService) sendMediaFileWithRetry(data *MediaStruct, fileData []byte,
}
mediaType = "ImageMessage"
case "video":
lowerMimeType := strings.ToLower(mimeType)
isGif := strings.HasPrefix(lowerMimeType, "image/gif") || strings.HasPrefix(lowerMimeType, "video/gif")
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,
Caption: proto.String(data.Caption),
URL: &uploaded.URL,
DirectPath: &uploaded.DirectPath,
Mimetype: proto.String(mimeType),
FileSHA256: uploaded.FileSHA256,
FileLength: &uploaded.FileLength,
GifPlayback: proto.Bool(isGif),
}}
} else {
media = &waE2E.Message{VideoMessage: &waE2E.VideoMessage{
Expand All @@ -952,6 +959,7 @@ func (s *sendService) sendMediaFileWithRetry(data *MediaStruct, fileData []byte,
FileEncSHA256: uploaded.FileEncSHA256,
FileSHA256: uploaded.FileSHA256,
FileLength: proto.Uint64(uint64(len(fileData))),
GifPlayback: proto.Bool(isGif),
}}
}
mediaType = "VideoMessage"
Expand Down Expand Up @@ -1217,14 +1225,17 @@ func (s *sendService) sendMediaUrlWithRetry(data *MediaStruct, instance *instanc
}
mediaType = "ImageMessage"
case "video":
lowerMimeType := strings.ToLower(mimeType)
isGif := strings.HasPrefix(lowerMimeType, "image/gif") || strings.HasPrefix(lowerMimeType, "video/gif")
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,
Caption: proto.String(data.Caption),
URL: &uploaded.URL,
DirectPath: &uploaded.DirectPath,
Mimetype: proto.String(mimeType),
FileSHA256: uploaded.FileSHA256,
FileLength: &uploaded.FileLength,
GifPlayback: proto.Bool(isGif),
}}
} else {
media = &waE2E.Message{VideoMessage: &waE2E.VideoMessage{
Expand All @@ -1236,6 +1247,7 @@ func (s *sendService) sendMediaUrlWithRetry(data *MediaStruct, instance *instanc
FileEncSHA256: uploaded.FileEncSHA256,
FileSHA256: uploaded.FileSHA256,
FileLength: proto.Uint64(uint64(len(fileData))),
GifPlayback: proto.Bool(isGif),
}}
}
mediaType = "VideoMessage"
Expand Down Expand Up @@ -1414,28 +1426,125 @@ func (s *sendService) sendPollWithRetry(data *PollStruct, instance *instance_mod
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
const (
stickerMaxDownloadSize = 10 * 1024 * 1024 // 10MB
stickerDownloadTimeout = 30 * time.Second
stickerFFmpegTimeout = 60 * time.Second
)

func isValidHexColor(s string) bool {
match, _ := regexp.MatchString(`^[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$`, s)
return match
}

resp, err := http.Get(imageData)
func convertVideoToWebP(inputData []byte, transparentColor string) ([]byte, error) {
tmpInput, err := os.CreateTemp("", "sticker-input-*.mp4")
if err != nil {
return nil, fmt.Errorf("failed to fetch image from URL: %v", err)
return nil, fmt.Errorf("failed to create temp file: %v", err)
}
defer resp.Body.Close()
defer os.Remove(tmpInput.Name())

img, _, err = image.Decode(resp.Body)
if _, err := tmpInput.Write(inputData); err != nil {
tmpInput.Close()
return nil, fmt.Errorf("failed to write to temp file: %v", err)
}

if err := tmpInput.Close(); err != nil {
return nil, fmt.Errorf("failed to close temp file: %v", err)
}

tmpOutput := tmpInput.Name() + ".webp"
defer os.Remove(tmpOutput)

// Filtros base: scale, pad, fps e loop
baseFilters := "fps=15,scale=512:512:force_original_aspect_ratio=decrease,pad=512:512:(ow-iw)/2:(oh-ih)/2:color=0x00000000"

filters := baseFilters
if transparentColor != "" {
cleanHex := strings.ReplaceAll(transparentColor, "#", "")
if !isValidHexColor(cleanHex) {
return nil, fmt.Errorf("invalid transparent color: %s", transparentColor)
}
filters = fmt.Sprintf("colorkey=0x%s:0.1:0.0,%s", cleanHex, baseFilters)
}

ctx, cancel := context.WithTimeout(context.Background(), stickerFFmpegTimeout)
defer cancel()

cmd := exec.CommandContext(ctx, "ffmpeg",
"-i", tmpInput.Name(),
"-vcodec", "libwebp",
"-filter:v", filters,
"-lossless", "0",
"-compression_level", "4",
"-q:v", "50",
"-loop", "0",
"-an",
"-f", "webp",
tmpOutput,
)

var stderr bytes.Buffer
cmd.Stderr = &stderr

if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("ffmpeg failed: %v, output: %s", err, stderr.String())
}

webpData, err := os.ReadFile(tmpOutput)
if err != nil {
return nil, fmt.Errorf("failed to decode image: %v", err)
return nil, fmt.Errorf("failed to read generated webp: %v", err)
}

var webpBuffer bytes.Buffer
err = webp.Encode(&webpBuffer, img, &webp.Options{Lossless: false, Quality: 80})
return webpData, nil
}

func convertToWebP(imageDataURL string, transparentColor string) ([]byte, error) {
client := &http.Client{
Timeout: stickerDownloadTimeout,
}

resp, err := client.Get(imageDataURL)
if err != nil {
return nil, fmt.Errorf("failed to encode image to WebP: %v", err)
return nil, fmt.Errorf("failed to fetch from URL: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch from URL: status code %d", resp.StatusCode)
}

// Limitar o tamanho da leitura para evitar exaustão de recursos
data, err := io.ReadAll(io.LimitReader(resp.Body, stickerMaxDownloadSize))
if err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err)
}

if int64(len(data)) >= stickerMaxDownloadSize {
return nil, fmt.Errorf("sticker size exceeds limit of %d bytes", stickerMaxDownloadSize)
}

mime := mimetype.Detect(data)

if mime.Is("image/webp") {
return data, nil
} else if mime.Is("video/mp4") || mime.Is("image/gif") {
return convertVideoToWebP(data, transparentColor)
} else if mime.Is("image/jpeg") || mime.Is("image/png") || mime.Is("image/jpg") {
img, _, err := image.Decode(bytes.NewReader(data))
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
}

return webpBuffer.Bytes(), nil
return nil, fmt.Errorf("unsupported format: %s", mime.String())
}

func (s *sendService) SendSticker(data *StickerStruct, instance *instance_model.Instance) (*MessageSendStruct, error) {
Expand All @@ -1448,7 +1557,7 @@ func (s *sendService) SendSticker(data *StickerStruct, instance *instance_model.
var filedata []byte

if strings.HasPrefix(data.Sticker, "http") {
webpData, err := convertToWebP(data.Sticker)
webpData, err := convertToWebP(data.Sticker, data.TransparentColor)
if err != nil {
return nil, fmt.Errorf("failed to convert image to WebP: %v", err)
}
Expand Down