From f6f6ed29ecee4d901b4e914b2b9e5525abb35721 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 26 Apr 2023 01:39:17 +0300 Subject: [PATCH] Add option to bypass homeserver for Discord media --- attachments.go | 8 +++- config/bridge.go | 113 +++++++++++++++++++++++++++++++++++++++++++- config/upgrade.go | 5 ++ example-config.yaml | 14 ++++++ portal_convert.go | 47 ++++++++++++------ puppet.go | 20 ++++++-- 6 files changed, 186 insertions(+), 21 deletions(-) diff --git a/attachments.go b/attachments.go index ad8d167..128ec6b 100644 --- a/attachments.go +++ b/attachments.go @@ -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, diff --git a/config/bridge.go b/config/bridge.go index 495dd3b..be5784a 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -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"` diff --git a/config/upgrade.go b/config/upgrade.go index b4034da..dfbb47c 100644 --- a/config/upgrade.go +++ b/config/upgrade.go @@ -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") diff --git a/example-config.yaml b/example-config.yaml index d6e0028..19afa73 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -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. diff --git a/portal_convert.go b/portal_convert.go index 233ff9e..1ea983d 100644 --- a/portal_convert.go +++ b/portal_convert.go @@ -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, diff --git a/puppet.go b/puppet.go index 70fd894..ac1e552 100644 --- a/puppet.go +++ b/puppet.go @@ -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 }