Cache files copied to Matrix
This commit is contained in:
@@ -7,18 +7,19 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"maunium.net/go/mautrix/crypto/attachment"
|
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
"maunium.net/go/mautrix/appservice"
|
"maunium.net/go/mautrix/appservice"
|
||||||
|
"maunium.net/go/mautrix/crypto/attachment"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
"go.mau.fi/mautrix-discord/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (portal *Portal) downloadDiscordAttachment(url string) ([]byte, error) {
|
func downloadDiscordAttachment(url string) ([]byte, error) {
|
||||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -68,48 +69,67 @@ func (portal *Portal) downloadMatrixAttachment(content *event.MessageEventConten
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) uploadMatrixAttachment(intent *appservice.IntentAPI, data []byte, content *event.MessageEventContent) error {
|
func (br *DiscordBridge) uploadMatrixAttachment(intent *appservice.IntentAPI, data []byte, url string, encrypt bool, attachmentID, mime string) (*database.File, error) {
|
||||||
content.Info.Size = len(data)
|
dbFile := br.DB.File.New()
|
||||||
if content.Info.Width == 0 && content.Info.Height == 0 && strings.HasPrefix(content.Info.MimeType, "image/") {
|
dbFile.Timestamp = time.Now()
|
||||||
|
dbFile.URL = url
|
||||||
|
dbFile.ID = attachmentID
|
||||||
|
dbFile.Size = len(data)
|
||||||
|
if strings.HasPrefix(mime, "image/") {
|
||||||
cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
|
cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
|
||||||
content.Info.Width = cfg.Width
|
dbFile.Width = cfg.Width
|
||||||
content.Info.Height = cfg.Height
|
dbFile.Height = cfg.Height
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadMime := content.Info.MimeType
|
uploadMime := mime
|
||||||
var file *attachment.EncryptedFile
|
if encrypt {
|
||||||
if portal.Encrypted {
|
dbFile.Encrypted = true
|
||||||
file = attachment.NewEncryptedFile()
|
dbFile.DecryptionInfo = attachment.NewEncryptedFile()
|
||||||
file.EncryptInPlace(data)
|
dbFile.DecryptionInfo.EncryptInPlace(data)
|
||||||
uploadMime = "application/octet-stream"
|
uploadMime = "application/octet-stream"
|
||||||
}
|
}
|
||||||
req := mautrix.ReqUploadMedia{
|
req := mautrix.ReqUploadMedia{
|
||||||
ContentBytes: data,
|
ContentBytes: data,
|
||||||
ContentType: uploadMime,
|
ContentType: uploadMime,
|
||||||
}
|
}
|
||||||
var mxc id.ContentURI
|
if br.Config.Homeserver.AsyncMedia {
|
||||||
if portal.bridge.Config.Homeserver.AsyncMedia {
|
resp, err := intent.UnstableCreateMXC()
|
||||||
uploaded, err := intent.UnstableUploadAsync(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
mxc = uploaded.ContentURI
|
dbFile.MXC = resp.ContentURI
|
||||||
|
req.UnstableMXC = resp.ContentURI
|
||||||
|
req.UploadURL = resp.UploadURL
|
||||||
|
go func() {
|
||||||
|
_, err = intent.UploadMedia(req)
|
||||||
|
if err != nil {
|
||||||
|
br.Log.Errorfln("Failed to upload %s: %v", req.UnstableMXC, err)
|
||||||
|
dbFile.Delete()
|
||||||
|
}
|
||||||
|
}()
|
||||||
} else {
|
} else {
|
||||||
uploaded, err := intent.UploadMedia(req)
|
uploaded, err := intent.UploadMedia(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
mxc = uploaded.ContentURI
|
dbFile.MXC = uploaded.ContentURI
|
||||||
}
|
}
|
||||||
|
dbFile.Insert(nil)
|
||||||
if file != nil {
|
return dbFile, nil
|
||||||
content.File = &event.EncryptedFileInfo{
|
}
|
||||||
EncryptedFile: *file,
|
|
||||||
URL: mxc.CUString(),
|
func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, url string, encrypt bool, attachmentID, mime string) (*database.File, error) {
|
||||||
}
|
dbFile := br.DB.File.Get(url, encrypt)
|
||||||
} else {
|
if dbFile == nil {
|
||||||
content.URL = mxc.CUString()
|
data, err := downloadDiscordAttachment(url)
|
||||||
}
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
return nil
|
}
|
||||||
|
|
||||||
|
dbFile, err = br.uploadMatrixAttachment(intent, data, url, encrypt, attachmentID, mime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dbFile, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type Database struct {
|
|||||||
Emoji *EmojiQuery
|
Emoji *EmojiQuery
|
||||||
Guild *GuildQuery
|
Guild *GuildQuery
|
||||||
Role *RoleQuery
|
Role *RoleQuery
|
||||||
|
File *FileQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(baseDB *dbutil.Database, log maulogger.Logger) *Database {
|
func New(baseDB *dbutil.Database, log maulogger.Logger) *Database {
|
||||||
@@ -65,6 +66,10 @@ func New(baseDB *dbutil.Database, log maulogger.Logger) *Database {
|
|||||||
db: db,
|
db: db,
|
||||||
log: log.Sub("Role"),
|
log: log.Sub("Role"),
|
||||||
}
|
}
|
||||||
|
db.File = &FileQuery{
|
||||||
|
db: db,
|
||||||
|
log: log.Sub("File"),
|
||||||
|
}
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
132
database/file.go
Normal file
132
database/file.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "maunium.net/go/maulogger/v2"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/crypto/attachment"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileQuery struct {
|
||||||
|
db *Database
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// language=postgresql
|
||||||
|
const (
|
||||||
|
fileSelect = "SELECT url, encrypted, id, mxc, size, width, height, decryption_info, timestamp FROM discord_file"
|
||||||
|
fileInsert = `
|
||||||
|
INSERT INTO discord_file (url, encrypted, id, mxc, size, width, height, decryption_info, timestamp)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
func (fq *FileQuery) New() *File {
|
||||||
|
return &File{
|
||||||
|
db: fq.db,
|
||||||
|
log: fq.log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fq *FileQuery) Get(url string, encrypted bool) *File {
|
||||||
|
query := fileSelect + " WHERE url=$1 AND encrypted=$2"
|
||||||
|
return fq.New().Scan(fq.db.QueryRow(query, url, encrypted))
|
||||||
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
db *Database
|
||||||
|
log log.Logger
|
||||||
|
|
||||||
|
URL string
|
||||||
|
Encrypted bool
|
||||||
|
|
||||||
|
ID string
|
||||||
|
MXC id.ContentURI
|
||||||
|
|
||||||
|
Size int
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
|
||||||
|
DecryptionInfo *attachment.EncryptedFile
|
||||||
|
|
||||||
|
Timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Scan(row dbutil.Scannable) *File {
|
||||||
|
var fileID sql.NullString
|
||||||
|
var decryptionInfo []byte
|
||||||
|
var width, height sql.NullInt32
|
||||||
|
var timestamp int64
|
||||||
|
var mxc string
|
||||||
|
err := row.Scan(&f.URL, &f.Encrypted, &fileID, &mxc, &f.Size, &width, &height, &decryptionInfo, ×tamp)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
f.log.Errorln("Database scan failed:", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
f.ID = fileID.String
|
||||||
|
f.Timestamp = time.UnixMilli(timestamp)
|
||||||
|
f.Width = int(width.Int32)
|
||||||
|
f.Height = int(height.Int32)
|
||||||
|
f.MXC, err = id.ParseContentURI(mxc)
|
||||||
|
if err != nil {
|
||||||
|
f.log.Errorfln("Failed to parse content URI %s: %v", mxc, err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if decryptionInfo != nil {
|
||||||
|
err = json.Unmarshal(decryptionInfo, &f.DecryptionInfo)
|
||||||
|
if err != nil {
|
||||||
|
f.log.Errorfln("Failed to unmarshal decryption info of %v: %v", f.MXC, err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func positiveIntToNullInt32(val int) (ptr sql.NullInt32) {
|
||||||
|
if val > 0 {
|
||||||
|
ptr.Valid = true
|
||||||
|
ptr.Int32 = int32(val)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Insert(txn dbutil.Execable) {
|
||||||
|
if txn == nil {
|
||||||
|
txn = f.db
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
var decryptionInfo []byte
|
||||||
|
if f.DecryptionInfo != nil {
|
||||||
|
decryptionInfo, err = json.Marshal(f.DecryptionInfo)
|
||||||
|
if err != nil {
|
||||||
|
f.log.Warnfln("Failed to marshal decryption info of %v: %v", f.MXC, err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = txn.Exec(fileInsert,
|
||||||
|
f.URL, f.Encrypted, strPtr(f.ID), f.MXC.String(), f.Size,
|
||||||
|
positiveIntToNullInt32(f.Width), positiveIntToNullInt32(f.Height),
|
||||||
|
decryptionInfo, f.Timestamp.UnixMilli(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
f.log.Warnfln("Failed to insert copied file %v: %v", f.MXC, err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Delete() {
|
||||||
|
_, err := f.db.Exec("DELETE FROM discord_file WHERE url=$1 AND encrypted=$2", f.URL, f.Encrypted)
|
||||||
|
if err != nil {
|
||||||
|
f.log.Warnfln("Failed to delete copied file %v: %v", f.MXC, err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
-- v0 -> v10: Latest revision
|
-- v0 -> v11: Latest revision
|
||||||
|
|
||||||
CREATE TABLE guild (
|
CREATE TABLE guild (
|
||||||
dcid TEXT PRIMARY KEY,
|
dcid TEXT PRIMARY KEY,
|
||||||
@@ -150,3 +150,21 @@ CREATE TABLE role (
|
|||||||
PRIMARY KEY (dc_guild_id, dcid),
|
PRIMARY KEY (dc_guild_id, dcid),
|
||||||
CONSTRAINT role_guild_fkey FOREIGN KEY (dc_guild_id) REFERENCES guild (dcid) ON DELETE CASCADE
|
CONSTRAINT role_guild_fkey FOREIGN KEY (dc_guild_id) REFERENCES guild (dcid) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE discord_file (
|
||||||
|
url TEXT,
|
||||||
|
encrypted BOOLEAN,
|
||||||
|
|
||||||
|
id TEXT,
|
||||||
|
mxc TEXT NOT NULL,
|
||||||
|
|
||||||
|
size BIGINT NOT NULL,
|
||||||
|
width INTEGER,
|
||||||
|
height INTEGER,
|
||||||
|
|
||||||
|
decryption_info jsonb,
|
||||||
|
|
||||||
|
timestamp BIGINT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (url, encrypted)
|
||||||
|
);
|
||||||
|
|||||||
18
database/upgrades/11-cache-reuploaded-files.sql
Normal file
18
database/upgrades/11-cache-reuploaded-files.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
-- v11: Cache files copied from Discord to Matrix
|
||||||
|
CREATE TABLE discord_file (
|
||||||
|
url TEXT,
|
||||||
|
encrypted BOOLEAN,
|
||||||
|
|
||||||
|
id TEXT,
|
||||||
|
mxc TEXT NOT NULL,
|
||||||
|
|
||||||
|
size BIGINT NOT NULL,
|
||||||
|
width INTEGER,
|
||||||
|
height INTEGER,
|
||||||
|
|
||||||
|
decryption_info jsonb,
|
||||||
|
|
||||||
|
timestamp BIGINT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (url, encrypted)
|
||||||
|
);
|
||||||
33
portal.go
33
portal.go
@@ -523,31 +523,46 @@ func (portal *Portal) markMessageHandled(discordID string, editIndex int, author
|
|||||||
msg.MassInsert(parts)
|
msg.MassInsert(parts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (portal *Portal) sendMediaFailedMessage(intent *appservice.IntentAPI, bridgeErr error) {
|
func (portal *Portal) sendMediaFailedMessage(intent *appservice.IntentAPI, bridgeErr error) id.EventID {
|
||||||
content := &event.MessageEventContent{
|
content := &event.MessageEventContent{
|
||||||
Body: fmt.Sprintf("Failed to bridge media: %v", bridgeErr),
|
Body: fmt.Sprintf("Failed to bridge media: %v", bridgeErr),
|
||||||
MsgType: event.MsgNotice,
|
MsgType: event.MsgNotice,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := portal.sendMatrixMessage(intent, event.EventMessage, content, nil, 0)
|
resp, err := portal.sendMatrixMessage(intent, event.EventMessage, content, nil, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
portal.log.Warnfln("Failed to send media error message to matrix: %v", err)
|
portal.log.Warnfln("Failed to send media error message to matrix: %v", err)
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
return resp.EventID
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiscordStickerSize = 160
|
const DiscordStickerSize = 160
|
||||||
|
|
||||||
func (portal *Portal) handleDiscordFile(typeName string, intent *appservice.IntentAPI, id, url string, content *event.MessageEventContent, ts time.Time, threadRelation *event.RelatesTo) *database.MessagePart {
|
func (portal *Portal) handleDiscordFile(typeName string, intent *appservice.IntentAPI, id, url string, content *event.MessageEventContent, ts time.Time, threadRelation *event.RelatesTo) *database.MessagePart {
|
||||||
data, err := portal.downloadDiscordAttachment(url)
|
dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, url, portal.Encrypted, id, content.Info.MimeType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
portal.sendMediaFailedMessage(intent, err)
|
errorEventID := portal.sendMediaFailedMessage(intent, err)
|
||||||
|
if errorEventID != "" {
|
||||||
|
return &database.MessagePart{
|
||||||
|
AttachmentID: id,
|
||||||
|
MXID: errorEventID,
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
content.Info.Size = dbFile.Size
|
||||||
err = portal.uploadMatrixAttachment(intent, data, content)
|
if content.Info.Width == 0 && content.Info.Height == 0 {
|
||||||
if err != nil {
|
content.Info.Width = dbFile.Width
|
||||||
portal.sendMediaFailedMessage(intent, err)
|
content.Info.Height = dbFile.Height
|
||||||
return nil
|
}
|
||||||
|
if dbFile.DecryptionInfo != nil {
|
||||||
|
content.File = &event.EncryptedFileInfo{
|
||||||
|
EncryptedFile: *dbFile.DecryptionInfo,
|
||||||
|
URL: dbFile.MXC.CUString(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content.URL = dbFile.MXC.CUString()
|
||||||
}
|
}
|
||||||
|
|
||||||
evtType := event.EventMessage
|
evtType := event.EventMessage
|
||||||
|
|||||||
Reference in New Issue
Block a user