Преглед на файлове

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
Gary Kramlich преди 3 години
родител
ревизия
a51b1074ba
променени са 7 файла, в които са добавени 232 реда и са изтрити 16 реда
  1. 53 0
      bridge/emoji.go
  2. 53 16
      bridge/portal.go
  3. 6 0
      database/database.go
  4. 70 0
      database/emoji.go
  5. 44 0
      database/emojiquery.go
  6. 5 0
      database/migrations/03-emoji.sql
  7. 1 0
      database/migrations/migrations.go

+ 53 - 0
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
+}

+ 53 - 16
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()
 	}

+ 6 - 0
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
 }

+ 70 - 0
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
+}

+ 44 - 0
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)
+}

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

+ 1 - 0
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 {