소스 검색

Add option to bypass homeserver for Discord media

Tulir Asokan 2 년 전
부모
커밋
f6f6ed29ec
6개의 변경된 파일186개의 추가작업 그리고 21개의 파일을 삭제
  1. 7 1
      attachments.go
  2. 112 1
      config/bridge.go
  3. 5 0
      config/upgrade.go
  4. 14 0
      example-config.yaml
  5. 33 14
      portal_convert.go
  6. 15 5
      puppet.go

+ 7 - 1
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,

+ 112 - 1
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"`

+ 5 - 0
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")

+ 14 - 0
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.

+ 33 - 14
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,

+ 15 - 5
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
 	}