From a51b1074baaad60674ae41d98ba05c6b96358e80 Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Sat, 19 Feb 2022 10:14:43 -0600 Subject: [PATCH] Add support for custom emoji in reactions. This seems to be working correctly, but element-desktop isn't rendering them, not sure if that's expected or not. Closes #4 --- bridge/emoji.go | 53 +++++++++++++++++++++++ bridge/portal.go | 69 +++++++++++++++++++++++------- database/database.go | 6 +++ database/emoji.go | 70 +++++++++++++++++++++++++++++++ database/emojiquery.go | 44 +++++++++++++++++++ database/migrations/03-emoji.sql | 5 +++ database/migrations/migrations.go | 1 + 7 files changed, 232 insertions(+), 16 deletions(-) create mode 100644 bridge/emoji.go create mode 100644 database/emoji.go create mode 100644 database/emojiquery.go create mode 100644 database/migrations/03-emoji.sql diff --git a/bridge/emoji.go b/bridge/emoji.go new file mode 100644 index 0000000..fe83bc0 --- /dev/null +++ b/bridge/emoji.go @@ -0,0 +1,53 @@ +package bridge + +import ( + "io/ioutil" + "net/http" + + "github.com/bwmarrin/discordgo" + + "maunium.net/go/mautrix/appservice" + "maunium.net/go/mautrix/id" +) + +func (p *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 (p *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/bridge/portal.go b/bridge/portal.go index 4f60d67..e55e2ff 100644 --- a/bridge/portal.go +++ b/bridge/portal.go @@ -802,9 +802,23 @@ func (p *Portal) handleMatrixReaction(evt *event.Event) { discordID = msg.DiscordID } - err := user.Session.MessageReactionAdd(p.Key.ChannelID, discordID, reaction.RelatesTo.Key) + // Figure out if this is a custom emoji or not. + emojiID := reaction.RelatesTo.Key + if strings.HasPrefix(emojiID, "mxc://") { + uri, _ := id.ParseContentURI(emojiID) + emoji := p.bridge.db.Emoji.GetByMatrixURL(uri) + if emoji == nil { + p.log.Errorfln("failed to find emoji for %s", emojiID) + + return + } + + emojiID = emoji.APIName() + } + + err := user.Session.MessageReactionAdd(p.Key.ChannelID, discordID, emojiID) if err != nil { - p.log.Debugf("Failed to send reaction %s@%s: %v", p.Key, discordID, err) + p.log.Debugf("Failed to send reaction %s id:%s: %v", p.Key, discordID, err) return } @@ -816,7 +830,7 @@ func (p *Portal) handleMatrixReaction(evt *event.Event) { dbReaction.DiscordMessageID = discordID dbReaction.AuthorID = user.ID dbReaction.MatrixName = reaction.RelatesTo.Key - dbReaction.DiscordID = reaction.RelatesTo.Key + dbReaction.DiscordID = emojiID dbReaction.Insert() } @@ -825,16 +839,41 @@ func (p *Portal) handleDiscordReaction(user *User, reaction *discordgo.MessageRe return } - // This is temporary until we add support for custom emoji. + intent := p.bridge.GetPuppetByID(reaction.UserID).IntentFor(p) + + var discordID string + var matrixID string + if reaction.Emoji.ID != "" { - p.log.Debugln("ignoring non-unicode reaction") + dbEmoji := p.bridge.db.Emoji.GetByDiscordID(reaction.Emoji.ID) - return - } + if dbEmoji == nil { + data, mimeType, err := p.downloadDiscordEmoji(reaction.Emoji.ID, reaction.Emoji.Animated) + if err != nil { + p.log.Warnfln("Failed to download emoji %s from discord: %v", reaction.Emoji.ID, err) - emoteID := reaction.Emoji.ID - if reaction.Emoji.Name != "" { - emoteID = reaction.Emoji.Name + return + } + + uri, err := p.uploadMatrixEmoji(intent, data, mimeType) + if err != nil { + p.log.Warnfln("Failed to upload discord emoji %s to homeserver: %v", reaction.Emoji.ID, err) + + return + } + + dbEmoji = p.bridge.db.Emoji.New() + dbEmoji.DiscordID = reaction.Emoji.ID + dbEmoji.DiscordName = reaction.Emoji.Name + dbEmoji.MatrixURL = uri + dbEmoji.Insert() + } + + discordID = dbEmoji.DiscordID + matrixID = dbEmoji.MatrixURL.String() + } else { + discordID = reaction.Emoji.Name + matrixID = reaction.Emoji.Name } // Find the message that we're working with. @@ -845,10 +884,8 @@ func (p *Portal) handleDiscordReaction(user *User, reaction *discordgo.MessageRe return } - intent := p.bridge.GetPuppetByID(reaction.UserID).IntentFor(p) - // Lookup an existing reaction - existing := p.bridge.db.Reaction.GetByDiscordID(p.Key, message.DiscordID, emoteID) + existing := p.bridge.db.Reaction.GetByDiscordID(p.Key, message.DiscordID, discordID) if !add { if existing == nil { @@ -871,7 +908,7 @@ func (p *Portal) handleDiscordReaction(user *User, reaction *discordgo.MessageRe RelatesTo: event.RelatesTo{ EventID: message.MatrixID, Type: event.RelAnnotation, - Key: reaction.Emoji.Name, + Key: matrixID, }, }} @@ -889,8 +926,8 @@ func (p *Portal) handleDiscordReaction(user *User, reaction *discordgo.MessageRe dbReaction.MatrixEventID = resp.EventID dbReaction.AuthorID = reaction.UserID - dbReaction.MatrixName = reaction.Emoji.Name - dbReaction.DiscordID = emoteID + dbReaction.MatrixName = matrixID + dbReaction.DiscordID = discordID dbReaction.Insert() } diff --git a/database/database.go b/database/database.go index 62a9bda..1edd6c2 100644 --- a/database/database.go +++ b/database/database.go @@ -22,6 +22,7 @@ type Database struct { Message *MessageQuery Reaction *ReactionQuery Attachment *AttachmentQuery + Emoji *EmojiQuery } func New(dbType, uri string, maxOpenConns, maxIdleConns int, baseLog log.Logger) (*Database, error) { @@ -79,5 +80,10 @@ func New(dbType, uri string, maxOpenConns, maxIdleConns int, baseLog log.Logger) log: db.log.Sub("Attachment"), } + db.Emoji = &EmojiQuery{ + db: db, + log: db.log.Sub("Emoji"), + } + return db, nil } diff --git a/database/emoji.go b/database/emoji.go new file mode 100644 index 0000000..9474edb --- /dev/null +++ b/database/emoji.go @@ -0,0 +1,70 @@ +package database + +import ( + "database/sql" + "errors" + + log "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix/id" +) + +type Emoji struct { + db *Database + log log.Logger + + DiscordID string + DiscordName string + + MatrixURL id.ContentURI +} + +func (e *Emoji) Scan(row 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) + } + + 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) + } +} + +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) + } +} + +func (e *Emoji) APIName() string { + if e.DiscordID != "" && e.DiscordName != "" { + return e.DiscordName + ":" + e.DiscordID + } + + if e.DiscordName != "" { + return e.DiscordName + } + + return e.DiscordID +} diff --git a/database/emojiquery.go b/database/emojiquery.go new file mode 100644 index 0000000..98bf6b2 --- /dev/null +++ b/database/emojiquery.go @@ -0,0 +1,44 @@ +package database + +import ( + log "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix/id" +) + +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 { + row := eq.db.QueryRow(query, args...) + if row == nil { + return nil + } + + return eq.New().Scan(row) +} diff --git a/database/migrations/03-emoji.sql b/database/migrations/03-emoji.sql new file mode 100644 index 0000000..b0f4283 --- /dev/null +++ b/database/migrations/03-emoji.sql @@ -0,0 +1,5 @@ +CREATE TABLE emoji ( + discord_id TEXT NOT NULL PRIMARY KEY, + discord_name TEXT, + matrix_url TEXT +); diff --git a/database/migrations/migrations.go b/database/migrations/migrations.go index c2147c7..a0d14dd 100644 --- a/database/migrations/migrations.go +++ b/database/migrations/migrations.go @@ -41,6 +41,7 @@ func Run(db *sql.DB, baseLog log.Logger) error { migrator.Migrations( migrationFromFile("01-initial.sql"), migrationFromFile("02-attachments.sql"), + migrationFromFile("03-emoji.sql"), ), ) if err != nil {