Browse Source

Add support for stickers from Discord

Tulir Asokan 3 years ago
parent
commit
752063f292
5 changed files with 133 additions and 66 deletions
  1. 9 6
      ROADMAP.md
  2. 29 17
      attachments.go
  3. 2 2
      go.mod
  4. 4 4
      go.sum
  5. 89 37
      portal.go

+ 9 - 6
ROADMAP.md

@@ -2,10 +2,10 @@
 * Matrix → Discord
   * [x] Message content
     * [x] Plain text
-    * [ ] Formatted messages
+    * [x] Formatted messages
     * [x] Media/files
     * [x] Replies
-    * [ ] Threads
+    * [x] Threads
   * [x] Message redactions
   * [x] Reactions
     * [x] Unicode emojis
@@ -26,14 +26,17 @@
 * Discord → Matrix
   * [ ] Message content
     * [x] Plain text
-    * [ ] Formatted messages
+    * [x] Formatted messages
     * [x] Media/files
     * [x] Replies
-    * [ ] Threads
+    * [x] Threads
+      * [ ] Auto-joining threads
+      * [ ] Backfilling threads after joining
+    * [x] Custom emojis
   * [x] Message deletions
-  * [ ] Reactions
+  * [x] Reactions
     * [x] Unicode emojis
-    * [ ] Custom emojis
+    * [x] Custom emojis (not yet supported on Matrix)
   * [x] Avatars
   * [ ] Presence
   * [ ] Typing notifications

+ 29 - 17
attachments.go

