msgconv: clean up reuploading attachments to Matrix
This commit is contained in:
@@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -100,7 +100,7 @@ func (dc *DiscordClient) FetchMessages(ctx context.Context, fetchParams bridgev2
|
|||||||
|
|
||||||
converted = append(converted, &bridgev2.BackfillMessage{
|
converted = append(converted, &bridgev2.BackfillMessage{
|
||||||
ID: networkid.MessageID(msg.ID),
|
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,
|
Sender: sender,
|
||||||
Timestamp: ts,
|
Timestamp: ts,
|
||||||
StreamOrder: streamOrder,
|
StreamOrder: streamOrder,
|
||||||
|
|||||||
@@ -30,14 +30,21 @@ type DiscordConnector struct {
|
|||||||
MsgConv *msgconv.MessageConverter
|
MsgConv *msgconv.MessageConverter
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ bridgev2.NetworkConnector = (*DiscordConnector)(nil)
|
var (
|
||||||
|
_ bridgev2.NetworkConnector = (*DiscordConnector)(nil)
|
||||||
|
_ bridgev2.MaxFileSizeingNetwork = (*DiscordConnector)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
func (d *DiscordConnector) Init(bridge *bridgev2.Bridge) {
|
func (d *DiscordConnector) Init(bridge *bridgev2.Bridge) {
|
||||||
d.Bridge = bridge
|
d.Bridge = bridge
|
||||||
d.MsgConv = msgconv.NewMessageConverter(bridge, d.ReuploadMedia)
|
d.MsgConv = msgconv.NewMessageConverter(bridge)
|
||||||
d.setUpProvisioningAPIs()
|
d.setUpProvisioningAPIs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DiscordConnector) SetMaxFileSize(maxSize int64) {
|
||||||
|
d.MsgConv.MaxFileSize = maxSize
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DiscordConnector) Start(ctx context.Context) error {
|
func (d *DiscordConnector) Start(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (m *DiscordMessage) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) {
|
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 {
|
func (m *DiscordMessage) GetID() networkid.MessageID {
|
||||||
|
|||||||
147
pkg/msgconv/attachments.go
Normal file
147
pkg/msgconv/attachments.go
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -26,22 +26,36 @@ import (
|
|||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"go.mau.fi/util/exmaps"
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/format"
|
"maunium.net/go/mautrix/format"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-discord/pkg/attachment"
|
|
||||||
"go.mau.fi/mautrix-discord/pkg/discordid"
|
"go.mau.fi/mautrix-discord/pkg/discordid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type contextKey int
|
||||||
|
|
||||||
|
const (
|
||||||
|
contextKeyPortal contextKey = iota
|
||||||
|
contextKeyIntent
|
||||||
|
contextKeyUserLogin
|
||||||
|
contextKeyDiscordClient
|
||||||
|
)
|
||||||
|
|
||||||
func (mc *MessageConverter) ToMatrix(
|
func (mc *MessageConverter) ToMatrix(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
portal *bridgev2.Portal,
|
portal *bridgev2.Portal,
|
||||||
intent bridgev2.MatrixAPI,
|
intent bridgev2.MatrixAPI,
|
||||||
source *bridgev2.UserLogin,
|
source *bridgev2.UserLogin,
|
||||||
|
session *discordgo.Session,
|
||||||
msg *discordgo.Message,
|
msg *discordgo.Message,
|
||||||
) *bridgev2.ConvertedMessage {
|
) *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)
|
predictedLength := len(msg.Attachments) + len(msg.StickerItems)
|
||||||
if msg.Content != "" {
|
if msg.Content != "" {
|
||||||
predictedLength++
|
predictedLength++
|
||||||
@@ -56,28 +70,26 @@ func (mc *MessageConverter) ToMatrix(
|
|||||||
Str("message_id", msg.ID).
|
Str("message_id", msg.ID).
|
||||||
Logger().WithContext(ctx)
|
Logger().WithContext(ctx)
|
||||||
log := zerolog.Ctx(ctx)
|
log := zerolog.Ctx(ctx)
|
||||||
handledIDs := make(map[string]struct{})
|
handledIDs := make(exmaps.Set[string])
|
||||||
|
|
||||||
for _, att := range msg.Attachments {
|
for _, att := range msg.Attachments {
|
||||||
if _, handled := handledIDs[att.ID]; handled {
|
if !handledIDs.Add(att.ID) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
handledIDs[att.ID] = struct{}{}
|
|
||||||
|
|
||||||
log := log.With().Str("attachment_id", att.ID).Logger()
|
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)
|
parts = append(parts, part)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sticker := range msg.StickerItems {
|
for _, sticker := range msg.StickerItems {
|
||||||
if _, handled := handledIDs[sticker.ID]; handled {
|
if !handledIDs.Add(sticker.ID) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
handledIDs[sticker.ID] = struct{}{}
|
|
||||||
|
|
||||||
log := log.With().Str("sticker_id", sticker.ID).Logger()
|
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)
|
parts = append(parts, part)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,17 +100,16 @@ func (mc *MessageConverter) ToMatrix(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Discord deduplicates embeds by URL. It makes things easier for us too.
|
// Discord deduplicates embeds by URL. It makes things easier for us too.
|
||||||
if _, handled := handledIDs[embed.URL]; handled {
|
if !handledIDs.Add(embed.URL) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
handledIDs[embed.URL] = struct{}{}
|
|
||||||
|
|
||||||
log := log.With().
|
log := log.With().
|
||||||
Str("computed_embed_type", "video").
|
Str("computed_embed_type", "video").
|
||||||
Str("embed_type", string(embed.Type)).
|
Str("embed_type", string(embed.Type)).
|
||||||
Int("embed_index", i).
|
Int("embed_index", i).
|
||||||
Logger()
|
Logger()
|
||||||
part := mc.renderDiscordVideoEmbed(log.WithContext(ctx), intent, portal, embed)
|
part := mc.renderDiscordVideoEmbed(log.WithContext(ctx), embed)
|
||||||
if part != nil {
|
if part != nil {
|
||||||
parts = append(parts, part)
|
parts = append(parts, part)
|
||||||
}
|
}
|
||||||
@@ -235,7 +246,7 @@ func (mc *MessageConverter) renderDiscordTextMessage(ctx context.Context, intent
|
|||||||
len(msg.MessageSnapshots) > 0 &&
|
len(msg.MessageSnapshots) > 0 &&
|
||||||
msg.MessageSnapshots[0].Message != nil {
|
msg.MessageSnapshots[0].Message != nil {
|
||||||
// Bridge forwarded messages.
|
// 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)
|
previews := make([]*event.BeeperLinkPreview, 0)
|
||||||
@@ -251,10 +262,10 @@ func (mc *MessageConverter) renderDiscordTextMessage(ctx context.Context, intent
|
|||||||
switch getEmbedType(msg, embed) {
|
switch getEmbedType(msg, embed) {
|
||||||
case EmbedRich:
|
case EmbedRich:
|
||||||
log := with.Str("computed_embed_type", "rich").Logger()
|
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:
|
case EmbedLinkPreview:
|
||||||
log := with.Str("computed_embed_type", "link preview").Logger()
|
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:
|
case EmbedVideo:
|
||||||
// Video embeds are handled as separate messages via renderDiscordVideoEmbed.
|
// Video embeds are handled as separate messages via renderDiscordVideoEmbed.
|
||||||
default:
|
default:
|
||||||
@@ -284,7 +295,7 @@ func (mc *MessageConverter) renderDiscordTextMessage(ctx context.Context, intent
|
|||||||
return &bridgev2.ConvertedMessagePart{Type: event.EventMessage, Content: &content, Extra: extraContent}
|
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)
|
log := zerolog.Ctx(ctx)
|
||||||
|
|
||||||
forwardedHTML := mc.renderDiscordMarkdownOnlyHTMLNoUnwrap(portal, msg.MessageSnapshots[0].Message.Content, true)
|
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
|
var proxyURL string
|
||||||
if embed.Video != nil {
|
if embed.Video != nil {
|
||||||
proxyURL = embed.Video.ProxyURL
|
proxyURL = embed.Video.ProxyURL
|
||||||
@@ -344,10 +355,7 @@ func (mc *MessageConverter) renderDiscordVideoEmbed(ctx context.Context, intent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
upload := attachment.AttachmentReupload{
|
reupload, err := mc.ReuploadUnknownMedia(ctx, proxyURL, true)
|
||||||
DownloadingURL: proxyURL,
|
|
||||||
}
|
|
||||||
reupload, err := mc.ReuploadMedia(ctx, intent, portal, upload)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to copy video embed to Matrix")
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to copy video embed to Matrix")
|
||||||
return &bridgev2.ConvertedMessagePart{
|
return &bridgev2.ConvertedMessagePart{
|
||||||
@@ -358,16 +366,13 @@ func (mc *MessageConverter) renderDiscordVideoEmbed(ctx context.Context, intent
|
|||||||
|
|
||||||
content := &event.MessageEventContent{
|
content := &event.MessageEventContent{
|
||||||
Body: embed.URL,
|
Body: embed.URL,
|
||||||
|
URL: reupload.MXC,
|
||||||
|
File: reupload.File,
|
||||||
Info: &event.FileInfo{
|
Info: &event.FileInfo{
|
||||||
MimeType: reupload.MimeType,
|
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 {
|
if embed.Video != nil {
|
||||||
content.MsgType = event.MsgVideo
|
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")
|
panic("unimplemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,7 +428,7 @@ const (
|
|||||||
embedFooterDateSeparator = ` • `
|
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)
|
log := zerolog.Ctx(ctx)
|
||||||
var htmlParts []string
|
var htmlParts []string
|
||||||
if embed.Author != nil {
|
if embed.Author != nil {
|
||||||
@@ -434,9 +439,7 @@ func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent b
|
|||||||
}
|
}
|
||||||
authorHTML = fmt.Sprintf(embedHTMLAuthorPlain, authorNameHTML)
|
authorHTML = fmt.Sprintf(embedHTMLAuthorPlain, authorNameHTML)
|
||||||
if embed.Author.ProxyIconURL != "" {
|
if embed.Author.ProxyIconURL != "" {
|
||||||
reupload, err := mc.ReuploadMedia(ctx, intent, portal, attachment.AttachmentReupload{
|
reupload, err := mc.ReuploadUnknownMedia(ctx, embed.Author.ProxyIconURL, false)
|
||||||
DownloadingURL: embed.Author.ProxyIconURL,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("Failed to reupload author icon in embed")
|
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)
|
htmlParts = append(htmlParts, authorHTML)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
portal := ctx.Value(contextKeyPortal).(*bridgev2.Portal)
|
||||||
if embed.Title != "" {
|
if embed.Title != "" {
|
||||||
var titleHTML string
|
var titleHTML string
|
||||||
baseTitleHTML := mc.renderDiscordMarkdownOnlyHTML(portal, embed.Title, false)
|
baseTitleHTML := mc.renderDiscordMarkdownOnlyHTML(portal, embed.Title, false)
|
||||||
@@ -492,9 +496,7 @@ func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent b
|
|||||||
}
|
}
|
||||||
|
|
||||||
if embed.Image != nil {
|
if embed.Image != nil {
|
||||||
reupload, err := mc.ReuploadMedia(ctx, intent, portal, attachment.AttachmentReupload{
|
reupload, err := mc.ReuploadUnknownMedia(ctx, embed.Image.ProxyURL, false)
|
||||||
DownloadingURL: embed.Image.ProxyURL,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("Failed to reupload image in embed")
|
log.Warn().Err(err).Msg("Failed to reupload image in embed")
|
||||||
} else {
|
} else {
|
||||||
@@ -522,9 +524,7 @@ func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent b
|
|||||||
}
|
}
|
||||||
footerHTML = fmt.Sprintf(embedHTMLFooterPlain, html.EscapeString(embed.Footer.Text), datePart)
|
footerHTML = fmt.Sprintf(embedHTMLFooterPlain, html.EscapeString(embed.Footer.Text), datePart)
|
||||||
if embed.Footer.ProxyIconURL != "" {
|
if embed.Footer.ProxyIconURL != "" {
|
||||||
reupload, err := mc.ReuploadMedia(ctx, intent, portal, attachment.AttachmentReupload{
|
reupload, err := mc.ReuploadUnknownMedia(ctx, embed.Footer.ProxyIconURL, false)
|
||||||
DownloadingURL: embed.Footer.ProxyIconURL,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("Failed to reupload footer icon in embed")
|
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
|
return compiledHTML
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mc *MessageConverter) renderDiscordLinkEmbedImage(ctx context.Context, intent bridgev2.MatrixAPI, portal *bridgev2.Portal, url string, width, height int, preview *event.BeeperLinkPreview) {
|
func (mc *MessageConverter) renderDiscordLinkEmbedImage(
|
||||||
reupload, err := mc.ReuploadMedia(ctx, intent, portal, attachment.AttachmentReupload{
|
ctx context.Context, url string, width, height int, preview *event.BeeperLinkPreview,
|
||||||
DownloadingURL: url,
|
) {
|
||||||
})
|
reupload, err := mc.ReuploadUnknownMedia(ctx, url, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to reupload image in URL preview, ignoring")
|
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to reupload image in URL preview, ignoring")
|
||||||
return
|
return
|
||||||
@@ -563,39 +563,42 @@ func (mc *MessageConverter) renderDiscordLinkEmbedImage(ctx context.Context, int
|
|||||||
preview.ImageWidth = event.IntOrString(width)
|
preview.ImageWidth = event.IntOrString(width)
|
||||||
preview.ImageHeight = event.IntOrString(height)
|
preview.ImageHeight = event.IntOrString(height)
|
||||||
}
|
}
|
||||||
preview.ImageSize = event.IntOrString(reupload.DownloadedSize)
|
preview.ImageSize = event.IntOrString(reupload.Size)
|
||||||
preview.ImageType = reupload.MimeType
|
preview.ImageType = reupload.MimeType
|
||||||
if reupload.EncryptedFile != nil {
|
preview.ImageURL, preview.ImageEncryption = reupload.MXC, reupload.File
|
||||||
preview.ImageEncryption = &event.EncryptedFileInfo{
|
|
||||||
EncryptedFile: reupload.EncryptedFile.EncryptedFile,
|
|
||||||
URL: reupload.MXC,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
var preview event.BeeperLinkPreview
|
||||||
preview.MatchedURL = embed.URL
|
preview.MatchedURL = embed.URL
|
||||||
preview.Title = embed.Title
|
preview.Title = embed.Title
|
||||||
preview.Description = embed.Description
|
preview.Description = embed.Description
|
||||||
if embed.Image != nil {
|
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 {
|
} 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
|
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{
|
content := &event.MessageEventContent{
|
||||||
Body: att.Filename,
|
Body: reupload.FileName,
|
||||||
Info: &event.FileInfo{
|
Info: &event.FileInfo{
|
||||||
Width: att.Width,
|
Width: att.Width,
|
||||||
Height: att.Height,
|
Height: att.Height,
|
||||||
MimeType: att.ContentType,
|
MimeType: reupload.MimeType,
|
||||||
|
Size: reupload.Size,
|
||||||
// This gets overwritten later after the file is uploaded to the homeserver
|
|
||||||
Size: att.Size,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,10 +610,10 @@ func (mc *MessageConverter) renderDiscordAttachment(ctx context.Context, intent
|
|||||||
|
|
||||||
if att.Description != "" {
|
if att.Description != "" {
|
||||||
content.Body = 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":
|
case "audio":
|
||||||
content.MsgType = event.MsgAudio
|
content.MsgType = event.MsgAudio
|
||||||
if att.Waveform != nil {
|
if att.Waveform != nil {
|
||||||
@@ -630,28 +633,12 @@ func (mc *MessageConverter) renderDiscordAttachment(ctx context.Context, intent
|
|||||||
content.MsgType = event.MsgFile
|
content.MsgType = event.MsgFile
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(skip): Support direct media.
|
content.URL, content.File = reupload.MXC, reupload.File
|
||||||
reupload, err := mc.ReuploadMedia(ctx, intent, portal, attachment.AttachmentReupload{
|
content.Info.Size = reupload.Size
|
||||||
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
|
|
||||||
if content.Info.Width == 0 && content.Info.Height == 0 {
|
if content.Info.Width == 0 && content.Info.Height == 0 {
|
||||||
content.Info.Width = att.Width
|
content.Info.Width = att.Width
|
||||||
content.Info.Height = att.Height
|
content.Info.Height = att.Height
|
||||||
}
|
}
|
||||||
if reupload.EncryptedFile != nil {
|
|
||||||
content.File = reupload.EncryptedFile
|
|
||||||
} else {
|
|
||||||
content.URL = reupload.MXC
|
|
||||||
}
|
|
||||||
|
|
||||||
return &bridgev2.ConvertedMessagePart{
|
return &bridgev2.ConvertedMessagePart{
|
||||||
Type: event.EventMessage,
|
Type: event.EventMessage,
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ func (mc *MessageConverter) ToDiscord(
|
|||||||
session *discordgo.Session,
|
session *discordgo.Session,
|
||||||
msg *bridgev2.MatrixMessage,
|
msg *bridgev2.MatrixMessage,
|
||||||
) (*discordgo.MessageSend, error) {
|
) (*discordgo.MessageSend, error) {
|
||||||
|
ctx = context.WithValue(ctx, contextKeyPortal, msg.Portal)
|
||||||
|
ctx = context.WithValue(ctx, contextKeyDiscordClient, session)
|
||||||
var req discordgo.MessageSend
|
var req discordgo.MessageSend
|
||||||
req.Nonce = generateMessageNonce()
|
req.Nonce = generateMessageNonce()
|
||||||
log := zerolog.Ctx(ctx)
|
log := zerolog.Ctx(ctx)
|
||||||
|
|||||||
@@ -17,35 +17,25 @@
|
|||||||
package msgconv
|
package msgconv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
"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 {
|
type MessageConverter struct {
|
||||||
Bridge *bridgev2.Bridge
|
Bridge *bridgev2.Bridge
|
||||||
|
|
||||||
nextDiscordUploadID atomic.Int32
|
nextDiscordUploadID atomic.Int32
|
||||||
|
|
||||||
// ReuploadMedia is called when the message converter wants to upload some
|
MaxFileSize int64
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMessageConverter(bridge *bridgev2.Bridge, reuploader MediaReuploader) *MessageConverter {
|
func NewMessageConverter(bridge *bridgev2.Bridge) *MessageConverter {
|
||||||
mc := &MessageConverter{
|
mc := &MessageConverter{
|
||||||
Bridge: bridge,
|
Bridge: bridge,
|
||||||
ReuploadMedia: reuploader,
|
MaxFileSize: 50 * 1024 * 1024,
|
||||||
}
|
}
|
||||||
|
|
||||||
mc.nextDiscordUploadID.Store(rand.Int31n(100))
|
mc.nextDiscordUploadID.Store(rand.Int31n(100))
|
||||||
|
|||||||
Reference in New Issue
Block a user