Add support for converting lottie stickers
This commit is contained in:
@@ -3,6 +3,8 @@
|
|||||||
* Started automatically subscribing to bridged guilds. This fixes two problems:
|
* Started automatically subscribing to bridged guilds. This fixes two problems:
|
||||||
* Typing notifications should now work automatically in guilds.
|
* Typing notifications should now work automatically in guilds.
|
||||||
* Huge guilds now actually get messages bridged.
|
* Huge guilds now actually get messages bridged.
|
||||||
|
* Added support for converting animated lottie stickers to raster formats using
|
||||||
|
[lottieconverter](https://github.com/sot-tech/LottieConverter).
|
||||||
* Improved markdown parsing to disable more features that don't exist on Discord.
|
* Improved markdown parsing to disable more features that don't exist on Discord.
|
||||||
* Removed width from inline images (e.g. in the `guilds status` output) to
|
* Removed width from inline images (e.g. in the `guilds status` output) to
|
||||||
handle non-square images properly.
|
handle non-square images properly.
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
FROM dock.mau.dev/tulir/lottieconverter AS lottie
|
||||||
|
|
||||||
FROM golang:1-alpine3.17 AS builder
|
FROM golang:1-alpine3.17 AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev
|
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev
|
||||||
@@ -11,8 +13,11 @@ FROM alpine:3.17
|
|||||||
ENV UID=1337 \
|
ENV UID=1337 \
|
||||||
GID=1337
|
GID=1337
|
||||||
|
|
||||||
RUN apk add --no-cache ffmpeg su-exec ca-certificates olm bash jq yq curl
|
RUN apk add --no-cache ffmpeg su-exec ca-certificates olm bash jq yq curl \
|
||||||
|
zlib libpng giflib libstdc++ libgcc
|
||||||
|
|
||||||
|
COPY --from=lottie /usr/lib/librlottie.so* /usr/lib/
|
||||||
|
COPY --from=lottie /usr/local/bin/lottieconverter /usr/local/bin/lottieconverter
|
||||||
COPY --from=builder /usr/bin/mautrix-discord /usr/bin/mautrix-discord
|
COPY --from=builder /usr/bin/mautrix-discord /usr/bin/mautrix-discord
|
||||||
COPY --from=builder /build/example-config.yaml /opt/mautrix-discord/example-config.yaml
|
COPY --from=builder /build/example-config.yaml /opt/mautrix-discord/example-config.yaml
|
||||||
COPY --from=builder /build/docker-run.sh /docker-run.sh
|
COPY --from=builder /build/docker-run.sh /docker-run.sh
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
|
FROM dock.mau.dev/tulir/lottieconverter AS lottie
|
||||||
|
|
||||||
FROM alpine:3.17
|
FROM alpine:3.17
|
||||||
|
|
||||||
ENV UID=1337 \
|
ENV UID=1337 \
|
||||||
GID=1337
|
GID=1337
|
||||||
|
|
||||||
RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq
|
RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq \
|
||||||
|
zlib libpng giflib libstdc++ libgcc
|
||||||
|
|
||||||
|
COPY --from=lottie /usr/lib/librlottie.so* /usr/lib/
|
||||||
|
COPY --from=lottie /usr/local/bin/lottieconverter /usr/local/bin/lottieconverter
|
||||||
ARG EXECUTABLE=./mautrix-discord
|
ARG EXECUTABLE=./mautrix-discord
|
||||||
COPY $EXECUTABLE /usr/bin/mautrix-discord
|
COPY $EXECUTABLE /usr/bin/mautrix-discord
|
||||||
COPY ./example-config.yaml /opt/mautrix-discord/example-config.yaml
|
COPY ./example-config.yaml /opt/mautrix-discord/example-config.yaml
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
FROM dock.mau.dev/tulir/lottieconverter AS lottie
|
||||||
|
|
||||||
FROM golang:1-alpine3.17 AS builder
|
FROM golang:1-alpine3.17 AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev bash jq yq curl
|
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev bash jq yq curl \
|
||||||
|
zlib libpng giflib libstdc++ libgcc
|
||||||
|
|
||||||
|
COPY --from=lottie /usr/lib/librlottie.so* /usr/lib/
|
||||||
|
COPY --from=lottie /usr/local/bin/lottieconverter /usr/local/bin/lottieconverter
|
||||||
COPY . /build
|
COPY . /build
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
RUN go build -o /usr/bin/mautrix-discord
|
RUN go build -o /usr/bin/mautrix-discord
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -18,6 +23,7 @@ import (
|
|||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
"maunium.net/go/mautrix/util"
|
"maunium.net/go/mautrix/util"
|
||||||
|
"maunium.net/go/mautrix/util/ffmpeg"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-discord/database"
|
"go.mau.fi/mautrix-discord/database"
|
||||||
)
|
)
|
||||||
@@ -151,6 +157,7 @@ type AttachmentMeta struct {
|
|||||||
MimeType string
|
MimeType string
|
||||||
EmojiName string
|
EmojiName string
|
||||||
CopyIfMissing bool
|
CopyIfMissing bool
|
||||||
|
Converter func([]byte) ([]byte, string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var NoMeta = AttachmentMeta{}
|
var NoMeta = AttachmentMeta{}
|
||||||
@@ -160,6 +167,78 @@ type attachmentKey struct {
|
|||||||
Encrypt bool
|
Encrypt bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (br *DiscordBridge) convertLottie(data []byte) ([]byte, string, error) {
|
||||||
|
fps := br.Config.Bridge.AnimatedSticker.Args.FPS
|
||||||
|
width := br.Config.Bridge.AnimatedSticker.Args.Width
|
||||||
|
height := br.Config.Bridge.AnimatedSticker.Args.Height
|
||||||
|
target := br.Config.Bridge.AnimatedSticker.Target
|
||||||
|
var lottieTarget, outputMime string
|
||||||
|
switch target {
|
||||||
|
case "png":
|
||||||
|
lottieTarget = "png"
|
||||||
|
outputMime = "image/png"
|
||||||
|
fps = 1
|
||||||
|
case "gif":
|
||||||
|
lottieTarget = "gif"
|
||||||
|
outputMime = "image/gif"
|
||||||
|
case "webm":
|
||||||
|
lottieTarget = "pngs"
|
||||||
|
outputMime = "video/webm"
|
||||||
|
case "webp":
|
||||||
|
lottieTarget = "pngs"
|
||||||
|
outputMime = "image/webp"
|
||||||
|
case "disable":
|
||||||
|
return data, "application/json", nil
|
||||||
|
default:
|
||||||
|
return nil, "", fmt.Errorf("invalid animated sticker target %q in bridge config", br.Config.Bridge.AnimatedSticker.Target)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
tempdir, err := os.MkdirTemp("", "mautrix_discord_lottie_")
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("failed to create temp dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lottieOutput := filepath.Join(tempdir, "out_")
|
||||||
|
if lottieTarget != "pngs" {
|
||||||
|
lottieOutput = filepath.Join(tempdir, "output."+lottieTarget)
|
||||||
|
}
|
||||||
|
cmd := exec.CommandContext(ctx, "lottieconverter", "-", lottieOutput, lottieTarget, fmt.Sprintf("%dx%d", width, height), strconv.Itoa(fps))
|
||||||
|
cmd.Stdin = bytes.NewReader(data)
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("failed to run lottieconverter: %w", err)
|
||||||
|
}
|
||||||
|
var path string
|
||||||
|
if lottieTarget == "pngs" {
|
||||||
|
var videoCodec string
|
||||||
|
outputExtension := "." + target
|
||||||
|
if target == "webm" {
|
||||||
|
videoCodec = "libvpx-vp9"
|
||||||
|
} else if target == "webp" {
|
||||||
|
videoCodec = "libwebp_anim"
|
||||||
|
} else {
|
||||||
|
panic(fmt.Errorf("impossible case: unknown target %q", target))
|
||||||
|
}
|
||||||
|
path, err = ffmpeg.ConvertPath(
|
||||||
|
ctx, lottieOutput+"*.png", outputExtension,
|
||||||
|
[]string{"-framerate", strconv.Itoa(fps), "-pattern_type", "glob"},
|
||||||
|
[]string{"-c:v", videoCodec, "-pix_fmt", "yuva420p", "-f", target},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("failed to run ffmpeg: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path = lottieOutput
|
||||||
|
}
|
||||||
|
data, err = os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("failed to read converted file: %w", err)
|
||||||
|
}
|
||||||
|
return data, outputMime, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, url string, encrypt bool, meta AttachmentMeta) (returnDBFile *database.File, returnErr error) {
|
func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, url string, encrypt bool, meta AttachmentMeta) (returnDBFile *database.File, returnErr error) {
|
||||||
isCacheable := !encrypt
|
isCacheable := !encrypt
|
||||||
returnDBFile = br.DB.File.Get(url, encrypt)
|
returnDBFile = br.DB.File.Get(url, encrypt)
|
||||||
@@ -180,6 +259,14 @@ func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, ur
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if meta.Converter != nil {
|
||||||
|
data, meta.MimeType, onceErr = meta.Converter(data)
|
||||||
|
if onceErr != nil {
|
||||||
|
onceErr = fmt.Errorf("failed to convert attachment: %w", onceErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onceDBFile, onceErr = br.uploadMatrixAttachment(intent, data, url, encrypt, meta)
|
onceDBFile, onceErr = br.uploadMatrixAttachment(intent, data, url, encrypt, meta)
|
||||||
if onceErr != nil {
|
if onceErr != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ type BridgeConfig struct {
|
|||||||
DeletePortalOnChannelDelete bool `yaml:"delete_portal_on_channel_delete"`
|
DeletePortalOnChannelDelete bool `yaml:"delete_portal_on_channel_delete"`
|
||||||
DeleteGuildOnLeave bool `yaml:"delete_guild_on_leave"`
|
DeleteGuildOnLeave bool `yaml:"delete_guild_on_leave"`
|
||||||
FederateRooms bool `yaml:"federate_rooms"`
|
FederateRooms bool `yaml:"federate_rooms"`
|
||||||
|
AnimatedSticker struct {
|
||||||
|
Target string `yaml:"target"`
|
||||||
|
Args struct {
|
||||||
|
Width int `yaml:"width"`
|
||||||
|
Height int `yaml:"height"`
|
||||||
|
FPS int `yaml:"fps"`
|
||||||
|
} `yaml:"args"`
|
||||||
|
} `yaml:"animated_sticker"`
|
||||||
|
|
||||||
DoublePuppetServerMap map[string]string `yaml:"double_puppet_server_map"`
|
DoublePuppetServerMap map[string]string `yaml:"double_puppet_server_map"`
|
||||||
DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"`
|
DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"`
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ func DoUpgrade(helper *up.Helper) {
|
|||||||
helper.Copy(up.Bool, "bridge", "delete_portal_on_channel_delete")
|
helper.Copy(up.Bool, "bridge", "delete_portal_on_channel_delete")
|
||||||
helper.Copy(up.Bool, "bridge", "delete_guild_on_leave")
|
helper.Copy(up.Bool, "bridge", "delete_guild_on_leave")
|
||||||
helper.Copy(up.Bool, "bridge", "federate_rooms")
|
helper.Copy(up.Bool, "bridge", "federate_rooms")
|
||||||
|
helper.Copy(up.Str, "bridge", "animated_sticker", "target")
|
||||||
|
helper.Copy(up.Int, "bridge", "animated_sticker", "args", "width")
|
||||||
|
helper.Copy(up.Int, "bridge", "animated_sticker", "args", "height")
|
||||||
|
helper.Copy(up.Int, "bridge", "animated_sticker", "args", "fps")
|
||||||
helper.Copy(up.Map, "bridge", "double_puppet_server_map")
|
helper.Copy(up.Map, "bridge", "double_puppet_server_map")
|
||||||
helper.Copy(up.Bool, "bridge", "double_puppet_allow_discovery")
|
helper.Copy(up.Bool, "bridge", "double_puppet_allow_discovery")
|
||||||
helper.Copy(up.Map, "bridge", "login_shared_secret_map")
|
helper.Copy(up.Map, "bridge", "login_shared_secret_map")
|
||||||
|
|||||||
@@ -140,6 +140,20 @@ bridge:
|
|||||||
# Whether or not created rooms should have federation enabled.
|
# Whether or not created rooms should have federation enabled.
|
||||||
# If false, created portal rooms will never be federated.
|
# If false, created portal rooms will never be federated.
|
||||||
federate_rooms: true
|
federate_rooms: true
|
||||||
|
# Settings for converting animated stickers.
|
||||||
|
animated_sticker:
|
||||||
|
# Format to which animated stickers should be converted.
|
||||||
|
# disable - No conversion, send as-is (lottie JSON)
|
||||||
|
# png - converts to non-animated png (fastest)
|
||||||
|
# gif - converts to animated gif
|
||||||
|
# webm - converts to webm video, requires ffmpeg executable with vp9 codec and webm container support
|
||||||
|
# webp - converts to animated webp, requires ffmpeg executable with webp codec/container support
|
||||||
|
target: webp
|
||||||
|
# Arguments for converter. All converters take width and height.
|
||||||
|
args:
|
||||||
|
width: 256
|
||||||
|
height: 256
|
||||||
|
fps: 25 # only for webm, webp and gif (2, 5, 10, 20 or 25 recommended)
|
||||||
# Servers to always allow double puppeting from
|
# Servers to always allow double puppeting from
|
||||||
double_puppet_server_map:
|
double_puppet_server_map:
|
||||||
example.com: https://example.com
|
example.com: https://example.com
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -14,7 +14,7 @@ require (
|
|||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.1
|
||||||
github.com/yuin/goldmark v1.5.4
|
github.com/yuin/goldmark v1.5.4
|
||||||
maunium.net/go/maulogger/v2 v2.3.2
|
maunium.net/go/maulogger/v2 v2.3.2
|
||||||
maunium.net/go/mautrix v0.13.1-0.20230204122701-3c0e64060114
|
maunium.net/go/mautrix v0.13.1-0.20230204140716-485b4f376dc6
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -77,5 +77,5 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
|||||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||||
maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
|
maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
|
||||||
maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
|
maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
|
||||||
maunium.net/go/mautrix v0.13.1-0.20230204122701-3c0e64060114 h1:H6/OwVn9Z5PNhwzeWSvYCU/Cw3nvTbLTcvJo5HS/lyU=
|
maunium.net/go/mautrix v0.13.1-0.20230204140716-485b4f376dc6 h1:mSq0HwzhpM5XOk+YRgOsEx62AVG6N/lonmz/3iBwf+A=
|
||||||
maunium.net/go/mautrix v0.13.1-0.20230204122701-3c0e64060114/go.mod h1:3u2Fz3JY/eXVLOzxn2ODVL4rzCIjkkt0jlygEx4Qnaw=
|
maunium.net/go/mautrix v0.13.1-0.20230204140716-485b4f376dc6/go.mod h1:3u2Fz3JY/eXVLOzxn2ODVL4rzCIjkkt0jlygEx4Qnaw=
|
||||||
|
|||||||
@@ -555,7 +555,11 @@ func (portal *Portal) sendMediaFailedMessage(intent *appservice.IntentAPI, bridg
|
|||||||
const DiscordStickerSize = 160
|
const DiscordStickerSize = 160
|
||||||
|
|
||||||
func (portal *Portal) handleDiscordFile(typeName string, intent *appservice.IntentAPI, id, url string, content *event.MessageEventContent, ts time.Time, threadRelation *event.RelatesTo) *database.MessagePart {
|
func (portal *Portal) handleDiscordFile(typeName string, intent *appservice.IntentAPI, id, url string, content *event.MessageEventContent, ts time.Time, threadRelation *event.RelatesTo) *database.MessagePart {
|
||||||
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, url, portal.Encrypted, AttachmentMeta{AttachmentID: id, MimeType: content.Info.MimeType})
|
meta := AttachmentMeta{AttachmentID: id, MimeType: content.Info.MimeType}
|
||||||
|
if typeName == "sticker" && content.Info.MimeType == "application/json" {
|
||||||
|
meta.Converter = portal.bridge.convertLottie
|
||||||
|
}
|
||||||
|
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, url, portal.Encrypted, meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorEventID := portal.sendMediaFailedMessage(intent, err)
|
errorEventID := portal.sendMediaFailedMessage(intent, err)
|
||||||
if errorEventID != "" {
|
if errorEventID != "" {
|
||||||
@@ -566,6 +570,9 @@ func (portal *Portal) handleDiscordFile(typeName string, intent *appservice.Inte
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if typeName == "sticker" && content.Info.MimeType == "application/json" {
|
||||||
|
content.Info.MimeType = dbFile.MimeType
|
||||||
|
}
|
||||||
content.Info.Size = dbFile.Size
|
content.Info.Size = dbFile.Size
|
||||||
if content.Info.Width == 0 && content.Info.Height == 0 {
|
if content.Info.Width == 0 && content.Info.Height == 0 {
|
||||||
content.Info.Width = dbFile.Width
|
content.Info.Width = dbFile.Width
|
||||||
|
|||||||
Reference in New Issue
Block a user