Skip to content
Closed
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
135 changes: 114 additions & 21 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 @@ -1414,28 +1418,117 @@ 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
)

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 os.Remove(tmpInput.Name())

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, "#", "")
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())
}
defer resp.Body.Close()

img, _, err = image.Decode(resp.Body)
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)
}

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 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)
}

var webpBuffer bytes.Buffer
err = webp.Encode(&webpBuffer, img, &webp.Options{Lossless: false, Quality: 80})
// 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 encode image to WebP: %v", err)
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 +1541,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