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

@@ -26,7 +26,8 @@ import (
"github.com/bwmarrin/discordgo"
"github.com/rs/zerolog"
"go.mau.fi/mautrix-discord/pkg/connector"
"go.mau.fi/mautrix-discord/pkg/attachment"
"go.mau.fi/mautrix-discord/pkg/discordid"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
@@ -111,6 +112,11 @@ func (mc *MessageConverter) ToMatrix(
// puppet.addMemberMeta(part, msg)
// }
// Assign incrementing part IDs.
for i, part := range parts {
part.ID = networkid.PartID(strconv.Itoa(i))
}
return &bridgev2.ConvertedMessage{Parts: parts}
}
@@ -143,7 +149,7 @@ func (mc *MessageConverter) renderDiscordTextMessage(ctx context.Context, intent
var htmlParts []string
if msg.Interaction != nil {
ghost, err := mc.connector.Bridge.GetGhostByID(ctx, networkid.UserID(msg.Interaction.User.ID))
ghost, err := mc.Bridge.GetGhostByID(ctx, networkid.UserID(msg.Interaction.User.ID))
// TODO(skip): Try doing ghost.UpdateInfoIfNecessary.
if err == nil {
htmlParts = append(htmlParts, fmt.Sprintf(msgInteractionTemplateHTML, ghost.Intent.GetMXID(), ghost.Name, msg.Interaction.Name))
@@ -154,22 +160,22 @@ func (mc *MessageConverter) renderDiscordTextMessage(ctx context.Context, intent
if msg.Content != "" && !isPlainGifMessage(msg) {
// Bridge basic text messages.
htmlParts = append(htmlParts, mc.renderDiscordMarkdownOnlyHTML(msg.Content, true))
htmlParts = append(htmlParts, mc.renderDiscordMarkdownOnlyHTML(portal, msg.Content, true))
} else if msg.MessageReference != nil &&
msg.MessageReference.Type == discordgo.MessageReferenceTypeForward &&
len(msg.MessageSnapshots) > 0 &&
msg.MessageSnapshots[0].Message != nil {
// Bridge forwarded messages.
forwardedHTML := mc.renderDiscordMarkdownOnlyHTMLNoUnwrap(msg.MessageSnapshots[0].Message.Content, true)
forwardedHTML := mc.renderDiscordMarkdownOnlyHTMLNoUnwrap(portal, msg.MessageSnapshots[0].Message.Content, true)
msgTSText := msg.MessageSnapshots[0].Message.Timestamp.Format("2006-01-02 15:04 MST")
origLink := fmt.Sprintf("unknown channel • %s", msgTSText)
if forwardedFromPortal, err := mc.connector.Bridge.DB.Portal.GetByKey(ctx, connector.MakePortalKeyWithID(msg.MessageReference.ChannelID)); err == nil && forwardedFromPortal != nil {
if origMessage, err := mc.connector.Bridge.DB.Message.GetFirstPartByID(ctx, source.ID, networkid.MessageID(msg.MessageReference.MessageID)); err == nil && origMessage != nil {
if forwardedFromPortal, err := mc.Bridge.DB.Portal.GetByKey(ctx, discordid.MakePortalKeyWithID(msg.MessageReference.ChannelID)); err == nil && forwardedFromPortal != nil {
if origMessage, err := mc.Bridge.DB.Message.GetFirstPartByID(ctx, source.ID, networkid.MessageID(msg.MessageReference.MessageID)); err == nil && origMessage != nil {
// We've bridged the message that was forwarded, so we can link to it directly.
origLink = fmt.Sprintf(
`<a href="%s">#%s • %s</a>`,
forwardedFromPortal.MXID.EventURI(origMessage.MXID, mc.connector.Bridge.Matrix.ServerName()),
forwardedFromPortal.MXID.EventURI(origMessage.MXID, mc.Bridge.Matrix.ServerName()),
forwardedFromPortal.Name,
msgTSText,
)
@@ -179,7 +185,7 @@ func (mc *MessageConverter) renderDiscordTextMessage(ctx context.Context, intent
// We don't have the message but we have the portal, so link to that.
origLink = fmt.Sprintf(
`<a href="%s">#%s</a> • %s`,
forwardedFromPortal.MXID.URI(mc.connector.Bridge.Matrix.ServerName()),
forwardedFromPortal.MXID.URI(mc.Bridge.Matrix.ServerName()),
forwardedFromPortal.Name,
msgTSText,
)
@@ -264,10 +270,10 @@ func (mc *MessageConverter) renderDiscordVideoEmbed(ctx context.Context, intent
}
}
upload := connector.AttachmentReupload{
upload := attachment.AttachmentReupload{
DownloadingURL: proxyURL,
}
reupload, err := mc.connector.ReuploadMedia(ctx, intent, portal, upload)
reupload, err := mc.ReuploadMedia(ctx, intent, portal, upload)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to copy video embed to Matrix")
return &bridgev2.ConvertedMessagePart{
@@ -282,10 +288,7 @@ func (mc *MessageConverter) renderDiscordVideoEmbed(ctx context.Context, intent
MimeType: reupload.MimeType,
Size: reupload.DownloadedSize,
},
File: &event.EncryptedFileInfo{
EncryptedFile: reupload.EncryptedFile.EncryptedFile,
URL: reupload.MXC,
},
File: reupload.EncryptedFile,
}
if embed.Video != nil {
@@ -321,14 +324,6 @@ func (mc *MessageConverter) renderDiscordSticker(context context.Context, intent
panic("unimplemented")
}
func (mc *MessageConverter) renderDiscordMarkdownOnlyHTML(text string, allowInlineLinks bool) string {
panic("unimplemented")
}
func (mc *MessageConverter) renderDiscordMarkdownOnlyHTMLNoUnwrap(text string, allowInlineLinks bool) string {
panic("unimplemented")
}
const (
embedHTMLWrapper = `<blockquote class="discord-embed">%s</blockquote>`
embedHTMLWrapperColor = `<blockquote class="discord-embed" background-color="#%06X">%s</blockquote>`
@@ -361,7 +356,7 @@ func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent b
}
authorHTML = fmt.Sprintf(embedHTMLAuthorPlain, authorNameHTML)
if embed.Author.ProxyIconURL != "" {
reupload, err := mc.connector.ReuploadMedia(ctx, intent, portal, connector.AttachmentReupload{
reupload, err := mc.ReuploadMedia(ctx, intent, portal, attachment.AttachmentReupload{
DownloadingURL: embed.Author.ProxyIconURL,
})
@@ -376,7 +371,7 @@ func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent b
if embed.Title != "" {
var titleHTML string
baseTitleHTML := mc.renderDiscordMarkdownOnlyHTML(embed.Title, false)
baseTitleHTML := mc.renderDiscordMarkdownOnlyHTML(portal, embed.Title, false)
if embed.URL != "" {
titleHTML = fmt.Sprintf(embedHTMLTitleWithLink, html.EscapeString(embed.URL), baseTitleHTML)
} else {
@@ -386,7 +381,7 @@ func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent b
}
if embed.Description != "" {
htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLDescription, mc.renderDiscordMarkdownOnlyHTML(embed.Description, true)))
htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLDescription, mc.renderDiscordMarkdownOnlyHTML(portal, embed.Description, true)))
}
for i := 0; i < len(embed.Fields); i++ {
@@ -405,21 +400,21 @@ func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent b
headerParts := make([]string, len(splitItems))
contentParts := make([]string, len(splitItems))
for j, splitItem := range splitItems {
headerParts[j] = fmt.Sprintf(embedHTMLFieldName, mc.renderDiscordMarkdownOnlyHTML(splitItem.Name, false))
contentParts[j] = fmt.Sprintf(embedHTMLFieldValue, mc.renderDiscordMarkdownOnlyHTML(splitItem.Value, true))
headerParts[j] = fmt.Sprintf(embedHTMLFieldName, mc.renderDiscordMarkdownOnlyHTML(portal, splitItem.Name, false))
contentParts[j] = fmt.Sprintf(embedHTMLFieldValue, mc.renderDiscordMarkdownOnlyHTML(portal, splitItem.Value, true))
}
htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLFields, strings.Join(headerParts, ""), strings.Join(contentParts, "")))
} else {
htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLLinearField,
strconv.FormatBool(item.Inline),
mc.renderDiscordMarkdownOnlyHTML(item.Name, false),
mc.renderDiscordMarkdownOnlyHTML(item.Value, true),
mc.renderDiscordMarkdownOnlyHTML(portal, item.Name, false),
mc.renderDiscordMarkdownOnlyHTML(portal, item.Value, true),
))
}
}
if embed.Image != nil {
reupload, err := mc.connector.ReuploadMedia(ctx, intent, portal, connector.AttachmentReupload{
reupload, err := mc.ReuploadMedia(ctx, intent, portal, attachment.AttachmentReupload{
DownloadingURL: embed.Image.ProxyURL,
})
if err != nil {
@@ -449,7 +444,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.connector.ReuploadMedia(ctx, intent, portal, connector.AttachmentReupload{
reupload, err := mc.ReuploadMedia(ctx, intent, portal, attachment.AttachmentReupload{
DownloadingURL: embed.Footer.ProxyIconURL,
})
@@ -478,7 +473,7 @@ func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent b
}
func (mc *MessageConverter) renderDiscordLinkEmbedImage(ctx context.Context, intent bridgev2.MatrixAPI, portal *bridgev2.Portal, url string, width, height int, preview *event.BeeperLinkPreview) {
reupload, err := mc.connector.ReuploadMedia(ctx, intent, portal, connector.AttachmentReupload{
reupload, err := mc.ReuploadMedia(ctx, intent, portal, attachment.AttachmentReupload{
DownloadingURL: url,
})
if err != nil {
@@ -556,7 +551,7 @@ func (mc *MessageConverter) renderDiscordAttachment(ctx context.Context, intent
}
// TODO(skip): Support direct media.
reupload, err := mc.connector.ReuploadMedia(ctx, intent, portal, connector.AttachmentReupload{
reupload, err := mc.ReuploadMedia(ctx, intent, portal, attachment.AttachmentReupload{
DownloadingURL: att.URL,
})
if err != nil {
@@ -572,10 +567,8 @@ func (mc *MessageConverter) renderDiscordAttachment(ctx context.Context, intent
content.Info.Width = att.Width
content.Info.Height = att.Height
}
content.File = &event.EncryptedFileInfo{
EncryptedFile: reupload.EncryptedFile.EncryptedFile,
URL: reupload.MXC,
}
content.URL = reupload.MXC
content.File = reupload.EncryptedFile
return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,