msgconv: port most of attachment and text message bridging

* Created a separate discordid package to avoid import cycles.
* Implemented attachment bridging. We still need to implement direct
  media, but this will do for now.
* Corrected how encrypted files (e.g. embed images and attachments) were
  bridged. Previously, the URL field would be empty.

Still a lot of missing pieces. Thoughts:

* Mentions to roles and custom emoji are not rendered properly. We need
  to maintain our own DB.
* We might not need the "attachments" leaf package anymore? It's just
  there to avoid an import cycle.

Bridging actual events (i.e. wiring up discordgo's event handlers) is
probably next.
This commit is contained in:
Skip R
2025-11-26 18:09:00 -08:00
parent 86e18c1f7d
commit b5e6db06f8
10 changed files with 629 additions and 111 deletions

View File

@@ -28,9 +28,8 @@ import (
"github.com/bwmarrin/discordgo"
"github.com/rs/zerolog"
"go.mau.fi/mautrix-discord/pkg/attachment"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
func downloadDiscordAttachment(cli *http.Client, url string, maxSize int64) ([]byte, error) {
@@ -69,20 +68,7 @@ func downloadDiscordAttachment(cli *http.Client, url string, maxSize int64) ([]b
}
}
type AttachmentReupload struct {
DownloadingURL string
FileName string
MimeType string
}
type ReuploadedAttachment struct {
AttachmentReupload
DownloadedSize int
MXC id.ContentURIString
EncryptedFile *event.EncryptedFileInfo
}
func (d *DiscordConnector) ReuploadMedia(ctx context.Context, intent bridgev2.MatrixAPI, portal *bridgev2.Portal, upload AttachmentReupload) (*ReuploadedAttachment, error) {
func (d *DiscordConnector) ReuploadMedia(ctx context.Context, intent bridgev2.MatrixAPI, portal *bridgev2.Portal, upload attachment.AttachmentReupload) (*attachment.ReuploadedAttachment, error) {
log := zerolog.Ctx(ctx)
// TODO(skip): Do we need to check if we've already downloaded this media before?
// TODO(skip): Read a maximum size from the config.
@@ -117,7 +103,7 @@ func (d *DiscordConnector) ReuploadMedia(ctx context.Context, intent bridgev2.Ma
return nil, err
}
return &ReuploadedAttachment{
return &attachment.ReuploadedAttachment{
AttachmentReupload: upload,
DownloadedSize: len(data),
MXC: mxc,

View File

@@ -23,9 +23,9 @@ import (
"github.com/bwmarrin/discordgo"
"github.com/rs/zerolog"
"go.mau.fi/mautrix-discord/pkg/msgconv"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
)
var (
@@ -65,15 +65,33 @@ func (dc *DiscordClient) FetchMessages(ctx context.Context, fetchParams bridgev2
}
converted := make([]*bridgev2.BackfillMessage, 0, len(msgs))
mc := msgconv.MessageConverter{
Bridge: dc.connector.Bridge,
ReuploadMedia: dc.connector.ReuploadMedia,
}
for _, msg := range msgs {
streamOrder, _ := strconv.ParseInt(msg.ID, 10, 64)
ts, _ := discordgo.SnowflakeTimestamp(msg.ID)
// FIXME(skip): Backfill reactions.
sender := dc.makeEventSender(msg.Author)
// Use the ghost's intent, falling back to the bridge's.
ghost, err := dc.connector.Bridge.GetGhostByID(ctx, sender.Sender)
if err != nil {
log.Err(err).Msg("Failed to look up ghost while converting backfilled message")
}
var intent bridgev2.MatrixAPI
if ghost == nil {
intent = fetchParams.Portal.Bridge.Bot
} else {
intent = ghost.Intent
}
converted = append(converted, &bridgev2.BackfillMessage{
ConvertedMessage: dc.convertMessage(msg),
ID: networkid.MessageID(msg.ID),
Sender: dc.makeEventSender(msg.Author),
ConvertedMessage: mc.ToMatrix(ctx, fetchParams.Portal, intent, dc.UserLogin, msg),
Sender: sender,
Timestamp: ts,
StreamOrder: streamOrder,
})
@@ -91,43 +109,3 @@ func (dc *DiscordClient) FetchMessages(ctx context.Context, fetchParams bridgev2
HasMore: len(msgs) == count,
}, nil
}
func (dc *DiscordClient) convertMessage(msg *discordgo.Message) *bridgev2.ConvertedMessage {
// FIXME(skip): This isn't bridging a lot of things (replies, forwards, voice messages, attachments, webhooks, embeds, etc.). Copy from main branch.
var parts []*bridgev2.ConvertedMessagePart
switch msg.Type {
case discordgo.MessageTypeCall:
parts = append(parts, &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgEmote,
Body: "started a call",
},
})
case discordgo.MessageTypeGuildMemberJoin:
parts = append(parts, &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgEmote,
Body: "joined the server",
},
})
}
if msg.Content != "" {
// FIXME(skip): This needs to render into HTML.
parts = append(parts, &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgText,
Body: msg.Content,
},
})
}
return &bridgev2.ConvertedMessage{
// TODO(skip): Replies.
Parts: parts,
}
}

View File

@@ -27,6 +27,7 @@ import (
"github.com/bwmarrin/discordgo"
"github.com/rs/zerolog"
"go.mau.fi/mautrix-discord/pkg/discordid"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
@@ -221,6 +222,14 @@ func makeChannelAvatar(ch *discordgo.Channel) *bridgev2.Avatar {
}
}
func (d *DiscordClient) makeEventSender(user *discordgo.User) bridgev2.EventSender {
return bridgev2.EventSender{
IsFromMe: user.ID == d.Session.State.User.ID,
SenderLogin: networkid.UserLoginID(user.ID),
Sender: networkid.UserID(user.ID),
}
}
func (d *DiscordClient) syncChannel(_ context.Context, ch *discordgo.Channel, selfIsInChannel bool) {
isGroup := len(ch.RecipientIDs) > 1
@@ -253,7 +262,7 @@ func (d *DiscordClient) syncChannel(_ context.Context, ch *discordgo.Channel, se
d.connector.Bridge.QueueRemoteEvent(d.UserLogin, &DiscordChatResync{
channel: ch,
portalKey: MakePortalKey(ch, d.UserLogin.ID, true),
portalKey: discordid.MakePortalKey(ch, d.UserLogin.ID, true),
info: &bridgev2.ChatInfo{
Name: &ch.Name,
Members: &members,

View File

@@ -1,44 +0,0 @@
// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2024 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 (
"github.com/bwmarrin/discordgo"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
)
func MakePortalKey(ch *discordgo.Channel, userLoginID networkid.UserLoginID, wantReceiver bool) (key networkid.PortalKey) {
key.ID = networkid.PortalID(ch.ID)
if wantReceiver {
key.Receiver = userLoginID
}
return
}
func MakePortalKeyWithID(channelID string) (key networkid.PortalKey) {
key.ID = networkid.PortalID(channelID)
return
}
func (d *DiscordClient) makeEventSender(user *discordgo.User) bridgev2.EventSender {
return bridgev2.EventSender{
IsFromMe: user.ID == d.Session.State.User.ID,
SenderLogin: networkid.UserLoginID(user.ID),
Sender: networkid.UserID(user.ID),
}
}