From bcf25ef26c3bbd594fb42870b451420588b57ed9 Mon Sep 17 00:00:00 2001 From: moothz Date: Sat, 4 Apr 2026 00:18:00 -0300 Subject: [PATCH 1/2] fix(sticker): register image decoders and add GIF support --- pkg/sendMessage/service/send_service.go | 112 +++++++++++++++++++----- 1 file changed, 91 insertions(+), 21 deletions(-) diff --git a/pkg/sendMessage/service/send_service.go b/pkg/sendMessage/service/send_service.go index 09db21a..d7f5586 100644 --- a/pkg/sendMessage/service/send_service.go +++ b/pkg/sendMessage/service/send_service.go @@ -8,6 +8,8 @@ import ( "errors" "fmt" "image" + _ "image/gif" + _ "image/jpeg" "image/png" "io" "mime/multipart" @@ -122,14 +124,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 { @@ -1414,28 +1417,95 @@ 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 +func convertVideoToWebP(inputData []byte, transparentColor string) ([]byte, error) { + tmpInput, err := os.CreateTemp("", "sticker-input-*.mp4") + if err != nil { + 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) + } + + cmd := exec.Command("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()) + } - resp, err := http.Get(imageData) + webpData, err := os.ReadFile(tmpOutput) if err != nil { - return nil, fmt.Errorf("failed to fetch image from URL: %v", err) + return nil, fmt.Errorf("failed to read generated webp: %v", err) } - defer resp.Body.Close() - img, _, err = image.Decode(resp.Body) + return webpData, nil +} + +func convertToWebP(imageDataURL string, transparentColor string) ([]byte, error) { + resp, err := http.Get(imageDataURL) if err != nil { - return nil, fmt.Errorf("failed to decode image: %v", err) + return nil, fmt.Errorf("failed to fetch from URL: %v", err) } + defer resp.Body.Close() - var webpBuffer bytes.Buffer - err = webp.Encode(&webpBuffer, img, &webp.Options{Lossless: false, Quality: 80}) + data, err := io.ReadAll(resp.Body) 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) + } + + 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) { @@ -1448,7 +1518,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) } From d2466edf4279cec07625254de68938a81b368926 Mon Sep 17 00:00:00 2001 From: moothz Date: Sat, 4 Apr 2026 16:53:26 -0300 Subject: [PATCH 2/2] fix(sticker): add timeouts, size limits and missing os import to sticker conversion --- pkg/sendMessage/service/send_service.go | 29 ++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/pkg/sendMessage/service/send_service.go b/pkg/sendMessage/service/send_service.go index d7f5586..6546798 100644 --- a/pkg/sendMessage/service/send_service.go +++ b/pkg/sendMessage/service/send_service.go @@ -14,6 +14,7 @@ import ( "io" "mime/multipart" "net/http" + "os" "os/exec" "regexp" "strconv" @@ -1417,6 +1418,12 @@ func (s *sendService) sendPollWithRetry(data *PollStruct, instance *instance_mod return nil, fmt.Errorf("failed to send poll after %d attempts", maxRetries) } +const ( + stickerMaxDownloadSize = 10 * 1024 * 1024 // 10MB + stickerDownloadTimeout = 30 * time.Second + stickerFFmpegTimeout = 60 * time.Second +) + func convertVideoToWebP(inputData []byte, transparentColor string) ([]byte, error) { tmpInput, err := os.CreateTemp("", "sticker-input-*.mp4") if err != nil { @@ -1445,7 +1452,10 @@ func convertVideoToWebP(inputData []byte, transparentColor string) ([]byte, erro filters = fmt.Sprintf("colorkey=0x%s:0.1:0.0,%s", cleanHex, baseFilters) } - cmd := exec.Command("ffmpeg", + ctx, cancel := context.WithTimeout(context.Background(), stickerFFmpegTimeout) + defer cancel() + + cmd := exec.CommandContext(ctx, "ffmpeg", "-i", tmpInput.Name(), "-vcodec", "libwebp", "-filter:v", filters, @@ -1474,17 +1484,30 @@ func convertVideoToWebP(inputData []byte, transparentColor string) ([]byte, erro } func convertToWebP(imageDataURL string, transparentColor string) ([]byte, error) { - resp, err := http.Get(imageDataURL) + 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() - data, err := io.ReadAll(resp.Body) + 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") {