Watch
1
0
Fork
You've already forked mautrix-discord
0
mirror of https://github.com/mautrix/discord.git synced 2026-11-13 01:29:18 -05:00
mautrix-discord/pkg/connector/relay.go

219 lines
8.8 KiB
Go

// mautrix-discord - A Matrix-Discord puppeting bridge.
// Copyright (C) 2026 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 (
"context"
"fmt"
"strings"
"github.com/bwmarrin/discordgo"
"github.com/rs/zerolog"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-discord/pkg/discordid"
)
func (d *DiscordConnector) relayMetadata(portal *bridgev2.Portal) discordid.PortalMetadata {
d.relayLock.RLock()
defer d.relayLock.RUnlock()
return *portal.Metadata.(*discordid.PortalMetadata)
}
func (d *DiscordConnector) isWebhookMessage(portal *bridgev2.Portal, message *database.Message) bool {
meta := d.relayMetadata(portal)
return message != nil && meta.RelayWebhookID != "" && message.SenderID == discordid.MakeUserID(meta.RelayWebhookID)
}
func webhookError(err error, secret string) error {
if err == nil {
return nil
}
if secret == "" {
return fmt.Errorf("webhook request failed: %w", err)
}
return fmt.Errorf("webhook request failed: %s", strings.ReplaceAll(err.Error(), secret, "<redacted>"))
}
func (d *DiscordClient) webhookAllowedMentions(ctx context.Context, portal *bridgev2.Portal, sender id.UserID, content string, allowed *discordgo.MessageAllowedMentions) *discordgo.MessageAllowedMentions {
if allowed == nil {
allowed = &discordgo.MessageAllowedMentions{Parse: []discordgo.AllowedMentionType{}}
}
if strings.Contains(content, "@everyone") || strings.Contains(content, "@here") {
levels, err := d.connector.Bridge.Matrix.GetPowerLevels(ctx, portal.MXID)
if err == nil && levels.GetUserLevel(sender) >= levels.Notifications.Room() {
allowed.Parse = append(allowed.Parse, discordgo.AllowedMentionTypeEveryone)
}
}
return allowed
}
func (d *DiscordClient) sendWebhookMessage(ctx context.Context, msg *bridgev2.MatrixMessage, threadID string) (*bridgev2.MatrixMessageResponse, error) {
meta := d.connector.relayMetadata(msg.Portal)
if meta.RelayWebhookID == "" || meta.RelayWebhookSecret == "" || meta.GuildID == "" {
return nil, fmt.Errorf("this channel has no relay webhook")
}
converted, err := d.connector.MsgConv.ToDiscord(ctx, d.connector.relayClient, msg, discordid.ParseChannelPortalID(msg.Portal.ID), discordgo.WithContext(ctx), true)
if err != nil {
return nil, err
}
sender := msg.OrigSender
name := sender.Displayname
if name == "" {
name = sender.UserID.String()
}
avatar := ""
if publicMedia, ok := d.connector.Bridge.Matrix.(bridgev2.MatrixConnectorWithPublicMedia); ok && sender.AvatarURL != "" {
avatar = publicMedia.GetPublicMediaAddress(sender.AvatarURL)
}
params := &discordgo.WebhookParams{
Content: converted.Content, Username: name, AvatarURL: avatar,
Files: converted.Files,
AllowedMentions: d.webhookAllowedMentions(ctx, msg.Portal, msg.Event.Sender, converted.Content, converted.AllowedMentions),
}
if msg.ReplyTo != nil && msg.ReplyTo.ThreadRoot == getMatrixThreadRootID(msg.ThreadRoot) {
if embed, err := d.webhookReplyEmbed(ctx, msg, threadID); err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to create webhook reply embed")
} else {
params.Embeds = []*discordgo.MessageEmbed{embed}
}
}
sent, err := d.connector.relayClient.WebhookThreadExecute(meta.RelayWebhookID, meta.RelayWebhookSecret, true, threadID, params, discordgo.WithContext(ctx))
if err != nil {
return nil, webhookError(err, meta.RelayWebhookSecret)
}
timestamp, _ := discordgo.SnowflakeTimestamp(sent.ID)
return &bridgev2.MatrixMessageResponse{DB: &database.Message{
ID: discordid.MakeMessageID(sent.ID), SenderID: discordid.MakeUserID(meta.RelayWebhookID), Timestamp: timestamp,
}}, nil
}
func getMatrixThreadRootID(root *database.Message) networkid.MessageID {
if root == nil {
return ""
}
if root.ThreadRoot != "" {
return root.ThreadRoot
}
return root.ID
}
func (d *DiscordClient) webhookReplyEmbed(ctx context.Context, msg *bridgev2.MatrixMessage, threadID string) (*discordgo.MessageEmbed, error) {
target, err := d.connector.Bridge.Bot.GetEvent(ctx, msg.Portal.MXID, msg.ReplyTo.MXID)
if err != nil {
return nil, err
}
if target.Content.Parsed == nil {
if err = target.Content.ParseRaw(target.Type); err != nil {
return nil, err
}
}
content, ok := target.Content.Parsed.(*event.MessageEventContent)
if !ok {
return nil, fmt.Errorf("reply target isn't a message")
}
copy := *content
copy.RemoveReplyFallback()
body := strings.TrimSpace(copy.Body)
firstLine, _, multiline := strings.Cut(body, "\n")
runes := []rune(firstLine)
if len(runes) > 72 {
firstLine = string(runes[:72]) + "…"
} else if multiline {
firstLine += " […]"
}
quoted, _ := d.connector.MsgConv.ConvertMatrixMessageContent(ctx, msg.Portal, &event.MessageEventContent{Body: firstLine}, nil)
author := target.Sender.String()
if member, err := d.connector.Bridge.Matrix.GetMemberInfo(ctx, msg.Portal.MXID, target.Sender); err == nil && member != nil && member.Displayname != "" {
author = member.Displayname
}
if targetID, ok := d.connector.Bridge.Matrix.ParseGhostMXID(target.Sender); ok {
author = "<@" + discordid.ParseUserID(targetID) + ">"
}
channelID := discordid.ParseChannelPortalID(msg.Portal.ID)
if threadID != "" {
channelID = threadID
}
url := fmt.Sprintf("https://discord.com/channels/%s/%s/%s", msg.Portal.Metadata.(*discordid.PortalMetadata).GuildID, channelID, discordid.ParseMessageID(msg.ReplyTo.ID))
return &discordgo.MessageEmbed{Description: fmt.Sprintf("**[Replying to](%s) %s**\n%s", url, author, quoted)}, nil
}
func (d *DiscordClient) webhookTarget(ctx context.Context, portal *bridgev2.Portal, target *database.Message) (discordid.PortalMetadata, string, error) {
meta := d.connector.relayMetadata(portal)
if target == nil || !d.connector.isWebhookMessage(portal, target) || meta.RelayWebhookSecret == "" {
return meta, "", fmt.Errorf("target message wasn't sent through this relay webhook")
}
threadID := ""
if target.ThreadRoot != "" {
thread, err := d.getThreadByRootMessageID(ctx, discordid.ParseMessageID(target.ThreadRoot))
if err != nil {
return meta, "", err
}
if thread == nil {
return meta, "", fmt.Errorf("couldn't find the target message's thread")
}
threadID = thread.ThreadChannelID
}
return meta, threadID, nil
}
func webhookRequestOptions(ctx context.Context, threadID string) []discordgo.RequestOption {
options := []discordgo.RequestOption{discordgo.WithContext(ctx)}
if threadID != "" {
options = append(options, discordgo.WithQueryParam("thread_id", threadID))
}
return options
}
func (d *DiscordClient) editWebhookMessage(ctx context.Context, msg *bridgev2.MatrixEdit) error {
meta, threadID, err := d.webhookTarget(ctx, msg.Portal, msg.EditTarget)
if err != nil {
return err
}
if msg.EditTarget.SenderMXID != msg.Event.Sender {
return fmt.Errorf("you can only edit your own relayed messages")
}
content, allowed := d.connector.MsgConv.ConvertMatrixMessageContent(ctx, msg.Portal, msg.Content, nil)
allowed = d.webhookAllowedMentions(ctx, msg.Portal, msg.Event.Sender, content, allowed)
_, err = d.connector.relayClient.WebhookMessageEdit(meta.RelayWebhookID, meta.RelayWebhookSecret, discordid.ParseMessageID(msg.EditTarget.ID), &discordgo.WebhookEdit{Content: &content, AllowedMentions: allowed}, webhookRequestOptions(ctx, threadID)...)
return webhookError(err, meta.RelayWebhookSecret)
}
func (d *DiscordClient) deleteWebhookMessage(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error {
meta, threadID, err := d.webhookTarget(ctx, msg.Portal, msg.TargetMessage)
if err != nil {
return err
}
if msg.TargetMessage.SenderMXID != msg.Event.Sender {
levels, err := d.connector.Bridge.Matrix.GetPowerLevels(ctx, msg.Portal.MXID)
if err != nil {
return err
}
if levels.GetUserLevel(msg.Event.Sender) < levels.Redact() {
return fmt.Errorf("you can only delete your own relayed messages")
}
}
err = d.connector.relayClient.WebhookMessageDelete(meta.RelayWebhookID, meta.RelayWebhookSecret, discordid.ParseMessageID(msg.TargetMessage.ID), webhookRequestOptions(ctx, threadID)...)
if isUnknownMessage(err) {
return nil
}
return webhookError(err, meta.RelayWebhookSecret)
}