Add support for stickers from Discord
This commit is contained in:
15
ROADMAP.md
15
ROADMAP.md
@@ -2,10 +2,10 @@
|
|||||||
* Matrix → Discord
|
* Matrix → Discord
|
||||||
* [x] Message content
|
* [x] Message content
|
||||||
* [x] Plain text
|
* [x] Plain text
|
||||||
* [ ] Formatted messages
|
* [x] Formatted messages
|
||||||
* [x] Media/files
|
* [x] Media/files
|
||||||
* [x] Replies
|
* [x] Replies
|
||||||
* [ ] Threads
|
* [x] Threads
|
||||||
* [x] Message redactions
|
* [x] Message redactions
|
||||||
* [x] Reactions
|
* [x] Reactions
|
||||||
* [x] Unicode emojis
|
* [x] Unicode emojis
|
||||||
@@ -26,14 +26,17 @@
|
|||||||
* Discord → Matrix
|
* Discord → Matrix
|
||||||
* [ ] Message content
|
* [ ] Message content
|
||||||
* [x] Plain text
|
* [x] Plain text
|
||||||
* [ ] Formatted messages
|
* [x] Formatted messages
|
||||||
* [x] Media/files
|
* [x] Media/files
|
||||||
* [x] Replies
|
* [x] Replies
|
||||||
* [ ] Threads
|
* [x] Threads
|
||||||
|
* [ ] Auto-joining threads
|
||||||
|
* [ ] Backfilling threads after joining
|
||||||
|
* [x] Custom emojis
|
||||||
* [x] Message deletions
|
* [x] Message deletions
|
||||||
* [ ] Reactions
|
* [x] Reactions
|
||||||
* [x] Unicode emojis
|
* [x] Unicode emojis
|
||||||
* [ ] Custom emojis
|
* [x] Custom emojis (not yet supported on Matrix)
|
||||||
* [x] Avatars
|
* [x] Avatars
|
||||||
* [ ] Presence
|
* [ ] Presence
|
||||||
* [ ] Typing notifications
|
* [ ] Typing notifications
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/crypto/attachment"
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
@@ -16,15 +19,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (portal *Portal) downloadDiscordAttachment(url string) ([]byte, error) {
|
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)
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -38,6 +32,10 @@ func (portal *Portal) downloadDiscordAttachment(url string) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
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)
|
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 {
|
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{
|
req := mautrix.ReqUploadMedia{
|
||||||
ContentBytes: data,
|
ContentBytes: data,
|
||||||
ContentType: content.Info.MimeType,
|
ContentType: uploadMime,
|
||||||
}
|
}
|
||||||
var mxc id.ContentURI
|
var mxc id.ContentURI
|
||||||
if portal.bridge.Config.Homeserver.AsyncMedia {
|
if portal.bridge.Config.Homeserver.AsyncMedia {
|
||||||
@@ -90,13 +102,13 @@ func (portal *Portal) uploadMatrixAttachment(intent *appservice.IntentAPI, data
|
|||||||
mxc = uploaded.ContentURI
|
mxc = uploaded.ContentURI
|
||||||
}
|
}
|
||||||
|
|
||||||
content.URL = mxc.CUString()
|
if file != nil {
|
||||||
content.Info.Size = len(data)
|
content.File = &event.EncryptedFileInfo{
|
||||||
|
EncryptedFile: *file,
|
||||||
if content.Info.Width == 0 && content.Info.Height == 0 && strings.HasPrefix(content.Info.MimeType, "image/") {
|
URL: mxc.CUString(),
|
||||||
cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
|
}
|
||||||
content.Info.Width = cfg.Width
|
} else {
|
||||||
content.Info.Height = cfg.Height
|
content.URL = mxc.CUString()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -11,7 +11,7 @@ require (
|
|||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/yuin/goldmark v1.4.12
|
github.com/yuin/goldmark v1.4.12
|
||||||
maunium.net/go/maulogger/v2 v2.3.2
|
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 (
|
require (
|
||||||
@@ -26,4 +26,4 @@ require (
|
|||||||
maunium.net/go/mauflag v1.0.0 // indirect
|
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
|
||||||
|
|||||||
8
go.sum
8
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/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 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0=
|
||||||
github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
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.20220703095519-7b2c44e4bc2f h1:Ag8rA+k9IRnEYxd0z671a7auMKoQ7DGw5FMtLpykFsA=
|
||||||
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/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-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 h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
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/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 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
|
||||||
maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
|
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.20220701202406-0b319c6d555a h1:3UzcmHoqhxYlXiP6DXdJuc/1ESCPn7rFl9OiAZlR0Aw=
|
||||||
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/go.mod h1:Lj4pBam5P0zIvieIFHnGsuaj+xfFtI3y/sC8yGlyna8=
|
||||||
|
|||||||
134
portal.go
134
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 {
|
const DiscordStickerSize = 160
|
||||||
// var captionContent *event.MessageEventContent
|
|
||||||
|
|
||||||
// if attachment.Description != "" {
|
func (portal *Portal) handleDiscordFile(typeName string, intent *appservice.IntentAPI, id, url string, content *event.MessageEventContent, ts time.Time, threadRelation *event.RelatesTo) *database.MessagePart {
|
||||||
// captionContent = &event.MessageEventContent{
|
data, err := portal.downloadDiscordAttachment(url)
|
||||||
// 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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
portal.sendMediaFailedMessage(intent, err)
|
portal.sendMediaFailedMessage(intent, err)
|
||||||
return nil
|
return nil
|
||||||
@@ -541,20 +509,94 @@ func (portal *Portal) handleDiscordAttachment(intent *appservice.IntentAPI, msgI
|
|||||||
return nil
|
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 {
|
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
|
// Update the fallback reply event for the next attachment
|
||||||
if threadRelation != nil {
|
if threadRelation != nil {
|
||||||
threadRelation.InReplyTo.EventID = resp.EventID
|
threadRelation.InReplyTo.EventID = resp.EventID
|
||||||
}
|
}
|
||||||
return &database.MessagePart{
|
return &database.MessagePart{
|
||||||
AttachmentID: attachment.ID,
|
AttachmentID: id,
|
||||||
MXID: resp.EventID,
|
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) {
|
func (portal *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message, thread *Thread) {
|
||||||
if portal.MXID == "" {
|
if portal.MXID == "" {
|
||||||
portal.log.Warnln("handle message called without a valid portal")
|
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)
|
go portal.sendDeliveryReceipt(resp.EventID)
|
||||||
}
|
}
|
||||||
|
for _, att := range msg.Attachments {
|
||||||
for _, attachment := range msg.Attachments {
|
part := portal.handleDiscordAttachment(intent, att, ts, threadRelation)
|
||||||
part := portal.handleDiscordAttachment(intent, msg.ID, attachment, ts, threadRelation, threadID)
|
if part != nil {
|
||||||
|
parts = append(parts, *part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, sticker := range msg.StickerItems {
|
||||||
|
part := portal.handleDiscordSticker(intent, sticker, ts, threadRelation)
|
||||||
if part != nil {
|
if part != nil {
|
||||||
parts = append(parts, *part)
|
parts = append(parts, *part)
|
||||||
}
|
}
|
||||||
@@ -706,6 +753,11 @@ func (portal *Portal) handleDiscordMessageUpdate(user *User, msg *discordgo.Mess
|
|||||||
delete(attachmentMap, remainingAttachment.ID)
|
delete(attachmentMap, remainingAttachment.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, remainingSticker := range msg.StickerItems {
|
||||||
|
if _, found := attachmentMap[remainingSticker.ID]; found {
|
||||||
|
delete(attachmentMap, remainingSticker.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, deletedAttachment := range attachmentMap {
|
for _, deletedAttachment := range attachmentMap {
|
||||||
_, err := intent.RedactEvent(portal.MXID, deletedAttachment.MXID)
|
_, err := intent.RedactEvent(portal.MXID, deletedAttachment.MXID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user