From 752063f2926240ede46159d9d01d2b6964da3360 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 3 Jul 2022 12:58:57 +0300 Subject: [PATCH] Add support for stickers from Discord --- ROADMAP.md | 15 +++--- attachments.go | 46 ++++++++++------- go.mod | 4 +- go.sum | 8 +-- portal.go | 134 ++++++++++++++++++++++++++++++++++--------------- 5 files changed, 137 insertions(+), 70 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 5224d23..c734d27 100644 --- a/ROADMAP.md +++ b/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 diff --git a/attachments.go b/attachments.go index 157cbf9..e115a44 100644 --- a/attachments.go +++ b/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 diff --git a/go.mod b/go.mod index 5d407e7..252e341 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index a63b502..8873300 100644 --- a/go.sum +++ b/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= diff --git a/portal.go b/portal.go index e50f5fd..2003c09 100644 --- a/portal.go +++ b/portal.go @@ -494,42 +494,10 @@ 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 { - // var captionContent *event.MessageEventContent +const DiscordStickerSize = 160 - // if attachment.Description != "" { - // captionContent = &event.MessageEventContent{ - // Body: attachment.Description, - // MsgType: event.MsgNotice, - // } - // } - // portal.Log.Debugfln("captionContent: %#v", captionContent) - - content := &event.MessageEventContent{ - Body: attachment.Filename, - Info: &event.FileInfo{ - Height: attachment.Height, - MimeType: attachment.ContentType, - Width: attachment.Width, - - // This gets overwritten later after the file is uploaded to the homeserver - Size: attachment.Size, - }, - RelatesTo: threadRelation, - } - - switch strings.ToLower(strings.Split(attachment.ContentType, "/")[0]) { - case "audio": - content.MsgType = event.MsgAudio - case "image": - content.MsgType = event.MsgImage - case "video": - content.MsgType = event.MsgVideo - default: - content.MsgType = event.MsgFile - } - - data, err := portal.downloadDiscordAttachment(attachment.URL) +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 @@ -541,20 +509,94 @@ func (portal *Portal) handleDiscordAttachment(intent *appservice.IntentAPI, msgI return nil } - resp, err := portal.sendMatrixMessage(intent, event.EventMessage, content, nil, ts.UnixMilli()) + 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 media message to matrix: %v", err) + 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: attachment.ID, + 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 att.Description != "" { + // captionContent = &event.MessageEventContent{ + // Body: att.Description, + // MsgType: event.MsgNotice, + // } + // } + // portal.Log.Debugfln("captionContent: %#v", captionContent) + + content := &event.MessageEventContent{ + Body: att.Filename, + Info: &event.FileInfo{ + Height: att.Height, + MimeType: att.ContentType, + Width: att.Width, + + // This gets overwritten later after the file is uploaded to the homeserver + Size: att.Size, + }, + RelatesTo: threadRelation, + } + + switch strings.ToLower(strings.Split(att.ContentType, "/")[0]) { + case "audio": + content.MsgType = event.MsgAudio + case "image": + content.MsgType = event.MsgImage + case "video": + content.MsgType = event.MsgVideo + default: + content.MsgType = event.MsgFile + } + return portal.handleDiscordFile("attachment", intent, att.ID, att.URL, content, ts, threadRelation) +} + func (portal *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message, thread *Thread) { if portal.MXID == "" { portal.log.Warnln("handle message called without a valid portal") @@ -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 {