@@ -2,11 +2,14 @@ package main
 
 import (
 	"bytes"
+	"fmt"
 	"image"
 	"io"
 	"net/http"
 	"strings"
 
+	"maunium.net/go/mautrix/crypto/attachment"
+
 	"github.com/bwmarrin/discordgo"
 
 	"maunium.net/go/mautrix"
@@ -16,15 +19,6 @@ import (
 )
 
 func (portal *Portal) downloadDiscordAttachment(url string) ([]byte, error) {
-	// We might want to make this save to disk in the future. Discord defaults
-	// to 8mb for all attachments to a messages for non-nitro users and
-	// non-boosted servers.
-	//
-	// If the user has nitro classic, their limit goes up to 50mb but if a user
-	// has regular nitro the limit is increased to 100mb.
-	//
-	// Servers boosted to level 2 will have the limit bumped to 50mb.
-
 	req, err := http.NewRequest(http.MethodGet, url, nil)
 	if err != nil {
 		return nil, err
@@ -38,6 +32,10 @@ func (portal *Portal) downloadDiscordAttachment(url string) ([]byte, error) {
 		return nil, err
 	}
 	defer resp.Body.Close()
+	if resp.StatusCode > 300 {
+		data, _ := io.ReadAll(resp.Body)
+		return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, data)
+	}
 	return io.ReadAll(resp.Body)
 }
 
@@ -71,9 +69,23 @@ func (portal *Portal) downloadMatrixAttachment(content *event.MessageEventConten
 }
 
 func (portal *Portal) uploadMatrixAttachment(intent *appservice.IntentAPI, data []byte, content *event.MessageEventContent) error {
+	content.Info.Size = len(data)
+	if content.Info.Width == 0 && content.Info.Height == 0 && strings.HasPrefix(content.Info.MimeType, "image/") {
+		cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
+		content.Info.Width = cfg.Width
+		content.Info.Height = cfg.Height
+	}
+
+	uploadMime := content.Info.MimeType
+	var file *attachment.EncryptedFile
+	if portal.Encrypted {
+		file = attachment.NewEncryptedFile()
+		file.EncryptInPlace(data)
+		uploadMime = "application/octet-stream"
+	}
 	req := mautrix.ReqUploadMedia{
 		ContentBytes: data,
-		ContentType:  content.Info.MimeType,
+		ContentType:  uploadMime,
 	}
 	var mxc id.ContentURI
 	if portal.bridge.Config.Homeserver.AsyncMedia {
@@ -90,13 +102,13 @@ func (portal *Portal) uploadMatrixAttachment(intent *appservice.IntentAPI, data
 		mxc = uploaded.ContentURI
 	}
 
-	content.URL = mxc.CUString()
-	content.Info.Size = len(data)
-
-	if content.Info.Width == 0 && content.Info.Height == 0 && strings.HasPrefix(content.Info.MimeType, "image/") {
-		cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
-		content.Info.Width = cfg.Width
-		content.Info.Height = cfg.Height
+	if file != nil {
+		content.File = &event.EncryptedFileInfo{
+			EncryptedFile: *file,
+			URL:           mxc.CUString(),
+		}
+	} else {
+		content.URL = mxc.CUString()
 	}
 
 	return nil

+ 2 - 2
go.mod

@@ -11,7 +11,7 @@ require (
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/yuin/goldmark v1.4.12
 	maunium.net/go/maulogger/v2 v2.3.2
-	maunium.net/go/mautrix v0.11.1-0.20220630174618-e98784f2fe26
+	maunium.net/go/mautrix v0.11.1-0.20220701202406-0b319c6d555a
 )
 
 require (
@@ -26,4 +26,4 @@ require (
 	maunium.net/go/mauflag v1.0.0 // indirect
 )
 
-replace github.com/bwmarrin/discordgo => gitlab.com/beeper/discordgo v0.23.3-0.20220528212118-5e6370d356e6
+replace github.com/bwmarrin/discordgo => gitlab.com/beeper/discordgo v0.23.3-0.20220703095519-7b2c44e4bc2f

+ 4 - 4
go.sum

@@ -30,8 +30,8 @@ github.com/tidwall/sjson v1.2.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc=
 github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM=
 github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0=
 github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-gitlab.com/beeper/discordgo v0.23.3-0.20220528212118-5e6370d356e6 h1:JegmFzU6WlZ0vW28fBFkKaZbMgVE/laetJlQJO3wQsk=
-gitlab.com/beeper/discordgo v0.23.3-0.20220528212118-5e6370d356e6/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
+gitlab.com/beeper/discordgo v0.23.3-0.20220703095519-7b2c44e4bc2f h1:Ag8rA+k9IRnEYxd0z671a7auMKoQ7DGw5FMtLpykFsA=
+gitlab.com/beeper/discordgo v0.23.3-0.20220703095519-7b2c44e4bc2f/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@@ -59,5 +59,5 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
 maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
 maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
 maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
-maunium.net/go/mautrix v0.11.1-0.20220630174618-e98784f2fe26 h1:wkfsp2ozyAQ9Vr9oAXbS9caWLhIffQ/Lxa04t7iUY54=
-maunium.net/go/mautrix v0.11.1-0.20220630174618-e98784f2fe26/go.mod h1:Lj4pBam5P0zIvieIFHnGsuaj+xfFtI3y/sC8yGlyna8=
+maunium.net/go/mautrix v0.11.1-0.20220701202406-0b319c6d555a h1:3UzcmHoqhxYlXiP6DXdJuc/1ESCPn7rFl9OiAZlR0Aw=
+maunium.net/go/mautrix v0.11.1-0.20220701202406-0b319c6d555a/go.mod h1:Lj4pBam5P0zIvieIFHnGsuaj+xfFtI3y/sC8yGlyna8=

+ 89 - 37
portal.go

@@ -494,31 +494,97 @@ func (portal *Portal) sendMediaFailedMessage(intent *appservice.IntentAPI, bridg
 	}
 }
 
-func (portal *Portal) handleDiscordAttachment(intent *appservice.IntentAPI, msgID string, attachment *discordgo.MessageAttachment, ts time.Time, threadRelation *event.RelatesTo, threadID string) *database.MessagePart {
+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 {
+	data, err := portal.downloadDiscordAttachment(url)
+	if err != nil {
+		portal.sendMediaFailedMessage(intent, err)
+		return nil
+	}
+
+	err = portal.uploadMatrixAttachment(intent, data, content)
+	if err != nil {
+		portal.sendMediaFailedMessage(intent, err)
+		return nil
+	}
+
+	evtType := event.EventMessage
+	if typeName == "sticker" && (content.Info.Width > DiscordStickerSize || content.Info.Height > DiscordStickerSize) {
+		if content.Info.Width > content.Info.Height {
+			content.Info.Height /= content.Info.Width / DiscordStickerSize
+			content.Info.Width = DiscordStickerSize
+		} else if content.Info.Width < content.Info.Height {
+			content.Info.Width /= content.Info.Height / DiscordStickerSize
+			content.Info.Height = DiscordStickerSize
+		} else {
+			content.Info.Width = DiscordStickerSize
+			content.Info.Height = DiscordStickerSize
+		}
+		evtType = event.EventSticker
+	}
+
+	resp, err := portal.sendMatrixMessage(intent, evtType, content, nil, ts.UnixMilli())
+	if err != nil {
+		portal.log.Warnfln("Failed to send %s to Matrix: %v", typeName, err)
+		return nil
+	}
+	// Update the fallback reply event for the next attachment
+	if threadRelation != nil {
+		threadRelation.InReplyTo.EventID = resp.EventID
+	}
+	return &database.MessagePart{
+		AttachmentID: id,
+		MXID:         resp.EventID,
+	}
+}
+
+func (portal *Portal) handleDiscordSticker(intent *appservice.IntentAPI, sticker *discordgo.Sticker, ts time.Time, threadRelation *event.RelatesTo) *database.MessagePart {
+	var mime string
+	switch sticker.FormatType {
+	case discordgo.StickerFormatTypePNG:
+		mime = "image/png"
+	case discordgo.StickerFormatTypeAPNG:
+		mime = "image/apng"
+	case discordgo.StickerFormatTypeLottie:
+		//mime = "application/json"
+		return nil
+	}
+	content := &event.MessageEventContent{
+		Body: sticker.Name, // TODO find description from somewhere?
+		Info: &event.FileInfo{
+			MimeType: mime,
+		},
+		RelatesTo: threadRelation,
+	}
+	return portal.handleDiscordFile("sticker", intent, sticker.ID, sticker.URL(), content, ts, threadRelation)
+}
+
+func (portal *Portal) handleDiscordAttachment(intent *appservice.IntentAPI, att *discordgo.MessageAttachment, ts time.Time, threadRelation *event.RelatesTo) *database.MessagePart {
 	// var captionContent *event.MessageEventContent
 
-	// if attachment.Description != "" {
+	// if att.Description != "" {
 	// 	captionContent = &event.MessageEventContent{
-	// 		Body:    attachment.Description,
+	// 		Body:    att.Description,
 	// 		MsgType: event.MsgNotice,
 	// 	}
 	// }
 	// portal.Log.Debugfln("captionContent: %#v", captionContent)
 
 	content := &event.MessageEventContent{
-		Body: attachment.Filename,
+		Body: att.Filename,
 		Info: &event.FileInfo{
-			Height:   attachment.Height,
-			MimeType: attachment.ContentType,
-			Width:    attachment.Width,
+			Height:   att.Height,
+			MimeType: att.ContentType,
+			Width:    att.Width,
 
 			// This gets overwritten later after the file is uploaded to the homeserver
-			Size: attachment.Size,
+			Size: att.Size,
 		},
 		RelatesTo: threadRelation,
 	}
 
-	switch strings.ToLower(strings.Split(attachment.ContentType, "/")[0]) {
+	switch strings.ToLower(strings.Split(att.ContentType, "/")[0]) {
 	case "audio":
 		content.MsgType = event.MsgAudio
 	case "image":
@@ -528,31 +594,7 @@ func (portal *Portal) handleDiscordAttachment(intent *appservice.IntentAPI, msgI
 	default:
 		content.MsgType = event.MsgFile
 	}
-
-	data, err := portal.downloadDiscordAttachment(attachment.URL)
-	if err != nil {
-		portal.sendMediaFailedMessage(intent, err)
-		return nil
-	}
-
-	err = portal.uploadMatrixAttachment(intent, data, content)
-	if err != nil {
-		portal.sendMediaFailedMessage(intent, err)
-		return nil
-	}
-
-	resp, err := portal.sendMatrixMessage(intent, event.EventMessage, content, nil, ts.UnixMilli())
-	if err != nil {
-		portal.log.Warnfln("failed to send media message to matrix: %v", err)
-	}
-	// Update the fallback reply event for the next attachment
-	if threadRelation != nil {
-		threadRelation.InReplyTo.EventID = resp.EventID
-	}
-	return &database.MessagePart{
-		AttachmentID: attachment.ID,
-		MXID:         resp.EventID,
-	}
+	return portal.handleDiscordFile("attachment", intent, att.ID, att.URL, content, ts, threadRelation)
 }
 
 func (portal *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message, thread *Thread) {
@@ -638,9 +680,14 @@ func (portal *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Mess
 		}
 		go portal.sendDeliveryReceipt(resp.EventID)
 	}
-
-	for _, attachment := range msg.Attachments {
-		part := portal.handleDiscordAttachment(intent, msg.ID, attachment, ts, threadRelation, threadID)
+	for _, att := range msg.Attachments {
+		part := portal.handleDiscordAttachment(intent, att, ts, threadRelation)
+		if part != nil {
+			parts = append(parts, *part)
+		}
+	}
+	for _, sticker := range msg.StickerItems {
+		part := portal.handleDiscordSticker(intent, sticker, ts, threadRelation)
 		if part != nil {
 			parts = append(parts, *part)
 		}
@@ -706,6 +753,11 @@ func (portal *Portal) handleDiscordMessageUpdate(user *User, msg *discordgo.Mess
 			delete(attachmentMap, remainingAttachment.ID)
 		}
 	}
+	for _, remainingSticker := range msg.StickerItems {
+		if _, found := attachmentMap[remainingSticker.ID]; found {
+			delete(attachmentMap, remainingSticker.ID)
+		}
+	}
 	for _, deletedAttachment := range attachmentMap {
 		_, err := intent.RedactEvent(portal.MXID, deletedAttachment.MXID)
 		if err != nil {