Files
mirrors-mautrix-telegram/pkg/connector/media/sticker.go

235 lines
6.2 KiB
Go

// mautrix-telegram - A Matrix-Telegram puppeting bridge.
// Copyright (C) 2025 Sumner Evans
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package media
import (
"context"
"fmt"
"io"
"os"
"strconv"
"github.com/klauspost/compress/gzip"
"github.com/rs/zerolog"
"go.mau.fi/util/ffmpeg"
"go.mau.fi/util/lottie"
)
type AnimatedStickerConfig struct {
Target string `yaml:"target"`
ConvertFromWebm bool `yaml:"convert_from_webm"`
Args struct {
Width int `yaml:"width"`
Height int `yaml:"height"`
FPS int `yaml:"fps"`
} `yaml:"args"`
}
type ConvertedSticker struct {
Success bool
NewPath string
MIMEType string
ThumbnailData []byte
ThumbnailMIMEType string
Width int
Height int
Size int
}
func (c *AnimatedStickerConfig) convertWebm(ctx context.Context, src *os.File) *ConvertedSticker {
if !c.ConvertFromWebm || c.Target == "webm" {
return nil
}
log := zerolog.Ctx(ctx).With().Str("animated_sticker_target", c.Target).Logger()
if !ffmpeg.Supported() {
log.Warn().Msg("Not converting webm sticker as ffmpeg is not installed")
return nil
}
var newPath string
var err error
switch c.Target {
case "png":
newPath, err = ffmpeg.ConvertPath(
ctx, src.Name(), ".png",
[]string{"-ss", "0", "-c:v", "libvpx-vp9"},
[]string{"-frames:v", "1"},
false,
)
case "gif":
newPath, err = ffmpeg.ConvertPath(
ctx, src.Name(), ".gif",
[]string{"-c:v", "libvpx-vp9"},
[]string{"-vf", "split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse"},
false,
)
case "webp":
newPath, err = ffmpeg.ConvertPath(
ctx, src.Name(), ".webp",
[]string{"-c:v", "libvpx-vp9"},
[]string{"-loop", "0"},
false,
)
default:
log.Error().Msg("Unknown target format for webm conversion")
return nil
}
if err != nil {
log.Err(err).Msg("Failed to convert webm sticker")
return nil
}
var outputSize int64
stat, err := os.Stat(newPath)
if err != nil {
log.Err(err).Msg("Failed to stat converted sticker")
} else {
outputSize = stat.Size()
}
_ = src.Close()
return &ConvertedSticker{
Success: true,
NewPath: newPath,
MIMEType: "image/" + c.Target,
Width: c.Args.Width,
Height: c.Args.Height,
Size: int(outputSize),
}
}
func CompressGZip(src *os.File) (replPath string, err error) {
tempFile, err := os.CreateTemp("", "telegram-sticker-gzip-*.tgs")
if err != nil {
return "", fmt.Errorf("failed to create temp file: %w", err)
}
writer := gzip.NewWriter(tempFile)
defer func() {
_ = tempFile.Close()
_ = writer.Close()
if replPath == "" {
_ = os.Remove(tempFile.Name())
}
}()
_, err = io.Copy(writer, src)
if err != nil {
return "", fmt.Errorf("failed to compress lottie gzip: %w", err)
}
err = writer.Close()
if err != nil {
return "", fmt.Errorf("failed to close gzip writer: %w", err)
}
return tempFile.Name(), nil
}
func extractGZip(src *os.File) (*ConvertedSticker, error) {
reader, err := gzip.NewReader(src)
if err != nil {
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
}
defer func() {
_ = reader.Close()
}()
replFile, err := os.OpenFile(src.Name()+".json", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return nil, fmt.Errorf("failed to create temp file: %w", err)
}
defer func() {
_ = replFile.Close()
}()
n, err := io.Copy(replFile, reader)
if err != nil {
return nil, fmt.Errorf("failed to extract lottie gzip: %w", err)
}
return &ConvertedSticker{
Success: true,
NewPath: replFile.Name(),
MIMEType: "video/lottie+json",
Size: int(n),
}, nil
}
func (c *AnimatedStickerConfig) convert(ctx context.Context, src *os.File) *ConvertedSticker {
log := zerolog.Ctx(ctx).With().Str("animated_sticker_target", c.Target).Logger()
if c.Target == "disable" {
converted, err := extractGZip(src)
if err != nil {
log.Err(err).Msg("Failed to extract lottie sticker")
}
return converted
}
if !lottie.Supported() {
log.Warn().Msg("Not converting lottie sticker as lottieconverter is not installed")
return nil
} else if (c.Target == "webp" || c.Target == "webm") && !ffmpeg.Supported() {
log.Warn().Msg("Not converting lottie sticker as target is webp/webm, but ffmpeg is not installed")
return nil
}
outputFilename := src.Name() + "." + c.Target
var thumbnailData []byte
var mimeType, thumbnailMIMEType string
var err error
switch c.Target {
case "png":
mimeType = "image/png"
err = lottie.Convert(ctx, src, outputFilename, nil, c.Target, c.Args.Width, c.Args.Height, "1")
case "gif":
mimeType = "image/gif"
err = lottie.Convert(ctx, src, outputFilename, nil, c.Target, c.Args.Width, c.Args.Height, strconv.Itoa(c.Args.FPS))
case "webm", "webp":
thumbnailMIMEType = "image/png"
if c.Target == "webm" {
mimeType = "video/webm"
} else {
mimeType = "image/webp"
}
thumbnailData, err = lottie.FFmpegConvert(ctx, src, outputFilename, c.Args.Width, c.Args.Height, c.Args.FPS)
if err != nil {
break
}
default:
log.Error().Msg("Unknown target format")
return nil
}
if err != nil {
_ = os.Remove(outputFilename)
log.Err(err).Msg("Failed to convert animated sticker")
return nil
}
var outputSize int64
stat, err := os.Stat(outputFilename)
if err != nil {
log.Err(err).Msg("Failed to stat converted sticker")
} else {
outputSize = stat.Size()
}
_ = src.Close()
return &ConvertedSticker{
Success: true,
NewPath: outputFilename,
MIMEType: mimeType,
ThumbnailData: thumbnailData,
ThumbnailMIMEType: thumbnailMIMEType,
Width: c.Args.Width,
Height: c.Args.Height,
Size: int(outputSize),
}
}