Files
mautrix-discord/pkg/msgconv/attachments.go
2026-01-28 17:21:19 +02:00

148 lines
4.0 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 msgconv
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"github.com/bwmarrin/discordgo"
"github.com/rs/zerolog"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
type ReuploadedAttachment struct {
MXC id.ContentURIString
File *event.EncryptedFileInfo
Size int
FileName string
MimeType string
}
func (d *MessageConverter) ReuploadUnknownMedia(
ctx context.Context,
url string,
allowEncryption bool,
) (*ReuploadedAttachment, error) {
return d.ReuploadMedia(ctx, url, "", "", -1, allowEncryption)
}
func mib(size int64) float64 {
return float64(size) / 1024 / 1024
}
func (d *MessageConverter) ReuploadMedia(
ctx context.Context,
downloadURL string,
mimeType string,
fileName string,
estimatedSize int,
allowEncryption bool,
) (*ReuploadedAttachment, error) {
if fileName == "" {
parsedURL, err := url.Parse(downloadURL)
if err != nil {
return nil, fmt.Errorf("couldn't parse URL to detect file name: %w", err)
}
fileName = path.Base(parsedURL.Path)
}
sess := ctx.Value(contextKeyDiscordClient).(*discordgo.Session)
httpClient := sess.Client
intent := ctx.Value(contextKeyIntent).(bridgev2.MatrixAPI)
var roomID id.RoomID
if allowEncryption {
roomID = ctx.Value(contextKeyPortal).(*bridgev2.Portal).MXID
}
req, err := http.NewRequest(http.MethodGet, downloadURL, nil)
if err != nil {
return nil, err
}
if sess.IsUser {
for key, value := range discordgo.DroidDownloadHeaders {
req.Header.Set(key, value)
}
}
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode > 300 {
errBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
logEvt := zerolog.Ctx(ctx).Error().
Str("media_url", downloadURL).
Int("status_code", resp.StatusCode)
if json.Valid(errBody) {
logEvt.RawJSON("error_json", errBody)
} else {
logEvt.Bytes("error_body", errBody)
}
logEvt.Msg("Media download failed")
return nil, fmt.Errorf("%w: unexpected status code %d", bridgev2.ErrMediaDownloadFailed, resp.StatusCode)
} else if resp.ContentLength > d.MaxFileSize {
return nil, fmt.Errorf("%w (%.2f MiB > %.2f MiB)", bridgev2.ErrMediaTooLarge, mib(resp.ContentLength), mib(d.MaxFileSize))
}
requireFile := mimeType == ""
var size int64
mxc, file, err := intent.UploadMediaStream(ctx, roomID, int64(estimatedSize), requireFile, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
var mbe *http.MaxBytesError
size, err = io.Copy(file, http.MaxBytesReader(nil, resp.Body, d.MaxFileSize))
if err != nil {
if errors.As(err, &mbe) {
return nil, fmt.Errorf("%w (over %.2f MiB)", bridgev2.ErrMediaTooLarge, mib(d.MaxFileSize))
}
return nil, err
}
if mimeType == "" {
mimeBuf := make([]byte, 512)
n, err := file.(*os.File).ReadAt(mimeBuf, 0)
if err != nil {
return nil, fmt.Errorf("couldn't read file for mime detection: %w", err)
}
mimeType = http.DetectContentType(mimeBuf[:n])
}
return &bridgev2.FileStreamResult{
FileName: fileName,
MimeType: mimeType,
}, nil
})
if err != nil {
return nil, err
}
return &ReuploadedAttachment{
Size: int(size),
MXC: mxc,
File: file,
FileName: fileName,
MimeType: mimeType,
}, nil
}