handlematrix: bridge outgoing message attachments

This commit is contained in:
Skip R
2026-01-05 22:12:39 -08:00
parent cbfbe65619
commit e71075cd0d
6 changed files with 179 additions and 22 deletions

View File

@@ -17,12 +17,16 @@
package msgconv
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"strconv"
"time"
"github.com/bwmarrin/discordgo"
"github.com/rs/zerolog"
"go.mau.fi/util/variationselector"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/event"
@@ -59,14 +63,43 @@ func parseAllowedLinkPreviews(raw map[string]any) []string {
return allowedLinkPreviews
}
func uploadDiscordAttachment(cli *http.Client, url string, data []byte) error {
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data))
if err != nil {
return err
}
for key, value := range discordgo.DroidBaseHeaders {
req.Header.Set(key, value)
}
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Referer", "https://discord.com/")
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Site", "cross-site")
resp, err := cli.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode > 300 {
respData, _ := io.ReadAll(resp.Body)
return fmt.Errorf("unexpected status %d: %s", resp.StatusCode, respData)
}
return nil
}
// ToDiscord converts a Matrix message into a discordgo.MessageSend that is appropriate
// for bridging the message to Discord.
func (mc *MessageConverter) ToDiscord(
ctx context.Context,
session *discordgo.Session,
msg *bridgev2.MatrixMessage,
) (discordgo.MessageSend, error) {
) (*discordgo.MessageSend, error) {
var req discordgo.MessageSend
req.Nonce = generateMessageNonce()
log := zerolog.Ctx(ctx)
if msg.ReplyTo != nil {
req.Reference = &discordgo.MessageReference{
@@ -75,18 +108,73 @@ func (mc *MessageConverter) ToDiscord(
}
}
switch msg.Content.MsgType {
case event.MsgText, event.MsgEmote, event.MsgNotice:
req.Content, req.AllowedMentions = mc.convertMatrixMessageContent(ctx, msg.Portal, msg.Content, parseAllowedLinkPreviews(msg.Event.Content.Raw))
if msg.Content.MsgType == event.MsgEmote {
portal := msg.Portal
channelID := string(portal.ID)
content := msg.Content
convertMatrix := func() {
req.Content, req.AllowedMentions = mc.convertMatrixMessageContent(ctx, msg.Portal, content, parseAllowedLinkPreviews(msg.Event.Content.Raw))
if content.MsgType == event.MsgEmote {
req.Content = fmt.Sprintf("_%s_", req.Content)
}
// TODO: Handle attachments.
}
switch content.MsgType {
case event.MsgText, event.MsgEmote, event.MsgNotice:
convertMatrix()
case event.MsgAudio, event.MsgFile, event.MsgVideo:
mediaData, err := mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
if err != nil {
log.Err(err).Msg("Failed to download Matrix attachment for bridging")
return nil, bridgev2.ErrMediaDownloadFailed
}
filename := content.Body
if content.FileName != "" && content.FileName != content.Body {
filename = content.FileName
convertMatrix()
}
if msg.Event.Content.Raw["page.codeberg.everypizza.msc4193.spoiler"] == true {
filename = "SPOILER_" + filename
}
// TODO: Support attachments for relay/webhook. (A branch was removed here.)
att := &discordgo.MessageAttachment{
ID: "0",
Filename: filename,
}
upload_id := mc.NextDiscordUploadID()
log.Debug().Str("upload_id", upload_id).Msg("Preparing attachment")
prep, err := session.ChannelAttachmentCreate(channelID, &discordgo.ReqPrepareAttachments{
Files: []*discordgo.FilePrepare{{
Size: len(mediaData),
Name: att.Filename,
ID: mc.NextDiscordUploadID(),
}},
// TODO: Populate with guild ID. Support threads.
}, discordgo.WithChannelReferer("", channelID))
if err != nil {
log.Err(err).Msg("Failed to create attachment in preparation for attachment reupload")
return nil, bridgev2.ErrMediaReuploadFailed
}
prepared := prep.Attachments[0]
att.UploadedFilename = prepared.UploadFilename
err = uploadDiscordAttachment(session.Client, prepared.UploadURL, mediaData)
if err != nil {
log.Err(err).Msg("Failed to reupload Discord attachment after preparing")
return nil, bridgev2.ErrMediaReuploadFailed
}
req.Attachments = append(req.Attachments, att)
}
// TODO: Handle (silent) replies and allowed mentions.
return req, nil
return &req, nil
}
func (mc *MessageConverter) convertMatrixMessageContent(ctx context.Context, portal *bridgev2.Portal, content *event.MessageEventContent, allowedLinkPreviews []string) (string, *discordgo.MessageAllowedMentions) {

View File

@@ -18,19 +18,41 @@ 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 func(ctx context.Context, intent bridgev2.MatrixAPI, portal *bridgev2.Portal, reupload attachment.AttachmentReupload) (*attachment.ReuploadedAttachment, error)
ReuploadMedia MediaReuploader
}
func NewMessageConverter(bridge *bridgev2.Bridge, reuploader MediaReuploader) *MessageConverter {
mc := &MessageConverter{
Bridge: bridge,
ReuploadMedia: reuploader,
}
mc.nextDiscordUploadID.Store(rand.Int31n(100))
return mc
}
func (mc *MessageConverter) NextDiscordUploadID() string {
val := mc.nextDiscordUploadID.Add(2)
return strconv.Itoa(int(val))
}