diff --git a/pkg/connector/client.go b/pkg/connector/client.go
index 490ab08..4eef523 100644
--- a/pkg/connector/client.go
+++ b/pkg/connector/client.go
@@ -253,7 +253,7 @@ func (d *DiscordClient) syncChannel(_ context.Context, ch *discordgo.Channel, se
d.connector.bridge.QueueRemoteEvent(d.UserLogin, &DiscordChatResync{
channel: ch,
- portalKey: d.makePortalKey(ch, d.UserLogin.ID, true),
+ portalKey: MakePortalKey(ch, d.UserLogin.ID, true),
info: &bridgev2.ChatInfo{
Name: &ch.Name,
Members: &members,
diff --git a/pkg/connector/id.go b/pkg/connector/id.go
index 984f07d..5519ddd 100644
--- a/pkg/connector/id.go
+++ b/pkg/connector/id.go
@@ -22,7 +22,7 @@ import (
"maunium.net/go/mautrix/bridgev2/networkid"
)
-func (d *DiscordClient) makePortalKey(ch *discordgo.Channel, userLoginID networkid.UserLoginID, wantReceiver bool) (key networkid.PortalKey) {
+func MakePortalKey(ch *discordgo.Channel, userLoginID networkid.UserLoginID, wantReceiver bool) (key networkid.PortalKey) {
key.ID = networkid.PortalID(ch.ID)
if wantReceiver {
key.Receiver = userLoginID
@@ -30,6 +30,11 @@ func (d *DiscordClient) makePortalKey(ch *discordgo.Channel, userLoginID network
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,
diff --git a/pkg/msgconv/embed.go b/pkg/msgconv/embed.go
new file mode 100644
index 0000000..79c1f96
--- /dev/null
+++ b/pkg/msgconv/embed.go
@@ -0,0 +1,97 @@
+// 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
+` + +const msgInteractionTemplateHTML = `↷ Forwarded
+%s +%s
+
+%s used /%s +` + +const msgComponentTemplateHTML = `
This message contains interactive elements. Use the Discord app to interact with the message.
` + +func (mc *MessageConverter) renderDiscordTextMessage(ctx context.Context, intent bridgev2.MatrixAPI, msg *discordgo.Message, source *bridgev2.UserLogin) *bridgev2.ConvertedMessagePart { + log := zerolog.Ctx(ctx) + if msg.Type == discordgo.MessageTypeCall { + return &bridgev2.ConvertedMessagePart{Type: event.EventMessage, Content: &event.MessageEventContent{ + MsgType: event.MsgEmote, + Body: "started a call", + }} + } else if msg.Type == discordgo.MessageTypeGuildMemberJoin { + return &bridgev2.ConvertedMessagePart{Type: event.EventMessage, Content: &event.MessageEventContent{ + MsgType: event.MsgEmote, + Body: "joined the server", + }} + } + + var htmlParts []string + + if msg.Interaction != nil { + 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)) + } else { + log.Err(err).Msg("Couldn't get ghost by ID while bridging interaction") + } + } + + if msg.Content != "" && !isPlainGifMessage(msg) { + // Bridge basic text messages. + htmlParts = append(htmlParts, mc.renderDiscordMarkdownOnlyHTML(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) + msgTSText := msg.MessageSnapshots[0].Message.Timestamp.Format("2006-01-02 15:04 MST") + origLink := fmt.Sprintf("unknown channel • %s", msgTSText) + if forwardedFromPortal, err := mc.bridge.DB.Portal.GetByKey(ctx, connector.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( + `#%s • %s`, + forwardedFromPortal.MXID.EventURI(origMessage.MXID, mc.bridge.Matrix.ServerName()), + forwardedFromPortal.Name, + msgTSText, + ) + } else if err != nil { + log.Err(err).Msg("Couldn't find corresponding message when bridging forwarded message") + } else if forwardedFromPortal.MXID != "" { + // We don't have the message but we have the portal, so link to that. + origLink = fmt.Sprintf( + `#%s • %s`, + forwardedFromPortal.MXID.URI(mc.bridge.Matrix.ServerName()), + forwardedFromPortal.Name, + msgTSText, + ) + } else if forwardedFromPortal.Name != "" { + // We only have the name of the portal. + origLink = fmt.Sprintf("%s • %s", forwardedFromPortal.Name, msgTSText) + } + } else { + log.Err(err).Msg("Couldn't find corresponding portal when bridging forwarded message") + } + + htmlParts = append(htmlParts, fmt.Sprintf(forwardTemplateHTML, forwardedHTML, origLink)) + } + + previews := make([]*event.BeeperLinkPreview, 0) + for i, embed := range msg.Embeds { + if i == 0 && msg.MessageReference == nil && isReplyEmbed(embed) { + continue + } + + with := log.With(). + Str("embed_type", string(embed.Type)). + Int("embed_index", i) + + switch getEmbedType(msg, embed) { + case EmbedRich: + log := with.Str("computed_embed_type", "rich").Logger() + htmlParts = append(htmlParts, mc.renderDiscordRichEmbed(log.WithContext(ctx), intent, embed, msg.ID, i)) + case EmbedLinkPreview: + log := with.Str("computed_embed_type", "link preview").Logger() + previews = append(previews, mc.renderDiscordLinkEmbed(log.WithContext(ctx), intent, embed)) + case EmbedVideo: + // Ignore video embeds, they're handled as separate messages. + default: + log := with.Logger() + log.Warn().Msg("Unknown embed type in message") + } + } + + if len(msg.Components) > 0 { + htmlParts = append(htmlParts, msgComponentTemplateHTML) + } + + if len(htmlParts) == 0 { + return nil + } + + fullHTML := strings.Join(htmlParts, "\n") + if !msg.MentionEveryone { + fullHTML = strings.ReplaceAll(fullHTML, "@room", "@\u2063ro\u2063om") + } + + content := format.HTMLToContent(fullHTML) + extraContent := map[string]any{ + "com.beeper.linkpreviews": previews, + } + + return &bridgev2.ConvertedMessagePart{Type: event.EventMessage, Content: &content, Extra: extraContent} +} + +func (mc *MessageConverter) renderDiscordVideoEmbed(context context.Context, intent bridgev2.MatrixAPI, embed *discordgo.MessageEmbed) *bridgev2.ConvertedMessagePart { + panic("unimplemented") +} + +func (mc *MessageConverter) renderDiscordSticker(context context.Context, intent bridgev2.MatrixAPI, sticker *discordgo.StickerItem) *bridgev2.ConvertedMessagePart { + panic("unimplemented") +} + +func (mc *MessageConverter) renderDiscordMarkdownOnlyHTML(text string, allowInlineLinks bool) string { + panic("unimplemented") +} + +func (mc *MessageConverter) renderDiscordMarkdownOnlyHTMLNoUnwrap(text string, allowInlineLinks bool) string { + panic("unimplemented") +} + +func (mc *MessageConverter) renderDiscordRichEmbed(context context.Context, intent bridgev2.MatrixAPI, embed *discordgo.MessageEmbed, messageID string, i int) string { + panic("unimplemented") +} + +func (mc *MessageConverter) renderDiscordLinkEmbed(context context.Context, intent bridgev2.MatrixAPI, embed *discordgo.MessageEmbed) *event.BeeperLinkPreview { + panic("unimplemented") +} + +func (mc *MessageConverter) renderDiscordAttachment(context context.Context, intent bridgev2.MatrixAPI, d string, att *discordgo.MessageAttachment) *bridgev2.ConvertedMessagePart { + panic("unimplemented") +} diff --git a/pkg/msgconv/msgconv.go b/pkg/msgconv/msgconv.go new file mode 100644 index 0000000..03a4aee --- /dev/null +++ b/pkg/msgconv/msgconv.go @@ -0,0 +1,23 @@ +// 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