From 466139164c7cd3d45f772355c8c9e23a5b897bd1 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Jan 2023 01:35:17 +0200 Subject: [PATCH] Merge emoji and discord_file tables Also fix duplicate reaction when reacting with custom emoji from Matrix --- attachments.go | 60 ++++++++--- database/database.go | 5 - database/emoji.go | 99 ------------------- database/file.go | 26 +++-- database/upgrades/00-latest-revision.sql | 27 ++--- .../13-merge-emoji-and-file.postgres.sql | 4 + .../13-merge-emoji-and-file.sqlite.sql | 24 +++++ emoji.go | 79 --------------- portal.go | 25 ++--- 9 files changed, 114 insertions(+), 235 deletions(-) delete mode 100644 database/emoji.go create mode 100644 database/upgrades/13-merge-emoji-and-file.postgres.sql create mode 100644 database/upgrades/13-merge-emoji-and-file.sqlite.sql delete mode 100644 emoji.go diff --git a/attachments.go b/attachments.go index 51255ab..1b0b5c7 100644 --- a/attachments.go +++ b/attachments.go @@ -16,6 +16,7 @@ import ( "maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/crypto/attachment" "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" "go.mau.fi/mautrix-discord/database" ) @@ -62,7 +63,7 @@ func uploadDiscordAttachment(url string, data []byte) error { return nil } -func (portal *Portal) downloadMatrixAttachment(content *event.MessageEventContent) ([]byte, error) { +func downloadMatrixAttachment(intent *appservice.IntentAPI, content *event.MessageEventContent) ([]byte, error) { var file *event.EncryptedFileInfo rawMXC := content.URL @@ -76,7 +77,7 @@ func (portal *Portal) downloadMatrixAttachment(content *event.MessageEventConten return nil, err } - data, err := portal.MainIntent().DownloadBytes(mxc) + data, err := intent.DownloadBytes(mxc) if err != nil { return nil, err } @@ -91,23 +92,24 @@ func (portal *Portal) downloadMatrixAttachment(content *event.MessageEventConten return data, nil } -func (br *DiscordBridge) uploadMatrixAttachment(intent *appservice.IntentAPI, data []byte, url string, encrypt bool, attachmentID, mime string) (*database.File, error) { +func (br *DiscordBridge) uploadMatrixAttachment(intent *appservice.IntentAPI, data []byte, url string, encrypt bool, meta AttachmentMeta) (*database.File, error) { dbFile := br.DB.File.New() dbFile.Timestamp = time.Now() dbFile.URL = url - dbFile.ID = attachmentID + dbFile.ID = meta.AttachmentID + dbFile.EmojiName = meta.EmojiName dbFile.Size = len(data) dbFile.MimeType = mimetype.Detect(data).String() - if mime == "" { - mime = dbFile.MimeType + if meta.MimeType == "" { + meta.MimeType = dbFile.MimeType } - if strings.HasPrefix(mime, "image/") { + if strings.HasPrefix(meta.MimeType, "image/") { cfg, _, _ := image.DecodeConfig(bytes.NewReader(data)) dbFile.Width = cfg.Width dbFile.Height = cfg.Height } - uploadMime := mime + uploadMime := meta.MimeType if encrypt { dbFile.Encrypted = true dbFile.DecryptionInfo = attachment.NewEncryptedFile() @@ -140,14 +142,16 @@ func (br *DiscordBridge) uploadMatrixAttachment(intent *appservice.IntentAPI, da } dbFile.MXC = uploaded.ContentURI } - // TODO add option to cache encrypted files too? - if !dbFile.Encrypted { - dbFile.Insert(nil) - } return dbFile, nil } -func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, url string, encrypt bool, attachmentID, mime string) (*database.File, error) { +type AttachmentMeta struct { + AttachmentID string + MimeType string + EmojiName string +} + +func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, url string, encrypt bool, meta *AttachmentMeta) (*database.File, error) { dbFile := br.DB.File.Get(url, encrypt) if dbFile == nil { data, err := downloadDiscordAttachment(url) @@ -155,10 +159,38 @@ func (br *DiscordBridge) copyAttachmentToMatrix(intent *appservice.IntentAPI, ur return nil, err } - dbFile, err = br.uploadMatrixAttachment(intent, data, url, encrypt, attachmentID, mime) + if meta == nil { + meta = &AttachmentMeta{} + } + dbFile, err = br.uploadMatrixAttachment(intent, data, url, encrypt, *meta) if err != nil { return nil, err } + // TODO add option to cache encrypted files too? + if !dbFile.Encrypted { + dbFile.Insert(nil) + } } return dbFile, nil } + +func (portal *Portal) getEmojiMXCByDiscordID(emojiID, name string, animated bool) id.ContentURI { + var url, mimeType string + if animated { + url = discordgo.EndpointEmojiAnimated(emojiID) + mimeType = "image/gif" + } else { + url = discordgo.EndpointEmoji(emojiID) + mimeType = "image/png" + } + dbFile, err := portal.bridge.copyAttachmentToMatrix(portal.MainIntent(), url, false, &AttachmentMeta{ + AttachmentID: emojiID, + MimeType: mimeType, + EmojiName: name, + }) + if err != nil { + portal.log.Warnfln("Failed to download emoji %s from discord: %v", emojiID, err) + return id.ContentURI{} + } + return dbFile.MXC +} diff --git a/database/database.go b/database/database.go index 080d5c6..00bd2ff 100644 --- a/database/database.go +++ b/database/database.go @@ -21,7 +21,6 @@ type Database struct { Message *MessageQuery Thread *ThreadQuery Reaction *ReactionQuery - Emoji *EmojiQuery Guild *GuildQuery Role *RoleQuery File *FileQuery @@ -54,10 +53,6 @@ func New(baseDB *dbutil.Database, log maulogger.Logger) *Database { db: db, log: log.Sub("Reaction"), } - db.Emoji = &EmojiQuery{ - db: db, - log: log.Sub("Emoji"), - } db.Guild = &GuildQuery{ db: db, log: log.Sub("Guild"), diff --git a/database/emoji.go b/database/emoji.go deleted file mode 100644 index 285b09f..0000000 --- a/database/emoji.go +++ /dev/null @@ -1,99 +0,0 @@ -package database - -import ( - "database/sql" - "errors" - - log "maunium.net/go/maulogger/v2" - - "maunium.net/go/mautrix/id" - "maunium.net/go/mautrix/util/dbutil" -) - -type EmojiQuery struct { - db *Database - log log.Logger -} - -const ( - emojiSelect = "SELECT discord_id, discord_name, matrix_url FROM emoji" -) - -func (eq *EmojiQuery) New() *Emoji { - return &Emoji{ - db: eq.db, - log: eq.log, - } -} - -func (eq *EmojiQuery) GetByDiscordID(discordID string) *Emoji { - query := emojiSelect + " WHERE discord_id=$1" - return eq.get(query, discordID) -} - -func (eq *EmojiQuery) GetByMatrixURL(matrixURL id.ContentURI) *Emoji { - query := emojiSelect + " WHERE matrix_url=$1" - return eq.get(query, matrixURL.String()) -} - -func (eq *EmojiQuery) get(query string, args ...interface{}) *Emoji { - return eq.New().Scan(eq.db.QueryRow(query, args...)) -} - -type Emoji struct { - db *Database - log log.Logger - - DiscordID string - DiscordName string - - MatrixURL id.ContentURI -} - -func (e *Emoji) Scan(row dbutil.Scannable) *Emoji { - var matrixURL sql.NullString - - err := row.Scan(&e.DiscordID, &e.DiscordName, &matrixURL) - if err != nil { - if !errors.Is(err, sql.ErrNoRows) { - e.log.Errorln("Database scan failed:", err) - panic(err) - } - return nil - } - - e.MatrixURL, _ = id.ParseContentURI(matrixURL.String) - return e -} - -func (e *Emoji) Insert() { - query := "INSERT INTO emoji" + - " (discord_id, discord_name, matrix_url)" + - " VALUES ($1, $2, $3);" - - _, err := e.db.Exec(query, e.DiscordID, e.DiscordName, e.MatrixURL.String()) - - if err != nil { - e.log.Warnfln("Failed to insert emoji %s: %v", e.DiscordID, err) - panic(err) - } -} - -func (e *Emoji) Delete() { - query := "DELETE FROM emoji WHERE discord_id=$1" - - _, err := e.db.Exec(query, e.DiscordID) - if err != nil { - e.log.Warnfln("Failed to delete emoji %s: %v", e.DiscordID, err) - panic(err) - } -} - -func (e *Emoji) APIName() string { - if e.DiscordID != "" && e.DiscordName != "" { - return e.DiscordName + ":" + e.DiscordID - } else if e.DiscordName != "" { - return e.DiscordName - } - return e.DiscordID -} diff --git a/database/file.go b/database/file.go index 4da8571..334915f 100644 --- a/database/file.go +++ b/database/file.go @@ -20,10 +20,10 @@ type FileQuery struct { // language=postgresql const ( - fileSelect = "SELECT url, encrypted, id, mxc, size, width, height, mime_type, decryption_info, timestamp FROM discord_file" + fileSelect = "SELECT url, encrypted, mxc, id, emoji_name, size, width, height, mime_type, decryption_info, timestamp FROM discord_file" fileInsert = ` - INSERT INTO discord_file (url, encrypted, id, mxc, size, width, height, mime_type, decryption_info, timestamp) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + INSERT INTO discord_file (url, encrypted, mxc, id, emoji_name, size, width, height, mime_type, decryption_info, timestamp) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ` ) @@ -39,15 +39,21 @@ func (fq *FileQuery) Get(url string, encrypted bool) *File { return fq.New().Scan(fq.db.QueryRow(query, url, encrypted)) } +func (fq *FileQuery) GetByMXC(mxc id.ContentURI) *File { + query := fileSelect + " WHERE mxc=$1" + return fq.New().Scan(fq.db.QueryRow(query, mxc.String())) +} + type File struct { db *Database log log.Logger URL string Encrypted bool + MXC id.ContentURI - ID string - MXC id.ContentURI + ID string + EmojiName string Size int Width int @@ -55,16 +61,15 @@ type File struct { MimeType string DecryptionInfo *attachment.EncryptedFile - - Timestamp time.Time + Timestamp time.Time } func (f *File) Scan(row dbutil.Scannable) *File { - var fileID, decryptionInfo sql.NullString + var fileID, emojiName, decryptionInfo sql.NullString var width, height sql.NullInt32 var timestamp int64 var mxc string - err := row.Scan(&f.URL, &f.Encrypted, &fileID, &mxc, &f.Size, &width, &height, &f.MimeType, &decryptionInfo, ×tamp) + err := row.Scan(&f.URL, &f.Encrypted, &mxc, &fileID, &emojiName, &f.Size, &width, &height, &f.MimeType, &decryptionInfo, ×tamp) if err != nil { if !errors.Is(err, sql.ErrNoRows) { f.log.Errorln("Database scan failed:", err) @@ -73,6 +78,7 @@ func (f *File) Scan(row dbutil.Scannable) *File { return nil } f.ID = fileID.String + f.EmojiName = emojiName.String f.Timestamp = time.UnixMilli(timestamp) f.Width = int(width.Int32) f.Height = int(height.Int32) @@ -114,7 +120,7 @@ func (f *File) Insert(txn dbutil.Execable) { decryptionInfoStr.String = string(decryptionInfo) } _, err := txn.Exec(fileInsert, - f.URL, f.Encrypted, strPtr(f.ID), f.MXC.String(), f.Size, + f.URL, f.Encrypted, f.MXC.String(), strPtr(f.ID), strPtr(f.EmojiName), f.Size, positiveIntToNullInt32(f.Width), positiveIntToNullInt32(f.Height), f.MimeType, decryptionInfoStr, f.Timestamp.UnixMilli(), ) diff --git a/database/upgrades/00-latest-revision.sql b/database/upgrades/00-latest-revision.sql index a04643a..bb80350 100644 --- a/database/upgrades/00-latest-revision.sql +++ b/database/upgrades/00-latest-revision.sql @@ -1,4 +1,4 @@ --- v0 -> v12: Latest revision +-- v0 -> v13: Latest revision CREATE TABLE guild ( dcid TEXT PRIMARY KEY, @@ -126,12 +126,6 @@ CREATE TABLE reaction ( CONSTRAINT reaction_message_fkey FOREIGN KEY (dc_msg_id, dc_first_attachment_id, _dc_first_edit_index, dc_chan_id, dc_chan_receiver) REFERENCES message (dcid, dc_attachment_id, dc_edit_index, dc_chan_id, dc_chan_receiver) ON DELETE CASCADE ); -CREATE TABLE emoji ( - discord_id TEXT PRIMARY KEY, - discord_name TEXT, - matrix_url TEXT -); - CREATE TABLE role ( dc_guild_id TEXT, dcid TEXT, @@ -151,21 +145,20 @@ CREATE TABLE role ( CONSTRAINT role_guild_fkey FOREIGN KEY (dc_guild_id) REFERENCES guild (dcid) ON DELETE CASCADE ); -CREATE TABLE discord_file ( +CREATE TABLE new_discord_file ( url TEXT, encrypted BOOLEAN, + mxc TEXT NOT NULL UNIQUE, - id TEXT, - mxc TEXT NOT NULL, - - size BIGINT NOT NULL, - width INTEGER, - height INTEGER, - mime_type TEXT NOT NULL, + id TEXT, + emoji_name TEXT, + size BIGINT NOT NULL, + width INTEGER, + height INTEGER, + mime_type TEXT NOT NULL, decryption_info jsonb, - - timestamp BIGINT NOT NULL, + timestamp BIGINT NOT NULL, PRIMARY KEY (url, encrypted) ); diff --git a/database/upgrades/13-merge-emoji-and-file.postgres.sql b/database/upgrades/13-merge-emoji-and-file.postgres.sql new file mode 100644 index 0000000..18ef607 --- /dev/null +++ b/database/upgrades/13-merge-emoji-and-file.postgres.sql @@ -0,0 +1,4 @@ +-- v13: Merge tables used for cached custom emojis and attachments +ALTER TABLE discord_file ADD CONSTRAINT mxc_unique UNIQUE (mxc); +ALTER TABLE discord_file ADD COLUMN emoji_name TEXT; +DROP TABLE emoji; diff --git a/database/upgrades/13-merge-emoji-and-file.sqlite.sql b/database/upgrades/13-merge-emoji-and-file.sqlite.sql new file mode 100644 index 0000000..ffe1b25 --- /dev/null +++ b/database/upgrades/13-merge-emoji-and-file.sqlite.sql @@ -0,0 +1,24 @@ +-- v13: Merge tables used for cached custom emojis and attachments +CREATE TABLE new_discord_file ( + url TEXT, + encrypted BOOLEAN, + mxc TEXT NOT NULL UNIQUE, + + id TEXT, + emoji_name TEXT, + + size BIGINT NOT NULL, + width INTEGER, + height INTEGER, + mime_type TEXT NOT NULL, + decryption_info jsonb, + timestamp BIGINT NOT NULL, + + PRIMARY KEY (url, encrypted) +); + +INSERT INTO new_discord_file (url, encrypted, id, mxc, size, width, height, mime_type, decryption_info, timestamp) +SELECT url, encrypted, id, mxc, size, width, height, mime_type, decryption_info, timestamp FROM discord_file; + +DROP TABLE discord_file; +ALTER TABLE new_discord_file RENAME TO discord_file; diff --git a/emoji.go b/emoji.go deleted file mode 100644 index 2365926..0000000 --- a/emoji.go +++ /dev/null @@ -1,79 +0,0 @@ -package main - -import ( - "io/ioutil" - "net/http" - - "github.com/bwmarrin/discordgo" - - "maunium.net/go/mautrix/appservice" - "maunium.net/go/mautrix/id" -) - -func (portal *Portal) getEmojiMXCByDiscordID(emojiID, name string, animated bool) id.ContentURI { - dbEmoji := portal.bridge.DB.Emoji.GetByDiscordID(emojiID) - - if dbEmoji == nil { - data, mimeType, err := portal.downloadDiscordEmoji(emojiID, animated) - if err != nil { - portal.log.Warnfln("Failed to download emoji %s from discord: %v", emojiID, err) - return id.ContentURI{} - } - - uri, err := portal.uploadMatrixEmoji(portal.MainIntent(), data, mimeType) - if err != nil { - portal.log.Warnfln("Failed to upload discord emoji %s to homeserver: %v", emojiID, err) - return id.ContentURI{} - } - - dbEmoji = portal.bridge.DB.Emoji.New() - dbEmoji.DiscordID = emojiID - dbEmoji.DiscordName = name - dbEmoji.MatrixURL = uri - dbEmoji.Insert() - } - - return dbEmoji.MatrixURL -} - -func (portal *Portal) downloadDiscordEmoji(id string, animated bool) ([]byte, string, error) { - var url string - var mimeType string - - if animated { - // This url requests a gif, so that's what we set the mimetype to. - url = discordgo.EndpointEmojiAnimated(id) - mimeType = "image/gif" - } else { - // This url requests a png, so that's what we set the mimetype to. - url = discordgo.EndpointEmoji(id) - mimeType = "image/png" - } - - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - return nil, mimeType, err - } - - req.Header.Set("User-Agent", discordgo.DroidBrowserUserAgent) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, mimeType, err - } - - defer resp.Body.Close() - - data, err := ioutil.ReadAll(resp.Body) - - return data, mimeType, err -} - -func (portal *Portal) uploadMatrixEmoji(intent *appservice.IntentAPI, data []byte, mimeType string) (id.ContentURI, error) { - uploaded, err := intent.UploadBytes(data, mimeType) - if err != nil { - return id.ContentURI{}, err - } - - return uploaded.ContentURI, nil -} diff --git a/portal.go b/portal.go index 3112447..66071be 100644 --- a/portal.go +++ b/portal.go @@ -555,7 +555,7 @@ func (portal *Portal) sendMediaFailedMessage(intent *appservice.IntentAPI, bridg 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 { - dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, url, portal.Encrypted, id, content.Info.MimeType) + dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, url, portal.Encrypted, &AttachmentMeta{AttachmentID: id, MimeType: content.Info.MimeType}) if err != nil { errorEventID := portal.sendMediaFailedMessage(intent, err) if errorEventID != "" { @@ -675,7 +675,7 @@ type ConvertedMessage struct { } func (portal *Portal) convertDiscordVideoEmbed(intent *appservice.IntentAPI, embed *discordgo.MessageEmbed) *ConvertedMessage { - dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Video.ProxyURL, portal.Encrypted, "", "") + dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Video.ProxyURL, portal.Encrypted, nil) if err != nil { return &ConvertedMessage{Content: portal.createMediaFailedMessage(err)} } @@ -768,7 +768,7 @@ func (portal *Portal) convertDiscordRichEmbed(intent *appservice.IntentAPI, embe } authorHTML = fmt.Sprintf(embedHTMLAuthorPlain, authorNameHTML) if embed.Author.ProxyIconURL != "" { - dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Author.ProxyIconURL, false, "", "") + dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Author.ProxyIconURL, false, nil) if err != nil { portal.log.Warnfln("Failed to reupload author icon in embed #%d of message %s: %v", index+1, msgID, err) } else { @@ -818,7 +818,7 @@ func (portal *Portal) convertDiscordRichEmbed(intent *appservice.IntentAPI, embe } } if embed.Image != nil { - dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Image.ProxyURL, false, "", "") + dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Image.ProxyURL, false, nil) if err != nil { portal.log.Warnfln("Failed to reupload image in embed #%d of message %s: %v", index+1, msgID, err) } else { @@ -844,7 +844,7 @@ func (portal *Portal) convertDiscordRichEmbed(intent *appservice.IntentAPI, embe } footerHTML = fmt.Sprintf(embedHTMLFooterPlain, html.EscapeString(embed.Footer.Text), datePart) if embed.Footer.ProxyIconURL != "" { - dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Footer.ProxyIconURL, false, "", "") + dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, embed.Footer.ProxyIconURL, false, nil) if err != nil { portal.log.Warnfln("Failed to reupload footer icon in embed #%d of message %s: %v", index+1, msgID, err) } else { @@ -876,7 +876,7 @@ type BeeperLinkPreview struct { } func (portal *Portal) convertDiscordLinkEmbedImage(intent *appservice.IntentAPI, url string, width, height int, preview *BeeperLinkPreview) { - dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, url, portal.Encrypted, "", "") + dbFile, err := portal.bridge.copyAttachmentToMatrix(intent, url, portal.Encrypted, nil) if err != nil { portal.log.Warnfln("Failed to copy image in URL preview: %v", err) } else { @@ -1576,7 +1576,7 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) { } sendReq.Content = portal.parseMatrixHTML(sender, content) case event.MsgAudio, event.MsgFile, event.MsgImage, event.MsgVideo: - data, err := portal.downloadMatrixAttachment(content) + data, err := downloadMatrixAttachment(portal.MainIntent(), content) if err != nil { go portal.sendMessageMetrics(evt, err, "Error downloading media in") return @@ -1819,17 +1819,20 @@ func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) { // Figure out if this is a custom emoji or not. emojiID := reaction.RelatesTo.Key + requestEmojiID := emojiID if strings.HasPrefix(emojiID, "mxc://") { uri, _ := id.ParseContentURI(emojiID) - emoji := portal.bridge.DB.Emoji.GetByMatrixURL(uri) - if emoji == nil { + emojiFile := portal.bridge.DB.File.GetByMXC(uri) + if emojiFile == nil || emojiFile.ID == "" || emojiFile.EmojiName == "" { go portal.sendMessageMetrics(evt, fmt.Errorf("%w %s", errUnknownEmoji, emojiID), "Ignoring") return } - emojiID = emoji.APIName() + emojiID = emojiFile.ID + requestEmojiID = fmt.Sprintf("%s:%s", emojiFile.EmojiName, emojiFile.ID) } else { emojiID = variationselector.Remove(emojiID) + requestEmojiID = emojiID } existing := portal.bridge.DB.Reaction.GetByDiscordID(portal.Key, msg.DiscordID, sender.DiscordID, emojiID) @@ -1839,7 +1842,7 @@ func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) { return } - err := sender.Session.MessageReactionAdd(msg.DiscordProtoChannelID(), msg.DiscordID, emojiID) + err := sender.Session.MessageReactionAdd(msg.DiscordProtoChannelID(), msg.DiscordID, requestEmojiID) go portal.sendMessageMetrics(evt, err, "Error sending") if err == nil { dbReaction := portal.bridge.DB.Reaction.New()