Add option to bypass homeserver for Discord media
This commit is contained in:
@@ -288,13 +288,19 @@ func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, ur
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) getEmojiMXCByDiscordID(emojiID, name string, animated bool) id.ContentURI {
|
func (portal *Portal) getEmojiMXCByDiscordID(emojiID, name string, animated bool) id.ContentURI {
|
||||||
var url, mimeType string
|
var url, mimeType, ext string
|
||||||
if animated {
|
if animated {
|
||||||
url = discordgo.EndpointEmojiAnimated(emojiID)
|
url = discordgo.EndpointEmojiAnimated(emojiID)
|
||||||
mimeType = "image/gif"
|
mimeType = "image/gif"
|
||||||
|
ext = "gif"
|
||||||
} else {
|
} else {
|
||||||
url = discordgo.EndpointEmoji(emojiID)
|
url = discordgo.EndpointEmoji(emojiID)
|
||||||
mimeType = "image/png"
|
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{
|
dbFile, err := portal.bridge.copyAttachmentToMatrix(portal.MainIntent(), url, false, AttachmentMeta{
|
||||||
AttachmentID: emojiID,
|
AttachmentID: emojiID,
|
||||||
|
|||||||
113
config/bridge.go
113
config/bridge.go
@@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
|
||||||
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BridgeConfig struct {
|
type BridgeConfig struct {
|
||||||
@@ -50,7 +51,10 @@ 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 {
|
|
||||||
|
MediaPatterns MediaPatterns `yaml:"media_patterns"`
|
||||||
|
|
||||||
|
AnimatedSticker struct {
|
||||||
Target string `yaml:"target"`
|
Target string `yaml:"target"`
|
||||||
Args struct {
|
Args struct {
|
||||||
Width int `yaml:"width"`
|
Width int `yaml:"width"`
|
||||||
@@ -89,6 +93,113 @@ type BridgeConfig struct {
|
|||||||
guildNameTemplate *template.Template `yaml:"-"`
|
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 {
|
type BackfillLimitPart struct {
|
||||||
DM int `yaml:"dm"`
|
DM int `yaml:"dm"`
|
||||||
Channel int `yaml:"channel"`
|
Channel int `yaml:"channel"`
|
||||||
|
|||||||
@@ -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_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.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.Str, "bridge", "animated_sticker", "target")
|
||||||
helper.Copy(up.Int, "bridge", "animated_sticker", "args", "width")
|
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", "height")
|
||||||
|
|||||||
@@ -145,6 +145,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
|
||||||
|
# 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.
|
# Settings for converting animated stickers.
|
||||||
animated_sticker:
|
animated_sticker:
|
||||||
# Format to which animated stickers should be converted.
|
# Format to which animated stickers should be converted.
|
||||||
|
|||||||
@@ -66,10 +66,6 @@ func (portal *Portal) convertDiscordFile(typeName string, intent *appservice.Int
|
|||||||
content.Info.Width = dbFile.Width
|
content.Info.Width = dbFile.Width
|
||||||
content.Info.Height = dbFile.Height
|
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 {
|
if dbFile.DecryptionInfo != nil {
|
||||||
content.File = &event.EncryptedFileInfo{
|
content.File = &event.EncryptedFileInfo{
|
||||||
EncryptedFile: *dbFile.DecryptionInfo,
|
EncryptedFile: *dbFile.DecryptionInfo,
|
||||||
@@ -78,8 +74,14 @@ func (portal *Portal) convertDiscordFile(typeName string, intent *appservice.Int
|
|||||||
} else {
|
} else {
|
||||||
content.URL = dbFile.MXC.CUString()
|
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 {
|
if content.Info.Width > content.Info.Height {
|
||||||
content.Info.Height /= content.Info.Width / DiscordStickerSize
|
content.Info.Height /= content.Info.Width / DiscordStickerSize
|
||||||
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
|
content.Info.Height = DiscordStickerSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return content
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) convertDiscordSticker(intent *appservice.IntentAPI, sticker *discordgo.Sticker) *ConvertedMessage {
|
func (portal *Portal) convertDiscordSticker(intent *appservice.IntentAPI, sticker *discordgo.Sticker) *ConvertedMessage {
|
||||||
var mime string
|
var mime, ext string
|
||||||
switch sticker.FormatType {
|
switch sticker.FormatType {
|
||||||
case discordgo.StickerFormatTypePNG:
|
case discordgo.StickerFormatTypePNG:
|
||||||
mime = "image/png"
|
mime = "image/png"
|
||||||
|
ext = "png"
|
||||||
case discordgo.StickerFormatTypeAPNG:
|
case discordgo.StickerFormatTypeAPNG:
|
||||||
mime = "image/apng"
|
mime = "image/apng"
|
||||||
|
ext = "png"
|
||||||
case discordgo.StickerFormatTypeLottie:
|
case discordgo.StickerFormatTypeLottie:
|
||||||
mime = "application/json"
|
mime = "application/json"
|
||||||
|
ext = "json"
|
||||||
case discordgo.StickerFormatTypeGIF:
|
case discordgo.StickerFormatTypeGIF:
|
||||||
mime = "image/gif"
|
mime = "image/gif"
|
||||||
|
ext = "gif"
|
||||||
default:
|
default:
|
||||||
portal.log.Warnfln("Unknown sticker format %d in %s", sticker.FormatType, sticker.ID)
|
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{
|
return &ConvertedMessage{
|
||||||
AttachmentID: sticker.ID,
|
AttachmentID: sticker.ID,
|
||||||
Type: event.EventSticker,
|
Type: event.EventSticker,
|
||||||
Content: portal.convertDiscordFile("sticker", intent, sticker.ID, sticker.URL(), &event.MessageEventContent{
|
Content: content,
|
||||||
Body: sticker.Name, // TODO find description from somewhere?
|
|
||||||
Info: &event.FileInfo{
|
|
||||||
MimeType: mime,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +172,12 @@ func (portal *Portal) convertDiscordAttachment(intent *appservice.IntentAPI, att
|
|||||||
default:
|
default:
|
||||||
content.MsgType = event.MsgFile
|
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{
|
return &ConvertedMessage{
|
||||||
AttachmentID: att.ID,
|
AttachmentID: att.ID,
|
||||||
Type: event.EventMessage,
|
Type: event.EventMessage,
|
||||||
|
|||||||
20
puppet.go
20
puppet.go
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
@@ -224,12 +225,21 @@ func (puppet *Puppet) UpdateAvatar(info *discordgo.User) bool {
|
|||||||
puppet.AvatarSet = false
|
puppet.AvatarSet = false
|
||||||
puppet.AvatarURL = id.ContentURI{}
|
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) {
|
if puppet.Avatar != "" && (puppet.AvatarURL.IsEmpty() || avatarChanged) {
|
||||||
url, err := uploadAvatar(puppet.DefaultIntent(), info.AvatarURL(""))
|
downloadURL := discordgo.EndpointUserAvatar(info.ID, info.Avatar)
|
||||||
if err != nil {
|
ext := "png"
|
||||||
puppet.log.Warn().Err(err).Str("avatar_id", puppet.Avatar).Msg("Failed to reupload user avatar")
|
if strings.HasPrefix(info.Avatar, "a_") {
|
||||||
return true
|
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
|
puppet.AvatarURL = url
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user