Browse Source

Merge emoji and discord_file tables

Also fix duplicate reaction when reacting with custom emoji from Matrix
Tulir Asokan 2 years ago
parent
commit
466139164c

+ 46 - 14
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
+}

+ 0 - 5
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"),

+ 0 - 99
database/emoji.go

@@ -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
-}

+ 16 - 10
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, &timestamp)
+	err := row.Scan(&f.URL, &f.Encrypted, &mxc, &fileID, &emojiName, &f.Size, &width, &height, &f.MimeType, &decryptionInfo, &timestamp)
 	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(),
 	)

+ 10 - 17
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)
 );

+ 4 - 0
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;

+ 24 - 0
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;

+ 0 - 79
emoji.go

@@ -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
-}

+ 14 - 11
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()