1
0
Fork 0
mirror of https://github.com/mautrix/discord.git synced 2026-07-06 22:33:18 -04:00
mautrix-discord/pkg/connector/directmedia.go

205 lines
6.3 KiB
Go
Raw Permalink Normal View History

2026-03-19 23:33:14 -07:00
// 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"
"encoding/binary"
"encoding/hex"
"fmt"
"net/url"
"time"
"github.com/bwmarrin/discordgo"
"github.com/rs/zerolog"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/mediaproxy"
"go.mau.fi/mautrix-discord/pkg/discordid"
)
var (
_ bridgev2.DirectMediableNetwork = (*DiscordConnector)(nil)
)
func (d *DiscordConnector) Download(
2026-03-19 23:33:14 -07:00
ctx context.Context,
mediaID networkid.MediaID,
params map[string]string,
) (mediaproxy.GetMediaResponse, error) {
info, err := discordid.ParseMediaID(mediaID)
if err != nil {
return nil, fmt.Errorf("failed to parse media id for download: %w", err)
}
return d.downloadAttachment(ctx, info)
2026-03-19 23:33:14 -07:00
}
func (d *DiscordConnector) SetUseDirectMedia() {
d.MsgConv.DirectMedia = true
2026-03-19 23:33:14 -07:00
}
func (d *DiscordConnector) downloadAttachment(
2026-03-19 23:33:14 -07:00
ctx context.Context,
info *discordid.MediaInfo,
) (*mediaproxy.GetMediaResponseURL, error) {
url, expiresAt, err := d.resolveAttachmentURL(ctx, info)
2026-03-19 23:33:14 -07:00
if err != nil {
return nil, fmt.Errorf("failed to refresh attachment url for download: %w", err)
}
if expiresAt.IsZero() {
// A zero expiry becomes effectively immutable caching in mediaproxy.
// Unknown expiry is safer as no-store for now.
expiresAt = time.Now()
}
return &mediaproxy.GetMediaResponseURL{
URL: url,
ExpiresAt: expiresAt,
}, nil
}
func (d *DiscordConnector) resolveAttachmentURL(ctx context.Context, info *discordid.MediaInfo) (url string, expires time.Time, err error) {
if entry, ok := d.attachmentCache.Get(info.MediaInfoV1); ok {
return entry.URL, entry.Expiry, nil
}
url, expiresAt, err := d.refreshAttachmentURL(ctx, info)
if err != nil {
return "", time.Time{}, err
}
d.attachmentCache.Insert(info, url)
return url, expiresAt, nil
}
func (d *DiscordConnector) refreshAttachmentURL(
2026-03-19 23:33:14 -07:00
ctx context.Context,
info *discordid.MediaInfo,
) (url string, expires time.Time, err error) {
log := zerolog.Ctx(ctx).With().Str("action", "refresh attachment url").Logger()
ctx = log.WithContext(ctx)
login, err := d.Bridge.GetExistingUserLoginByID(ctx, info.UserLoginID)
2026-03-19 23:33:14 -07:00
if err != nil {
return "", time.Time{}, err
} else if login == nil {
return "", time.Time{}, mautrix.MNotFound.WithMessage("Direct media login not found")
}
client, ok := login.Client.(*DiscordClient)
if !ok || client == nil || !client.IsLoggedIn() {
return "", time.Time{}, mautrix.MNotFound.WithMessage("Direct media login is not connected")
}
channelID := info.ChannelID
messageID := info.MessageID
attachmentID := info.AttachmentID
parentChannelID := channelID
threadChannelID := ""
threadInfo, err := d.DB.Thread.GetByThreadChannelID(ctx, string(info.UserLoginID), channelID)
2026-03-19 23:33:14 -07:00
if err != nil {
return "", time.Time{}, fmt.Errorf("failed to query thread info: %w", err)
} else if threadInfo != nil {
parentChannelID = threadInfo.ParentChannelID
threadChannelID = threadInfo.ThreadChannelID
}
var requestOptions []discordgo.RequestOption
portalKey := discordid.MakeChannelPortalKey(parentChannelID, info.UserLoginID, d.Bridge.Config.SplitPortals)
portal, err := d.Bridge.GetExistingPortalByKey(ctx, portalKey)
2026-03-19 23:33:14 -07:00
if err != nil {
return "", time.Time{}, fmt.Errorf("failed to query portal for direct media: %w", err)
} else if portal != nil {
if meta, ok := portal.Metadata.(*discordid.PortalMetadata); ok {
requestOptions = append(requestOptions, makeDiscordReferer(meta.GuildID, parentChannelID, threadChannelID))
}
} else if threadChannelID == "" {
// DMs still benefit from @me referers.
requestOptions = append(requestOptions, makeDiscordReferer("", parentChannelID, ""))
}
var messages []*discordgo.Message
if client.Session.IsUser {
messages, err = client.Session.ChannelMessages(channelID, 5, "", "", messageID, requestOptions...)
} else {
var msg *discordgo.Message
msg, err = client.Session.ChannelMessage(channelID, messageID, requestOptions...)
if err == nil && msg != nil {
messages = []*discordgo.Message{msg}
}
}
if err != nil {
return "", time.Time{}, fmt.Errorf("failed to fetch direct media message: %w", err)
}
for _, msg := range messages {
for _, att := range msg.Attachments {
if att.ID == attachmentID {
expiresAt := normalizeAttachmentExpiry(parseAttachmentExpiryFromURL(att.URL))
2026-03-19 23:33:14 -07:00
// (Trace is not the default log level, so this is only visible
// in development scenarios.)
log.Trace().
Str("channel_id", channelID).
Str("message_id", messageID).
Str("attachment_id", attachmentID).
Time("expires_at", expiresAt).
Msg("Resolved direct media attachment URL")
// TODO(skip): This is ignoring the rest of the attachments.
2026-03-19 23:33:14 -07:00
return att.URL, expiresAt, nil
}
}
}
return "", time.Time{}, mautrix.MNotFound.WithMessage("Attachment not found in message")
}
func parseAttachmentExpiryParam(ex string) time.Time {
tsBytes, err := hex.DecodeString(ex)
if err != nil || len(tsBytes) != 4 {
return time.Time{}
}
parsedTS := int64(binary.BigEndian.Uint32(tsBytes))
now := time.Now()
expiry := time.Unix(parsedTS, 0)
if expiry.Before(now) || expiry.After(now.Add(365*24*time.Hour)) {
// Looks to be invalid.
return time.Time{}
}
return expiry
}
func parseAttachmentExpiryFromURL(rawURL string) time.Time {
parsedURL, err := url.Parse(rawURL)
if err != nil {
return time.Time{}
}
return parseAttachmentExpiryParam(parsedURL.Query().Get("ex"))
}
func normalizeAttachmentExpiry(expiry time.Time) time.Time {
// Default to a validity period of 24 hours.
if expiry.IsZero() {
return time.Now().Add(24 * time.Hour)
}
return expiry
}