From d89746d099cfb6f65137cd60a8c50decddbcc77f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 28 Jan 2026 17:05:23 +0200 Subject: [PATCH] msgconv: clean up reuploading attachments to Matrix --- pkg/attachment/attachment.go | 39 --------- pkg/connector/attachments.go | 104 ----------------------- pkg/connector/backfill.go | 2 +- pkg/connector/connector.go | 11 ++- pkg/connector/handlediscord.go | 2 +- pkg/msgconv/attachments.go | 147 +++++++++++++++++++++++++++++++++ pkg/msgconv/from-discord.go | 141 ++++++++++++++----------------- pkg/msgconv/from-matrix.go | 2 + pkg/msgconv/msgconv.go | 18 +--- 9 files changed, 228 insertions(+), 238 deletions(-) delete mode 100644 pkg/attachment/attachment.go delete mode 100644 pkg/connector/attachments.go create mode 100644 pkg/msgconv/attachments.go diff --git a/pkg/attachment/attachment.go b/pkg/attachment/attachment.go deleted file mode 100644 index 5890029..0000000 --- a/pkg/attachment/attachment.go +++ /dev/null @@ -1,39 +0,0 @@ -// mautrix-discord - A Matrix-Discord puppeting bridge. -// Copyright (C) 2026 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package attachment - -import ( - "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/id" -) - -// TODO(skip): These types are only in a leaf package to avoid import cycles. -// Perhaps figure out a better way to structure this so that this package is unnecessary. - -type AttachmentReupload struct { - DownloadingURL string - FileName string - MimeType string -} - -type ReuploadedAttachment struct { - AttachmentReupload - DownloadedSize int - MXC id.ContentURIString - // This can be nil if the room isn't encrypted. - EncryptedFile *event.EncryptedFileInfo -} diff --git a/pkg/connector/attachments.go b/pkg/connector/attachments.go deleted file mode 100644 index c552deb..0000000 --- a/pkg/connector/attachments.go +++ /dev/null @@ -1,104 +0,0 @@ -// mautrix-discord - A Matrix-Discord puppeting bridge. -// Copyright (C) 2026 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package connector - -import ( - "context" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "path" - "strconv" - - "github.com/bwmarrin/discordgo" - "maunium.net/go/mautrix/bridgev2" - - "go.mau.fi/mautrix-discord/pkg/attachment" -) - -func downloadDiscordAttachment(cli *http.Client, url string, maxSize int64) ([]byte, error) { - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - return nil, err - } - for key, value := range discordgo.DroidDownloadHeaders { - req.Header.Set(key, value) - } - - resp, err := cli.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode > 300 { - data, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("unexpected status %d downloading %s: %s", resp.StatusCode, url, data) - } - if resp.Header.Get("Content-Length") != "" { - length, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64) - if err != nil { - return nil, fmt.Errorf("failed to parse content length: %w", err) - } else if length > maxSize { - return nil, fmt.Errorf("attachment too large (%d > %d)", length, maxSize) - } - return io.ReadAll(resp.Body) - } else { - var mbe *http.MaxBytesError - data, err := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxSize)) - if err != nil && errors.As(err, &mbe) { - return nil, fmt.Errorf("attachment too large (over %d)", maxSize) - } - return data, err - } -} - -func (d *DiscordConnector) ReuploadMedia(ctx context.Context, intent bridgev2.MatrixAPI, portal *bridgev2.Portal, upload attachment.AttachmentReupload) (*attachment.ReuploadedAttachment, error) { - // TODO(skip): Do we need to check if we've already downloaded this media before? - // TODO(skip): Read a maximum size from the config. - data, err := downloadDiscordAttachment(http.DefaultClient, upload.DownloadingURL, 1_024*1_024*50) - if err != nil { - return nil, fmt.Errorf("couldn't download attachment for reupload: %w", err) - } - - if upload.FileName == "" { - url, err := url.Parse(upload.DownloadingURL) - if err != nil { - return nil, fmt.Errorf("couldn't parse URL to download for media reupload: %w", err) - } - fileName := path.Base(url.Path) - upload.FileName = fileName - } - - if upload.MimeType == "" { - mime := http.DetectContentType(data) - upload.MimeType = mime - } - - mxc, file, err := intent.UploadMedia(ctx, portal.MXID, data, upload.FileName, upload.MimeType) - if err != nil { - return nil, err - } - - return &attachment.ReuploadedAttachment{ - AttachmentReupload: upload, - DownloadedSize: len(data), - MXC: mxc, - EncryptedFile: file, - }, nil -} diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index f14e151..b49354d 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -100,7 +100,7 @@ func (dc *DiscordClient) FetchMessages(ctx context.Context, fetchParams bridgev2 converted = append(converted, &bridgev2.BackfillMessage{ ID: networkid.MessageID(msg.ID), - ConvertedMessage: dc.connector.MsgConv.ToMatrix(ctx, fetchParams.Portal, intent, dc.UserLogin, msg), + ConvertedMessage: dc.connector.MsgConv.ToMatrix(ctx, fetchParams.Portal, intent, dc.UserLogin, dc.Session, msg), Sender: sender, Timestamp: ts, StreamOrder: streamOrder, diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 10a625f..d866e49 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -30,14 +30,21 @@ type DiscordConnector struct { MsgConv *msgconv.MessageConverter } -var _ bridgev2.NetworkConnector = (*DiscordConnector)(nil) +var ( + _ bridgev2.NetworkConnector = (*DiscordConnector)(nil) + _ bridgev2.MaxFileSizeingNetwork = (*DiscordConnector)(nil) +) func (d *DiscordConnector) Init(bridge *bridgev2.Bridge) { d.Bridge = bridge - d.MsgConv = msgconv.NewMessageConverter(bridge, d.ReuploadMedia) + d.MsgConv = msgconv.NewMessageConverter(bridge) d.setUpProvisioningAPIs() } +func (d *DiscordConnector) SetMaxFileSize(maxSize int64) { + d.MsgConv.MaxFileSize = maxSize +} + func (d *DiscordConnector) Start(ctx context.Context) error { return nil } diff --git a/pkg/connector/handlediscord.go b/pkg/connector/handlediscord.go index 67227e4..eb328dd 100644 --- a/pkg/connector/handlediscord.go +++ b/pkg/connector/handlediscord.go @@ -63,7 +63,7 @@ var ( ) func (m *DiscordMessage) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) { - return m.Client.connector.MsgConv.ToMatrix(ctx, portal, intent, m.Client.UserLogin, m.Data), nil + return m.Client.connector.MsgConv.ToMatrix(ctx, portal, intent, m.Client.UserLogin, m.Client.Session, m.Data), nil } func (m *DiscordMessage) GetID() networkid.MessageID { diff --git a/pkg/msgconv/attachments.go b/pkg/msgconv/attachments.go new file mode 100644 index 0000000..72a7ae9 --- /dev/null +++ b/pkg/msgconv/attachments.go @@ -0,0 +1,147 @@ +// mautrix-discord - A Matrix-Discord puppeting bridge. +// Copyright (C) 2026 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package msgconv + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + + "github.com/bwmarrin/discordgo" + "github.com/rs/zerolog" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +type ReuploadedAttachment struct { + MXC id.ContentURIString + File *event.EncryptedFileInfo + Size int + FileName string + MimeType string +} + +func (d *MessageConverter) ReuploadUnknownMedia( + ctx context.Context, + url string, + allowEncryption bool, +) (*ReuploadedAttachment, error) { + return d.ReuploadMedia(ctx, url, "", "", -1, allowEncryption) +} + +func mib(size int64) float64 { + return float64(size) / 1024 / 1024 +} + +func (d *MessageConverter) ReuploadMedia( + ctx context.Context, + downloadURL string, + mimeType string, + fileName string, + estimatedSize int, + allowEncryption bool, +) (*ReuploadedAttachment, error) { + if fileName == "" { + parsedURL, err := url.Parse(downloadURL) + if err != nil { + return nil, fmt.Errorf("couldn't parse URL to detect file name: %w", err) + } + fileName = path.Base(parsedURL.Path) + } + + sess := ctx.Value(contextKeyDiscordClient).(*discordgo.Session) + httpClient := sess.Client + intent := ctx.Value(contextKeyIntent).(bridgev2.MatrixAPI) + var roomID id.RoomID + if allowEncryption { + roomID = ctx.Value(contextKeyPortal).(*bridgev2.Portal).MXID + } + + req, err := http.NewRequest(http.MethodGet, downloadURL, nil) + if err != nil { + return nil, err + } + if sess.IsUser { + for key, value := range discordgo.DroidDownloadHeaders { + req.Header.Set(key, value) + } + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode > 300 { + errBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + logEvt := zerolog.Ctx(ctx).Error(). + Str("media_url", downloadURL). + Int("status_code", resp.StatusCode) + if json.Valid(errBody) { + logEvt.RawJSON("error_json", errBody) + } else { + logEvt.Bytes("error_body", errBody) + } + logEvt.Msg("Media download failed") + return nil, fmt.Errorf("%w: unexpected status code %d", bridgev2.ErrMediaDownloadFailed, resp.StatusCode) + } else if resp.ContentLength > d.MaxFileSize { + return nil, fmt.Errorf("%w (%.2f MiB > %.2f MiB)", bridgev2.ErrMediaTooLarge, mib(resp.ContentLength), mib(d.MaxFileSize)) + } + + requireFile := mimeType == "" + var size int64 + mxc, file, err := intent.UploadMediaStream(ctx, roomID, int64(estimatedSize), requireFile, func(file io.Writer) (*bridgev2.FileStreamResult, error) { + var mbe *http.MaxBytesError + size, err = io.Copy(file, http.MaxBytesReader(nil, resp.Body, d.MaxFileSize)) + if err != nil { + if errors.As(err, &mbe) { + return nil, fmt.Errorf("%w (over %.2f MiB)", bridgev2.ErrMediaTooLarge, mib(d.MaxFileSize)) + } + return nil, err + } + if mimeType == "" { + mimeBuf := make([]byte, 512) + n, err := file.(*os.File).ReadAt(mimeBuf, 0) + if err != nil { + return nil, fmt.Errorf("couldn't read file for mime detection: %w", err) + } + mimeType = http.DetectContentType(mimeBuf[:n]) + } + return &bridgev2.FileStreamResult{ + FileName: fileName, + MimeType: mimeType, + }, nil + }) + if err != nil { + return nil, err + } + + return &ReuploadedAttachment{ + Size: int(size), + MXC: mxc, + File: file, + FileName: fileName, + MimeType: mimeType, + }, nil +} diff --git a/pkg/msgconv/from-discord.go b/pkg/msgconv/from-discord.go index b99ddfe..5dfa9dd 100644 --- a/pkg/msgconv/from-discord.go +++ b/pkg/msgconv/from-discord.go @@ -26,22 +26,36 @@ import ( "github.com/bwmarrin/discordgo" "github.com/rs/zerolog" + "go.mau.fi/util/exmaps" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" - "go.mau.fi/mautrix-discord/pkg/attachment" "go.mau.fi/mautrix-discord/pkg/discordid" ) +type contextKey int + +const ( + contextKeyPortal contextKey = iota + contextKeyIntent + contextKeyUserLogin + contextKeyDiscordClient +) + func (mc *MessageConverter) ToMatrix( ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, source *bridgev2.UserLogin, + session *discordgo.Session, msg *discordgo.Message, ) *bridgev2.ConvertedMessage { + ctx = context.WithValue(ctx, contextKeyUserLogin, source) + ctx = context.WithValue(ctx, contextKeyIntent, intent) + ctx = context.WithValue(ctx, contextKeyPortal, portal) + ctx = context.WithValue(ctx, contextKeyDiscordClient, session) predictedLength := len(msg.Attachments) + len(msg.StickerItems) if msg.Content != "" { predictedLength++ @@ -56,28 +70,26 @@ func (mc *MessageConverter) ToMatrix( Str("message_id", msg.ID). Logger().WithContext(ctx) log := zerolog.Ctx(ctx) - handledIDs := make(map[string]struct{}) + handledIDs := make(exmaps.Set[string]) for _, att := range msg.Attachments { - if _, handled := handledIDs[att.ID]; handled { + if !handledIDs.Add(att.ID) { continue } - handledIDs[att.ID] = struct{}{} log := log.With().Str("attachment_id", att.ID).Logger() - if part := mc.renderDiscordAttachment(log.WithContext(ctx), intent, portal, att); part != nil { + if part := mc.renderDiscordAttachment(log.WithContext(ctx), att); part != nil { parts = append(parts, part) } } for _, sticker := range msg.StickerItems { - if _, handled := handledIDs[sticker.ID]; handled { + if !handledIDs.Add(sticker.ID) { continue } - handledIDs[sticker.ID] = struct{}{} log := log.With().Str("sticker_id", sticker.ID).Logger() - if part := mc.renderDiscordSticker(log.WithContext(ctx), intent, sticker); part != nil { + if part := mc.renderDiscordSticker(log.WithContext(ctx), sticker); part != nil { parts = append(parts, part) } } @@ -88,17 +100,16 @@ func (mc *MessageConverter) ToMatrix( continue } // Discord deduplicates embeds by URL. It makes things easier for us too. - if _, handled := handledIDs[embed.URL]; handled { + if !handledIDs.Add(embed.URL) { continue } - handledIDs[embed.URL] = struct{}{} log := log.With(). Str("computed_embed_type", "video"). Str("embed_type", string(embed.Type)). Int("embed_index", i). Logger() - part := mc.renderDiscordVideoEmbed(log.WithContext(ctx), intent, portal, embed) + part := mc.renderDiscordVideoEmbed(log.WithContext(ctx), embed) if part != nil { parts = append(parts, part) } @@ -235,7 +246,7 @@ func (mc *MessageConverter) renderDiscordTextMessage(ctx context.Context, intent len(msg.MessageSnapshots) > 0 && msg.MessageSnapshots[0].Message != nil { // Bridge forwarded messages. - htmlParts = append(htmlParts, mc.forwardedMessageHtmlPart(ctx, portal, source, msg)) + htmlParts = append(htmlParts, mc.forwardedMessageHTMLPart(ctx, portal, source, msg)) } previews := make([]*event.BeeperLinkPreview, 0) @@ -251,10 +262,10 @@ func (mc *MessageConverter) renderDiscordTextMessage(ctx context.Context, intent switch getEmbedType(msg, embed) { case EmbedRich: log := with.Str("computed_embed_type", "rich").Logger() - htmlParts = append(htmlParts, mc.renderDiscordRichEmbed(log.WithContext(ctx), intent, portal, embed)) + htmlParts = append(htmlParts, mc.renderDiscordRichEmbed(log.WithContext(ctx), embed)) case EmbedLinkPreview: log := with.Str("computed_embed_type", "link preview").Logger() - previews = append(previews, mc.renderDiscordLinkEmbed(log.WithContext(ctx), intent, portal, embed)) + previews = append(previews, mc.renderDiscordLinkEmbed(log.WithContext(ctx), embed)) case EmbedVideo: // Video embeds are handled as separate messages via renderDiscordVideoEmbed. default: @@ -284,7 +295,7 @@ func (mc *MessageConverter) renderDiscordTextMessage(ctx context.Context, intent return &bridgev2.ConvertedMessagePart{Type: event.EventMessage, Content: &content, Extra: extraContent} } -func (mc *MessageConverter) forwardedMessageHtmlPart(ctx context.Context, portal *bridgev2.Portal, source *bridgev2.UserLogin, msg *discordgo.Message) string { +func (mc *MessageConverter) forwardedMessageHTMLPart(ctx context.Context, portal *bridgev2.Portal, source *bridgev2.UserLogin, msg *discordgo.Message) string { log := zerolog.Ctx(ctx) forwardedHTML := mc.renderDiscordMarkdownOnlyHTMLNoUnwrap(portal, msg.MessageSnapshots[0].Message.Content, true) @@ -327,7 +338,7 @@ func mediaFailedMessage(err error) *event.MessageEventContent { } } -func (mc *MessageConverter) renderDiscordVideoEmbed(ctx context.Context, intent bridgev2.MatrixAPI, portal *bridgev2.Portal, embed *discordgo.MessageEmbed) *bridgev2.ConvertedMessagePart { +func (mc *MessageConverter) renderDiscordVideoEmbed(ctx context.Context, embed *discordgo.MessageEmbed) *bridgev2.ConvertedMessagePart { var proxyURL string if embed.Video != nil { proxyURL = embed.Video.ProxyURL @@ -344,10 +355,7 @@ func (mc *MessageConverter) renderDiscordVideoEmbed(ctx context.Context, intent } } - upload := attachment.AttachmentReupload{ - DownloadingURL: proxyURL, - } - reupload, err := mc.ReuploadMedia(ctx, intent, portal, upload) + reupload, err := mc.ReuploadUnknownMedia(ctx, proxyURL, true) if err != nil { zerolog.Ctx(ctx).Err(err).Msg("Failed to copy video embed to Matrix") return &bridgev2.ConvertedMessagePart{ @@ -358,16 +366,13 @@ func (mc *MessageConverter) renderDiscordVideoEmbed(ctx context.Context, intent content := &event.MessageEventContent{ Body: embed.URL, + URL: reupload.MXC, + File: reupload.File, Info: &event.FileInfo{ MimeType: reupload.MimeType, - Size: reupload.DownloadedSize, + Size: reupload.Size, }, } - if reupload.EncryptedFile != nil { - content.File = reupload.EncryptedFile - } else { - content.URL = reupload.MXC - } if embed.Video != nil { content.MsgType = event.MsgVideo @@ -398,7 +403,7 @@ func (mc *MessageConverter) renderDiscordVideoEmbed(ctx context.Context, intent } } -func (mc *MessageConverter) renderDiscordSticker(context context.Context, intent bridgev2.MatrixAPI, sticker *discordgo.StickerItem) *bridgev2.ConvertedMessagePart { +func (mc *MessageConverter) renderDiscordSticker(ctx context.Context, sticker *discordgo.StickerItem) *bridgev2.ConvertedMessagePart { panic("unimplemented") } @@ -423,7 +428,7 @@ const ( embedFooterDateSeparator = ` • ` ) -func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent bridgev2.MatrixAPI, portal *bridgev2.Portal, embed *discordgo.MessageEmbed) string { +func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, embed *discordgo.MessageEmbed) string { log := zerolog.Ctx(ctx) var htmlParts []string if embed.Author != nil { @@ -434,9 +439,7 @@ func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent b } authorHTML = fmt.Sprintf(embedHTMLAuthorPlain, authorNameHTML) if embed.Author.ProxyIconURL != "" { - reupload, err := mc.ReuploadMedia(ctx, intent, portal, attachment.AttachmentReupload{ - DownloadingURL: embed.Author.ProxyIconURL, - }) + reupload, err := mc.ReuploadUnknownMedia(ctx, embed.Author.ProxyIconURL, false) if err != nil { log.Warn().Err(err).Msg("Failed to reupload author icon in embed") @@ -447,6 +450,7 @@ func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent b htmlParts = append(htmlParts, authorHTML) } + portal := ctx.Value(contextKeyPortal).(*bridgev2.Portal) if embed.Title != "" { var titleHTML string baseTitleHTML := mc.renderDiscordMarkdownOnlyHTML(portal, embed.Title, false) @@ -492,9 +496,7 @@ func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent b } if embed.Image != nil { - reupload, err := mc.ReuploadMedia(ctx, intent, portal, attachment.AttachmentReupload{ - DownloadingURL: embed.Image.ProxyURL, - }) + reupload, err := mc.ReuploadUnknownMedia(ctx, embed.Image.ProxyURL, false) if err != nil { log.Warn().Err(err).Msg("Failed to reupload image in embed") } else { @@ -522,9 +524,7 @@ func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent b } footerHTML = fmt.Sprintf(embedHTMLFooterPlain, html.EscapeString(embed.Footer.Text), datePart) if embed.Footer.ProxyIconURL != "" { - reupload, err := mc.ReuploadMedia(ctx, intent, portal, attachment.AttachmentReupload{ - DownloadingURL: embed.Footer.ProxyIconURL, - }) + reupload, err := mc.ReuploadUnknownMedia(ctx, embed.Footer.ProxyIconURL, false) if err != nil { log.Warn().Err(err).Msg("Failed to reupload footer icon in embed") @@ -550,10 +550,10 @@ func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent b return compiledHTML } -func (mc *MessageConverter) renderDiscordLinkEmbedImage(ctx context.Context, intent bridgev2.MatrixAPI, portal *bridgev2.Portal, url string, width, height int, preview *event.BeeperLinkPreview) { - reupload, err := mc.ReuploadMedia(ctx, intent, portal, attachment.AttachmentReupload{ - DownloadingURL: url, - }) +func (mc *MessageConverter) renderDiscordLinkEmbedImage( + ctx context.Context, url string, width, height int, preview *event.BeeperLinkPreview, +) { + reupload, err := mc.ReuploadUnknownMedia(ctx, url, true) if err != nil { zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to reupload image in URL preview, ignoring") return @@ -563,39 +563,42 @@ func (mc *MessageConverter) renderDiscordLinkEmbedImage(ctx context.Context, int preview.ImageWidth = event.IntOrString(width) preview.ImageHeight = event.IntOrString(height) } - preview.ImageSize = event.IntOrString(reupload.DownloadedSize) + preview.ImageSize = event.IntOrString(reupload.Size) preview.ImageType = reupload.MimeType - if reupload.EncryptedFile != nil { - preview.ImageEncryption = &event.EncryptedFileInfo{ - EncryptedFile: reupload.EncryptedFile.EncryptedFile, - URL: reupload.MXC, - } - } + preview.ImageURL, preview.ImageEncryption = reupload.MXC, reupload.File } -func (mc *MessageConverter) renderDiscordLinkEmbed(ctx context.Context, intent bridgev2.MatrixAPI, portal *bridgev2.Portal, embed *discordgo.MessageEmbed) *event.BeeperLinkPreview { +func (mc *MessageConverter) renderDiscordLinkEmbed(ctx context.Context, embed *discordgo.MessageEmbed) *event.BeeperLinkPreview { var preview event.BeeperLinkPreview preview.MatchedURL = embed.URL preview.Title = embed.Title preview.Description = embed.Description if embed.Image != nil { - mc.renderDiscordLinkEmbedImage(ctx, intent, portal, embed.Image.ProxyURL, embed.Image.Width, embed.Image.Height, &preview) + mc.renderDiscordLinkEmbedImage(ctx, embed.Image.ProxyURL, embed.Image.Width, embed.Image.Height, &preview) } else if embed.Thumbnail != nil { - mc.renderDiscordLinkEmbedImage(ctx, intent, portal, embed.Thumbnail.ProxyURL, embed.Thumbnail.Width, embed.Thumbnail.Height, &preview) + mc.renderDiscordLinkEmbedImage(ctx, embed.Thumbnail.ProxyURL, embed.Thumbnail.Width, embed.Thumbnail.Height, &preview) } return &preview } -func (mc *MessageConverter) renderDiscordAttachment(ctx context.Context, intent bridgev2.MatrixAPI, portal *bridgev2.Portal, att *discordgo.MessageAttachment) *bridgev2.ConvertedMessagePart { +func (mc *MessageConverter) renderDiscordAttachment(ctx context.Context, att *discordgo.MessageAttachment) *bridgev2.ConvertedMessagePart { + // TODO(skip): Support direct media. + reupload, err := mc.ReuploadMedia(ctx, att.URL, att.ContentType, att.Filename, att.Size, true) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to copy attachment to Matrix") + return &bridgev2.ConvertedMessagePart{ + Type: event.EventMessage, + Content: mediaFailedMessage(err), + } + } + content := &event.MessageEventContent{ - Body: att.Filename, + Body: reupload.FileName, Info: &event.FileInfo{ Width: att.Width, Height: att.Height, - MimeType: att.ContentType, - - // This gets overwritten later after the file is uploaded to the homeserver - Size: att.Size, + MimeType: reupload.MimeType, + Size: reupload.Size, }, } @@ -607,10 +610,10 @@ func (mc *MessageConverter) renderDiscordAttachment(ctx context.Context, intent if att.Description != "" { content.Body = att.Description - content.FileName = att.Filename + content.FileName = reupload.FileName } - switch strings.ToLower(strings.Split(att.ContentType, "/")[0]) { + switch strings.ToLower(strings.Split(content.Info.MimeType, "/")[0]) { case "audio": content.MsgType = event.MsgAudio if att.Waveform != nil { @@ -630,28 +633,12 @@ func (mc *MessageConverter) renderDiscordAttachment(ctx context.Context, intent content.MsgType = event.MsgFile } - // TODO(skip): Support direct media. - reupload, err := mc.ReuploadMedia(ctx, intent, portal, attachment.AttachmentReupload{ - DownloadingURL: att.URL, - }) - if err != nil { - zerolog.Ctx(ctx).Err(err).Msg("Failed to copy attachment to Matrix") - return &bridgev2.ConvertedMessagePart{ - Type: event.EventMessage, - Content: mediaFailedMessage(err), - } - } - - content.Info.Size = reupload.DownloadedSize + content.URL, content.File = reupload.MXC, reupload.File + content.Info.Size = reupload.Size if content.Info.Width == 0 && content.Info.Height == 0 { content.Info.Width = att.Width content.Info.Height = att.Height } - if reupload.EncryptedFile != nil { - content.File = reupload.EncryptedFile - } else { - content.URL = reupload.MXC - } return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index 317a48b..e8cd391 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -99,6 +99,8 @@ func (mc *MessageConverter) ToDiscord( session *discordgo.Session, msg *bridgev2.MatrixMessage, ) (*discordgo.MessageSend, error) { + ctx = context.WithValue(ctx, contextKeyPortal, msg.Portal) + ctx = context.WithValue(ctx, contextKeyDiscordClient, session) var req discordgo.MessageSend req.Nonce = generateMessageNonce() log := zerolog.Ctx(ctx) diff --git a/pkg/msgconv/msgconv.go b/pkg/msgconv/msgconv.go index 7f40710..9636ac7 100644 --- a/pkg/msgconv/msgconv.go +++ b/pkg/msgconv/msgconv.go @@ -17,35 +17,25 @@ package msgconv import ( - "context" "math/rand" "strconv" "sync/atomic" "maunium.net/go/mautrix/bridgev2" - - "go.mau.fi/mautrix-discord/pkg/attachment" ) -type MediaReuploader func(ctx context.Context, intent bridgev2.MatrixAPI, portal *bridgev2.Portal, reupload attachment.AttachmentReupload) (*attachment.ReuploadedAttachment, error) - type MessageConverter struct { Bridge *bridgev2.Bridge nextDiscordUploadID atomic.Int32 - // ReuploadMedia is called when the message converter wants to upload some - // media it is attempting to bridge. - // - // This can be directly forwarded to the ReuploadMedia method on DiscordConnector. - // The indirection is only necessary to prevent an import cycle. - ReuploadMedia MediaReuploader + MaxFileSize int64 } -func NewMessageConverter(bridge *bridgev2.Bridge, reuploader MediaReuploader) *MessageConverter { +func NewMessageConverter(bridge *bridgev2.Bridge) *MessageConverter { mc := &MessageConverter{ - Bridge: bridge, - ReuploadMedia: reuploader, + Bridge: bridge, + MaxFileSize: 50 * 1024 * 1024, } mc.nextDiscordUploadID.Store(rand.Int31n(100))