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:
38
pkg/attachment/attachment.go
Normal file
38
pkg/attachment/attachment.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// 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 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
|
||||||
|
EncryptedFile *event.EncryptedFileInfo
|
||||||
|
}
|
||||||
@@ -28,9 +28,8 @@ import (
|
|||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"go.mau.fi/mautrix-discord/pkg/attachment"
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
"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) {
|
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 {
|
func (d *DiscordConnector) ReuploadMedia(ctx context.Context, intent bridgev2.MatrixAPI, portal *bridgev2.Portal, upload attachment.AttachmentReupload) (*attachment.ReuploadedAttachment, error) {
|
||||||
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) {
|
|
||||||
log := zerolog.Ctx(ctx)
|
log := zerolog.Ctx(ctx)
|
||||||
// TODO(skip): Do we need to check if we've already downloaded this media before?
|
// TODO(skip): Do we need to check if we've already downloaded this media before?
|
||||||
// TODO(skip): Read a maximum size from the config.
|
// 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 nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ReuploadedAttachment{
|
return &attachment.ReuploadedAttachment{
|
||||||
AttachmentReupload: upload,
|
AttachmentReupload: upload,
|
||||||
DownloadedSize: len(data),
|
DownloadedSize: len(data),
|
||||||
MXC: mxc,
|
MXC: mxc,
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ import (
|
|||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"go.mau.fi/mautrix-discord/pkg/msgconv"
|
||||||
"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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -65,15 +65,33 @@ func (dc *DiscordClient) FetchMessages(ctx context.Context, fetchParams bridgev2
|
|||||||
}
|
}
|
||||||
|
|
||||||
converted := make([]*bridgev2.BackfillMessage, 0, len(msgs))
|
converted := make([]*bridgev2.BackfillMessage, 0, len(msgs))
|
||||||
|
mc := msgconv.MessageConverter{
|
||||||
|
Bridge: dc.connector.Bridge,
|
||||||
|
ReuploadMedia: dc.connector.ReuploadMedia,
|
||||||
|
}
|
||||||
for _, msg := range msgs {
|
for _, msg := range msgs {
|
||||||
streamOrder, _ := strconv.ParseInt(msg.ID, 10, 64)
|
streamOrder, _ := strconv.ParseInt(msg.ID, 10, 64)
|
||||||
ts, _ := discordgo.SnowflakeTimestamp(msg.ID)
|
ts, _ := discordgo.SnowflakeTimestamp(msg.ID)
|
||||||
|
|
||||||
// FIXME(skip): Backfill reactions.
|
// 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{
|
converted = append(converted, &bridgev2.BackfillMessage{
|
||||||
ConvertedMessage: dc.convertMessage(msg),
|
|
||||||
ID: networkid.MessageID(msg.ID),
|
ID: networkid.MessageID(msg.ID),
|
||||||
Sender: dc.makeEventSender(msg.Author),
|
ConvertedMessage: mc.ToMatrix(ctx, fetchParams.Portal, intent, dc.UserLogin, msg),
|
||||||
|
Sender: sender,
|
||||||
Timestamp: ts,
|
Timestamp: ts,
|
||||||
StreamOrder: streamOrder,
|
StreamOrder: streamOrder,
|
||||||
})
|
})
|
||||||
@@ -91,43 +109,3 @@ func (dc *DiscordClient) FetchMessages(ctx context.Context, fetchParams bridgev2
|
|||||||
HasMore: len(msgs) == count,
|
HasMore: len(msgs) == count,
|
||||||
}, nil
|
}, 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"go.mau.fi/mautrix-discord/pkg/discordid"
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
"maunium.net/go/mautrix/bridgev2/database"
|
"maunium.net/go/mautrix/bridgev2/database"
|
||||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
"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) {
|
func (d *DiscordClient) syncChannel(_ context.Context, ch *discordgo.Channel, selfIsInChannel bool) {
|
||||||
isGroup := len(ch.RecipientIDs) > 1
|
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{
|
d.connector.Bridge.QueueRemoteEvent(d.UserLogin, &DiscordChatResync{
|
||||||
channel: ch,
|
channel: ch,
|
||||||
portalKey: MakePortalKey(ch, d.UserLogin.ID, true),
|
portalKey: discordid.MakePortalKey(ch, d.UserLogin.ID, true),
|
||||||
info: &bridgev2.ChatInfo{
|
info: &bridgev2.ChatInfo{
|
||||||
Name: &ch.Name,
|
Name: &ch.Name,
|
||||||
Members: &members,
|
Members: &members,
|
||||||
|
|||||||
@@ -14,11 +14,10 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package connector
|
package discordid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
|
||||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,11 +33,3 @@ func MakePortalKeyWithID(channelID string) (key networkid.PortalKey) {
|
|||||||
key.ID = networkid.PortalID(channelID)
|
key.ID = networkid.PortalID(channelID)
|
||||||
return
|
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
99
pkg/msgconv/formatter.go
Normal file
99
pkg/msgconv/formatter.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// 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 msgconv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
"github.com/yuin/goldmark/extension"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
|
"maunium.net/go/mautrix/format"
|
||||||
|
"maunium.net/go/mautrix/format/mdext"
|
||||||
|
)
|
||||||
|
|
||||||
|
// escapeFixer is a hacky partial fix for the difference in escaping markdown, used with escapeReplacement
|
||||||
|
//
|
||||||
|
// Discord allows escaping with just one backslash, e.g. \__a__,
|
||||||
|
// but standard markdown requires both to be escaped (\_\_a__)
|
||||||
|
var escapeFixer = regexp.MustCompile(`\\(__[^_]|\*\*[^*])`)
|
||||||
|
|
||||||
|
func escapeReplacement(s string) string {
|
||||||
|
return s[:2] + `\` + s[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// indentableParagraphParser is the default paragraph parser with CanAcceptIndentedLine.
|
||||||
|
// Used when disabling CodeBlockParser (as disabling it without a replacement will make indented blocks disappear).
|
||||||
|
type indentableParagraphParser struct {
|
||||||
|
parser.BlockParser
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultIndentableParagraphParser = &indentableParagraphParser{BlockParser: parser.NewParagraphParser()}
|
||||||
|
|
||||||
|
func (b *indentableParagraphParser) CanAcceptIndentedLine() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeFeaturesExceptLinks = []any{
|
||||||
|
parser.NewListParser(), parser.NewListItemParser(), parser.NewHTMLBlockParser(), parser.NewRawHTMLParser(),
|
||||||
|
parser.NewSetextHeadingParser(), parser.NewThematicBreakParser(),
|
||||||
|
parser.NewCodeBlockParser(),
|
||||||
|
}
|
||||||
|
var removeFeaturesAndLinks = append(removeFeaturesExceptLinks, parser.NewLinkParser())
|
||||||
|
var fixIndentedParagraphs = goldmark.WithParserOptions(parser.WithBlockParsers(util.Prioritized(defaultIndentableParagraphParser, 500)))
|
||||||
|
var discordExtensions = goldmark.WithExtensions(extension.Strikethrough, mdext.SimpleSpoiler, mdext.DiscordUnderline, ExtDiscordEveryone, ExtDiscordTag)
|
||||||
|
|
||||||
|
var discordRenderer = goldmark.New(
|
||||||
|
goldmark.WithParser(mdext.ParserWithoutFeatures(removeFeaturesAndLinks...)),
|
||||||
|
fixIndentedParagraphs, format.HTMLOptions, discordExtensions,
|
||||||
|
)
|
||||||
|
var discordRendererWithInlineLinks = goldmark.New(
|
||||||
|
goldmark.WithParser(mdext.ParserWithoutFeatures(removeFeaturesExceptLinks...)),
|
||||||
|
fixIndentedParagraphs, format.HTMLOptions, discordExtensions,
|
||||||
|
)
|
||||||
|
|
||||||
|
// renderDiscordMarkdownOnlyHTML converts Discord-flavored Markdown text to HTML.
|
||||||
|
//
|
||||||
|
// After conversion, if the text is surrounded by a single outermost paragraph
|
||||||
|
// tag, it is unwrapped.
|
||||||
|
func (mc *MessageConverter) renderDiscordMarkdownOnlyHTML(portal *bridgev2.Portal, text string, allowInlineLinks bool) string {
|
||||||
|
return format.UnwrapSingleParagraph(mc.renderDiscordMarkdownOnlyHTMLNoUnwrap(portal, text, allowInlineLinks))
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderDiscordMarkdownOnlyHTMLNoUnwrap converts Discord-flavored Markdown text to HTML.
|
||||||
|
func (mc *MessageConverter) renderDiscordMarkdownOnlyHTMLNoUnwrap(portal *bridgev2.Portal, text string, allowInlineLinks bool) string {
|
||||||
|
text = escapeFixer.ReplaceAllStringFunc(text, escapeReplacement)
|
||||||
|
|
||||||
|
var buf strings.Builder
|
||||||
|
ctx := parser.NewContext()
|
||||||
|
ctx.Set(parserContextPortal, portal)
|
||||||
|
renderer := discordRenderer
|
||||||
|
if allowInlineLinks {
|
||||||
|
renderer = discordRendererWithInlineLinks
|
||||||
|
}
|
||||||
|
err := renderer.Convert([]byte(text), &buf, parser.WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("markdown parser errored: %w", err))
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(skip): Stopping here for now. Continue at formatterContextPortalKey.
|
||||||
110
pkg/msgconv/formatter_everyone.go
Normal file
110
pkg/msgconv/formatter_everyone.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// 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 msgconv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
"github.com/yuin/goldmark/renderer"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type astDiscordEveryone struct {
|
||||||
|
ast.BaseInline
|
||||||
|
onlyHere bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ast.Node = (*astDiscordEveryone)(nil)
|
||||||
|
var astKindDiscordEveryone = ast.NewNodeKind("DiscordEveryone")
|
||||||
|
|
||||||
|
func (n *astDiscordEveryone) Dump(source []byte, level int) {
|
||||||
|
ast.DumpHelper(n, source, level, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *astDiscordEveryone) Kind() ast.NodeKind {
|
||||||
|
return astKindDiscordEveryone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *astDiscordEveryone) String() string {
|
||||||
|
if n.onlyHere {
|
||||||
|
return "@here"
|
||||||
|
}
|
||||||
|
return "@everyone"
|
||||||
|
}
|
||||||
|
|
||||||
|
type discordEveryoneParser struct{}
|
||||||
|
|
||||||
|
var discordEveryoneRegex = regexp.MustCompile(`@(everyone|here)`)
|
||||||
|
var defaultDiscordEveryoneParser = &discordEveryoneParser{}
|
||||||
|
|
||||||
|
func (s *discordEveryoneParser) Trigger() []byte {
|
||||||
|
return []byte{'@'}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *discordEveryoneParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
|
||||||
|
line, _ := block.PeekLine()
|
||||||
|
match := discordEveryoneRegex.FindSubmatch(line)
|
||||||
|
if match == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
block.Advance(len(match[0]))
|
||||||
|
return &astDiscordEveryone{
|
||||||
|
onlyHere: string(match[1]) == "here",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *discordEveryoneParser) CloseBlock(parent ast.Node, pc parser.Context) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
type discordEveryoneHTMLRenderer struct{}
|
||||||
|
|
||||||
|
func (r *discordEveryoneHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||||
|
reg.Register(astKindDiscordEveryone, r.renderDiscordEveryone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *discordEveryoneHTMLRenderer) renderDiscordEveryone(w util.BufWriter, source []byte, n ast.Node, entering bool) (status ast.WalkStatus, err error) {
|
||||||
|
status = ast.WalkContinue
|
||||||
|
if !entering {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mention, _ := n.(*astDiscordEveryone)
|
||||||
|
class := "everyone"
|
||||||
|
if mention != nil && mention.onlyHere {
|
||||||
|
class = "here"
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(w, `<span class="discord-mention-%s">@room</span>`, class)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type discordEveryone struct{}
|
||||||
|
|
||||||
|
var ExtDiscordEveryone = &discordEveryone{}
|
||||||
|
|
||||||
|
func (e *discordEveryone) Extend(m goldmark.Markdown) {
|
||||||
|
m.Parser().AddOptions(parser.WithInlineParsers(
|
||||||
|
util.Prioritized(defaultDiscordEveryoneParser, 600),
|
||||||
|
))
|
||||||
|
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||||
|
util.Prioritized(&discordEveryoneHTMLRenderer{}, 600),
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -16,7 +16,77 @@
|
|||||||
|
|
||||||
package msgconv
|
package msgconv
|
||||||
|
|
||||||
// TODO(skip): Port the rest of this.
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
"github.com/yuin/goldmark/renderer"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
"go.mau.fi/mautrix-discord/pkg/discordid"
|
||||||
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
|
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
type astDiscordTag struct {
|
||||||
|
ast.BaseInline
|
||||||
|
portal *bridgev2.Portal
|
||||||
|
id int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ast.Node = (*astDiscordTag)(nil)
|
||||||
|
var astKindDiscordTag = ast.NewNodeKind("DiscordTag")
|
||||||
|
|
||||||
|
func (n *astDiscordTag) Dump(source []byte, level int) {
|
||||||
|
ast.DumpHelper(n, source, level, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *astDiscordTag) Kind() ast.NodeKind {
|
||||||
|
return astKindDiscordTag
|
||||||
|
}
|
||||||
|
|
||||||
|
type astDiscordUserMention struct {
|
||||||
|
astDiscordTag
|
||||||
|
hasNick bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *astDiscordUserMention) String() string {
|
||||||
|
if n.hasNick {
|
||||||
|
return fmt.Sprintf("<@!%d>", n.id)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("<@%d>", n.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
type astDiscordRoleMention struct {
|
||||||
|
astDiscordTag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *astDiscordRoleMention) String() string {
|
||||||
|
return fmt.Sprintf("<@&%d>", n.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
type astDiscordChannelMention struct {
|
||||||
|
astDiscordTag
|
||||||
|
|
||||||
|
guildID int64
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *astDiscordChannelMention) String() string {
|
||||||
|
if n.guildID != 0 {
|
||||||
|
return fmt.Sprintf("<#%d:%d:%s>", n.id, n.guildID, n.name)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("<#%d>", n.id)
|
||||||
|
}
|
||||||
|
|
||||||
type discordTimestampStyle rune
|
type discordTimestampStyle rune
|
||||||
|
|
||||||
@@ -38,3 +108,237 @@ func (dts discordTimestampStyle) Format() string {
|
|||||||
return "2 January 2006 15:04 MST"
|
return "2 January 2006 15:04 MST"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type astDiscordTimestamp struct {
|
||||||
|
astDiscordTag
|
||||||
|
|
||||||
|
timestamp int64
|
||||||
|
style discordTimestampStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *astDiscordTimestamp) String() string {
|
||||||
|
if n.style == 'f' {
|
||||||
|
return fmt.Sprintf("<t:%d>", n.timestamp)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("<t:%d:%c>", n.timestamp, n.style)
|
||||||
|
}
|
||||||
|
|
||||||
|
type astDiscordCustomEmoji struct {
|
||||||
|
astDiscordTag
|
||||||
|
name string
|
||||||
|
animated bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *astDiscordCustomEmoji) String() string {
|
||||||
|
if n.animated {
|
||||||
|
return fmt.Sprintf("<a%s%d>", n.name, n.id)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("<%s%d>", n.name, n.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
type discordTagParser struct{}
|
||||||
|
|
||||||
|
// Regex to match everything in https://discord.com/developers/docs/reference#message-formatting
|
||||||
|
var discordTagRegex = regexp.MustCompile(`<(a?:\w+:|@[!&]?|#|t:)(\d+)(?::([tTdDfFR])|(\d+):(.+?))?>`)
|
||||||
|
var defaultDiscordTagParser = &discordTagParser{}
|
||||||
|
|
||||||
|
func (s *discordTagParser) Trigger() []byte {
|
||||||
|
return []byte{'<'}
|
||||||
|
}
|
||||||
|
|
||||||
|
var parserContextPortal = parser.NewContextKey()
|
||||||
|
|
||||||
|
func (s *discordTagParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
|
||||||
|
portal := pc.Get(parserContextPortal).(*bridgev2.Portal)
|
||||||
|
//before := block.PrecendingCharacter()
|
||||||
|
line, _ := block.PeekLine()
|
||||||
|
match := discordTagRegex.FindSubmatch(line)
|
||||||
|
if match == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
//seg := segment.WithStop(segment.Start + len(match[0]))
|
||||||
|
block.Advance(len(match[0]))
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(string(match[2]), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tag := astDiscordTag{id: id, portal: portal}
|
||||||
|
tagName := string(match[1])
|
||||||
|
switch {
|
||||||
|
case tagName == "@":
|
||||||
|
return &astDiscordUserMention{astDiscordTag: tag}
|
||||||
|
case tagName == "@!":
|
||||||
|
return &astDiscordUserMention{astDiscordTag: tag, hasNick: true}
|
||||||
|
case tagName == "@&":
|
||||||
|
return &astDiscordRoleMention{astDiscordTag: tag}
|
||||||
|
case tagName == "#":
|
||||||
|
var guildID int64
|
||||||
|
var channelName string
|
||||||
|
if len(match[4]) > 0 && len(match[5]) > 0 {
|
||||||
|
guildID, _ = strconv.ParseInt(string(match[4]), 10, 64)
|
||||||
|
channelName = string(match[5])
|
||||||
|
}
|
||||||
|
return &astDiscordChannelMention{astDiscordTag: tag, guildID: guildID, name: channelName}
|
||||||
|
case tagName == "t:":
|
||||||
|
var style discordTimestampStyle
|
||||||
|
if len(match[3]) == 0 {
|
||||||
|
style = 'f'
|
||||||
|
} else {
|
||||||
|
style = discordTimestampStyle(match[3][0])
|
||||||
|
}
|
||||||
|
return &astDiscordTimestamp{
|
||||||
|
astDiscordTag: tag,
|
||||||
|
timestamp: id,
|
||||||
|
style: style,
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(tagName, ":"):
|
||||||
|
return &astDiscordCustomEmoji{name: tagName, astDiscordTag: tag}
|
||||||
|
case strings.HasPrefix(tagName, "a:"):
|
||||||
|
return &astDiscordCustomEmoji{name: tagName[1:], astDiscordTag: tag, animated: true}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *discordTagParser) CloseBlock(parent ast.Node, pc parser.Context) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
type discordTagHTMLRenderer struct{}
|
||||||
|
|
||||||
|
var defaultDiscordTagHTMLRenderer = &discordTagHTMLRenderer{}
|
||||||
|
|
||||||
|
func (r *discordTagHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||||
|
reg.Register(astKindDiscordTag, r.renderDiscordMention)
|
||||||
|
}
|
||||||
|
|
||||||
|
func relativeTimeFormat(ts time.Time) string {
|
||||||
|
now := time.Now()
|
||||||
|
if ts.Year() >= 2262 {
|
||||||
|
return "date out of range for relative format"
|
||||||
|
}
|
||||||
|
duration := ts.Sub(now)
|
||||||
|
word := "in %s"
|
||||||
|
if duration < 0 {
|
||||||
|
duration = -duration
|
||||||
|
word = "%s ago"
|
||||||
|
}
|
||||||
|
var count int
|
||||||
|
var unit string
|
||||||
|
switch {
|
||||||
|
case duration < time.Second:
|
||||||
|
count = int(duration.Milliseconds())
|
||||||
|
unit = "millisecond"
|
||||||
|
case duration < time.Minute:
|
||||||
|
count = int(math.Round(duration.Seconds()))
|
||||||
|
unit = "second"
|
||||||
|
case duration < time.Hour:
|
||||||
|
count = int(math.Round(duration.Minutes()))
|
||||||
|
unit = "minute"
|
||||||
|
case duration < 24*time.Hour:
|
||||||
|
count = int(math.Round(duration.Hours()))
|
||||||
|
unit = "hour"
|
||||||
|
case duration < 30*24*time.Hour:
|
||||||
|
count = int(math.Round(duration.Hours() / 24))
|
||||||
|
unit = "day"
|
||||||
|
case duration < 365*24*time.Hour:
|
||||||
|
count = int(math.Round(duration.Hours() / 24 / 30))
|
||||||
|
unit = "month"
|
||||||
|
default:
|
||||||
|
count = int(math.Round(duration.Hours() / 24 / 365))
|
||||||
|
unit = "year"
|
||||||
|
}
|
||||||
|
var diff string
|
||||||
|
if count == 1 {
|
||||||
|
diff = fmt.Sprintf("a %s", unit)
|
||||||
|
} else {
|
||||||
|
diff = fmt.Sprintf("%d %ss", count, unit)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(word, diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source []byte, n ast.Node, entering bool) (status ast.WalkStatus, err error) {
|
||||||
|
status = ast.WalkContinue
|
||||||
|
if !entering {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.TODO()
|
||||||
|
|
||||||
|
switch node := n.(type) {
|
||||||
|
case *astDiscordUserMention:
|
||||||
|
var mxid id.UserID
|
||||||
|
var name string
|
||||||
|
if ghost, _ := node.portal.Bridge.GetGhostByID(ctx, networkid.UserID(strconv.FormatInt(node.id, 10))); ghost != nil {
|
||||||
|
mxid = ghost.Intent.GetMXID()
|
||||||
|
name = ghost.Name
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(w, `<a href="%s">%s</a>`, mxid.URI().MatrixToURL(), name)
|
||||||
|
return
|
||||||
|
case *astDiscordRoleMention:
|
||||||
|
// FIXME(skip): Implement.
|
||||||
|
// role := node.portal.Bridge.DB.Role.GetByID(node.portal.GuildID, strconv.FormatInt(node.id, 10))
|
||||||
|
// if role != nil {
|
||||||
|
_, _ = fmt.Fprintf(w, `<strong>@unknown-role</strong>`)
|
||||||
|
// _, _ = fmt.Fprintf(w, `<font color="#%06x"><strong>@%s</strong></font>`, role.Color, role.Name)
|
||||||
|
return
|
||||||
|
// }
|
||||||
|
case *astDiscordChannelMention:
|
||||||
|
if portal, _ := node.portal.Bridge.GetPortalByKey(ctx, discordid.MakePortalKeyWithID(
|
||||||
|
strconv.FormatInt(node.id, 10),
|
||||||
|
)); portal != nil {
|
||||||
|
if portal.MXID != "" {
|
||||||
|
_, _ = fmt.Fprintf(w, `<a href="%s">%s</a>`, portal.MXID.URI(portal.Bridge.Matrix.ServerName()).MatrixToURL(), portal.Name)
|
||||||
|
} else {
|
||||||
|
_, _ = w.WriteString(portal.Name)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case *astDiscordCustomEmoji:
|
||||||
|
// FIXME(skip): Implement.
|
||||||
|
_, _ = fmt.Fprintf(w, `(emoji)`)
|
||||||
|
// reactionMXC := node.portal.Bridge.getEmojiMXCByDiscordID(strconv.FormatInt(node.id, 10), node.name, node.animated)
|
||||||
|
// if !reactionMXC.IsEmpty() {
|
||||||
|
// attrs := "data-mx-emoticon"
|
||||||
|
// if node.animated {
|
||||||
|
// attrs += " data-mau-animated-emoji"
|
||||||
|
// }
|
||||||
|
// _, _ = fmt.Fprintf(w, `<img %[3]s src="%[1]s" alt="%[2]s" title="%[2]s" height="32"/>`, reactionMXC.String(), node.name, attrs)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
case *astDiscordTimestamp:
|
||||||
|
ts := time.Unix(node.timestamp, 0).UTC()
|
||||||
|
var formatted string
|
||||||
|
if node.style == 'R' {
|
||||||
|
formatted = relativeTimeFormat(ts)
|
||||||
|
} else {
|
||||||
|
formatted = ts.Format(node.style.Format())
|
||||||
|
}
|
||||||
|
// https://github.com/matrix-org/matrix-spec-proposals/pull/3160
|
||||||
|
const fullDatetimeFormat = "2006-01-02T15:04:05.000-0700"
|
||||||
|
fullRFC := ts.Format(fullDatetimeFormat)
|
||||||
|
fullHumanReadable := ts.Format(discordTimestampStyle('F').Format())
|
||||||
|
_, _ = fmt.Fprintf(w, `<time title="%s" datetime="%s" data-discord-style="%c"><strong>%s</strong></time>`, fullHumanReadable, fullRFC, node.style, formatted)
|
||||||
|
}
|
||||||
|
stringifiable, ok := n.(fmt.Stringer)
|
||||||
|
if ok {
|
||||||
|
_, _ = w.WriteString(stringifiable.String())
|
||||||
|
} else {
|
||||||
|
_, _ = w.Write(source)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type discordTag struct{}
|
||||||
|
|
||||||
|
var ExtDiscordTag = &discordTag{}
|
||||||
|
|
||||||
|
func (e *discordTag) Extend(m goldmark.Markdown) {
|
||||||
|
m.Parser().AddOptions(parser.WithInlineParsers(
|
||||||
|
util.Prioritized(defaultDiscordTagParser, 600),
|
||||||
|
))
|
||||||
|
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||||
|
util.Prioritized(defaultDiscordTagHTMLRenderer, 600),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import (
|
|||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
"github.com/rs/zerolog"
|
"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"
|
||||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
@@ -111,6 +112,11 @@ func (mc *MessageConverter) ToMatrix(
|
|||||||
// puppet.addMemberMeta(part, msg)
|
// 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}
|
return &bridgev2.ConvertedMessage{Parts: parts}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +149,7 @@ func (mc *MessageConverter) renderDiscordTextMessage(ctx context.Context, intent
|
|||||||
var htmlParts []string
|
var htmlParts []string
|
||||||
|
|
||||||
if msg.Interaction != nil {
|
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.
|
// TODO(skip): Try doing ghost.UpdateInfoIfNecessary.
|
||||||
if err == nil {
|
if err == nil {
|
||||||
htmlParts = append(htmlParts, fmt.Sprintf(msgInteractionTemplateHTML, ghost.Intent.GetMXID(), ghost.Name, msg.Interaction.Name))
|
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) {
|
if msg.Content != "" && !isPlainGifMessage(msg) {
|
||||||
// Bridge basic text messages.
|
// 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 &&
|
} else if msg.MessageReference != nil &&
|
||||||
msg.MessageReference.Type == discordgo.MessageReferenceTypeForward &&
|
msg.MessageReference.Type == discordgo.MessageReferenceTypeForward &&
|
||||||
len(msg.MessageSnapshots) > 0 &&
|
len(msg.MessageSnapshots) > 0 &&
|
||||||
msg.MessageSnapshots[0].Message != nil {
|
msg.MessageSnapshots[0].Message != nil {
|
||||||
// Bridge forwarded messages.
|
// 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")
|
msgTSText := msg.MessageSnapshots[0].Message.Timestamp.Format("2006-01-02 15:04 MST")
|
||||||
origLink := fmt.Sprintf("unknown channel • %s", msgTSText)
|
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 forwardedFromPortal, err := mc.Bridge.DB.Portal.GetByKey(ctx, discordid.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 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.
|
// We've bridged the message that was forwarded, so we can link to it directly.
|
||||||
origLink = fmt.Sprintf(
|
origLink = fmt.Sprintf(
|
||||||
`<a href="%s">#%s • %s</a>`,
|
`<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,
|
forwardedFromPortal.Name,
|
||||||
msgTSText,
|
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.
|
// We don't have the message but we have the portal, so link to that.
|
||||||
origLink = fmt.Sprintf(
|
origLink = fmt.Sprintf(
|
||||||
`<a href="%s">#%s</a> • %s`,
|
`<a href="%s">#%s</a> • %s`,
|
||||||
forwardedFromPortal.MXID.URI(mc.connector.Bridge.Matrix.ServerName()),
|
forwardedFromPortal.MXID.URI(mc.Bridge.Matrix.ServerName()),
|
||||||
forwardedFromPortal.Name,
|
forwardedFromPortal.Name,
|
||||||
msgTSText,
|
msgTSText,
|
||||||
)
|
)
|
||||||
@@ -264,10 +270,10 @@ func (mc *MessageConverter) renderDiscordVideoEmbed(ctx context.Context, intent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
upload := connector.AttachmentReupload{
|
upload := attachment.AttachmentReupload{
|
||||||
DownloadingURL: proxyURL,
|
DownloadingURL: proxyURL,
|
||||||
}
|
}
|
||||||
reupload, err := mc.connector.ReuploadMedia(ctx, intent, portal, upload)
|
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{
|
||||||
@@ -282,10 +288,7 @@ func (mc *MessageConverter) renderDiscordVideoEmbed(ctx context.Context, intent
|
|||||||
MimeType: reupload.MimeType,
|
MimeType: reupload.MimeType,
|
||||||
Size: reupload.DownloadedSize,
|
Size: reupload.DownloadedSize,
|
||||||
},
|
},
|
||||||
File: &event.EncryptedFileInfo{
|
File: reupload.EncryptedFile,
|
||||||
EncryptedFile: reupload.EncryptedFile.EncryptedFile,
|
|
||||||
URL: reupload.MXC,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if embed.Video != nil {
|
if embed.Video != nil {
|
||||||
@@ -321,14 +324,6 @@ func (mc *MessageConverter) renderDiscordSticker(context context.Context, intent
|
|||||||
panic("unimplemented")
|
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 (
|
const (
|
||||||
embedHTMLWrapper = `<blockquote class="discord-embed">%s</blockquote>`
|
embedHTMLWrapper = `<blockquote class="discord-embed">%s</blockquote>`
|
||||||
embedHTMLWrapperColor = `<blockquote class="discord-embed" background-color="#%06X">%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)
|
authorHTML = fmt.Sprintf(embedHTMLAuthorPlain, authorNameHTML)
|
||||||
if embed.Author.ProxyIconURL != "" {
|
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,
|
DownloadingURL: embed.Author.ProxyIconURL,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -376,7 +371,7 @@ func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent b
|
|||||||
|
|
||||||
if embed.Title != "" {
|
if embed.Title != "" {
|
||||||
var titleHTML string
|
var titleHTML string
|
||||||
baseTitleHTML := mc.renderDiscordMarkdownOnlyHTML(embed.Title, false)
|
baseTitleHTML := mc.renderDiscordMarkdownOnlyHTML(portal, embed.Title, false)
|
||||||
if embed.URL != "" {
|
if embed.URL != "" {
|
||||||
titleHTML = fmt.Sprintf(embedHTMLTitleWithLink, html.EscapeString(embed.URL), baseTitleHTML)
|
titleHTML = fmt.Sprintf(embedHTMLTitleWithLink, html.EscapeString(embed.URL), baseTitleHTML)
|
||||||
} else {
|
} else {
|
||||||
@@ -386,7 +381,7 @@ func (mc *MessageConverter) renderDiscordRichEmbed(ctx context.Context, intent b
|
|||||||
}
|
}
|
||||||
|
|
||||||
if embed.Description != "" {
|
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++ {
|
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))
|
headerParts := make([]string, len(splitItems))
|
||||||
contentParts := make([]string, len(splitItems))
|
contentParts := make([]string, len(splitItems))
|
||||||
for j, splitItem := range splitItems {
|
for j, splitItem := range splitItems {
|
||||||
headerParts[j] = fmt.Sprintf(embedHTMLFieldName, mc.renderDiscordMarkdownOnlyHTML(splitItem.Name, false))
|
headerParts[j] = fmt.Sprintf(embedHTMLFieldName, mc.renderDiscordMarkdownOnlyHTML(portal, splitItem.Name, false))
|
||||||
contentParts[j] = fmt.Sprintf(embedHTMLFieldValue, mc.renderDiscordMarkdownOnlyHTML(splitItem.Value, true))
|
contentParts[j] = fmt.Sprintf(embedHTMLFieldValue, mc.renderDiscordMarkdownOnlyHTML(portal, splitItem.Value, true))
|
||||||
}
|
}
|
||||||
htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLFields, strings.Join(headerParts, ""), strings.Join(contentParts, "")))
|
htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLFields, strings.Join(headerParts, ""), strings.Join(contentParts, "")))
|
||||||
} else {
|
} else {
|
||||||
htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLLinearField,
|
htmlParts = append(htmlParts, fmt.Sprintf(embedHTMLLinearField,
|
||||||
strconv.FormatBool(item.Inline),
|
strconv.FormatBool(item.Inline),
|
||||||
mc.renderDiscordMarkdownOnlyHTML(item.Name, false),
|
mc.renderDiscordMarkdownOnlyHTML(portal, item.Name, false),
|
||||||
mc.renderDiscordMarkdownOnlyHTML(item.Value, true),
|
mc.renderDiscordMarkdownOnlyHTML(portal, item.Value, true),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if embed.Image != nil {
|
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,
|
DownloadingURL: embed.Image.ProxyURL,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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)
|
footerHTML = fmt.Sprintf(embedHTMLFooterPlain, html.EscapeString(embed.Footer.Text), datePart)
|
||||||
if embed.Footer.ProxyIconURL != "" {
|
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,
|
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) {
|
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,
|
DownloadingURL: url,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -556,7 +551,7 @@ func (mc *MessageConverter) renderDiscordAttachment(ctx context.Context, intent
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO(skip): Support direct media.
|
// 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,
|
DownloadingURL: att.URL,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -572,10 +567,8 @@ func (mc *MessageConverter) renderDiscordAttachment(ctx context.Context, intent
|
|||||||
content.Info.Width = att.Width
|
content.Info.Width = att.Width
|
||||||
content.Info.Height = att.Height
|
content.Info.Height = att.Height
|
||||||
}
|
}
|
||||||
content.File = &event.EncryptedFileInfo{
|
content.URL = reupload.MXC
|
||||||
EncryptedFile: reupload.EncryptedFile.EncryptedFile,
|
content.File = reupload.EncryptedFile
|
||||||
URL: reupload.MXC,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &bridgev2.ConvertedMessagePart{
|
return &bridgev2.ConvertedMessagePart{
|
||||||
Type: event.EventMessage,
|
Type: event.EventMessage,
|
||||||
|
|||||||
@@ -17,9 +17,19 @@
|
|||||||
package msgconv
|
package msgconv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"go.mau.fi/mautrix-discord/pkg/connector"
|
"context"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-discord/pkg/attachment"
|
||||||
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MessageConverter struct {
|
type MessageConverter struct {
|
||||||
connector *connector.DiscordConnector
|
Bridge *bridgev2.Bridge
|
||||||
|
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user