Add option to bypass homeserver for Discord media

This commit is contained in:
Tulir Asokan 2023-04-26 01:39:17 +03:00
parent f247c679de
commit f6f6ed29ec
6 changed files with 186 additions and 21 deletions

View file

@ -288,13 +288,19 @@ func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, ur
}
func (portal *Portal) getEmojiMXCByDiscordID(emojiID, name string, animated bool) id.ContentURI {
var url, mimeType string
var url, mimeType, ext string
if animated {
url = discordgo.EndpointEmojiAnimated(emojiID)
mimeType = "image/gif"
ext = "gif"
} else {
url = discordgo.EndpointEmoji(emojiID)
mimeType = "image/png"
ext = "png"
}
mxc := portal.bridge.Config.Bridge.MediaPatterns.Emoji(emojiID, ext)
if !mxc.IsEmpty() {
return mxc
}
dbFile, err := portal.bridge.copyAttachmentToMatrix(portal.MainIntent(), url, false, AttachmentMeta{
AttachmentID: emojiID,

View file

@ -25,6 +25,7 @@ import (
"github.com/bwmarrin/discordgo"
"maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/id"
)
type BridgeConfig struct {
@ -50,7 +51,10 @@ type BridgeConfig struct {
DeletePortalOnChannelDelete bool `yaml:"delete_portal_on_channel_delete"`
DeleteGuildOnLeave bool `yaml:"delete_guild_on_leave"`
FederateRooms bool `yaml:"federate_rooms"`
AnimatedSticker struct {
MediaPatterns MediaPatterns `yaml:"media_patterns"`
AnimatedSticker struct {
Target string `yaml:"target"`
Args struct {
Width int `yaml:"width"`
@ -89,6 +93,113 @@ type BridgeConfig struct {
guildNameTemplate *template.Template `yaml:"-"`
}
type MediaPatterns struct {
Enabled bool `yaml:"enabled"`
TplAttachments string `yaml:"attachments"`
TplEmojis string `yaml:"emojis"`
TplStickers string `yaml:"stickers"`
TplAvatars string `yaml:"avatars"`
attachments *template.Template `yaml:"-"`
emojis *template.Template `yaml:"-"`
stickers *template.Template `yaml:"-"`
avatars *template.Template `yaml:"-"`
}
type umMediaPatterns MediaPatterns
func (mp *MediaPatterns) UnmarshalYAML(unmarshal func(interface{}) error) error {
err := unmarshal((*umMediaPatterns)(mp))
if err != nil {
return err
}
tpl := template.New("media_patterns")
pairs := []struct {
ptr **template.Template
name string
template string
}{
{&mp.attachments, "attachments", mp.TplAttachments},
{&mp.emojis, "emojis", mp.TplEmojis},
{&mp.stickers, "stickers", mp.TplStickers},
{&mp.avatars, "avatars", mp.TplAvatars},
}
for _, pair := range pairs {
if pair.template == "" {
continue
}
*pair.ptr, err = tpl.New(pair.name).Parse(pair.template)
if err != nil {
return err
}
}
return nil
}
type attachmentParams struct {
ChannelID string
AttachmentID string
FileName string
}
type emojiStickerParams struct {
ID string
Ext string
}
type avatarParams struct {
UserID string
AvatarID string
Ext string
}
func (mp *MediaPatterns) execute(tpl *template.Template, params any) id.ContentURI {
if tpl == nil || !mp.Enabled {
return id.ContentURI{}
}
var out strings.Builder
err := tpl.Execute(&out, params)
if err != nil {
panic(err)
}
uri, err := id.ParseContentURI(out.String())
if err != nil {
panic(err)
}
return uri
}
func (mp *MediaPatterns) Attachment(channelID, attachmentID, filename string) id.ContentURI {
return mp.execute(mp.attachments, attachmentParams{
ChannelID: channelID,
AttachmentID: attachmentID,
FileName: filename,
})
}
func (mp *MediaPatterns) Emoji(emojiID, ext string) id.ContentURI {
return mp.execute(mp.emojis, emojiStickerParams{
ID: emojiID,
Ext: ext,
})
}
func (mp *MediaPatterns) Sticker(stickerID, ext string) id.ContentURI {
return mp.execute(mp.stickers, emojiStickerParams{
ID: stickerID,
Ext: ext,
})
}
func (mp *MediaPatterns) Avatar(userID, avatarID, ext string) id.ContentURI {
return mp.execute(mp.avatars, avatarParams{
UserID: userID,
AvatarID: avatarID,
Ext: ext,
})
}
type BackfillLimitPart struct {
DM int `yaml:"dm"`
Channel int `yaml:"channel"`

View file

@ -55,6 +55,11 @@ func DoUpgrade(helper *up.Helper) {
helper.Copy(up.Bool, "bridge", "delete_portal_on_channel_delete")
helper.Copy(up.Bool, "bridge", "delete_guild_on_leave")
helper.Copy(up.Bool, "bridge", "federate_rooms")
helper.Copy(up.Bool, "bridge", "media_patterns", "enabled")
helper.Copy(up.Str|up.Null, "bridge", "media_patterns", "attachments")
helper.Copy(up.Str|up.Null, "bridge", "media_patterns", "emojis")
helper.Copy(up.Str|up.Null, "bridge", "media_patterns", "stickers")
helper.Copy(up.Str|up.Null, "bridge", "media_patterns", "avatars")
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")

View file

@ -145,6 +145,20 @@ bridge:
# Whether or not created rooms should have federation enabled.
# If false, created portal rooms will never be federated.
federate_rooms: true
# Patterns for converting Discord media to custom mxc:// URIs instead of reuploading.
# Each of the patterns can be set to null to disable custom URIs for that type of media.
# More details can be found at https://docs.mau.fi/bridges/go/discord/direct-media.html
media_patterns:
# Should custom mxc:// URIs be used instead of reuploading media?
enabled: false
# Pattern for normal message attachments.
attachments: mxc://discord-media.mau.dev/attachments|{{.ChannelID}}|{{.AttachmentID}}|{{.FileName}}
# Pattern for custom emojis.
emojis: mxc://discord-media.mau.dev/emojis|{{.ID}}.{{.Ext}}
# Pattern for stickers. Note that animated lottie stickers will not be converted if this is enabled.
stickers: mxc://discord-media.mau.dev/stickers|{{.ID}}.{{.Ext}}
# Pattern for static user avatars.
avatars: mxc://discord-media.mau.dev/avatars|{{.UserID}}|{{.AvatarID}}.{{.Ext}}
# Settings for converting animated stickers.
animated_sticker:
# Format to which animated stickers should be converted.

View file

@ -66,10 +66,6 @@ func (portal *Portal) convertDiscordFile(typeName string, intent *appservice.Int
content.Info.Width = dbFile.Width
content.Info.Height = dbFile.Height
}
if content.Info.Width == 0 && content.Info.Height == 0 && typeName == "sticker" {
content.Info.Width = DiscordStickerSize
content.Info.Height = DiscordStickerSize
}
if dbFile.DecryptionInfo != nil {
content.File = &event.EncryptedFileInfo{
EncryptedFile: *dbFile.DecryptionInfo,
@ -78,8 +74,14 @@ func (portal *Portal) convertDiscordFile(typeName string, intent *appservice.Int
} else {
content.URL = dbFile.MXC.CUString()
}
return content
}
if typeName == "sticker" && (content.Info.Width > DiscordStickerSize || content.Info.Height > DiscordStickerSize) {
func (portal *Portal) cleanupConvertedStickerInfo(content *event.MessageEventContent) {
if content.Info.Width == 0 && content.Info.Height == 0 {
content.Info.Width = DiscordStickerSize
content.Info.Height = DiscordStickerSize
} else if content.Info.Width > DiscordStickerSize || content.Info.Height > DiscordStickerSize {
if content.Info.Width > content.Info.Height {
content.Info.Height /= content.Info.Width / DiscordStickerSize
content.Info.Width = DiscordStickerSize
@ -91,32 +93,44 @@ func (portal *Portal) convertDiscordFile(typeName string, intent *appservice.Int
content.Info.Height = DiscordStickerSize
}
}
return content
}
func (portal *Portal) convertDiscordSticker(intent *appservice.IntentAPI, sticker *discordgo.Sticker) *ConvertedMessage {
var mime string
var mime, ext string
switch sticker.FormatType {
case discordgo.StickerFormatTypePNG:
mime = "image/png"
ext = "png"
case discordgo.StickerFormatTypeAPNG:
mime = "image/apng"
ext = "png"
case discordgo.StickerFormatTypeLottie:
mime = "application/json"
ext = "json"
case discordgo.StickerFormatTypeGIF:
mime = "image/gif"
ext = "gif"
default:
portal.log.Warnfln("Unknown sticker format %d in %s", sticker.FormatType, sticker.ID)
}
content := &event.MessageEventContent{
Body: sticker.Name, // TODO find description from somewhere?
Info: &event.FileInfo{
MimeType: mime,
},
}
mxc := portal.bridge.Config.Bridge.MediaPatterns.Sticker(sticker.ID, ext)
if mxc.IsEmpty() {
content = portal.convertDiscordFile("sticker", intent, sticker.ID, sticker.URL(), content)
} else {
content.URL = mxc.CUString()
}
portal.cleanupConvertedStickerInfo(content)
return &ConvertedMessage{
AttachmentID: sticker.ID,
Type: event.EventSticker,
Content: portal.convertDiscordFile("sticker", intent, sticker.ID, sticker.URL(), &event.MessageEventContent{
Body: sticker.Name, // TODO find description from somewhere?
Info: &event.FileInfo{
MimeType: mime,
},
}),
Content: content,
}
}
@ -158,7 +172,12 @@ func (portal *Portal) convertDiscordAttachment(intent *appservice.IntentAPI, att
default:
content.MsgType = event.MsgFile
}
content = portal.convertDiscordFile("attachment", intent, att.ID, att.URL, content)
mxc := portal.bridge.Config.Bridge.MediaPatterns.Attachment(portal.Key.ChannelID, att.ID, att.Filename)
if mxc.IsEmpty() {
content = portal.convertDiscordFile("attachment", intent, att.ID, att.URL, content)
} else {
content.URL = mxc.CUString()
}
return &ConvertedMessage{
AttachmentID: att.ID,
Type: event.EventMessage,

View file

@ -3,6 +3,7 @@ package main
import (
"fmt"
"regexp"
"strings"
"sync"
"github.com/bwmarrin/discordgo"
@ -224,12 +225,21 @@ func (puppet *Puppet) UpdateAvatar(info *discordgo.User) bool {
puppet.AvatarSet = false
puppet.AvatarURL = id.ContentURI{}
// TODO should we just use discord's default avatars for users with no avatar?
if puppet.Avatar != "" && (puppet.AvatarURL.IsEmpty() || avatarChanged) {
url, err := uploadAvatar(puppet.DefaultIntent(), info.AvatarURL(""))
if err != nil {
puppet.log.Warn().Err(err).Str("avatar_id", puppet.Avatar).Msg("Failed to reupload user avatar")
return true
downloadURL := discordgo.EndpointUserAvatar(info.ID, info.Avatar)
ext := "png"
if strings.HasPrefix(info.Avatar, "a_") {
downloadURL = discordgo.EndpointUserAvatarAnimated(info.ID, info.Avatar)
ext = "gif"
}
url := puppet.bridge.Config.Bridge.MediaPatterns.Avatar(info.ID, info.Avatar, ext)
if url.IsEmpty() {
var err error
url, err = uploadAvatar(puppet.DefaultIntent(), downloadURL)
if err != nil {
puppet.log.Warn().Err(err).Str("avatar_id", puppet.Avatar).Msg("Failed to reupload user avatar")
return true
}
}
puppet.AvatarURL = url
}